Skip to main content

rustyfi_lang/
ast.rs

1//! The elaborated abstract syntax tree (a subset of
2//! `abstract_tree` in types.cppo.ml). Produced from the surface CST by
3//! `elaborate`; consumed by the evaluator.
4//!
5//! # The identifier parameter `I`
6//!
7//! Every node type here is generic over `I`, the representation of a
8//! **lexical identifier** — precisely those names that become keys in the
9//! runtime environment ([`crate::value::Env`]). Two instantiations exist:
10//!
11//! * `Ast<Symbol<'s>>` — the *compile-side* tree, produced by
12//!   [`crate::elaborate`] and consumed by [`crate::typecheck`]. Identifiers
13//!   are interned [`crate::symbol::Symbol`]s: `Copy`, 4 bytes, compared and
14//!   hashed as integers, and branded to the [`crate::symbol::SymbolStore`]
15//!   that minted them.
16//! * `Ast<String>` — the *runtime* tree, and the *default*, so unadorned
17//!   `Ast` means this one. It is what `crate::compile` lowers and what
18//!   [`crate::value::Value`] embeds in its quoted-text and closure payloads.
19//!   It has **no lifetime**, which is the whole point: a branded `Symbol<'s>`
20//!   reaching `Value` would cascade `'s` through all 172 `prim_*` functions
21//!   for zero speed.
22//!
23//! `compile::compile_program` is the membrane between the two: it
24//! [de-brands](Ast::map_idents) the branded tree to `Ast<String>` once, at
25//! compile time, so the compiler and the entire runtime stay untouched.
26//!
27//! ## What is *not* parameterised
28//!
29//! `I` marks environment keys and nothing else. String literals
30//! ([`Ast::Str`], [`Pattern::Str`], [`MathElem::Chars`], [`IText::Text`]) are
31//! char data. Record field labels, constructor tags, and labeled-optional
32//! *labels* are separate data namespaces that reach the runtime as
33//! `BTreeMap` keys and value tags — they stay `String` end-to-end and must
34//! never be symbolized. Note the asymmetry in
35//! [`Ast::LambdaOpt`]: an optional argument's **label** is data (`String`),
36//! its **binder** is a lexical variable (`I`).
37
38use rustyfi_backend::Length;
39use rustyfi_syntax::{RustyfiVersion, Span};
40use std::rc::Rc;
41
42/// The **branded** instantiation of every node type in this module: what
43/// [`crate::elaborate`] produces and [`crate::typecheck`] consumes, with each
44/// lexical identifier interned into a [`crate::symbol::SymbolStore`].
45///
46/// The names deliberately shadow the unparameterised ones, so that after a
47/// `use crate::ast::branded::{Ast, Pattern, ..}` the front half of the
48/// pipeline reads exactly like the unbranded one, apart from the `<'s>` its
49/// enclosing signature carries.
50pub mod branded {
51    use crate::symbol::Symbol;
52
53    pub type Ast<'s> = super::Ast<Symbol<'s>>;
54    pub type BText<'s> = super::BText<Symbol<'s>>;
55    pub type CmdArg<'s> = super::CmdArg<Symbol<'s>>;
56    pub type IText<'s> = super::IText<Symbol<'s>>;
57    pub type MatchArm<'s> = super::MatchArm<Symbol<'s>>;
58    pub type MathElem<'s> = super::MathElem<Symbol<'s>>;
59    pub type Pattern<'s> = super::Pattern<Symbol<'s>>;
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub enum Ast<I = String> {
64    Unit,
65    Bool(bool),
66    Int(i64),
67    Float(f64),
68    Length(Length),
69    Str(String),
70    Var(I, Span),
71    Apply(Box<Ast<I>>, Box<Ast<I>>),
72    Lambda(I, Rc<Ast<I>>),
73    LetIn(I, Box<Ast<I>>, Box<Ast<I>>),
74    /// Mutually recursive bindings (`let-rec … and …`); every body must be a
75    /// `Lambda`, all names are in scope in all bodies.
76    LetRecIn(Vec<(I, Rc<Ast<I>>)>, Box<Ast<I>>),
77    /// `let-math \cmd param* = expr in body` — a math-command binding.
78    /// Evaluates identically to `LetIn`; the DISTINCT variant exists purely
79    /// so the typechecker can tell it apart from an ordinary `\`-sigiled
80    /// `LetIn` (a `let-inline` binding, or a qualified-name alias of one)
81    /// without re-deriving that from the shared `\` sigil — see
82    /// `typecheck.rs`'s `Checker::math_command_scheme`.
83    LetMathIn(I, Box<Ast<I>>, Box<Ast<I>>),
84    IfThenElse(Box<Ast<I>>, Box<Ast<I>>, Box<Ast<I>>),
85    Match(Box<Ast<I>>, Vec<MatchArm<I>>),
86    Tuple(Vec<Ast<I>>),
87    /// A variant constructor, optionally applied (`None` / `Some 3`).
88    /// The tag is a data-level name, not an environment key — see the module
89    /// doc comment on what `I` does and does not cover.
90    Ctor(String, Option<Box<Ast<I>>>),
91    Record(Vec<(String, Ast<I>)>),
92    List(Vec<Ast<I>>),
93    /// Quoted inline text: evaluated only when `read-inline` runs it.
94    InlineText(Rc<Vec<IText<I>>>),
95    /// Quoted block text: evaluated only when `read-block` runs it.
96    BlockText(Rc<Vec<BText<I>>>),
97    /// Quoted math text (`${…}`); typesetting is deferred, the
98    /// value is carried opaquely until then.
99    MathText(Rc<Vec<MathElem<I>>>),
100    /// `let-mutable x <- init in body` — binds `x` to a mutable cell.
101    LetMutableIn(I, Box<Ast<I>>, Box<Ast<I>>),
102    /// `x <- e` — overwrite a mutable cell; evaluates to unit.
103    Overwrite(I, Span, Box<Ast<I>>),
104    /// `while cond do body` — evaluates to unit.
105    WhileDo(Box<Ast<I>>, Box<Ast<I>>),
106    /// `e1 before e2` (`UTSequential`) — evaluate `e1` for effect, then `e2`.
107    Sequential(Box<Ast<I>>, Box<Ast<I>>),
108    /// `e#label` (`UTAccessField`). The label is a record field, not an
109    /// environment key — see the module doc comment.
110    AccessField(Box<Ast<I>>, String, Span),
111    /// `(| e with label = v |)` (`UTUpdateField`) — functional record update.
112    UpdateField(Box<Ast<I>>, String, Box<Ast<I>>),
113    /// `f ?(l = e, …) arg` — SATySFi 0.1 labeled-optional application
114    /// (upstream `Apply(labmap, e1, e2)`). `opts` is non-empty by
115    /// construction: a bundle-less 0.1 application lowers to plain
116    /// [`Ast::Apply`]. Labels are deduplicated at elaboration; at
117    /// beta-reduction a provided `?(l = e)` binds the closure's `l` binder to
118    /// `Some e`, and any declared label the call omits binds `None` (see
119    /// `eval::Interp::apply_with_opts`).
120    ApplyOpt {
121        func: Box<Ast<I>>,
122        opts: Vec<(String, Ast<I>)>,
123        arg: Box<Ast<I>>,
124    },
125    /// `fun ?(l = x, …) p -> body` — SATySFi 0.1 labeled-optional lambda
126    /// (upstream `Function(evid_labmap, patbr)`). `opts` maps each label to
127    /// the binder name that receives its `option`-typed value. Pattern
128    /// params are pre-desugared (by `elaborate`) to a fresh var + `Match`,
129    /// like `rec_clause_value`, so `param` here is always a plain binder.
130    ///
131    /// Note the mixed pair: each label is *data* (`String` — it is matched
132    /// against a call site's `?(l = e)` labels), while each binder is a
133    /// *lexical variable* (`I` — it becomes an environment key).
134    LambdaOpt {
135        opts: Vec<(String, I)>,
136        param: I,
137        body: Rc<Ast<I>>,
138    },
139    /// A version tag around one spliced cross-version dependency binding's
140    /// RHS. `elaborate.rs`'s cross-version splice
141    /// wraps each binding contributed by a `LoadedCst::V0_0` dependency in
142    /// `VersionScope(V0_0, rhs)`, at RHS granularity (never the surrounding
143    /// `LetIn`/`LetRecIn` node, and never the continuation that follows it).
144    /// Three consumers push/pop a cursor around recursing into `body`:
145    /// - `compile.rs`'s `Compiler::current_version` — which base
146    ///   environment (`V0_1`'s or `V0_0`'s) an unshadowed `Ast::Var`
147    ///   constant-folds against, so a version-forked primitive
148    ///   (`page-break`, `math-*`, …) freezes to the RIGHT version's
149    ///   `PrimDef` at compile time (the only version-sensitive resolution in
150    ///   the whole pipeline).
151    /// - `eval.rs`'s `Interp::version` — any runtime fork that reads it
152    ///   (`primitives.rs`'s `reflect_math_elem`/`coerce_graphics_result`/
153    ///   `make_paren_run`) sees `V0_0` while evaluating on behalf of this
154    ///   subtree.
155    /// - `typecheck.rs`'s base-type-env swap — the subtree's *internal*
156    ///   forked-primitive-type use checks against `V0_0`'s primitive types.
157    ///
158    /// **Never emitted on a pure single-version load** — structurally inert
159    /// on the pure-0.0.6/pure-0.1 paths: no arm executes, no runtime check
160    /// is involved.
161    VersionScope(RustyfiVersion, Box<Ast<I>>),
162    /// `ModuleScope(["M", "N"], rhs)`: marks that `rhs` is the body of a
163    /// member of module `M.N`, so a BARE constructor reference inside it
164    /// resolves against that module's constructors first (the type/ctor
165    /// analog of `push_named_binding`'s value `Scope::rename`). Transparent
166    /// everywhere except `Checker::infer`/`bind_pattern`, which push the
167    /// path and try qualified ctor keys before the bare fallback — no
168    /// constructor NAME string ever changes (eval, exhaustiveness, and
169    /// error/warning text stay byte-identical). Wraps a module member's RHS
170    /// only, exactly like `VersionScope`.
171    ModuleScope(Vec<String>, Box<Ast<I>>),
172    /// Marks `body` as coming from a file that declared a stage other than
173    /// the default (`@stage: 0` / `@stage: persistent`), so the typechecker
174    /// reads it at that stage and its `&` quotes are legal there.
175    ///
176    /// The stage analogue of [`Ast::VersionScope`], and for the same reason:
177    /// the loader concatenates every library's prelude into ONE file, so a
178    /// per-file property has to travel with the bindings it came from or be
179    /// lost at the merge.
180    StageScope(crate::types::Stage, Box<Ast<I>>),
181    /// `&e` — quote. Evaluated at stage 0, it does NOT run `e`: it partially
182    /// evaluates it into residual code and yields that code as a value
183    /// (`Value::Code`), typed `code ty`. Upstream's `UTNext`/`Next`
184    /// (`parser.mly:796`, `evaluator.cppo.ml`'s `interpret_1`).
185    Next(Box<Ast<I>>),
186    /// `~e` — splice. Legal inside a quote (stage 1): `e` is evaluated NOW,
187    /// at stage 0, and the code it yields is spliced in where the `~e` stood.
188    /// Upstream's `UTPrev`/`Prev` (`parser.mly:797`).
189    Prev(Box<Ast<I>>),
190}
191
192/// One command-application argument (3b-β): `arg` is the ordinary
193/// positional argument value, `opts` is this argument's supplied
194/// `?(l = e, …)` labeled-optional bundle — upstream's `UTCommandArg of
195/// (label * expr) list * expr` (`types.cppo.ml:583-584`). All of 0.0.6, and
196/// every V0_1 command call with no bundle, emits `opts: vec![]`, behaving
197/// exactly like a bare `Ast`. A non-empty `opts` folds through
198/// `eval::Interp::apply_with_opts` (like `Ast::ApplyOpt`) instead of a plain
199/// `apply`; each label the command declares but this call omits still
200/// defaults to `None` there.
201#[derive(Clone, Debug, PartialEq)]
202pub struct CmdArg<I = String> {
203    pub opts: Vec<(String, Ast<I>)>,
204    pub arg: Ast<I>,
205}
206
207/// One quoted math element (structure mirrors the `mathmain`/`mathtop`/
208/// `mathbot` rules; only carried, not typeset, until later).
209#[derive(Clone, Debug, PartialEq)]
210pub enum MathElem<I = String> {
211    /// A run of math characters/symbols (`MATHCHAR`).
212    Chars(String),
213    /// `{ … }` grouping.
214    Group(Vec<MathElem<I>>),
215    /// `base _ script`
216    Sub(Box<MathElem<I>>, Vec<MathElem<I>>),
217    /// `base ^ script`
218    Sup(Box<MathElem<I>>, Vec<MathElem<I>>),
219    /// `base '`+ (primes count as a superscript)
220    Primes(Box<MathElem<I>>, usize),
221    /// `\cmd args…` in math mode; sigil included. `args` is [`CmdArg`]-shaped
222    /// for uniformity with `IText::Cmd`/`BText::Cmd` (the runtime command
223    /// fold is shared across all three); the math-mode application grammar
224    /// has no `?(l=e)` bundle form at all (math command *arguments* are
225    /// always bracket groups — `{…}` / `!{…}` / `!<…>` / `!(…)`, upstream
226    /// `narg`), so every `CmdArg` here has `opts: vec![]` by construction.
227    Cmd {
228        name: I,
229        span: Span,
230        args: Vec<CmdArg<I>>,
231    },
232    /// `#x` in math mode.
233    Embed { expr: Ast<I>, span: Span },
234}
235
236#[derive(Clone, Debug, PartialEq)]
237pub struct MatchArm<I = String> {
238    pub pat: Pattern<I>,
239    /// `when` guard, if any.
240    pub guard: Option<Ast<I>>,
241    pub body: Ast<I>,
242}
243
244/// Match patterns (`untyped_pattern_tree`).
245#[derive(Clone, Debug, PartialEq)]
246pub enum Pattern<I = String> {
247    Wild,
248    Var(I),
249    Unit,
250    Bool(bool),
251    Int(i64),
252    Str(String),
253    Tuple(Vec<Pattern<I>>),
254    EmptyList,
255    /// `head :: tail`
256    Cons(Box<Pattern<I>>, Box<Pattern<I>>),
257    Ctor(String, Option<Box<Pattern<I>>>),
258    /// `pat as name`
259    As(Box<Pattern<I>>, I),
260}
261
262/// One inline-text element (`input_horz_element`).
263#[derive(Clone, Debug, PartialEq)]
264pub enum IText<I = String> {
265    Text(String),
266    /// A backtick literal inside inline text (`` `…` ``;
267    /// `UTInputHorzEmbeddedCodeText`). Kept apart from `Text` because the
268    /// context's installed code-text command decides how it is set — see
269    /// `Context::code_text_command` and `read_inline`'s arm.
270    CodeText(String),
271    Cmd {
272        /// Sigil included (`\emph`), matching the environment entry.
273        name: I,
274        span: Span,
275        args: Vec<CmdArg<I>>,
276    },
277    /// `#expr;` — an embedded expression evaluating to inline-text, spliced
278    /// in place (`UTInputHorzContent`).
279    Embed {
280        expr: Ast<I>,
281        span: Span,
282    },
283    /// `${…}` embedded math (`UTInputHorzEmbeddedMath`). `read_inline`'s
284    /// `EmbedMath` arm applies the context's installed `[math] inline-cmd`
285    /// (`Context::math_command`) to `(ctx, math value)`, exactly like
286    /// upstream — `\cmd`/`#var` inside the literal go through
287    /// `reflect_math_elem`/`as_math`. Contexts with no installed command
288    /// (built by `Context::initial` directly, i.e. unit tests) fall back to
289    /// reflecting + laying out directly.
290    EmbedMath {
291        elems: Rc<Vec<MathElem<I>>>,
292        span: Span,
293    },
294}
295
296/// One block-text element (`input_vert_element`).
297#[derive(Clone, Debug, PartialEq)]
298pub enum BText<I = String> {
299    Cmd {
300        /// Sigil included (`+p`).
301        name: I,
302        span: Span,
303        args: Vec<CmdArg<I>>,
304    },
305    /// `#expr;` — an embedded expression evaluating to block-text
306    /// (`UTInputVertContent`).
307    Embed { expr: Ast<I>, span: Span },
308}
309
310// ---------------------------------------------------------------------------
311// Identifier remapping — the compile membrane's de-branding (see `debrand`)
312// ---------------------------------------------------------------------------
313
314impl<I> Ast<I> {
315    /// Rebuild this tree with every lexical identifier mapped through `f`.
316    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> Ast<J> {
317        let go = |a: &Ast<I>| a.map_idents(f);
318        match self {
319            Ast::Unit => Ast::Unit,
320            Ast::Bool(b) => Ast::Bool(*b),
321            Ast::Int(n) => Ast::Int(*n),
322            Ast::Float(x) => Ast::Float(*x),
323            Ast::Length(l) => Ast::Length(*l),
324            Ast::Str(s) => Ast::Str(s.clone()),
325            Ast::Var(n, sp) => Ast::Var(f(n), *sp),
326            Ast::Apply(g, a) => Ast::Apply(Box::new(go(g)), Box::new(go(a))),
327            Ast::Lambda(p, b) => Ast::Lambda(f(p), Rc::new(go(b))),
328            Ast::LetIn(n, v, r) => Ast::LetIn(f(n), Box::new(go(v)), Box::new(go(r))),
329            Ast::LetRecIn(bs, body) => Ast::LetRecIn(
330                bs.iter().map(|(n, v)| (f(n), Rc::new(go(v)))).collect(),
331                Box::new(go(body)),
332            ),
333            Ast::LetMathIn(n, v, r) => Ast::LetMathIn(f(n), Box::new(go(v)), Box::new(go(r))),
334            Ast::IfThenElse(c, t, e) => {
335                Ast::IfThenElse(Box::new(go(c)), Box::new(go(t)), Box::new(go(e)))
336            }
337            Ast::Match(s, arms) => Ast::Match(
338                Box::new(go(s)),
339                arms.iter().map(|a| a.map_idents(f)).collect(),
340            ),
341            Ast::Tuple(items) => Ast::Tuple(items.iter().map(go).collect()),
342            Ast::Ctor(tag, arg) => Ast::Ctor(tag.clone(), arg.as_ref().map(|a| Box::new(go(a)))),
343            Ast::Record(fields) => {
344                Ast::Record(fields.iter().map(|(l, e)| (l.clone(), go(e))).collect())
345            }
346            Ast::List(items) => Ast::List(items.iter().map(go).collect()),
347            Ast::InlineText(elems) => {
348                Ast::InlineText(Rc::new(elems.iter().map(|e| e.map_idents(f)).collect()))
349            }
350            Ast::BlockText(elems) => {
351                Ast::BlockText(Rc::new(elems.iter().map(|e| e.map_idents(f)).collect()))
352            }
353            Ast::MathText(elems) => {
354                Ast::MathText(Rc::new(elems.iter().map(|e| e.map_idents(f)).collect()))
355            }
356            Ast::LetMutableIn(n, i, b) => Ast::LetMutableIn(f(n), Box::new(go(i)), Box::new(go(b))),
357            Ast::Overwrite(n, sp, v) => Ast::Overwrite(f(n), *sp, Box::new(go(v))),
358            Ast::WhileDo(c, b) => Ast::WhileDo(Box::new(go(c)), Box::new(go(b))),
359            Ast::Sequential(a, b) => Ast::Sequential(Box::new(go(a)), Box::new(go(b))),
360            Ast::AccessField(e, l, sp) => Ast::AccessField(Box::new(go(e)), l.clone(), *sp),
361            Ast::UpdateField(e, l, v) => {
362                Ast::UpdateField(Box::new(go(e)), l.clone(), Box::new(go(v)))
363            }
364            Ast::ApplyOpt { func, opts, arg } => Ast::ApplyOpt {
365                func: Box::new(go(func)),
366                opts: opts.iter().map(|(l, e)| (l.clone(), go(e))).collect(),
367                arg: Box::new(go(arg)),
368            },
369            Ast::LambdaOpt { opts, param, body } => Ast::LambdaOpt {
370                // Label stays data, binder is a lexical variable.
371                opts: opts.iter().map(|(l, b)| (l.clone(), f(b))).collect(),
372                param: f(param),
373                body: Rc::new(go(body)),
374            },
375            Ast::VersionScope(v, b) => Ast::VersionScope(*v, Box::new(go(b))),
376            Ast::ModuleScope(path, b) => Ast::ModuleScope(path.clone(), Box::new(go(b))),
377            Ast::StageScope(st, b) => Ast::StageScope(*st, Box::new(go(b))),
378            Ast::Next(e) => Ast::Next(Box::new(go(e))),
379            Ast::Prev(e) => Ast::Prev(Box::new(go(e))),
380        }
381    }
382}
383
384impl<I> MatchArm<I> {
385    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> MatchArm<J> {
386        MatchArm {
387            pat: self.pat.map_idents(f),
388            guard: self.guard.as_ref().map(|g| g.map_idents(f)),
389            body: self.body.map_idents(f),
390        }
391    }
392}
393
394impl<I> Pattern<I> {
395    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> Pattern<J> {
396        match self {
397            Pattern::Wild => Pattern::Wild,
398            Pattern::Var(n) => Pattern::Var(f(n)),
399            Pattern::Unit => Pattern::Unit,
400            Pattern::Bool(b) => Pattern::Bool(*b),
401            Pattern::Int(n) => Pattern::Int(*n),
402            Pattern::Str(s) => Pattern::Str(s.clone()),
403            Pattern::Tuple(ps) => Pattern::Tuple(ps.iter().map(|p| p.map_idents(f)).collect()),
404            Pattern::EmptyList => Pattern::EmptyList,
405            Pattern::Cons(h, t) => {
406                Pattern::Cons(Box::new(h.map_idents(f)), Box::new(t.map_idents(f)))
407            }
408            Pattern::Ctor(tag, p) => {
409                Pattern::Ctor(tag.clone(), p.as_ref().map(|p| Box::new(p.map_idents(f))))
410            }
411            Pattern::As(p, n) => Pattern::As(Box::new(p.map_idents(f)), f(n)),
412        }
413    }
414}
415
416impl<I> CmdArg<I> {
417    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> CmdArg<J> {
418        CmdArg {
419            opts: self
420                .opts
421                .iter()
422                .map(|(l, e)| (l.clone(), e.map_idents(f)))
423                .collect(),
424            arg: self.arg.map_idents(f),
425        }
426    }
427}
428
429impl<I> IText<I> {
430    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> IText<J> {
431        match self {
432            IText::Text(s) => IText::Text(s.clone()),
433            IText::CodeText(s) => IText::CodeText(s.clone()),
434            IText::Cmd { name, span, args } => IText::Cmd {
435                name: f(name),
436                span: *span,
437                args: args.iter().map(|a| a.map_idents(f)).collect(),
438            },
439            IText::Embed { expr, span } => IText::Embed {
440                expr: expr.map_idents(f),
441                span: *span,
442            },
443            IText::EmbedMath { elems, span } => IText::EmbedMath {
444                elems: Rc::new(elems.iter().map(|e| e.map_idents(f)).collect()),
445                span: *span,
446            },
447        }
448    }
449}
450
451impl<I> BText<I> {
452    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> BText<J> {
453        match self {
454            BText::Cmd { name, span, args } => BText::Cmd {
455                name: f(name),
456                span: *span,
457                args: args.iter().map(|a| a.map_idents(f)).collect(),
458            },
459            BText::Embed { expr, span } => BText::Embed {
460                expr: expr.map_idents(f),
461                span: *span,
462            },
463        }
464    }
465}
466
467impl<I> MathElem<I> {
468    pub fn map_idents<J>(&self, f: &impl Fn(&I) -> J) -> MathElem<J> {
469        match self {
470            MathElem::Chars(s) => MathElem::Chars(s.clone()),
471            MathElem::Group(es) => MathElem::Group(es.iter().map(|e| e.map_idents(f)).collect()),
472            MathElem::Sub(b, s) => MathElem::Sub(
473                Box::new(b.map_idents(f)),
474                s.iter().map(|e| e.map_idents(f)).collect(),
475            ),
476            MathElem::Sup(b, s) => MathElem::Sup(
477                Box::new(b.map_idents(f)),
478                s.iter().map(|e| e.map_idents(f)).collect(),
479            ),
480            MathElem::Primes(b, n) => MathElem::Primes(Box::new(b.map_idents(f)), *n),
481            MathElem::Cmd { name, span, args } => MathElem::Cmd {
482                name: f(name),
483                span: *span,
484                args: args.iter().map(|a| a.map_idents(f)).collect(),
485            },
486            MathElem::Embed { expr, span } => MathElem::Embed {
487                expr: expr.map_idents(f),
488                span: *span,
489            },
490        }
491    }
492}
493
494/// **The compile membrane**: resolve every interned `Symbol` in a branded,
495/// elaborated tree back to its text, producing the lifetime-free
496/// `Ast<String>` that `crate::compile` lowers and the whole runtime works
497/// on — nothing downstream ever sees a `Symbol` or the `'s` it carries (see
498/// the module doc comment).
499///
500/// [`Ast::map_idents`] rebuilds the same tree with each symbol replaced by
501/// precisely the string it was interned from. Cost is one deep copy per
502/// compile — not per fixpoint trial, since the trials re-run the resulting
503/// compiled closure.
504pub fn debrand(ast: &branded::Ast<'_>, store: &crate::symbol::SymbolStore) -> Ast {
505    ast.map_idents(&|sym| store.resolve(*sym).to_string())
506}