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        LetPatternIn {
705            kw: KwLet,
706            pat: super::PatErased,
707            eq: DefEqTok,
708            value: Box<Expr>,
709            in_kw: KwIn,
710            body: Box<Expr>,
711        },
712        /// `if cond then a else b` (`nxif`; `else` is never optional in
713        /// this grammar, so there is no dangling-else ambiguity).
714        If {
715            kw: KwIf,
716            cond: Box<Expr>,
717            then_kw: KwThen,
718            then_branch: Box<Expr>,
719            else_kw: KwElse,
720            else_branch: Box<Expr>,
721        },
722        /// `fun x y -> body` (`nxlambda`'s `LAMBDA argpats ARROW nxlor`
723        /// production, `parser.mly:713`). `argpats = list(patbot)`
724        /// upstream — a lambda's parameters are full `patbot`s, not merely
725        /// variables (e.g. the bundled `list.satyg`'s `mapi-adjacent`:
726        /// `fun (i, acc) x leftopt rightopt -> ..`, a tuple-DESTRUCTURING
727        /// first parameter), lowered by `curry_lambda_abstract_pattern` —
728        /// this port's `elaborate::rec_clause_value` (shared with
729        /// multi-clause `let-rec`, which faces the exact same
730        /// arity-preserving pattern-currying problem) reproduces that
731        /// directly, so this field is `PatBot`, matching `RecBinding`'s.
732        Fun {
733            kw: KwFun,
734            params: Vec<PatBot>,
735            arrow: ArrowTok,
736            body: Box<Expr>,
737        },
738        /// `fun ?(l = x, …) p -> body` — a SATySFi 0.1 labeled-optional
739        /// lambda unit (one `?(…)` bundle + one positional param). This is
740        /// an **additive** 0.1 node: 0.0.6 has no `?(…)` param bundle, so it
741        /// is reachable in a 0.0.6 parse only for input that used to be a
742        /// parse error (a leading `?` cannot begin `Fun`'s `Vec<PatBot>`),
743        /// where `elaborate` rejects it under a V0_0 [`crate::version`]
744        /// gate. The V0_1 pipeline reaches it by lowering a `cst_v1` param
745        /// bundle (multi-unit lambdas lower to a nested `FunRows`/`Fun`
746        /// chain). Placed right after [`Expr::Fun`] so a plain `fun x -> …`
747        /// still matches `Fun` first (its `?`-headed `opts` cannot begin a
748        /// `PatBot`, so `Fun` cleanly backtracks here for a bundled unit).
749        FunRows {
750            kw: KwFun,
751            opts: CstOptBinders,
752            param: PatBot,
753            arrow: ArrowTok,
754            body: Box<Expr>,
755        },
756        /// `match scrutinee with [|] pat [when g] -> body (| pat [when g] -> body)*`
757        Match {
758            kw: KwMatch,
759            scrutinee: Box<Expr>,
760            with_kw: KwWith,
761            leading_bar: Option<BarTok>,
762            first: MatchArm,
763            rest: Vec<BarArm>,
764        },
765        /// `let-mutable name <- init in body` (`nxletsub`'s `LETMUTABLE`
766        /// case; `init`/`body` are both `nxlet` in `parser.mly`, simplified
767        /// here to a direct `Expr` self-loop like `LetIn`).
768        LetMutableIn {
769            kw: KwLetMutable,
770            name: VarTok,
771            arrow: OverwriteEqTok,
772            init: Box<Expr>,
773            in_kw: KwIn,
774            body: Box<Expr>,
775        },
776        /// `let-math \cmd param* = expr in body` (`nxletsub`'s `LETMATH`
777        /// case, `parser.mly:688` — upstream's ONLY command binding with an
778        /// expression-level `in` form; `LETHORZ`/`LETVERT` stay
779        /// top-level-only, see the module doc comment on
780        /// [`super::TopBinding::LetInline`]/`LetBlock`). Same shape as
781        /// [`super::TopBinding::LetMath`] — no leading context variable,
782        /// `cmd` reuses the plain `HorzCmdTok` token — plus the `in body`
783        /// suffix; `Box<Expr>` self-loops on the recurse root like `LetIn`.
784        LetMathIn {
785            kw: KwLetMath,
786            cmd: HorzCmdTok,
787            params: Vec<Param>,
788            eq: DefEqTok,
789            value: Box<Expr>,
790            in_kw: KwIn,
791            body: Box<Expr>,
792        },
793        /// `open Name in body` (`nxletsub`'s `OPEN` case).
794        OpenIn {
795            kw: KwOpen,
796            name: CtorTok,
797            in_kw: KwIn,
798            body: Box<Expr>,
799        },
800        /// `while cond do body` (`nxwhl`; `body` is `nxwhl` itself in
801        /// `parser.mly`, i.e. right-nested `while`s — simplified here to a
802        /// plain `Expr`).
803        WhileDo {
804            kw: KwWhile,
805            cond: Box<Expr>,
806            do_kw: KwDo,
807            body: Box<Expr>,
808        },
809        /// `name <- value` (`nxlambda`'s `OVERWRITEEQ` case). Starts with a
810        /// bare `VarTok`, which is also how `Ops` can start (`x` alone) —
811        /// **must** stay before `Ops` so backtracking tries the `<-` shape
812        /// first. `value` is `nxlor` in `parser.mly`; routed through
813        /// `ExprErased` here rather than mirrored precisely, both to keep
814        /// `Expr` a singleton SCC and because this is already a `Var`-headed
815        /// alternative sitting awkwardly among the keyword-headed ones.
816        Overwrite {
817            name: VarTok,
818            arrow: OverwriteEqTok,
819            value: super::ExprErased,
820        },
821        /// The flattened binary-operator chain — see the module doc comment
822        /// on precedence flattening. Must stay last (no leading keyword).
823        Ops(OpChain),
824    }
825
826    /// One `name [: ty] [|] patbot* = value [| patbot* = value]*` clause
827    /// GROUP of a `let-rec` (also reused, from outside this module, by
828    /// top-level `let-rec`). `ascription` is `parser.mly`'s rarer
829    /// `COLON ty` type-annotated form (`recdecargpart`'s `COLON ty BAR`
830    /// alternative), e.g. the bundled `itemize.satyh`'s `let-rec
831    /// listing-item : context -> int -> bool -> bool -> itemize ->
832    /// block-boxes | ctx depth is-first is-last (Item(...)) = ..`. Parsed
833    /// but not enforced — there is no enforcement pass for value-level
834    /// ascriptions (only module `val`/`direct` signature items reach
835    /// `typecheck.rs`'s `command_scheme`/sig machinery) — so it is a
836    /// parse-and-ignore stand-in whose only job is making verbatim upstream
837    /// source parse. `params` is
838    /// `patbot*` (`recdecargpart`'s plain `argpats` form, optionally
839    /// preceded by a `leading_bar` — `recdecargpart`'s `BAR argpatlst`
840    /// alternative, used both for the OCaml-style "every clause, including
841    /// the first, gets a `|`" layout the bundled packages write, e.g.
842    /// `list.satyg`'s `let-rec map\n  | f [] = []\n  | f (x :: xs) = ..`,
843    /// and for the `COLON ty BAR` form above, whose single clause is *only*
844    /// reachable via a leading `|`). `extra` holds any further
845    /// `| patbot* = value` continuation clauses (`nxrecdecpar`) — SATySFi's
846    /// multi-clause pattern-matching function-definition sugar. Every
847    /// clause in the group must bind the same number of parameters (checked
848    /// at elaboration — upstream's `IllegalArgumentLength` — not here); the
849    /// (possibly plural) clauses desugar to one curried function that
850    /// matches a tuple of fresh parameters against each clause's patterns
851    /// in turn — see `elaborate.rs`'s `rec_clause_value`.
852    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
853    pub struct RecBinding {
854        pub name: super::BindName,
855        pub ascription: Option<RecAscription>,
856        pub leading_bar: Option<BarTok>,
857        pub params: Vec<PatBot>,
858        pub eq: DefEqTok,
859        pub value: super::ExprErased,
860        pub extra: Vec<RecClause>,
861    }
862
863    /// A `let-rec` binding's optional `: ty` ascription (see [`RecBinding`]'s
864    /// doc comment). A direct (non-erased) `TypeExpr` field: `RecBinding` is
865    /// already inside this `#[recurse]` module (embedded directly by
866    /// `Expr::LetRecIn`, not through an eraser), and connecting it straight
867    /// to `TypeExpr` — one of the module's three self-recursive SCC roots —
868    /// is exactly the same kind of cross-root DAG edge `RecBinding.params:
869    /// Vec<PatBot>` already makes to the `PatBot` root; `TypeExpr` never
870    /// refers back to `Expr`/`PatBot`/`RecBinding`, so no new cycle results.
871    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
872    pub struct RecAscription {
873        pub colon: ColonTok,
874        pub ty: TypeExpr,
875    }
876
877    /// A `| patbot* = value` continuation clause of a multi-clause
878    /// `let-rec` binding (see [`RecBinding`]'s doc comment).
879    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
880    pub struct RecClause {
881        pub bar: BarTok,
882        pub params: Vec<PatBot>,
883        pub eq: DefEqTok,
884        pub value: super::ExprErased,
885    }
886
887    /// An `and name param* = value` continuation of a `let-rec`.
888    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
889    pub struct AndBinding {
890        pub and_kw: KwAnd,
891        pub binding: RecBinding,
892    }
893
894    /// One `pat [when guard] -> body` match arm. The pattern and body sit
895    /// behind the stream-erasing wrappers (deref to reach the inner nodes).
896    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
897    pub struct MatchArm {
898        pub pat: super::PatErased,
899        pub guard: Option<Guard>,
900        pub arrow: ArrowTok,
901        pub body: super::ExprErased,
902    }
903
904    /// A match arm's `when cond` guard.
905    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
906    pub struct Guard {
907        pub when_kw: KwWhen,
908        pub cond: super::ExprErased,
909    }
910
911    /// A `| pat [when guard] -> body` continuation of a match's arm list.
912    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
913    pub struct BarArm {
914        pub bar: BarTok,
915        pub arm: MatchArm,
916    }
917
918    /// A flattened binary-operator chain: `head (op rhs)*`, left-folded
919    /// (with correct per-operator precedence/associativity) during
920    /// elaboration. `before` is `nxbfr`'s postfix (`e1 before e2`), attached
921    /// here rather than modeled at its own precedence level: `nxbfr` sits
922    /// between `nxif` and `nxlambda`, i.e. *above* `nxlor`/`OpChain`'s own
923    /// level, so `parser.mly`'s left operand is actually `nxlambda` (which
924    /// also covers `Fun`/`Overwrite`) — attaching to `OpChain` alone misses
925    /// `(fun x -> e1) before e2`/`(x <- e1) before e2` as the left operand;
926    /// such input is rejected here (a documented simplification, not a
927    /// silent misparse). `body` is threaded through `ExprErased` (not
928    /// boxed directly) to keep `Expr` a singleton SCC: a direct `Box<Expr>`
929    /// field on `OpChain` would make `OpChain` itself part of `Expr`'s SCC
930    /// (a second, non-`Expr`-variant self-loop edge), which is exactly the
931    /// multi-type-cycle shape the module doc warns about.
932    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
933    pub struct OpChain {
934        pub head: AppExpr,
935        pub tail: Vec<OpRhs>,
936        pub before: Option<BeforeTail>,
937    }
938
939    /// The `before body` suffix of an [`OpChain`] (`nxbfr`).
940    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
941    pub struct BeforeTail {
942        pub kw: KwBefore,
943        pub body: super::ExprErased,
944    }
945
946    /// One `op rhs` continuation of an [`OpChain`].
947    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
948    pub struct OpRhs {
949        pub op: BinOpTok,
950        pub rhs: AppExpr,
951    }
952
953    /// `nxun`/`nxapp`/`nxunsub` flattened: an optional leading unary minus,
954    /// an optional leading `!`/`!!`/... deref (`UNOP_EXCLAM`, `nxunsub`), an
955    /// atomic head with any `#label` field accesses (`nxbot ACCESS var`,
956    /// left-recursive in `parser.mly` — flattened to a postfix `Vec` here,
957    /// the same technique as `PatCons`'s `::`), and an application-chain
958    /// tail (`nxapp nxunsub` / `nxapp CONSTRUCTOR` / `nxapp OPTIONAL
959    /// nxunsub` / `nxapp OMISSION`, left-folded during elaboration).
960    /// `EXACT_AMP`/`EXACT_TILDE` (`&`/`~`) are the
961    /// staging prefixes, carried in `stage` (see [`StagePrefix`]). First-class command references (`command \cmd`,
962    /// upstream's `nxapp: COMMAND hcmd`) are modeled one level down, as
963    /// [`Atomic::Command`] — see its doc comment for the rationale.
964    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
965    pub struct AppExpr {
966        pub minus: Option<ExactMinusTok>,
967        pub stage: Option<StagePrefix>,
968        pub excl: Option<UnopExclamTok>,
969        pub head: Atomic,
970        pub head_accesses: Vec<AccessSeg>,
971        pub args: Vec<AppArg>,
972    }
973
974    /// A staging prefix on a `nxunsub` operand: `&e` builds code for the next
975    /// stage, `~e` splices the result of a previous-stage computation
976    /// (`parser.mly:796-797`, `UTNext`/`UTPrev`).
977    ///
978    /// Upstream spells these as alternatives of `nxunsub`, alongside the `!`
979    /// deref; this grammar flattens that level, so like `minus`/`excl` they
980    /// become an optional prefix field. The looser shape accepts a few
981    /// combinations upstream's grammar does not (`&!x`), which is the same
982    /// latitude `AppExpr` already takes for `-!x`.
983    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
984    pub enum StagePrefix {
985        /// `&e` — quote: the value is `e`'s code, to run one stage later.
986        Next(ExactAmpTok),
987        /// `~e` — splice: run `e` now and drop its code in here.
988        Prev(ExactTildeTok),
989    }
990
991    /// One `#label` field-access segment (`nxbot ACCESS var`).
992    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
993    pub struct AccessSeg {
994        pub hash: AccessTok,
995        pub label: VarTok,
996    }
997
998    /// One application-chain argument: an optional-argument value (`?:
999    /// arg`), an omitted optional argument (`?*`), a plain atomic value
1000    /// (with its own optional `!` prefix and `#access` suffixes, mirroring
1001    /// `AppExpr`'s head position — each `nxunsub`/`nxbot` in the `nxapp`
1002    /// chain is independent), or a bare constructor applied nullarily
1003    /// (`nxapp CONSTRUCTOR`).
1004    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1005    pub enum AppArg {
1006        /// `?: arg` (`nxapp OPTIONAL nxunsub`, simplified to a bare
1007        /// `Atomic` operand rather than the full `nxunsub`).
1008        Optional { q: OptionalTok, value: Atomic },
1009        /// `?*` (`nxapp OMISSION`).
1010        Omission(OmissionTok),
1011        Atom {
1012            stage: Option<StagePrefix>,
1013            excl: Option<UnopExclamTok>,
1014            atom: Atomic,
1015            accesses: Vec<AccessSeg>,
1016        },
1017        Ctor(CtorTok),
1018        /// `?(l = e, …) atom` — a SATySFi 0.1 labeled-optional application
1019        /// bundle paired with the positional argument it precedes (pairing
1020        /// them in one arm rejects a dangling trailing bundle `f x ?(l=1)` at
1021        /// parse time, as upstream does). Additive 0.1 node; the `?(`-head is
1022        /// token-disjoint from every 0.0.6 `AppArg` arm (0.0.6's `Optional`
1023        /// is `?:`-headed, a distinct token), so no previously-parsing input
1024        /// changes shape. Elaboration rejects it under a V0_0 version gate.
1025        Bundled {
1026            opts: CstOptArgs,
1027            excl: Option<UnopExclamTok>,
1028            atom: Atomic,
1029            accesses: Vec<AccessSeg>,
1030        },
1031        /// `?(l = e, …) Ctor` — as [`AppArg::Bundled`] but the positional
1032        /// argument is a bare constructor.
1033        BundledCtor { opts: CstOptArgs, ctor: CtorTok },
1034    }
1035
1036    /// A SATySFi 0.1 `?(l = x, …)` optional-parameter binder bundle (for
1037    /// [`Expr::FunRows`]): the `?` sigil, then a parenthesized `,`-separated
1038    /// list of `label = binder` entries.
1039    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1040    pub struct CstOptBinders {
1041        pub q: OptionalTypeTok,
1042        pub paren: ParenGroup<()>,
1043        #[group(self.paren)]
1044        pub entries: Vec<CstOptBinderEntry>,
1045    }
1046
1047    /// One `label = binder` entry of a [`CstOptBinders`] bundle (the last
1048    /// `,` is optional; `=` is upstream's `EXACT_EQ`, reusing [`DefEqTok`]).
1049    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1050    pub struct CstOptBinderEntry {
1051        pub label: VarTok,
1052        pub eq: DefEqTok,
1053        pub var: VarTok,
1054        pub comma: Option<CommaTok>,
1055    }
1056
1057    /// A SATySFi 0.1 `?(l = e, …)` optional-argument bundle (for
1058    /// [`AppArg::Bundled`]/[`AppArg::BundledCtor`]): the `?` sigil, then a
1059    /// parenthesized `,`-separated list of `label = expr` entries.
1060    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1061    pub struct CstOptArgs {
1062        pub q: OptionalTypeTok,
1063        pub paren: ParenGroup<()>,
1064        #[group(self.paren)]
1065        pub entries: Vec<CstOptArgEntry>,
1066    }
1067
1068    /// One `label = expr` entry of a [`CstOptArgs`] bundle — a FULL
1069    /// expression (`?(bias = 1 + n)`), routed through [`super::ExprErased`]
1070    /// so this satellite never joins `Expr`'s SCC.
1071    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1072    pub struct CstOptArgEntry {
1073        pub label: VarTok,
1074        pub eq: DefEqTok,
1075        pub value: super::ExprErased,
1076        pub comma: Option<CommaTok>,
1077    }
1078
1079    /// `nxbot` (plus the ctor-head case usually found in `nxun`): an atomic
1080    /// expression.
1081    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1082    pub enum Atomic {
1083        Length(LengthTok),
1084        Float(FloatTok),
1085        Int(IntTok),
1086        Literal(LiteralTok),
1087        True(KwTrue),
1088        False(KwFalse),
1089        /// A bare constructor, e.g. `None`, or the head of `Some 1`.
1090        Ctor(CtorTok),
1091        Var(VarTok),
1092        /// `Mod.x` — a module-qualified variable (`VARWITHMOD`).
1093        VarWithMod(VarWithModTok),
1094        /// `( ‹op› )` — a bare reference to a (possibly user-defined)
1095        /// operator as a first-class value, e.g. `(+++)`, `(-->)`
1096        /// (`nxbot`'s `LPAREN binop RPAREN` alternative — the same syntax
1097        /// `super::BindName` accepts in binding position, here used as an
1098        /// ordinary atomic expression). Resolves via the same name
1099        /// `BinOpTok::op_text` yields, exactly like `Var` (`elaborate.rs`).
1100        OpRef(OpNameTok),
1101        /// `command \cmd` (upstream `nxapp: COMMAND hcmd` →
1102        /// `UTContentOf(mods, csnm)`): a first-class *value* that simply
1103        /// names an inline command's own binding — no argument tail, so
1104        /// modeling it as an atom (rather than upstream's `nxapp` level)
1105        /// is strictly simpler and covers every bundled usage (always
1106        /// parenthesized, e.g. `(command \math)`). Only the horizontal
1107        /// form is spelled upstream; if a package ever writes `command
1108        /// +cmd`/a math form, extend with an `AnyVertCmdTok`/math
1109        /// alternative then.
1110        Command { kw: CommandTok, name: AnyHorzCmdTok },
1111        /// `()`
1112        Unit { paren: UnitParen },
1113        /// `( expr )` or `( expr, expr, … )` (the latter elaborates to a
1114        /// tuple).
1115        Paren {
1116            paren: ParenGroup<()>,
1117            #[group(self.paren)]
1118            inner: Box<ParenBody>,
1119        },
1120        /// `Mod.(e)` ≡ `open Mod in e` (`nxbot`'s `OPENMODULE nxlet RPAREN`
1121        /// production). Reuses `ParenBody` exactly like `Atomic::Paren`
1122        /// above (so `Mod.(e, e, …)` would elaborate to a tuple the same
1123        /// way, though no bundled package writes it that way) — the `Mod.(`
1124        /// sigil is the open delimiter (`OpenModuleTok`, carrying the
1125        /// module name), closed by a plain `)`. Elaborated via the same
1126        /// machinery as `Expr::OpenIn` (`elaborate.rs`'s `open_module`
1127        /// helper).
1128        OpenModule {
1129            grp: OpenModuleGroup<()>,
1130            #[group(self.grp)]
1131            body: Box<ParenBody>,
1132        },
1133        /// `(| label = expr; … |)` or `(| base with label = expr; … |)`
1134        /// (`nxrecordsynt`; see [`RecordBody`]).
1135        Record {
1136            rec: RecordGroup<()>,
1137            #[group(self.rec)]
1138            body: RecordBody,
1139        },
1140        /// `[ expr; … ]`
1141        List {
1142            list: ListGroup<()>,
1143            #[group(self.list)]
1144            items: Vec<ListItem>,
1145        },
1146        /// `{ inline text }`
1147        InlineText {
1148            igrp: InlineGroup<()>,
1149            #[group(self.igrp)]
1150            elems: Vec<InlineElem>,
1151        },
1152        /// `'< block text >`
1153        BlockText {
1154            bgrp: BlockGroup<()>,
1155            #[group(self.bgrp)]
1156            elems: Vec<BlockElem>,
1157        },
1158        /// `${ math }` (`nxbot`'s `BMATHGRP mathblock EMATHGRP` case).
1159        MathText {
1160            mgrp: MathGroup<()>,
1161            #[group(self.mgrp)]
1162            elems: Vec<super::MathErased>,
1163        },
1164    }
1165
1166    /// `(| … |)`'s content: either a plain field list, or a *record update*
1167    /// `base with l = e; …` (`nxrecordsynt`'s third alternative). `Update`
1168    /// is tried first (it backtracks cleanly to `Fields` — parsing `base`
1169    /// as an expression stops right before a bare `label = expr`'s `=`,
1170    /// since `=` isn't a valid expression continuation, so the `with`
1171    /// keyword check fails fast and `Fields` picks it up). `base` is
1172    /// `nxbot` in `parser.mly` (an atomic expression); routed through
1173    /// `ExprErased` here instead, which is strictly more permissive
1174    /// (accepts any expression as the base, not just an atomic one) — a
1175    /// deliberate simplification, and also the only way to reference it
1176    /// without adding a second, non-`Group` recursion edge into `Atomic`
1177    /// (which — like `AppExpr`/`OpChain` — is *not* itself part of `Expr`'s
1178    /// SCC, and should stay that way).
1179    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1180    pub enum RecordBody {
1181        Update {
1182            base: super::ExprErased,
1183            with_kw: KwWith,
1184            fields: Vec<RecordField>,
1185        },
1186        Fields(Vec<RecordField>),
1187    }
1188
1189    /// The parenthesized-expression group's content: one expression, plus
1190    /// any `, expr` continuations (present only for a tuple).
1191    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1192    pub struct ParenBody {
1193        pub first: super::ExprErased,
1194        pub rest: Vec<CommaExpr>,
1195    }
1196
1197    /// A `, expr` continuation inside a parenthesized tuple.
1198    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1199    pub struct CommaExpr {
1200        pub comma: CommaTok,
1201        pub value: super::ExprErased,
1202    }
1203
1204    /// One record field `label = expr;` (the last `;` is optional).
1205    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1206    pub struct RecordField {
1207        pub name: VarTok,
1208        pub eq: DefEqTok,
1209        pub value: super::ExprErased,
1210        pub semi: Option<ListPunctTok>,
1211    }
1212
1213    /// One list element `expr;` (the last `;` is optional).
1214    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1215    pub struct ListItem {
1216        pub value: super::ExprErased,
1217        pub semi: Option<ListPunctTok>,
1218    }
1219
1220    /// One inline-text element (`ih`/`ihtext`/`ihcmd` in parser.mly).
1221    ///
1222    /// `ItemBullet`/`Sep` are flat markers rather than the nested tree
1223    /// `parser.mly` builds in-grammar (`sxsep`'s `nonempty_list(sxitem)` /
1224    /// `sxlist`): since `InlineText`'s content is already a flat
1225    /// `Vec<InlineElem>`, regrouping consecutive `ItemBullet`-headed runs
1226    /// into an itemize tree (and `Sep`-delimited runs into columns, for
1227    /// tabular/math use later) is deferred to the elaborator. Token-level
1228    /// round-tripping is unaffected either way.
1229    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1230    pub enum InlineElem {
1231        Char(CharTok),
1232        /// A backtick literal written inside inline text (`` `…` ``). Its own
1233        /// arm, not a `Char` run, so the elaborator can dispatch it through the
1234        /// context's code-text command — see `Token::CodeText`.
1235        CodeText(CodeTextTok),
1236        Space(SpaceTok),
1237        Break(BreakTok),
1238        /// `#var;` — embeds a program variable's value as inline content.
1239        Embed { var: VarInHorzTok, semi: EndActiveTok },
1240        /// `${ math }` — embeds math content as inline text (`ihcmd`'s
1241        /// `BMATHGRP mathblock EMATHGRP` case).
1242        EmbedMath {
1243            mgrp: MathGroup<()>,
1244            #[group(self.mgrp)]
1245            elems: Vec<super::MathErased>,
1246        },
1247        /// `\cmd …` (`name` also accepts the module-qualified
1248        /// `\Mod.cmd` form).
1249        Cmd { name: AnyHorzCmdTok, tail: CmdTail },
1250        /// An itemize bullet (`*`+) marker — see the variant-group doc above.
1251        ItemBullet(ItemTok),
1252        /// A `|` separator marker — see the variant-group doc above.
1253        Sep(SepTok),
1254    }
1255
1256    /// One block-text element (`vxbot`).
1257    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1258    pub enum BlockElem {
1259        /// `#var;` — embeds a program variable's value as block content.
1260        Embed { var: VarInVertTok, semi: EndActiveTok },
1261        /// `+cmd …` (`name` also accepts the module-qualified
1262        /// `+Mod.cmd` form).
1263        Cmd { name: AnyVertCmdTok, tail: CmdTail },
1264    }
1265
1266    /// A command's arguments (`narg* sargs` in parser.mly, upstream's own
1267    /// dedicated grammar — *not* a reuse of the general application chain
1268    /// like `AppExpr`). Either a bare `;` (no arguments) or a flat,
1269    /// non-empty sequence of [`AppArg`]s: each is `?: value` (a supplied
1270    /// optional `narg`), `?*` (an omitted optional `narg`), or a plain
1271    /// (possibly `!`/`#access`-decorated) atomic value — `(expr)`,
1272    /// `(|record|)`, `[list]`, `{inline}`, `<block>`, a bare ctor, etc. —
1273    /// covering both `narg`'s mandatory forms and `sargs`'s group forms
1274    /// uniformly (this port's usual simplification: `AppArg::Atom`'s
1275    /// `Atomic` already spans every shape upstream splits across `narg`/
1276    /// `sargs`). Optional/omitted `narg`s may lead (`\ref?:(x){text}`,
1277    /// `\ref?*{text}`) since every element is independently one `AppArg` —
1278    /// an `Expr`-based encoding could not, its head being a plain atom.
1279    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1280    pub enum CmdTail {
1281        /// `;` — no arguments.
1282        Semi(EndActiveTok),
1283        /// The argument chain: at least one [`AppArg`], via [`super::AppArgErased`].
1284        Args {
1285            first: super::AppArgErased,
1286            rest: Vec<super::AppArgErased>,
1287            semi: Option<EndActiveTok>,
1288        },
1289    }
1290
1291    /// `patas`: a pattern, plus an optional `as name` binding.
1292    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1293    pub struct Pattern {
1294        pub head: PatCons,
1295        pub as_clause: Option<AsClause>,
1296    }
1297
1298    /// The `as name` suffix of a pattern.
1299    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1300    pub struct AsClause {
1301        pub as_kw: KwAs,
1302        pub name: VarTok,
1303    }
1304
1305    /// One curried parameter of an ordinary (non-`let-rec`) `let`, or of a
1306    /// `let-inline`/`let-block`/`let-math` command binding — upstream's
1307    /// `arg` nonterminal (`nxnonrecdec`'s `argpart`/`cmdarglst`,
1308    /// `parser.mly:622-624`: `arg: patbot | OPTIONAL defedvar`): a full
1309    /// pattern, or the def-site optional-parameter marker `?:name`
1310    /// (`parser.mly`'s `OPTIONAL vartok`), e.g. `stdja.satyh`'s `let
1311    /// document record ?:configopt inner = ..` and `annot.satyh`'s
1312    /// `let-inline ctx \href ?:borderopt uri inner = ..`. Upstream's
1313    /// `let-rec`/`fun` argument grammar (`recdecargpart`/`argpats` —
1314    /// [`RecBinding`]/[`AndBinding`]/`Expr::Fun`) has no such alternative,
1315    /// only plain `let` and the three command-binding forms do — all four
1316    /// keep `Vec<Param>` ([`super::TopLet`], `Expr::LetIn`,
1317    /// [`super::TopBinding::LetInline`]/`LetBlock`/`LetMath`,
1318    /// [`Expr::LetMathIn`]). Elaborated (`elaborate.rs`) by widening
1319    /// `Optional` to `PatBot::Var` (`params_to_patbots`) before the ordinary
1320    /// pattern-currying machinery runs (plain `let`'s `rec_clause_value`, or
1321    /// a command binding's `curry_cmd_params`) — the `?:` marker carries no
1322    /// further semantics of its own in this port (`typecheck.rs`'s
1323    /// `command_scheme` doc comment: optionality is inferred structurally,
1324    /// not from this marker); for a command binding, the maximal *leading*
1325    /// run of `?:`-marked params is additionally counted by `elaborate.rs`'s
1326    /// `leading_optional_count` and recorded into the binding's
1327    /// `Scope::optional_arity`, so a marker-less call site can auto-omit
1328    /// those slots (see `cmd_args`/`math_bot`'s `Cmd` arm).
1329    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1330    pub enum Param {
1331        Optional { q: OptionalTok, name: VarTok },
1332        Pat(PatBot),
1333        /// A SATySFi 0.1 `?(l = x, …)` labeled-optional command-parameter
1334        /// bundle, lowered from
1335        /// `cst_v1::Param { opts: Some(_), body }` by
1336        /// `v1/lower.rs::lower_command_params`. Reuses [`CstOptBinders`]
1337        /// verbatim (the same node a value-level `fun ?(l = x) p -> ..`
1338        /// bundle lowers to) — `?(`-headed, so distinct from
1339        /// the 0.0.6 `?:`-headed [`Param::Optional`] above (no arm overlap,
1340        /// no grammar ambiguity: this variant is never PARSED directly by
1341        /// this 0.0.6-frozen `cst.rs`, only ever *constructed* by the 0.1
1342        /// lowering path). Consumed by `elaborate.rs`'s bundle-aware
1343        /// `curry_cmd_params_v1`, which emits `Ast::LambdaOpt` for it — see
1344        /// that function's doc comment.
1345        Bundled { opts: CstOptBinders, body: PatBot },
1346    }
1347
1348    /// `pattr`: a `patbot`, followed by any number of `:: patbot` segments.
1349    /// `parser.mly` writes this as right recursion (`patbot :: pattr`, always
1350    /// fine, unlike left recursion) but it is flattened to a `Vec` here (the
1351    /// same right-fold-at-elaboration technique as `OpChain`, `::` being
1352    /// right-associative) so that `PatCons` need not be self-referential: a
1353    /// `PatCons`/`ConsRest` pair of mutually-referencing wrapper structs
1354    /// would form a 2-cycle with no self-loop of its own, which the
1355    /// `#[recurse]` depth engine rejects ("a sub-cycle running entirely
1356    /// through non-root types").
1357    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1358    pub struct PatCons {
1359        pub head: PatBot,
1360        pub tail: Vec<ConsSeg>,
1361    }
1362
1363    /// One `:: patbot` continuation of a cons pattern.
1364    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1365    pub struct ConsSeg {
1366        pub cons: ConsTok,
1367        pub tail: PatBot,
1368    }
1369
1370    /// `patbot`, plus the constructor-pattern forms `pattr` adds in
1371    /// `parser.mly` (folded in here to keep `PatCons` a plain struct).
1372    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1373    pub enum PatBot {
1374        /// `Ctor patbot` — a constructor applied to one argument pattern.
1375        CtorApplied { ctor: CtorTok, arg: Box<PatBot> },
1376        /// A bare (nullary) constructor pattern.
1377        Ctor(CtorTok),
1378        Int(IntTok),
1379        True(KwTrue),
1380        False(KwFalse),
1381        Str(LiteralTok),
1382        Wild(WildcardTok),
1383        Var(VarTok),
1384        /// `()`
1385        Unit { paren: UnitParen },
1386        /// `( pat )` or `( pat, pat, … )` (the latter elaborates to a tuple
1387        /// pattern).
1388        Paren {
1389            paren: ParenGroup<()>,
1390            #[group(self.paren)]
1391            inner: Box<PatternParenBody>,
1392        },
1393        /// `[ pat; … ]` (also matches `[]`).
1394        List {
1395            plist: ListGroup<()>,
1396            #[group(self.plist)]
1397            items: Vec<PatListItem>,
1398        },
1399    }
1400
1401    /// The parenthesized-pattern group's content: one pattern, plus any
1402    /// `, pat` continuations (present only for a tuple pattern).
1403    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1404    pub struct PatternParenBody {
1405        pub first: super::PatErased,
1406        pub rest: Vec<CommaPattern>,
1407    }
1408
1409    /// A `, pat` continuation inside a parenthesized tuple pattern.
1410    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1411    pub struct CommaPattern {
1412        pub comma: CommaTok,
1413        pub value: super::PatErased,
1414    }
1415
1416    /// One list-pattern element `pat;` (the last `;` is optional).
1417    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1418    pub struct PatListItem {
1419        pub value: super::PatErased,
1420        pub semi: Option<ListPunctTok>,
1421    }
1422
1423    /// A minimal type-expression grammar for `type` declarations and
1424    /// signature (`val .. : ty`) annotations (`txfunc`/`txprod`/`txapppre`/
1425    /// `txapp`/`txbot`, simplified). Function arrows (right-associative,
1426    /// with an optional-argument `?->` prefix chain — see [`OptArrowDom`]),
1427    /// 2+-way product types (`*`, [`TypeProd`]), a SINGLE-argument postfix
1428    /// type-constructor application (`'a option`, `'a list`; see
1429    /// [`TypeApp`]), command-argument-list types (`[ty; ty?; ..]
1430    /// inline-cmd`/`block-cmd`/`math-cmd`; see [`TypeAtom::Cmd`]),
1431    /// parenthesized grouping, closed record types (`(| l : ty; … |)`; see
1432    /// [`TypeAtom::Record`]), bare/qualified names, and type variables are
1433    /// supported; N-ary applied constructors are not — such input is
1434    /// rejected with a parse error. Self-recursive only through `Fun`'s
1435    /// codomain (right recursion); parenthesized nesting goes through the
1436    /// [`super::TyErased`] leaf.
1437    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1438    pub enum TypeExpr {
1439        /// `opts?-> dom -> cod` (right-associative). `dom` is a [`TypeProd`]
1440        /// (not just [`TypeAtom`]) so e.g. `'a option -> 'b option` and `'a *
1441        /// 'b -> 'c` both parse at their expected precedence (application
1442        /// binds tighter than `*`, which binds tighter than `->`). `opts` is
1443        /// upstream's `txfuncopts` prefix (`parser.mly:880-882`): zero or
1444        /// more `ty ?->` domains greedily consumed *before* the final
1445        /// mandatory `dom -> cod`, e.g. `config ?-> block-text -> document`
1446        /// parses as `opts = [config]`, `dom = block-text`, `cod =
1447        /// document`. Lowered (`typecheck.rs`) to an `option`-wrapped
1448        /// mandatory domain per optional entry — see that module's doc
1449        /// comment on `lower_type_expr`.
1450        Fun {
1451            opts: Vec<OptArrowDom>,
1452            dom: TypeProd,
1453            arrow: ArrowTok,
1454            cod: Box<TypeExpr>,
1455        },
1456        /// The non-arrow fallthrough. Despite the name (kept stable — see
1457        /// the module's compile-time-blowup note on why every recursion
1458        /// edge here is deliberate), this holds a full [`TypeProd`], not a
1459        /// bare [`TypeAtom`]: a product/application with no enclosing arrow
1460        /// is still just "the whole type expression minus `->`".
1461        Atom(TypeProd),
1462        /// `?(l1 : ty1, …) dom -> cod` — a SATySFi 0.1 labeled-optional
1463        /// function TYPE domain (upstream `typ`'s second production,
1464        /// `parser_v1.mly:688-691`). Lowered
1465        /// (`typecheck.rs`) to `MonoType::Func(Row::Cons(l1, ty1, …
1466        /// Row::Empty), dom, cod)` — a CLOSED row, matching what
1467        /// `Ast::LambdaOpt` infers, so an explicit `?(l:τ)->`
1468        /// signature unifies against an actual `?(l=x)`-taking function.
1469        /// `?`-headed — token-disjoint from `Fun`/`Atom` (neither
1470        /// [`TypeProd`] nor [`TypeAtom`] can start with [`OptionalTypeTok`]),
1471        /// so declared order is safety-neutral; appended last, this file's
1472        /// convention for 0.1 additions. It widens the 0.0.6 ACCEPT surface:
1473        /// a 0.0.6 program containing `?(l : int) -> int` parses here and
1474        /// reaches `typecheck.rs::lower_type_expr`'s version gate, which
1475        /// rejects it under `V0_0` with a version-error message (better
1476        /// diagnostics than a parse error).
1477        OptRowFun {
1478            opt_dom: CstTypeOptDom,
1479            dom: TypeProd,
1480            arrow: ArrowTok,
1481            cod: Box<TypeExpr>,
1482        },
1483    }
1484
1485    /// `?(l = ty, …)` — the closed labeled-optional-domain prefix of
1486    /// [`TypeExpr::OptRowFun`]. No row-variable-tail field: row-tailed
1487    /// optional domains need signature-level row quantification
1488    /// (`parser_v1.mly`'s `rowquant`/`quant`) — not implemented here;
1489    /// `cst_v1`'s own `TypeOptDomInnerV1` models the tail at parse
1490    /// level and rejects it with a `LowerError` before ever reaching here
1491    /// (`v1/lower.rs`).
1492    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1493    pub struct CstTypeOptDom {
1494        pub q: OptionalTypeTok,
1495        pub paren: ParenGroup<()>,
1496        #[group(self.paren)]
1497        pub entries: Vec<CstTypeOptEntry>,
1498    }
1499
1500    /// One `label : ty,` entry of a [`CstTypeOptDom`] (last `,` optional —
1501    /// matching the 0.1 lowering convention this file's other additive nodes
1502    /// use, e.g. [`CstOptArgEntry`], rather than the frozen grammar's `;`).
1503    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1504    pub struct CstTypeOptEntry {
1505        pub label: VarTok,
1506        pub colon: ColonTok,
1507        pub ty: super::TyErased,
1508        pub comma: Option<CommaTok>,
1509    }
1510
1511    /// One `ty ?->` leading domain of a [`TypeExpr::Fun`]'s optional-argument
1512    /// prefix (`parser.mly`'s `txfuncopts`, 880-882).
1513    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1514    pub struct OptArrowDom {
1515        pub ty: TypeProd,
1516        pub arrow: OptionalArrowTok,
1517    }
1518
1519    /// `txprod`: one or more `*`-separated [`TypeApp`]s (a product type),
1520    /// or just a single one if there's no `*` at all — flattened to a
1521    /// `Vec` (the same deferred-fold technique as `OpChain`/`PatCons`)
1522    /// rather than modeled as its own right-recursive rule, keeping
1523    /// `TypeExpr` a singleton SCC.
1524    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1525    pub struct TypeProd {
1526        pub first: TypeApp,
1527        pub rest: Vec<StarType>,
1528    }
1529
1530    /// A `* ty` continuation of a [`TypeProd`].
1531    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1532    pub struct StarType {
1533        pub star: ExactTimesTok,
1534        pub ty: TypeApp,
1535    }
1536
1537    /// `txapp` — a postfix type application `arg1 arg2 … argN ctor`, upstream's
1538    /// N-ary chain flattened into a greedy atom run (the way `OpChain`/`PatCons`
1539    /// flatten their own left-recursions). [`head`](TypeApp::head) is always
1540    /// present; when [`rest`](TypeApp::rest) is non-empty the LAST atom is the
1541    /// type constructor (a bare or `Mod.`-qualified name — `list`/`option`/
1542    /// `result`/`Eq.t`/`implicit`) and every atom before it (including `head`)
1543    /// is one of its arguments. This is unambiguous because SATySFi always
1544    /// parenthesizes a nested single-argument application (`('a list) list`,
1545    /// `('a t) implicit` — never `'a list list`), so a flat run of atoms can
1546    /// only be one constructor applied to the preceding arguments:
1547    ///
1548    /// - `int` → `head = int`, `rest = []` (a bare atom).
1549    /// - `'a option` → `head = 'a`, `rest = [option]` (one arg).
1550    /// - `'a 'e result` → `head = 'a`, `rest = ['e, result]` (`satysfi-base`'s
1551    ///   two-parameter `result`/`either`/`t`/`map`).
1552    /// - `('a Eq.t) implicit` → `head = ('a Eq.t)`, `rest = [implicit]`
1553    ///   (`satysfi-base`'s typeclass-dictionary marker).
1554    ///
1555    /// Elaboration (`typecheck::lower_type_app`) does the head/args/ctor split;
1556    /// the grammar itself is a plain, always-terminating greedy `Vec<TypeAtom>`
1557    /// (each atom consumes ≥1 token, stopping at the first non-atom — `->`,
1558    /// `*`, `)`, `;`, …). Both the unqualified and `Mod.`-qualified constructor
1559    /// forms fall out for free, since a `Mod.t` ctor is just a
1560    /// [`TypeAtom::NameMod`] like any other atom.
1561    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1562    pub struct TypeApp {
1563        pub head: TypeAtom,
1564        pub rest: Vec<TypeAtom>,
1565    }
1566
1567    /// An atomic type expression.
1568    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1569    pub enum TypeAtom {
1570        /// `[ty; ty?; ..] inline-cmd` / `block-cmd` / `math-cmd`
1571        /// (`parser.mly`'s `txapppre` command-type productions, 903-919) —
1572        /// tried first: unambiguous, since no other `TypeAtom` starts with
1573        /// `[` (`BListTok`). Each `;`-separated element is a [`TypeCmdArgItem`].
1574        Cmd {
1575            list: ListGroup<()>,
1576            #[group(self.list)]
1577            args: Vec<TypeCmdArgItem>,
1578            kind: CmdTypeKind,
1579        },
1580        /// `( ty )`
1581        Paren {
1582            paren: ParenGroup<()>,
1583            #[group(self.paren)]
1584            inner: super::TyErased,
1585        },
1586        /// `(| l1 : ty1; l2 : ty2; … |)` — a closed record type (`txbot`'s
1587        /// `txrecord` case, `parser.mly:955-961`), lowered to
1588        /// `MonoType::Record` (a `Row::Cons` chain ending in `Row::Empty` —
1589        /// see `typecheck.rs`'s `lower_type_atom`). Distinguished from
1590        /// [`TypeAtom::Paren`] (opens on plain `LParenTok`, i.e. `(`) and
1591        /// from a record-VALUE expression ([`Atomic::Record`], a different
1592        /// grammar position — only reachable where an `Expr` is expected,
1593        /// never in type position) purely by lexer-level delimiter token:
1594        /// `(|`/`|)` lex as the dedicated `BRecordTok`/`ERecordTok` pair
1595        /// (same as [`super::RecordKind`]'s use at `constraint 'a :: (|…|)`), so no
1596        /// backtracking between any of these three shapes is needed.
1597        Record {
1598            rec: RecordGroup<()>,
1599            #[group(self.rec)]
1600            fields: Vec<TypeRecordField>,
1601        },
1602        /// A type variable, e.g. `'a`.
1603        Var(TypeVarTok),
1604        /// An unqualified type name, e.g. `int`, `string`.
1605        Name(VarTok),
1606        /// `Mod.t` — a bare module-qualified type name in atomic (0-ary,
1607        /// non-applied) position, e.g. `Eq.t` in `val eq : Eq.t -> Eq.t ->
1608        /// ordering`. Sibling of [`Name`](TypeAtom::Name) (not a widened
1609        /// field on it); as the last atom of a [`TypeApp`] it is a
1610        /// module-qualified type constructor (`int M.t`, `ordering Eq.t`).
1611        /// **Tried after `Name`** only by placement convention (the two are
1612        /// token-disjoint: `VarTok`/`VarWithModTok` are separate lexer
1613        /// tokens, so there is no real backtracking ambiguity between them).
1614        NameMod(VarWithModTok),
1615        /// `(| l1 : ty1, … | ?'r |)` — a SATySFi 0.1 OPEN record type: a
1616        /// row-variable tail after the fields (upstream `typ_bot`'s SECOND
1617        /// `L_RECORD`/`R_RECORD` production, `parser_v1.mly:748-749`).
1618        /// Lowered (`typecheck.rs`) to
1619        /// `MonoType::Record(Row::Cons(l1, ty1, … Row::Var(fresh)))` — the
1620        /// row variable unifies structurally as an open record's tail
1621        /// (permitting additional fields at the unification site), reusing
1622        /// the existing generic `Row`/`RowVarRef`/`unify_row` machinery — no
1623        /// new type machinery needed. Genuinely a NEW shape, not a widening
1624        /// of the frozen [`TypeAtom::Record`] (0.0.6's `txrecord` grammar has
1625        /// no row-var tail at all, confirmed by grep of upstream
1626        /// `parser.mly`). Comma-separated fields, matching this file's 0.1
1627        /// additive
1628        /// nodes ([`CstOptArgEntry`], [`CstOptBinderEntry`]) rather than the
1629        /// frozen `Record`'s upstream-0.0.6 `;` separator. Unreachable from a
1630        /// `V0_0` token stream by construction: [`RowVarTok`] is only ever
1631        /// emitted by the lexer under [`crate::version::RustyfiVersion::
1632        /// V0_1`] (`lexer.rs`'s `'?'` arm), so no 0.0.6 parse can ever
1633        /// produce this variant — no elaborate/typecheck-time version gate
1634        /// is needed here (contrast [`TypeExpr::OptRowFun`], which IS
1635        /// reachable from 0.0.6 lexing and so DOES need one).
1636        // NOTE the group field is `orec`, not `rec`: syan names a group
1637        // substruct after (group-field name, ENUM name) with no variant
1638        // component, so a second `rec` group in `TypeAtom` collides with
1639        // `Record`'s (E0428 + E0119, and the survivor has the wrong fields).
1640        RecordOpen {
1641            orec: RecordGroup<()>,
1642            #[group(self.orec)]
1643            inner: CstRecordOpenInner,
1644        },
1645    }
1646
1647    /// A [`TypeAtom::RecordOpen`]'s group content: one or more `,`-separated
1648    /// fields (nonempty enforced at lowering, matching the closed form),
1649    /// then a mandatory `| ?'r` row-variable tail.
1650    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1651    pub struct CstRecordOpenInner {
1652        pub fields: Vec<CstRecordOpenField>,
1653        pub bar: BarTok,
1654        pub var: RowVarTok,
1655    }
1656
1657    /// One `l : ty,` field of a [`TypeAtom::RecordOpen`] (last `,` optional).
1658    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1659    pub struct CstRecordOpenField {
1660        pub name: VarTok,
1661        pub colon: ColonTok,
1662        pub ty: super::TyErased,
1663        pub comma: Option<CommaTok>,
1664    }
1665
1666    /// One `l : ty;` field of a [`TypeAtom::Record`] (`txrecord`,
1667    /// `parser.mly:962-965`) — sibling of [`super::RecordKindField`], but
1668    /// (unlike that struct, defined *outside* the `#[recurse]` module and so
1669    /// free to hold a direct `ast::TypeExpr` field) this one lives inside
1670    /// `TypeAtom`'s own SCC, so the field type is routed through
1671    /// [`super::TyErased`] instead — a direct `ast::TypeExpr` field here
1672    /// would close a fresh cycle back through `TypeAtom` itself (the same
1673    /// hazard [`TypeCmdArgItem`]'s doc comment explains).
1674    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1675    pub struct TypeRecordField {
1676        pub name: VarTok,
1677        pub colon: ColonTok,
1678        pub ty: super::TyErased,
1679        pub semi: Option<ListPunctTok>,
1680    }
1681
1682    /// One `;`-separated element of a [`TypeAtom::Cmd`]'s bracketed argument
1683    /// list: a mandatory `ty`, or an optional `ty?` (`parser.mly`'s `txlist`,
1684    /// 955-960) — routed through [`super::TyErased`] rather than the
1685    /// narrower `TypeApp` upstream uses, both to stay a DAG leaf (a direct
1686    /// `TypeApp` field here would close `TypeAtom -> Cmd -> ... -> TypeApp ->
1687    /// TypeAtom`, a fresh cycle through non-root types — see
1688    /// `AppArgErased`'s doc comment for the identical hazard) and per this
1689    /// port's usual permissive-superset simplification.
1690    ///
1691    /// `opt_labels` is the lowered
1692    /// `?(l:τ,…)` command-type row PREFIX on this slot (`TypeCmdOptDomV1` at
1693    /// the `cst_v1` side): a flat list of `label : ty` fields, no wrapping
1694    /// `?(` sigil/group of its own at this (already-lowered) target — purely
1695    /// a data carrier, populated by `v1/lower.rs::lower_type_cmd_args` and
1696    /// read by `typecheck.rs`'s `lower_type_atom` `Cmd` arm. Every
1697    /// 0.0.6-parsed fixture yields `opt_labels == []`: no real 0.0.6
1698    /// `TypeCmdArgItem` position can contain a bare `label :` shape (0.0.6's
1699    /// grammar has no colon here at all).
1700    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1701    pub struct TypeCmdArgItem {
1702        pub opt_labels: Vec<TypeCmdOptField>,
1703        pub ty: super::TyErased,
1704        pub opt: Option<OptionalTypeTok>,
1705        pub semi: Option<ListPunctTok>,
1706    }
1707
1708    /// One `label : ty,` field of a [`TypeCmdArgItem::opt_labels`] bundle
1709    /// (the last `,` is optional, matching
1710    /// this port's other 0.1-additive comma-separated satellite fields —
1711    /// [`CstOptBinderEntry`], [`CstTypeOptEntry`]).
1712    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1713    pub struct TypeCmdOptField {
1714        pub label: VarTok,
1715        pub colon: ColonTok,
1716        pub ty: super::TyErased,
1717        pub comma: Option<CommaTok>,
1718    }
1719
1720    /// The command-type keyword closing a [`TypeAtom::Cmd`]'s bracketed list.
1721    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1722    pub enum CmdTypeKind {
1723        Inline(HorzCmdTypeTok),
1724        Block(VertCmdTypeTok),
1725        Math(MathCmdTypeTok),
1726    }
1727
1728    /// `mathtop`: one math element, i.e. a `mathbot` base with any postfix
1729    /// `^`/`_`/`'` script combos (`mathtop`'s seven alternatives, flattened
1730    /// to a `Vec` in source order — the same `Ops`/`OpChain` deferred-
1731    /// precedence technique, since combos 3–6 interleave sub/superscript
1732    /// application order in a way elaboration is better placed to resolve).
1733    ///
1734    /// **No direct self-loop.** Unlike `Expr`/`PatBot`/`TypeExpr`, this
1735    /// grammar corner needs no fourth singleton SCC at all: `scripts` is a
1736    /// `Vec`, so an empty run already degenerates to plain `mathbot`, and
1737    /// `mathbot`'s only recursive spot (`{ … }` re-entering `mathmain`) is
1738    /// threaded through `MathErased` exactly like every *other* nested
1739    /// reference to "one math element" (`matharg`'s math-mode argument,
1740    /// `Atomic::MathText`'s program-mode embed, `InlineElem::EmbedMath`'s
1741    /// inline-text embed, `MathGroupArg`'s `{ … }` script operand). So
1742    /// `MathElemCst` is structurally acyclic within `#[recurse]`'s SCC
1743    /// analysis — like `OpChain`/`AppExpr`/`Atomic` — and monomorphizes
1744    /// exactly once (one stream type). This is *safer* than carving out a
1745    /// real self-loop would have been, not a shortcut: every recursive edge
1746    /// is erased, so there is no bounded-depth engine to blow up.
1747    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1748    pub struct MathElemCst {
1749        pub base: MathBot,
1750        pub scripts: Vec<MathScript>,
1751    }
1752
1753    /// `mathbot`.
1754    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1755    pub enum MathBot {
1756        /// `\cmd matharg*` (`mcmd list(matharg)`), sigil-only or
1757        /// module-qualified (`\Mod.cmd matharg*`).
1758        Cmd { name: AnyMathCmdTok, args: Vec<MathArg> },
1759        Chars(MathCharTok),
1760        /// `#var` (`VARINMATH`; math mode never trails this with `;` —
1761        /// unlike `#var;` in inline/block text, the lexer doesn't switch to
1762        /// an active mode here).
1763        Embed(VarInMathTok),
1764        /// A `|` separator marker (flat; elaborator regroups, e.g. for
1765        /// tabular/matrix columns — `mathblock`'s `SEP mathlist` case).
1766        Sep(SepTok),
1767        /// `{ … }` — re-enters `mathmain` (`mathgroup`'s `BMATHGRP mathmain
1768        /// EMATHGRP` case, reached here via `mathbot`). Content is erased
1769        /// (see [`MathElemCst`]'s doc comment).
1770        Group {
1771            mgrp: MathGroup<()>,
1772            #[group(self.mgrp)]
1773            elems: Vec<super::MathErased>,
1774        },
1775    }
1776
1777    /// One postfix script combo of a [`MathElemCst`] (`mathtop`'s
1778    /// `SUPERSCRIPT`/`SUBSCRIPT`/`PRIMES` suffixes, one at a time).
1779    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1780    pub enum MathScript {
1781        /// `^ group`
1782        Super { hat: SuperscriptTok, group: MathGroupArg },
1783        /// `_ group`
1784        Sub { under: SubscriptTok, group: MathGroupArg },
1785        /// A run of `'` marks — sugar for a superscript of primes
1786        /// characters; kept as its own token (not desugared here) since
1787        /// elaboration already special-cases it per `parser.mly`.
1788        Primes(PrimesTok),
1789    }
1790
1791    /// `mathgroup`: a script's operand is either a bracketed math group or a
1792    /// bare `mathbot`.
1793    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1794    pub enum MathGroupArg {
1795        Group {
1796            mgrp: MathGroup<()>,
1797            #[group(self.mgrp)]
1798            elems: Vec<super::MathErased>,
1799        },
1800        Bot(Box<MathBot>),
1801    }
1802
1803    /// `matharg` (parser.mly:1138-1146 + narg 1201-1210): one math-mode
1804    /// command argument — a mandatory body, a `?:`-supplied optional
1805    /// (UTOptionalArgument), or `?*` (UTOmission). The six body shapes live
1806    /// once in [`MathArgBody`]; Optional/Omission/Plain are first-token-disjoint.
1807    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1808    pub enum MathArg {
1809        Optional { q: OptionalTok, body: MathArgBody },
1810        Omission(OmissionTok),
1811        Plain(MathArgBody),
1812    }
1813
1814    /// The six body shapes shared by mandatory and `?:`-optional math args:
1815    /// a math/inline/block group, or a `!`-escaped program-mode value. The
1816    /// lexer already switches mode on the escape sigil (`!(` / `![` / `!(|` /
1817    /// `!{` / `!<` all emit ordinary `LParen`/`BList`/`BRecord`/`BHorzGrp`/
1818    /// `BVertGrp` tokens — see `lexer.rs`'s `lex_math`), so at the token
1819    /// level the escapes are indistinguishable from `Atomic`'s own
1820    /// `Paren`/`List`/`Record` shapes; reusing those bodies directly here
1821    /// (rather than going through a full `ExprErased`, which would also
1822    /// happily swallow a *following* `matharg` bracket group as a trailing
1823    /// application argument) keeps each `matharg` exactly one bracket group.
1824    /// NOT `Box<MathArg>`: a direct self-loop on a non-root type is what
1825    /// `#[recurse]` rejects, and upstream's grammar is non-recursive here
1826    /// anyway.
1827    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1828    pub enum MathArgBody {
1829        /// `{ math }`.
1830        Math {
1831            mgrp: MathGroup<()>,
1832            #[group(self.mgrp)]
1833            elems: Vec<super::MathErased>,
1834        },
1835        /// `!{ inline text }`.
1836        Inline {
1837            igrp: InlineGroup<()>,
1838            #[group(self.igrp)]
1839            elems: Vec<InlineElem>,
1840        },
1841        /// `!<block text>`.
1842        Block {
1843            bgrp: BlockGroup<()>,
1844            #[group(self.bgrp)]
1845            elems: Vec<BlockElem>,
1846        },
1847        /// `!(e)` / `!(e, e, …)`.
1848        ParenEscape {
1849            paren: ParenGroup<()>,
1850            #[group(self.paren)]
1851            inner: Box<ParenBody>,
1852        },
1853        /// `![e; …]`.
1854        ListEscape {
1855            list: ListGroup<()>,
1856            #[group(self.list)]
1857            items: Vec<ListItem>,
1858        },
1859        /// `!(|l = e; …|)`.
1860        RecordEscape {
1861            rec: RecordGroup<()>,
1862            #[group(self.rec)]
1863            body: RecordBody,
1864        },
1865    }
1866}
1867
1868/// A parse failure with the source position recovered from the failing parse.
1869///
1870/// The span is whatever syan's span-carrying [`ParseError`](syan::error::ParseError)
1871/// reports for the failure (recovered via `span_of::<Span>`); with our
1872/// [`Span::migrate`](crate::span::Span) being a union it covers the attempted
1873/// region rather than pinpointing a single token.
1874#[derive(Debug, thiserror::Error)]
1875#[error("{span}: parse error: {message}")]
1876pub struct ParseFileError {
1877    pub span: Span,
1878    pub message: String,
1879}
1880
1881/// Lex and parse a whole `.saty` source file.
1882pub fn parse_file(src: &str) -> Result<File, ParseFileError> {
1883    let atoms = crate::lexer::lex(src).map_err(|e| ParseFileError {
1884        span: e.span,
1885        message: e.msg,
1886    })?;
1887    let mut stream = crate::stream::AtomStream::new(atoms);
1888    <File as Parse<_>>::parse(&mut stream).map_err(|e| ParseFileError {
1889        span: *e.span(),
1890        message: render_parse_error(&e),
1891    })
1892}
1893
1894/// Flatten syan's nested error tree into one readable line.
1895fn render_parse_error(err: &syan::error::ParseError<Span>) -> String {
1896    format!("{err:?}")
1897}