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