Skip to main content

rustyfi_lang/
value.rs

1//! Runtime values (a subset of `syntactic_value`).
2
3use crate::compile::CompiledExpr;
4use crate::primitives::PrimDef;
5use crate::quoted::{BText, IText, MathElem};
6use rustyfi_backend::{
7    AnnotAction, Color, Context, DecoId, DocExtras, FrameDecoration, HorzBox, HyphenLang, ImageId,
8    ImageResource, Length, MathCharClass, MathKind, Page, PageGeometry, VertBox,
9};
10use std::cell::RefCell;
11use std::collections::{BTreeMap, HashMap};
12use std::rc::Rc;
13
14// `Value::CompiledClosure` carries a crate-internal `CompiledExpr` body.
15// External code can obtain such a value but cannot name, construct, or
16// inspect its body, which is the intent — so `private_interfaces` is
17// deliberately allowed for that one field.
18#[allow(private_interfaces)]
19#[derive(Clone, Debug)]
20pub enum Value {
21    Unit,
22    Bool(bool),
23    Int(i64),
24    Float(f64),
25    Length(Length),
26    Str(String),
27    List(Vec<Value>),
28    Tuple(Vec<Value>),
29    /// A variant constructor value, optionally carrying a payload
30    /// (`None` / `Some 3`).
31    Ctor(String, Option<Box<Value>>),
32    Record(BTreeMap<String, Value>),
33    Context(Box<Context>),
34    /// Quoted inline text with its captured environment
35    /// (`InputHorzWithEnvironment`). `elems` is the COMPILED element tree
36    /// ([`crate::quoted`]): command names and embedded expressions were
37    /// resolved at compile time, so nothing here is looked up by name at
38    /// layout time — the environment is still captured because a compiled
39    /// node resolves its *locals* against the environment it runs in.
40    InlineText {
41        elems: Rc<Vec<IText>>,
42        env: Env,
43    },
44    /// Quoted block text with its captured environment.
45    BlockText {
46        elems: Rc<Vec<BText>>,
47        env: Env,
48    },
49    /// Quoted math text with its captured environment (mirrors
50    /// `InlineText`/`BlockText`); typesetting is deferred, so this
51    /// is carried opaquely for now.
52    MathText {
53        elems: Rc<Vec<MathElem>>,
54        env: Env,
55    },
56    /// The faithful `math` value — what every `math-*` primitive
57    /// (`math-char`, `math-concat`, `math-sup`, …) builds and consumes, as
58    /// opposed to `MathText`'s elaborator-fused literal form. A `math`
59    /// value is always a *sequence* of atoms (mirroring upstream `MathValue
60    /// of math list`, `types.cppo.ml:888` — each `Math` here is one
61    /// already-classed atom, not a further list), so `math-concat` is a
62    /// plain `Vec` append and `math-group`/`math-sup`/… each wrap the whole
63    /// inner `Vec` as ONE new atom. Both this and `MathText` type as
64    /// `"math"` — a `${…}` literal and a `math-*`-primitive-built value are
65    /// interchangeable wherever a `math`-typed argument is expected (see
66    /// `primitives.rs`'s `as_math`, which accepts either).
67    Math(Rc<Vec<Math>>),
68    /// `math-boxes` (V0_1 only) — the evaluated math tree `read-math`
69    /// produces, wrapping the SAME `Math` atom tree `Value::Math` uses so
70    /// every layout/primitive helper is shared unchanged. Distinct from
71    /// `Value::Math`: no V0_0 primitive ever produces or consumes this
72    /// variant, and no V0_1 primitive ever produces `Value::Math` — kept
73    /// apart so a V0_1 program can't silently pass a `math-text` where
74    /// `math-boxes` is required (`as_math_boxes` is strict).
75    MathBoxes(Rc<Vec<Math>>),
76    /// A mutable cell (`let-mutable`'s binding; v0.0.6's `Location`/store
77    /// entry). This port uses a directly-shared `RefCell` instead of an
78    /// indirection through a separate store table.
79    Ref(Rc<RefCell<Value>>),
80    /// `inline-boxes` (the `Horz` base constant).
81    InlineBoxes(Vec<HorzBox>),
82    /// `block-boxes` (the `Vert` base constant).
83    BlockBoxes(Vec<VertBox>),
84    /// `image` (`load-image`'s result): an index into the document-wide
85    /// image table (`Interp::images`), moved into `DocumentValue::images`
86    /// once `page-break` packages the final document. Carrying just the
87    /// index (not the decoded bytes) keeps this value cheap to clone, same
88    /// as `Value::Ref`'s `Rc`.
89    Image(ImageId),
90    Document(Rc<DocumentValue>),
91    /// A closure. Its body is an already-compiled `CompiledExpr`, run
92    /// directly by [`crate::eval::Interp::apply`] — the only closure
93    /// representation.
94    CompiledClosure {
95        /// SATySFi 0.1 labeled optional LABELS, in binder order; empty for
96        /// every 0.0.6-built closure. Each receives an `option`-typed value at
97        /// application (`Some v` when the call supplies `?(label = v)`, `None`
98        /// otherwise). Only the labels survive — a call site matches against
99        /// them by name — while the binders they bind to are slots `0..n` of
100        /// the frame application pushes, so their names are gone.
101        opt_labels: Vec<String>,
102        /// The positional parameter's slot is `opt_labels.len()`, immediately
103        /// after the optional binders, so it needs no field of its own.
104        body: CompiledExpr,
105        env: Env,
106    },
107    /// `&e` — a quoted expression awaiting the next stage, with the
108    /// environment it was quoted in. Typed `code ty` ([`crate::types::MonoType::Code`]).
109    ///
110    /// The same shape as [`Value::CompiledClosure`] minus a parameter, and
111    /// for the same reason: this evaluator compiles to slot-indexed
112    /// closures, so a fragment cannot be carried as a re-compilable syntax
113    /// tree the way upstream's `code_value` is — its variable references
114    /// are already bound to the frames of the scope it was written in.
115    /// Carrying the compiled body with its environment keeps those
116    /// references meaning what they said, which is what `~` then forces.
117    Code {
118        body: CompiledExpr,
119        env: Env,
120    },
121    /// A (possibly partially applied) native primitive.
122    Prim {
123        def: &'static PrimDef,
124        applied: Vec<Value>,
125    },
126    /// `pre-path` (`start-path`/`line-to`'s result).
127    PrePath(rustyfi_backend::PrePath),
128    /// `path` (`terminate-path`/`close-with-line`'s result).
129    Path(rustyfi_backend::Path),
130    /// `graphics` — one resolved drawing element (`fill`/`stroke`'s result);
131    /// a `graphics list` is just `Value::List` of these, same as upstream.
132    Graphics(rustyfi_backend::GraphicsElem),
133    /// `font` (**V0_1 only**; upstream `saphe-split`'s `BCFontKey of
134    /// FontKey.t`) — an OPAQUE handle on one loaded face, already resolved
135    /// through the metrics provider's font store at the point the value was
136    /// minted. Upstream mints one per `files[]` entry of a FONT ENVELOPE
137    /// (`envelopeChecker.ml`'s `check_font_envelope`, evaluating the
138    /// internal `LoadSingleFont{path}`/`LoadCollectionFont{path;index}`
139    /// node to `BCFontKey`); this port's bundled 0.1 font envelopes are
140    /// `.satyh` stand-ins that mint theirs through the LOCAL
141    /// `load-single-font` primitive instead.
142    ///
143    /// Deliberately carries NO abbrev/name/path: it is a store INDEX, the
144    /// same thing upstream's `FontKey.t` is, and nothing in the language can
145    /// map it back. That opacity is load-bearing for the cross-version
146    /// boundary: 0.0.6's font-consuming primitives want an ABBREV naming a
147    /// row of `dist/hash/fonts.satysfi-hash`, and no such name is
148    /// recoverable from a key.
149    Font(rustyfi_backend::FontKey),
150    /// `text-info` (the text-mode-context sliver).
151    TextInfo(TextInfo),
152    /// `hyphenation` (`load-hyphenation-dictionary`'s result) — the tag
153    /// `set-hyphenation-dictionary` writes into
154    /// `Context::hyphen_dictionary`, naming which dictionary was requested.
155    Hyphenation(HyphenLang),
156}
157
158impl Value {
159    /// A short type name for error messages.
160    pub fn type_name(&self) -> &'static str {
161        match self {
162            Value::Unit => "unit",
163            Value::Bool(_) => "bool",
164            Value::Int(_) => "int",
165            Value::Float(_) => "float",
166            Value::Length(_) => "length",
167            Value::Str(_) => "string",
168            Value::List(_) => "list",
169            Value::Tuple(_) => "tuple",
170            Value::Ctor(_, _) => "variant",
171            Value::Record(_) => "record",
172            Value::Context(_) => "context",
173            Value::InlineText { .. } => "inline-text",
174            Value::BlockText { .. } => "block-text",
175            Value::MathText { .. } => "math",
176            Value::Math(_) => "math",
177            Value::MathBoxes(_) => "math-boxes",
178            Value::Ref(_) => "mutable",
179            Value::InlineBoxes(_) => "inline-boxes",
180            Value::BlockBoxes(_) => "block-boxes",
181            Value::Image(_) => "image",
182            Value::Document(_) => "document",
183            Value::CompiledClosure { .. } => "function",
184            Value::Code { .. } => "code",
185            Value::Prim { .. } => "function",
186            Value::PrePath(_) => "pre-path",
187            Value::Path(_) => "path",
188            Value::Graphics(_) => "graphics",
189            Value::Font(_) => "font",
190            Value::TextInfo(_) => "text-info",
191            Value::Hyphenation(_) => "hyphenation",
192        }
193    }
194}
195
196/// One atom of a faithful `math` value (`Value::Math`'s element type) —
197/// trimmed mirror of upstream `math` (`types.cppo.ml:1024`). Every
198/// closure-typed field upstream carries (kern functions, a paren pair's
199/// sizing closures, `math-pull-in-scripts`' resolver, `text-in-math`'s
200/// embedded-box callback) is stored here OPAQUELY as a plain `Value` —
201/// constructing one of these variants never *calls* such a closure, exactly
202/// like upstream, where a `math` value is inert data until the real layout
203/// engine walks it.
204#[derive(Clone, Debug)]
205pub enum Math {
206    /// One base atom — a char run, a styled char, or embedded text. See
207    /// [`MathElement`].
208    Pure(MathElement),
209    /// `math-group`: override the left/right math-class of a sub-`math`
210    /// (`\mathbin`, `\mathrel`, …) — the two classes can differ (unlike
211    /// every other variant here, which presents one class on both sides),
212    /// which is exactly why upstream gives it its own node rather than
213    /// folding it into `ChangeContext`.
214    Group(MathKind, MathKind, Vec<Math>),
215    /// `math-sup`: `base ^ script`.
216    Sup(Vec<Math>, Vec<Math>),
217    /// `math-sub`: `base _ script`.
218    Sub(Vec<Math>, Vec<Math>),
219    /// `math-color`.
220    ChangeColor(Color, Vec<Math>),
221    /// `math-char-class` (`\mathrm`/`\mathbf`/…) — the resolved
222    /// [`MathCharClass`] a `math-char-class` primitive call named (`\mathrm`
223    /// -> `MathRoman` -> `MathCharClass::Roman`, …). Its layout arm
224    /// (`primitives.rs`) sets `Context::math_char_class` to this while
225    /// laying out the inner list, which is what makes `VariantCharPending`'s
226    /// per-char remap style-sensitive.
227    ChangeCharClass(MathCharClass, Vec<Math>),
228    /// `math-frac`: numerator, denominator.
229    Fraction(Vec<Math>, Vec<Math>),
230    /// `math-radical`: `\sqrt[degree]{radicand}` — `None` degree is the
231    /// common `\sqrt` case (`math-radical None radicand`); upstream's own
232    /// `MathRadicalWithDegree` is `failwith`-unimplemented too
233    /// (`math.ml:886`), so a `Some` degree here is carried faithfully but
234    /// never rendered specially (matches upstream by parity).
235    Radical(Option<Vec<Math>>, Vec<Math>),
236    /// `math-paren`: left/right paren-sizing closures (each a `paren =
237    /// length -> length -> length -> length -> color -> inline-boxes *
238    /// (length -> length)`, carried opaquely) plus the bracketed content.
239    Paren(Box<Value>, Box<Value>, Vec<Math>),
240    /// `math-paren-with-middle`: left/right/middle paren closures plus the
241    /// `\setsep`-style list of bracketed sub-`math`s.
242    ParenWithMiddle(Box<Value>, Box<Value>, Box<Value>, Vec<Vec<Math>>),
243    /// `math-upper`: base with an over-script (`\overline`-adjacent, big-
244    /// operator upper limit).
245    UpperLimit(Vec<Math>, Vec<Math>),
246    /// `math-lower`: base with an under-script (big-operator lower limit).
247    LowerLimit(Vec<Math>, Vec<Math>),
248    /// `math-pull-in-scripts`: a big operator's own left/right class plus
249    /// the `(math option -> math option -> math)` resolver closure that
250    /// routes an eventual `^`/`_` into limits instead of corner scripts
251    /// (`\sum^n_i`-style). The closure is carried opaquely, same as
252    /// `Paren`'s; only actually invoked by the real layout engine.
253    PullInScripts(MathKind, MathKind, Box<Value>),
254    /// V0_1 only: `read-math`'s captured reading context — the port's
255    /// coarse-grained stand-in for upstream's
256    /// per-node `context` fields (`types.cppo.ml:1051-1110`). Constructed
257    /// ONLY by the V0_1 primitive `read-math`; no V0_0 path ever builds
258    /// or matches this variant. Its layout arm (`primitives.rs`'s
259    /// `layout_math_list`) lays `inner` out with ambient context = `*ctx`
260    /// and size = `ctx.font_size` as an ABSOLUTE override — a `WithContext`
261    /// produced under an `enter_script`ed context already carries the
262    /// script-shrunk size, so the engine's own Sup/Sub shrink never
263    /// double-applies to it.
264    WithContext(Box<Context>, Vec<Math>),
265}
266
267/// The base-atom payload of [`Math::Pure`] — mirrors upstream
268/// `math_element_main` (`types.cppo.ml:1009`), flattened (the math-class
269/// lives directly on each variant here, rather than in a separate wrapping
270/// `MathElement(kind, math_char_main)` layer) since nothing else needs the
271/// undecorated `math_char_main` on its own.
272#[derive(Clone, Debug)]
273pub enum MathElement {
274    /// `math-char` / `math-big-char`: a run of math characters, one atom.
275    /// `big` selects the large-operator size class (`\sum`/`\int`-style;
276    /// layout does not yet upscale it).
277    Char {
278        class: MathKind,
279        big: bool,
280        chars: String,
281    },
282    /// `math-char-with-kern` / `math-big-char-with-kern`: like `Char`, plus
283    /// opaque left/right kern-function closures (each `length -> length ->
284    /// length`, fontsize/y-position -> kern amount; `\int`'s
285    /// italic-correction kern is the motivating case). Not yet consulted by
286    /// layout.
287    CharWithKern {
288        class: MathKind,
289        big: bool,
290        chars: String,
291        kern_l: Box<Value>,
292        kern_r: Box<Value>,
293    },
294    /// `text-in-math` (`\text`, `\cases`): an embedded `context ->
295    /// inline-boxes` closure, carried opaquely — the box it eventually
296    /// produces isn't yet nestable into a math run's glyph model,
297    /// so this is stored faithfully but not rendered.
298    EmbeddedText { class: MathKind, body: Box<Value> },
299    /// `math-variant-char` (`primitives.cppo.ml`'s `MathVariantCharDirect`)
300    /// — one atom with a per-style codepoint set (Greek letters, `math.
301    /// satyh`'s `greek-lowercase`/`greek-uppercase`). `big` mirrors `Char`'s
302    /// (unused upstream for variant chars in practice, kept for shape
303    /// parity).
304    VariantChar {
305        class: MathKind,
306        big: bool,
307        style: Box<MathVariantStyle>,
308    },
309    /// One MATHCHAR token from a `${…}` literal, not yet resolved to a
310    /// `MathKind`/codepoint — `reflect_math_elem`'s `MathElem::Chars` arm
311    /// pushes exactly one of these per token, deferring both the
312    /// whole-token class-map lookup and the
313    /// per-char variant remap to layout time, where the current
314    /// `Context::font`/`math_char_class` are available to metrics-probe
315    /// the remap (`resolve_variant_char`).
316    VariantCharPending(String),
317    /// V0_1 only: `embed-inline-to-math`'s payload — already-evaluated
318    /// inline boxes carrying an explicit math class. Contrast
319    /// `EmbeddedText`'s 0.0.6 closure (evaluated lazily at layout time
320    /// under a `context`); this is eager, already-materialized data,
321    /// matching upstream's `embed_inline_to_math` (which has no context to
322    /// re-apply a closure under). Layout: the same stand-in rendering path
323    /// `EmbeddedText` gets — `math_glyphs_of_inline_boxes` over
324    /// `boxes` directly, no closure application.
325    EmbeddedBoxes {
326        class: MathKind,
327        boxes: Vec<HorzBox>,
328    },
329}
330
331/// `math-variant-char`'s 9-field per-style codepoint record
332/// (`math.satyh`'s `greek-lowercase`/`greek-uppercase` build one per Greek
333/// letter). Field order/names mirror the record literal math.satyh
334/// constructs.
335#[derive(Clone, Debug)]
336pub struct MathVariantStyle {
337    pub italic: String,
338    pub bold_italic: String,
339    pub roman: String,
340    pub bold_roman: String,
341    pub script: String,
342    pub bold_script: String,
343    pub fraktur: String,
344    pub bold_fraktur: String,
345    pub double_struck: String,
346}
347
348/// `text-info` (v0.0.6 `BCTextModeContext` carrying
349/// `TextBackend.text_mode_context`, src/text-mode/textBackend.ml:1-5).
350/// PDF-port sliver: upstream's second field, `escape_list`, is omitted —
351/// no v0.0.6 primitive can set it (TextBackend.set_escape_list has no
352/// vminst.ml caller), so it is invariantly `[]` upstream. `indent` is
353/// invariantly >= 0 (`deepen_indent` clamps the increment).
354#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub struct TextInfo {
356    pub indent: i64,
357}
358
359/// The final result of evaluating a document.
360#[derive(Clone, Debug)]
361pub struct DocumentValue {
362    pub geometry: PageGeometry,
363    pub pages: Vec<Page>,
364    /// Every image `load-image` decoded while evaluating this document,
365    /// indexed by `ImageId` (`PureHorzBox::Image::image` / `Value::Image`
366    /// point in here). Moved out of `eval::Interp::images` by `page-break`
367    /// when it packages the final document; threaded to
368    /// `rustyfi_pdf::render_pdf`/`render_pdf_ttf` so the PDF writer can emit
369    /// one Image XObject per image actually used.
370    pub images: Vec<ImageResource>,
371    /// Extras (annotations / destinations / outline / per-page deco
372    /// overlays), attached by the compile driver AFTER the final trial's
373    /// `fire_hooks` — `prim_page_break` cannot fill this (hooks/decos fire
374    /// only after placement), so it packages `DocExtras::default()` and
375    /// `compile_document_cst_with_trials` overwrites it on the winning trial.
376    pub extras: DocExtras,
377    /// Reflowable/semantic HTML side-channel: a clone of the
378    /// flat `Vec<VertBox>` as it existed just BEFORE `page_break_core`
379    /// handed it to `chop_page` — the document's natural linear flow, with
380    /// paragraph boundaries (`Skip`), frame nesting (`FrameStart`/`FrameEnd`
381    /// marker pairs), and `ClearPage` all intact, not yet sliced into pages
382    /// or carrying injected headers/footers/footnotes. Populated
383    /// unconditionally (one negligible `Vec<VertBox>` clone per compile,
384    /// rather than threading a `want_reflow` flag through every entry
385    /// point). `Option` keeps the field's meaning ("present only for a
386    /// reflow-capable compile") self-documenting even though every current
387    /// producer fills it.
388    ///
389    /// **Purely additive.** Neither `rustyfi_pdf::render_pdf*` nor the
390    /// `html-support` branch's faithful HTML backend reads this field —
391    /// only that branch's REFLOWABLE backend does.
392    pub reflow_source: Option<Vec<VertBox>>,
393    /// Links: one `(DecoId, action)` per `register-link-to-uri`/
394    /// `-to-location` call made from inside a firing deco closure — needed
395    /// because (unlike `extras.annotations`, which is page-absolute with no
396    /// `DecoId`) this is what lets the reflow backend find which
397    /// `PureHorzBox::Frame` in `reflow_source` a link belongs to.
398    ///
399    /// NOTE for whoever merges `html-support`: `\href`/`\ref` go through
400    /// `inline-frame-breakable`, which no longer builds a `Frame` at all —
401    /// it splices its contents between a `PureHorzBox::InlineFrameMarker`
402    /// pair — so the reflow walker has to match the MARKER's `DecoId` (and
403    /// wrap the boxes between the pair) rather than a `Frame`'s, or every
404    /// link silently stops being wrapped. Filled in by `eval_document_trials`
405    /// AFTER `fire_hooks`. Empty by default, same "purely additive" policy
406    /// as `reflow_source`.
407    pub reflow_links: Vec<(DecoId, AnnotAction)>,
408    /// Same idea as `reflow_links`, for `register-destination`
409    /// (`annot.satyh`'s `register-location-frame` idiom): `(DecoId, name)`.
410    pub reflow_dests: Vec<(DecoId, String)>,
411    /// Each block frame's own decoration at its natural size, box-local
412    /// (`FrameDecoration`). Same provenance and lifetime as `reflow_dests`:
413    /// filled by `fire_hooks`, drained by `eval_document_trials`, read only
414    /// by the reflowable HTML backend — which has no page grid and so cannot
415    /// re-run a deco callback itself.
416    pub reflow_frame_decos: Vec<(DecoId, FrameDecoration)>,
417}
418
419/// FxHash — the fast, NON-cryptographic hasher `rustc` uses (rustc-hash),
420/// reimplemented here dependency-free. Variable lookup walks the environment
421/// frame chain probing each frame's map by name (~192M probes on a graphics-
422/// heavy doc); std's default SipHash is DoS-resistant but slow for these short,
423/// non-adversarial identifier keys, and dominated the interpreter's runtime.
424/// Processing 8/4/2/1 bytes at a step with a rotate-xor-multiply is ~3-5x
425/// faster and is exactly what an internal, trusted env map wants.
426#[derive(Default)]
427struct FxHasher {
428    hash: usize,
429}
430
431/// rustc-hash's multiplier, per pointer width — the golden ratio scaled to
432/// 2^64 and to 2^32 respectively, which is the pair rustc-hash itself uses.
433///
434/// Spelled per width because the 64-bit literal does not FIT a 32-bit `usize`:
435/// it silently truncates to a different (and much worse) constant, and rustc
436/// rejects it outright. `wasm32-unknown-unknown` is a 32-bit target, which is
437/// where this first mattered.
438#[cfg(target_pointer_width = "64")]
439const FX_SEED: usize = 0x51_7c_c1_b7_27_22_0a_95;
440#[cfg(not(target_pointer_width = "64"))]
441const FX_SEED: usize = 0x9e_37_79_b9;
442
443impl FxHasher {
444    #[inline]
445    fn add(&mut self, i: usize) {
446        self.hash = (self.hash.rotate_left(5) ^ i).wrapping_mul(FX_SEED);
447    }
448}
449
450impl std::hash::Hasher for FxHasher {
451    #[inline]
452    fn write(&mut self, mut bytes: &[u8]) {
453        // One `usize` per step: 8 bytes on a 64-bit target, 4 on a 32-bit one.
454        // The width was hard-coded as 8, which on a 32-bit build both
455        // over-advanced the cursor and panicked in `try_into` — a 4-byte
456        // `usize` cannot be built from an 8-byte slice. On 64-bit this is the
457        // same sequence of `add` calls as before, byte for byte.
458        const WIDE: usize = std::mem::size_of::<usize>();
459        while bytes.len() >= WIDE {
460            let (head, rest) = bytes.split_at(WIDE);
461            self.add(usize::from_le_bytes(head.try_into().unwrap()));
462            bytes = rest;
463        }
464        // Skipped where `usize` is already 4 bytes: the loop above consumed
465        // every whole 4-byte group there.
466        if WIDE > 4 && bytes.len() >= 4 {
467            self.add(u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize);
468            bytes = &bytes[4..];
469        }
470        if bytes.len() >= 2 {
471            self.add(u16::from_le_bytes(bytes[..2].try_into().unwrap()) as usize);
472            bytes = &bytes[2..];
473        }
474        if let Some(&b) = bytes.first() {
475            self.add(b as usize);
476        }
477    }
478    #[inline]
479    fn write_u8(&mut self, i: u8) {
480        self.add(i as usize);
481    }
482    #[inline]
483    fn write_usize(&mut self, i: usize) {
484        self.add(i);
485    }
486    #[inline]
487    fn finish(&self) -> u64 {
488        self.hash as u64
489    }
490}
491
492#[derive(Default, Clone)]
493struct FxBuild;
494impl std::hash::BuildHasher for FxBuild {
495    type Hasher = FxHasher;
496    #[inline]
497    fn build_hasher(&self) -> FxHasher {
498        FxHasher::default()
499    }
500}
501
502type FxMap = HashMap<Rc<str>, Value, FxBuild>;
503
504/// The **compile-time** environment: the flat name -> value table of
505/// primitives and base constants that `crate::compile` folds unshadowed
506/// references against, and whose [`BaseEnv::names`] seed the elaborator's
507/// scope.
508///
509/// This is deliberately NOT the runtime environment, and is not the root of
510/// the runtime frame chain. Nothing resolves a name at run time —
511/// top-level bindings go through the compiler's `Globals` table, locals
512/// through slot indices, and unshadowed base names are constant-folded at
513/// compile time — so the two are what they actually are: a name map used
514/// while compiling, and a stack of positional frames used while running.
515#[derive(Clone, Debug, Default)]
516pub struct BaseEnv {
517    vars: FxMap,
518}
519
520impl BaseEnv {
521    pub fn new() -> BaseEnv {
522        BaseEnv::default()
523    }
524
525    /// A copy that can be extended without disturbing this one. There is no
526    /// frame chain here — shadowing is just overwriting in the copy.
527    pub fn child(&self) -> BaseEnv {
528        self.clone()
529    }
530
531    pub fn define(&mut self, name: impl Into<Rc<str>>, value: Value) {
532        self.vars.insert(name.into(), value);
533    }
534
535    pub fn lookup(&self, name: &str) -> Option<Value> {
536        self.vars.get(name).cloned()
537    }
538
539    /// Every name bound here (feeds the elaborator's scope).
540    pub fn names(&self) -> Vec<String> {
541        self.vars.keys().map(|k| k.to_string()).collect()
542    }
543}
544
545/// The **runtime** environment: a chain of positional frames.
546///
547/// A frame is a plain `Vec<Value>`, and a compiled variable reference
548/// is a `(depth, index)` pair resolved at compile time — walk `depth` parents,
549/// index the vector. There are no names here at all: the compiler's scope
550/// stack is 1:1 with this chain (it pushes exactly where a frame is created),
551/// so every local is a static coordinate.
552///
553/// `RefCell` because `let rec` back-patches its siblings into a shared frame
554/// one at a time: the frame is created pre-sized with placeholders and filled
555/// in order, and a closure that captured it sees the later fills.
556#[derive(Clone, Debug)]
557pub struct Env(Rc<Frame>);
558
559#[derive(Debug)]
560struct Frame {
561    slots: RefCell<Vec<Value>>,
562    parent: Option<Env>,
563}
564
565impl Env {
566    /// The empty root frame every program runs in.
567    pub fn root() -> Env {
568        Env(Rc::new(Frame {
569            slots: RefCell::new(Vec::new()),
570            parent: None,
571        }))
572    }
573
574    /// Push a frame holding `slots`, in the order the compiler assigned them.
575    pub fn child(&self, slots: Vec<Value>) -> Env {
576        Env(Rc::new(Frame {
577            slots: RefCell::new(slots),
578            parent: Some(self.clone()),
579        }))
580    }
581
582    #[inline]
583    fn frame_at(&self, depth: u16) -> &Frame {
584        let mut f = self;
585        for _ in 0..depth {
586            f =
587                f.0.parent
588                    .as_ref()
589                    .expect("compiled slot depth exceeds the runtime frame chain");
590        }
591        &f.0
592    }
593
594    /// Read the local at `(depth, index)`.
595    #[inline]
596    pub fn slot(&self, depth: u16, index: u16) -> Value {
597        self.frame_at(depth).slots.borrow()[index as usize].clone()
598    }
599
600    /// Overwrite the local at `(depth, index)` — `let rec`'s back-patch.
601    #[inline]
602    pub fn set_slot(&self, depth: u16, index: u16, value: Value) {
603        self.frame_at(depth).slots.borrow_mut()[index as usize] = value;
604    }
605}