sui_eval/resolve_env.rs
1//! ENV-RESOLVE M0 — the tree-walker's *consume* side of the `sui-resolve`
2//! parse-time variable-resolution side-table.
3//!
4//! This module owns three things:
5//!
6//! 1. The `SUI_RESOLVE=1` env flag (read once via a `OnceLock`, default
7//! off) — mirrors `perf::enabled()`'s one-way latch.
8//! 2. A thread-local resolution table keyed by `(source_id, text_offset)` —
9//! the *identical* key shape `value::intern_cached` uses for the ident
10//! symbol cache, so a resolution recorded during `bind_vars` on one
11//! parse tree never collides with an ident at the same offset in a
12//! different (imported) parse tree.
13//! 3. The hot-path lookup `resolution_for(offset)` that the eval `Ident`
14//! arm consults, plus `populate`/`clear` lifecycle hooks wired into
15//! `eval_with_file`.
16//!
17//! # Parity by construction
18//!
19//! Enabling this flag only changes the tree-walker's *hot Ident arm*: on a
20//! `Resolution::Lexical{sym}` it probes the environment's lexical bindings
21//! with the precomputed Symbol and returns on a hit — the byte-identical
22//! value the unchanged `lookup_fast` returns (which probes the same lexical
23//! map, by the same Symbol, first). On ANY miss (blackhole / unrecorded /
24//! `Dynamic`) it falls back to today's exact runtime path. See
25//! `sui-resolve`'s crate docs.
26
27use std::cell::RefCell;
28use std::sync::OnceLock;
29
30use sui_resolve::{Resolution, ResolveTable};
31
32/// One-time read of `SUI_RESOLVE`. `true` iff `SUI_RESOLVE=1`.
33static ENABLED: OnceLock<bool> = OnceLock::new();
34
35/// Whether the ENV-RESOLVE M0 fast path is enabled (`SUI_RESOLVE=1`).
36///
37/// Read once and cached — matches `perf::enabled()`'s one-way latch. When
38/// `false`, every consume site takes today's exact unchanged runtime path.
39#[must_use]
40pub fn enabled() -> bool {
41 *ENABLED.get_or_init(|| std::env::var("SUI_RESOLVE").ok().as_deref() == Some("1"))
42}
43
44thread_local! {
45 /// `(source_id << 32) | text_offset` -> resolution. Mirrors the
46 /// `IDENT_CACHE` keying in `value.rs`: a resolution recorded for an
47 /// ident at offset `o` in the parse tree with source id `s` is stored
48 /// under `(s << 32) | o`. Only `Lexical` entries live here; an absent
49 /// key reads back as `Dynamic` (the fail-safe fallback).
50 static RESOLVE_TABLE: RefCell<rustc_hash::FxHashMap<u64, Resolution>> =
51 RefCell::new(rustc_hash::FxHashMap::default());
52}
53
54#[inline]
55fn key(source_id: u32, text_offset: u32) -> u64 {
56 (u64::from(source_id) << 32) | u64::from(text_offset)
57}
58
59/// Merge a freshly-computed [`ResolveTable`] (for the parse tree tagged
60/// `source_id`) into the thread-local table. No-op when the flag is off.
61///
62/// Called once per `eval_with_file` parse, right after `next_source_id()`
63/// assigns the tree its id — so imports (which re-enter `eval_with_file`
64/// with their own id) contribute their own resolutions without clobbering
65/// the outer file's.
66pub fn populate(source_id: u32, table: &ResolveTable) {
67 if !enabled() {
68 return;
69 }
70 RESOLVE_TABLE.with(|c| {
71 let mut map = c.borrow_mut();
72 for (offset, res) in table.entries() {
73 map.insert(key(source_id, offset), res);
74 }
75 });
76}
77
78/// Resolution recorded for the ident at `(source_id, text_offset)`.
79/// Returns [`Resolution::Dynamic`] for any unrecorded key — the fail-safe
80/// path (the caller then takes today's exact runtime lookup).
81#[must_use]
82pub fn resolution_for(source_id: u32, text_offset: u32) -> Resolution {
83 RESOLVE_TABLE.with(|c| {
84 c.borrow()
85 .get(&key(source_id, text_offset))
86 .copied()
87 .unwrap_or(Resolution::Dynamic)
88 })
89}
90
91/// Clear the thread-local resolution table. Called at the top-level
92/// (`nesting == 0`) re-entry of `eval_with_file`, alongside
93/// `clear_ident_cache()`, so offsets from previous top-level evals don't
94/// persist. No-op when the flag is off.
95pub fn clear() {
96 if !enabled() {
97 return;
98 }
99 RESOLVE_TABLE.with(|c| c.borrow_mut().clear());
100}