sui-resolve 0.1.182

Parse-time variable-resolution side-table for the sui tree-walker — precomputes each ident's interned Symbol so the hot Ident arm skips the per-lookup re-intern (ENV-RESOLVE M0, fail-safe to Dynamic)
Documentation

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.