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