Skip to main content

Crate sui_normalize

Crate sui_normalize 

Source
Expand description

Parse-time attrset-binding normalizer — CppNix’s ExprAttrs::addAttr.

§The defect this exists to remove

sui decides duplicate-key merge-vs-overwrite at EVAL time, by forcing the colliding value to WHNF and asking “is it an attrset?”. Real nix decides it at PARSE time, from SYNTAX. Because sui asks a different question at a different time, all three engines give SILENT WRONG ANSWERS on legal nix. Measured against nix 2.31.5 on 2026-08-18:

let a = {b=1;}; a = {c=2;}; in a       nix {b=1;c=2;}    sui {c=2;}
rec { o = {e=1;}; o.x = 2; }           nix {o={e=1;x=2;};} sui {o={x=2;};}
let b=1; in { a={x=2;}; a=rec{b=99;c=b;}; }
                                       nix c=1           sui c=99

Every row exits 0. No error anywhere.

§The rule, measured

Merge iff both sides are syntactic attrset literals ({…} / rec {…}, including the implicit ones a dotted path creates), recursively. Reject otherwise. Decided from SYNTAX, never from the runtime value — so let x = {b=1;}; in { s = x; s = {c=2;}; } is a parse error even though the value of x is an attrset.

§★ It is a destructive SPLICE, not predicate-plus-union

nix splices the second side’s bindings INTO THE FIRST-DECLARED NODE. The first node’s rec governs and a later rec is DISCARDED; the second side’s bindings are RE-SCOPED into the first node’s scope:

{ a = rec {b=1;}; a = {c = b+1;}; }.a.c   -> 2    non-rec side binds to the FIRST's rec scope
{ a = {b=1;}; a = rec {c=2; d=c;}; }      -> parse error: undefined variable 'c'
                                                 a standalone-valid rec block's INTERNAL
                                                 reference is destroyed by the merge
{ a = rec {b=1; c=b+1;}; a.d = 3; }       -> `{ a = rec { b=1; c=(b+1); d=3; }; }`
                                                 the DOTTED member lands INSIDE the rec

No value-level merge can express re-scoping. That is why this is a structural pass and not a fix inside any engine’s collection loop.

§Why a side-table rather than a rewritten tree

rnix’s CST is lossless and IMMUTABLE — the tree cannot be spliced. So this pass emits a NormalizeTable: a plan per binding-group node, which each engine consumes INSTEAD OF its own for entry in set.entries() loop. The shape is deliberately modelled on sui-resolve’s ResolveTable, including its most useful property:

An absent entry means “nothing to normalize” — no collision, no dotted path — so every engine’s existing fast path is untouched for the overwhelming majority of attrsets, and the table stays small. That is the parity-by-construction argument: this pass can only change the groups it records, and it records only the groups that today are wrong.

§Placement

This crate must be reachable by all three engines, so it sits at sui-resolve’s level and depends on sui-intern + rnix + rowan only. It must NOT depend on sui-eval: sui-bytecode carries sui-eval as a path-only dev-dep to break a publish cycle, and a real edge here would close it. Hence the error type is self-contained — no EvalError — and each engine maps NormalizeError into its own error on the way out.

§Status

STAGE 0: UNWIRED. Nothing consumes this yet, by design — the rule is built and proven against the oracle before it can change any engine’s behaviour. Wiring is stages 1–4.

Structs§

DynamicBinding
A dynamic-keyed binding, kept out of the static map.
GroupPlan
The normalized plan for one binding group — an attrset, a let, a rec, or a legacy let { … }.
NormalizeTable
The normalized plans for one parsed source tree.
StaticBinding
One static binding, with the source position of its FIRST definition.

Enums§

AttrKey
One component of an attribute path, after constant-folding.
Binding
A binding after normalization.
NormalizeError
A parse-time rejection. Carries what is needed to render nix’s message.

Functions§

as_attrset_literal
True iff expr is a syntactic attrset literal, seeing through PARENTHESES.
fold_attr
fold_attr_name
Constant-fold an attribute-path component, per the rule on AttrKey. The TEXT-level half of fold_attr: the static name an attribute folds to, or None if it is genuinely dynamic.
normalize
Normalize every binding group in a parsed tree.
plan_for_group
The plan for ONE binding group, computed on demand.
plan_for_group_total
The plan for a group, whether or not it needs one — the total sibling of plan_for_group.
show_attr_component
Render one path component the way CppNix’s showAttrPath does.
strip_parens
Strip (…) recursively. CppNix’s grammar discards parens outright, so anything that asks “what KIND of expression is this?” must strip them first or it will answer about the wrapper.