Skip to main content

sui_eval/
normalize_env.rs

1//! The tree-walker's *consume* side of the `sui-normalize` attrset-binding
2//! plan.
3//!
4//! Mirrors [`crate::resolve_env`]'s three parts — a one-way env-flag latch, a
5//! thread-local table keyed by `(source_id, text_offset)`, and
6//! populate/lookup/clear hooks wired into `eval_with_file` — because the
7//! keying hazard is identical: a plan recorded for a binder at offset `o` in
8//! one parse tree must never be read for a different (imported) tree that
9//! happens to have a binder at the same offset.
10//!
11//! # ★ The failure discipline here is the INVERSE of `resolve_env`'s
12//!
13//! `sui-resolve` fails SAFE to `Dynamic` because its fallback is
14//! *equivalent* — `lookup_fast` probes the same map with the same symbol, so
15//! falling back costs only speed.
16//!
17//! **This table's fallback path is the divergence itself.** Falling back means
18//! "walk `set.entries()` yourself", which is precisely the code that produces
19//! the silent wrong answers `sui-normalize` exists to remove. So a miss must
20//! never be treated as "nothing to do" in a group that NEEDED a plan.
21//!
22//! The shape that makes that safe: `sui-normalize` records a group **only**
23//! when it has a duplicate static key or a dotted path. A miss therefore means
24//! "this group has neither", which is exactly when the existing path is
25//! already correct. The absence is a *positive* statement, not a fallback —
26//! and that is what bounds this change's blast radius to the groups that are
27//! wrong today.
28//!
29//! # Flag-gated on purpose
30//!
31//! `SUI_NORMALIZE=1` opts in. Off, every consume site takes today's exact
32//! unchanged path, so landing this cannot move a single byte for anyone until
33//! the flag is proven and the default flipped.
34
35use std::cell::RefCell;
36use std::sync::OnceLock;
37
38use sui_normalize::{GroupPlan, NormalizeTable};
39
40/// One-time read of `SUI_NORMALIZE`. `true` iff `SUI_NORMALIZE=1`.
41static ENABLED: OnceLock<bool> = OnceLock::new();
42
43/// Whether plan-driven attrset construction is enabled (`SUI_NORMALIZE=1`).
44///
45/// Read once and cached — matches `resolve_env::enabled()`'s one-way latch.
46#[must_use]
47pub fn enabled() -> bool {
48    *ENABLED.get_or_init(|| std::env::var("SUI_NORMALIZE").ok().as_deref() == Some("1"))
49}
50
51thread_local! {
52    /// `(source_id << 32) | text_offset` -> the binder's plan. Same keying as
53    /// `resolve_env::RESOLVE_TABLE` and `value::intern_cached`.
54    static PLAN_TABLE: RefCell<rustc_hash::FxHashMap<u64, GroupPlan>> =
55        RefCell::new(rustc_hash::FxHashMap::default());
56}
57
58#[inline]
59fn key(source_id: u32, text_offset: u32) -> u64 {
60    (u64::from(source_id) << 32) | u64::from(text_offset)
61}
62
63/// Merge a freshly-computed [`NormalizeTable`] into the thread-local table.
64/// No-op when the flag is off.
65pub fn populate(source_id: u32, table: &NormalizeTable) {
66    if !enabled() {
67        return;
68    }
69    PLAN_TABLE.with(|t| {
70        let mut t = t.borrow_mut();
71        for (offset, plan) in table.iter() {
72            t.insert(key(source_id, offset), plan.clone());
73        }
74    });
75}
76
77/// The plan for the binder node at `text_offset` in `source_id`, if one was
78/// recorded. `None` means the group needs no normalization — see the module
79/// docs on why that is a positive statement rather than a fallback.
80#[must_use]
81pub fn plan_for(source_id: u32, text_offset: u32) -> Option<GroupPlan> {
82    if !enabled() {
83        return None;
84    }
85    PLAN_TABLE.with(|t| t.borrow().get(&key(source_id, text_offset)).cloned())
86}
87
88/// Drop every recorded plan. Wired into the same lifecycle point as
89/// `resolve_env::clear`.
90pub fn clear() {
91    PLAN_TABLE.with(|t| t.borrow_mut().clear());
92}