Skip to main content

rustyfi_syntax/
cst.rs

1//! The SATySFi 0.0.6 surface grammar (a subset of the v0.0.6 `parser.mly`),
2//! parsed with syan derives over the SATySFi token atoms.
3//!
4//! Application and the `nxlor`..`nxrtimes` binary-operator levels are
5//! left-recursive in the Menhir grammar; here they are head-plus-arguments
6//! `Vec`s, folded left during elaboration — recursive descent must never see
7//! left recursion. All ten binop precedence levels are flattened into one
8//! `OpChain`, deferring precedence/associativity to the elaborator — a
9//! deliberate deviation from `parser.mly`'s *structure*, not its *token set*
10//! (every operator it accepts is accepted here too).
11//!
12//! **`let-inline`/`let-block` are top-level-only**, matching `parser.mly`:
13//! `LETHORZ`/`LETVERT` only appear in `nxtoplevel`/`nxstruct` (via
14//! `nxhorzdec`/`nxvertdec`), never in `nxletsub` — unlike `let`/`let-rec` they
15//! have no local (`in`-bodied) form, only as one of a file's leading
16//! [`TopBinding`]s.
17
18use crate::leaf::*;
19use crate::span::Span;
20use newer_type::implement;
21use syan::parse::{Parse, Unparse};
22
23/// A whole `.saty`/`.satyh` file: headers, top-level bindings, `in`, the
24/// document expression (`main`/`nxtoplevel`/`nxtopsubseq` in parser.mly).
25///
26/// A `.satyh` library is just headers + bindings + `EOI`, no `in body`
27/// (`nxtopsubseq`'s bare `EOI` alternative) — hence `body` is optional:
28/// `in_kw` present implies `body` present, checked at elaboration, not here.
29#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
30pub struct File {
31    pub headers: Vec<Header>,
32    pub prelude: Vec<TopBinding>,
33    /// Required whenever `body` is present (checked at elaboration).
34    pub in_kw: Option<KwIn>,
35    /// Absent for a library file (`nxtopsubseq`'s bare `EOI` case).
36    pub body: Option<ast::Expr>,
37    pub eoi: EoiTok,
38}
39
40/// `@require:` / `@import:` / `@stage:` header element.
41#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
42pub enum Header {
43    /// Accepted and currently ignored (driven by the loader crate).
44    Require(HeaderRequireTok),
45    /// Accepted and currently ignored (driven by the loader crate).
46    Import(HeaderImportTok),
47    /// `@stage: persistent` / `@stage: 0` / `@stage: 1` — the stage EVERY
48    /// binding in this file is written at (0.1 says the same thing per
49    /// binding instead, see [`TopStage`]). Unlike `Require`/`Import`, this
50    /// one is HONOURED: `elaborate.rs` wraps every one of that file's
51    /// bindings in `Ast::StageScope`, so a stage-0 library may use `&(…)` and
52    /// the document that requires it may not.
53    Stage(HeaderStageTok),
54}
55
56/// A binding-position NAME: a plain variable, or `( ‹op› )` — a
57/// parenthesized (possibly user-defined) operator name (`OpNameTok`), e.g.
58/// `let (+++>) = ..`. Upstream's `var` nonterminal folds `VAR` and `LPAREN
59/// binop RPAREN` into one production; this is that nonterminal, reused by
60/// [`TopLet::name`], [`ast::Expr::LetIn`]'s `name`, [`ast::RecBinding::name`],
61/// and [`SigItem::Val`]'s `name` — the four binding positions upstream admits
62/// it in. `.name`/`.span` mirror `VarTok`'s own public fields exactly, so an
63/// `elaborate.rs`/`typecheck.rs` callsite reading `foo.name.name`/
64/// `foo.name.span` works against either type. See also
65/// [`ast::Atomic::OpRef`], the matching atomic-expression form.
66#[derive(Debug, Clone, PartialEq)]
67pub struct BindName {
68    pub name: String,
69    pub span: Span,
70    repr: BindNameRepr,
71}
72
73/// [`BindName`]'s two surface forms — kept as a separate, non-`pub`-facing
74/// enum purely so `#[derive(Parse, Unparse)]` can pick between them;
75/// [`BindName`] itself is hand-written so it can additionally expose the
76/// precomputed `name`/`span` fields.
77#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
78enum BindNameRepr {
79    Op(OpNameTok),
80    Var(VarTok),
81}
82
83impl Parse<crate::token::Atom> for BindName {
84    type Error = syan::error::ParseError<crate::span::Span>;
85
86    fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
87        stream: &mut S,
88    ) -> Result<Self, Self::Error> {
89        let repr = BindNameRepr::parse_stream(stream)?;
90        let (name, span) = match &repr {
91            BindNameRepr::Op(op) => (op.name.clone(), op.span),
92            BindNameRepr::Var(v) => (v.name.clone(), v.span),
93        };
94        Ok(BindName { name, span, repr })
95    }
96}
97
98impl Unparse<crate::token::Atom> for BindName {
99    fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
100        &self,
101        sink: &mut S,
102    ) -> Result<(), S::Error> {
103        self.repr.unparse(sink)
104    }
105}
106
107impl From<VarTok> for BindName {
108    /// Synthesizes a binding name from a bare variable token; used only by
109    /// the 0.1 lowering (`v1/lower.rs`), which builds synthetic 0.0.6 CST.
110    /// Adds no parse production, so this doesn't breach the "frozen"
111    /// 0.0.6-grammar contract (`cst_v1.rs`'s module doc).
112    fn from(v: VarTok) -> BindName {
113        BindName {
114            name: v.name.clone(),
115            span: v.span,
116            repr: BindNameRepr::Var(v),
117        }
118    }
119}
120
121#[cfg(test)]
122mod bind_name_tests {
123    use super::*;
124
125    #[test]
126    fn from_var_tok_preserves_name_and_span() {
127        let v = VarTok {
128            name: "foo".to_string(),
129            span: Span::default(),
130        };
131        let bn: BindName = v.clone().into();
132        assert_eq!(bn.name, v.name);
133        assert_eq!(bn.span, v.span);
134    }
135}
136
137/// A top-level non-recursive binding: `let name param* = expr`. `params` is
138/// `Vec<ast::PatBot>` (not merely `Vec<VarTok>`) — `nxnonrecdec`'s `argpart`
139/// is `patbot*` upstream too, matching [`ast::RecBinding`]'s field of the same
140/// name. Elaborated by the same `rec_clause_value` helper `RecBinding` uses
141/// (`elaborate.rs`), with no multi-clause `extra`.
142#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
143pub struct TopLet {
144    pub let_kw: KwLet,
145    /// The stage this ONE binding is written at, when it is not the default
146    /// (see [`TopStage`]). 0.0.6 source never sets it — 0.0.6 declares a
147    /// stage per FILE, with a `@stage:` header — so it parses as `None` for
148    /// every 0.0.6 program; it exists because SATySFi **0.1** declares the
149    /// stage per BINDING (`val ~x = e`, `parser_v1.mly:417-421`) and
150    /// `v1/lower.rs` lowers 0.1 binds into this very node. Carrying the
151    /// stage ON the binding (rather than in a side table keyed by prelude
152    /// index, which is how the 0.0.6 `@stage:` header reaches
153    /// `elaborate.rs`) is what lets it survive the loader's prelude merge,
154    /// module nesting and cross-version splicing unharmed: an index-keyed
155    /// map cannot name a binding *inside* a `module … = struct … end`, and
156    /// every 0.1 `val` is inside one.
157    pub stage: Option<TopStage>,
158    pub name: BindName,
159    /// Optional `: ty` type ascription (`let f : ty x = e`, `let x : ty = e`),
160    /// upstream's `patbotwithann` — parse-and-ignore, exactly like
161    /// [`ast::RecBinding::ascription`] (this untyped elaborator has nothing to
162    /// check it against). Sits before `params`, matching `patbotwithann
163    /// argpart`.
164    pub ascription: Option<ast::RecAscription>,
165    /// The `|` upstream's `nonrecdecargpart` allows between the name (or its
166    /// ascription) and the argument list — `let f : τ | x = e` and
167    /// `let f | x = e`, `parser.mly:610-614`. Unlike `let-rec`'s, a non-rec
168    /// `|` introduces NO further clauses (`nonrecdecargpart` has no
169    /// `nxrecdecpar` tail): it is purely a separator, so nothing downstream
170    /// reads this field — it exists to make verbatim upstream source parse,
171    /// exactly like [`ast::RecBinding::leading_bar`].
172    ///
173    /// Real source writes it: `azmath`'s `util.satyh` opens with
174    /// `let math-in-math : math-class -> (context -> math) -> math`
175    /// `| mcls embedf = ..`, and without this the whole file failed at its
176    /// first binding — the package's ONLY blocker, in both the 0.0.6 and
177    /// the cross-version arm.
178    pub leading_bar: Option<BarTok>,
179    pub params: Vec<ast::Param>,
180    pub eq: DefEqTok,
181    pub value: ast::Expr,
182}
183
184/// A binding's own stage qualifier: `~` (stage 0) or `persistent ~`
185/// (persistent stage), the prefix SATySFi 0.1 writes between `val` and the
186/// bound name (`parser_v1.mly:417-421`, `UTBindValue(Stage0 |
187/// Persistent0, _)`; the absent prefix is `Stage1`, the document stage).
188///
189/// Spelled with tokens rather than a `rustyfi_lang::types::Stage` because
190/// this crate is the syntax layer and knows nothing of the type layer;
191/// `elaborate.rs` maps the pair to a `Stage` (`top_let_stage`).
192///
193/// `persistent` is a 0.1-only keyword (`lexer.rs`'s version-gated table), so
194/// under 0.0.6 the `Some(_)` shape is unreachable through the `persistent`
195/// spelling and reachable only as a bare `let ~x = e` — syntax upstream
196/// 0.0.6 does not have (its `EXACT_TILDE` is a splice operand prefix,
197/// `v0.0.6 parser.mly:797`, or macro syntax, `:608`/`:1199` — never a
198/// binding qualifier). PARSING it under 0.0.6 is the usual additive-accept
199/// latitude this shared cst takes; ELABORATING it is not — `elaborate.rs`'s
200/// `binding_stage` refuses a stage qualifier on 0.0.6-authored input with a
201/// version error, so no 0.0.6 file can quietly acquire a per-binding stage.
202#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
203pub struct TopStage {
204    pub persistent: Option<KwPersistent>,
205    pub tilde: ExactTildeTok,
206}
207
208/// One top-level declaration (`nxtoplevel`/`nxstruct`'s per-declaration
209/// alternatives). `LetInline`/`LetBlock` only exist here — see the module
210/// doc comment.
211#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
212pub enum TopBinding {
213    /// `let-rec name param* = expr (and name param* = expr)*`
214    LetRec {
215        kw: KwLetRec,
216        /// See [`TopLet::stage`] — upstream 0.1 puts the qualifier before the
217        /// WHOLE `bind_value`, and `bind_value` covers `rec`/`mutable`/
218        /// `inline`/`block`/`math` as well as the plain non-recursive form
219        /// (`dev-0-1-0 parser.mly:417-421` → `:581-593`), so every binding
220        /// shape below carries one too. The stage applies to each `and`
221        /// clause of this one `let-rec`, exactly as it does upstream (one
222        /// `UTBindValue(stage, UTRec(binds))` for the whole chain).
223        stage: Option<TopStage>,
224        first: ast::RecBinding,
225        ands: Vec<ast::AndBinding>,
226    },
227    /// `let name param* = expr`
228    Let(TopLet),
229    /// `let pat = expr` — a top-level (or `struct`-level) DESTRUCTURING `let`
230    /// whose target is a general pattern, not a plain variable (e.g.
231    /// `satysfi-xpath`'s `let (ulim1, ulim2) = (0. -. eps, 1. +. eps)`). The
232    /// `struct`-body twin of [`ast::Expr::LetPatternIn`], and — for the same
233    /// reason it sits after `Expr::LetIn` — **must stay after `Let`**: an
234    /// ordinary `let x = e` parses through `Let` first (its `name: BindName`
235    /// only accepts a bare var/op), leaving this to match only a non-variable
236    /// pattern target. That "leaving" is enforced by the target's own type
237    /// ([`PatNonVarErased`]) rather than left to variant order, so the two
238    /// alternatives are disjoint and neither re-parses the other's `value`.
239    /// No `argpart` (curried params after the pattern), as upstream's
240    /// `nxnonrecdec` never uses one here.
241    LetPattern {
242        let_kw: KwLet,
243        pat: PatNonVarErased,
244        eq: DefEqTok,
245        value: ast::Expr,
246    },
247    /// `[ctxvar] let-inline \cmd param* = expr` (`nxhorzdec`; each `param`
248    /// is upstream's `arg` — a full patbot, or a `?:`-marked variable, see
249    /// [`ast::Param`]'s doc comment — `parser.mly:622-624`).
250    LetInline {
251        kw: KwLetHorz,
252        /// See [`TopBinding::LetRec::stage`].
253        stage: Option<TopStage>,
254        ctx: Option<VarTok>,
255        cmd: HorzCmdTok,
256        params: Vec<ast::Param>,
257        eq: DefEqTok,
258        value: ast::Expr,
259    },
260    /// `[ctxvar] let-block +cmd param* = expr` (`nxvertdec`).
261    LetBlock {
262        kw: KwLetVert,
263        /// See [`TopBinding::LetRec::stage`].
264        stage: Option<TopStage>,
265        ctx: Option<VarTok>,
266        cmd: VertCmdTok,
267        params: Vec<ast::Param>,
268        eq: DefEqTok,
269        value: ast::Expr,
270    },
271    /// `let-math \cmd param* = expr` (`nxmathdec`, `parser.mly:586-591`).
272    /// **No leading context variable** — unlike `LetInline`/`LetBlock`,
273    /// upstream's `nxmathdec` curries straight from the command name into
274    /// `cmdarglst*` with no `ctxvar` slot at all (`UTLambdaMath`, not
275    /// `UTLambdaHorz`/`UTLambdaVert`), since a math command's own type
276    /// (`math-cmd`) carries no implicit `context` argument the way
277    /// `inline-cmd`/`block-cmd` do. `cmd` reuses the plain `HorzCmdTok`
278    /// token (upstream's `nxmathdec` also reuses `HORZCMD`, not a
279    /// math-specific token — `\frac` here is lexed exactly like `\frac` in
280    /// `let-inline`; the two forms are told apart only by which keyword
281    /// introduced them).
282    LetMath {
283        kw: KwLetMath,
284        /// See [`TopBinding::LetRec::stage`].
285        stage: Option<TopStage>,
286        cmd: HorzCmdTok,
287        params: Vec<ast::Param>,
288        eq: DefEqTok,
289        value: ast::Expr,
290    },
291    /// `type name = [|] Ctor [of ty] (| Ctor [of ty])*` (a variant
292    /// declaration) or `type name = ty` (a transparent type *synonym*) —
293    /// `nxvariantdec`; see [`TypeDeclBody`] for how the two are told apart.
294    Type(TypeDecl),
295    /// `let-mutable name <- expr` (top-level; `nxtoplevel`/`nxstruct`'s
296    /// `LETMUTABLE` case — the local, `in`-bodied form is
297    /// [`ast::Expr::LetMutableIn`]).
298    LetMutable {
299        kw: KwLetMutable,
300        /// See [`TopBinding::LetRec::stage`].
301        stage: Option<TopStage>,
302        name: VarTok,
303        arrow: OverwriteEqTok,
304        value: ast::Expr,
305    },
306    /// `module Name [: sig ... end] = struct ... end` (`nxtoplevel`'s
307    /// `MODULE` case).
308    Module {
309        kw: KwModule,
310        name: CtorTok,
311        sig: Option<SigAnnot>,
312        eq: DefEqTok,
313        struct_kw: KwStruct,
314        decls: Vec<StructDecl>,
315        end_kw: KwEnd,
316    },
317    /// `open Name` (`nxtoplevel`'s `OPEN` case; the local, `in`-bodied form
318    /// is [`ast::Expr::OpenIn`]).
319    Open { kw: KwOpen, name: CtorTok },
320}
321
322/// One declaration inside a `module ... = struct ... end` body (`nxstruct`).
323/// `nxstruct`'s alternatives are a strict subset of `nxtoplevel`'s (every
324/// form it has, [`TopBinding`] also has, once `Module`/`Open` are added), so
325/// this simply re-parses a [`TopBinding`] — but *not* by naming `TopBinding`
326/// as a field type directly: `TopBinding` lives **outside** the
327/// `#[recurse]` module, so `TopBinding -> Module -> Vec<StructDecl> ->
328/// TopBinding` would be a self-recursive cycle through a plain
329/// `#[derive(Parse)]`, which (without the `#[recurse]` engine to back it)
330/// is an `E0275` hazard (an unbounded recursive trait-bound obligation).
331/// Hand-writing `Parse`/`Unparse` here — the same trick as
332/// [`ExprErased`] et al. — sidesteps that: the impl has no recursive
333/// where-bound for the compiler to try to satisfy, it just calls
334/// `TopBinding::parse` through the stream-erasing adapter at runtime.
335#[derive(Debug, Clone, PartialEq)]
336pub struct StructDecl(pub Box<TopBinding>);
337
338impl Parse<crate::token::Atom> for StructDecl {
339    type Error = syan::error::ParseError<crate::span::Span>;
340
341    fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
342        stream: &mut S,
343    ) -> Result<Self, Self::Error> {
344        let value = <TopBinding as Parse<_>>::parse_stream(stream)?;
345        Ok(StructDecl(Box::new(value)))
346    }
347}
348
349impl Unparse<crate::token::Atom> for StructDecl {
350    fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
351        &self,
352        sink: &mut S,
353    ) -> Result<(), S::Error> {
354        self.0.unparse(sink)
355    }
356}
357
358/// `: sig ... end` (`nxsigopt`/`nxsigelem`, drastically simplified — see
359/// [`SigItem`]).
360#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
361pub struct SigAnnot {
362    pub colon: ColonTok,
363    pub sig_kw: KwSig,
364    pub items: Vec<SigItem>,
365    pub end_kw: KwEnd,
366}
367
368/// One `nxsigelem`. Type parameters/type synonyms on `type` items are not
369/// supported — such input is rejected with a parse error. Each item may
370/// carry a trailing `constrnts` (`parser.mly:526-530`) — see
371/// [`SigConstraint`].
372#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
373pub enum SigItem {
374    /// `val \cmd : ty` / `val +cmd : ty`.
375    ValHorzCmd {
376        kw: KwVal,
377        name: HorzCmdTok,
378        colon: ColonTok,
379        ty: ast::TypeExpr,
380        constraints: Vec<SigConstraint>,
381    },
382    ValVertCmd {
383        kw: KwVal,
384        name: VertCmdTok,
385        colon: ColonTok,
386        ty: ast::TypeExpr,
387        constraints: Vec<SigConstraint>,
388    },
389    /// `val name : ty` / `val ( ‹op› ) : ty`.
390    Val {
391        kw: KwVal,
392        name: BindName,
393        colon: ColonTok,
394        ty: ast::TypeExpr,
395        constraints: Vec<SigConstraint>,
396    },
397    /// `direct \cmd : ty` / `direct +cmd : ty`.
398    DirectHorzCmd {
399        kw: KwDirect,
400        name: HorzCmdTok,
401        colon: ColonTok,
402        ty: ast::TypeExpr,
403        constraints: Vec<SigConstraint>,
404    },
405    DirectVertCmd {
406        kw: KwDirect,
407        name: VertCmdTok,
408        colon: ColonTok,
409        ty: ast::TypeExpr,
410        constraints: Vec<SigConstraint>,
411    },
412    /// `type tyvar* name` (no synonym).
413    Type {
414        kw: KwType,
415        tyvars: Vec<TypeVarTok>,
416        name: VarTok,
417        constraints: Vec<SigConstraint>,
418    },
419}
420
421/// One `constrnt`: `constraint 'a :: (| l1 : ty1; l2 : ty2; … |)`
422/// (`parser.mly:526-530`), a per-item suffix binding *that item's* type
423/// variable to a row-kind obligation — **not** a standalone `SigItem` (a
424/// reader expecting the latter should see this doc: upstream attaches
425/// `constrnts` to `SigValue`/`SigDirect`/`SigType` directly, so the suffix
426/// form here is the faithful one and avoids an ambiguous "which item does
427/// this constrain?").
428#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
429pub struct SigConstraint {
430    pub kw: ConstraintTok,
431    pub tyvar: TypeVarTok,
432    pub cons: ConsTok,
433    pub kind: RecordKind,
434}
435
436/// `kxtop`: `(| l1 : ty1; … |)`, a record-kind bound — "the constrained
437/// type variable must be a record containing at least these labels"
438/// (upstream `MRecordKind`; lowers to this port's `Kind::Record` row
439/// obligation, presence-only — see `typecheck.rs`).
440#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
441pub struct RecordKind {
442    pub rec: RecordGroup<()>,
443    #[group(self.rec)]
444    pub fields: Vec<RecordKindField>,
445}
446
447/// One `l : ty;` field of a [`RecordKind`] (`txrecord`,
448/// `parser.mly:962-965`). The field *type* is parsed but currently dropped
449/// during lowering (only the label is kept, matching `Kind::Record`'s
450/// label-only representation) — a documented limitation, not a
451/// grammar gap.
452#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
453pub struct RecordKindField {
454    pub name: VarTok,
455    pub colon: ColonTok,
456    pub ty: ast::TypeExpr,
457    pub semi: Option<ListPunctTok>,
458}
459
460/// A `type` declaration, optionally with mutual (`and`) recursion between
461/// several type declarations (`parser.mly`'s `nxvariantdec` `and`-chain, e.g.
462/// `satysfi-base`'s `stream.satyg`: `type 'a state = … and 'a u = ('a state)
463/// Promise.t`). The head clause is `kw`..`body`; every further `and`-clause is
464/// an [`AndTypeClause`]. All clauses in one chain are mutually visible — they
465/// lower to consecutive `UserTypeDecl`/`UserSynonymDecl`s, exactly the shape
466/// the 0.1 lowering (`v1/lower.rs`) already produces for `type … and …`, which
467/// the typechecker resolves with the same forward-reference tolerance.
468#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
469pub struct TypeDecl {
470    pub kw: KwType,
471    pub tyvars: Vec<TypeVarTok>,
472    pub name: VarTok,
473    pub eq: DefEqTok,
474    pub body: TypeDeclBody,
475    pub ands: Vec<AndTypeClause>,
476}
477
478/// One `and 'a name = body` continuation of a [`TypeDecl`]'s mutual-recursion
479/// chain (mirrors [`ast::AndBinding`] for `let`-rec).
480#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
481pub struct AndTypeClause {
482    pub and_kw: KwAnd,
483    pub tyvars: Vec<TypeVarTok>,
484    pub name: VarTok,
485    pub eq: DefEqTok,
486    pub body: TypeDeclBody,
487}
488
489/// The right-hand side of a `type` declaration: either a variant's
490/// constructor list, or (transparently) a type-synonym body. Trying the
491/// variant shape first is unambiguous: a type name is always a bare `VAR`
492/// in this grammar (`txbot`), so no type expression can ever start with the
493/// `BarTok`/`CtorTok` a variant list requires — any input that isn't a
494/// variant list falls through to `Synonym` cleanly, exactly like upstream's
495/// `nxvariantdec` telling `variants` (always `CONSTRUCTOR`-headed) apart
496/// from `txfunc` by lookahead.
497#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
498pub enum TypeDeclBody {
499    /// `[|] Ctor [of ty] (| Ctor [of ty])*`.
500    Variant {
501        leading_bar: Option<BarTok>,
502        first: VariantDef,
503        rest: Vec<BarVariantDef>,
504    },
505    /// `ty` — a transparent type synonym, e.g. `type point = length *
506    /// length` (`typechecker.ml`'s `SynonymType`/`add_synonym`: the name is
507    /// replaced by this body wherever it appears in type position, so it
508    /// never reaches unification itself). `TypeDecl::tyvars` is parsed the
509    /// same way for a synonym as for a variant (`type 'a foo = ..`), but
510    /// only the zero-param case can actually be *referenced* anywhere today
511    /// — this grammar has no applied-type-constructor syntax (`TypeAtom`'s
512    /// doc comment) to spell a synonym's argument at a use site.
513    Synonym(ast::TypeExpr),
514}
515
516/// One `Ctor [of ty]` variant definition.
517#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
518pub struct VariantDef {
519    pub ctor: CtorTok,
520    pub of_ty: Option<OfType>,
521}
522
523/// The `of ty` suffix of a variant definition.
524#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
525pub struct OfType {
526    pub of_kw: KwOf,
527    pub ty: ast::TypeExpr,
528}
529
530/// A `| Ctor [of ty]` continuation of a variant list.
531#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
532pub struct BarVariantDef {
533    pub bar: BarTok,
534    pub def: VariantDef,
535}
536
537/// Recursion-edge eraser types.
538///
539/// These exist purely for **compile-time sanity**. The `#[recurse]` engine's
540/// generated code is monomorphized per concrete parse-stream type, and syan's
541/// backtracking wraps the stream in a fresh `Dup<&mut _, _>` layer at every
542/// enum/`Vec`/`Option` boundary — so an engine covering a large SCC gets
543/// re-instantiated combinatorially (measured on the naive transcription of
544/// this grammar: rustc >16 minutes, >10 GB, and >7 minutes for `cargo check`
545/// alone). Routing every recursion edge *except the roots' own self-loops*
546/// through these hand-written leaves keeps each SCC a singleton (`Expr`,
547/// `PatBot`, `TypeExpr`), so each engine stays tiny, and parses the wrapped
548/// grammar directly: `parse_stream` reborrows, so one stream type serves
549/// the whole descent. Defined *outside* the `#[recurse]` module so the
550/// macro treats them as opaque leaves (they never appear as cycle edges).
551macro_rules! erased_leaf {
552    ($($(#[$doc:meta])* $name:ident => $target:ty;)*) => {
553        $(
554            $(#[$doc])*
555            #[implement(newer_type_std::ops::Deref)]
556            #[derive(Debug, Clone, PartialEq)]
557            pub struct $name(pub Box<$target>);
558
559            impl Parse<crate::token::Atom> for $name {
560                type Error = syan::error::ParseError<crate::span::Span>;
561
562                // No stream erasure: `parse_stream` takes `&mut S` and
563                // recursion REBORROWS, so `S` is a genuine fixed point and the
564                // instantiation set is finite by construction. The wrapper
565                // only boxes the value.
566                fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
567                    stream: &mut S,
568                ) -> Result<Self, Self::Error> {
569                    let value = <$target as Parse<_>>::parse_stream(stream)?;
570                    Ok($name(Box::new(value)))
571                }
572            }
573
574            impl Unparse<crate::token::Atom> for $name {
575                fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
576                    &self,
577                    sink: &mut S,
578                ) -> Result<(), S::Error> {
579                    self.0.unparse(sink)
580                }
581            }
582        )*
583    };
584}
585
586erased_leaf! {
587    /// An [`ast::Expr`] behind a stream-erasing parse (see above).
588    ExprErased => ast::Expr;
589    /// An [`ast::Pattern`] behind a stream-erasing parse (see above).
590    PatErased => ast::Pattern;
591    /// An [`ast::PatBot`] behind a stream-erasing parse (see above). Kept
592    /// separate from [`PatErased`] because a constructor pattern's argument
593    /// is a `patbot`, *not* a full `patas` (`Some x as y` binds `y` to the
594    /// whole value, not to `x`).
595    PatBotErased => ast::PatBot;
596    /// An [`ast::TypeExpr`] behind a stream-erasing parse (see above).
597    TyErased => ast::TypeExpr;
598    /// An [`ast::MathElemCst`] behind a stream-erasing parse (see above).
599    /// Unlike the other three erasers this one isn't bridging a *self*-loop
600    /// of its target's own SCC — `MathElemCst` turns out to have no direct
601    /// self-loop at all (see its doc comment) — but every nested reference
602    /// to "one math element" still goes through here, for the same
603    /// monomorphize-once reason.
604    MathErased => ast::MathElemCst;
605    /// An [`ast::AppArg`] behind a stream-erasing parse (see above). Bridges
606    /// a command tail's argument chain (`CmdTail::Args`, below) into
607    /// `AppArg`'s own parser *without* a direct field reference: `CmdTail` is
608    /// reached from `Expr`'s SCC via `Atomic::InlineText`/`BlockText` ->
609    /// `InlineElem`/`BlockElem` -> `CmdTail`, so a *direct* `AppArg` field
610    /// here would close a brand-new cycle back into `Atomic`
611    /// (`AppArg::Atom.atom: Atomic`) entirely through non-root types — the
612    /// exact "sub-cycle running entirely through non-root types" shape the
613    /// `#[recurse]` engine rejects (see `PatCons`'s doc comment for the same
614    /// hazard). Routing through this eraser keeps `CmdTail` a DAG leaf, same
615    /// as every other cross-reference here.
616    AppArgErased => ast::AppArg;
617}
618
619impl ast::Pattern {
620    /// Whether this pattern is a lone variable — `x`, and nothing else: no
621    /// `:: rest`, no `as y`.
622    ///
623    /// This is the predicate that separates a DESTRUCTURING `let` from an
624    /// ordinary one, and it exists because the two must not overlap; see
625    /// [`PatNonVarErased`] for what the overlap used to cost.
626    pub fn is_bare_var(&self) -> bool {
627        self.as_clause.is_none()
628            && self.head.tail.is_empty()
629            && matches!(self.head.head, ast::PatBot::Var(_))
630    }
631}
632
633/// An [`ast::Pattern`] that is **not** a bare variable — the target of a
634/// destructuring `let`, and the left factor that keeps the two `let` forms
635/// from backtracking over each other.
636///
637/// # Why this type exists
638///
639/// `let ‹target› = ‹value› in ‹body›` is written twice in this grammar, as
640/// [`ast::Expr::LetIn`] (target a [`BindName`], plus curried `params`) and
641/// [`ast::Expr::LetPatternIn`] (target a full pattern) — and at the top level
642/// again as [`TopBinding::Let`]/[`TopBinding::LetPattern`]. Upstream writes
643/// each of those ONCE: 0.0.6's `nxnonrecdec` is the single production
644/// `patbot nonrecdecargpart DEFEQ nxlet` (`parser.mly:652-656`), and 0.1
645/// spells the second target `pattern_non_var` (`parser_v1.mly:796`) — a
646/// nonterminal that, as its name says, cannot derive a bare variable.
647///
648/// Splitting one production into two whose common prefix is not factored is
649/// what made this port's worst backtracking blow-up. `LetIn` is tried first
650/// and subsumes every bare-variable target, so `LetPatternIn` could never
651/// *succeed* on one — but it was still *tried*, and trying it re-parses
652/// `value` and `body` in full. With `body` being the rest of the file, each
653/// enclosing `let` doubled the cost of every failure below it: measured over
654/// a chain of `let vN = N in` ending in a broken `let`, 1,115 serves at 3,
655/// 9,459 at 6, 76,211 at 9, 610,227 at 12, 4,882,355 at 15 — ×2.000 per
656/// `let`, in both grammars. A 129-line document with a missing `in`
657/// sixteen `let`s deep cost 17.9M serves and was reported as
658/// [`crate::ParseFailureKind::GaveUp`] rather than as the one-word syntax
659/// error it is.
660///
661/// Requiring a non-variable target here restores upstream's disjointness: the
662/// two alternatives now differ at the token *after* `let`, so the choice is
663/// made on the target alone and no tail is ever parsed twice. The same chain
664/// costs 374, 677, 980, 1,283, 1,586 — linear, ~17 serves per atom.
665///
666/// # Why it loses nothing
667///
668/// `LetPatternIn` on a bare-variable target is unreachable-on-success today,
669/// and provably so rather than by inspection: if it matched, the token after
670/// the name is `=`, so `LetIn`'s greedy `params` is empty (no [`ast::Param`]
671/// begins with `=`), its optional `ascription`/`leading_bar` are absent, and
672/// its `value`/`in_kw`/`body` are the identical grammar — so `LetIn`, tried
673/// first, has already succeeded and this variant is never reached. Only
674/// `PatBot::Var` is excluded: `_`, `Ctor`, `(a, b)`, `[…]`, `x :: xs` and
675/// `x as y` all still take this route, because [`BindName`] accepts none of
676/// them.
677#[implement(newer_type_std::ops::Deref)]
678#[derive(Debug, Clone, PartialEq)]
679pub struct PatNonVarErased(pub Box<ast::Pattern>);
680
681impl Parse<crate::token::Atom> for PatNonVarErased {
682    type Error = syan::error::ParseError<crate::span::Span>;
683
684    fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
685        stream: &mut S,
686    ) -> Result<Self, Self::Error> {
687        let value = <ast::Pattern as Parse<_>>::parse_stream(stream)?;
688        if value.is_bare_var() {
689            // `is_bare_var` guarantees this shape; the match is how the
690            // offending token's span is named, not a second test.
691            let ast::PatBot::Var(v) = &value.head.head else {
692                unreachable!("a bare-variable pattern has a variable head")
693            };
694            // The enclosing variant's checkpoint rolls the consumed pattern
695            // back, exactly as it would for any other field that rejects.
696            return Err(syan::error::ParseError::expected(
697                v.span,
698                "a destructuring pattern (a plain `let x = …` is not one)",
699            ));
700        }
701        Ok(PatNonVarErased(Box::new(value)))
702    }
703}
704
705impl Unparse<crate::token::Atom> for PatNonVarErased {
706    fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
707        &self,
708        sink: &mut S,
709    ) -> Result<(), S::Error> {
710        self.0.unparse(sink)
711    }
712}
713
714/// The recursive expression/pattern/type/text grammar. Program expressions
715/// embed inline/block text (`{…}`, `'<…>`), text embeds commands, and
716/// command arguments re-enter program expressions.
717///
718/// **Recursion structure.** Grammatically this is one big knot, but at the
719/// type level every recursion edge except three self-loops is routed through
720/// the stream-erasing leaf wrappers defined above ([`ExprErased`],
721/// [`PatErased`], [`TyErased`]) — see their doc comment for the measured
722/// compile-time blowup that forced this. The `#[recurse]` macro therefore
723/// sees exactly three singleton SCCs, each a directly self-referential root:
724///
725/// * `Expr` (its own variants' `Box<Expr>` children — `nxlet` nesting like
726///   `if … then if … else …` runs on the engine);
727/// * `PatBot` (`CtorApplied`'s `Box<PatBot>` argument — `Some Some x`);
728/// * `TypeExpr` (`Fun`'s right-recursive `Box<TypeExpr>` codomain).
729///
730/// Every sub-cycle trivially passes through its root. All other nesting
731/// (command arguments, parenthesized/tuple bodies, record/list elements,
732/// match-arm bodies, …) recurses at *runtime* through the erasers'
733/// hand-written `Parse` impls, which is unbounded by construction.
734///
735/// A command's arguments are still represented as one application-chain
736/// `Expr` (`CmdTail::Args`) rather than a dedicated argument list — faithful
737/// to the OCaml AST, where command arguments are a curried `UTApply` chain
738/// anyway. Elaboration flattens that chain back into the argument list.
739#[syan::parse::recurse]
740pub mod ast {
741    use super::super::leaf::*;
742    use syan::parse::{Parse, Unparse};
743
744    /// `nxlet`: a let/if/match/lambda-headed expression, falling through to
745    /// the flattened operator chain (`Ops`, `OpChain`) at the bottom.
746    /// Variant order is parse priority; `Ops` has no distinguishing leading
747    /// keyword, so it must stay last.
748    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
749    pub enum Expr {
750        /// `let-rec name param* = expr (and name param* = expr)* in body`
751        LetRecIn {
752            kw: KwLetRec,
753            first: RecBinding,
754            ands: Vec<AndBinding>,
755            in_kw: KwIn,
756            body: Box<Expr>,
757        },
758        /// `let name param* = expr in body` (`nxletsub`'s `LETNONREC` case;
759        /// the bound TARGET is a plain variable — a general pattern target
760        /// is [`Expr::LetPatternIn`], below — but `param*` is a full
761        /// `patbot*`, matching `parser.mly`'s `nxnonrecdec` (and this port's
762        /// own `TopLet`/`Fun`/`RecBinding`, which already use `PatBot` here
763        /// too): e.g. `hdecoset.satyh`'s `let deco _ _ _ _ = [] in ..`
764        /// (Tier-2 decoration/graphics wave). Lowered by the same
765        /// `elaborate::rec_clause_value` single-clause path `Fun`'s doc
766        /// comment describes.
767        LetIn {
768            kw: KwLet,
769            name: super::BindName,
770            /// Optional `: ty` ascription (`let f : ty x = e in ..`) —
771            /// parse-and-ignore, like [`RecBinding::ascription`].
772            ascription: Option<RecAscription>,
773            /// The `|` of `nonrecdecargpart` — see
774            /// [`super::TopLet::leading_bar`], whose doc comment carries the
775            /// whole story; this is the expression-level twin.
776            leading_bar: Option<BarTok>,
777            params: Vec<Param>,
778            eq: DefEqTok,
779            value: Box<Expr>,
780            in_kw: KwIn,
781            body: Box<Expr>,
782        },
783        /// `let pat = value in body` (`nxnonrecdec`'s zero-additional-
784        /// parameter case: the bound target is a general pattern, not
785        /// merely a variable name — SATySFi's destructuring `let`, e.g.
786        /// `let (_, acc) = pair in acc`, used by the bundled
787        /// `list.satyg`'s `mapi-adjacent`). Kept as a SEPARATE variant from
788        /// [`Expr::LetIn`] above (rather than widening `LetIn`'s `name:
789        /// VarTok` field to a pattern) because `LetIn` additionally curries
790        /// `params` for the ordinary `let f x y = ..` function-definition
791        /// shape, which upstream keys off the bound target being a plain
792        /// variable — the two shapes never overlap in real source (a
793        /// destructuring target is never itself applied to further curried
794        /// parameters). Only the no-`argpart` (no additional curried
795        /// parameters after the pattern) form is implemented —
796        /// `nxnonrecdec`'s `argpart` has no use in the bundled stdlib.
797        ///
798        /// **This variant used to be the port's worst backtracking blow-up.**
799        /// A bare-variable target like `let x = 1 in x` derives through here
800        /// too (`PatBot::Var`), and while [`Expr::LetIn`] is tried first and
801        /// always wins, this one was still *tried* — re-deriving `value` and
802        /// `body` in full, so a failure deep inside a `let … in` chain cost
803        /// ×2 per enclosing `let`. The target is now
804        /// [`super::PatNonVarErased`], which refuses a bare variable outright
805        /// (upstream 0.1 spells the same restriction `pattern_non_var`,
806        /// `parser_v1.mly:796`); the two alternatives are disjoint at the
807        /// token after `let` and the chain is linear. That type's doc comment
808        /// carries the measurements and the proof that nothing is lost.
809        /// **Must still stay after `LetIn`** — variant order no longer
810        /// decides correctness, but it keeps the common case first.
811        LetPatternIn {
812            kw: KwLet,
813            pat: super::PatNonVarErased,
814            eq: DefEqTok,
815            value: Box<Expr>,
816            in_kw: KwIn,
817            body: Box<Expr>,
818        },
819        /// `if cond then a else b` (`nxif`; `else` is never optional in
820        /// this grammar, so there is no dangling-else ambiguity).
821        If {
822            kw: KwIf,
823            cond: Box<Expr>,
824            then_kw: KwThen,
825            then_branch: Box<Expr>,
826            else_kw: KwElse,
827            else_branch: Box<Expr>,
828        },
829        /// `fun x y -> body` (`nxlambda`'s `LAMBDA argpats ARROW nxlor`
830        /// production, `parser.mly:713`). `argpats = list(patbot)`
831        /// upstream — a lambda's parameters are full `patbot`s, not merely
832        /// variables (e.g. the bundled `list.satyg`'s `mapi-adjacent`:
833        /// `fun (i, acc) x leftopt rightopt -> ..`, a tuple-DESTRUCTURING
834        /// first parameter), lowered by `curry_lambda_abstract_pattern` —
835        /// this port's `elaborate::rec_clause_value` (shared with
836        /// multi-clause `let-rec`, which faces the exact same
837        /// arity-preserving pattern-currying problem) reproduces that
838        /// directly, so this field is `PatBot`, matching `RecBinding`'s.
839        Fun {
840            kw: KwFun,
841            params: Vec<PatBot>,
842            arrow: ArrowTok,
843            body: Box<Expr>,
844        },
845        /// `fun ?(l = x, …) p -> body` — a SATySFi 0.1 labeled-optional
846        /// lambda unit (one `?(…)` bundle + one positional param). This is
847        /// an **additive** 0.1 node: 0.0.6 has no `?(…)` param bundle, so it
848        /// is reachable in a 0.0.6 parse only for input that used to be a
849        /// parse error (a leading `?` cannot begin `Fun`'s `Vec<PatBot>`),
850        /// where `elaborate` rejects it under a V0_0 [`crate::version`]
851        /// gate. The V0_1 pipeline reaches it by lowering a `cst_v1` param
852        /// bundle (multi-unit lambdas lower to a nested `FunRows`/`Fun`
853        /// chain). Placed right after [`Expr::Fun`] so a plain `fun x -> …`
854        /// still matches `Fun` first (its `?`-headed `opts` cannot begin a
855        /// `PatBot`, so `Fun` cleanly backtracks here for a bundled unit).
856        FunRows {
857            kw: KwFun,
858            opts: CstOptBinders,
859            param: PatBot,
860            arrow: ArrowTok,
861            body: Box<Expr>,
862        },
863        /// `match scrutinee with [|] pat [when g] -> body (| pat [when g] -> body)*`
864        Match {
865            kw: KwMatch,
866            scrutinee: Box<Expr>,
867            with_kw: KwWith,
868            leading_bar: Option<BarTok>,
869            first: MatchArm,
870            rest: Vec<BarArm>,
871        },
872        /// `let-mutable name <- init in body` (`nxletsub`'s `LETMUTABLE`
873        /// case; `init`/`body` are both `nxlet` in `parser.mly`, simplified
874        /// here to a direct `Expr` self-loop like `LetIn`).
875        LetMutableIn {
876            kw: KwLetMutable,
877            name: VarTok,
878            arrow: OverwriteEqTok,
879            init: Box<Expr>,
880            in_kw: KwIn,
881            body: Box<Expr>,
882        },
883        /// `let-math \cmd param* = expr in body` (`nxletsub`'s `LETMATH`
884        /// case, `parser.mly:688` — upstream's ONLY command binding with an
885        /// expression-level `in` form; `LETHORZ`/`LETVERT` stay
886        /// top-level-only, see the module doc comment on
887        /// [`super::TopBinding::LetInline`]/`LetBlock`). Same shape as
888        /// [`super::TopBinding::LetMath`] — no leading context variable,
889        /// `cmd` reuses the plain `HorzCmdTok` token — plus the `in body`
890        /// suffix; `Box<Expr>` self-loops on the recurse root like `LetIn`.
891        LetMathIn {
892            kw: KwLetMath,
893            cmd: HorzCmdTok,
894            params: Vec<Param>,
895            eq: DefEqTok,
896            value: Box<Expr>,
897            in_kw: KwIn,
898            body: Box<Expr>,
899        },
900        /// `open Name in body` (`nxletsub`'s `OPEN` case).
901        OpenIn {
902            kw: KwOpen,
903            name: CtorTok,
904            in_kw: KwIn,
905            body: Box<Expr>,
906        },
907        /// `while cond do body` (`nxwhl`; `body` is `nxwhl` itself in
908        /// `parser.mly`, i.e. right-nested `while`s — simplified here to a
909        /// plain `Expr`).
910        WhileDo {
911            kw: KwWhile,
912            cond: Box<Expr>,
913            do_kw: KwDo,
914            body: Box<Expr>,
915        },
916        /// `name <- value` (`nxlambda`'s `OVERWRITEEQ` case). Starts with a
917        /// bare `VarTok`, which is also how `Ops` can start (`x` alone) —
918        /// **must** stay before `Ops` so backtracking tries the `<-` shape
919        /// first. `value` is `nxlor` in `parser.mly`; routed through
920        /// `ExprErased` here rather than mirrored precisely, both to keep
921        /// `Expr` a singleton SCC and because this is already a `Var`-headed
922        /// alternative sitting awkwardly among the keyword-headed ones.
923        Overwrite {
924            name: VarTok,
925            arrow: OverwriteEqTok,
926            value: super::ExprErased,
927        },
928        /// The flattened binary-operator chain — see the module doc comment
929        /// on precedence flattening. Must stay last (no leading keyword).
930        Ops(OpChain),
931    }
932
933    /// One `name [: ty] [|] patbot* = value [| patbot* = value]*` clause
934    /// GROUP of a `let-rec` (also reused, from outside this module, by
935    /// top-level `let-rec`). `ascription` is `parser.mly`'s rarer
936    /// `COLON ty` type-annotated form (`recdecargpart`'s `COLON ty BAR`
937    /// alternative), e.g. the bundled `itemize.satyh`'s `let-rec
938    /// listing-item : context -> int -> bool -> bool -> itemize ->
939    /// block-boxes | ctx depth is-first is-last (Item(...)) = ..`. Parsed
940    /// but not enforced — there is no enforcement pass for value-level
941    /// ascriptions (only module `val`/`direct` signature items reach
942    /// `typecheck.rs`'s `command_scheme`/sig machinery) — so it is a
943    /// parse-and-ignore stand-in whose only job is making verbatim upstream
944    /// source parse. `params` is
945    /// `patbot*` (`recdecargpart`'s plain `argpats` form, optionally
946    /// preceded by a `leading_bar` — `recdecargpart`'s `BAR argpatlst`
947    /// alternative, used both for the OCaml-style "every clause, including
948    /// the first, gets a `|`" layout the bundled packages write, e.g.
949    /// `list.satyg`'s `let-rec map\n  | f [] = []\n  | f (x :: xs) = ..`,
950    /// and for the `COLON ty BAR` form above, whose single clause is *only*
951    /// reachable via a leading `|`). `extra` holds any further
952    /// `| patbot* = value` continuation clauses (`nxrecdecpar`) — SATySFi's
953    /// multi-clause pattern-matching function-definition sugar. Every
954    /// clause in the group must bind the same number of parameters (checked
955    /// at elaboration — upstream's `IllegalArgumentLength` — not here); the
956    /// (possibly plural) clauses desugar to one curried function that
957    /// matches a tuple of fresh parameters against each clause's patterns
958    /// in turn — see `elaborate.rs`'s `rec_clause_value`.
959    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
960    pub struct RecBinding {
961        pub name: super::BindName,
962        pub ascription: Option<RecAscription>,
963        pub leading_bar: Option<BarTok>,
964        pub params: Vec<PatBot>,
965        pub eq: DefEqTok,
966        pub value: super::ExprErased,
967        pub extra: Vec<RecClause>,
968    }
969
970    /// A `let-rec` binding's optional `: ty` ascription (see [`RecBinding`]'s
971    /// doc comment). A direct (non-erased) `TypeExpr` field: `RecBinding` is
972    /// already inside this `#[recurse]` module (embedded directly by
973    /// `Expr::LetRecIn`, not through an eraser), and connecting it straight
974    /// to `TypeExpr` — one of the module's three self-recursive SCC roots —
975    /// is exactly the same kind of cross-root DAG edge `RecBinding.params:
976    /// Vec<PatBot>` already makes to the `PatBot` root; `TypeExpr` never
977    /// refers back to `Expr`/`PatBot`/`RecBinding`, so no new cycle results.
978    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
979    pub struct RecAscription {
980        pub colon: ColonTok,
981        pub ty: TypeExpr,
982    }
983
984    /// A `| patbot* = value` continuation clause of a multi-clause
985    /// `let-rec` binding (see [`RecBinding`]'s doc comment).
986    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
987    pub struct RecClause {
988        pub bar: BarTok,
989        pub params: Vec<PatBot>,
990        pub eq: DefEqTok,
991        pub value: super::ExprErased,
992    }
993
994    /// An `and name param* = value` continuation of a `let-rec`.
995    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
996    pub struct AndBinding {
997        pub and_kw: KwAnd,
998        pub binding: RecBinding,
999    }
1000
1001    /// One `pat [when guard] -> body` match arm. The pattern and body sit
1002    /// behind the stream-erasing wrappers (deref to reach the inner nodes).
1003    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1004    pub struct MatchArm {
1005        pub pat: super::PatErased,
1006        pub guard: Option<Guard>,
1007        pub arrow: ArrowTok,
1008        pub body: super::ExprErased,
1009    }
1010
1011    /// A match arm's `when cond` guard.
1012    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1013    pub struct Guard {
1014        pub when_kw: KwWhen,
1015        pub cond: super::ExprErased,
1016    }
1017
1018    /// A `| pat [when guard] -> body` continuation of a match's arm list.
1019    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1020    pub struct BarArm {
1021        pub bar: BarTok,
1022        pub arm: MatchArm,
1023    }
1024
1025    /// A flattened binary-operator chain: `head (op rhs)*`, left-folded
1026    /// (with correct per-operator precedence/associativity) during
1027    /// elaboration. `before` is `nxbfr`'s postfix (`e1 before e2`), attached
1028    /// here rather than modeled at its own precedence level: `nxbfr` sits
1029    /// between `nxif` and `nxlambda`, i.e. *above* `nxlor`/`OpChain`'s own
1030    /// level, so `parser.mly`'s left operand is actually `nxlambda` (which
1031    /// also covers `Fun`/`Overwrite`) — attaching to `OpChain` alone misses
1032    /// `(fun x -> e1) before e2`/`(x <- e1) before e2` as the left operand;
1033    /// such input is rejected here (a documented simplification, not a
1034    /// silent misparse). `body` is threaded through `ExprErased` (not
1035    /// boxed directly) to keep `Expr` a singleton SCC: a direct `Box<Expr>`
1036    /// field on `OpChain` would make `OpChain` itself part of `Expr`'s SCC
1037    /// (a second, non-`Expr`-variant self-loop edge), which is exactly the
1038    /// multi-type-cycle shape the module doc warns about.
1039    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1040    pub struct OpChain {
1041        pub head: AppExpr,
1042        pub tail: Vec<OpRhs>,
1043        pub before: Option<BeforeTail>,
1044    }
1045
1046    /// The `before body` suffix of an [`OpChain`] (`nxbfr`).
1047    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1048    pub struct BeforeTail {
1049        pub kw: KwBefore,
1050        pub body: super::ExprErased,
1051    }
1052
1053    /// One `op rhs` continuation of an [`OpChain`].
1054    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1055    pub struct OpRhs {
1056        pub op: BinOpTok,
1057        pub rhs: AppExpr,
1058    }
1059
1060    /// `nxun`/`nxapp`/`nxunsub` flattened: an optional leading unary minus,
1061    /// an optional leading `!`/`!!`/... deref (`UNOP_EXCLAM`, `nxunsub`), an
1062    /// atomic head with any `#label` field accesses (`nxbot ACCESS var`,
1063    /// left-recursive in `parser.mly` — flattened to a postfix `Vec` here,
1064    /// the same technique as `PatCons`'s `::`), and an application-chain
1065    /// tail (`nxapp nxunsub` / `nxapp CONSTRUCTOR` / `nxapp OPTIONAL
1066    /// nxunsub` / `nxapp OMISSION`, left-folded during elaboration).
1067    /// `EXACT_AMP`/`EXACT_TILDE` (`&`/`~`) are the
1068    /// staging prefixes, carried in `stage` (see [`StagePrefix`]). First-class command references (`command \cmd`,
1069    /// upstream's `nxapp: COMMAND hcmd`) are modeled one level down, as
1070    /// [`Atomic::Command`] — see its doc comment for the rationale.
1071    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1072    pub struct AppExpr {
1073        pub minus: Option<ExactMinusTok>,
1074        pub stage: Option<StagePrefix>,
1075        pub excl: Option<UnopExclamTok>,
1076        pub head: Atomic,
1077        pub head_accesses: Vec<AccessSeg>,
1078        pub args: Vec<AppArg>,
1079    }
1080
1081    /// A staging prefix on a `nxunsub` operand: `&e` builds code for the next
1082    /// stage, `~e` splices the result of a previous-stage computation
1083    /// (`parser.mly:796-797`, `UTNext`/`UTPrev`).
1084    ///
1085    /// Upstream spells these as alternatives of `nxunsub`, alongside the `!`
1086    /// deref; this grammar flattens that level, so like `minus`/`excl` they
1087    /// become an optional prefix field. The looser shape accepts a few
1088    /// combinations upstream's grammar does not (`&!x`), which is the same
1089    /// latitude `AppExpr` already takes for `-!x`.
1090    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1091    pub enum StagePrefix {
1092        /// `&e` — quote: the value is `e`'s code, to run one stage later.
1093        Next(ExactAmpTok),
1094        /// `~e` — splice: run `e` now and drop its code in here.
1095        Prev(ExactTildeTok),
1096    }
1097
1098    /// One `#label` field-access segment (`nxbot ACCESS var`).
1099    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1100    pub struct AccessSeg {
1101        pub hash: AccessTok,
1102        pub label: VarTok,
1103    }
1104
1105    /// One application-chain argument: an optional-argument value (`?:
1106    /// arg`), an omitted optional argument (`?*`), a plain atomic value
1107    /// (with its own optional `!` prefix and `#access` suffixes, mirroring
1108    /// `AppExpr`'s head position — each `nxunsub`/`nxbot` in the `nxapp`
1109    /// chain is independent), or a bare constructor applied nullarily
1110    /// (`nxapp CONSTRUCTOR`).
1111    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1112    pub enum AppArg {
1113        /// `?: arg` (`nxapp OPTIONAL nxunsub`, simplified to a bare
1114        /// `Atomic` operand rather than the full `nxunsub`).
1115        Optional { q: OptionalTok, value: Atomic },
1116        /// `?*` (`nxapp OMISSION`).
1117        Omission(OmissionTok),
1118        Atom {
1119            stage: Option<StagePrefix>,
1120            excl: Option<UnopExclamTok>,
1121            atom: Atomic,
1122            accesses: Vec<AccessSeg>,
1123        },
1124        Ctor(CtorTok),
1125        /// `?(l = e, …) atom` — a SATySFi 0.1 labeled-optional application
1126        /// bundle paired with the positional argument it precedes (pairing
1127        /// them in one arm rejects a dangling trailing bundle `f x ?(l=1)` at
1128        /// parse time, as upstream does). Additive 0.1 node; the `?(`-head is
1129        /// token-disjoint from every 0.0.6 `AppArg` arm (0.0.6's `Optional`
1130        /// is `?:`-headed, a distinct token), so no previously-parsing input
1131        /// changes shape. Elaboration rejects it under a V0_0 version gate.
1132        Bundled {
1133            opts: CstOptArgs,
1134            excl: Option<UnopExclamTok>,
1135            atom: Atomic,
1136            accesses: Vec<AccessSeg>,
1137        },
1138        /// `?(l = e, …) Ctor` — as [`AppArg::Bundled`] but the positional
1139        /// argument is a bare constructor.
1140        BundledCtor { opts: CstOptArgs, ctor: CtorTok },
1141    }
1142
1143    /// A SATySFi 0.1 `?(l = x, …)` optional-parameter binder bundle (for
1144    /// [`Expr::FunRows`]): the `?` sigil, then a parenthesized `,`-separated
1145    /// list of `label = binder` entries.
1146    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1147    pub struct CstOptBinders {
1148        pub q: OptionalTypeTok,
1149        pub paren: ParenGroup<()>,
1150        #[group(self.paren)]
1151        pub entries: Vec<CstOptBinderEntry>,
1152    }
1153
1154    /// One `label = binder` entry of a [`CstOptBinders`] bundle (the last
1155    /// `,` is optional; `=` is upstream's `EXACT_EQ`, reusing [`DefEqTok`]).
1156    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1157    pub struct CstOptBinderEntry {
1158        pub label: VarTok,
1159        pub eq: DefEqTok,
1160        pub var: VarTok,
1161        pub comma: Option<CommaTok>,
1162    }
1163
1164    /// A SATySFi 0.1 `?(l = e, …)` optional-argument bundle (for
1165    /// [`AppArg::Bundled`]/[`AppArg::BundledCtor`]): the `?` sigil, then a
1166    /// parenthesized `,`-separated list of `label = expr` entries.
1167    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1168    pub struct CstOptArgs {
1169        pub q: OptionalTypeTok,
1170        pub paren: ParenGroup<()>,
1171        #[group(self.paren)]
1172        pub entries: Vec<CstOptArgEntry>,
1173    }
1174
1175    /// One `label = expr` entry of a [`CstOptArgs`] bundle — a FULL
1176    /// expression (`?(bias = 1 + n)`), routed through [`super::ExprErased`]
1177    /// so this satellite never joins `Expr`'s SCC.
1178    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1179    pub struct CstOptArgEntry {
1180        pub label: VarTok,
1181        pub eq: DefEqTok,
1182        pub value: super::ExprErased,
1183        pub comma: Option<CommaTok>,
1184    }
1185
1186    /// `nxbot` (plus the ctor-head case usually found in `nxun`): an atomic
1187    /// expression.
1188    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1189    pub enum Atomic {
1190        Length(LengthTok),
1191        Float(FloatTok),
1192        Int(IntTok),
1193        Literal(LiteralTok),
1194        True(KwTrue),
1195        False(KwFalse),
1196        /// A bare constructor, e.g. `None`, or the head of `Some 1`.
1197        Ctor(CtorTok),
1198        Var(VarTok),
1199        /// `Mod.x` — a module-qualified variable (`VARWITHMOD`).
1200        VarWithMod(VarWithModTok),
1201        /// `( ‹op› )` — a bare reference to a (possibly user-defined)
1202        /// operator as a first-class value, e.g. `(+++)`, `(-->)`
1203        /// (`nxbot`'s `LPAREN binop RPAREN` alternative — the same syntax
1204        /// `super::BindName` accepts in binding position, here used as an
1205        /// ordinary atomic expression). Resolves via the same name
1206        /// `BinOpTok::op_text` yields, exactly like `Var` (`elaborate.rs`).
1207        OpRef(OpNameTok),
1208        /// `command \cmd` (upstream `nxapp: COMMAND hcmd` →
1209        /// `UTContentOf(mods, csnm)`): a first-class *value* that simply
1210        /// names an inline command's own binding — no argument tail, so
1211        /// modeling it as an atom (rather than upstream's `nxapp` level)
1212        /// is strictly simpler and covers every bundled usage (always
1213        /// parenthesized, e.g. `(command \math)`). Only the horizontal
1214        /// form is spelled upstream; if a package ever writes `command
1215        /// +cmd`/a math form, extend with an `AnyVertCmdTok`/math
1216        /// alternative then.
1217        Command { kw: CommandTok, name: AnyHorzCmdTok },
1218        /// `()`
1219        Unit { paren: UnitParen },
1220        /// `( expr )` or `( expr, expr, … )` (the latter elaborates to a
1221        /// tuple).
1222        Paren {
1223            paren: ParenGroup<()>,
1224            #[group(self.paren)]
1225            inner: Box<ParenBody>,
1226        },
1227        /// `Mod.(e)` ≡ `open Mod in e` (`nxbot`'s `OPENMODULE nxlet RPAREN`
1228        /// production). Reuses `ParenBody` exactly like `Atomic::Paren`
1229        /// above (so `Mod.(e, e, …)` would elaborate to a tuple the same
1230        /// way, though no bundled package writes it that way) — the `Mod.(`
1231        /// sigil is the open delimiter (`OpenModuleTok`, carrying the
1232        /// module name), closed by a plain `)`. Elaborated via the same
1233        /// machinery as `Expr::OpenIn` (`elaborate.rs`'s `open_module`
1234        /// helper).
1235        OpenModule {
1236            grp: OpenModuleGroup<()>,
1237            #[group(self.grp)]
1238            body: Box<ParenBody>,
1239        },
1240        /// `(| label = expr; … |)` or `(| base with label = expr; … |)`
1241        /// (`nxrecordsynt`; see [`RecordBody`]).
1242        Record {
1243            rec: RecordGroup<()>,
1244            #[group(self.rec)]
1245            body: RecordBody,
1246        },
1247        /// `[ expr; … ]`
1248        List {
1249            list: ListGroup<()>,
1250            #[group(self.list)]
1251            items: Vec<ListItem>,
1252        },
1253        /// `{ inline text }`
1254        InlineText {
1255            igrp: InlineGroup<()>,
1256            #[group(self.igrp)]
1257            elems: Vec<InlineElem>,
1258        },
1259        /// `'< block text >`
1260        BlockText {
1261            bgrp: BlockGroup<()>,
1262            #[group(self.bgrp)]
1263            elems: Vec<BlockElem>,
1264        },
1265        /// `${ math }` (`nxbot`'s `BMATHGRP mathblock EMATHGRP` case).
1266        MathText {
1267            mgrp: MathGroup<()>,
1268            #[group(self.mgrp)]
1269            elems: Vec<super::MathErased>,
1270        },
1271    }
1272
1273    /// `(| … |)`'s content: either a plain field list, or a *record update*
1274    /// `base with l = e; …` (`nxrecordsynt`'s third alternative). `Update`
1275    /// is tried first (it backtracks cleanly to `Fields` — parsing `base`
1276    /// as an expression stops right before a bare `label = expr`'s `=`,
1277    /// since `=` isn't a valid expression continuation, so the `with`
1278    /// keyword check fails fast and `Fields` picks it up). `base` is
1279    /// `nxbot` in `parser.mly` (an atomic expression); routed through
1280    /// `ExprErased` here instead, which is strictly more permissive
1281    /// (accepts any expression as the base, not just an atomic one) — a
1282    /// deliberate simplification, and also the only way to reference it
1283    /// without adding a second, non-`Group` recursion edge into `Atomic`
1284    /// (which — like `AppExpr`/`OpChain` — is *not* itself part of `Expr`'s
1285    /// SCC, and should stay that way).
1286    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1287    pub enum RecordBody {
1288        Update {
1289            base: super::ExprErased,
1290            with_kw: KwWith,
1291            fields: Vec<RecordField>,
1292        },
1293        Fields(Vec<RecordField>),
1294    }
1295
1296    /// The parenthesized-expression group's content: one expression, plus
1297    /// any `, expr` continuations (present only for a tuple).
1298    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1299    pub struct ParenBody {
1300        pub first: super::ExprErased,
1301        pub rest: Vec<CommaExpr>,
1302    }
1303
1304    /// A `, expr` continuation inside a parenthesized tuple.
1305    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1306    pub struct CommaExpr {
1307        pub comma: CommaTok,
1308        pub value: super::ExprErased,
1309    }
1310
1311    /// One record field `label = expr;` (the last `;` is optional).
1312    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1313    pub struct RecordField {
1314        pub name: VarTok,
1315        pub eq: DefEqTok,
1316        pub value: super::ExprErased,
1317        pub semi: Option<ListPunctTok>,
1318    }
1319
1320    /// One list element `expr;` (the last `;` is optional).
1321    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1322    pub struct ListItem {
1323        pub value: super::ExprErased,
1324        pub semi: Option<ListPunctTok>,
1325    }
1326
1327    /// One inline-text element (`ih`/`ihtext`/`ihcmd` in parser.mly).
1328    ///
1329    /// `ItemBullet`/`Sep` are flat markers rather than the nested tree
1330    /// `parser.mly` builds in-grammar (`sxsep`'s `nonempty_list(sxitem)` /
1331    /// `sxlist`): since `InlineText`'s content is already a flat
1332    /// `Vec<InlineElem>`, regrouping consecutive `ItemBullet`-headed runs
1333    /// into an itemize tree (and `Sep`-delimited runs into columns, for
1334    /// tabular/math use later) is deferred to the elaborator. Token-level
1335    /// round-tripping is unaffected either way.
1336    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1337    pub enum InlineElem {
1338        Char(CharTok),
1339        /// A backtick literal written inside inline text (`` `…` ``). Its own
1340        /// arm, not a `Char` run, so the elaborator can dispatch it through the
1341        /// context's code-text command — see `Token::CodeText`.
1342        CodeText(CodeTextTok),
1343        Space(SpaceTok),
1344        Break(BreakTok),
1345        /// `#var;` — embeds a program variable's value as inline content.
1346        Embed { var: VarInHorzTok, semi: EndActiveTok },
1347        /// `${ math }` — embeds math content as inline text (`ihcmd`'s
1348        /// `BMATHGRP mathblock EMATHGRP` case).
1349        EmbedMath {
1350            mgrp: MathGroup<()>,
1351            #[group(self.mgrp)]
1352            elems: Vec<super::MathErased>,
1353        },
1354        /// `\cmd …` (`name` also accepts the module-qualified
1355        /// `\Mod.cmd` form).
1356        Cmd { name: AnyHorzCmdTok, tail: CmdTail },
1357        /// An itemize bullet (`*`+) marker — see the variant-group doc above.
1358        ItemBullet(ItemTok),
1359        /// A `|` separator marker — see the variant-group doc above.
1360        Sep(SepTok),
1361    }
1362
1363    /// One block-text element (`vxbot`).
1364    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1365    pub enum BlockElem {
1366        /// `#var;` — embeds a program variable's value as block content.
1367        Embed { var: VarInVertTok, semi: EndActiveTok },
1368        /// `+cmd …` (`name` also accepts the module-qualified
1369        /// `+Mod.cmd` form).
1370        Cmd { name: AnyVertCmdTok, tail: CmdTail },
1371    }
1372
1373    /// A command's arguments (`narg* sargs` in parser.mly, upstream's own
1374    /// dedicated grammar — *not* a reuse of the general application chain
1375    /// like `AppExpr`). Either a bare `;` (no arguments) or a flat,
1376    /// non-empty sequence of [`AppArg`]s: each is `?: value` (a supplied
1377    /// optional `narg`), `?*` (an omitted optional `narg`), or a plain
1378    /// (possibly `!`/`#access`-decorated) atomic value — `(expr)`,
1379    /// `(|record|)`, `[list]`, `{inline}`, `<block>`, a bare ctor, etc. —
1380    /// covering both `narg`'s mandatory forms and `sargs`'s group forms
1381    /// uniformly (this port's usual simplification: `AppArg::Atom`'s
1382    /// `Atomic` already spans every shape upstream splits across `narg`/
1383    /// `sargs`). Optional/omitted `narg`s may lead (`\ref?:(x){text}`,
1384    /// `\ref?*{text}`) since every element is independently one `AppArg` —
1385    /// an `Expr`-based encoding could not, its head being a plain atom.
1386    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1387    pub enum CmdTail {
1388        /// `;` — no arguments.
1389        Semi(EndActiveTok),
1390        /// The argument chain: at least one [`AppArg`], via [`super::AppArgErased`].
1391        Args {
1392            first: super::AppArgErased,
1393            rest: Vec<super::AppArgErased>,
1394            semi: Option<EndActiveTok>,
1395        },
1396    }
1397
1398    /// `patas`: a pattern, plus an optional `as name` binding.
1399    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1400    pub struct Pattern {
1401        pub head: PatCons,
1402        pub as_clause: Option<AsClause>,
1403    }
1404
1405    /// The `as name` suffix of a pattern.
1406    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1407    pub struct AsClause {
1408        pub as_kw: KwAs,
1409        pub name: VarTok,
1410    }
1411
1412    /// One curried parameter of an ordinary (non-`let-rec`) `let`, or of a
1413    /// `let-inline`/`let-block`/`let-math` command binding — upstream's
1414    /// `arg` nonterminal (`nxnonrecdec`'s `argpart`/`cmdarglst`,
1415    /// `parser.mly:622-624`: `arg: patbot | OPTIONAL defedvar`): a full
1416    /// pattern, or the def-site optional-parameter marker `?:name`
1417    /// (`parser.mly`'s `OPTIONAL vartok`), e.g. `stdja.satyh`'s `let
1418    /// document record ?:configopt inner = ..` and `annot.satyh`'s
1419    /// `let-inline ctx \href ?:borderopt uri inner = ..`. Upstream's
1420    /// `let-rec`/`fun` argument grammar (`recdecargpart`/`argpats` —
1421    /// [`RecBinding`]/[`AndBinding`]/`Expr::Fun`) has no such alternative,
1422    /// only plain `let` and the three command-binding forms do — all four
1423    /// keep `Vec<Param>` ([`super::TopLet`], `Expr::LetIn`,
1424    /// [`super::TopBinding::LetInline`]/`LetBlock`/`LetMath`,
1425    /// [`Expr::LetMathIn`]). Elaborated (`elaborate.rs`) by widening
1426    /// `Optional` to `PatBot::Var` (`params_to_patbots`) before the ordinary
1427    /// pattern-currying machinery runs (plain `let`'s `rec_clause_value`, or
1428    /// a command binding's `curry_cmd_params`) — the `?:` marker carries no
1429    /// further semantics of its own in this port (`typecheck.rs`'s
1430    /// `command_scheme` doc comment: optionality is inferred structurally,
1431    /// not from this marker); for a command binding, the maximal *leading*
1432    /// run of `?:`-marked params is additionally counted by `elaborate.rs`'s
1433    /// `leading_optional_count` and recorded into the binding's
1434    /// `Scope::optional_arity`, so a marker-less call site can auto-omit
1435    /// those slots (see `cmd_args`/`math_bot`'s `Cmd` arm).
1436    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1437    pub enum Param {
1438        Optional { q: OptionalTok, name: VarTok },
1439        Pat(PatBot),
1440        /// A SATySFi 0.1 `?(l = x, …)` labeled-optional command-parameter
1441        /// bundle, lowered from
1442        /// `cst_v1::Param { opts: Some(_), body }` by
1443        /// `v1/lower.rs::lower_command_params`. Reuses [`CstOptBinders`]
1444        /// verbatim (the same node a value-level `fun ?(l = x) p -> ..`
1445        /// bundle lowers to) — `?(`-headed, so distinct from
1446        /// the 0.0.6 `?:`-headed [`Param::Optional`] above (no arm overlap,
1447        /// no grammar ambiguity: this variant is never PARSED directly by
1448        /// this 0.0.6-frozen `cst.rs`, only ever *constructed* by the 0.1
1449        /// lowering path). Consumed by `elaborate.rs`'s bundle-aware
1450        /// `curry_cmd_params_v1`, which emits `Ast::LambdaOpt` for it — see
1451        /// that function's doc comment.
1452        Bundled { opts: CstOptBinders, body: PatBot },
1453    }
1454
1455    /// `pattr`: a `patbot`, followed by any number of `:: patbot` segments.
1456    /// `parser.mly` writes this as right recursion (`patbot :: pattr`, always
1457    /// fine, unlike left recursion) but it is flattened to a `Vec` here (the
1458    /// same right-fold-at-elaboration technique as `OpChain`, `::` being
1459    /// right-associative) so that `PatCons` need not be self-referential: a
1460    /// `PatCons`/`ConsRest` pair of mutually-referencing wrapper structs
1461    /// would form a 2-cycle with no self-loop of its own, which the
1462    /// `#[recurse]` depth engine rejects ("a sub-cycle running entirely
1463    /// through non-root types").
1464    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1465    pub struct PatCons {
1466        pub head: PatBot,
1467        pub tail: Vec<ConsSeg>,
1468    }
1469
1470    /// One `:: patbot` continuation of a cons pattern.
1471    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1472    pub struct ConsSeg {
1473        pub cons: ConsTok,
1474        pub tail: PatBot,
1475    }
1476
1477    /// `patbot`, plus the constructor-pattern forms `pattr` adds in
1478    /// `parser.mly` (folded in here to keep `PatCons` a plain struct).
1479    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1480    pub enum PatBot {
1481        /// `Ctor patbot` — a constructor applied to one argument pattern.
1482        CtorApplied { ctor: CtorTok, arg: Box<PatBot> },
1483        /// A bare (nullary) constructor pattern.
1484        Ctor(CtorTok),
1485        Int(IntTok),
1486        True(KwTrue),
1487        False(KwFalse),
1488        Str(LiteralTok),
1489        Wild(WildcardTok),
1490        Var(VarTok),
1491        /// `()`
1492        Unit { paren: UnitParen },
1493        /// `( pat )` or `( pat, pat, … )` (the latter elaborates to a tuple
1494        /// pattern).
1495        Paren {
1496            paren: ParenGroup<()>,
1497            #[group(self.paren)]
1498            inner: Box<PatternParenBody>,
1499        },
1500        /// `[ pat; … ]` (also matches `[]`).
1501        List {
1502            plist: ListGroup<()>,
1503            #[group(self.plist)]
1504            items: Vec<PatListItem>,
1505        },
1506    }
1507
1508    /// The parenthesized-pattern group's content: one pattern, plus any
1509    /// `, pat` continuations (present only for a tuple pattern).
1510    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1511    pub struct PatternParenBody {
1512        pub first: super::PatErased,
1513        pub rest: Vec<CommaPattern>,
1514    }
1515
1516    /// A `, pat` continuation inside a parenthesized tuple pattern.
1517    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1518    pub struct CommaPattern {
1519        pub comma: CommaTok,
1520        pub value: super::PatErased,
1521    }
1522
1523    /// One list-pattern element `pat;` (the last `;` is optional).
1524    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1525    pub struct PatListItem {
1526        pub value: super::PatErased,
1527        pub semi: Option<ListPunctTok>,
1528    }
1529
1530    /// A minimal type-expression grammar for `type` declarations and
1531    /// signature (`val .. : ty`) annotations (`txfunc`/`txprod`/`txapppre`/
1532    /// `txapp`/`txbot`, simplified). Function arrows (right-associative,
1533    /// with an optional-argument `?->` prefix chain — see [`OptArrowDom`]),
1534    /// 2+-way product types (`*`, [`TypeProd`]), a SINGLE-argument postfix
1535    /// type-constructor application (`'a option`, `'a list`; see
1536    /// [`TypeApp`]), command-argument-list types (`[ty; ty?; ..]
1537    /// inline-cmd`/`block-cmd`/`math-cmd`; see [`TypeAtom::Cmd`]),
1538    /// parenthesized grouping, closed record types (`(| l : ty; … |)`; see
1539    /// [`TypeAtom::Record`]), bare/qualified names, and type variables are
1540    /// supported; N-ary applied constructors are not — such input is
1541    /// rejected with a parse error. Self-recursive only through `Fun`'s
1542    /// codomain (right recursion); parenthesized nesting goes through the
1543    /// [`super::TyErased`] leaf.
1544    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1545    pub enum TypeExpr {
1546        /// `opts?-> dom -> cod` (right-associative). `dom` is a [`TypeProd`]
1547        /// (not just [`TypeAtom`]) so e.g. `'a option -> 'b option` and `'a *
1548        /// 'b -> 'c` both parse at their expected precedence (application
1549        /// binds tighter than `*`, which binds tighter than `->`). `opts` is
1550        /// upstream's `txfuncopts` prefix (`parser.mly:880-882`): zero or
1551        /// more `ty ?->` domains greedily consumed *before* the final
1552        /// mandatory `dom -> cod`, e.g. `config ?-> block-text -> document`
1553        /// parses as `opts = [config]`, `dom = block-text`, `cod =
1554        /// document`. Lowered (`typecheck.rs`) to an `option`-wrapped
1555        /// mandatory domain per optional entry — see that module's doc
1556        /// comment on `lower_type_expr`.
1557        Fun {
1558            opts: Vec<OptArrowDom>,
1559            dom: TypeProd,
1560            arrow: ArrowTok,
1561            cod: Box<TypeExpr>,
1562        },
1563        /// The non-arrow fallthrough. Despite the name (kept stable — see
1564        /// the module's compile-time-blowup note on why every recursion
1565        /// edge here is deliberate), this holds a full [`TypeProd`], not a
1566        /// bare [`TypeAtom`]: a product/application with no enclosing arrow
1567        /// is still just "the whole type expression minus `->`".
1568        Atom(TypeProd),
1569        /// `?(l1 : ty1, …) dom -> cod` — a SATySFi 0.1 labeled-optional
1570        /// function TYPE domain (upstream `typ`'s second production,
1571        /// `parser_v1.mly:688-691`). Lowered
1572        /// (`typecheck.rs`) to `MonoType::Func(Row::Cons(l1, ty1, …
1573        /// Row::Empty), dom, cod)` — a CLOSED row, matching what
1574        /// `Ast::LambdaOpt` infers, so an explicit `?(l:τ)->`
1575        /// signature unifies against an actual `?(l=x)`-taking function.
1576        /// `?`-headed — token-disjoint from `Fun`/`Atom` (neither
1577        /// [`TypeProd`] nor [`TypeAtom`] can start with [`OptionalTypeTok`]),
1578        /// so declared order is safety-neutral; appended last, this file's
1579        /// convention for 0.1 additions. It widens the 0.0.6 ACCEPT surface:
1580        /// a 0.0.6 program containing `?(l : int) -> int` parses here and
1581        /// reaches `typecheck.rs::lower_type_expr`'s version gate, which
1582        /// rejects it under `V0_0` with a version-error message (better
1583        /// diagnostics than a parse error).
1584        OptRowFun {
1585            opt_dom: CstTypeOptDom,
1586            dom: TypeProd,
1587            arrow: ArrowTok,
1588            cod: Box<TypeExpr>,
1589        },
1590    }
1591
1592    /// `?(l = ty, …)` — the closed labeled-optional-domain prefix of
1593    /// [`TypeExpr::OptRowFun`]. No row-variable-tail field: row-tailed
1594    /// optional domains need signature-level row quantification
1595    /// (`parser_v1.mly`'s `rowquant`/`quant`) — not implemented here;
1596    /// `cst_v1`'s own `TypeOptDomInnerV1` models the tail at parse
1597    /// level and rejects it with a `LowerError` before ever reaching here
1598    /// (`v1/lower.rs`).
1599    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1600    pub struct CstTypeOptDom {
1601        pub q: OptionalTypeTok,
1602        pub paren: ParenGroup<()>,
1603        #[group(self.paren)]
1604        pub entries: Vec<CstTypeOptEntry>,
1605    }
1606
1607    /// One `label : ty,` entry of a [`CstTypeOptDom`] (last `,` optional —
1608    /// matching the 0.1 lowering convention this file's other additive nodes
1609    /// use, e.g. [`CstOptArgEntry`], rather than the frozen grammar's `;`).
1610    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1611    pub struct CstTypeOptEntry {
1612        pub label: VarTok,
1613        pub colon: ColonTok,
1614        pub ty: super::TyErased,
1615        pub comma: Option<CommaTok>,
1616    }
1617
1618    /// One `ty ?->` leading domain of a [`TypeExpr::Fun`]'s optional-argument
1619    /// prefix (`parser.mly`'s `txfuncopts`, 880-882).
1620    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1621    pub struct OptArrowDom {
1622        pub ty: TypeProd,
1623        pub arrow: OptionalArrowTok,
1624    }
1625
1626    /// `txprod`: one or more `*`-separated [`TypeApp`]s (a product type),
1627    /// or just a single one if there's no `*` at all — flattened to a
1628    /// `Vec` (the same deferred-fold technique as `OpChain`/`PatCons`)
1629    /// rather than modeled as its own right-recursive rule, keeping
1630    /// `TypeExpr` a singleton SCC.
1631    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1632    pub struct TypeProd {
1633        pub first: TypeApp,
1634        pub rest: Vec<StarType>,
1635    }
1636
1637    /// A `* ty` continuation of a [`TypeProd`].
1638    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1639    pub struct StarType {
1640        pub star: ExactTimesTok,
1641        pub ty: TypeApp,
1642    }
1643
1644    /// `txapp` — a postfix type application `arg1 arg2 … argN ctor`, upstream's
1645    /// N-ary chain flattened into a greedy atom run (the way `OpChain`/`PatCons`
1646    /// flatten their own left-recursions). [`head`](TypeApp::head) is always
1647    /// present; when [`rest`](TypeApp::rest) is non-empty the LAST atom is the
1648    /// type constructor (a bare or `Mod.`-qualified name — `list`/`option`/
1649    /// `result`/`Eq.t`/`implicit`) and every atom before it (including `head`)
1650    /// is one of its arguments. This is unambiguous because SATySFi always
1651    /// parenthesizes a nested single-argument application (`('a list) list`,
1652    /// `('a t) implicit` — never `'a list list`), so a flat run of atoms can
1653    /// only be one constructor applied to the preceding arguments:
1654    ///
1655    /// - `int` → `head = int`, `rest = []` (a bare atom).
1656    /// - `'a option` → `head = 'a`, `rest = [option]` (one arg).
1657    /// - `'a 'e result` → `head = 'a`, `rest = ['e, result]` (`satysfi-base`'s
1658    ///   two-parameter `result`/`either`/`t`/`map`).
1659    /// - `('a Eq.t) implicit` → `head = ('a Eq.t)`, `rest = [implicit]`
1660    ///   (`satysfi-base`'s typeclass-dictionary marker).
1661    ///
1662    /// Elaboration (`typecheck::lower_type_app`) does the head/args/ctor split;
1663    /// the grammar itself is a plain, always-terminating greedy `Vec<TypeAtom>`
1664    /// (each atom consumes ≥1 token, stopping at the first non-atom — `->`,
1665    /// `*`, `)`, `;`, …). Both the unqualified and `Mod.`-qualified constructor
1666    /// forms fall out for free, since a `Mod.t` ctor is just a
1667    /// [`TypeAtom::NameMod`] like any other atom.
1668    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1669    pub struct TypeApp {
1670        pub head: TypeAtom,
1671        pub rest: Vec<TypeAtom>,
1672    }
1673
1674    /// An atomic type expression.
1675    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1676    pub enum TypeAtom {
1677        /// `[ty; ty?; ..] inline-cmd` / `block-cmd` / `math-cmd`
1678        /// (`parser.mly`'s `txapppre` command-type productions, 903-919) —
1679        /// tried first: unambiguous, since no other `TypeAtom` starts with
1680        /// `[` (`BListTok`). Each `;`-separated element is a [`TypeCmdArgItem`].
1681        Cmd {
1682            list: ListGroup<()>,
1683            #[group(self.list)]
1684            args: Vec<TypeCmdArgItem>,
1685            kind: CmdTypeKind,
1686        },
1687        /// `( ty )`
1688        Paren {
1689            paren: ParenGroup<()>,
1690            #[group(self.paren)]
1691            inner: super::TyErased,
1692        },
1693        /// `(| l1 : ty1; l2 : ty2; … |)` — a closed record type (`txbot`'s
1694        /// `txrecord` case, `parser.mly:955-961`), lowered to
1695        /// `MonoType::Record` (a `Row::Cons` chain ending in `Row::Empty` —
1696        /// see `typecheck.rs`'s `lower_type_atom`). Distinguished from
1697        /// [`TypeAtom::Paren`] (opens on plain `LParenTok`, i.e. `(`) and
1698        /// from a record-VALUE expression ([`Atomic::Record`], a different
1699        /// grammar position — only reachable where an `Expr` is expected,
1700        /// never in type position) purely by lexer-level delimiter token:
1701        /// `(|`/`|)` lex as the dedicated `BRecordTok`/`ERecordTok` pair
1702        /// (same as [`super::RecordKind`]'s use at `constraint 'a :: (|…|)`), so no
1703        /// backtracking between any of these three shapes is needed.
1704        Record {
1705            rec: RecordGroup<()>,
1706            #[group(self.rec)]
1707            fields: Vec<TypeRecordField>,
1708        },
1709        /// A type variable, e.g. `'a`.
1710        Var(TypeVarTok),
1711        /// An unqualified type name, e.g. `int`, `string`.
1712        Name(VarTok),
1713        /// `Mod.t` — a bare module-qualified type name in atomic (0-ary,
1714        /// non-applied) position, e.g. `Eq.t` in `val eq : Eq.t -> Eq.t ->
1715        /// ordering`. Sibling of [`Name`](TypeAtom::Name) (not a widened
1716        /// field on it); as the last atom of a [`TypeApp`] it is a
1717        /// module-qualified type constructor (`int M.t`, `ordering Eq.t`).
1718        /// **Tried after `Name`** only by placement convention (the two are
1719        /// token-disjoint: `VarTok`/`VarWithModTok` are separate lexer
1720        /// tokens, so there is no real backtracking ambiguity between them).
1721        NameMod(VarWithModTok),
1722        /// `(| l1 : ty1, … | ?'r |)` — a SATySFi 0.1 OPEN record type: a
1723        /// row-variable tail after the fields (upstream `typ_bot`'s SECOND
1724        /// `L_RECORD`/`R_RECORD` production, `parser_v1.mly:748-749`).
1725        /// Lowered (`typecheck.rs`) to
1726        /// `MonoType::Record(Row::Cons(l1, ty1, … Row::Var(fresh)))` — the
1727        /// row variable unifies structurally as an open record's tail
1728        /// (permitting additional fields at the unification site), reusing
1729        /// the existing generic `Row`/`RowVarRef`/`unify_row` machinery — no
1730        /// new type machinery needed. Genuinely a NEW shape, not a widening
1731        /// of the frozen [`TypeAtom::Record`] (0.0.6's `txrecord` grammar has
1732        /// no row-var tail at all, confirmed by grep of upstream
1733        /// `parser.mly`). Comma-separated fields, matching this file's 0.1
1734        /// additive
1735        /// nodes ([`CstOptArgEntry`], [`CstOptBinderEntry`]) rather than the
1736        /// frozen `Record`'s upstream-0.0.6 `;` separator. Unreachable from a
1737        /// `V0_0` token stream by construction: [`RowVarTok`] is only ever
1738        /// emitted by the lexer under [`crate::version::RustyfiVersion::
1739        /// V0_1`] (`lexer.rs`'s `'?'` arm), so no 0.0.6 parse can ever
1740        /// produce this variant — no elaborate/typecheck-time version gate
1741        /// is needed here (contrast [`TypeExpr::OptRowFun`], which IS
1742        /// reachable from 0.0.6 lexing and so DOES need one).
1743        // NOTE the group field is `orec`, not `rec`: syan names a group
1744        // substruct after (group-field name, ENUM name) with no variant
1745        // component, so a second `rec` group in `TypeAtom` collides with
1746        // `Record`'s (E0428 + E0119, and the survivor has the wrong fields).
1747        RecordOpen {
1748            orec: RecordGroup<()>,
1749            #[group(self.orec)]
1750            inner: CstRecordOpenInner,
1751        },
1752    }
1753
1754    /// A [`TypeAtom::RecordOpen`]'s group content: one or more `,`-separated
1755    /// fields (nonempty enforced at lowering, matching the closed form),
1756    /// then a mandatory `| ?'r` row-variable tail.
1757    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1758    pub struct CstRecordOpenInner {
1759        pub fields: Vec<CstRecordOpenField>,
1760        pub bar: BarTok,
1761        pub var: RowVarTok,
1762    }
1763
1764    /// One `l : ty,` field of a [`TypeAtom::RecordOpen`] (last `,` optional).
1765    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1766    pub struct CstRecordOpenField {
1767        pub name: VarTok,
1768        pub colon: ColonTok,
1769        pub ty: super::TyErased,
1770        pub comma: Option<CommaTok>,
1771    }
1772
1773    /// One `l : ty;` field of a [`TypeAtom::Record`] (`txrecord`,
1774    /// `parser.mly:962-965`) — sibling of [`super::RecordKindField`], but
1775    /// (unlike that struct, defined *outside* the `#[recurse]` module and so
1776    /// free to hold a direct `ast::TypeExpr` field) this one lives inside
1777    /// `TypeAtom`'s own SCC, so the field type is routed through
1778    /// [`super::TyErased`] instead — a direct `ast::TypeExpr` field here
1779    /// would close a fresh cycle back through `TypeAtom` itself (the same
1780    /// hazard [`TypeCmdArgItem`]'s doc comment explains).
1781    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1782    pub struct TypeRecordField {
1783        pub name: VarTok,
1784        pub colon: ColonTok,
1785        pub ty: super::TyErased,
1786        pub semi: Option<ListPunctTok>,
1787    }
1788
1789    /// One `;`-separated element of a [`TypeAtom::Cmd`]'s bracketed argument
1790    /// list: a mandatory `ty`, or an optional `ty?` (`parser.mly`'s `txlist`,
1791    /// 955-960) — routed through [`super::TyErased`] rather than the
1792    /// narrower `TypeApp` upstream uses, both to stay a DAG leaf (a direct
1793    /// `TypeApp` field here would close `TypeAtom -> Cmd -> ... -> TypeApp ->
1794    /// TypeAtom`, a fresh cycle through non-root types — see
1795    /// `AppArgErased`'s doc comment for the identical hazard) and per this
1796    /// port's usual permissive-superset simplification.
1797    ///
1798    /// `opt_labels` is the lowered
1799    /// `?(l:τ,…)` command-type row PREFIX on this slot (`TypeCmdOptDomV1` at
1800    /// the `cst_v1` side): a flat list of `label : ty` fields, no wrapping
1801    /// `?(` sigil/group of its own at this (already-lowered) target — purely
1802    /// a data carrier, populated by `v1/lower.rs::lower_type_cmd_args` and
1803    /// read by `typecheck.rs`'s `lower_type_atom` `Cmd` arm. Every
1804    /// 0.0.6-parsed fixture yields `opt_labels == []`: no real 0.0.6
1805    /// `TypeCmdArgItem` position can contain a bare `label :` shape (0.0.6's
1806    /// grammar has no colon here at all).
1807    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1808    pub struct TypeCmdArgItem {
1809        pub opt_labels: Vec<TypeCmdOptField>,
1810        pub ty: super::TyErased,
1811        pub opt: Option<OptionalTypeTok>,
1812        pub semi: Option<ListPunctTok>,
1813    }
1814
1815    /// One `label : ty,` field of a [`TypeCmdArgItem::opt_labels`] bundle
1816    /// (the last `,` is optional, matching
1817    /// this port's other 0.1-additive comma-separated satellite fields —
1818    /// [`CstOptBinderEntry`], [`CstTypeOptEntry`]).
1819    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1820    pub struct TypeCmdOptField {
1821        pub label: VarTok,
1822        pub colon: ColonTok,
1823        pub ty: super::TyErased,
1824        pub comma: Option<CommaTok>,
1825    }
1826
1827    /// The command-type keyword closing a [`TypeAtom::Cmd`]'s bracketed list.
1828    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1829    pub enum CmdTypeKind {
1830        Inline(HorzCmdTypeTok),
1831        Block(VertCmdTypeTok),
1832        Math(MathCmdTypeTok),
1833    }
1834
1835    /// `mathtop`: one math element, i.e. a `mathbot` base with any postfix
1836    /// `^`/`_`/`'` script combos (`mathtop`'s seven alternatives, flattened
1837    /// to a `Vec` in source order — the same `Ops`/`OpChain` deferred-
1838    /// precedence technique, since combos 3–6 interleave sub/superscript
1839    /// application order in a way elaboration is better placed to resolve).
1840    ///
1841    /// **No direct self-loop.** Unlike `Expr`/`PatBot`/`TypeExpr`, this
1842    /// grammar corner needs no fourth singleton SCC at all: `scripts` is a
1843    /// `Vec`, so an empty run already degenerates to plain `mathbot`, and
1844    /// `mathbot`'s only recursive spot (`{ … }` re-entering `mathmain`) is
1845    /// threaded through `MathErased` exactly like every *other* nested
1846    /// reference to "one math element" (`matharg`'s math-mode argument,
1847    /// `Atomic::MathText`'s program-mode embed, `InlineElem::EmbedMath`'s
1848    /// inline-text embed, `MathGroupArg`'s `{ … }` script operand). So
1849    /// `MathElemCst` is structurally acyclic within `#[recurse]`'s SCC
1850    /// analysis — like `OpChain`/`AppExpr`/`Atomic` — and monomorphizes
1851    /// exactly once (one stream type). This is *safer* than carving out a
1852    /// real self-loop would have been, not a shortcut: every recursive edge
1853    /// is erased, so there is no bounded-depth engine to blow up.
1854    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1855    pub struct MathElemCst {
1856        pub base: MathBot,
1857        pub scripts: Vec<MathScript>,
1858    }
1859
1860    /// `mathbot`.
1861    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1862    pub enum MathBot {
1863        /// `\cmd matharg*` (`mcmd list(matharg)`), sigil-only or
1864        /// module-qualified (`\Mod.cmd matharg*`).
1865        Cmd { name: AnyMathCmdTok, args: Vec<MathArg> },
1866        Chars(MathCharTok),
1867        /// `#var` (`VARINMATH`; math mode never trails this with `;` —
1868        /// unlike `#var;` in inline/block text, the lexer doesn't switch to
1869        /// an active mode here).
1870        Embed(VarInMathTok),
1871        /// A `|` separator marker (flat; elaborator regroups, e.g. for
1872        /// tabular/matrix columns — `mathblock`'s `SEP mathlist` case).
1873        Sep(SepTok),
1874        /// `{ … }` — re-enters `mathmain` (`mathgroup`'s `BMATHGRP mathmain
1875        /// EMATHGRP` case, reached here via `mathbot`). Content is erased
1876        /// (see [`MathElemCst`]'s doc comment).
1877        Group {
1878            mgrp: MathGroup<()>,
1879            #[group(self.mgrp)]
1880            elems: Vec<super::MathErased>,
1881        },
1882    }
1883
1884    /// One postfix script combo of a [`MathElemCst`] (`mathtop`'s
1885    /// `SUPERSCRIPT`/`SUBSCRIPT`/`PRIMES` suffixes, one at a time).
1886    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1887    pub enum MathScript {
1888        /// `^ group`
1889        Super { hat: SuperscriptTok, group: MathGroupArg },
1890        /// `_ group`
1891        Sub { under: SubscriptTok, group: MathGroupArg },
1892        /// A run of `'` marks — sugar for a superscript of primes
1893        /// characters; kept as its own token (not desugared here) since
1894        /// elaboration already special-cases it per `parser.mly`.
1895        Primes(PrimesTok),
1896    }
1897
1898    /// `mathgroup`: a script's operand is either a bracketed math group or a
1899    /// bare `mathbot`.
1900    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1901    pub enum MathGroupArg {
1902        Group {
1903            mgrp: MathGroup<()>,
1904            #[group(self.mgrp)]
1905            elems: Vec<super::MathErased>,
1906        },
1907        Bot(Box<MathBot>),
1908    }
1909
1910    /// `matharg` (parser.mly:1138-1146 + narg 1201-1210): one math-mode
1911    /// command argument — a mandatory body, a `?:`-supplied optional
1912    /// (UTOptionalArgument), or `?*` (UTOmission). The six body shapes live
1913    /// once in [`MathArgBody`]; Optional/Omission/Plain are first-token-disjoint.
1914    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1915    pub enum MathArg {
1916        Optional { q: OptionalTok, body: MathArgBody },
1917        Omission(OmissionTok),
1918        Plain(MathArgBody),
1919    }
1920
1921    /// The six body shapes shared by mandatory and `?:`-optional math args:
1922    /// a math/inline/block group, or a `!`-escaped program-mode value. The
1923    /// lexer already switches mode on the escape sigil (`!(` / `![` / `!(|` /
1924    /// `!{` / `!<` all emit ordinary `LParen`/`BList`/`BRecord`/`BHorzGrp`/
1925    /// `BVertGrp` tokens — see `lexer.rs`'s `lex_math`), so at the token
1926    /// level the escapes are indistinguishable from `Atomic`'s own
1927    /// `Paren`/`List`/`Record` shapes; reusing those bodies directly here
1928    /// (rather than going through a full `ExprErased`, which would also
1929    /// happily swallow a *following* `matharg` bracket group as a trailing
1930    /// application argument) keeps each `matharg` exactly one bracket group.
1931    /// NOT `Box<MathArg>`: a direct self-loop on a non-root type is what
1932    /// `#[recurse]` rejects, and upstream's grammar is non-recursive here
1933    /// anyway.
1934    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1935    pub enum MathArgBody {
1936        /// `{ math }`.
1937        Math {
1938            mgrp: MathGroup<()>,
1939            #[group(self.mgrp)]
1940            elems: Vec<super::MathErased>,
1941        },
1942        /// `!{ inline text }`.
1943        Inline {
1944            igrp: InlineGroup<()>,
1945            #[group(self.igrp)]
1946            elems: Vec<InlineElem>,
1947        },
1948        /// `!<block text>`.
1949        Block {
1950            bgrp: BlockGroup<()>,
1951            #[group(self.bgrp)]
1952            elems: Vec<BlockElem>,
1953        },
1954        /// `!(e)` / `!(e, e, …)`.
1955        ParenEscape {
1956            paren: ParenGroup<()>,
1957            #[group(self.paren)]
1958            inner: Box<ParenBody>,
1959        },
1960        /// `![e; …]`.
1961        ListEscape {
1962            list: ListGroup<()>,
1963            #[group(self.list)]
1964            items: Vec<ListItem>,
1965        },
1966        /// `!(|l = e; …|)`.
1967        RecordEscape {
1968            rec: RecordGroup<()>,
1969            #[group(self.rec)]
1970            body: RecordBody,
1971        },
1972    }
1973}
1974
1975/// A parse failure, positioned at the construct that caused it.
1976///
1977/// Defined in [`crate::parse_error`] and re-exported here, where it has always
1978/// been named from; that module also holds [`parse_file`]'s error rendering,
1979/// which [`crate::cst_v1::parse_file_v1`] needs verbatim.
1980pub use crate::parse_error::ParseFileError;
1981
1982/// Lex and parse a whole `.saty` source file.
1983pub fn parse_file(src: &str) -> Result<File, ParseFileError> {
1984    let atoms = crate::lexer::lex(src).map_err(ParseFileError::from_lex)?;
1985    let mut stream = crate::stream::AtomStream::new(atoms);
1986    match <File as Parse<_>>::parse(&mut stream) {
1987        Ok(file) => Ok(file),
1988        Err(e) => Err(crate::parse_error::locate(src, &stream, &e)),
1989    }
1990}