Skip to main content

rustyfi_syntax/
cst_v1.rs

1//! The SATySFi **0.1.0** (`dev-0-1-0`) surface grammar — a *fork* of
2//! [`crate::cst`], not a version-gate of it (gating one shared `cst.rs` would
3//! mean hand-writing `Parse` for nearly every node, destroying the derive
4//! idiom and risking 0.0.6 on every 0.1 edit). [`crate::cst`] stays frozen:
5//! this module imports only its token-`Atom`-generic, non-recursive helpers
6//! ([`crate::cst::Header`], [`crate::cst::ParseFileError`]) and re-declares
7//! everything else — including its own `*ErasedV1` eraser leaves and its own
8//! copy of `render_parse_error` — so that touching `cst_v1.rs` never touches
9//! `cst.rs`.
10//!
11//! **Scope.** SATySFi 0.1's grammar adds a whole ML-style module system
12//! (`bind`/`modexpr`/`sigexpr`/`decl`) on top of an expr/pattern/type layer
13//! that is structurally close to 0.0.6's, plus five surface deltas:
14//! `,`-separated lists/records (not `;`), `EXACT_EQ` for both definitional
15//! and record `=` (reusing [`DefEqTok`] — no new `=` leaf), mandatory
16//! `match … with … end`, per-binding staging instead of a whole-file
17//! `@stage:` header, and no `when`/`while`/`before` at all. This module
18//! builds:
19//!
20//! * [`FileV1`] — `header* expr EOI` or `header* module Name
21//!   option(sig_annot) = struct bind* end EOI`.
22//! * [`Bind`] — every arm of upstream `bind`: `val`/`val inline`/`val
23//!   block`, `val math` (math-split — see
24//!   [`Bind::ValueMath`]), `val rec … and …`/`val mutable`/`type … and …`,
25//!   `module … = modexpr`/`signature … = sigexpr`/`include
26//!   modexpr`.
27//! * [`ast::ModExpr`]/[`ast::SigExpr`]/[`ast::Decl`] — the full
28//!   module/signature grammar: functor literals/
29//!   application, module paths/aliases, `:>` coercion, `sig … end` with
30//!   every `decl` form, `with type` refinement, `include`.
31//! * A copy of [`crate::cst::ast`]'s expr/pattern/type layer with the 0.1
32//!   deltas applied (see each type's doc comment for the exact delta),
33//!   including the full `let rec … and … in`/`let mutable … in` expression
34//!   forms, a widened `TypeExpr` grammar (products, prefix application),
35//!   `inline […]`/`block […]`/`math […]` command types
36//!   (`parser_v1.mly:730-735`) and `LONG_LOWER` qualified type paths
37//!   (`:720-728,742-743`; `TypeApp::AppliedLong`/`TypeAtom::LongName`).
38//!
39//! **Grammar shipped with placeholder semantics only.** Every
40//! module/signature construct beyond the struct-literal
41//! `ModExpr::Struct` body PARSES and round-trips, but lowers to a precise
42//! `LowerError` (`v1/lower.rs`) rather than real semantics — see that
43//! module's doc comment for the placeholder set and the seal rule.
44//!
45//! **Deliberately NOT built**: macro binds/decls
46//! and row quantifiers (`rowquant`). Staging DOES parse — the
47//! operand prefixes `&e`/`~e` ([`ast::StagePrefix`],
48//! `parser_v1.mly:870-873`) and the per-binding qualifier of `val ~x`/`val
49//! persistent ~x` ([`BindStageV1`], `:417-421` and the decl form
50//! `:600-603`), with `persistent` a 0.1-only keyword token.
51//!
52//! **The `#[recurse]` SCC story (five roots).** Five
53//! singleton, directly self-referential roots — the same shape
54//! [`crate::cst::ast`] uses, for the same reason (see its module doc comment
55//! for the measured compile-time blowup a naive transcription hits):
56//!
57//! * [`ast::Expr`] (its variants' own `Box<Expr>` children);
58//! * [`ast::PatBot`] (`CtorApplied`'s `Box<PatBot>` argument);
59//! * [`ast::TypeExpr`] (`Fun`'s right-recursive `Box<TypeExpr>` codomain);
60//! * [`ast::ModExpr`] (`Functor.body`'s `Box<ModExpr>` self-loop);
61//! * [`ast::SigExpr`] (`Functor.dom`/`Functor.cod`'s `Box<SigExpr>`
62//!   self-loop — encoded left-recursion-safe, `with` is
63//!   bot+suffix, never `With { base: Box<SigExpr> }`; see [`ast::SigExpr`]'s
64//!   own doc comment).
65//!
66//! Every other recursion edge is routed through the erasers declared below
67//! ([`ExprErasedV1`], [`PatErasedV1`], [`PatBotErasedV1`], [`TyErasedV1`],
68//! [`MathErasedV1`], [`ModExprErasedV1`], [`SigExprErasedV1`],
69//! [`TypeBindsErasedV1`]), keeping each SCC a singleton and the wrapped
70//! grammar's recursion reborrowing one stream type (syan pins it, not us).
71//! [`ast::Decl`] is a satellite, not a root: it has no `Box<Self>` anywhere
72//! and no type inside the `#[recurse]` module ever names it — it is reached
73//! only through the hand-written [`StructDeclV1`] connector (an opaque leaf
74//! to the SCC analysis, mirroring [`StructBindV1`]), so `SigExpr ↔ Decl`
75//! never forms a rootless static sub-cycle.
76
77use crate::leaf::*;
78use crate::span::Span;
79use newer_type::implement;
80use syan::parse::{Parse, Unparse};
81
82/// `@require:` / `@import:` header element — byte-identical between 0.0.6
83/// and `dev-0-1-0` (this port's own confirmation), so 0.1 simply reuses
84/// [`crate::cst`]'s definition rather than re-declaring an identical enum.
85/// 0.1 has no `@stage:` header at all (the shared lexer's `V0_1` path
86/// rejects it outright — see `lexer.rs`'s `lex_header`), so
87/// [`crate::cst::Header`]'s absence of a `Stage` variant costs nothing here.
88pub use crate::cst::Header;
89
90/// A 0.1 header element — the UNION of BOTH packaging generations' header
91/// forms (Axis B). `Legacy` is `dev-0-1-0`'s `@require:`/`@import:`
92/// (byte-identical to 0.0.6's, reusing [`Header`]); the three `Use*`
93/// forms are `saphe-split`'s `headerelem` (`parser.mly:371-380 @ b836d512`).
94/// Which family is *legal* is a `LoadMode` question the loader answers
95/// (`rustyfi_loader`), not a grammar question — this ONE `V0_1` grammar
96/// accepts both so the mode error can be raised at load time with a better
97/// message than a lex error would give.
98///
99/// Variant order is parse priority (syan ordered-alternatives, most-specific
100/// first): `UsePackage` (the `package` keyword disambiguates) precedes the
101/// `of`-suffixed `UseOf`, which precedes bare `Use` (longest-match: `use M of
102/// …` must claim its `of` before a bare `use M` matches), which precedes the
103/// token-disjoint `Legacy` (`@`-headers lex to distinct tokens).
104#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
105pub enum HeaderV1 {
106    /// `USE PACKAGE optional_open mod_chain` — depend on an installed package
107    /// by its consumer-chosen alias (`used_as`). Header attributes
108    /// (`#[test-only]` etc., upstream `list(attribute)`) are DEFERRED.
109    UsePackage {
110        use_kw: KwUse,
111        package_kw: KwPackage,
112        open_kw: Option<KwOpen>,
113        path: ast::ModChainV1,
114    },
115    /// `USE optional_open mod_chain OF STRING` — load a local file by
116    /// backtick-quoted relative path (the `@import:` analog).
117    UseOf {
118        use_kw: KwUse,
119        open_kw: Option<KwOpen>,
120        path: ast::ModChainV1,
121        of_kw: KwOf,
122        relpath: LiteralTok,
123    },
124    /// `USE optional_open mod_chain` — sibling module inside the same package
125    /// (closed resolution; only legal inside envelope source trees, enforced
126    /// by the loader).
127    Use {
128        use_kw: KwUse,
129        open_kw: Option<KwOpen>,
130        path: ast::ModChainV1,
131    },
132    /// `@require:`/`@import:` — Legacy packaging, unchanged shape.
133    Legacy(Header),
134}
135
136impl HeaderV1 {
137    /// A short human-readable name for this header, for loader diagnostics
138    /// (e.g. `use package Stdlib`, `@require: foo`).
139    pub fn display_name(&self) -> String {
140        match self {
141            Self::UsePackage { path, .. } => format!("use package {}", path.render()),
142            Self::UseOf { path, relpath, .. } => {
143                format!("use {} of `{}`", path.render(), relpath.body)
144            }
145            Self::Use { path, .. } => format!("use {}", path.render()),
146            Self::Legacy(Header::Require(t)) => format!("@require: {}", t.content),
147            Self::Legacy(Header::Import(t)) => format!("@import: {}", t.content),
148            Self::Legacy(Header::Stage(_)) => "@stage:".to_string(),
149        }
150    }
151}
152
153impl ast::ModChainV1 {
154    /// The dotted path as source text, e.g. `Stdlib.Logo` or `Local`.
155    pub fn render(&self) -> String {
156        match self {
157            Self::Long(t) => {
158                let mut parts = t.mods.clone();
159                parts.push(t.name.clone());
160                parts.join(".")
161            }
162            Self::Single(t) => t.name.clone(),
163        }
164    }
165
166    /// The HEAD component — the module/envelope identifier the loader keys
167    /// dependency resolution off (upstream's `used_as` map is keyed by it;
168    /// the tail is submodule access, a typecheck-time concern). For `A.B.C`
169    /// that is `A`; for a bare `A` it is `A`.
170    pub fn head_name(&self) -> String {
171        match self {
172            Self::Long(t) => t.mods.first().cloned().unwrap_or_else(|| t.name.clone()),
173            Self::Single(t) => t.name.clone(),
174        }
175    }
176}
177
178/// A binding-position NAME: `LOWER | ( binop )` — upstream 0.1's
179/// `bound_identifier` (`parser_v1.mly:358-363`) is the same nonterminal
180/// 0.0.6's `var` folds ([`crate::cst::BindName`]'s doc comment), and the
181/// leaf-level parse (`VarTok` | `OpNameTok`) is identical in both
182/// generations, so 0.1 reuses the type rather than re-declaring it —
183/// another token-generic, non-recursive import like [`Header`] above.
184pub use crate::cst::BindName;
185
186/// A whole 0.1 `.saty`/`.satyh` file (`main`, upstream `parser_v1.mly:364-
187/// 368`): a header list followed by either a library (`main_lib`) or a
188/// document expression. Unlike 0.0.6's [`crate::cst::File`] (a flat prelude
189/// of top-level `let`s with an optional trailing `in body`), 0.1 has no flat
190/// top-level binding sequence at all: a document body is *just* an
191/// [`ast::Expr`] (every `let` chains its own `in`), and a library is exactly
192/// one `module … = struct … end`.
193#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
194pub enum FileV1 {
195    /// `header* expr EOI` (`parser_v1.mly:367`).
196    Document {
197        headers: Vec<HeaderV1>,
198        body: ast::Expr,
199        eoi: EoiTok,
200    },
201    /// `header* MODULE UPPER option(sig_annot) EXACT_EQ STRUCT bind* END
202    /// EOI` (`parser_v1.mly:372-375`, `main_lib`; `sig_annot = COERCE
203    /// sigexpr`, `:555-557`). Note 0.1's annotation sigil is
204    /// `:>` (COERCE), never 0.0.6's `: sig … end`.
205    Library {
206        headers: Vec<HeaderV1>,
207        module_kw: KwModule,
208        name: CtorTok,
209        sig_annot: Option<SigAnnotV1>,
210        eq: DefEqTok,
211        struct_kw: KwStruct,
212        binds: Vec<Bind>,
213        end_kw: KwEnd,
214        eoi: EoiTok,
215    },
216}
217
218/// `COERCE sigexpr` — a signature annotation `:> S` (`sig_annot`,
219/// `parser_v1.mly:555-557`). 0.1's annotation sigil is `:>` (COERCE,
220/// `lexer_v1.mll:280`), NOT 0.0.6's `: sig … end` ([`crate::cst::SigAnnot`],
221/// `cst.rs:295-303`) — `module M : S = …` is a 0.1 parse error (pinned in
222/// tests). The signature body goes through [`SigExprErasedV1`]: `SigAnnotV1`
223/// lives outside the `#[recurse]` module, so this is a cross-boundary edge
224/// into the `SigExpr` root (see the module doc comment's SCC story).
225#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
226pub struct SigAnnotV1 {
227    pub coerce: CoerceTok,
228    pub sig_: SigExprErasedV1,
229}
230
231/// One parameter of a [`Bind`] (`param_unit`, `parser_v1.mly:635-646`): an
232/// optional `?(l = x, …)` labeled-optional binder bundle, then either a
233/// plain `patbot` or a `( pat : τ )` ascribed pattern
234/// ([`ast::ParamBody::Ascribed`]). Defined INSIDE [`mod@ast`]
235/// and re-exported here so that [`ast::Expr::Fun`]/[`ast::Expr::LetIn`]/
236/// [`ast::RecClauseV1`] can reference it without a boundary-crossing
237/// Parse-trait cycle (the `TypeBindsErasedV1` E0275 hazard).
238pub use ast::{AscribedInnerV1, OptParamEntryV1, OptParamsV1, Param, ParamBody};
239
240/// Every arm of `bind` (`parser_v1.mly:415-440`) — upstream's own
241/// nonterminal name (helper types like [`StructBindV1`]/
242/// [`TypeBindSingleV1`] keep a `V1` suffix). Every value arm's `=` is
243/// `EXACT_EQ` ([`DefEqTok`]) and body is an [`ast::Expr`]. `name` is a
244/// [`crate::cst::BindName`] wherever upstream's `bound_identifier` reaches it
245/// (`Value`, and the rec clauses inside [`ast::RecClauseV1`]); `ValueMutable`
246/// and `ValueInline`/`ValueBlock`'s `ctx` stay plain [`VarTok`]s — upstream's
247/// `MUTABLE LOWER …`/ctx-variable productions are a plain `LOWER`, not
248/// `bound_identifier` (see [`crate::cst::BindName`]'s doc comment for the
249/// ordered-choice-safety argument).
250#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
251pub enum Bind {
252    /// `VAL PERSISTENT? EXACT_TILDE? bind_value_nonrec`
253    /// (`parser_v1.mly:416-421,442,459-465`): `val <stage>? <name> <param>*
254    /// = <expr>`, where `<stage>` is `~` (stage 0) or `persistent ~` (the
255    /// persistent stage) and its absence means stage 1, the document stage.
256    ///
257    /// The prefix is an `Option<BindStageV1>` tried before `name`; on an
258    /// unstaged `val x = …` it fails at the first token, collapses to `None`
259    /// and steals nothing, so every existing fixture parses unchanged. It
260    /// also keeps this arm ordered-choice-safe against the keyword-headed
261    /// `Value*` arms below: `val ~rec …` still fails here (at `name`, which
262    /// cannot match the `rec` keyword) and falls through, exactly as `val
263    /// rec …` does.
264    Value {
265        kw: KwVal,
266        stage: Option<BindStageV1>,
267        name: BindName,
268        params: Vec<Param>,
269        eq: DefEqTok,
270        body: ast::Expr,
271    },
272    /// `VAL INLINE bind_inline` (`parser_v1.mly:422-431` dispatch → `448` →
273    /// `466-491`): `val inline <ctx> \cmd <param>* = <expr>` (the
274    /// heavyweight, ctx-explicit form — the only one `stdja-mini` uses;
275    /// `ctx` stays `Option` so the lightweight, ctx-synthesized form parses
276    /// too, for free).
277    ValueInline {
278        kw: KwVal,
279        /// See [`Bind::Value::stage`] — upstream's qualifier sits before the
280        /// whole `bind_value`, and `bind_value` is what `inline`/`block`/
281        /// `math`/`rec`/`mutable` select between (`parser_v1.mly:417-421` →
282        /// `:581-593`), so every arm below carries the same prefix.
283        stage: Option<BindStageV1>,
284        inline_kw: KwInline,
285        ctx: Option<VarTok>,
286        cmd: AnyHorzCmdTok,
287        params: Vec<Param>,
288        eq: DefEqTok,
289        body: ast::Expr,
290    },
291    /// `VAL BLOCK bind_block` (`parser_v1.mly:450` → `493-518`): `val block
292    /// <ctx> +cmd <param>* = <expr>`.
293    ValueBlock {
294        kw: KwVal,
295        /// See [`Bind::ValueInline::stage`].
296        stage: Option<BindStageV1>,
297        block_kw: KwBlock,
298        ctx: Option<VarTok>,
299        cmd: AnyVertCmdTok,
300        params: Vec<Param>,
301        eq: DefEqTok,
302        body: ast::Expr,
303    },
304    /// `VAL MATH bind_math` (`parser_v1.mly:452-453` dispatch → `520-531`):
305    /// `val math <ctx> \cmd <param>* [with <sub> <sup>] = <expr>`.
306    /// Unlike `ValueInline`/`ValueBlock`, `ctx` is MANDATORY — upstream
307    /// has no lightweight ctx-less form (contrast `bind_inline`'s two
308    /// productions, :466-491). Placed after
309    /// `ValueBlock`, ordered-choice-safe for the same reason as `Value`
310    /// above: `math`/`with` both lex as keyword tokens under V0_1, so no
311    /// arm can steal another's input.
312    ValueMath {
313        kw: KwVal,
314        /// See [`Bind::ValueInline::stage`].
315        stage: Option<BindStageV1>,
316        math_kw: KwMath,
317        ctx: VarTok,
318        /// `\cmd` — math commands share the `\` sigil with inline commands
319        /// (there is no separate math-command token; see `elaborate.rs`'s
320        /// `command_scheme` doc comment, which notes the same sharing on
321        /// the eval side).
322        cmd: AnyHorzCmdTok,
323        params: Vec<Param>,
324        scripts: Option<ScriptsParamV1>,
325        eq: DefEqTok,
326        body: ast::Expr,
327    },
328    /// `VAL REC bind_value_nonrec (AND bind_value_nonrec)*`
329    /// (`parser_v1.mly:444-445,455-465`): `val rec f p* = e (and g p* = e)*`.
330    /// With `rec`/`mutable`/`inline`/`block` all lexed as keyword tokens
331    /// under V0_1, no arm can steal another's input —
332    /// `Value.name: BindName` cannot match a keyword token — so declared
333    /// order is a documentation/perf choice; `Value` stays first because it
334    /// is the overwhelmingly common arm.
335    ValueRec {
336        kw: KwVal,
337        /// See [`Bind::ValueInline::stage`]. One qualifier covers the whole
338        /// `and`-chain, matching upstream's single `UTBindValue(stage,
339        /// UTRec(binds))`.
340        stage: Option<BindStageV1>,
341        rec_kw: KwRec,
342        first: ast::RecClauseV1,
343        ands: Vec<ast::AndClauseV1>,
344    },
345    /// `VAL MUTABLE LOWER REVERSED_ARROW expr` (`parser_v1.mly:446-447`):
346    /// `val mutable x <- e`. The name is a plain `LOWER` upstream (not
347    /// `bound_identifier`), hence `VarTok`, matching the cst target
348    /// (`cst::TopBinding::LetMutable.name`, `cst.rs:237`).
349    ValueMutable {
350        kw: KwVal,
351        /// See [`Bind::ValueInline::stage`].
352        stage: Option<BindStageV1>,
353        mutable_kw: KwMutable,
354        name: VarTok,
355        arrow: OverwriteEqTok,
356        value: ast::Expr,
357    },
358    /// `TYPE bind_type_single (AND bind_type_single)*`
359    /// (`parser_v1.mly:432-433,535-544`): `type t 'a* = body (and u 'a* =
360    /// body)*` — variant and synonym forms, mutually recursive across the
361    /// `and` chain.
362    Type {
363        kw: KwType,
364        first: TypeBindSingleV1,
365        ands: Vec<TypeAndV1>,
366    },
367    /// `MODULE UPPER option(sig_annot) EXACT_EQ modexpr` — upstream
368    /// `bind`'s MODULE arm (`parser_v1.mly:434-435`), with the FULL
369    /// `modexpr` body and the optional `:>` annotation. The
370    /// body goes
371    /// through [`ModExprErasedV1`]: `Bind` is outside the `#[recurse]`
372    /// module, and `Bind → ModExpr → StructBindV1 → Bind` is the runtime
373    /// cycle both connectors erase (one break per direction).
374    Module {
375        module_kw: KwModule,
376        name: CtorTok,
377        sig_annot: Option<SigAnnotV1>,
378        eq: DefEqTok,
379        body: ModExprErasedV1,
380    },
381    /// `SIGNATURE UPPER EXACT_EQ sigexpr` (`parser_v1.mly:436-437`).
382    Signature {
383        kw: KwSignature,
384        name: CtorTok,
385        eq: DefEqTok,
386        sig_: SigExprErasedV1,
387    },
388    /// `INCLUDE modexpr` (`:438-439`) — a bind-include includes a MODULE
389    /// (contrast [`ast::Decl::Include`], which includes a signature).
390    Include { kw: KwInclude, body: ModExprErasedV1 },
391}
392
393/// The stage qualifier of a `val` bind or `val` decl: `~` alone is stage 0,
394/// `persistent ~` is the persistent stage (`parser_v1.mly:417-421` for binds,
395/// `:600-603` for decls). No prefix at all is stage 1 — the document stage,
396/// where an ordinary `val` lives — which is why the field holding this is an
397/// `Option`.
398///
399/// This is 0.1's replacement for 0.0.6's whole-file `@stage:` header: the
400/// same three stages, chosen per binding instead of per file.
401#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
402pub struct BindStageV1 {
403    pub persistent: Option<KwPersistent>,
404    pub tilde: ExactTildeTok,
405}
406
407/// `scripts_param` (`parser_v1.mly:532-534`): `WITH sub=LOWER sup=LOWER` —
408/// `val math`'s optional `with sub sup` suffix, binding the two
409/// script-callback parameters directly rather than synthesizing the
410/// hidden `%math-attach-scripts` wrapper.
411#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
412pub struct ScriptsParamV1 {
413    pub with_kw: KwWith,
414    pub sub: VarTok,
415    pub sup: VarTok,
416}
417
418/// One `bind_type_single` (`parser_v1.mly:539-544`). **0.1 delta from
419/// [`crate::cst::TypeDecl`]:** the type parameters come AFTER the name
420/// (`type t 'a = …`, `tyident LOWER; tyvars list(TYPEVAR)`), where 0.0.6
421/// writes them before (`type 'a t = …`, `cst.rs:401-408`) — the lowering
422/// reorders the fields. No `constraint` suffix exists in 0.1's production.
423#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
424pub struct TypeBindSingleV1 {
425    pub name: VarTok,
426    pub tyvars: Vec<TypeVarTok>,
427    pub eq: DefEqTok,
428    pub body: TypeBodyV1,
429}
430
431/// An `and bind_type_single` continuation (`bind_type`'s
432/// `separated_nonempty_list(AND, …)`, `parser_v1.mly:535-537`).
433#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
434pub struct TypeAndV1 {
435    pub and_kw: KwAnd,
436    pub bind: TypeBindSingleV1,
437}
438
439/// One whole `bind_type` chain — `bind_type_single (AND bind_type_single)*`
440/// (`parser_v1.mly:535-537`) — grouped into a single struct so the sig
441/// layer ([`ast::SigExpr::WithType`], [`ast::Decl::Type`]) can reference the
442/// chain through ONE eraser ([`TypeBindsErasedV1`]). [`Bind::Type`] keeps
443/// its flattened `first`/`ands` fields unchanged (avoiding call-site
444/// churn); the two spellings are the same grammar.
445#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
446pub struct TypeBindsV1 {
447    pub first: TypeBindSingleV1,
448    pub ands: Vec<TypeAndV1>,
449}
450
451/// The right-hand side of one type bind: a variant's constructor list
452/// (`EXACT_EQ BAR? variants`, `parser_v1.mly:540-541,545-553`) or a
453/// transparent synonym (`EXACT_EQ typ`, `:542-543`). Variant-first is
454/// unambiguous for the same reason as [`crate::cst::TypeDeclBody`]
455/// (`cst.rs:410-418`): a variant list is `BarTok`/`CtorTok`-headed and no
456/// [`ast::TypeExpr`] can start with either.
457#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
458pub enum TypeBodyV1 {
459    Variant {
460        leading_bar: Option<BarTok>,
461        first: VariantDefV1,
462        rest: Vec<BarVariantDefV1>,
463    },
464    Synonym(ast::TypeExpr),
465}
466
467/// One `UPPER [OF typ]` variant (`parser_v1.mly:549-553`).
468#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
469pub struct VariantDefV1 {
470    pub ctor: CtorTok,
471    pub of_ty: Option<OfTypeV1>,
472}
473
474/// The `of typ` payload suffix.
475#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
476pub struct OfTypeV1 {
477    pub of_kw: KwOf,
478    pub ty: ast::TypeExpr,
479}
480
481/// A `| UPPER [OF typ]` continuation (`variants`' `separated_nonempty_
482/// list(BAR, variant)`, `parser_v1.mly:545-548`).
483#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
484pub struct BarVariantDefV1 {
485    pub bar: BarTok,
486    pub def: VariantDefV1,
487}
488
489/// One declaration inside a `module … = struct … end` body.
490/// [`Bind`]'s own alternatives are exactly what a struct
491/// body may contain (`bind*`), so this simply re-parses a [`Bind`] — but
492/// *not* by naming `Bind` as a field type directly: [`Bind`] lives
493/// **outside** the `#[recurse]` module (below), so `Bind -> ModExpr ->
494/// Vec<StructBindV1> -> Bind` would be a self-recursive cycle through a
495/// plain `#[derive(Parse)]`, which (without the `#[recurse]` engine to back
496/// it) is an `E0275` hazard (an unbounded recursive trait-bound
497/// obligation) — exactly [`crate::cst::StructDecl`]'s own rationale
498/// (`cst.rs:262-269`). Hand-writing `Parse`/`Unparse` here — the same trick
499/// as the `erased_leaf_v1!` macro below — sidesteps that: the impl has no
500/// recursive where-bound for the compiler to try to satisfy, it just calls
501/// `Bind::parse` through the stream-erasing adapter at runtime.
502#[derive(Debug, Clone, PartialEq)]
503pub struct StructBindV1(pub Box<Bind>);
504
505impl Parse<crate::token::Atom> for StructBindV1 {
506    type Error = syan::error::ParseError<crate::span::Span>;
507
508    fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
509        stream: &mut S,
510    ) -> Result<Self, Self::Error> {
511        let value = <Bind as Parse<_>>::parse_stream(stream)?;
512        Ok(StructBindV1(Box::new(value)))
513    }
514}
515
516impl Unparse<crate::token::Atom> for StructBindV1 {
517    fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
518        &self,
519        sink: &mut S,
520    ) -> Result<(), S::Error> {
521        self.0.unparse(sink)
522    }
523}
524
525/// One declaration inside a `sig … end` body (`list(decl)`,
526/// `parser_v1.mly:591`) — [`StructBindV1`]'s twin, hand-written `Parse`/
527/// `Unparse` for the same `E0275` reason. [`ast::Decl`] lives INSIDE the
528/// `#[recurse]` module and `SigExpr → SigBotV1 → StructDeclV1 → Decl →
529/// SigExpr` is a runtime cycle; naming `ast::Decl` as a plain derived field
530/// of [`ast::SigBotV1`] would re-enter the module's own SCC analysis. As an
531/// opaque leaf it closes that cycle at RUNTIME while keeping both SCCs
532/// singletons. NOTE: named after [`crate::cst::StructDecl`] (the mechanism),
533/// even though it carries a sig-`decl`, not a struct binding.
534#[derive(Debug, Clone, PartialEq)]
535pub struct StructDeclV1(pub Box<ast::Decl>);
536
537impl Parse<crate::token::Atom> for StructDeclV1 {
538    type Error = syan::error::ParseError<crate::span::Span>;
539
540    fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
541        stream: &mut S,
542    ) -> Result<Self, Self::Error> {
543        let value = <ast::Decl as Parse<_>>::parse_stream(stream)?;
544        Ok(StructDeclV1(Box::new(value)))
545    }
546}
547
548impl Unparse<crate::token::Atom> for StructDeclV1 {
549    fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
550        &self,
551        sink: &mut S,
552    ) -> Result<(), S::Error> {
553        self.0.unparse(sink)
554    }
555}
556
557/// Recursion-edge eraser types for the expr/pattern/type layer —
558/// the `cst_v1` analogue of [`crate::cst`]'s `erased_leaf!` macro (see its
559/// doc comment for the measured compile-time blowup that makes this
560/// mandatory). Suffixed `V1` throughout so these never collide with
561/// [`crate::cst`]'s own erasers, even though the two live in sibling
562/// modules and could not actually name-clash. Defined *outside* the
563/// `#[recurse]` module so the macro treats them as opaque leaves.
564macro_rules! erased_leaf_v1 {
565    ($($(#[$doc:meta])* $name:ident => $target:ty;)*) => {
566        $(
567            $(#[$doc])*
568            #[implement(newer_type_std::ops::Deref)]
569            #[derive(Debug, Clone, PartialEq)]
570            pub struct $name(pub Box<$target>);
571
572            impl Parse<crate::token::Atom> for $name {
573                type Error = syan::error::ParseError<crate::span::Span>;
574
575                fn parse_stream<S: syan::parse::ParseStream<Atom = crate::token::Atom>>(
576                    stream: &mut S,
577                ) -> Result<Self, Self::Error> {
578                    // No erasure any more — see `cst.rs`'s `erased_leaf!`.
579                    let value = <$target as Parse<_>>::parse_stream(stream)?;
580                    Ok($name(Box::new(value)))
581                }
582            }
583
584            impl Unparse<crate::token::Atom> for $name {
585                fn unparse<S: syan::parse::unparse::Emitter<crate::token::Atom>>(
586                    &self,
587                    sink: &mut S,
588                ) -> Result<(), S::Error> {
589                    self.0.unparse(sink)
590                }
591            }
592        )*
593    };
594}
595
596erased_leaf_v1! {
597    /// An [`ast::Expr`] behind a stream-erasing parse (see above).
598    ExprErasedV1 => ast::Expr;
599    /// An [`ast::Pattern`] behind a stream-erasing parse (see above).
600    PatErasedV1 => ast::Pattern;
601    /// An [`ast::PatBot`] behind a stream-erasing parse (see above). Kept
602    /// separate from [`PatErasedV1`] for the same reason
603    /// [`crate::cst`]'s `PatErased`/`PatBotErased` split exists: a
604    /// constructor pattern's argument is a `patbot`, not a full `patas`.
605    PatBotErasedV1 => ast::PatBot;
606    /// An [`ast::TypeExpr`] behind a stream-erasing parse (see above).
607    TyErasedV1 => ast::TypeExpr;
608    /// An [`ast::MathElemCst`] behind a stream-erasing parse (see above).
609    MathErasedV1 => ast::MathElemCst;
610    /// An [`ast::ModExpr`] behind a stream-erasing parse. Carries the
611    /// OUTSIDE→INSIDE edge `Bind::Module.body → ModExpr` — the one edge of
612    /// the `Bind → ModExpr → StructBindV1 → Bind` runtime cycle not already
613    /// erased by the connector (the erasers are for the INSIDE types, not
614    /// for `Bind`).
615    ModExprErasedV1 => ast::ModExpr;
616    /// An [`ast::SigExpr`] behind a stream-erasing parse. Used by
617    /// [`SigAnnotV1`] and [`Bind::Signature`] (outside → the SigExpr root).
618    SigExprErasedV1 => ast::SigExpr;
619    /// A [`TypeBindsV1`] behind a stream-erasing parse. Unlike every other
620    /// eraser this one targets an OUTSIDE type: `SigExpr::WithType` /
621    /// `Decl::Type` (inside) must reach `bind_type`, whose
622    /// `TypeBindSingleV1` re-enters the module through plain-derived
623    /// `ast::TypeExpr` fields — an inside→outside-plain-derive→inside-root
624    /// chain with no precedent in `cst.rs`'s discipline. Erasing at the
625    /// boundary keeps the re-entry cheap (one stream type, monomorphized
626    /// once), exactly like every other cross-boundary edge.
627    TypeBindsErasedV1 => TypeBindsV1;
628}
629
630/// The recursive expression/pattern/type/text grammar for SATySFi 0.1.
631/// A copy of [`crate::cst::ast`] with the deltas documented on each
632/// type; see the module doc comment for the SCC/root story.
633#[syan::parse::recurse]
634pub mod ast {
635    use crate::leaf::*;
636    use syan::parse::{Parse, Unparse};
637
638    /// One `param_unit` (`parser_v1.mly:635-646`): an optional `?(l = x, …)`
639    /// labeled-optional binder bundle, then a [`ParamBody`] (a plain
640    /// `patbot`, or a `( pat : τ )` ascribed pattern). Held inside `mod ast`
641    /// (re-exported at [`super::Param`]) so the roots that carry param lists
642    /// (`Expr::Fun`/`Expr::LetIn`/`RecClauseV1`) reference it without a
643    /// boundary Parse-trait cycle. A bare `patbot` param parses `Param {
644    /// opts: None, body: ParamBody::Pat(_) }` directly — the `?`-headed
645    /// `opts` `Option` is tried first, failing on a non-`?` head with no
646    /// token stolen, so an all-plain param list parses unchanged.
647    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
648    pub struct Param {
649        pub opts: Option<OptParamsV1>,
650        pub body: ParamBody,
651    }
652
653    /// A `param_unit`'s trailing shape (`parser_v1.mly:635-646`): either a
654    /// plain `patbot`, or a `( pattern : typ )` ascribed pattern
655    /// (`parser_v1.mly:641-645`).
656    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
657    pub enum ParamBody {
658        /// Tried FIRST: `( x : int )` fails `patbot`'s own paren body at the
659        /// `:` (a `patbot` paren group expects only more patterns/`,`/`)`)
660        /// and backtracks to `Ascribed` cleanly (ordered choice, no token
661        /// stolen).
662        Pat(PatBot),
663        /// `( pattern : typ )` — a FULL `pattern` (not `patbot`) ascribed
664        /// with a full `typ`, both via erasers (same cycle-avoidance
665        /// discipline as every other satellite in this module).
666        Ascribed {
667            paren: ParenGroup<()>,
668            #[group(self.paren)]
669            inner: AscribedInnerV1,
670        },
671    }
672
673    /// An ascribed param's group content: `pattern : typ` (`parser_v1.mly`'s
674    /// `param_unit`, `:641-645`).
675    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
676    pub struct AscribedInnerV1 {
677        pub pat: super::PatErasedV1,
678        pub colon: ColonTok,
679        pub ty: super::TyErasedV1,
680    }
681
682    /// A `?(l = x, …)` labeled-optional parameter bundle (`parser_v1.mly`'s
683    /// optional-`param_unit` head). The `?` reuses [`OptionalTypeTok`]
684    /// (SATySFi 0.1 dropped the fused `?:` sigil); the `(…)` is a paren
685    /// group of `,`-separated `label = binder` entries (non-empty enforced
686    /// at lowering).
687    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
688    pub struct OptParamsV1 {
689        pub q: OptionalTypeTok,
690        pub paren: ParenGroup<()>,
691        #[group(self.paren)]
692        pub entries: Vec<OptParamEntryV1>,
693    }
694
695    /// One `label = binder` entry of an [`OptParamsV1`] bundle (the last `,`
696    /// is optional; `=` is upstream's `EXACT_EQ`, reusing [`DefEqTok`]).
697    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
698    pub struct OptParamEntryV1 {
699        pub label: VarTok,
700        pub eq: DefEqTok,
701        pub var: VarTok,
702        pub comma: Option<CommaTok>,
703    }
704
705    /// `nxlet`-analogue: a let/if/match/lambda-headed expression, falling
706    /// through to the flattened operator chain ([`Expr::Ops`], [`OpChain`]) at the
707    /// bottom. Variant order is parse priority (ordered-choice
708    /// backtracking): every `let`-headed form is tried before the fallback
709    /// [`Expr::Overwrite`]/[`Expr::Ops`] (which may also start with a bare
710    /// variable), and `Ops` — having no distinguishing leading keyword —
711    /// must stay last.
712    ///
713    /// **0.1 deltas from [`crate::cst::ast::Expr`]:** `Match` gains a
714    /// mandatory trailing `end` (`parser_v1.mly:792`); `let-rec` becomes
715    /// `let rec … in …` (a plain `let` followed by the new [`KwRec`]
716    /// keyword) with full `and`-chained mutual recursion (see
717    /// [`Expr::LetRecIn`]); a new `LetMutableIn` form covers `let mutable x
718    /// <- init in body`; a new `LetPatternIn` form covers
719    /// `let pat = value in body` for any non-bare-variable pattern
720    /// (`parser_v1.mly:796`, `pattern_non_var`); `open` requires a leading
721    /// `let` (`parser_v1.mly:798`, `LET OPEN UPPER IN`) where 0.0.6 allows a
722    /// bare `open Name in body`; and `WhileDo`, the `Guard`/`when` match-arm
723    /// suffix, and `OpChain`'s `before` postfix are dropped entirely —
724    /// SATySFi 0.1's grammar has no `WHEN`/`WHILE`/`BEFORE` tokens at all
725    /// (confirmed by grep of `parser_v1.mly`). `Overwrite` (`name <- value`)
726    /// is kept unchanged (`parser_v1.mly:810-812`, `REVERSED_ARROW`) — it is
727    /// unrelated to the removed `before` postfix.
728    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
729    pub enum Expr {
730        /// `let rec clause (and clause)* in body` (`parser_v1.mly:794-795`
731        /// dispatching to `bind_value_rec`, `:455-458`) — full mutual
732        /// recursion.
733        LetRecIn {
734            let_kw: KwLet,
735            rec_kw: KwRec,
736            first: RecClauseV1,
737            ands: Vec<AndClauseV1>,
738            in_kw: KwIn,
739            body: Box<Expr>,
740        },
741        /// `let mutable x <- init in body` (`parser_v1.mly:794-795`
742        /// dispatching to `bind_value`'s MUTABLE arm, `:446-447`). Same
743        /// shape as [`crate::cst::ast::Expr::LetMutableIn`] minus the fused
744        /// keyword: 0.1 spells it `let mutable` (two tokens), 0.0.6
745        /// `let-mutable` (one). Disambiguated one token after `let` by the
746        /// V0_1-gated `mutable` keyword, so declared order relative to the
747        /// other `let`-headed arms is correctness-irrelevant.
748        LetMutableIn {
749            let_kw: KwLet,
750            mutable_kw: KwMutable,
751            name: VarTok,
752            arrow: OverwriteEqTok,
753            init: Box<Expr>,
754            in_kw: KwIn,
755            body: Box<Expr>,
756        },
757        /// `let name param* = value in body` (only a plain variable target
758        /// is supported here — a general pattern falls through to
759        /// [`Expr::LetPatternIn`]). `name` is a [`super::BindName`] —
760        /// upstream's expression-level `let` reaches the
761        /// same `bind_value_nonrec` (`:794-795` → `:459-465`) `val`/`val
762        /// rec` do, so `let (+++) a b = … in` is valid 0.1 here too.
763        LetIn {
764            kw: KwLet,
765            name: super::BindName,
766            params: Vec<Param>,
767            eq: DefEqTok,
768            value: Box<Expr>,
769            in_kw: KwIn,
770            body: Box<Expr>,
771        },
772        /// `let pat = value in body` (`parser_v1.mly:796`,
773        /// `pattern_non_var`) — any pattern shape, tried only after the
774        /// bare-variable form [`Expr::LetIn`] fails to match.
775        LetPatternIn {
776            kw: KwLet,
777            pat: super::PatErasedV1,
778            eq: DefEqTok,
779            value: Box<Expr>,
780            in_kw: KwIn,
781            body: Box<Expr>,
782        },
783        /// `let open Name in body` (`parser_v1.mly:798`; unlike 0.0.6's
784        /// bare `open Name in body`, 0.1 requires the leading `let`).
785        OpenIn {
786            let_kw: KwLet,
787            open_kw: KwOpen,
788            name: CtorTok,
789            in_kw: KwIn,
790            body: Box<Expr>,
791        },
792        /// `if cond then a else b` (`else` is never optional, so there is
793        /// no dangling-else ambiguity).
794        If {
795            kw: KwIf,
796            cond: Box<Expr>,
797            then_kw: KwThen,
798            then_branch: Box<Expr>,
799            else_kw: KwElse,
800            else_branch: Box<Expr>,
801        },
802        /// `fun x y -> body`. Each parameter is a full `patbot` (through
803        /// `Param`), not a bare variable: upstream `parser_v1.mly:849-863`'s
804        /// `fun` genuinely binds a `patbot` per parameter (`ELambda(patbot,
805        /// e)` in `types.cppo.ml`), so `fun _ -> …` (wildcard) and `fun (a,
806        /// b) -> …` (tuple-destructuring) are legal upstream syntax (gaps
807        /// 2+3 of the V0_1-only language-completeness sweep). Reaching
808        /// `PatBot` from here is the same cross-root DAG edge
809        /// [`RecClauseV1::params`] already makes (both `Expr` and `PatBot`
810        /// are roots inside this `#[recurse]` module — see the module doc
811        /// comment), so no new SCC edge.
812        Fun {
813            kw: KwFun,
814            params: Vec<Param>,
815            arrow: ArrowTok,
816            body: Box<Expr>,
817        },
818        /// `match scrutinee with [|] pat -> body (| pat -> body)* end`
819        /// (`parser_v1.mly:792`). Mandatorily closed with `end`
820        /// (`tokR=END`); no `when` guards (0.1 has no `WHEN` token).
821        Match {
822            kw: KwMatch,
823            scrutinee: Box<Expr>,
824            with_kw: KwWith,
825            leading_bar: Option<BarTok>,
826            first: MatchArm,
827            rest: Vec<BarArm>,
828            end_kw: KwEnd,
829        },
830        /// `name <- value` (`expr_overwrite`, `parser_v1.mly:810-812`,
831        /// `REVERSED_ARROW`). Starts with a bare [`VarTok`], which is also
832        /// how [`Expr::Ops`] can start — must stay before `Ops` so
833        /// backtracking tries the `<-` shape first.
834        Overwrite {
835            name: VarTok,
836            arrow: OverwriteEqTok,
837            value: super::ExprErasedV1,
838        },
839        /// The flattened binary-operator chain — see
840        /// [`crate::cst::ast::Expr`]'s module doc comment on precedence
841        /// flattening (unchanged approach here). Must stay last (no leading
842        /// keyword).
843        Ops(OpChain),
844    }
845
846    /// One `name param* = value` clause of a `val rec`/`let rec` group —
847    /// upstream `bind_value_nonrec` (`parser_v1.mly:459-465`) as reached
848    /// from `bind_value_rec` (`:455-458`). **0.1 deltas from
849    /// [`crate::cst::ast::RecBinding`]:** no `: ty` ascription and no
850    /// multi-clause `| patbot* = value` sugar exist in 0.1 at all
851    /// (`bind_value_nonrec` has neither a `COLON ty` nor a `BAR`
852    /// alternative — 0.0.6's `recdecargpart` machinery has no 0.1
853    /// counterpart), so there are no `ascription`/`leading_bar`/`extra`
854    /// fields to mirror. `params` is upstream's `list(param_unit)` (see
855    /// [`super::Param`]'s doc comment), reaching `PatBot` through the same
856    /// cross-root DAG edge `cst::ast::RecBinding.params` makes
857    /// (`cst.rs:753-762`); `value` goes through [`super::ExprErasedV1`] (not
858    /// `Box<Expr>`) so this struct never joins `Expr`'s SCC — byte-for-byte
859    /// `cst.rs`'s own `RecBinding.value: ExprErased` discipline.
860    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
861    pub struct RecClauseV1 {
862        pub name: super::BindName,
863        pub params: Vec<Param>,
864        pub eq: DefEqTok,
865        pub value: super::ExprErasedV1,
866    }
867
868    /// An `and name param* = value` continuation of a `val rec`/`let rec`
869    /// group (`bind_value_rec`'s `separated_nonempty_list(AND, …)`,
870    /// `parser_v1.mly:455-458`). `and` lexes as `Token::LetAnd` → [`KwAnd`]
871    /// in both generations (`lexer.rs:141`), so no new token is needed.
872    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
873    pub struct AndClauseV1 {
874        pub and_kw: KwAnd,
875        pub clause: RecClauseV1,
876    }
877
878    /// One `pat -> body` match arm (`parser_v1.mly:959`). Unlike
879    /// [`crate::cst::ast::MatchArm`], has no `when` guard — 0.1's grammar
880    /// has none.
881    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
882    pub struct MatchArm {
883        pub pat: super::PatErasedV1,
884        pub arrow: ArrowTok,
885        pub body: super::ExprErasedV1,
886    }
887
888    /// A `| pat -> body` continuation of a match's arm list.
889    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
890    pub struct BarArm {
891        pub bar: BarTok,
892        pub arm: MatchArm,
893    }
894
895    /// A flattened binary-operator chain: `head (op rhs)*`, left-folded
896    /// (with correct per-operator precedence/associativity) during
897    /// elaboration — see [`crate::cst::ast::OpChain`]'s doc comment.
898    /// **Delta:** no `before` postfix field — 0.1 has no `BEFORE` token at
899    /// all (confirmed by grep of `parser_v1.mly`), so
900    /// [`crate::cst::ast::OpChain::before`] simply has no 0.1 counterpart.
901    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
902    pub struct OpChain {
903        pub head: AppExpr,
904        pub tail: Vec<OpRhs>,
905    }
906
907    /// One `op rhs` continuation of an [`OpChain`].
908    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
909    pub struct OpRhs {
910        pub op: BinOpTok,
911        pub rhs: AppExpr,
912    }
913
914    /// `nxun`/`nxapp`/`nxunsub`-analogue flattened: an optional leading
915    /// unary minus, an optional leading `!`/`!!`/... deref, an atomic head
916    /// with any `#label` field accesses, and an application-chain tail —
917    /// structurally identical to [`crate::cst::ast::AppExpr`] (`expr_app`,
918    /// `parser_v1.mly:849-863`, no 0.1 delta).
919    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
920    pub struct AppExpr {
921        pub minus: Option<ExactMinusTok>,
922        pub stage: Option<StagePrefix>,
923        pub excl: Option<UnopExclamTok>,
924        pub head: Atomic,
925        pub head_accesses: Vec<AccessSeg>,
926        pub args: Vec<AppArg>,
927    }
928
929    /// A staging prefix on a 0.1 operand: `&e` builds code for the next
930    /// stage, `~e` splices the result of a previous-stage computation
931    /// (`expr_un`, `parser_v1.mly:870-873`, `UTNext`/`UTPrev` — the same two
932    /// productions 0.0.6 has, unchanged).
933    ///
934    /// A fork of [`crate::cst::ast::StagePrefix`], not a re-export: it lives
935    /// inside 0.0.6's `#[recurse]` module, and this module's whole discipline
936    /// is that touching `cst_v1.rs` never touches `cst.rs` (module doc
937    /// comment). The two are token-identical, so `rustyfi-lang`'s
938    /// `v1::lower` maps one to the other by moving tokens.
939    ///
940    /// Upstream puts these on `expr_un`, one level BELOW `expr_app`, so
941    /// `&f x` is `(&f) x` and never `&(f x)`. This grammar flattens
942    /// `expr_un`/`expr_app` into one node, so the prefix is an optional field
943    /// on the *head* ([`AppExpr`]) and on each *argument* ([`AppArg`])
944    /// independently — which reproduces exactly that reading.
945    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
946    pub enum StagePrefix {
947        /// `&e` — quote: the value is `e`'s code, to run one stage later.
948        Next(ExactAmpTok),
949        /// `~e` — splice: run `e` now and drop its code in here.
950        Prev(ExactTildeTok),
951    }
952
953    /// One `#label` field-access segment (`expr_bot ACCESS`,
954    /// `parser_v1.mly:878`).
955    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
956    pub struct AccessSeg {
957        pub hash: AccessTok,
958        pub label: VarTok,
959    }
960
961    /// One application-chain argument. **0.1 delta:** the 0.0.6 `?:`/`?*`
962    /// (`Optional`/`Omission`) forms are gone — SATySFi 0.1 dropped the fused
963    /// `?:` sigil (`?:`/`?*` now lex as `?` + `:`/`*`, a downstream parse
964    /// error), replaced by the labeled `?(l = e, …)` bundle
965    /// ([`AppArg::Bundled`]).
966    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
967    pub enum AppArg {
968        /// `?(l = e, …) atom` — a labeled-optional bundle paired with the
969        /// positional argument it precedes (pairing them rejects a dangling
970        /// trailing bundle at parse time). `?(`-headed, token-disjoint from
971        /// the `Atom`/`Ctor` arms.
972        Bundled {
973            opts: OptArgsV1,
974            excl: Option<UnopExclamTok>,
975            atom: Atomic,
976            accesses: Vec<AccessSeg>,
977        },
978        /// `?(l = e, …) Ctor` — as [`AppArg::Bundled`] but the positional
979        /// argument is a bare constructor.
980        BundledCtor { opts: OptArgsV1, ctor: CtorTok },
981        Atom {
982            stage: Option<StagePrefix>,
983            excl: Option<UnopExclamTok>,
984            atom: Atomic,
985            accesses: Vec<AccessSeg>,
986        },
987        Ctor(CtorTok),
988    }
989
990    /// A `?(l = e, …)` labeled-optional application bundle: the `?` sigil
991    /// (reusing [`OptionalTypeTok`]), then a paren group of `,`-separated
992    /// `label = expr` entries (non-empty enforced at lowering).
993    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
994    pub struct OptArgsV1 {
995        pub q: OptionalTypeTok,
996        pub paren: ParenGroup<()>,
997        #[group(self.paren)]
998        pub entries: Vec<OptArgEntryV1>,
999    }
1000
1001    /// One `label = expr` entry of an [`OptArgsV1`] bundle — a FULL
1002    /// expression (`?(bias = 1 + n)`), routed through [`super::ExprErasedV1`]
1003    /// so this satellite never joins `Expr`'s SCC.
1004    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1005    pub struct OptArgEntryV1 {
1006        pub label: VarTok,
1007        pub eq: DefEqTok,
1008        pub value: super::ExprErasedV1,
1009        pub comma: Option<CommaTok>,
1010    }
1011
1012    /// `expr_bot`-analogue: an atomic expression. **Delta:** [`Atomic::List`]
1013    /// and [`Atomic::Record`] are now `,`-separated
1014    /// (`optterm_list(COMMA, …)`, `parser_v1.mly:935,942`) rather than
1015    /// 0.0.6's `;`-separated forms — see [`ListItem`]/[`RecordField`].
1016    /// Parenthesized/tuple bodies were already `,`-separated in 0.0.6 and
1017    /// are unchanged (`parser_v1.mly:914`).
1018    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1019    pub enum Atomic {
1020        Length(LengthTok),
1021        Float(FloatTok),
1022        Int(IntTok),
1023        Literal(LiteralTok),
1024        True(KwTrue),
1025        False(KwFalse),
1026        /// A bare constructor, e.g. `None`, or the head of `Some 1`.
1027        Ctor(CtorTok),
1028        Var(VarTok),
1029        /// `Mod.x` — a module-qualified variable.
1030        VarWithMod(VarWithModTok),
1031        /// `command \cmd` (upstream `parser_v1.mly:906`, `L_PAREN COMMAND
1032        /// backslash_cmd R_PAREN` — the parens arrive via [`Atomic::Paren`]
1033        /// here, exactly like [`crate::cst::ast::Atomic::Command`], which
1034        /// this reproduces verbatim; the `plus_cmd` alternative at :908 is
1035        /// deferred with the same rationale as the 0.0.6 comment). Needed by
1036        /// the transliterated `v01-mini.satyh`'s `(command \math)`.
1037        Command { kw: CommandTok, name: AnyHorzCmdTok },
1038        /// `()`
1039        Unit { paren: UnitParen },
1040        /// `( expr )` or `( expr, expr, … )` (the latter elaborates to a
1041        /// tuple).
1042        Paren {
1043            paren: ParenGroup<()>,
1044            #[group(self.paren)]
1045            inner: Box<ParenBody>,
1046        },
1047        /// `(| label = expr, … |)` or `(| base with label = expr, … |)`
1048        /// (`,`-separated — see [`RecordBody`]).
1049        Record {
1050            rec: RecordGroup<()>,
1051            #[group(self.rec)]
1052            body: RecordBody,
1053        },
1054        /// `[ expr, … ]` (`,`-separated — see [`ListItem`]).
1055        List {
1056            list: ListGroup<()>,
1057            #[group(self.list)]
1058            items: Vec<ListItem>,
1059        },
1060        /// `{ inline text }`
1061        InlineText {
1062            igrp: InlineGroup<()>,
1063            #[group(self.igrp)]
1064            elems: Vec<InlineElem>,
1065        },
1066        /// `'< block text >`
1067        BlockText {
1068            bgrp: BlockGroup<()>,
1069            #[group(self.bgrp)]
1070            elems: Vec<BlockElem>,
1071        },
1072        /// `${ math }`. Parses the same math grammar as 0.0.6; the
1073        /// `math-text`/`math-boxes` value split is a lowering/typing
1074        /// concern, not a cst_v1 shape change.
1075        MathText {
1076            mgrp: MathGroup<()>,
1077            #[group(self.mgrp)]
1078            elems: Vec<super::MathErasedV1>,
1079        },
1080    }
1081
1082    /// `(| … |)`'s content: either a plain field list, or a *record
1083    /// update* `base with l = e, …` (`parser_v1.mly:942-957`). `Update` is
1084    /// tried first (backtracks cleanly to `Fields`, same rationale as
1085    /// [`crate::cst::ast::RecordBody`]).
1086    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1087    pub enum RecordBody {
1088        Update {
1089            base: super::ExprErasedV1,
1090            with_kw: KwWith,
1091            fields: Vec<RecordField>,
1092        },
1093        Fields(Vec<RecordField>),
1094    }
1095
1096    /// The parenthesized-expression group's content: one expression, plus
1097    /// any `, expr` continuations (present only for a tuple) — unchanged
1098    /// from 0.0.6 (already `,`-separated, `parser_v1.mly:914`).
1099    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1100    pub struct ParenBody {
1101        pub first: super::ExprErasedV1,
1102        pub rest: Vec<CommaExpr>,
1103    }
1104
1105    /// A `, expr` continuation inside a parenthesized tuple.
1106    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1107    pub struct CommaExpr {
1108        pub comma: CommaTok,
1109        pub value: super::ExprErasedV1,
1110    }
1111
1112    /// One record field `label = expr,` (the last `,` is optional; `=` is
1113    /// upstream's `EXACT_EQ`, reusing [`DefEqTok`]). **Delta from
1114    /// [`crate::cst::ast::RecordField`]:** `,` separator, not `;`.
1115    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1116    pub struct RecordField {
1117        pub name: VarTok,
1118        pub eq: DefEqTok,
1119        pub value: super::ExprErasedV1,
1120        pub comma: Option<CommaTok>,
1121    }
1122
1123    /// One list element `expr,` (the last `,` is optional). **Delta from
1124    /// [`crate::cst::ast::ListItem`]:** `,` separator, not `;`.
1125    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1126    pub struct ListItem {
1127        pub value: super::ExprErasedV1,
1128        pub comma: Option<CommaTok>,
1129    }
1130
1131    /// One inline-text element — identical shape to
1132    /// [`crate::cst::ast::InlineElem`] (no 0.1 delta; text-mode content is
1133    /// untouched by the comma/`end` deltas).
1134    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1135    pub enum InlineElem {
1136        Char(CharTok),
1137        /// A backtick literal written inside inline text (`` `…` ``). Its own
1138        /// arm, not a `Char` run, so the elaborator can dispatch it through the
1139        /// context's code-text command — see `Token::CodeText`.
1140        CodeText(CodeTextTok),
1141        Space(SpaceTok),
1142        Break(BreakTok),
1143        /// `#var;` — embeds a program variable's value as inline content.
1144        Embed { var: VarInHorzTok, semi: EndActiveTok },
1145        /// `${ math }` — embeds math content as inline text.
1146        EmbedMath {
1147            mgrp: MathGroup<()>,
1148            #[group(self.mgrp)]
1149            elems: Vec<super::MathErasedV1>,
1150        },
1151        /// `\cmd …` (`name` also accepts the module-qualified `\Mod.cmd`
1152        /// form).
1153        Cmd { name: AnyHorzCmdTok, tail: CmdTail },
1154        /// An itemize bullet (`*`+) marker.
1155        ItemBullet(ItemTok),
1156        /// A `|` separator marker.
1157        Sep(SepTok),
1158    }
1159
1160    /// One block-text element — identical shape to
1161    /// [`crate::cst::ast::BlockElem`] (no 0.1 delta).
1162    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1163    pub enum BlockElem {
1164        /// `#var;` — embeds a program variable's value as block content.
1165        Embed { var: VarInVertTok, semi: EndActiveTok },
1166        /// `+cmd …` (`name` also accepts the module-qualified `+Mod.cmd`
1167        /// form).
1168        Cmd { name: AnyVertCmdTok, tail: CmdTail },
1169    }
1170
1171    /// A command's arguments — identical shape to
1172    /// [`crate::cst::ast::CmdTail`] — **0.1 delta:** an optional LEADING
1173    /// `?(l = e, …)` bundle. A command
1174    /// applied with an optional on its FIRST argument (`\cmd ?(l = e){arg}`,
1175    /// `+sec ?(label = t){title}<body>` — the ONLY shape the capstone census
1176    /// finds) can't ride inside `args` (an `expr_app` application chain whose
1177    /// *head* must be a bare `Atomic`, never a `?`-headed bundle — the head
1178    /// slot has no place for a leading bundle), so it is peeled off here as
1179    /// `lead_opts` and re-attached to the first argument at lowering
1180    /// (`v1::lower::lower_cmd_tail`). A bundle on a LATER argument
1181    /// (`\cmd{a} ?(l = e){b}`) still rides inside `args` as an ordinary
1182    /// [`AppArg::Bundled`]. `?(`-headed, token-disjoint from every
1183    /// `args` head shape, so a bundle-less tail parses `lead_opts: None`.
1184    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1185    pub enum CmdTail {
1186        /// `;` — no arguments.
1187        Semi(EndActiveTok),
1188        /// The argument chain, optionally prefixed by a leading `?(l = e, …)`
1189        /// bundle on the first argument.
1190        Args {
1191            lead_opts: Option<OptArgsV1>,
1192            args: super::ExprErasedV1,
1193            semi: Option<EndActiveTok>,
1194        },
1195    }
1196
1197    /// `patas`-analogue: a pattern, plus an optional `as name` binding —
1198    /// identical shape to [`crate::cst::ast::Pattern`] (no 0.1 delta).
1199    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1200    pub struct Pattern {
1201        pub head: PatCons,
1202        pub as_clause: Option<AsClause>,
1203    }
1204
1205    /// The `as name` suffix of a pattern.
1206    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1207    pub struct AsClause {
1208        pub as_kw: KwAs,
1209        pub name: VarTok,
1210    }
1211
1212    /// `pattr`-analogue: a `patbot`, followed by any number of `:: patbot`
1213    /// segments — identical shape to [`crate::cst::ast::PatCons`] (see its
1214    /// doc comment for why this is a flattened `Vec` rather than right
1215    /// recursion; no 0.1 delta).
1216    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1217    pub struct PatCons {
1218        pub head: PatBot,
1219        pub tail: Vec<ConsSeg>,
1220    }
1221
1222    /// One `:: patbot` continuation of a cons pattern.
1223    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1224    pub struct ConsSeg {
1225        pub cons: ConsTok,
1226        pub tail: PatBot,
1227    }
1228
1229    /// `patbot`, plus the constructor-pattern forms `pattr` adds —
1230    /// identical shape to [`crate::cst::ast::PatBot`], except
1231    /// [`PatBot::List`] is now `,`-separated (`parser_v1.mly:990-1015`,
1232    /// comma-sep list/tuple patterns).
1233    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1234    pub enum PatBot {
1235        /// `Ctor patbot` — a constructor applied to one argument pattern.
1236        /// This field is `PatBot`'s own self-loop (the root SCC).
1237        CtorApplied { ctor: CtorTok, arg: Box<PatBot> },
1238        /// A bare (nullary) constructor pattern.
1239        Ctor(CtorTok),
1240        Int(IntTok),
1241        True(KwTrue),
1242        False(KwFalse),
1243        Str(LiteralTok),
1244        Wild(WildcardTok),
1245        Var(VarTok),
1246        /// `()`
1247        Unit { paren: UnitParen },
1248        /// `( pat )` or `( pat, pat, … )` (the latter elaborates to a tuple
1249        /// pattern; already `,`-separated in 0.0.6, unchanged).
1250        Paren {
1251            paren: ParenGroup<()>,
1252            #[group(self.paren)]
1253            inner: Box<PatternParenBody>,
1254        },
1255        /// `[ pat, … ]` (also matches `[]`). **Delta:** `,` separator, not
1256        /// `;`.
1257        List {
1258            plist: ListGroup<()>,
1259            #[group(self.plist)]
1260            items: Vec<PatListItem>,
1261        },
1262    }
1263
1264    /// The parenthesized-pattern group's content: one pattern, plus any
1265    /// `, pat` continuations (present only for a tuple pattern).
1266    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1267    pub struct PatternParenBody {
1268        pub first: super::PatErasedV1,
1269        pub rest: Vec<CommaPattern>,
1270    }
1271
1272    /// A `, pat` continuation inside a parenthesized tuple pattern.
1273    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1274    pub struct CommaPattern {
1275        pub comma: CommaTok,
1276        pub value: super::PatErasedV1,
1277    }
1278
1279    /// One list-pattern element `pat,` (the last `,` is optional). **Delta
1280    /// from [`crate::cst::ast::PatListItem`]:** `,` separator, not `;`.
1281    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1282    pub struct PatListItem {
1283        pub value: super::PatErasedV1,
1284        pub comma: Option<CommaTok>,
1285    }
1286
1287    /// A type-expression grammar (`typ`/`typ_prod`/`typ_app`/`typ_bot`,
1288    /// `parser_v1.mly:685-752`, simplified — same scope as
1289    /// [`crate::cst::ast::TypeExpr`]). Spells products (`length * length`,
1290    /// [`TypeProd`]) and prefix type application (`list int`, [`TypeApp`]) —
1291    /// without them a `type` bind could declare almost nothing — plus the
1292    /// `?(…)` labeled-optional domain prefix
1293    /// ([`TypeExpr::OptRowFun`]), where a
1294    /// row-variable TAIL (`?(… | ?'r) ->`) parses but is rejected at
1295    /// lowering (it needs signature-level row quantification, not yet
1296    /// implemented). Self-recursive only through `Fun`'s/`OptRowFun`'s
1297    /// codomain (right recursion); parenthesized nesting goes through
1298    /// [`super::TyErasedV1`].
1299    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1300    pub enum TypeExpr {
1301        /// `?(l : ty, … [| ?'r]) dom -> cod` (`typ` `:688-693`, `typ_opt_dom`
1302        /// `:753-758`). `?`-headed — neither
1303        /// `Fun`/`Atom` (headed by `TypeProd`) can start with
1304        /// `OptionalTypeTok`, so declared order relative to them is
1305        /// safety-neutral; declared first to mirror the upstream `typ`
1306        /// production order. Lowered (`v1/lower.rs`) to
1307        /// `cst::ast::TypeExpr::OptRowFun`, thence (`typecheck.rs`) to
1308        /// `MonoType::Func(Row::Cons(l1, ty1, … Row::Empty), dom, cod)` — a
1309        /// CLOSED row, matching what `Ast::LambdaOpt` infers,
1310        /// so an explicit `?(l:τ)->` signature unifies against an actual
1311        /// `?(l=x)`-taking function.
1312        OptRowFun {
1313            opt_dom: TypeOptDomV1,
1314            dom: TypeProd,
1315            arrow: ArrowTok,
1316            cod: Box<TypeExpr>,
1317        },
1318        /// `dom -> cod` (right-associative). This field is `TypeExpr`'s own
1319        /// self-loop (the root SCC). `dom` widened from [`TypeAtom`] to
1320        /// [`TypeProd`] so e.g. `'a option -> 'b option` and
1321        /// `'a * 'b -> 'c` both parse at their expected precedence.
1322        Fun {
1323            dom: TypeProd,
1324            arrow: ArrowTok,
1325            cod: Box<TypeExpr>,
1326        },
1327        /// The non-arrow fallthrough — widened from [`TypeAtom`] to
1328        /// [`TypeProd`]: a product/application with no
1329        /// enclosing arrow is still just "the whole type expression minus
1330        /// `->`".
1331        Atom(TypeProd),
1332    }
1333
1334    /// `?(l : ty, … [| ?'r])` — the (possibly row-tailed) labeled-optional
1335    /// domain prefix of a [`TypeExpr::OptRowFun`] (`typ_opt_dom`,
1336    /// `parser_v1.mly:753-758`).
1337    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1338    pub struct TypeOptDomV1 {
1339        pub q: OptionalTypeTok,
1340        pub paren: ParenGroup<()>,
1341        #[group(self.paren)]
1342        pub inner: TypeOptDomInnerV1,
1343    }
1344
1345    /// A [`TypeOptDomV1`]'s group content: one or more `label : typ` entries
1346    /// (nonempty enforced at lowering), then an optional `| ?'r` row-variable
1347    /// tail (`typ_opt_dom` `:756-757`) — parsed, but rejected with a
1348    /// `LowerError` (needs signature-level row quantification, not
1349    /// implemented; contrast [`TypeRecordInnerV1`]'s own
1350    /// `row_tail`, which IS fully supported, since a bare
1351    /// record-typed value has no `quant`-list obligation to satisfy).
1352    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1353    pub struct TypeOptDomInnerV1 {
1354        pub entries: Vec<TypeOptEntryV1>,
1355        pub row_tail: Option<RowTailV1>,
1356    }
1357
1358    /// One `label : typ,` entry of a [`TypeOptDomV1`] (last `,` optional;
1359    /// `typ_opt_dom_entry`, `parser_v1.mly:759-762` — COLON, unlike the
1360    /// value-level `?(l = e)` bundle's `=`).
1361    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1362    pub struct TypeOptEntryV1 {
1363        pub label: VarTok,
1364        pub colon: ColonTok,
1365        pub ty: super::TyErasedV1,
1366        pub comma: Option<CommaTok>,
1367    }
1368
1369    /// `| ?'r` — a row-variable tail (shared by [`TypeOptDomInnerV1`] and
1370    /// [`TypeRecordInnerV1`]; `parser_v1.mly:748-749`/`:756-757`).
1371    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1372    pub struct RowTailV1 {
1373        pub bar: BarTok,
1374        pub var: RowVarTok,
1375    }
1376
1377    /// `typ_prod` (`parser_v1.mly:696-709`): one or more `*`-separated
1378    /// [`TypeApp`]s, flattened to head+`Vec` exactly like
1379    /// [`crate::cst::ast::TypeProd`] (`cst.rs:1284-1295`) — the same
1380    /// deferred-fold technique as `OpChain`/`PatCons`, keeping `TypeExpr` a
1381    /// singleton SCC.
1382    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1383    pub struct TypeProd {
1384        pub first: TypeApp,
1385        pub rest: Vec<StarType>,
1386    }
1387
1388    /// A `* ty` continuation of a [`TypeProd`].
1389    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1390    pub struct StarType {
1391        pub star: ExactTimesTok,
1392        pub ty: TypeApp,
1393    }
1394
1395    /// `typ_app` (`parser_v1.mly:711-739`). **0.1 delta from
1396    /// [`crate::cst::ast::TypeApp`] (`cst.rs:1297-1312`):** application is
1397    /// PREFIX and n-ary (`list int`, `pair int bool`), not 0.0.6's postfix
1398    /// single-argument (`int list`) — the prefix→postfix bridge (with an
1399    /// arity-1 guard: arity ≥ 2 is a `LowerError`, not a parse error) lives
1400    /// in `v1/lower.rs`. `Applied`/`AppliedLong` (needing at least one
1401    /// argument atom) are tried before `Atom` — a bare name has no argument
1402    /// atom to consume and falls through cleanly (a following
1403    /// keyword/`=`/`and`/`->`/`*` never parses as a [`TypeAtom`]).
1404    ///
1405    /// Four further arms are keyword- or token-headed and so
1406    /// disjoint from `Applied`/`Atom`'s `VarTok`-headed shapes (ordering them
1407    /// BEFORE those is cosmetic, not load-bearing):
1408    ///
1409    /// - [`TypeApp::InlineCmdTy`]/[`TypeApp::BlockCmdTy`]/
1410    ///   [`TypeApp::MathCmdTy`]: `inline [τ, …]`/
1411    ///   `block [τ, …]`/`math [τ, …]` command types (`parser.mly:730-735`,
1412    ///   `typ_cmd_arg` `:763-774`; `math […]`: `parser.mly:830-831`),
1413    ///   `KwInline`/`KwBlock`/`KwMath`-headed (all three are V0_1 keywords
1414    ///   already — `val inline`/`val block` binds; `math` since the
1415    ///   math-split). One deliberate superset of upstream remains: each
1416    ///   bracketed slot is a full [`super::TyErasedV1`] (`TypeExpr`), not upstream's
1417    ///   narrower `typ_prod`. The `?(label: τ, …)` optional-labeled-slot
1418    ///   prefix is modeled — see [`TypeCmdArgItemV1::opts`];
1419    ///   `MathCmdTy` reuses `TypeCmdArgItemV1` as-is, so
1420    ///   `math [?(l : τ) …]` sig rows come for free.
1421    /// - [`AppliedLong`](TypeApp::AppliedLong): `M.t τ…` — the `LONG_LOWER`
1422    ///   qualified-head twin of `Applied` (`parser.mly:720-728`,
1423    ///   `LONG_LOWER` `lexer.mll:318`), `VarWithModTok`-headed (lexed by the
1424    ///   program-mode capital-head scan, `lexer.rs:753-777`). Needed to NAME
1425    ///   an abstract type from outside its sealing module — without
1426    ///   it, an opaque `M.t` could never appear in another module's
1427    ///   signature at all.
1428    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1429    pub enum TypeApp {
1430        /// `inline [τ, …]` — see the enum doc comment.
1431        //
1432        // NOTE the group fields are `ilist`/`blist`/`mlist`, not three `list`s:
1433        // syan names a group substruct after (group-field name, ENUM name) with
1434        // no variant component, so same-named groups in one enum collide
1435        // (E0428 + E0119).
1436        InlineCmdTy {
1437            kw: KwInline,
1438            ilist: ListGroup<()>,
1439            #[group(self.ilist)]
1440            args: Vec<TypeCmdArgItemV1>,
1441        },
1442        /// `block [τ, …]` — see the enum doc comment.
1443        BlockCmdTy {
1444            kw: KwBlock,
1445            blist: ListGroup<()>,
1446            #[group(self.blist)]
1447            args: Vec<TypeCmdArgItemV1>,
1448        },
1449        /// `math [τ, …]` (upstream
1450        /// `parser.mly:830-831` `MATH L_SQUARE optterm_list(COMMA,
1451        /// typ_cmd_arg) R_SQUARE → MMathCommandType(mncmdargtys)` — same
1452        /// `typ_cmd_arg` as inline/block). `KwMath`-headed, so this arm is
1453        /// disjoint from `Applied`/`Atom` and ambiguity-free: a bare `math`
1454        /// can never lex as a `VarTok` under V0_1 at all.
1455        MathCmdTy {
1456            kw: KwMath,
1457            mlist: ListGroup<()>,
1458            #[group(self.mlist)]
1459            args: Vec<TypeCmdArgItemV1>,
1460        },
1461        /// `M.t τ…` — see the enum doc comment. Mirrors
1462        /// `Applied`'s n-ary shape (`v1/lower.rs`'s prefix→postfix bridge
1463        /// rejects arity ≥ 2 identically for both).
1464        AppliedLong {
1465            ctor: VarWithModTok,
1466            first: TypeAtom,
1467            rest: Vec<TypeAtom>,
1468        },
1469        Applied {
1470            ctor: VarTok,
1471            first: TypeAtom,
1472            rest: Vec<TypeAtom>,
1473        },
1474        Atom(TypeAtom),
1475    }
1476
1477    /// One `[…]`-bracketed command-type argument slot: an optional
1478    /// `?(l : τ, …)` labeled-optional bundle PREFIX (upstream
1479    /// `typ_cmd_arg : option(typ_opt_dom) typ_prod`,
1480    /// `parser.mly:753-773`), then the mandatory `τ,` (`,`-separated, last
1481    /// `,` optional — the [`ListItem`] pattern). A full [`super::TyErasedV1`] per
1482    /// slot (permissive superset of upstream's narrower `typ_prod`). `opts`
1483    /// is `Option`-tried first: a non-`?`-headed slot fails the `?` head with
1484    /// no token stolen, so a plain slot parses `opts: None`.
1485    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1486    pub struct TypeCmdArgItemV1 {
1487        pub opts: Option<TypeCmdOptDomV1>,
1488        pub ty: super::TyErasedV1,
1489        pub comma: Option<CommaTok>,
1490    }
1491
1492    /// `?(l : τ, …)` — a CLOSED command-type optional bundle (upstream
1493    /// `typ_opt_dom`, `parser.mly:755-761`,
1494    /// minus the `| ?'r` row-variable tail: command optional-argument types
1495    /// are closed maps, never rows — upstream itself silently DISCARDS a
1496    /// written row variable here, `parser.mly:859-869`'s literal `TODO
1497    /// (error)` — so this port doesn't model one either; a stray `?'r` inside
1498    /// a command-type bracket is a parse error, faithfully matching
1499    /// upstream's "never actually usable" treatment of it). Mirrors
1500    /// [`crate::cst::ast::CstTypeOptDom`]-shaped satellites elsewhere in this file.
1501    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1502    pub struct TypeCmdOptDomV1 {
1503        pub q: OptionalTypeTok,
1504        pub paren: ParenGroup<()>,
1505        #[group(self.paren)]
1506        pub entries: Vec<TypeCmdOptEntryV1>,
1507    }
1508
1509    /// One `label : τ,` entry of a [`TypeCmdOptDomV1`] bundle (the last `,`
1510    /// is optional).
1511    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1512    pub struct TypeCmdOptEntryV1 {
1513        pub label: VarTok,
1514        pub colon: ColonTok,
1515        pub ty: super::TyErasedV1,
1516        pub comma: Option<CommaTok>,
1517    }
1518
1519    /// An atomic type expression. `parser_v1.mly:740-752`'s record forms are
1520    /// fully modeled: both the closed form and the open (row-var-tailed) form
1521    /// share [`TypeAtom::Record`], distinguished by
1522    /// [`TypeRecordInnerV1::row_tail`].
1523    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1524    pub enum TypeAtom {
1525        /// `( ty )`
1526        Paren {
1527            paren: ParenGroup<()>,
1528            #[group(self.paren)]
1529            inner: super::TyErasedV1,
1530        },
1531        /// `(| l1 : ty1, l2 : ty2, … |)` (closed) or `(| l1 : ty1, … | ?'r |)`
1532        /// (open — a row-variable tail)
1533        /// (`typ_bot`'s two `L_RECORD` arms, `parser_v1.mly:746-749`;
1534        /// `typ_record_elem` `:775-777` — COLON fields, unlike record
1535        /// EXPRESSIONS' `l = e`). Lowered (`v1/lower.rs`): the closed form to
1536        /// the existing `cst::ast::TypeAtom::Record` (`cst.rs:1344`) and
1537        /// thence to a closed `MonoType::Record` row (`typecheck.rs:512`);
1538        /// the open form to the additive `cst::ast::TypeAtom::RecordOpen`
1539        /// and thence to an OPEN `MonoType::Record(Row::Var(…))` — a fresh
1540        /// row variable, using the existing generic `Row`/`RowVarRef`/
1541        /// `unify_row` machinery (no new type machinery needed).
1542        Record {
1543            rec: RecordGroup<()>,
1544            #[group(self.rec)]
1545            inner: TypeRecordInnerV1,
1546        },
1547        /// A type variable, e.g. `'a`.
1548        Var(TypeVarTok),
1549        /// `M.t` — a qualified type name (upstream
1550        /// `LONG_LOWER`, `parser.mly:742-743`). `VarWithModTok`-headed,
1551        /// token-disjoint from `Var`/`Name`/`Paren` (`TypeVarTok`/`VarTok`/
1552        /// `LParenTok`) — see [`TypeApp::AppliedLong`]'s doc comment.
1553        LongName(VarWithModTok),
1554        /// A (possibly qualified) type name, e.g. `int`, `string`.
1555        Name(VarTok),
1556    }
1557
1558    /// A [`TypeAtom::Record`]'s group content: the field list, plus an
1559    /// optional `| ?'r` row-variable tail (present ⇒ an OPEN record type;
1560    /// absent ⇒ closed).
1561    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1562    pub struct TypeRecordInnerV1 {
1563        pub fields: Vec<TypeRecordFieldV1>,
1564        pub row_tail: Option<RowTailV1>,
1565    }
1566
1567    /// One `l : ty,` field (last `,` optional — the [`ListItem`] pattern).
1568    /// **Deltas from [`crate::cst::ast::TypeRecordField`] (`cst.rs:1363`):**
1569    /// `,` separator, not `;` (same delta as [`RecordField`]); field type is
1570    /// a full [`super::TyErasedV1`] (upstream `typ_record_elem :776` takes a
1571    /// full `typ`) — erased, not a direct `TypeExpr`, for the same
1572    /// cycle-avoidance reason `cst.rs:1355-1362` documents.
1573    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1574    pub struct TypeRecordFieldV1 {
1575        pub name: VarTok,
1576        pub colon: ColonTok,
1577        pub ty: super::TyErasedV1,
1578        pub comma: Option<CommaTok>,
1579    }
1580
1581    /// `mathtop`-analogue: one math element — identical shape to
1582    /// [`crate::cst::ast::MathElemCst`] (no 0.1 delta; see its doc comment
1583    /// for why this needs no direct self-loop of its own).
1584    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1585    pub struct MathElemCst {
1586        pub base: MathBot,
1587        pub scripts: Vec<MathScript>,
1588    }
1589
1590    /// `mathbot` — identical shape to [`crate::cst::ast::MathBot`] (no 0.1
1591    /// delta; `name` accepts a module-qualified `\Mod.cmd` math command
1592    /// too, `AnyMathCmdTok::Mod` — the lexer already emits
1593    /// `Token::MathCmdWithMod` for one, `lexer.rs`'s `\\` arm in `Mode::
1594    /// Math`, since `${\Math.paren{…}}`-shaped
1595    /// qualified references need it to parse at all).
1596    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1597    pub enum MathBot {
1598        /// `\cmd matharg*`, sigil-only or module-qualified (`\Mod.cmd
1599        /// matharg*`).
1600        Cmd { name: AnyMathCmdTok, args: Vec<MathArg> },
1601        Chars(MathCharTok),
1602        /// `#var` (math mode never trails this with `;`).
1603        Embed(VarInMathTok),
1604        /// A `|` separator marker (flat; elaborator regroups).
1605        Sep(SepTok),
1606        /// `{ … }` — re-enters the math grammar.
1607        Group {
1608            mgrp: MathGroup<()>,
1609            #[group(self.mgrp)]
1610            elems: Vec<super::MathErasedV1>,
1611        },
1612    }
1613
1614    /// One postfix script combo of a [`MathElemCst`] — identical shape to
1615    /// [`crate::cst::ast::MathScript`] (no 0.1 delta).
1616    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1617    pub enum MathScript {
1618        /// `^ group`
1619        Super { hat: SuperscriptTok, group: MathGroupArg },
1620        /// `_ group`
1621        Sub { under: SubscriptTok, group: MathGroupArg },
1622        /// A run of `'` marks — sugar for a superscript of primes
1623        /// characters.
1624        Primes(PrimesTok),
1625    }
1626
1627    /// `mathgroup`-analogue: a script's operand is either a bracketed math
1628    /// group or a bare `mathbot`.
1629    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1630    pub enum MathGroupArg {
1631        Group {
1632            mgrp: MathGroup<()>,
1633            #[group(self.mgrp)]
1634            elems: Vec<super::MathErasedV1>,
1635        },
1636        Bot(Box<MathBot>),
1637    }
1638
1639    /// `matharg`-analogue: one command argument in math mode — identical
1640    /// shape to [`crate::cst::ast::MathArg`] (no 0.1 delta; the escape
1641    /// bodies reuse the now-comma-separated [`ParenBody`]/[`ListItem`]/
1642    /// [`RecordBody`] defined above).
1643    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1644    pub enum MathArg {
1645        /// `{ math }`.
1646        Math {
1647            mgrp: MathGroup<()>,
1648            #[group(self.mgrp)]
1649            elems: Vec<super::MathErasedV1>,
1650        },
1651        /// `!{ inline text }`.
1652        Inline {
1653            igrp: InlineGroup<()>,
1654            #[group(self.igrp)]
1655            elems: Vec<InlineElem>,
1656        },
1657        /// `!<block text>`.
1658        Block {
1659            bgrp: BlockGroup<()>,
1660            #[group(self.bgrp)]
1661            elems: Vec<BlockElem>,
1662        },
1663        /// `!(e)` / `!(e, e, …)`.
1664        ParenEscape {
1665            paren: ParenGroup<()>,
1666            #[group(self.paren)]
1667            inner: Box<ParenBody>,
1668        },
1669        /// `![e, …]`.
1670        ListEscape {
1671            list: ListGroup<()>,
1672            #[group(self.list)]
1673            items: Vec<ListItem>,
1674        },
1675        /// `!(|l = e, …|)`.
1676        RecordEscape {
1677            rec: RecordGroup<()>,
1678            #[group(self.rec)]
1679            body: RecordBody,
1680        },
1681    }
1682
1683    // ---- the module/signature layer --------------------------------------
1684
1685    /// `mod_chain`: `UPPER | LONG_UPPER` (`parser_v1.mly:404-414`). `M.N.P`
1686    /// arrives as ONE [`LongUpperTok`] (the V0_1 lexer branch), so a chain is
1687    /// always exactly one token — which is what makes [`ModExpr::App`]'s two-
1688    /// chain juxtaposition (`F X`, `F.G X.Y`) unambiguous at the token level.
1689    /// Token-disjoint arms; order is cosmetic.
1690    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1691    pub enum ModChainV1 {
1692        Long(LongUpperTok),
1693        Single(CtorTok),
1694    }
1695
1696    /// `modexpr` (`parser_v1.mly:380-403`). SELF-LOOP ROOT: `Functor.body:
1697    /// Box<ModExpr>` (`:381-382`). Variant order is parse priority:
1698    /// `Functor` (`fun`-headed) and `Struct` (`struct`-headed) are
1699    /// keyword-disjoint from everything; `Coerce` (`UPPER :>`) must precede
1700    /// `App`/`Var` so the `:>` suffix is claimed before a bare chain matches;
1701    /// `App` (two chains, `modexpr_app` `:388-394`) precedes `Var` (one
1702    /// chain, `modexpr_bot` `:398-400`) for longest-match. Struct bodies go
1703    /// through [`super::StructBindV1`] (the struct-body connector, erased), so
1704    /// `ModExpr` never statically references [`super::Bind`] — see the
1705    /// module doc comment's SCC story.
1706    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1707    pub enum ModExpr {
1708        /// `FUN ( UPPER : sigexpr ) ARROW modexpr` (`parser_v1.mly:381-382`).
1709        Functor {
1710            fun_kw: KwFun,
1711            lp: LParenTok,
1712            param: CtorTok,
1713            colon: ColonTok,
1714            dom: Box<SigExpr>,
1715            rp: RParenTok,
1716            arrow: ArrowTok,
1717            body: Box<ModExpr>,
1718        },
1719        /// `UPPER COERCE sigexpr` (`:383-384`) — coercion applies to a BARE
1720        /// module name only, upstream-faithfully (`A.B :> S` is a parse
1721        /// error there too).
1722        Coerce {
1723            name: CtorTok,
1724            coerce: CoerceTok,
1725            sig_: Box<SigExpr>,
1726        },
1727        /// `mod_chain mod_chain` — functor application (`:389-394`).
1728        App { func: ModChainV1, arg: ModChainV1 },
1729        /// `mod_chain` — a (possibly long) module path (`:399-400`).
1730        Var(ModChainV1),
1731        /// `STRUCT list(bind) END` (`:401-402`) — the only form `v1/lower.rs`
1732        /// gives real semantics to; reuses the struct-body connector.
1733        Struct {
1734            struct_kw: KwStruct,
1735            binds: Vec<super::StructBindV1>,
1736            end_kw: KwEnd,
1737        },
1738    }
1739
1740    /// `sigexpr` (`parser_v1.mly:558-573`). SELF-LOOP ROOT: `Functor.dom`/
1741    /// `Functor.cod: Box<SigExpr>` (`:570-571`).
1742    ///
1743    /// **Left-recursion note (load-bearing).** The naive sketch would write
1744    /// `With { base: Box<SigExpr>, … }` — as a syan2 ordered-choice
1745    /// production that is LEFT RECURSION (`SigExpr` would begin by parsing
1746    /// `SigExpr`; syan2 gives no diagnostic, it just recurses/fails at parse
1747    /// time — a known consumer hazard). Upstream is *not* left-recursive:
1748    /// the `with` base is `sigexpr_bot` (`:559,564`) and `with` cannot chain
1749    /// (the result of a `with` is never itself a valid `with` base). So the
1750    /// faithful encoding is bot + one optional-shaped suffix arm, tried
1751    /// before the bare-bot fallthrough: `S with type t = int with type u =
1752    /// bool` is a parse error here exactly as upstream (pinned in tests).
1753    /// NO arm of this enum may ever begin with `Box<SigExpr>`/`SigExpr` as
1754    /// its first field — reviewer checklist item.
1755    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1756    pub enum SigExpr {
1757        /// `( UPPER : sigexpr ) ARROW sigexpr` (`:570-571`) — the functor
1758        /// signature. `(`-headed; no [`SigBotV1`] starts with `(`, so this
1759        /// is token-disjoint from the other arms.
1760        Functor {
1761            lp: LParenTok,
1762            param: CtorTok,
1763            colon: ColonTok,
1764            dom: Box<SigExpr>,
1765            rp: RParenTok,
1766            arrow: ArrowTok,
1767            cod: Box<SigExpr>,
1768        },
1769        /// `sigexpr_bot WITH TYPE bind_type` (`:559-563`) /
1770        /// `sigexpr_bot WITH mod_chain TYPE bind_type` (`:564-569`). The
1771        /// `Option<ModChainV1>` is greedy-then-backtrack: on `with type` the
1772        /// chain fails (`type` is a keyword token, not `UPPER`/`LONG_UPPER`)
1773        /// and collapses to `None`. `binds` goes through
1774        /// [`super::TypeBindsErasedV1`].
1775        WithType {
1776            base: SigBotV1,
1777            with_kw: KwWith,
1778            path: Option<ModChainV1>,
1779            type_kw: KwType,
1780            binds: super::TypeBindsErasedV1,
1781        },
1782        /// A bare `sigexpr_bot` (`:572-573`). Must come after [`SigExpr::WithType`]
1783        /// (maximal munch of the `with` suffix).
1784        Bot(SigBotV1),
1785    }
1786
1787    /// `sigexpr_bot` (`parser_v1.mly:575-595`) — a satellite (no self-loop;
1788    /// no edge back to `SigExpr`). Sig bodies go through
1789    /// [`super::StructDeclV1`] (opaque hand-written connector), so
1790    /// `SigBotV1` never statically references [`Decl`].
1791    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1792    pub enum SigBotV1 {
1793        /// `LONG_UPPER` — a signature path `M.N.S` (`:581-590`).
1794        Path(LongUpperTok),
1795        /// `UPPER` — a signature name (`:576-580`).
1796        Var(CtorTok),
1797        /// `SIG list(decl) END` (`:591-595`). `sig` is a version-independent
1798        /// keyword (`lexer.rs`).
1799        Sig {
1800            sig_kw: KwSig,
1801            decls: Vec<super::StructDeclV1>,
1802            end_kw: KwEnd,
1803        },
1804    }
1805
1806    /// `decl` (`parser_v1.mly:597-621`) — one item of a `sig … end` body.
1807    /// NOT a root: no arm contains `Decl`; reached only through
1808    /// [`super::StructDeclV1`], so `SigExpr ↔ Decl` never forms a rootless
1809    /// static sub-cycle (the shape `cst.rs`'s `AppArgErased` doc warns the
1810    /// engine rejects). Its recursion-bearing edges are plain DAG edges INTO
1811    /// roots: `ty: TypeExpr` (the same satellite→root shape as
1812    /// `RecClauseV1.params: Vec<PatBot>`) and `sig_: Box<SigExpr>`.
1813    ///
1814    /// Deferred arms (parse errors): macro decls `val \m : macro-type`
1815    /// (`:608-611`), row quantifiers (`rowquant`, `:631-633` — no
1816    /// `ROWVAR` token for this position yet).
1817    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1818    pub enum Decl {
1819        /// `VAL PERSISTENT? EXACT_TILDE? bound_identifier quant COLON typ`
1820        /// (`:598-603`; `quant`'s tyvar list `:623-630` — `val map 'a 'b :
1821        /// ('a -> 'b) -> …`). The stage prefix is the decl-side twin of
1822        /// [`super::Bind::Value`]'s own `stage` field, with the same
1823        /// ordered-choice argument (see that arm's doc comment).
1824        Val {
1825            kw: KwVal,
1826            stage: Option<super::BindStageV1>,
1827            name: super::BindName,
1828            quant: Vec<TypeVarTok>,
1829            colon: ColonTok,
1830            ty: TypeExpr,
1831        },
1832        /// `VAL BACKSLASH_CMD quant COLON typ` (`:604-605`). Plain
1833        /// [`HorzCmdTok`] — upstream uses the bare token, and program mode
1834        /// already lexes `\cmd`. Naming mirrors
1835        /// [`crate::cst::SigItem::ValHorzCmd`].
1836        ValHorzCmd {
1837            kw: KwVal,
1838            cmd: HorzCmdTok,
1839            quant: Vec<TypeVarTok>,
1840            colon: ColonTok,
1841            ty: TypeExpr,
1842        },
1843        /// `VAL PLUS_CMD quant COLON typ` (`:606-607`).
1844        ValVertCmd {
1845            kw: KwVal,
1846            cmd: VertCmdTok,
1847            quant: Vec<TypeVarTok>,
1848            colon: ColonTok,
1849            ty: TypeExpr,
1850        },
1851        /// `TYPE LOWER CONS kind` — an OPAQUE type (`:612-613`). Tried
1852        /// before the transparent [`Decl::Type`]: the two share the `type
1853        /// name` prefix and are told apart by `::` vs `=`/tyvars
1854        /// (backtracking is two tokens deep, cheap).
1855        TypeOpaque {
1856            kw: KwType,
1857            name: VarTok,
1858            cons: ConsTok,
1859            kind: KindV1,
1860        },
1861        /// `TYPE bind_type` — transparent type(s) (`:614-615`), sharing the
1862        /// grouped chain with [`SigExpr::WithType`].
1863        Type {
1864            kw: KwType,
1865            binds: super::TypeBindsErasedV1,
1866        },
1867        /// `MODULE UPPER COLON sigexpr` (`:616-617`) — note `:` here (a
1868        /// decl constrains), vs `:>` on binds (a bind seals).
1869        Module {
1870            kw: KwModule,
1871            name: CtorTok,
1872            colon: ColonTok,
1873            sig_: Box<SigExpr>,
1874        },
1875        /// `SIGNATURE UPPER EXACT_EQ sigexpr` (`:618-619`).
1876        Signature {
1877            kw: KwSignature,
1878            name: CtorTok,
1879            eq: DefEqTok,
1880            sig_: Box<SigExpr>,
1881        },
1882        /// `INCLUDE sigexpr` (`:620-621`) — a decl-include includes a
1883        /// SIGNATURE (contrast [`super::Bind::Include`], which includes a
1884        /// MODULE).
1885        Include { kw: KwInclude, sig_: Box<SigExpr> },
1886    }
1887
1888    // Ordered-choice safety of `Decl`: all arms are keyword-headed
1889    // (`val`/`type`/`module`/`signature`/`include`); within `val`, the
1890    // second token (`BindName`'s `Var`-or-`LParen` vs `HorzCmdTok` vs
1891    // `VertCmdTok`) is disjoint; within `type`, `TypeOpaque`-before-`Type`
1892    // as documented.
1893
1894    /// `kind` (`parser_v1.mly:672-677`): `kind_base (ARROW kind_base)*`
1895    /// flattened head+`Vec` — the same deferred-fold shape as
1896    /// [`TypeProd`]/[`PatCons`], keeping the type acyclic. `kind_base` is a
1897    /// bare LOWER (`:678-681`, `MKindName`), so the whole kind grammar is
1898    /// token-only. (`kind_row`, `:682-683`, arrives with row quantifiers,
1899    /// not yet implemented.)
1900    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1901    pub struct KindV1 {
1902        pub first: VarTok,
1903        pub rest: Vec<KindArrowV1>,
1904    }
1905
1906    /// An `-> kind_base` continuation of a [`KindV1`].
1907    #[derive(Parse, Unparse, Debug, Clone, PartialEq)]
1908    pub struct KindArrowV1 {
1909        pub arrow: ArrowTok,
1910        pub base: VarTok,
1911    }
1912
1913    // (`Quant` needs no struct: upstream `quant = list(tyquant)
1914    // list(rowquant)` (`:623-625`), and with rowquants deferred it is
1915    // exactly `Vec<TypeVarTok>` — inlined into `Decl::Val*` above.)
1916}
1917
1918/// Lex ([`crate::lexer::lex_with_version`] under [`crate::version::RustyfiVersion::V0_1`])
1919/// and parse a whole 0.1 `.saty`/`.satyh` source file. Mirrors
1920/// [`crate::cst::parse_file`]'s two-step shape exactly, sharing its
1921/// [`crate::cst::ParseFileError`] (no new error type).
1922pub fn parse_file_v1(src: &str) -> Result<FileV1, crate::cst::ParseFileError> {
1923    let atoms = crate::lexer::lex_with_version(src, crate::version::RustyfiVersion::V0_1)
1924        .map_err(|e| crate::cst::ParseFileError {
1925            span: e.span,
1926            message: e.msg,
1927        })?;
1928    let mut stream = crate::stream::AtomStream::new(atoms);
1929    <FileV1 as Parse<_>>::parse(&mut stream).map_err(|e| crate::cst::ParseFileError {
1930        span: *e.span(),
1931        message: render_parse_error(&e),
1932    })
1933}
1934
1935/// Flatten syan's nested error tree into one readable line. A private copy
1936/// of [`crate::cst`]'s identical helper (not `pub(crate)` there, and
1937/// `cst.rs` stays untouched — see the module doc comment).
1938fn render_parse_error(err: &syan::error::ParseError<Span>) -> String {
1939    format!("{err:?}")
1940}