Parse-time variable resolution side-table (ENV-RESOLVE M0).
A single bind_vars pass over the rnix AST — walked against a
cppnix-shaped [StaticEnv] chain — precomputes, for each variable
reference (ast::Ident used as an expression), whether that name is
provably resolvable in an enclosing lexical binder with no with
scope between the reference and its binder. If so, the reference's
interned [sui_intern::Symbol] is recorded in a [ResolveTable] keyed
by the ident node's byte offset. At eval time, the tree-walker's hot
Ident arm consults the table: on a [Resolution::Lexical] hit it
probes the environment's lexical bindings with the precomputed Symbol
directly, skipping the per-lookup ident_text().to_string() +
intern() round-trip.
Parity by construction (M0)
This is a pure hash-key-caching optimization. The tree-walker's
Env::lookup_fast already probes the lexical im_rc bindings map FIRST
(by the same interned Symbol) before ever touching the with-chain. So
a Lexical fast path that only shortcuts on a lexical-bindings hit
returns exactly the value the unchanged path would have — the same
Env, the same Symbol, the same map probe, just pre-interned. On ANY
miss (a mid-fixpoint blackhole, an ident the table doesn't record, or
Resolution::Dynamic) the consumer falls back to today's exact runtime
path (intern + lookup_fast + the with-chain). A byte-identical
parity result is therefore a sufficient proof for M0.
Fail-safe to Dynamic (the load-bearing discipline)
bind_vars marks a reference [Resolution::Lexical] only when it
is provably resolvable in an enclosing let/rec/lambda-param/pattern
binder with no with scope between the reference and its binder. On
any uncertainty — a reference resolved while a with-barrier frame sits
between it and its nearest matching lexical binder, an unresolved name,
an rnix node shape it doesn't model, or a binder whose name it can't
statically read — it emits [Resolution::Dynamic] (or simply records
nothing, which the consumer reads back as Dynamic). Slower-but-correct
is always right; a mis-marked Lexical under a with would be the only
thing that could change scoping, and this pass structurally cannot emit
one.
M1+ (positional {up, slot} frames, VM sharing) is explicitly out of
scope here — see docs/ENV-RESOLVE-DESIGN.md.