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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! 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 Rc;
use GroupPlan;
/// 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!
/// The plan for one binder node, computed ON DEMAND and memoized.
///
/// ★ Replaces a parse-door walk that planned every binder in every parsed
/// file. Laziness means most of those are never evaluated, so that work was
/// mostly discarded — measured as a ~4% wall-clock tax on a real nixpkgs eval.
/// Planning at first evaluation is identical in result (a group's plan depends
/// only on its own entries) and pays only for groups that are reached.
///
/// The memo is keyed exactly as before, so a plan computed for a node at
/// offset `o` in one parse tree is never read for a different (imported) tree
/// with a binder at the same offset.
///
/// A `None` return is a POSITIVE statement — the group has no duplicate static
/// key and no dotted path, so the caller's existing path is already correct.
/// See the module docs on why that is not a fallback.
/// A memo entry. `Memo(None)` records "this group needs no plan", which must be
/// remembered too — otherwise every evaluation of an ordinary attrset re-runs
/// `needs_plan`, which is the cost this change exists to remove.
;
/// Drop every recorded plan. Wired into the same lifecycle point as
/// `resolve_env::clear`.