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}