Skip to main content

praxis_input_parser/
ast.rs

1//! The typed AST of the input-parser DSL (§7.9).
2//!
3//! The DSL has its **own** typed AST — the design (§7.9) forbids lowering parser
4//! expressions directly into string-splitting calls. The ordinary language
5//! parser produces rowan nodes; `praxis-hir` converts those into this `ParserAst`
6//! before validation, type synthesis, and plan construction.
7//!
8//! The node set is §7.4's ten atomics ([`AtomicKind`]), §7.5's fourteen
9//! structural constructors ([`Constructor`]), and backtick templates with
10//! `{name:parser}` / `{parser}` captures ([`TemplatePart`]).
11
12use praxis_source::Span;
13
14/// One of the atomic parsers (§7.4).
15///
16/// **§7.4's list is closed, and this is all ten of it.** A name the design
17/// document requires but this enum omits gets "unknown atomic parser".
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum AtomicKind {
20    /// Signed decimal integer → `Int`.
21    Int,
22    /// Non-negative decimal integer → `Int`.
23    ///
24    /// **`Int`, not `ScalarType::UInt`.** `UInt` is reserved and has no runtime
25    /// object at all: `praxis-repr` answers `NoRuntimeRepr` for it, and a JIT
26    /// compile fails when a descriptor is missing — so a `uint` capture typed
27    /// `UInt` would make every program containing one fail to compile. The
28    /// non-negativity is enforced by the *parse rule* (a leading `-` is
29    /// refused), which is what §7.4 asks for.
30    UInt,
31    /// Decimal floating-point number → `Float`.
32    Float,
33    /// A decimal integer in `0..=255` → `Byte`.
34    ///
35    /// A decimal integer and not a raw input byte: a raw byte cannot be
36    /// re-sliced as `Text` without breaking the UTF-8 invariant every
37    /// source-slice `Text` relies on.
38    Byte,
39    /// One Unicode scalar value → `Char`.
40    Char,
41    /// One decimal digit → `Int`.
42    Digit,
43    /// Non-empty run excluding whitespace and parser-delimiter punctuation → `Text`.
44    Word,
45    /// An identifier run → `Text`.
46    ///
47    /// §7.4 says "ASCII-like identifier syntax by default"; this uses §4.1's
48    /// **one** identifier class (`praxis_syntax::ident`), which is a deliberate
49    /// widening. A parser that accepted a narrower set of names than the
50    /// language itself does would refuse identifiers a Praxis program can
51    /// declare, and the workspace keeps exactly one copy of that rule.
52    Identifier,
53    /// Minimally consumes text until the following template literal can match → `Text`.
54    Text,
55    /// The remainder of the current region → `Text`.
56    Rest,
57}
58
59impl AtomicKind {
60    /// The source keyword for this atomic.
61    ///
62    /// The **only** place these ten strings are spelled. Completion labels,
63    /// hover, and the runtime's `expected …` parse-fault names all read them
64    /// from here, so a second copy cannot drift into naming two atomics the
65    /// same.
66    pub fn keyword(self) -> &'static str {
67        match self {
68            AtomicKind::Int => "int",
69            AtomicKind::UInt => "uint",
70            AtomicKind::Float => "float",
71            AtomicKind::Byte => "byte",
72            AtomicKind::Char => "char",
73            AtomicKind::Digit => "digit",
74            AtomicKind::Word => "word",
75            AtomicKind::Identifier => "identifier",
76            AtomicKind::Text => "text",
77            AtomicKind::Rest => "rest",
78        }
79    }
80
81    /// One line per atomic, for hover. Exhaustive, for the reason
82    /// [`Constructor::doc`] is.
83    pub fn doc(self) -> &'static str {
84        match self {
85            AtomicKind::Int => {
86                "Signed decimal integer. Surrounding horizontal space is the \
87                 caller's, not the atomic's."
88            }
89            AtomicKind::UInt => "Non-negative decimal integer; a leading `-` is refused.",
90            AtomicKind::Float => "Decimal floating-point number.",
91            AtomicKind::Byte => "A decimal integer in `0..=255` — a number, not a raw input byte.",
92            AtomicKind::Char => "One Unicode scalar value, whitespace included where offered.",
93            AtomicKind::Digit => "One decimal digit.",
94            AtomicKind::Word => {
95                "A non-empty run excluding whitespace and parser-delimiter punctuation."
96            }
97            AtomicKind::Identifier => "An identifier, by the language's own identifier rule.",
98            AtomicKind::Text => {
99                "Consumes as little as possible until the literal run that \
100                 follows can match."
101            }
102            AtomicKind::Rest => "The remainder of the current region.",
103        }
104    }
105
106    /// Parse an atomic name into its kind, or `None` if unknown.
107    pub fn from_keyword(name: &str) -> Option<Self> {
108        Some(match name {
109            "int" => AtomicKind::Int,
110            "uint" => AtomicKind::UInt,
111            "float" => AtomicKind::Float,
112            "byte" => AtomicKind::Byte,
113            "char" => AtomicKind::Char,
114            "digit" => AtomicKind::Digit,
115            "word" => AtomicKind::Word,
116            "identifier" => AtomicKind::Identifier,
117            "text" => AtomicKind::Text,
118            "rest" => AtomicKind::Rest,
119            _ => return None,
120        })
121    }
122
123    /// Every atomic, in §7.4's order. The list is **closed**: a test sweeps it,
124    /// so a new atomic cannot be added without a type and a runtime rule.
125    pub const ALL: &'static [AtomicKind] = &[
126        AtomicKind::Int,
127        AtomicKind::UInt,
128        AtomicKind::Float,
129        AtomicKind::Byte,
130        AtomicKind::Char,
131        AtomicKind::Digit,
132        AtomicKind::Word,
133        AtomicKind::Identifier,
134        AtomicKind::Text,
135        AtomicKind::Rest,
136    ];
137}
138
139/// How a template literal run of whitespace matches input (§7.2).
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum WsPolicy {
142    /// The literal had no whitespace run in front of it, so no whitespace is
143    /// consumed before matching it.
144    ///
145    /// This variant is what lets [`SpaceRun`](Self::SpaceRun) mean what it
146    /// says. Without it every literal would be tagged `SpaceRun` whether or not
147    /// the template wrote a space run, the interpreter could not tell the two
148    /// apart, and the only way to keep templates matching would be to implement
149    /// `SpaceRun` as zero-or-more — contradicting its own definition.
150    None,
151    /// A run of ordinary spaces matches one or more spaces or tabs (the default,
152    /// flexible rule for AoC column alignment, §7.2).
153    SpaceRun,
154    /// `\s*` — zero or more spaces or tabs.
155    ZeroOrMore,
156    /// `\s+` — one or more spaces or tabs.
157    OneOrMore,
158    /// `\x20` — exactly one ASCII space.
159    ExactSpace,
160    /// `\n` — one line ending.
161    Newline,
162    /// `\t` — one tab.
163    Tab,
164}
165
166/// The name of a template capture — **an identifier by construction**.
167///
168/// §4.1 allows Unicode identifiers and the workspace has one character class
169/// for them, `praxis_syntax::ident::is_ident`, which is the predicate here. A
170/// private ASCII copy of the rule would leave `{λ:int}` unrecognized as a
171/// *named* capture, silently reinterpreting the whole body `λ:int` as the
172/// parser expression: a name a consumer cannot accept must be reported, never
173/// rewritten into a different name.
174#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
175pub struct CaptureName(Box<str>);
176
177/// The one way [`CaptureName::parse`] fails: the text is not an identifier.
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct InvalidCaptureName;
180
181impl CaptureName {
182    /// The **only** constructor.
183    ///
184    /// # Errors
185    /// [`InvalidCaptureName`] when `text` is not a §4.1 identifier.
186    pub fn parse(text: &str) -> Result<Self, InvalidCaptureName> {
187        if praxis_syntax::ident::is_ident(text) {
188            Ok(CaptureName(text.into()))
189        } else {
190            Err(InvalidCaptureName)
191        }
192    }
193
194    /// The name text.
195    #[must_use]
196    pub fn as_str(&self) -> &str {
197        &self.0
198    }
199}
200
201impl std::fmt::Display for CaptureName {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.write_str(&self.0)
204    }
205}
206
207/// One part of a backtick template (§7.9 `TemplatePart`).
208///
209/// **Every part carries its own source span**, for the same reason every
210/// [`ParserAst`] node does (§7.10, ADR-078): the editor has to colour the
211/// capture *name* differently from the capture *type* (§19.11 criterion 4), and
212/// the only alternative to recording the extent where the scanner already knows
213/// it is a second scanner in the language server re-deriving it — the failure
214/// ADR-098 exists to prevent. Spans are interior-relative until
215/// [`shift_part_spans`] rebases them onto the file, exactly like the spans on
216/// the parser nodes underneath.
217#[derive(Clone, Debug)]
218pub enum TemplatePart {
219    /// A literal run: the raw matched bytes plus the whitespace policy.
220    ///
221    /// `span` covers the **source** the run was decoded from, which is not the
222    /// same length as `text`: `` \` `` is two source bytes and one character,
223    /// and a policy part decoded from `\s+` has an empty `text` and a two-byte
224    /// span.
225    Literal {
226        text: String,
227        ws: WsPolicy,
228        span: Span,
229    },
230    /// A capture `{name? : parser}`. `name` is `None` for anonymous captures.
231    ///
232    /// `parser` is the capture's **own** parser expression, parsed from its own
233    /// body (ADR-072), so captures in one template do not share a kind.
234    ///
235    /// `span` covers the whole capture including both braces; `name_span`
236    /// covers the name **as trimmed** (`{ n :int}` names `n`, not `" n "`), and
237    /// is `None` exactly when `name` is. The capture's *type* needs no span
238    /// here: it is `parser.span()`.
239    Capture {
240        name: Option<CaptureName>,
241        parser: Box<ParserAst>,
242        span: Span,
243        name_span: Option<Span>,
244    },
245}
246
247impl TemplatePart {
248    /// The part's source span.
249    #[must_use]
250    pub fn span(&self) -> Span {
251        match self {
252            TemplatePart::Literal { span, .. } | TemplatePart::Capture { span, .. } => *span,
253        }
254    }
255}
256
257/// A parser expression (§7.9 `ParserExpr`).
258#[derive(Clone, Debug)]
259pub enum ParserAst {
260    /// An atomic parser: `int`, `char`, etc.
261    Atomic { kind: AtomicKind, span: Span },
262    /// A backtick template: `` `{x:int},{y:int}` ``.
263    Template {
264        parts: Vec<TemplatePart>,
265        span: Span,
266    },
267    /// `lines(P)` → `Vec[result(P)]`.
268    Lines { child: Box<ParserAst>, span: Span },
269    /// `sections(P)` → `Vec[result(P)]` (homogeneous; the named form is
270    /// [`SectionsNamed`](Self::SectionsNamed)).
271    Sections { child: Box<ParserAst>, span: Span },
272    /// Named heterogeneous `sections(name: P, ..., tail: repeated(P))` (§7.5).
273    /// Result is an anonymous record with one field per named argument, in
274    /// source order.
275    ///
276    /// A named argument takes one of three forms, and the split between
277    /// `fields` and `repeated_tail` is what keeps the third one's rule
278    /// structural rather than remembered:
279    ///
280    /// - `name: P` — one section, one field of `result(P)`
281    ///   ([`SectionItem::One`]);
282    /// - `name: repeated(P, N)` — exactly `N` consecutive sections, one field
283    ///   of `Vec[result(P)]` ([`SectionItem::Counted`]). It is **bounded**, so
284    ///   it may sit anywhere among the named arguments and other fields may
285    ///   follow it;
286    /// - `name: repeated(P)` — every section that is left, one field of
287    ///   `Vec[result(P)]`. It is greedy, so nothing can follow it: there is at
288    ///   most one and it is last, which is `repeated_tail` being a single
289    ///   `Option` outside the list rather than a variant inside it.
290    SectionsNamed {
291        /// The named arguments other than the unbounded tail, in source order.
292        /// Each contributes exactly one record field and consumes
293        /// [`SectionItem::sections_wanted`] sections.
294        fields: Vec<SectionItem>,
295        /// The unbounded `repeated(P)` tail, if present. The name (e.g.
296        /// `"boards"`) becomes the record's last field; the parser consumes
297        /// every remaining section into a `Vec[result(P)]`.
298        repeated_tail: Option<(String, Box<ParserAst>)>,
299        span: Span,
300    },
301    /// `csv(P)` → `Vec[result(P)]`.
302    Csv { child: Box<ParserAst>, span: Span },
303    /// `ws(P)` → `Vec[result(P)]` (whitespace-separated).
304    Ws { child: Box<ParserAst>, span: Span },
305    /// `sep(separator, P)` → `Vec[result(P)]`. The separator is a
306    /// [`Separator`], which cannot be empty.
307    Sep {
308        separator: Separator,
309        child: Box<ParserAst>,
310        span: Span,
311    },
312    /// `grid(P)` → `Grid[result(P)]`.
313    Grid { child: Box<ParserAst>, span: Span },
314    /// `block(item, ...)` (§7.5): apply sequential parsers within one
315    /// region. A positional item that is a named-capture template *flattens*
316    /// its captures into the block's record; a positional scalar must be
317    /// named (else rejected). A named item contributes one field. Result is a
318    /// flattened anonymous record.
319    Block { items: Vec<BlockItem>, span: Span },
320    /// `choice(Name: P, Name: P, ...)` (§7.5): parse one of several
321    /// alternatives, generating an anonymous enum. Each case's parser produces
322    /// the payload (a record for a named-capture template, a scalar otherwise).
323    /// The first alternative that matches wins (source order).
324    Choice {
325        cases: Vec<(String, ParserAst)>,
326        span: Span,
327    },
328    /// `optional(P)` (§7.5): parse `P` if it matches, else consume nothing
329    /// and return `None`. Result is `Option[result(P)]`. Failure consumes no
330    /// input (parser-level optionality, not exception recovery).
331    Optional { child: Box<ParserAst>, span: Span },
332    /// `scan(P)` (§7.5): find repeated `P` matches inside otherwise
333    /// irrelevant text (e.g. corrupted AoC input). Returns matches in source
334    /// order as `Vec[result(P)]`, ignoring unmatched text.
335    Scan { child: Box<ParserAst>, span: Span },
336    /// `one_of("LR")` (§7.5): match one character from a literal character
337    /// set. Result is `Char`.
338    OneOf { chars: String, span: Span },
339    /// `chars(P, skip:)` (§7.5): apply a char-parser repeatedly. Result is
340    /// `Vec[result(P)]` — **not** `Vec[Char]` whatever `P` is, since the
341    /// runtime stores what `P` produced: `chars(int, skip: none)` is a
342    /// `Vec[Int]`, and `chars(one_of("LR"))` is a `Vec[Char]` because `one_of`
343    /// is `Char`. The `skip` policy trims between matches; see [`SkipPolicy`],
344    /// and note that `newlines` is the *broader* of the two non-`none`
345    /// policies.
346    Characters {
347        child: Box<ParserAst>,
348        skip: SkipPolicy,
349        span: Span,
350    },
351    /// `matrix(P)` (§7.5, ADR-030): parse lines of whitespace-separated
352    /// tokens into a rectangular `Grid[result(P)]`. Same result type as `grid`
353    /// but tokenizes on whitespace rather than per-character.
354    Matrix { child: Box<ParserAst>, span: Span },
355    /// Ragged `grid(P, ragged, fill:)` (§7.5): permit uneven rows and pad
356    /// to the maximum width with `fill`. The plain `grid(P)` keeps its own arm.
357    GridRagged {
358        child: Box<ParserAst>,
359        /// The fill character/value, as the literal text from source (parsed by
360        /// the cell parser at runtime).
361        fill: String,
362        span: Span,
363    },
364}
365
366/// The separator of a `sep(separator, P)` call — **non-empty by construction**.
367///
368/// An empty separator is not a parser that matches nothing: it is a cursor that
369/// never advances. `walk_sep` in `praxis-runtime` asks
370/// `region[pos..].starts_with(sep_bytes)`, which is *unconditionally true* for
371/// an empty needle, so `pos += sep_bytes.len()` is `pos += 0` and the loop
372/// pushes a freshly allocated value forever — an infinite loop that also grows
373/// the heap without bound.
374///
375/// A `validate` check would have caught the value only where someone remembered
376/// to call it; the type catches it at every construction site there will ever
377/// be, which is the house maxim (`AGENTS.md`: make illegal states
378/// unrepresentable).
379#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
380pub struct Separator(Box<str>);
381
382/// The one way [`Separator::new`] fails: the text was empty.
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384pub struct EmptySeparator;
385
386impl std::fmt::Display for EmptySeparator {
387    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388        f.write_str("a `sep` separator may not be empty: it could never advance")
389    }
390}
391
392impl std::error::Error for EmptySeparator {}
393
394impl Separator {
395    /// The **only** constructor. Refuses the empty string.
396    ///
397    /// # Errors
398    /// [`EmptySeparator`] when `text` is empty.
399    pub fn new(text: &str) -> Result<Self, EmptySeparator> {
400        if text.is_empty() {
401            return Err(EmptySeparator);
402        }
403        Ok(Separator(text.into()))
404    }
405
406    /// The separator text. Never empty.
407    #[must_use]
408    pub fn as_str(&self) -> &str {
409        &self.0
410    }
411}
412
413impl std::fmt::Display for Separator {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        f.write_str(&self.0)
416    }
417}
418
419/// The `N` of a `repeated(P, N)` — **at least one section, by construction**.
420///
421/// A group of no sections parses nothing: `repeated(P, 0)` would produce an
422/// empty `Vec` while consuming no input, which is not a parser anybody means to
423/// write and reads as a typo for the unbounded form. A negative count names no
424/// sections at all. Both are the same kind of value [`Separator`] refuses one
425/// field over — a number the runtime would have to invent a meaning for — and
426/// they are refused the same way, by the one constructor, rather than by a
427/// `validate` arm the next construction site can forget.
428///
429/// The upper bound is the plan's: [`crate::plan::SectionItemNode`] stores the
430/// count as a `u32`, because the plan is a flat `&'static` repr the runtime
431/// reads without allocating. A count that does not fit is refused here, where
432/// the source span is still in hand, rather than truncated there.
433#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
434pub struct RepeatCount(std::num::NonZeroU32);
435
436/// The two ways [`RepeatCount::new`] fails.
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub enum InvalidRepeatCount {
439    /// Zero or negative: a group of no sections parses nothing.
440    NotPositive,
441    /// Larger than a `u32`, which is what the plan node holds.
442    TooLarge,
443}
444
445impl std::fmt::Display for InvalidRepeatCount {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447        f.write_str(match self {
448            InvalidRepeatCount::NotPositive => {
449                "a `repeated` count must be at least 1: a group of no sections parses nothing"
450            }
451            InvalidRepeatCount::TooLarge => "a `repeated` count must fit in 32 bits",
452        })
453    }
454}
455
456impl std::error::Error for InvalidRepeatCount {}
457
458impl RepeatCount {
459    /// The **only** constructor. Refuses a count that names no sections.
460    ///
461    /// # Errors
462    /// [`InvalidRepeatCount`] for `n <= 0` or `n > u32::MAX`.
463    pub fn new(n: i64) -> Result<Self, InvalidRepeatCount> {
464        if n <= 0 {
465            return Err(InvalidRepeatCount::NotPositive);
466        }
467        let n = u32::try_from(n).map_err(|_| InvalidRepeatCount::TooLarge)?;
468        // Non-zero by the check above; `NonZeroU32::new` cannot fail here.
469        std::num::NonZeroU32::new(n)
470            .map(RepeatCount)
471            .ok_or(InvalidRepeatCount::NotPositive)
472    }
473
474    /// The count. Never zero.
475    #[must_use]
476    pub fn get(self) -> u32 {
477        self.0.get()
478    }
479}
480
481impl std::fmt::Display for RepeatCount {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        write!(f, "{}", self.0.get())
484    }
485}
486
487/// One named argument of a heterogeneous `sections(...)` other than its
488/// unbounded tail (§7.5).
489///
490/// Each variant contributes exactly one field to the generated record; they
491/// differ in how many sections they consume, which is what
492/// [`sections_wanted`](Self::sections_wanted) answers. The count lives *in the
493/// item* rather than in a parallel position map beside the field list, because
494/// a position recorded twice is a position that can disagree with itself —
495/// which is the drift ADR-073 was written about.
496#[derive(Clone, Debug)]
497pub enum SectionItem {
498    /// `name: P` — one section, one field of `result(P)`.
499    One { name: String, parser: ParserAst },
500    /// `name: repeated(P, N)` — exactly `N` consecutive sections, one field of
501    /// `Vec[result(P)]`.
502    Counted {
503        name: String,
504        count: RepeatCount,
505        parser: ParserAst,
506    },
507}
508
509impl SectionItem {
510    /// The record field this item contributes.
511    #[must_use]
512    pub fn name(&self) -> &str {
513        match self {
514            SectionItem::One { name, .. } | SectionItem::Counted { name, .. } => name,
515        }
516    }
517
518    /// The parser applied to each of this item's sections.
519    #[must_use]
520    pub fn parser(&self) -> &ParserAst {
521        match self {
522            SectionItem::One { parser, .. } | SectionItem::Counted { parser, .. } => parser,
523        }
524    }
525
526    /// The parser, mutably — for [`ParserAst::shift_spans`].
527    pub fn parser_mut(&mut self) -> &mut ParserAst {
528        match self {
529            SectionItem::One { parser, .. } | SectionItem::Counted { parser, .. } => parser,
530        }
531    }
532
533    /// How many sections this item consumes.
534    #[must_use]
535    pub fn sections_wanted(&self) -> usize {
536        match self {
537            SectionItem::One { .. } => 1,
538            SectionItem::Counted { count, .. } => count.get() as usize,
539        }
540    }
541}
542
543/// How `chars(P, skip:)` trims between matches (§7.5).
544///
545/// **Read the two non-`None` variants as an inclusion, because the names do not
546/// say so.** `Whitespace` is *horizontal* whitespace; `Newlines` is horizontal
547/// whitespace **and** line endings. So `Newlines` skips strictly more than
548/// `Whitespace` — `whitespace` is the narrower policy despite being the broader
549/// English word, and `skip: newlines` means "newlines *as well*", not "newlines
550/// only".
551///
552/// That inversion is not academic: `chars(one_of("^v<>"), skip: whitespace)` —
553/// §7.5's own example — looks like it should absorb an input file's trailing
554/// `\n`. It does not, and it does not have to: the terminator is **inside** the
555/// root region — the root region is the whole buffer, and nothing is trimmed off
556/// it — and it is forgiven because it is whitespace the character parser
557/// declined (`walk_characters` asks the child first and accepts a
558/// whitespace-only leftover through `ByteRegion::is_all_whitespace`, the bound
559/// half of ADR-078's rule). No skip policy has to account for it. The sets are
560/// the ones §7.5's example needs, and swapping them would silently change what
561/// every existing `skip: newlines` program accepts. `walk_characters` /
562/// `skip_chars` in `praxis-runtime` is the implementation, and
563/// `SkipPolicy::skips` below is the single description both the runtime comment
564/// and the `skip:` diagnostic quote.
565#[derive(Clone, Copy, Debug, PartialEq, Eq)]
566pub enum SkipPolicy {
567    /// No trimming between matches: every byte of the region is the child's.
568    None,
569    /// Skip **horizontal** whitespace — spaces and tabs — between matches. Not
570    /// line endings: see the type's own documentation.
571    Whitespace,
572    /// Skip horizontal whitespace **and** line endings between matches. The
573    /// broader of the two policies.
574    Newlines,
575}
576
577impl SkipPolicy {
578    /// Parse a `skip:` keyword value, or `None` if unknown.
579    pub fn from_keyword(name: &str) -> Option<Self> {
580        Some(match name {
581            "none" => SkipPolicy::None,
582            "whitespace" => SkipPolicy::Whitespace,
583            "newlines" => SkipPolicy::Newlines,
584            _ => return None,
585        })
586    }
587
588    /// What this policy skips, in the words the `skip:` diagnostic uses.
589    ///
590    /// One description, quoted by the diagnostic and by the runtime, so a
591    /// reader who reaches either one is told that `newlines` is the broader
592    /// policy rather than left to infer it from the names.
593    pub fn skips(self) -> &'static str {
594        match self {
595            SkipPolicy::None => "nothing",
596            SkipPolicy::Whitespace => "spaces and tabs",
597            SkipPolicy::Newlines => "spaces, tabs and line endings",
598        }
599    }
600
601    /// Every policy, in §7.5's order. The list is **closed**: a test sweeps it.
602    pub const ALL: &'static [SkipPolicy] = &[
603        SkipPolicy::None,
604        SkipPolicy::Whitespace,
605        SkipPolicy::Newlines,
606    ];
607}
608
609/// One item in a `block(...)` (§7.5).
610#[derive(Clone, Debug)]
611pub enum BlockItem {
612    /// A positional parser. If it is a named-capture template, its captures
613    /// flatten into the enclosing block record; otherwise (a scalar) it must
614    /// be the sole contributor or validation rejects it for an unclear field
615    /// name (§7.5).
616    Positional(ParserAst),
617    /// A named item `name: parser` contributing one field.
618    Named { name: String, parser: ParserAst },
619}
620
621impl ParserAst {
622    /// The source span of this parser node (§7.10: every node carries one).
623    pub fn span(&self) -> Span {
624        match self {
625            ParserAst::Atomic { span, .. }
626            | ParserAst::Template { span, .. }
627            | ParserAst::Lines { span, .. }
628            | ParserAst::Sections { span, .. }
629            | ParserAst::SectionsNamed { span, .. }
630            | ParserAst::Csv { span, .. }
631            | ParserAst::Ws { span, .. }
632            | ParserAst::Sep { span, .. }
633            | ParserAst::Grid { span, .. }
634            | ParserAst::Block { span, .. }
635            | ParserAst::Choice { span, .. }
636            | ParserAst::Optional { span, .. }
637            | ParserAst::Scan { span, .. }
638            | ParserAst::OneOf { span, .. }
639            | ParserAst::Characters { span, .. }
640            | ParserAst::Matrix { span, .. }
641            | ParserAst::GridRagged { span, .. } => *span,
642        }
643    }
644
645    /// Shift every span in this subtree by `delta` bytes.
646    ///
647    /// The template scanner works in **interior-relative** offsets: it is given
648    /// the text between the backticks and knows nothing about where the token
649    /// sits in the file. The HIR bridge, which does know, rebases the tree by
650    /// the token's start + 1 (the opening backtick). Without this a capture
651    /// body's diagnostic caret would land near the top of the file.
652    pub fn shift_spans(&mut self, delta: u32) {
653        // Bind the span mutably in one place, then recurse into the children.
654        match self {
655            ParserAst::Atomic { span, .. } | ParserAst::OneOf { span, .. } => {
656                *span = span.shifted(delta);
657            }
658            ParserAst::Template { parts, span } => {
659                *span = span.shifted(delta);
660                shift_part_spans(parts, delta);
661            }
662            ParserAst::Lines { child, span }
663            | ParserAst::Sections { child, span }
664            | ParserAst::Csv { child, span }
665            | ParserAst::Ws { child, span }
666            | ParserAst::Grid { child, span }
667            | ParserAst::Sep { child, span, .. }
668            | ParserAst::Optional { child, span }
669            | ParserAst::Scan { child, span }
670            | ParserAst::Matrix { child, span }
671            | ParserAst::GridRagged { child, span, .. }
672            | ParserAst::Characters { child, span, .. } => {
673                *span = span.shifted(delta);
674                child.shift_spans(delta);
675            }
676            ParserAst::SectionsNamed {
677                fields,
678                repeated_tail,
679                span,
680            } => {
681                *span = span.shifted(delta);
682                for item in fields {
683                    item.parser_mut().shift_spans(delta);
684                }
685                if let Some((_, tail)) = repeated_tail {
686                    tail.shift_spans(delta);
687                }
688            }
689            ParserAst::Block { items, span } => {
690                *span = span.shifted(delta);
691                for item in items {
692                    match item {
693                        BlockItem::Positional(p) | BlockItem::Named { parser: p, .. } => {
694                            p.shift_spans(delta);
695                        }
696                    }
697                }
698            }
699            ParserAst::Choice { cases, span } => {
700                *span = span.shifted(delta);
701                for (_, p) in cases {
702                    p.shift_spans(delta);
703                }
704            }
705        }
706    }
707}
708
709/// Shift every span inside a template's parts by `delta` bytes.
710///
711/// Separate from [`ParserAst::shift_spans`] because the two callers need
712/// different halves: the `Template` arm of `shift_spans` rebases the node's own
713/// span *and* its parts, while [`crate::body`] — which has just scanned a
714/// nested template whose interior has its own offsets — has to rebase the parts
715/// **without** touching the enclosing span, which it already built in the outer
716/// text's offsets.
717pub fn shift_part_spans(parts: &mut [TemplatePart], delta: u32) {
718    for part in parts {
719        match part {
720            TemplatePart::Literal { span, .. } => *span = span.shifted(delta),
721            TemplatePart::Capture {
722                parser,
723                span,
724                name_span,
725                ..
726            } => {
727                *span = span.shifted(delta);
728                if let Some(n) = name_span {
729                    *n = n.shifted(delta);
730                }
731                parser.shift_spans(delta);
732            }
733        }
734    }
735}
736
737/// The name of a structural constructor — **the whole of §7.5**.
738///
739/// Every constructor is dispatched from this table and nowhere else. A
740/// constructor with no row here would have no arity, and therefore no arity
741/// *error* either: its name would become `None` with no diagnostic at all.
742#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
743pub enum Constructor {
744    Lines,
745    Sections,
746    Csv,
747    Ws,
748    Sep,
749    Grid,
750    Matrix,
751    Chars,
752    OneOf,
753    Block,
754    Choice,
755    Optional,
756    Scan,
757    /// `repeated(P)` / `repeated(P, N)` — legal **only** as a named argument of
758    /// a `sections` call (§7.5). It is in the table so that the name is known
759    /// and its misuse is `MisplacedRepeatedTail` rather than "unknown
760    /// constructor".
761    Repeated,
762}
763
764/// The **shape** of a constructor call's argument list (§7.5).
765///
766/// A count is not enough: `sep` takes a string and then a parser, `choice`
767/// takes named arguments and no positional ones, `chars` takes a parser and an
768/// optional keyword. Checking a positional arity alone would accept
769/// `optional(int, word)` and `choice(int)`.
770#[derive(Clone, Copy, Debug, PartialEq, Eq)]
771pub enum ArgShape {
772    /// Exactly `n` positional parsers and nothing else.
773    Positional(usize),
774    /// `sep("s", P)` — one string literal, then one parser.
775    StringThenParser,
776    /// `one_of("LR")` — one string literal.
777    OneString,
778    /// `chars(P, skip: policy)` — one parser and an optional `skip:` keyword.
779    ParserWithSkip,
780    /// `repeated(P)` or `repeated(P, N)` — one parser and an optional count
781    /// literal. The count must be a literal because the parser plan is built
782    /// when the program is compiled, so there is no runtime value in scope to
783    /// read one from.
784    ParserWithOptionalCount,
785    /// `grid(P)` or `grid(P, ragged, fill: value)` — the ragged flag and the
786    /// fill value come as a pair or not at all.
787    GridMaybeRagged,
788    /// `sections(P)` **or** `sections(name: P, …)` — the homogeneous and
789    /// heterogeneous forms are one name with two shapes.
790    OnePositionalOrNamed,
791    /// `block(item, …)` — one or more positional parsers and/or named items.
792    Items,
793    /// `choice(Name: P, …)` — named arguments only, at least `at_least` of them.
794    NamedOnly { at_least: usize },
795}
796
797impl Constructor {
798    /// Parse a constructor name, or `None` if no §7.5 constructor is spelled
799    /// that way.
800    pub fn from_keyword(name: &str) -> Option<Self> {
801        Some(match name {
802            "lines" => Constructor::Lines,
803            "sections" => Constructor::Sections,
804            "csv" => Constructor::Csv,
805            "ws" => Constructor::Ws,
806            "sep" => Constructor::Sep,
807            "grid" => Constructor::Grid,
808            "matrix" => Constructor::Matrix,
809            "chars" => Constructor::Chars,
810            "one_of" => Constructor::OneOf,
811            "block" => Constructor::Block,
812            "choice" => Constructor::Choice,
813            "optional" => Constructor::Optional,
814            "scan" => Constructor::Scan,
815            "repeated" => Constructor::Repeated,
816            _ => return None,
817        })
818    }
819
820    /// The source keyword for this constructor.
821    pub fn keyword(self) -> &'static str {
822        match self {
823            Constructor::Lines => "lines",
824            Constructor::Sections => "sections",
825            Constructor::Csv => "csv",
826            Constructor::Ws => "ws",
827            Constructor::Sep => "sep",
828            Constructor::Grid => "grid",
829            Constructor::Matrix => "matrix",
830            Constructor::Chars => "chars",
831            Constructor::OneOf => "one_of",
832            Constructor::Block => "block",
833            Constructor::Choice => "choice",
834            Constructor::Optional => "optional",
835            Constructor::Scan => "scan",
836            Constructor::Repeated => "repeated",
837        }
838    }
839
840    /// Every constructor, so a test can sweep the table — and so the editor can
841    /// offer them: completion, signature help and the parser keyword list read
842    /// this, so a name missing here is a name the editor never offers.
843    /// `constructor_round_trips_keywords_and_states_its_shape` is what keeps it
844    /// complete.
845    pub const ALL: &'static [Constructor] = &[
846        Constructor::Lines,
847        Constructor::Sections,
848        Constructor::Csv,
849        Constructor::Ws,
850        Constructor::Sep,
851        Constructor::Grid,
852        Constructor::Matrix,
853        Constructor::Chars,
854        Constructor::OneOf,
855        Constructor::Block,
856        Constructor::Choice,
857        Constructor::Optional,
858        Constructor::Scan,
859        Constructor::Repeated,
860    ];
861
862    /// One line of §7.5, for hover (§15.2's "method documentation", and its
863    /// parser half).
864    ///
865    /// Exhaustive, and here rather than in the language server for the reason
866    /// every other table is: a constructor added to §7.5 cannot ship without
867    /// saying what it does, and the editor cannot describe one differently from
868    /// the compiler. The wording is §7.5's own, compressed to a line.
869    pub fn doc(self) -> &'static str {
870        match self {
871            Constructor::Lines => {
872                "Split the region into lines and apply the parser to each. Every \
873                 line must be consumed whole."
874            }
875            Constructor::Sections => {
876                "Split the region on blank lines and apply the parser to each \
877                 section. With named arguments, parses fixed sections in order \
878                 into a record."
879            }
880            Constructor::Csv => {
881                "Split the region on commas. Whitespace around a comma is \
882                 forgiven, because the field's own parser does not read it."
883            }
884            Constructor::Ws => {
885                "Split on runs of whitespace — line endings included, so a token \
886                 never spans a line."
887            }
888            Constructor::Sep => "Split on an exact separator string, with no implicit trimming.",
889            Constructor::Grid => {
890                "Parse rectangular lines into a `Grid[T]`, one cell per parser \
891                 application. `ragged` with `fill:` permits uneven rows."
892            }
893            Constructor::Matrix => {
894                "Parse lines of whitespace-separated elements into a `Grid[T]`. \
895                 Unlike `lines(ws(P))`, a row with no tokens is not a row."
896            }
897            Constructor::Chars => {
898                "Apply a parser repeatedly to characters. `skip:` says what is \
899                 passed over between matches: `none`, `whitespace`, `newlines`."
900            }
901            Constructor::OneOf => "Match one character from a literal set.",
902            Constructor::Block => {
903                "Apply parsers in sequence within one region. A positional item \
904                 contributes its captures; a named one contributes a field."
905            }
906            Constructor::Choice => {
907                "Parse one of several alternatives into an anonymous enum, one \
908                 variant per named case."
909            }
910            Constructor::Optional => {
911                "Return `Option[T]`. A failure consumes no input — this is \
912                 parser-level optionality, not recovery."
913            }
914            Constructor::Scan => {
915                "Find repeated matches inside otherwise irrelevant text, for \
916                 input that embeds its data in noise."
917            }
918            Constructor::Repeated => {
919                "A repeating group of sections in a heterogeneous `sections`. \
920                 `repeated(P, N)` takes exactly N and may be followed; \
921                 `repeated(P)` takes every section left, so it must be last."
922            }
923        }
924    }
925
926    /// The one named argument this constructor takes whose value is a
927    /// **keyword and not a parser** — `chars(P, skip: policy)`'s `skip:` and
928    /// `grid(P, ragged, fill: value)`'s `fill:` (§7.5). `None` for every other
929    /// constructor.
930    ///
931    /// A keyword belongs to a constructor, so the constructor is what answers
932    /// the question. Deciding it from the argument's *name* alone would mint a
933    /// `block` item or a `sections` field legitimately named `fill` or `skip`
934    /// as a keyword argument, and the field would then vanish from the record
935    /// with no diagnostic.
936    pub fn keyword_arg(self) -> Option<&'static str> {
937        match self {
938            Constructor::Chars => Some("skip"),
939            Constructor::Grid => Some("fill"),
940            _ => None,
941        }
942    }
943
944    /// The one **bare keyword flag** this constructor takes — the `ragged` of
945    /// `grid(P, ragged, fill: value)` (§7.5). `None` for every other
946    /// constructor.
947    ///
948    /// The companion to [`Constructor::keyword_arg`], and here for the same
949    /// reason: a flag belongs to a constructor, so the constructor is what
950    /// answers the question. Minting a `CallArg::Flag` from the bare *name*
951    /// would make `ragged` a flag in **every** constructor's argument list, so
952    /// `lines(ragged)` would be told it had written a flag where a parser
953    /// belongs rather than that `ragged` is not a parser, and the word would be
954    /// reserved everywhere instead of in `grid`.
955    pub fn flag_arg(self) -> Option<&'static str> {
956        match self {
957            Constructor::Grid => Some("ragged"),
958            _ => None,
959        }
960    }
961
962    /// The shape of this constructor's argument list (§7.5).
963    pub fn arg_shape(self) -> ArgShape {
964        match self {
965            Constructor::Lines
966            | Constructor::Csv
967            | Constructor::Ws
968            | Constructor::Matrix
969            | Constructor::Optional
970            | Constructor::Scan => ArgShape::Positional(1),
971            Constructor::Repeated => ArgShape::ParserWithOptionalCount,
972            Constructor::Sections => ArgShape::OnePositionalOrNamed,
973            Constructor::Sep => ArgShape::StringThenParser,
974            Constructor::OneOf => ArgShape::OneString,
975            Constructor::Chars => ArgShape::ParserWithSkip,
976            Constructor::Grid => ArgShape::GridMaybeRagged,
977            Constructor::Block => ArgShape::Items,
978            Constructor::Choice => ArgShape::NamedOnly { at_least: 1 },
979        }
980    }
981}
982
983#[cfg(test)]
984mod tests {
985    use super::*;
986
987    /// §7.4's list is a **closed set of ten**, and every one of the design
988    /// document's atomic names round-trips through the table.
989    #[test]
990    fn atomic_round_trips_keywords() {
991        for kind in AtomicKind::ALL {
992            assert_eq!(AtomicKind::from_keyword(kind.keyword()), Some(*kind));
993            // Every §7.4 name is spelled here, exhaustively, for the reason
994            // `Constructor`'s sweep below gives: the `names` assertion under
995            // this loop looks like it pins the set, but it collects *from
996            // `ALL`*, so an eleventh atomic left out of `ALL` leaves that list
997            // ten long and green. Adding a variant fails to compile here
998            // instead.
999            match kind {
1000                AtomicKind::Int
1001                | AtomicKind::UInt
1002                | AtomicKind::Float
1003                | AtomicKind::Byte
1004                | AtomicKind::Char
1005                | AtomicKind::Digit
1006                | AtomicKind::Word
1007                | AtomicKind::Identifier
1008                | AtomicKind::Text
1009                | AtomicKind::Rest => {}
1010            }
1011        }
1012        assert_eq!(AtomicKind::from_keyword("nope"), None);
1013
1014        // §7.4 verbatim, in its own order.
1015        let names: Vec<&str> = AtomicKind::ALL.iter().map(|k| k.keyword()).collect();
1016        assert_eq!(
1017            names,
1018            vec![
1019                "int",
1020                "uint",
1021                "float",
1022                "byte",
1023                "char",
1024                "digit",
1025                "word",
1026                "identifier",
1027                "text",
1028                "rest"
1029            ]
1030        );
1031        // And nothing else is an atomic — an eleventh name would have to be
1032        // added to §7.4 first.
1033        for not_an_atomic in ["uint8", "integer", "string", "line", "lines", "sep"] {
1034            assert_eq!(AtomicKind::from_keyword(not_an_atomic), None);
1035        }
1036    }
1037
1038    /// Every §7.5 constructor round-trips through the table and states its
1039    /// argument **shape**, not just a count: a count cannot say that `sep`'s
1040    /// first argument is a *string*, that `choice` takes no positional argument
1041    /// at all, or that `optional` takes one and not two.
1042    #[test]
1043    fn constructor_round_trips_keywords_and_states_its_shape() {
1044        for ctor in Constructor::ALL {
1045            assert_eq!(
1046                Constructor::from_keyword(ctor.keyword()),
1047                Some(*ctor),
1048                "`{}` must round-trip through the table",
1049                ctor.keyword()
1050            );
1051            // Every §7.5 name is spelled here, exhaustively: adding a
1052            // constructor fails to compile at this match rather than passing
1053            // quietly out of `ALL`. A length assertion could not do that — it
1054            // is green while the list is short and only fires when the list
1055            // *was* updated and the number was not. `ALL` feeds completion,
1056            // signature help and the keyword list as well as this sweep, so a
1057            // name missing from it is one the editor never offers.
1058            match ctor {
1059                Constructor::Lines
1060                | Constructor::Sections
1061                | Constructor::Csv
1062                | Constructor::Ws
1063                | Constructor::Sep
1064                | Constructor::Grid
1065                | Constructor::Matrix
1066                | Constructor::Chars
1067                | Constructor::OneOf
1068                | Constructor::Block
1069                | Constructor::Choice
1070                | Constructor::Optional
1071                | Constructor::Scan
1072                | Constructor::Repeated => {}
1073            }
1074        }
1075        assert_eq!(Constructor::from_keyword("frobnicate"), None);
1076
1077        // And a constructor cannot be added without deciding what its arguments
1078        // look like.
1079        assert_eq!(Constructor::Lines.arg_shape(), ArgShape::Positional(1));
1080        assert_eq!(Constructor::Optional.arg_shape(), ArgShape::Positional(1));
1081        assert_eq!(Constructor::Sep.arg_shape(), ArgShape::StringThenParser);
1082        assert_eq!(Constructor::OneOf.arg_shape(), ArgShape::OneString);
1083        assert_eq!(Constructor::Chars.arg_shape(), ArgShape::ParserWithSkip);
1084        assert_eq!(Constructor::Grid.arg_shape(), ArgShape::GridMaybeRagged);
1085        assert_eq!(
1086            Constructor::Sections.arg_shape(),
1087            ArgShape::OnePositionalOrNamed
1088        );
1089        assert_eq!(Constructor::Block.arg_shape(), ArgShape::Items);
1090        assert_eq!(
1091            Constructor::Choice.arg_shape(),
1092            ArgShape::NamedOnly { at_least: 1 }
1093        );
1094        assert_eq!(
1095            Constructor::Repeated.arg_shape(),
1096            ArgShape::ParserWithOptionalCount
1097        );
1098
1099        // And which constructor owns `ragged` — one, and not "whichever call
1100        // happens to have a bare `ragged` in it".
1101        for ctor in Constructor::ALL {
1102            let expected = (*ctor == Constructor::Grid).then_some("ragged");
1103            assert_eq!(ctor.flag_arg(), expected, "`{}`", ctor.keyword());
1104        }
1105    }
1106
1107    /// **The count that names no sections is not constructible.**
1108    ///
1109    /// `repeated(P, 0)` would consume nothing and produce an empty `Vec`, which
1110    /// is a parser nobody writes on purpose and reads as a typo for the
1111    /// unbounded form; a negative count names no sections at all. A `validate`
1112    /// arm would catch either only where somebody remembered to call it, so the
1113    /// one constructor refuses them — the same argument `Separator` makes about
1114    /// the separator that never advances.
1115    #[test]
1116    fn a_repeat_count_is_positive_by_construction() {
1117        assert_eq!(RepeatCount::new(0), Err(InvalidRepeatCount::NotPositive));
1118        assert_eq!(RepeatCount::new(-3), Err(InvalidRepeatCount::NotPositive));
1119        assert_eq!(
1120            RepeatCount::new(1 << 33),
1121            Err(InvalidRepeatCount::TooLarge),
1122            "the plan node holds a u32, so the refusal happens where the span is"
1123        );
1124
1125        assert_eq!(RepeatCount::new(1).expect("one section").get(), 1);
1126        assert_eq!(RepeatCount::new(6).expect("six sections").get(), 6);
1127        assert_eq!(
1128            RepeatCount::new(i64::from(u32::MAX))
1129                .expect("the largest count the plan can hold")
1130                .get(),
1131            u32::MAX
1132        );
1133    }
1134
1135    /// A counted item wants its count's worth of sections and a plain one wants
1136    /// exactly one — the number the runtime's cursor advances by, stated once
1137    /// here so the walk and the shortfall diagnostic cannot disagree about it.
1138    #[test]
1139    fn a_section_items_appetite_is_its_count() {
1140        let atom = || ParserAst::Atomic {
1141            kind: AtomicKind::Int,
1142            span: Span::at(0),
1143        };
1144        let one = SectionItem::One {
1145            name: "regions".to_string(),
1146            parser: atom(),
1147        };
1148        let counted = SectionItem::Counted {
1149            name: "shapes".to_string(),
1150            count: RepeatCount::new(6).expect("six sections"),
1151            parser: atom(),
1152        };
1153        assert_eq!(one.sections_wanted(), 1);
1154        assert_eq!(counted.sections_wanted(), 6);
1155        assert_eq!(one.name(), "regions");
1156        assert_eq!(counted.name(), "shapes");
1157    }
1158}