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}