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