Skip to main content

rustyfi_syntax/
leaf.rs

1//! Leaf token types and the delimiter `Group` aliases.
2//!
3//! The unit and single-field leaves (`KwLet`, `VarTok`, `IntTok`, ...) are
4//! generated by `token_leaves!` in [`crate::token`] and re-exported here (so
5//! `use crate::leaf::*` still names them). This module keeps only what that
6//! macro cannot express — the multi-field qualified-name leaves,
7//! `LengthTok`/`LiteralTok`, the multi-variant `BinOpTok`, and the
8//! `AnyHorzCmdTok`/`AnyVertCmdTok` alternations — plus the delimiter `Group`
9//! aliases, which are the core atom-generic `syan::nested::group::Group`.
10
11use crate::span::Span;
12// Re-export the generated leaves (and `Token`/`Atom`) so downstream
13// `use crate::leaf::*` resolves every leaf type from one place.
14pub use crate::token::*;
15use syan::error::ParseError;
16use syan::parse::unparse::Emitter;
17use syan::parse::{Parse, ParseStream, Unparse};
18use syan::span::Spanned;
19
20/// Multi-field qualified-name leaves: a `(Vec<String>, String)` payload
21/// (module path + bare name). `token_leaves!` supports only unit and
22/// single-field variants, so these stay hand-written — same peek → match →
23/// push-back-on-mismatch shape as the generated leaves.
24macro_rules! qualified_name_tokens {
25    ($($(#[$doc:meta])* $name:ident => $variant:ident, $desc:literal;)*) => {
26        $(
27            $(#[$doc])*
28            #[derive(Clone, Debug, PartialEq)]
29            pub struct $name {
30                pub mods: Vec<String>,
31                pub name: String,
32                pub span: Span,
33            }
34
35            impl Parse<Atom> for $name {
36                type Error = ParseError<Span>;
37
38                fn parse_stream<S: ParseStream<Atom = Atom>>(
39                        stream: &mut S,
40                    ) -> Result<Self, Self::Error> {
41                    match stream.next() {
42                        Some(Atom { slot: Token::$variant(mods, name), span }) => {
43                            Ok($name { mods, name, span })
44                        }
45                        Some(atom) => {
46                            let span = atom.span;
47                            stream.push(atom);
48                            Err(ParseError::expected(span, $desc))
49                        }
50                        None => Err(ParseError::eof(Span::default())),
51                    }
52                }
53            }
54
55            impl Unparse<Atom> for $name {
56                fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
57                    sink.write_one(Atom {
58                        slot: Token::$variant(self.mods.clone(), self.name.clone()),
59                        span: self.span,
60                    })
61                }
62            }
63
64            impl Spanned for $name {
65                type Span = Span;
66                fn span(&self) -> Span {
67                    self.span
68                }
69            }
70        )*
71    };
72}
73
74qualified_name_tokens! {
75    /// `#var` (or `#Mod.var`) in inline text, before the mode switches to active.
76    VarInHorzTok => VarInHorz, "a variable reference in inline text";
77    /// `#var` (or `#Mod.var`) in block text, before the mode switches to active.
78    VarInVertTok => VarInVert, "a variable reference in block text";
79    /// `#var` (or `#Mod.var`) in math mode.
80    VarInMathTok => VarInMath, "a variable reference in math";
81    /// A module-qualified variable, e.g. `Mod.x`.
82    VarWithModTok => VarWithMod, "a qualified variable name";
83    /// A dotted module path ending in an UPPER segment, e.g. `A.B.C`
84    /// (upstream `LONG_UPPER`) — module-expression paths, functor
85    /// application operands, and signature paths (`mod_chain`,
86    /// `sigexpr_bot`). V0_1-only (a lex error under V0_0).
87    LongUpperTok => LongUpper, "a qualified module path";
88    /// A module-qualified inline command, e.g. `\Mod.cmd`.
89    HorzCmdWithModTok => HorzCmdWithMod, "a qualified inline command";
90    /// A module-qualified block command, e.g. `+Mod.cmd`.
91    VertCmdWithModTok => VertCmdWithMod, "a qualified block command";
92    /// A module-qualified math command, e.g. `\Mod.cmd` in math mode.
93    MathCmdWithModTok => MathCmdWithMod, "a qualified math command";
94}
95
96/// Either sigil-only (`\cmd`) or module-qualified (`\Mod.cmd`) inline command
97/// name — `hcmd` in `parser.mly`. Not itself recursive, so (unlike
98/// [`crate::cst::ast::InlineElem`]) it can be a plain top-level derive.
99#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
100pub enum AnyHorzCmdTok {
101    Plain(HorzCmdTok),
102    Mod(HorzCmdWithModTok),
103}
104
105/// Either sigil-only (`+cmd`) or module-qualified (`+Mod.cmd`) block command
106/// name — `vcmd` in `parser.mly`.
107#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
108pub enum AnyVertCmdTok {
109    Plain(VertCmdTok),
110    Mod(VertCmdWithModTok),
111}
112
113/// Either sigil-only (`\cmd`) or module-qualified (`\Mod.cmd`) math
114/// command name in math mode — the math analogue of `AnyHorzCmdTok`.
115#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
116pub enum AnyMathCmdTok {
117    Plain(MathCmdTok),
118    Mod(MathCmdWithModTok),
119}
120
121/// A length constant such as `12pt` (two payload fields, hand-written).
122#[derive(Clone, Debug, PartialEq)]
123pub struct LengthTok {
124    pub value: f64,
125    pub unit: String,
126    pub span: Span,
127}
128
129impl Parse<Atom> for LengthTok {
130    type Error = ParseError<Span>;
131
132    fn parse_stream<S: ParseStream<Atom = Atom>>(
133            stream: &mut S,
134        ) -> Result<Self, Self::Error> {
135        match stream.next() {
136            Some(Atom {
137                slot: Token::LengthConst(value, unit),
138                span,
139            }) => Ok(LengthTok { value, unit, span }),
140            Some(atom) => {
141                let span = atom.span;
142                stream.push(atom);
143                Err(ParseError::expected(span, "a length constant"))
144            }
145            None => Err(ParseError::eof(Span::default())),
146        }
147    }
148}
149
150impl Unparse<Atom> for LengthTok {
151    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
152        sink.write_one(Atom {
153            slot: Token::LengthConst(self.value, self.unit.clone()),
154            span: self.span,
155        })
156    }
157}
158
159impl Spanned for LengthTok {
160    type Span = Span;
161    fn span(&self) -> Span {
162        self.span
163    }
164}
165
166/// A backtick string literal with its space-trimming flags.
167#[derive(Clone, Debug, PartialEq)]
168pub struct LiteralTok {
169    pub body: String,
170    pub omit_pre: bool,
171    pub omit_post: bool,
172    pub span: Span,
173}
174
175impl Parse<Atom> for LiteralTok {
176    type Error = ParseError<Span>;
177
178    fn parse_stream<S: ParseStream<Atom = Atom>>(
179            stream: &mut S,
180        ) -> Result<Self, Self::Error> {
181        match stream.next() {
182            Some(Atom {
183                slot:
184                    Token::Literal {
185                        body,
186                        omit_pre,
187                        omit_post,
188                    },
189                span,
190            }) => Ok(LiteralTok {
191                body,
192                omit_pre,
193                omit_post,
194                span,
195            }),
196            Some(atom) => {
197                let span = atom.span;
198                stream.push(atom);
199                Err(ParseError::expected(span, "a string literal"))
200            }
201            None => Err(ParseError::eof(Span::default())),
202        }
203    }
204}
205
206impl Unparse<Atom> for LiteralTok {
207    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
208        sink.write_one(Atom {
209            slot: Token::Literal {
210                body: self.body.clone(),
211                omit_pre: self.omit_pre,
212                omit_post: self.omit_post,
213            },
214            span: self.span,
215        })
216    }
217}
218
219impl Spanned for LiteralTok {
220    type Span = Span;
221    fn span(&self) -> Span {
222        self.span
223    }
224}
225
226/// A standalone `*` (`EXACT_TIMES` in `parser.mly`) in *type*-expression
227/// position — the product-type separator (`type point = length * length`,
228/// `cst.rs`'s `TypeProd`). Hand-written (not `#[leaf(...)]`-generated on
229/// `Token::ExactTimes` in `token.rs`) because `Token::ExactTimes` already
230/// doubles as one of `BinOpTok`'s matched variants for the *expression*-level
231/// `*`; this leaf lets `TypeProd` consume the same token without pulling in
232/// the rest of the binop set, mirroring `LengthTok`'s boilerplate.
233#[derive(Clone, Debug, PartialEq)]
234pub struct ExactTimesTok {
235    pub span: Span,
236}
237
238impl Parse<Atom> for ExactTimesTok {
239    type Error = ParseError<Span>;
240
241    fn parse_stream<S: ParseStream<Atom = Atom>>(
242            stream: &mut S,
243        ) -> Result<Self, Self::Error> {
244        match stream.next() {
245            Some(Atom {
246                slot: Token::ExactTimes,
247                span,
248            }) => Ok(ExactTimesTok { span }),
249            Some(atom) => {
250                let span = atom.span;
251                stream.push(atom);
252                Err(ParseError::expected(span, "'*'"))
253            }
254            None => Err(ParseError::eof(Span::default())),
255        }
256    }
257}
258
259impl Unparse<Atom> for ExactTimesTok {
260    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
261        sink.write_one(Atom {
262            slot: Token::ExactTimes,
263            span: self.span,
264        })
265    }
266}
267
268impl Spanned for ExactTimesTok {
269    type Span = Span;
270    fn span(&self) -> Span {
271        self.span
272    }
273}
274
275/// A binary-operator token: the `binop` family of `nxlor`..`nxrtimes` plus the
276/// `mod`/`::` operators that also act as binops (`parser.mly`'s `binop`
277/// nonterminal, minus `UNOP_EXCLAM`/`BEFORE`/`LNOT`, which are not simple
278/// infix operators in this grammar's flattened operator-chain shape).
279/// Deliberately excludes `Token::Bar` (match-arm separator) and
280/// `Token::ExactAmp` (the `&`-prefixed "next" unary operator) — see
281/// `cst.rs`'s note on `Bar` handling.
282#[derive(Clone, Debug, PartialEq)]
283pub struct BinOpTok {
284    pub tok: Token,
285    pub span: Span,
286}
287
288impl Parse<Atom> for BinOpTok {
289    type Error = ParseError<Span>;
290
291    fn parse_stream<S: ParseStream<Atom = Atom>>(
292            stream: &mut S,
293        ) -> Result<Self, Self::Error> {
294        match stream.next() {
295            Some(Atom { slot, span })
296                if matches!(
297                    slot,
298                    Token::BinopPlus(_)
299                        | Token::BinopMinus(_)
300                        | Token::BinopTimes(_)
301                        | Token::BinopDivides(_)
302                        | Token::BinopEq(_)
303                        | Token::BinopLt(_)
304                        | Token::BinopGt(_)
305                        | Token::BinopAmp(_)
306                        | Token::BinopBar(_)
307                        | Token::BinopHat(_)
308                        | Token::ExactMinus
309                        | Token::ExactTimes
310                        | Token::Mod
311                        | Token::Cons
312                ) =>
313            {
314                Ok(BinOpTok { tok: slot, span })
315            }
316            Some(atom) => {
317                let span = atom.span;
318                stream.push(atom);
319                Err(ParseError::expected(span, "a binary operator"))
320            }
321            None => Err(ParseError::eof(Span::default())),
322        }
323    }
324}
325
326impl Unparse<Atom> for BinOpTok {
327    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
328        sink.write_one(Atom {
329            slot: self.tok.clone(),
330            span: self.span,
331        })
332    }
333}
334
335impl Spanned for BinOpTok {
336    type Span = Span;
337    fn span(&self) -> Span {
338        self.span
339    }
340}
341
342impl BinOpTok {
343    /// The operator's source text, e.g. `"+"`, `"mod"`, `"::"`.
344    pub fn op_text(&self) -> String {
345        match &self.tok {
346            Token::BinopPlus(s)
347            | Token::BinopMinus(s)
348            | Token::BinopTimes(s)
349            | Token::BinopDivides(s)
350            | Token::BinopEq(s)
351            | Token::BinopLt(s)
352            | Token::BinopGt(s)
353            | Token::BinopAmp(s)
354            | Token::BinopBar(s)
355            | Token::BinopHat(s) => s.clone(),
356            Token::ExactMinus => "-".to_string(),
357            Token::ExactTimes => "*".to_string(),
358            Token::Mod => "mod".to_string(),
359            Token::Cons => "::".to_string(),
360            _ => unreachable!("BinOpTok only ever holds one of the matched variants"),
361        }
362    }
363}
364
365/// The operator token accepted inside a `( ‹op› )` NAMING form (see
366/// [`OpNameTok`]) — [`BinOpTok`]'s whole alternative set, PLUS
367/// `!`/`before`. Upstream `parser.mly`'s `binop` nonterminal is used in
368/// exactly two productions, both naming forms (`VAL LPAREN binop RPAREN`
369/// and `LPAREN binop RPAREN` as a bare atomic-expression reference) —
370/// *never* for infix chaining — and it accepts `UNOP_EXCLAM`/`BEFORE`/
371/// `LNOT` there, unlike this grammar's flattened infix operator chain
372/// (`cst.rs`'s `OpRhs`, which uses [`BinOpTok`] directly and correctly
373/// excludes them: `!`/`before` are not simple infix operators here).
374/// `LNOT` (`not`) has no reserved keyword token in this port at all (see the
375/// `not` note beside [`crate::token::Token::Mod`]): it lexes as an ordinary
376/// `VarTok`, so `let not x = ..`/`val not : ty` (unparenthesized) already
377/// parse via the plain-name arm of `cst.rs`'s `BindName`/`ast::Atomic::
378/// OpRef`'s sibling `Var` arm, with no token to add here. Kept as its own
379/// struct (mirroring
380/// [`BinOpTok`]'s shape) rather than widening `BinOpTok` itself, so the
381/// infix-chain grammar's exclusion stays intact by construction — nothing
382/// downstream of [`BinOpTok::parse`] can ever see `!`/`before`.
383#[derive(Clone, Debug, PartialEq)]
384pub struct NamingOpTok {
385    pub tok: Token,
386    pub span: Span,
387}
388
389impl Parse<Atom> for NamingOpTok {
390    type Error = ParseError<Span>;
391
392    fn parse_stream<S: ParseStream<Atom = Atom>>(
393            stream: &mut S,
394        ) -> Result<Self, Self::Error> {
395        match stream.next() {
396            Some(Atom { slot, span })
397                if matches!(
398                    slot,
399                    Token::BinopPlus(_)
400                        | Token::BinopMinus(_)
401                        | Token::BinopTimes(_)
402                        | Token::BinopDivides(_)
403                        | Token::BinopEq(_)
404                        | Token::BinopLt(_)
405                        | Token::BinopGt(_)
406                        | Token::BinopAmp(_)
407                        | Token::BinopBar(_)
408                        | Token::BinopHat(_)
409                        | Token::ExactMinus
410                        | Token::ExactTimes
411                        | Token::Mod
412                        | Token::Cons
413                        | Token::UnopExclam(_)
414                        | Token::Before
415                ) =>
416            {
417                Ok(NamingOpTok { tok: slot, span })
418            }
419            Some(atom) => {
420                let span = atom.span;
421                stream.push(atom);
422                Err(ParseError::expected(
423                    span,
424                    "a binary operator, '!', or 'before'",
425                ))
426            }
427            None => Err(ParseError::eof(Span::default())),
428        }
429    }
430}
431
432impl Unparse<Atom> for NamingOpTok {
433    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
434        sink.write_one(Atom {
435            slot: self.tok.clone(),
436            span: self.span,
437        })
438    }
439}
440
441impl Spanned for NamingOpTok {
442    type Span = Span;
443    fn span(&self) -> Span {
444        self.span
445    }
446}
447
448impl NamingOpTok {
449    /// The operator's source text, e.g. `"+"`, `"mod"`, `"::"`, `"!"`,
450    /// `"before"` — [`BinOpTok::op_text`] plus the two naming-only
451    /// additions.
452    pub fn op_text(&self) -> String {
453        match &self.tok {
454            Token::BinopPlus(s)
455            | Token::BinopMinus(s)
456            | Token::BinopTimes(s)
457            | Token::BinopDivides(s)
458            | Token::BinopEq(s)
459            | Token::BinopLt(s)
460            | Token::BinopGt(s)
461            | Token::BinopAmp(s)
462            | Token::BinopBar(s)
463            | Token::BinopHat(s)
464            | Token::UnopExclam(s) => s.clone(),
465            Token::ExactMinus => "-".to_string(),
466            Token::ExactTimes => "*".to_string(),
467            Token::Mod => "mod".to_string(),
468            Token::Cons => "::".to_string(),
469            Token::Before => "before".to_string(),
470            _ => unreachable!("NamingOpTok only ever holds one of the matched variants"),
471        }
472    }
473}
474
475/// `( ‹op› )` — a parenthesized (possibly user-defined) operator name.
476/// Two surface uses share this leaf: a binding-position NAME (`cst.rs`'s
477/// `BindName`, e.g. `let (+++>) = ..` / `val (-->) : ty` — the gap blocking
478/// `itemize.satyh`/`progsynt.satyh`) and a bare atomic-expression reference
479/// to an operator as a first-class value (`cst.rs`'s `ast::Atomic::OpRef`,
480/// e.g. `(+++)`). `.name` is the operator's text (`NamingOpTok::op_text`);
481/// `.span` covers the whole `( .. )`, delimiters included. Both are
482/// precomputed here (rather than left to the `Spanned` trait) so a
483/// downstream crate with no direct `syan` dependency (`rustyfi-lang`) can
484/// read them as plain fields, exactly like every other leaf's `.name`/
485/// `.span`. Hand-written (not `#[leaf(...)]`-generated) for the same reason
486/// as [`BinOpTok`]: it spans three atoms, not one. The inner operator is a
487/// [`NamingOpTok`] (not a bare [`BinOpTok`]) so `(!)`/`(before)` — valid
488/// only in this naming position, per upstream's `binop` nonterminal — also
489/// parse; see [`NamingOpTok`]'s doc comment.
490#[derive(Clone, Debug, PartialEq)]
491pub struct OpNameTok {
492    pub name: String,
493    pub span: Span,
494    pub lparen: LParenTok,
495    pub op: NamingOpTok,
496    pub rparen: RParenTok,
497}
498
499impl Parse<Atom> for OpNameTok {
500    type Error = ParseError<Span>;
501
502    fn parse_stream<S: ParseStream<Atom = Atom>>(
503            stream: &mut S,
504        ) -> Result<Self, Self::Error> {
505        let lparen = LParenTok::parse_stream(&mut *stream)?;
506        let op = NamingOpTok::parse_stream(&mut *stream)?;
507        let rparen = RParenTok::parse_stream(&mut *stream)?;
508        let name = op.op_text();
509        let span = lparen.span().unite(rparen.span());
510        Ok(OpNameTok {
511            name,
512            span,
513            lparen,
514            op,
515            rparen,
516        })
517    }
518}
519
520impl Unparse<Atom> for OpNameTok {
521    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
522        self.lparen.unparse(sink)?;
523        self.op.unparse(sink)?;
524        self.rparen.unparse(sink)
525    }
526}
527
528impl Spanned for OpNameTok {
529    type Span = Span;
530    fn span(&self) -> Span {
531        self.span
532    }
533}
534
535/// `@stage: persistent` / `@stage: 0` / `@stage: 1` header token
536/// (`cst.rs`'s `Header::Stage`). Hand-written like [`BinOpTok`] above: the
537/// three spellings are separate unit `Token` variants with no shared
538/// payload for a `#[leaf(...)]` derive to key off, so this matches any of
539/// them and keeps whichever one matched (needed for a lossless round-trip —
540/// `Unparse` replays exactly the token that was read).
541#[derive(Clone, Debug, PartialEq)]
542pub struct HeaderStageTok {
543    pub tok: Token,
544    pub span: Span,
545}
546
547impl Parse<Atom> for HeaderStageTok {
548    type Error = ParseError<Span>;
549
550    fn parse_stream<S: ParseStream<Atom = Atom>>(
551            stream: &mut S,
552        ) -> Result<Self, Self::Error> {
553        match stream.next() {
554            Some(Atom { slot, span })
555                if matches!(
556                    slot,
557                    Token::HeaderStage0 | Token::HeaderStage1 | Token::HeaderPersistent0
558                ) =>
559            {
560                Ok(HeaderStageTok { tok: slot, span })
561            }
562            Some(atom) => {
563                let span = atom.span;
564                stream.push(atom);
565                Err(ParseError::expected(span, "'@stage:'"))
566            }
567            None => Err(ParseError::eof(Span::default())),
568        }
569    }
570}
571
572impl Unparse<Atom> for HeaderStageTok {
573    fn unparse<S: Emitter<Atom>>(&self, sink: &mut S) -> Result<(), S::Error> {
574        sink.write_one(Atom {
575            slot: self.tok.clone(),
576            span: self.span,
577        })
578    }
579}
580
581impl Spanned for HeaderStageTok {
582    type Span = Span;
583    fn span(&self) -> Span {
584        self.span
585    }
586}
587
588// ---- delimiter groups ---------------------------------------------------------
589
590// Atom-generic groups: `syan::nested::group::Group<T, Open, Close>` supplies
591// `Parse`/`Unparse` (flat-family blankets over the leaf open/close atoms),
592// `Spanned` (delimiter-only, so empty groups still have a span), `Deref` to the
593// slot, and `EmptyGroup for Group<(), _, _>`. The field names the grammar reads
594// are `.open`/`.slot`/`.close`.
595
596/// `( … )` in program mode.
597pub type ParenGroup<T> = syan::nested::group::Group<T, LParenTok, RParenTok>;
598/// `(| … |)` record.
599pub type RecordGroup<T> = syan::nested::group::Group<T, BRecordTok, ERecordTok>;
600/// `[ … ]` list.
601pub type ListGroup<T> = syan::nested::group::Group<T, BListTok, EListTok>;
602/// `{ … }` inline text.
603pub type InlineGroup<T> = syan::nested::group::Group<T, BHorzGrpTok, EHorzGrpTok>;
604/// `'< … >` / `< … >` block text.
605pub type BlockGroup<T> = syan::nested::group::Group<T, BVertGrpTok, EVertGrpTok>;
606/// `${ … }` / `{ … }` math (the latter when already inside math mode).
607pub type MathGroup<T> = syan::nested::group::Group<T, BMathGrpTok, EMathGrpTok>;
608/// `Mod.( … )` — the open-module-scope expression (`cst.rs`'s
609/// `Atomic::OpenModule`). The open delimiter (`OpenModuleTok`, carrying the
610/// module name) is `Mod.(`; the close is a plain `)` (`RParenTok`, exactly
611/// like [`ParenGroup`]'s).
612pub type OpenModuleGroup<T> = syan::nested::group::Group<T, OpenModuleTok, RParenTok>;
613
614/// `Unparse` for an EMPTY group used as an ordinary field rather than as the
615/// target of a `#[group(self.x)]` — `PatBot::Unit { paren: ParenGroup<()> }`
616/// and its three siblings, which spell `()` with nothing inside.
617///
618/// syan core has a generic `Parse` for `Group<T, O, C>` but no generic
619/// `Unparse`: the only `Unparse for Group<..>` impls there are the
620/// `proc_macro2` ones, where a delimited group is a single `TokenTree` rather
621/// than three atoms. A `#[group(..)]` holder never needs it (it is emitted
622/// through `GroupUnparse::unparse_group`), so the gap only shows up for a
623/// holder standing alone as a field, where `#[derive(Unparse)]` synthesizes an
624/// ordinary `FieldTy: Unparse<Atom>` predicate.
625///
626/// Hence a local node rather than an `impl Unparse<Atom> for ParenGroup<()>`:
627/// `Atom` is `syan::span::WithSpan<Token, Span>`, an alias for a FOREIGN type,
628/// so nothing in that impl would be local and the orphan rule rejects it
629/// (E0117). The fields keep `Group`'s `open`/`close` names, the derived
630/// `Parse` is the same two-token sequence `Group`'s generic `Parse` produces,
631/// and the delimiters emit in source order, so `parse . unparse` still
632/// round-trips.
633#[derive(Parse, Unparse, Debug, Clone, PartialEq)]
634pub struct UnitParen {
635    pub open: LParenTok,
636    pub close: RParenTok,
637}
638
639impl Spanned for UnitParen {
640    type Span = Span;
641    fn span(&self) -> Span {
642        self.open.span().unite(self.close.span())
643    }
644}
645
646// ---- PartialEq bridge for the generated leaves --------------------------------
647
648// `token_leaves!` emits only `Clone` + `Debug` per leaf, but the surface
649// grammar (`cst.rs`) derives `PartialEq` on the nodes that embed these leaves
650// (and the `Group` aliases need their delimiter leaves `PartialEq` too), so
651// every generated leaf must also be `PartialEq` — span included.
652macro_rules! leaf_eq {
653    (
654        span_only: $($unit:ident),* $(,)?;
655        with_fields: $($payload:ident { $($field:ident),* }),* $(,)?
656    ) => {
657        $(
658            impl PartialEq for $unit {
659                fn eq(&self, other: &Self) -> bool {
660                    self.0 == other.0
661                }
662            }
663        )*
664        $(
665            impl PartialEq for $payload {
666                fn eq(&self, other: &Self) -> bool {
667                    $(self.$field == other.$field &&)* self.span == other.span
668                }
669            }
670        )*
671    };
672}
673
674leaf_eq! {
675    span_only:
676        KwLet, KwLetRec, KwLetHorz, KwLetVert, KwLetMath, KwAnd, KwIn, KwFun,
677        KwIf, KwThen, KwElse, KwTrue, KwFalse, ArrowTok, DefEqTok, ListPunctTok,
678        CommaTok, LParenTok, RParenTok, BRecordTok, ERecordTok, BListTok, EListTok,
679        BHorzGrpTok, EHorzGrpTok, BVertGrpTok, EVertGrpTok, SpaceTok, BreakTok,
680        EndActiveTok, EoiTok, KwMatch, KwWith, KwWhen, KwAs, KwType, KwOf, BarTok,
681        WildcardTok, ConsTok, ColonTok, ExactMinusTok, KwLetMutable, KwWhile, KwDo,
682        KwBefore, OverwriteEqTok, AccessTok, KwModule, KwStruct, KwSig, KwEnd,
683        KwOpen, KwVal, KwDirect, OptionalTok, OmissionTok, SuperscriptTok,
684        SubscriptTok, SepTok, BMathGrpTok, EMathGrpTok, HorzCmdTypeTok,
685        VertCmdTypeTok, MathCmdTypeTok, OptionalTypeTok, OptionalArrowTok,
686        ConstraintTok, CommandTok, KwRec, KwInline, KwBlock, KwMutable,
687        CoerceTok, KwSignature, KwInclude, KwUse, KwPackage, KwMath,
688        KwPersistent, ExactAmpTok, ExactTildeTok;
689    with_fields:
690        VarTok { name }, CtorTok { name }, IntTok { value }, FloatTok { value },
691        HorzCmdTok { name }, VertCmdTok { name }, CharTok { text }, ItemTok { depth },
692        CodeTextTok { text },
693        HeaderRequireTok { content }, HeaderImportTok { content }, TypeVarTok { name },
694        UnopExclamTok { text }, MathCharTok { text }, MathCmdTok { name },
695        PrimesTok { count }, OpenModuleTok { name }, RowVarTok { name }
696}