1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//! The tree-walker's *consume* side of the `sui-normalize` attrset-binding
//! plan.
//!
//! Mirrors [`crate::resolve_env`]'s three parts — a one-way env-flag latch, a
//! thread-local table keyed by `(source_id, text_offset)`, and
//! populate/lookup/clear hooks wired into `eval_with_file` — because the
//! keying hazard is identical: a plan recorded for a binder at offset `o` in
//! one parse tree must never be read for a different (imported) tree that
//! happens to have a binder at the same offset.
//!
//! # ★ The failure discipline here is the INVERSE of `resolve_env`'s
//!
//! `sui-resolve` fails SAFE to `Dynamic` because its fallback is
//! *equivalent* — `lookup_fast` probes the same map with the same symbol, so
//! falling back costs only speed.
//!
//! **This table's fallback path is the divergence itself.** Falling back means
//! "walk `set.entries()` yourself", which is precisely the code that produces
//! the silent wrong answers `sui-normalize` exists to remove. So a miss must
//! never be treated as "nothing to do" in a group that NEEDED a plan.
//!
//! The shape that makes that safe: `sui-normalize` records a group **only**
//! when it has a duplicate static key or a dotted path. A miss therefore means
//! "this group has neither", which is exactly when the existing path is
//! already correct. The absence is a *positive* statement, not a fallback —
//! and that is what bounds this change's blast radius to the groups that are
//! wrong today.
//!
//! # Default ON since 2026-08-18
//!
//! `SUI_NORMALIZE=0` opts OUT, restoring the pre-plan construction path. The
//! latch survives the flip on purpose — a divergence suspected to come from
//! this pass is then one command away from being confirmed or cleared, which
//! is worth more than the tidiness of deleting it.
use RefCell;
use OnceLock;
use ;
/// One-time read of `SUI_NORMALIZE`. Default ON; `SUI_NORMALIZE=0` opts out.
static ENABLED: = new;
/// Whether plan-driven attrset construction is enabled. **Default: yes.**
///
/// Flipped from opt-in to opt-out on 2026-08-18, on this evidence:
///
/// * every wrong-answer shape in the class matches nix, including the
/// acceptance case `{ a = rec { b = c+1; d = 2; }; a.c = d+3; }.a.b` -> 6,
/// which needs mutual recursion ACROSS the merge boundary;
/// * a fleet scan of 4562 `.nix` files found ZERO false rejects — the one
/// rejection is a file `nix-instantiate --parse` also refuses;
/// * `sui perf-seal` moved DOWN or held on all three attr-merge rows
/// (`dotted full-set leaf deep-merge` 6 -> 5), which is what confirms the
/// splice happens at PARSE time rather than adding eval work;
/// * the suites are green both ways.
///
/// The latch is KEPT, deliberately, in the `SUI_SCOPE_NARROW` spirit:
/// `SUI_NORMALIZE=0` restores the pre-plan construction path, so a divergence
/// suspected to come from this pass can be bisected in one command instead of
/// a revert. That is also why the old entry loops are not deleted yet.
thread_local!
/// Merge a freshly-computed [`NormalizeTable`] into the thread-local table.
/// No-op when the flag is off.
/// The plan for the binder node at `text_offset` in `source_id`, if one was
/// recorded. `None` means the group needs no normalization — see the module
/// docs on why that is a positive statement rather than a fallback.
/// Drop every recorded plan. Wired into the same lifecycle point as
/// `resolve_env::clear`.