Skip to main content

praxis_runtime/
parser.rs

1//! The runtime input-parser interpreter (§7).
2//!
3//! Evaluates a compiled [`ParserPlan`] against the process-input buffer (or a
4//! `Text` value), allocating GC results (`Int`, `Float`, `Byte`, `Char`,
5//! source-slice `Text`, `Vec`, `Grid`, `Record`, `Tuple`, enum values) and
6//! raising `FaultKind::ParseFailed` on mismatch.
7//!
8//! The plan type and global arena live in `praxis-input-parser`; this
9//! interpreter looks up a plan by its [`PlanId`] and walks its node arena.
10//!
11//! **The arena is not `#[repr(C)]`.** It is ordinary Rust enums and slices, and
12//! nothing here crosses an FFI boundary — only the plan *id* is a JIT immediate.
13//! `praxis_input_parser::plan`'s own doc is the authority on its layout.
14
15mod cursor;
16
17use crate::GcRef;
18use crate::context::RuntimeContext;
19use crate::parse_detail::ParseFail;
20use crate::roots::{NativeScope, RuntimeRoots};
21use crate::scalars;
22use crate::text::TextPayload;
23use cursor::{ByteRegion, Cursor, Input, Walked, split_lines, split_sections, trailing_blank_run};
24use praxis_input_parser::synthesize::AtomicClass;
25use praxis_input_parser::{AtomicKind, ParserPlan, PlanNode, SectionItemNode, TemplateShape};
26
27/// Run the parser plan named by `raw_id` against `input`, returning the parsed
28/// result or `None` on failure (a value that names no plan → `None`; parse
29/// mismatch → sets `ParseFailed` fault + `None`).
30///
31/// `raw_id` arrives as the payload of a boxed `Int` — an `i64` the ABI cannot
32/// constrain — so it is validated here rather than narrowed with an `as`, which
33/// would fold `0x1_0000_0005` onto plan 5 and every negative onto a huge index.
34/// Zero is rejected too: [`PlanId`](praxis_input_parser::PlanId) is non-zero
35/// precisely so a failure sentinel cannot name a plan.
36///
37/// # Safety
38/// `ctx` must be live and wired; `input` must be a valid `Text` GcRef.
39pub unsafe fn run_plan_by_id(ctx: *mut RuntimeContext, raw_id: i64, input: GcRef) -> Option<GcRef> {
40    let id = u32::try_from(raw_id)
41        .ok()
42        .and_then(praxis_input_parser::PlanId::from_raw)?;
43    let plan = praxis_input_parser::get_plan(id)?;
44    // SAFETY: caller guarantees ctx/input validity.
45    Some(unsafe { run_plan(ctx, plan, input) })
46}
47
48/// Run a parser plan against an input buffer.
49///
50/// Clears the runtime's [`ParseDetail`] slot at the start so a stale failure
51/// from a prior parse does not leak in; on a mismatch, the deepest failure is
52/// recorded there (§7.11) before the `ParseFailed` fault is raised.
53///
54/// # Safety
55/// `ctx` must be live and wired; `input` must be a valid `Text` GcRef.
56unsafe fn run_plan(ctx: *mut RuntimeContext, plan: &ParserPlan, input: GcRef) -> GcRef {
57    // The buffer **and its owner** both come from the `input` argument. Taking
58    // the bytes from here and the owner from `ctx.input_source` would make
59    // `parse(text, P)` produce `Text` values that are views of the stdin buffer
60    // at the offsets of a different string.
61    // SAFETY: the caller guarantees `input` is a valid Text GcRef.
62    let Some(i) = (unsafe { Input::new(input) }) else {
63        unsafe { clear_parse_detail(ctx) };
64        return unsafe { fault_sentinel(ctx) };
65    };
66    // **The root region is the whole buffer, and no terminator is trimmed off
67    // it.** Trailing whitespace is handled where it arises — `walk_exact` lets
68    // a child that leaves only whitespace fill its bound, `trailing_blank_run`
69    // lets a line-splitting construct leave a trailing blank line to nobody
70    // when its parser makes nothing of it, and `split_lines` does not end a
71    // region in empty lines — so nothing is left here to special-case. A trim
72    // count is the wrong kind of answer (ADR-078).
73    //
74    // It also matters *whose* buffer this is: `run_plan` is the single body
75    // behind both `read <parser>` and the host `parse(text, P)`, so a trim here
76    // would delete a byte from a Text the program wrote itself, and
77    // `parse(t, rest)` would stop being the identity on `t`.
78    let region = i.whole();
79    // Root the input for the whole parse. `RuntimeRoots`'s `input` arm reads
80    // `ctx.input_source`, which for `parse(text, P)` is a *different* Text —
81    // and this one owns every source-slice the parse produces.
82    // SAFETY: ctx is live and outlives this scope.
83    let scope = unsafe { NativeScope::new(ctx) };
84    let _input = scope.root(input);
85    // Clear any stale detail from a prior parse, then run.
86    unsafe { clear_parse_detail(ctx) };
87    let result = unsafe { walk(ctx, &i, plan, plan.root, region) };
88    match result {
89        // The root does **not** require exhaustion. Every real input ends with
90        // a newline (`praxis-cli`'s runner reads the file verbatim), so a root
91        // that demanded its region be consumed would fault on every file in the
92        // corpus. Exhaustion is a *parent's* decision, made by `walk_exact`.
93        Ok(walked) => walked.value,
94        Err(fail) => {
95            // Record the deepest failure into the runtime's detail slot, then
96            // raise the fault. The host reads the detail after `ParseFailed`.
97            // The preview is taken against the **whole** buffer: failure
98            // offsets are absolute, and `i.whole()` is the region the parse
99            // ran against, so the two cannot drift.
100            unsafe { record_fail(ctx, fail, i.whole().bytes(&i)) };
101            unsafe { fault_sentinel(ctx) }
102        }
103    }
104}
105
106/// Run `plan`'s root against `input` and hand back the value or the failure,
107/// with no fault raised and no detail recorded — which is what lets the
108/// interpreter's own unit tests assert on a [`ParseFail`] directly.
109#[cfg(test)]
110unsafe fn run_root(
111    ctx: *mut RuntimeContext,
112    plan: &ParserPlan,
113    input: GcRef,
114) -> Result<GcRef, ParseFail> {
115    // SAFETY: the caller guarantees `input` is a valid Text GcRef.
116    let i = unsafe { Input::new(input) }.expect("the test's input is a Text");
117    // The same root region `run_plan` uses, for the same reason.
118    let region = i.whole();
119    // SAFETY: ctx is live and outlives this scope.
120    let scope = unsafe { NativeScope::new(ctx) };
121    let _input = scope.root(input);
122    // SAFETY: the caller guarantees ctx is live and wired.
123    unsafe { walk(ctx, &i, plan, plan.root, region) }.map(|w| w.value)
124}
125
126/// Set a `ParseFailed` fault and return the sentinel.
127unsafe fn fault_sentinel(ctx: *mut RuntimeContext) -> GcRef {
128    unsafe { set_parse_fault(ctx) };
129    unsafe { (*ctx).unit_ref }
130}
131
132/// Mark a parse fault on the context.
133unsafe fn set_parse_fault(ctx: *mut RuntimeContext) {
134    let fault = unsafe { &mut *(*ctx).pending_fault };
135    fault.set(crate::context::RaisedFault::PARSE_FAILED);
136}
137
138/// Clear the runtime's [`ParseDetail`] slot at the start of a parse.
139///
140/// `pub(crate)` for `praxis_run_parser`'s §6.3 descriptor guard, which returns
141/// before `run_plan` and so has to do its own clearing — otherwise it reports
142/// the *previous* parse's offset and expectation for a parse that never ran.
143///
144/// # Safety
145/// `ctx` must be live and wired with a non-null `parse_detail`.
146pub(crate) unsafe fn clear_parse_detail(ctx: *mut RuntimeContext) {
147    // SAFETY: caller guarantees `ctx` is live.
148    if unsafe { (*ctx).parse_detail.is_null() } {
149        return;
150    }
151    // SAFETY: caller guarantees parse_detail points at a live ParseDetail.
152    unsafe { (*(*ctx).parse_detail).clear() };
153}
154
155/// Record a [`ParseFail`] into the runtime's [`ParseDetail`] slot, keeping the
156/// deepest (most specific) failure (§7.11).
157///
158/// # Safety
159/// `ctx` must be live and wired; `input` is the buffer the failure was against
160/// (used for the actual-preview).
161unsafe fn record_fail(ctx: *mut RuntimeContext, fail: ParseFail, input: &[u8]) {
162    // SAFETY: caller guarantees `ctx` is live.
163    if unsafe { (*ctx).parse_detail.is_null() } {
164        return;
165    }
166    // SAFETY: caller guarantees parse_detail points at a live ParseDetail.
167    unsafe { (*(*ctx).parse_detail).consider(fail, input) };
168}
169
170/// The outcome of walking a node: a value + **the absolute position parsing
171/// stopped at**, or an error carrying the §7.11 structured detail. The deepest
172/// (highest-offset) failure wins at the [`run_plan`] boundary; inner failures
173/// propagate up with their already-specific detail, so an outer constructor
174/// only overrides when it has *more* specific information (it generally does
175/// not).
176type WalkResult = Result<Walked, ParseFail>;
177
178/// The runtime, extracted from the context for allocation calls.
179struct Rt {
180    ctx: *mut RuntimeContext,
181}
182
183/// Access the heap from the context (same-crate, so we read the raw pointer).
184unsafe fn heap_ref<'a>(ctx: *mut RuntimeContext) -> &'a crate::Heap {
185    // SAFETY: caller guarantees ctx is valid and wired.
186    unsafe { &*(*ctx).heap }
187}
188
189impl Rt {
190    /// Give the collector its chance, against the whole root set.
191    ///
192    /// Every allocation in this file goes through here, which is safe only
193    /// because the `NativeScope`s in the helpers below root the interpreter's
194    /// `Vec<GcRef>` intermediates. Were they invisible to the root set, a
195    /// collection anywhere inside a parse would reclaim the values the parse is
196    /// in the middle of assembling (ADR-040, hazard H1).
197    fn safepoint(&self) -> (&crate::Heap, crate::heap::Safepoint<'_>) {
198        // SAFETY: ctx is valid (caller upholds).
199        let heap = unsafe { heap_ref(self.ctx) };
200        // SAFETY: as above.
201        let roots = unsafe { RuntimeRoots::from_context(self.ctx) };
202        let safepoint = heap.pace(&roots);
203        (heap, safepoint)
204    }
205
206    /// The boxed `Int` for `value` — the interned immortal when it is small
207    /// ([`crate::small_int`]), a fresh allocation otherwise.
208    ///
209    /// The safepoint is taken either way, for [`Rt::safepoint`]'s reason and
210    /// not out of symmetry: an `int` atomic repeated over a large input is one
211    /// of the few things in a parse that allocates on every step, so it is
212    /// exactly where the collector must keep being offered a turn even once most
213    /// of the digits it parses answer from the table.
214    fn alloc_int(&self, value: i64) -> GcRef {
215        let (heap, safepoint) = self.safepoint();
216        match crate::small_int::index_of(value) {
217            // SAFETY: `ctx` is valid (the `Rt`'s invariant) and `index_of`
218            // bounds `i` by the table's length.
219            Some(i) => {
220                drop(safepoint);
221                unsafe { *(*self.ctx).small_ints.add(i) }
222            }
223            None => heap.alloc(safepoint, scalars::INT_PAYLOAD, value),
224        }
225    }
226
227    /// The boxed `Char` for `value` — the interned immortal when it is ASCII
228    /// ([`crate::small_char`]), a fresh allocation otherwise.
229    ///
230    /// [`Rt::alloc_int`]'s shape, and the site ADR-107 was written for: the
231    /// `char` atomic runs once per **grid cell**, so without interning
232    /// `read grid(char)` over a 140×140 AoC map boxes 19,600 objects of which at
233    /// most 128 have distinct values.
234    ///
235    /// The safepoint is taken either way, for [`Rt::alloc_int`]'s reason: a grid
236    /// parse is one of the few things that allocates on every step, so it is
237    /// exactly where the collector must keep being offered a turn even once every
238    /// cell answers from the table. The `Grid`'s own item vector is what still
239    /// grows, and it is what a collection here would have to find rooted — which
240    /// is `walk_grid`'s `NativeScope`'s job.
241    fn alloc_char(&self, value: u32) -> GcRef {
242        let (heap, safepoint) = self.safepoint();
243        match crate::small_char::index_of(value) {
244            // SAFETY: `ctx` is valid (the `Rt`'s invariant) and `index_of`
245            // bounds `i` by the table's length.
246            Some(i) => {
247                drop(safepoint);
248                unsafe { *(*self.ctx).small_chars.add(i) }
249            }
250            None => heap.alloc(safepoint, scalars::CHAR_PAYLOAD, value),
251        }
252    }
253
254    /// Allocate a boxed `Float` (§7.4's `float` atomic).
255    fn alloc_float(&self, value: f64) -> GcRef {
256        let (heap, safepoint) = self.safepoint();
257        heap.alloc(safepoint, scalars::FLOAT_PAYLOAD, value)
258    }
259
260    /// Allocate a boxed `Byte` (§7.4's `byte` atomic).
261    fn alloc_byte(&self, value: u8) -> GcRef {
262        let (heap, safepoint) = self.safepoint();
263        heap.alloc(safepoint, scalars::BYTE_PAYLOAD, value)
264    }
265
266    /// Allocate a source-slice `Text` pointing into `owner`, or `None` if the
267    /// range is not a `Text`.
268    ///
269    /// The parser computes its offsets from byte positions in the very buffer
270    /// it is slicing, so `None` means the interpreter has a bug — but it must
271    /// still surface as a parse fault rather than a panic across the ABI
272    /// (§10.4), which is why this is fallible rather than an assert.
273    fn alloc_text_slice(&self, owner: GcRef, start: usize, len: usize) -> Option<GcRef> {
274        // SAFETY: `owner` is the context's input buffer, a live Text.
275        let slice = unsafe { crate::text::SourceSlice::new(owner, start, len) }?;
276        let payload = TextPayload::Slice(slice);
277        let (heap, safepoint) = self.safepoint();
278        // SAFETY: TextPayload is TEXT's payload type.
279        Some(unsafe { heap.alloc_payload(safepoint, &crate::text::TEXT, payload) })
280    }
281
282    /// Allocate an **owned** `Text` holding a copy of `s`.
283    ///
284    /// Used only for a ragged grid's `fill` literal, which lives in plan
285    /// storage rather than in the input. Giving it a `Text` of its own is what
286    /// lets the cell parser slice it: walking the fill's bytes while allocating
287    /// slices against the *input* would make a `Text` fill cell name input bytes
288    /// chosen by the fill's length.
289    fn alloc_text_owned(&self, s: &str) -> GcRef {
290        let payload = TextPayload::owned(s);
291        let (heap, safepoint) = self.safepoint();
292        // SAFETY: TextPayload is TEXT's payload type.
293        unsafe { heap.alloc_payload(safepoint, &crate::text::TEXT, payload) }
294    }
295
296    /// Allocate a `Vec` from element refs.
297    fn alloc_vec(
298        &self,
299        element_descriptor: &'static crate::TypeDescriptor,
300        items: Vec<GcRef>,
301    ) -> GcRef {
302        let payload = crate::collections::VecPayload {
303            element_descriptor,
304            items: items.into(),
305        };
306        let (heap, safepoint) = self.safepoint();
307        // SAFETY: VecPayload is VEC's payload type.
308        unsafe { heap.alloc_payload(safepoint, &crate::collections::VEC, payload) }
309    }
310
311    /// Allocate an enum value: `schema` says which enum type it is, `tag`
312    /// selects the variant, and `items` are the payload values. Matches the
313    /// `EnumPayload` layout that codegen-produced `match` code expects (§4.6).
314    /// Used by `choice`/`optional`.
315    fn alloc_enum(
316        &self,
317        schema: *const crate::enums::EnumSchema,
318        tag: u32,
319        items: Vec<GcRef>,
320    ) -> GcRef {
321        let payload = crate::enums::EnumPayload { schema, tag, items };
322        let (heap, safepoint) = self.safepoint();
323        // SAFETY: EnumPayload is ENUM's payload type.
324        unsafe { heap.alloc_payload(safepoint, &crate::enums::ENUM, payload) }
325    }
326}
327
328/// Walk a plan node against `region`, producing a value and the absolute
329/// position where matching stopped.
330///
331/// The node begins at `region.start()` and may not read past `region.end()`.
332/// Whether it must *reach* `region.end()` is the parent's decision, made by
333/// [`walk_exact`]: `lines` requires it of each line, `scan` does not require it
334/// of a match. That is one rule in one place, which is what makes
335/// `scan(choice(…))` and `lines(choice(…))` both correct without `choice`
336/// itself having a policy.
337///
338/// # Safety
339/// `ctx` must be live and wired.
340unsafe fn walk(
341    ctx: *mut RuntimeContext,
342    i: &Input<'_>,
343    plan: &ParserPlan,
344    node: u32,
345    region: ByteRegion,
346) -> WalkResult {
347    let rt = Rt { ctx };
348    let node = &plan.nodes[node as usize];
349    match node {
350        PlanNode::Atomic { kind } => walk_atomic(&rt, i, *kind, region),
351        PlanNode::Lines { child } => walk_lines(&rt, i, plan, *child, region),
352        PlanNode::Sections { child } => walk_sections(&rt, i, plan, *child, region),
353        PlanNode::SectionsNamed {
354            fields,
355            repeated_tail,
356            field_order,
357        } => walk_sections_named(&rt, i, plan, fields, *repeated_tail, field_order, region),
358        PlanNode::Block { items, field_order } => {
359            walk_block(&rt, i, plan, items, field_order, region)
360        }
361        PlanNode::Choice { cases } => walk_choice(&rt, i, plan, cases, region),
362        PlanNode::Optional { child } => walk_optional(&rt, i, plan, *child, region),
363        PlanNode::Scan { child } => walk_scan(&rt, i, plan, *child, region),
364        PlanNode::OneOf { chars_index } => {
365            let chars = plan.literals[*chars_index as usize];
366            walk_one_of(&rt, i, chars, region)
367        }
368        PlanNode::Characters { child, skip } => {
369            walk_characters(&rt, i, plan, *child, *skip, region)
370        }
371        PlanNode::Matrix { child } => walk_matrix(&rt, i, plan, *child, region),
372        PlanNode::GridRagged { child, fill_index } => {
373            let fill = plan.literals[*fill_index as usize];
374            walk_grid_ragged(&rt, i, plan, *child, fill, region)
375        }
376        PlanNode::Csv { child } => walk_csv(&rt, i, plan, *child, region),
377        PlanNode::Ws { child } => walk_ws(&rt, i, plan, *child, region),
378        PlanNode::Sep {
379            separator_index,
380            child,
381        } => {
382            let sep = plan.literals[*separator_index as usize];
383            walk_sep(&rt, i, plan, *child, sep, region)
384        }
385        PlanNode::Grid { child } => walk_grid(&rt, i, plan, *child, region),
386        PlanNode::Template { parts, field_order } => {
387            walk_template(&rt, i, plan, parts, field_order, region)
388        }
389    }
390}
391
392// ---- atomics (§7.4) -------------------------------------------------------
393
394fn walk_atomic(rt: &Rt, i: &Input<'_>, kind: AtomicKind, region: ByteRegion) -> WalkResult {
395    let rest = region.bytes(i);
396    // Every atomic starts by skipping horizontal whitespace; `at` is where the
397    // value itself begins, in the input's own coordinates.
398    let s = trim_leading_ws(rest);
399    let at = region.start().advance(rest.len() - s.len());
400    // The name a failure reports is the atomic's own keyword, taken from the one
401    // place that owns those ten strings ([`AtomicKind::keyword`]) rather than
402    // spelled per-arm — arms are shared between kinds, so a per-arm literal
403    // would report a parser the program did not write.
404    let what = kind.keyword();
405    match kind {
406        AtomicKind::Int => {
407            // Parse a signed decimal integer.
408            let (digits, len) = take_int_run(s);
409            if digits.is_empty() {
410                return Err(ParseFail::at(at.offset(), 0, what));
411            }
412            let value: i64 = digits
413                .parse()
414                .map_err(|_| ParseFail::at(at.offset(), len, what))?;
415            Ok(Walked {
416                value: rt.alloc_int(value),
417                next: at.advance(len),
418            })
419        }
420        AtomicKind::Digit => {
421            let Some(&b) = s.first() else {
422                return Err(ParseFail::at(at.offset(), 0, what));
423            };
424            if !b.is_ascii_digit() {
425                return Err(ParseFail::at(at.offset(), 1, what));
426            }
427            let value = (b - b'0') as i64;
428            Ok(Walked {
429                value: rt.alloc_int(value),
430                next: at.advance(1),
431            })
432        }
433        AtomicKind::Char => {
434            // One Unicode scalar value, stepped by the region.
435            //
436            // **A space is a character**, so `char` reads the scalar at the
437            // cursor and does not trim first. §7.4's "surrounding horizontal
438            // space handled by caller" is a rule for the *numeric* atomics; a
439            // character parser that skipped spaces cannot represent one. That
440            // is not a nicety: a `grid` column is positional, so a trim here
441            // would make `grid(char)` over `"ab\na b\n"` count two cells in
442            // both rows and report a genuinely ragged input as a clean 2x2
443            // grid with `b` shifted into the space's slot.
444            let at = region.start();
445            let Some(next) = region.next_scalar(i, at) else {
446                return Err(ParseFail::at(at.offset(), 0, what));
447            };
448            let text = region
449                .subregion(at, next)
450                .str(i)
451                .ok_or_else(|| ParseFail::at(at.offset(), 0, what))?;
452            let ch = text
453                .chars()
454                .next()
455                .ok_or_else(|| ParseFail::at(at.offset(), 0, what))?;
456            Ok(Walked {
457                value: rt.alloc_char(ch as u32),
458                next,
459            })
460        }
461        AtomicKind::Word => {
462            let (word, len) = take_word_run(s);
463            if word.is_empty() {
464                return Err(ParseFail::at(at.offset(), 0, what));
465            }
466            let slice = rt
467                .alloc_text_slice(i.owner(), i.owner_offset(at.offset()), len)
468                .ok_or_else(|| ParseFail::at(at.offset(), len, what))?;
469            Ok(Walked {
470                value: slice,
471                next: at.advance(len),
472            })
473        }
474        AtomicKind::UInt => {
475            // §7.4's `uint`. Its **type** is `Int` (`ScalarType::UInt` is
476            // reserved and has no runtime object); the non-negativity is this
477            // rule: a leading `-` is not a `uint`, it is a parse failure.
478            if s.first() == Some(&b'-') {
479                return Err(ParseFail::at(at.offset(), 1, what));
480            }
481            let (digits, len) = take_int_run(s);
482            if digits.is_empty() {
483                return Err(ParseFail::at(at.offset(), 0, what));
484            }
485            let value: i64 = digits
486                .parse()
487                .map_err(|_| ParseFail::at(at.offset(), len, what))?;
488            Ok(Walked {
489                value: rt.alloc_int(value),
490                next: at.advance(len),
491            })
492        }
493        AtomicKind::Float => {
494            let (text, len) = take_float_run(s);
495            if text.is_empty() {
496                return Err(ParseFail::at(at.offset(), 0, what));
497            }
498            let value: f64 = text
499                .parse()
500                .map_err(|_| ParseFail::at(at.offset(), len, what))?;
501            Ok(Walked {
502                value: rt.alloc_float(value),
503                next: at.advance(len),
504            })
505        }
506        AtomicKind::Byte => {
507            // A decimal integer in `0..=255`, not a raw input byte: a raw byte
508            // cannot be re-sliced as `Text` without breaking the UTF-8
509            // invariant every source-slice `Text` relies on.
510            let (digits, len) = take_int_run(s);
511            if digits.is_empty() {
512                return Err(ParseFail::at(at.offset(), 0, what));
513            }
514            let value: u8 = digits
515                .parse()
516                .map_err(|_| ParseFail::at(at.offset(), len, what))?;
517            Ok(Walked {
518                value: rt.alloc_byte(value),
519                next: at.advance(len),
520            })
521        }
522        AtomicKind::Identifier => {
523            // §4.1's identifier class, not a local ASCII rule. §7.4 says
524            // "ASCII-like … by default"; accepting fewer names than the
525            // language itself declares would be the narrower mistake.
526            let len = take_ident_run(s);
527            if len == 0 {
528                return Err(ParseFail::at(at.offset(), 0, what));
529            }
530            let slice = rt
531                .alloc_text_slice(i.owner(), i.owner_offset(at.offset()), len)
532                .ok_or_else(|| ParseFail::at(at.offset(), len, what))?;
533            Ok(Walked {
534                value: slice,
535                next: at.advance(len),
536            })
537        }
538        AtomicKind::Text | AtomicKind::Rest => {
539            // `text`/`rest` consume the rest of **the region**, not the rest of
540            // the buffer: running to `bytes.len()` would let a `text` capture
541            // swallow the literal that follows it, making every
542            // `pre{body:text}post` template unmatchable. Leading whitespace is
543            // part of the text.
544            //
545            // The two kinds share a rule but not a name: `what` is the keyword
546            // the program actually wrote.
547            let start = region.start();
548            let len = region.end().delta_from(start);
549            let slice = rt
550                .alloc_text_slice(i.owner(), i.owner_offset(start.offset()), len)
551                .ok_or_else(|| ParseFail::at(start.offset(), len, what))?;
552            Ok(Walked {
553                value: slice,
554                next: region.end(),
555            })
556        }
557    }
558}
559
560// ---- constructors (§7.5) --------------------------------------------------
561
562/// The kind of bound a [`walk_exact`] caller computed, for the mismatch it
563/// names.
564///
565/// A closed set rather than a free-form `&'static str`, so each description is
566/// spelled once for the dozen call sites that name one. The strings are user
567/// visible: the book's fault reference (`docs/book/src/input/faults.md`)
568/// tabulates them verbatim against the constructor that raises each.
569#[derive(Clone, Copy)]
570enum ExactBound {
571    Line,
572    Section,
573    Token,
574    Field,
575    Capture,
576    Fill,
577}
578
579impl ExactBound {
580    /// The description [`ParseFail`] reports after `expected `.
581    const fn describe(self) -> &'static str {
582        match self {
583            ExactBound::Line => "the rest of the line",
584            ExactBound::Section => "the rest of the section",
585            ExactBound::Token => "the rest of the token",
586            ExactBound::Field => "the rest of the field",
587            ExactBound::Capture => "the rest of the capture",
588            ExactBound::Fill => "the rest of the fill",
589        }
590    }
591}
592
593/// Walk `node` against `region` and require it to consume the region **exactly**.
594///
595/// §7.5's rule for a bounded construct is that "each application must consume
596/// the entire line" (and the same for a section, a CSV field, a
597/// whitespace-delimited token, a matrix cell).
598///
599/// Returning a bare `GcRef` is the point: there is no cursor left for a caller
600/// to forget to check, so "I bounded the child but did not require it to fill
601/// the bound" stops being expressible.
602///
603/// **What the child leaves is whitespace, or it is a mismatch** — the *bound*
604/// half of the rule stated in [`cursor`]. §7.4 puts "surrounding horizontal
605/// space" on the caller, and this is the caller for every bounded construct
606/// there is: a line, a section, a CSV field, a `ws`/`sep` token, a matrix cell,
607/// a template capture. The rule lives in this one place, so no two constructs
608/// can disagree about a leftover space.
609///
610/// It is deliberately the child's answer and not the region's. `int` cannot
611/// read `"1 "`'s trailing space, so the space is padding; `char` reads it as a
612/// cell, so `grid(char)` over `"ab\ncd \n"` is a **ragged grid** — a complaint
613/// about the data, not about a file convention. The same answer covers the
614/// shape next door: `grid(char)` over `"ab\ncd\n  \n"` is three rows, because a
615/// trailing line of spaces is offered too (`cursor::trailing_blank_run`) and
616/// `char` reads it.
617///
618/// And it is only what the child *leaves*: `lines(int)` over `"12junk"` faults,
619/// because `"junk"` is not whitespace, which is what this check exists for.
620///
621/// # Safety
622/// `ctx` must be live and wired.
623unsafe fn walk_exact(
624    rt: &Rt,
625    i: &Input<'_>,
626    plan: &ParserPlan,
627    node: u32,
628    region: ByteRegion,
629    what: ExactBound,
630) -> Result<GcRef, ParseFail> {
631    // SAFETY: forwarded from this function's contract.
632    let walked = unsafe { walk(rt.ctx, i, plan, node, region)? };
633    if walked.next != region.end() && !region.from(walked.next).is_all_whitespace(i) {
634        return Err(ParseFail::at(
635            walked.next.offset(),
636            region.end().delta_from(walked.next),
637            what.describe(),
638        ));
639    }
640    Ok(walked.value)
641}
642
643/// The text a region spans, or a parse failure naming `what`.
644///
645/// A region of a validated [`Input`] can only fail this by splitting a scalar,
646/// which is an interpreter bug; it is reported as a parse failure rather than
647/// asserted, because this runs inside `extern "C"`. Substituting an empty `str`
648/// for an unconvertible region would answer a mismatch with a zero-row,
649/// zero-width `Grid`.
650fn region_str<'a>(
651    i: &Input<'a>,
652    region: ByteRegion,
653    what: &'static str,
654) -> Result<&'a str, ParseFail> {
655    region
656        .str(i)
657        .ok_or_else(|| ParseFail::at(region.start().offset(), region.len(), what))
658}
659
660/// The whitespace-delimited tokens of `region`, whose text is `s`, as absolute
661/// subregions.
662///
663/// Bounds are computed while splitting rather than recovered afterwards by
664/// searching the region for the token's text, which would map every duplicate
665/// token to the first occurrence.
666fn whitespace_tokens(region: ByteRegion, s: &str) -> Vec<ByteRegion> {
667    let base = region.start();
668    let mut out = Vec::new();
669    let mut start: Option<usize> = None;
670    for (idx, ch) in s.char_indices() {
671        if ch.is_whitespace() {
672            if let Some(st) = start.take() {
673                out.push(region.subregion(base.advance(st), base.advance(idx)));
674            }
675        } else if start.is_none() {
676            start = Some(idx);
677        }
678    }
679    if let Some(st) = start {
680        out.push(region.subregion(base.advance(st), region.end()));
681    }
682    out
683}
684
685/// The comma-separated fields of `region`, whose text is `s`, as absolute
686/// subregions. A field runs from one comma to the next, **untrimmed**.
687///
688/// §7.5's csv entry says "ignore horizontal whitespace around each comma".
689/// Implementing that with `str::trim()` on every field would decide about
690/// whitespace *without asking the field's parser*, the one thing §7.5's rule
691/// forbids — and `trim()` eats vertical whitespace too, which is more than the
692/// entry authorises.
693///
694/// The entry's promise is kept by the rule instead: `walk_csv` hands each field
695/// to [`walk_exact`], `int` (like every atomic §7.4 puts surrounding space on
696/// the caller for) skips leading horizontal whitespace itself, and the bound
697/// half forgives a leftover run that is all whitespace. So `csv(int)` over
698/// `" 1, 2, 3"` reads three ints, and `csv(char)` over `"a, ,c"` reads three
699/// characters — one of them a space, because `char` reads spaces everywhere
700/// else too.
701///
702/// An empty field yields an **empty region**.
703fn csv_tokens(region: ByteRegion, s: &str) -> Vec<ByteRegion> {
704    let base = region.start();
705    let mut out = Vec::new();
706    let mut field_start = 0usize;
707    for (idx, ch) in s.char_indices() {
708        if ch == ',' {
709            out.push(region.subregion(base.advance(field_start), base.advance(idx)));
710            field_start = idx + ch.len_utf8();
711        }
712    }
713    out.push(region.subregion(base.advance(field_start), region.end()));
714    out
715}
716
717/// Parse one grid row: apply the cell parser from the row's start until the row
718/// is consumed, appending each cell to `items`. Returns the row's cell count.
719///
720/// **A cell is whatever the cell parser reads.** §7.5's `grid` examples are
721/// `grid(char)` and `grid(digit)`, and `digit` exists *for* the
722/// one-digit-per-cell case — if `grid(int)` meant that too, `digit` would name
723/// nothing. So a cell parser inside `grid` parses a cell exactly as it would
724/// parse anywhere else: `char` is one scalar, `digit` is one digit, `int` is an
725/// integer token.
726///
727/// The row is exactly consumed by construction: the cell is bounded to the row,
728/// so it cannot overshoot, and the loop only ends at the row's end or on the
729/// cell parser's own failure.
730///
731/// # Safety
732/// `ctx` must be live and wired.
733unsafe fn walk_grid_row(
734    rt: &Rt,
735    i: &Input<'_>,
736    plan: &ParserPlan,
737    child: u32,
738    line: ByteRegion,
739    items: &mut Vec<GcRef>,
740    scope: &NativeScope<'_>,
741) -> Result<usize, ParseFail> {
742    let mut cells = 0usize;
743    let mut cursor = line.start();
744    while cursor < line.end() {
745        // SAFETY: forwarded from this function's contract.
746        let walked = match unsafe { walk(rt.ctx, i, plan, child, line.from(cursor)) } {
747            Ok(walked) => walked,
748            Err(fail) => {
749                // **A trailing run the cell parser cannot read is padding, not
750                // a cell** — `walk_exact`'s bound rule, in the second loop that
751                // is not `walk_exact`-shaped, through the same predicate.
752                // Trailing spaces are ordinary in real input, and `matrix(int)`
753                // already drops them (`whitespace_tokens` never emits an empty
754                // token); without this rule `grid(int)` would fault on the very
755                // same file. §7.5 asks only that every row have the same cell
756                // count.
757                //
758                // A cell parser that *can* read the run never gets here:
759                // `grid(char)` reads a space as a space, which is what keeps a
760                // char grid positional — and is why `grid(char)` over
761                // `"ab\ncd \n"` is a ragged grid rather than a 2x2 one.
762                if line.from(cursor).is_all_whitespace(i) {
763                    break;
764                }
765                return Err(fail);
766            }
767        };
768        if walked.next <= cursor {
769            // A cell parser that reads nothing would loop forever. `text`/`rest`
770            // over an empty tail is the shape that gets here.
771            return Err(ParseFail::at(cursor.offset(), 0, "a cell that reads input"));
772        }
773        scope.root(walked.value);
774        items.push(walked.value);
775        cursor = walked.next;
776        cells += 1;
777    }
778    Ok(cells)
779}
780
781/// **The uniform-row rule (§7.5), stated where it is enforced.** Every row holds
782/// the same count as the first, and the fault names **the row that broke it** —
783/// the line's own region, never the region the constructor was handed. Returns
784/// the width to carry forward.
785///
786/// Two constructors enforce this, so the rule is stated once here. What the
787/// count *counts* is the caller's — cells for `grid`, whitespace tokens for
788/// `matrix` — and so is `expected`; where the fault points is not. ADR-078
789/// consequence 2 and §7.11 say a fault names the position parsing broke at.
790///
791/// It answers the width rather than taking `&mut Option<usize>` so that a caller
792/// cannot check the rule and forget to record the first row's width: the check
793/// **is** how the width is obtained.
794fn uniform_row_width(
795    first: Option<usize>,
796    count: usize,
797    line: ByteRegion,
798    expected: &'static str,
799) -> Result<usize, ParseFail> {
800    match first {
801        None => Ok(count),
802        Some(w) if w != count => Err(ParseFail::at(line.start().offset(), line.len(), expected)),
803        Some(w) => Ok(w),
804    }
805}
806
807fn walk_lines(
808    rt: &Rt,
809    i: &Input<'_>,
810    plan: &ParserPlan,
811    child: u32,
812    region: ByteRegion,
813) -> WalkResult {
814    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
815    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
816    // scope opened deeper covers everything its callers hold too.
817    // SAFETY: ctx is live and outlives this scope.
818    let scope = unsafe { NativeScope::new(rt.ctx) };
819    let mut items = Vec::new();
820    let lines = split_lines(i, region);
821    // The trailing run of blank lines is *offered* like any other line; what
822    // happens to it is the child's answer (`cursor`'s rule, bound half).
823    let blank_run = trailing_blank_run(i, &lines);
824    for (n, line) in lines.iter().enumerate() {
825        // One line, consumed exactly.
826        // SAFETY: ctx is valid (upheld by `walk`'s caller).
827        match unsafe { walk_exact(rt, i, plan, child, *line, ExactBound::Line) } {
828            Ok(value) => {
829                scope.root(value);
830                items.push(value);
831            }
832            // A trailing line of nothing but whitespace the child makes nothing
833            // of belongs to nobody: `lines(int)` over `"1\n2\n  \n"` is two
834            // elements. The child is asked rather than the line being deleted
835            // before anyone sees it — deleting it would also delete it for the
836            // children that *can* read it, and `lines(rest)` losing a line is
837            // `rest`'s identity property failing one level up. So
838            // `lines(rest)` and `lines(char)` keep it.
839            Err(fail) => {
840                if n < blank_run {
841                    return Err(fail);
842                }
843            }
844        }
845    }
846    let elem_desc = child_descriptor(plan, child);
847    Ok(Walked {
848        value: rt.alloc_vec(elem_desc, items),
849        next: region.end(),
850    })
851}
852
853fn walk_sections(
854    rt: &Rt,
855    i: &Input<'_>,
856    plan: &ParserPlan,
857    child: u32,
858    region: ByteRegion,
859) -> WalkResult {
860    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
861    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
862    // scope opened deeper covers everything its callers hold too.
863    // SAFETY: ctx is live and outlives this scope.
864    let scope = unsafe { NativeScope::new(rt.ctx) };
865    let mut items = Vec::new();
866    for section in split_sections(i, region) {
867        // A **narrowing of the same buffer**, not a re-slice walked at offset
868        // zero: the child's offsets are the input's own, so a `word` in section
869        // 2 slices the bytes it actually matched.
870        // SAFETY: ctx is valid.
871        let value = unsafe { walk_exact(rt, i, plan, child, section, ExactBound::Section)? };
872        scope.root(value);
873        items.push(value);
874    }
875    let elem_desc = child_descriptor(plan, child);
876    Ok(Walked {
877        value: rt.alloc_vec(elem_desc, items),
878        next: region.end(),
879    })
880}
881
882/// Walk named heterogeneous `sections(name: P, ..., tail: repeated(P))` (§7.5).
883/// The region is split on blank lines into sections, and the named arguments
884/// consume them **through one cursor, in source order**: a
885/// `SectionItemNode::One` takes the section at the cursor, a
886/// `SectionItemNode::Counted` takes its count's worth and collects them into a
887/// `Vec`, and the unbounded `repeated(P)` tail — if there is one — takes
888/// whatever the cursor has not reached. The result is an anonymous record
889/// assembled via [`alloc_record`].
890///
891/// The cursor is what makes a counted group followable: a rule of "the fields
892/// take `sections[0..fields.len()]` and the tail takes the rest" cannot express
893/// a field that wants six sections, let alone one that wants six and is
894/// followed by another field.
895fn walk_sections_named(
896    rt: &Rt,
897    i: &Input<'_>,
898    plan: &ParserPlan,
899    fields: &'static [SectionItemNode],
900    repeated_tail: Option<(&'static str, u32)>,
901    field_order: &'static [&'static str],
902    region: ByteRegion,
903) -> WalkResult {
904    let sections = split_sections(i, region);
905    // Too few sections is a parse fault, for a counted group exactly as for a
906    // fixed field: a group of six that finds four is input that did not match
907    // the parser, not a `Vec` of four. Truncating it silently would be the one
908    // outcome no program can notice, since the whole point of writing the count
909    // is that the program knows how many there are.
910    let required: usize = fields.iter().map(SectionItemNode::sections_wanted).sum();
911    if sections.len() < required {
912        return Err(ParseFail::at(
913            region.start().offset(),
914            region.len(),
915            sections_shortfall(fields, sections.len()),
916        ));
917    }
918    // Each section is a narrowing of the input, so the child's offsets are the
919    // input's own offsets and a source-slice `Text` is right by construction.
920    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
921    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
922    // scope opened deeper covers everything its callers hold too.
923    // SAFETY: ctx is live and outlives this scope.
924    let scope = unsafe { NativeScope::new(rt.ctx) };
925    let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
926    let mut at = 0usize;
927    for item in fields {
928        match item {
929            SectionItemNode::One { name, child } => {
930                // SAFETY: ctx is valid.
931                let value =
932                    unsafe { walk_exact(rt, i, plan, *child, sections[at], ExactBound::Section)? };
933                scope.root(value);
934                captures.push((Some(*name), *child, value));
935            }
936            SectionItemNode::Counted { name, child, count } => {
937                let mut group = Vec::with_capacity(*count as usize);
938                for section in &sections[at..at + *count as usize] {
939                    // SAFETY: ctx is valid.
940                    let value =
941                        unsafe { walk_exact(rt, i, plan, *child, *section, ExactBound::Section)? };
942                    scope.root(value);
943                    group.push(value);
944                }
945                let elem_desc = child_descriptor(plan, *child);
946                let group_vec = rt.alloc_vec(elem_desc, group);
947                // Rooted before the next allocation: a collection between two
948                // sections would otherwise drop the Vec this field *is*.
949                scope.root(group_vec);
950                captures.push((Some(*name), *child, group_vec));
951            }
952        }
953        at += item.sections_wanted();
954    }
955    if let Some((tail_name, tail_child)) = repeated_tail {
956        // The tail consumes every section the cursor has not reached, parsed
957        // per-section by its child into a Vec.
958        let mut tail_items = Vec::new();
959        for section in &sections[at..] {
960            // SAFETY: ctx is valid.
961            let value =
962                unsafe { walk_exact(rt, i, plan, tail_child, *section, ExactBound::Section)? };
963            scope.root(value);
964            tail_items.push(value);
965        }
966        let elem_desc = child_descriptor(plan, tail_child);
967        let tail_vec = rt.alloc_vec(elem_desc, tail_items);
968        scope.root(tail_vec);
969        // The tail field's "child" node for descriptor purposes is the tail
970        // child; its value is the assembled Vec.
971        captures.push((Some(tail_name), tail_child, tail_vec));
972    }
973    let record = alloc_record(rt, &captures, field_order);
974    Ok(Walked {
975        value: record,
976        next: region.end(),
977    })
978}
979
980/// What a `sections(...)` with too few sections was expecting, in the words
981/// [`ParseFail`] renders after "expected".
982///
983/// A call of fixed fields says `section header`, the message the book
984/// documents: every field wants one section, so "another section" is the whole
985/// of what is missing. A counted group is different — the number is written in
986/// the program, and the reader's question is *which* group came up short — so
987/// the first item the section list cannot satisfy names itself and its count.
988fn sections_shortfall(fields: &'static [SectionItemNode], available: usize) -> String {
989    let mut at = 0usize;
990    for item in fields {
991        let wanted = item.sections_wanted();
992        if at + wanted > available {
993            if let SectionItemNode::Counted { name, count, .. } = item {
994                return format!("{count} sections for `{name}`");
995            }
996            break;
997        }
998        at += wanted;
999    }
1000    "section header".to_string()
1001}
1002
1003/// Walk `block(item, ...)` (§7.5): apply sequential parsers within one region,
1004/// advancing the cursor after each. A positional named-capture template
1005/// *flattens* its fields into the block record; a named item contributes one
1006/// field. The result is a flattened anonymous record assembled via
1007/// [`alloc_record`].
1008///
1009/// Cursor model: each item is walked against a window computed from the current
1010/// cursor, and the item's returned position becomes the next cursor. Every
1011/// position in play is absolute. Line-anchoring is two questions with two
1012/// answers, and both live elsewhere: where an item *starts* is
1013/// [`skip_line_boundary`]'s, and how far it may *reach* is
1014/// [`block_item_window`]'s.
1015fn walk_block(
1016    rt: &Rt,
1017    i: &Input<'_>,
1018    plan: &ParserPlan,
1019    items: &'static [praxis_input_parser::BlockItemNode],
1020    field_order: &'static [&'static str],
1021    region: ByteRegion,
1022) -> WalkResult {
1023    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1024    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1025    // scope opened deeper covers everything its callers hold too.
1026    // SAFETY: ctx is live and outlives this scope.
1027    let scope = unsafe { NativeScope::new(rt.ctx) };
1028    let mut cursor = region.start();
1029    // Captures collected as (name, child_node_for_descriptor, value). For a
1030    // flattened positional record, we expand its fields into separate entries.
1031    let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
1032    for (n, item) in items.iter().enumerate() {
1033        // Before every item after the first, skip the line boundary. The first
1034        // item starts at the region head.
1035        if n > 0 {
1036            cursor = skip_line_boundary(i, region, cursor);
1037        }
1038        match item {
1039            praxis_input_parser::BlockItemNode::Positional { child } => {
1040                // SAFETY: ctx is valid.
1041                let walked = unsafe {
1042                    walk(
1043                        rt.ctx,
1044                        i,
1045                        plan,
1046                        *child,
1047                        block_item_window(i, plan, *child, region, cursor),
1048                    )?
1049                };
1050                scope.root(walked.value);
1051                cursor = walked.next;
1052                // If the positional produced a record (named-capture template),
1053                // flatten its fields into the block record. We detect a record
1054                // by pointer-equality of its descriptor against RECORD.
1055                if std::ptr::eq(walked.value.descriptor(), &crate::records::RECORD) {
1056                    flatten_record_into(rt, walked.value, &mut captures);
1057                }
1058                // A non-record positional (scalar) was rejected by validation
1059                // (I026); if we reach one here it contributes no field.
1060            }
1061            praxis_input_parser::BlockItemNode::Named { name, child } => {
1062                // The same window as the positional arm, deliberately: the
1063                // window is read off the item's plan node, so whether the item
1064                // carries a name has no part in it.
1065                // SAFETY: ctx is valid.
1066                let walked = unsafe {
1067                    walk(
1068                        rt.ctx,
1069                        i,
1070                        plan,
1071                        *child,
1072                        block_item_window(i, plan, *child, region, cursor),
1073                    )?
1074                };
1075                scope.root(walked.value);
1076                cursor = walked.next;
1077                captures.push((Some(name), *child, walked.value));
1078            }
1079        }
1080    }
1081    let record = alloc_record(rt, &captures, field_order);
1082    Ok(Walked {
1083        value: record,
1084        next: cursor,
1085    })
1086}
1087
1088/// **The window a `block` item is offered** (ADR-090, §7.5). A *template* item
1089/// gets the line it starts on, plus one more line for each `\n` the template
1090/// writes; every other item gets the rest of the region.
1091///
1092/// This is the one statement of the rule. Every other sequencing construct
1093/// narrows for its children — `lines` to a line, `sections` to a section, `csv`
1094/// to a field, `ws`/`sep`/`matrix` to a token — and ADR-078's thesis is that
1095/// the window is the *parent's* job. With no parent bound, a capture that is
1096/// its template's last part meets `walk_template`'s unbounded-last-part rule
1097/// and is handed the rest of the section: §7.7's own example, whose
1098/// `` `  Starting items: {items:csv(int)}` `` would feed the remaining five
1099/// lines of the monkey to `csv`, where the identical template under `lines`
1100/// reads two ints because `lines` bounded it.
1101///
1102/// **Why the split is templates and not a list of greedy constructors.** §7.2
1103/// defines a template as a description of characters *within a line*, and gives
1104/// `\n` as the template's own way of saying it spans another one — so a
1105/// template states its extent and this function reads it off. `lines`,
1106/// `sections`, `grid` and `matrix` are defined on several lines by their §7.5
1107/// entries and compute their own extent, so bounding them here would be a
1108/// second, disagreeing opinion. Any other split — "is this parser greedy?" —
1109/// would need a per-constructor table, which is the rule-in-N-places trap
1110/// ADR-078's corollary warns against.
1111///
1112/// It is a **narrowing and not a bound**: the item may stop short of the window
1113/// and `block` carries its cursor to the next item, which is how two items on
1114/// one line still work. Requiring exhaustion here ([`walk_exact`]) breaks
1115/// ``block(`a: {a:int}`, `b: {b:int}`)`` over `"a: 1 b: 2"` and every named
1116/// `lines(...)` item, which is §7.5's own `block` example.
1117///
1118/// The gap it leaves, named rather than papered over: a **non-template** greedy
1119/// item followed by another item (``block(`h:`, a: csv(int), b: word)``) still
1120/// swallows. It is a loud fault rather than a wrong answer, and closing it is
1121/// the per-constructor table above.
1122fn block_item_window(
1123    i: &Input<'_>,
1124    plan: &ParserPlan,
1125    child: u32,
1126    region: ByteRegion,
1127    cursor: Cursor,
1128) -> ByteRegion {
1129    let PlanNode::Template { parts, .. } = &plan.nodes[child as usize] else {
1130        return region.from(cursor);
1131    };
1132    let extra = parts
1133        .iter()
1134        .filter(|p| {
1135            matches!(
1136                p,
1137                praxis_input_parser::TemplatePartNode::Literal {
1138                    ws: praxis_input_parser::WsPolicy::Newline,
1139                    ..
1140                }
1141            )
1142        })
1143        .count();
1144    region.subregion(cursor, cursor::line_window_end(i, region, cursor, extra))
1145}
1146
1147/// Skip the line boundary between sequential `block` items (§7.5): any run of
1148/// horizontal whitespace, then an optional single line ending (`\n` or `\r\n`).
1149/// Returns the new cursor. If no line ending is present (e.g. the items are on
1150/// one line separated by spaces), only the horizontal whitespace is consumed.
1151///
1152/// Where the *next* item starts, and only that. How far it may then reach is
1153/// [`block_item_window`]'s — the other half of "block items are line-anchored".
1154///
1155/// Byte-wise on purpose: space, tab, CR and LF are single-byte scalars and
1156/// cannot occur inside a multi-byte one, so scanning bytes here can never land
1157/// mid-scalar. (The cell and scan loops step by scalar because *they* can.)
1158fn skip_line_boundary(i: &Input<'_>, region: ByteRegion, cursor: Cursor) -> Cursor {
1159    let tail = region.from(cursor);
1160    let bytes = tail.bytes(i);
1161    let mut n = horizontal_ws_run(bytes);
1162    if bytes.get(n) == Some(&b'\r') {
1163        n += 1;
1164    }
1165    if bytes.get(n) == Some(&b'\n') {
1166        n += 1;
1167    }
1168    cursor.advance(n)
1169}
1170
1171/// Flatten a positional record's fields into the block captures (§7.5
1172/// flattening). Reads the value's `RecordPayload` schema + items and pushes one
1173/// `(name, child_for_descriptor, value)` entry per field. The per-field
1174/// descriptor is read from the value's own header at record-format/eq/hash time,
1175/// so the `child` placeholder here is only a fallback tag.
1176fn flatten_record_into(
1177    _rt: &Rt,
1178    record_ref: GcRef,
1179    captures: &mut Vec<(Option<&'static str>, u32, GcRef)>,
1180) {
1181    let payload = record_ref.payload::<u8>() as *const crate::records::RecordPayload;
1182    // SAFETY: record_ref is a valid RECORD GcRef (descriptor checked by caller).
1183    let (schema, items) = unsafe {
1184        let p = &*payload;
1185        (p.schema, &p.items)
1186    };
1187    // SAFETY: schema is a valid RecordSchema pointer, owned by the schema cache
1188    // and live until `retire_schemas`.
1189    let schema = unsafe { &*schema };
1190    for (n, field) in schema.fields.iter().enumerate() {
1191        if let Some(value) = items.get(n) {
1192            captures.push((Some(field.name), u32::MAX, *value));
1193        }
1194    }
1195}
1196
1197/// Walk `choice(Name: P, ...)` (§7.5): try each case in source order from
1198/// the region's start. The first case whose parser succeeds wins; its value
1199/// becomes the variant's payload and the cursor advances to where that parser
1200/// stopped. If a case fails, the next case is tried from the same start
1201/// (backtracking). If no case matches, this is a parse fault.
1202///
1203/// `choice` does **not** require its region to be exhausted. Whether a match
1204/// must fill its region is the bounded parent's question — `lines(choice(…))`
1205/// requires it through `walk_exact`, `scan(choice(…))` matches fragments by
1206/// design — and answering it in one place is what makes both correct.
1207///
1208/// Backtracking note: a failed case may have allocated GC objects (since `walk`
1209/// allocates eagerly); those are unreferenced and collected later. Only the
1210/// cursor is restored — there is no allocator rollback, which is fine because
1211/// failed allocations are simply garbage.
1212fn walk_choice(
1213    rt: &Rt,
1214    i: &Input<'_>,
1215    plan: &ParserPlan,
1216    cases: &'static [(&'static str, u32)],
1217    region: ByteRegion,
1218) -> WalkResult {
1219    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1220    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1221    // scope opened deeper covers everything its callers hold too.
1222    // SAFETY: ctx is live and outlives this scope.
1223    let scope = unsafe { NativeScope::new(rt.ctx) };
1224    let mut deepest: Option<ParseFail> = None;
1225    for (tag, (_name, child)) in cases.iter().enumerate() {
1226        // SAFETY: ctx is valid.
1227        match unsafe { walk(rt.ctx, i, plan, *child, region) } {
1228            Ok(walked) => {
1229                // First match wins. Tag with this case's index; the value is
1230                // the single payload slot, rooted across `alloc_enum`.
1231                scope.root(walked.value);
1232                let schema = enum_schema_for(cases);
1233                let enum_ref = rt.alloc_enum(schema, tag as u32, vec![walked.value]);
1234                return Ok(Walked {
1235                    value: enum_ref,
1236                    next: walked.next,
1237                });
1238            }
1239            Err(inner) => {
1240                // Backtrack, **keeping the deepest case failure**. The deepest
1241                // failure is the most specific one — it is the same rule
1242                // `ParseDetail::consider` applies across a whole parse — and a
1243                // case that got further is the case the input was trying to be.
1244                // Discarding them for a generic message at the choice's own
1245                // offset would make §7.11's detail name the outermost construct
1246                // and point where nothing had gone wrong yet.
1247                let deeper = match &deepest {
1248                    None => true,
1249                    Some(best) => inner.input_span.0 > best.input_span.0,
1250                };
1251                if deeper {
1252                    deepest = Some(inner);
1253                }
1254            }
1255        }
1256    }
1257    // A choice with no cases has no case failure to report; that is the only
1258    // shape the generic message describes honestly.
1259    Err(deepest.unwrap_or_else(|| ParseFail::at(region.start().offset(), 0, "any choice case")))
1260}
1261
1262/// Walk `optional(P)` (§7.5): parse `P`; on success return `Some(value)`
1263/// (Option tag 0) advancing the cursor, on failure return `None` (tag 1) and
1264/// consume NO input (the cursor stays at the region's start). No fault is
1265/// raised on a miss — this is parser-level optionality, not exception recovery.
1266fn walk_optional(
1267    rt: &Rt,
1268    i: &Input<'_>,
1269    plan: &ParserPlan,
1270    child: u32,
1271    region: ByteRegion,
1272) -> WalkResult {
1273    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1274    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1275    // scope opened deeper covers everything its callers hold too.
1276    // SAFETY: ctx is live and outlives this scope.
1277    let scope = unsafe { NativeScope::new(rt.ctx) };
1278    // SAFETY: ctx is valid.
1279    match unsafe { walk(rt.ctx, i, plan, child, region) } {
1280        Ok(walked) => {
1281            // Rooted across `alloc_enum`, which paces.
1282            scope.root(walked.value);
1283            let some_ref = rt.alloc_enum(crate::enums::option_schema(), 0, vec![walked.value]);
1284            Ok(Walked {
1285                value: some_ref,
1286                next: walked.next,
1287            })
1288        }
1289        Err(_) => {
1290            // Consume nothing; return None (tag 1, no payload). The inner
1291            // failure is intentionally swallowed — `optional` is parser-level
1292            // optionality, not exception recovery.
1293            let none_ref = rt.alloc_enum(crate::enums::option_schema(), 1, Vec::new());
1294            Ok(Walked {
1295                value: none_ref,
1296                next: region.start(),
1297            })
1298        }
1299    }
1300}
1301
1302/// Walk `scan(P)` (§7.5): slide a cursor across the region; at each position
1303/// try `P`. On success, push the value and advance past the match
1304/// (so overlapping matches aren't found); on failure, advance one position.
1305/// All unmatched text is ignored. Returns `Vec[result(P)]` in source order.
1306fn walk_scan(
1307    rt: &Rt,
1308    i: &Input<'_>,
1309    plan: &ParserPlan,
1310    child: u32,
1311    region: ByteRegion,
1312) -> WalkResult {
1313    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1314    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1315    // scope opened deeper covers everything its callers hold too.
1316    // SAFETY: ctx is live and outlives this scope.
1317    let scope = unsafe { NativeScope::new(rt.ctx) };
1318    let mut items = Vec::new();
1319    let mut cursor = region.start();
1320    while cursor < region.end() {
1321        // SAFETY: ctx is valid.
1322        match unsafe { walk(rt.ctx, i, plan, child, region.from(cursor)) } {
1323            Ok(walked) => {
1324                // A match must advance the cursor (otherwise we'd loop forever
1325                // on a zero-width match). If it didn't, step one position.
1326                scope.root(walked.value);
1327                items.push(walked.value);
1328                cursor = if walked.next > cursor {
1329                    walked.next
1330                } else {
1331                    match region.next_scalar(i, cursor) {
1332                        Some(next) => next,
1333                        None => break,
1334                    }
1335                };
1336            }
1337            Err(_) => {
1338                cursor = match region.next_scalar(i, cursor) {
1339                    Some(next) => next,
1340                    None => break,
1341                };
1342            }
1343        }
1344    }
1345    let elem_desc = child_descriptor(plan, child);
1346    Ok(Walked {
1347        value: rt.alloc_vec(elem_desc, items),
1348        next: region.end(),
1349    })
1350}
1351
1352/// Walk `one_of("LR")` (§7.5): match one character from a literal set.
1353///
1354/// Like [`AtomicKind::Char`], it reads the scalar **at** the cursor: it is a
1355/// character class, and a class that skipped spaces before matching could not
1356/// contain one — nor could `chars(one_of(…), skip: none)` mean what it says.
1357/// A caller that wants leading space skipped has `skip:` or `walk_exact`'s token
1358/// bounds. **Not** a template's pre-capture skip: that skip *bounds* a capture
1359/// and does not feed it, so it deletes nothing before the child is offered the
1360/// bytes. Offering it as a third way would make ``lines(`{a:char}`)`` and
1361/// `lines(char)` disagree about the same file (ADR-079).
1362fn walk_one_of(rt: &Rt, i: &Input<'_>, chars: &str, region: ByteRegion) -> WalkResult {
1363    let at = region.start();
1364    let Some(next) = region.next_scalar(i, at) else {
1365        return Err(ParseFail::at(at.offset(), 0, "char"));
1366    };
1367    let ch = region
1368        .subregion(at, next)
1369        .str(i)
1370        .and_then(|t| t.chars().next())
1371        .ok_or_else(|| ParseFail::at(at.offset(), 0, "char"))?;
1372    if !chars.contains(ch) {
1373        return Err(ParseFail::at(
1374            at.offset(),
1375            ch.len_utf8(),
1376            format!("one of \"{chars}\""),
1377        ));
1378    }
1379    Ok(Walked {
1380        value: rt.alloc_char(ch as u32),
1381        next,
1382    })
1383}
1384
1385/// Walk `chars(P, skip:)` (§7.5): apply a char-parser repeatedly, trimming
1386/// between matches per the skip policy.
1387fn walk_characters(
1388    rt: &Rt,
1389    i: &Input<'_>,
1390    plan: &ParserPlan,
1391    child: u32,
1392    skip: praxis_input_parser::SkipPolicy,
1393    region: ByteRegion,
1394) -> WalkResult {
1395    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1396    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1397    // scope opened deeper covers everything its callers hold too.
1398    // SAFETY: ctx is live and outlives this scope.
1399    let scope = unsafe { NativeScope::new(rt.ctx) };
1400    let mut items = Vec::new();
1401    let mut cursor = region.start();
1402    loop {
1403        cursor = skip_chars(i, region, cursor, skip);
1404        if cursor >= region.end() {
1405            break;
1406        }
1407        // **What is left is whitespace, or the child's failure is the parse's
1408        // failure.** Breaking on the failure half instead would return `Ok` at
1409        // the first mismatch and silently drop the rest of the region —
1410        // `chars(digit)` over `"12x34"` answering `[1, 2]` and reporting
1411        // nothing.
1412        //
1413        // The whitespace half is `walk_exact`'s bound rule, in the one loop
1414        // that is not `walk_exact`-shaped: `chars` has no bound to fill, it
1415        // consumes until the region runs out. Without it, whether §7.5's own
1416        // `chars(one_of("^v<>"), skip: whitespace)` could read an ordinary file
1417        // came down to whether its skip policy happened to include line endings
1418        // — and `whitespace` is horizontal whitespace, so it did not. It is
1419        // only what is *left*: `chars(digit, skip: none)` over `"1\n2"` still
1420        // faults, because `"\n2"` is not whitespace. And it is asked *after*
1421        // the child, so a character parser that can read whitespace still reads
1422        // it — `chars(one_of(" "))` counts spaces rather than skipping them.
1423        // SAFETY: ctx is valid.
1424        let walked = match unsafe { walk(rt.ctx, i, plan, child, region.from(cursor)) } {
1425            Ok(walked) => walked,
1426            Err(fail) => {
1427                if region.from(cursor).is_all_whitespace(i) {
1428                    break;
1429                }
1430                return Err(fail);
1431            }
1432        };
1433        cursor = if walked.next > cursor {
1434            walked.next
1435        } else {
1436            match region.next_scalar(i, cursor) {
1437                Some(next) => next,
1438                None => break,
1439            }
1440        };
1441        scope.root(walked.value);
1442        items.push(walked.value);
1443    }
1444    // The element descriptor is the child's, not a hardcoded `CHAR`: a Vec
1445    // tagged `Char` whatever it held would make `chars(int, …)` a `Vec[Char]`
1446    // full of `Int` objects, with `vec_format`/`vec_equals`/`vec_hash`
1447    // dispatching through the wrong callback.
1448    let elem_desc = child_descriptor(plan, child);
1449    Ok(Walked {
1450        value: rt.alloc_vec(elem_desc, items),
1451        next: region.end(),
1452    })
1453}
1454
1455/// Skip bytes at `cursor` per the `chars` skip policy (§7.5).
1456///
1457/// **`Newlines` is the broader policy, not the narrower one.** `Whitespace`
1458/// skips spaces and tabs; `Newlines` skips those *and* line endings. The names
1459/// do not say so and the arms below look backwards to a reader who assumes
1460/// "whitespace" is the superset. In particular `skip: whitespace` cannot absorb
1461/// an input file's trailing newline, and does not have to: the terminator is
1462/// **inside** the region — the root region is the whole buffer — and it is
1463/// forgiven because it is whitespace no child read. [`walk_characters`] asks the
1464/// child first and accepts a whitespace-only leftover through
1465/// `ByteRegion::is_all_whitespace`, the bound half of `parser::cursor`'s rule.
1466/// `SkipPolicy`'s own documentation in `praxis-input-parser` carries the full
1467/// note, and `the_skip_policies_are_ordered_by_what_they_skip` pins the
1468/// inclusion so the sets cannot be quietly swapped.
1469///
1470/// Byte-wise like [`skip_line_boundary`], and sound for the same reason: every
1471/// byte it tests is ASCII whitespace, which cannot appear inside a multi-byte
1472/// scalar.
1473fn skip_chars(
1474    i: &Input<'_>,
1475    region: ByteRegion,
1476    cursor: Cursor,
1477    skip: praxis_input_parser::SkipPolicy,
1478) -> Cursor {
1479    use praxis_input_parser::SkipPolicy;
1480    let bytes = region.from(cursor).bytes(i);
1481    let n = match skip {
1482        SkipPolicy::None => 0,
1483        SkipPolicy::Whitespace => horizontal_ws_run(bytes),
1484        SkipPolicy::Newlines => ascii_ws_run(bytes),
1485    };
1486    cursor.advance(n)
1487}
1488
1489/// Walk `matrix(P)` (§7.5, ADR-030): parse lines of whitespace-separated tokens
1490/// into a rectangular `Grid[result(P)]`. Each row must have the same token
1491/// count.
1492fn walk_matrix(
1493    rt: &Rt,
1494    i: &Input<'_>,
1495    plan: &ParserPlan,
1496    child: u32,
1497    region: ByteRegion,
1498) -> WalkResult {
1499    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1500    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1501    // scope opened deeper covers everything its callers hold too.
1502    // SAFETY: ctx is live and outlives this scope.
1503    let scope = unsafe { NativeScope::new(rt.ctx) };
1504    let lines = split_lines(i, region);
1505    let blank_run = trailing_blank_run(i, &lines);
1506    // **One loop, because the offending line has to still be in scope at the
1507    // width check**: tokenizing every line first and checking afterwards leaves
1508    // the width check with only `region` to name, so a ragged `matrix` would
1509    // report the whole input where the identical `grid` rule reports the line.
1510    // The single loop is order-preserving: the first observable failure is the
1511    // earliest row's, and `region_str` can only fail on a non-scalar-boundary
1512    // region, which `split_lines` over a validated `Input` cannot produce.
1513    let mut items = Vec::with_capacity(lines.len());
1514    let mut width: Option<usize> = None;
1515    for (n, line) in lines.iter().enumerate() {
1516        let text = region_str(i, *line, "matrix row")?;
1517        let tokens = whitespace_tokens(*line, text);
1518        // A **trailing** blank line yields no tokens, so `matrix` makes nothing
1519        // of it and it belongs to nobody — the same rule `grid` and `lines`
1520        // answer from, not a `matrix` special case. Skipping *any* line that
1521        // trims to nothing, interior ones included, would be the
1522        // per-constructor whitespace exception ADR-078's corollary warns
1523        // against: `matrix(int)` would silently drop the middle of
1524        // `"1 2\n  \n3 4\n"` where `lines(int)` and `grid(digit)` fault on the
1525        // identical shape. An interior blank line is structure, so it is a
1526        // zero-token row and the width check below rejects it.
1527        if tokens.is_empty() && n >= blank_run {
1528            continue;
1529        }
1530        // Uniform in **whitespace tokens**, which is matrix's own unit — grid
1531        // counts cells. `uniform_row_width` owns the half that is not: which
1532        // span the fault names.
1533        width = Some(uniform_row_width(
1534            width,
1535            tokens.len(),
1536            *line,
1537            "rectangular matrix row",
1538        )?);
1539        for token in &tokens {
1540            // The token's own region, not its bytes copied into a fresh buffer
1541            // walked at offset zero, and consumed exactly.
1542            // SAFETY: ctx is valid.
1543            let value = unsafe { walk_exact(rt, i, plan, child, *token, ExactBound::Token)? };
1544            scope.root(value);
1545            items.push(value);
1546        }
1547    }
1548    let width = width.unwrap_or(0);
1549    let elem_desc = child_descriptor(plan, child);
1550    alloc_grid(rt, elem_desc, items, width, region.end())
1551}
1552
1553/// Walk ragged `grid(P, ragged, fill:)` (§7.5): permit uneven rows and pad
1554/// to the maximum width with the `fill` value (parsed by the cell parser).
1555fn walk_grid_ragged(
1556    rt: &Rt,
1557    i: &Input<'_>,
1558    plan: &ParserPlan,
1559    child: u32,
1560    fill: &str,
1561    region: ByteRegion,
1562) -> WalkResult {
1563    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1564    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1565    // scope opened deeper covers everything its callers hold too.
1566    // SAFETY: ctx is live and outlives this scope.
1567    let scope = unsafe { NativeScope::new(rt.ctx) };
1568    let lines = split_lines(i, region);
1569    // **The fill is not a region of the input.** It is a plan literal, so it
1570    // gets its own owned `Text` and its own `Input` — which is what makes a
1571    // sliced fill cell name the fill rather than unrelated input bytes.
1572    let fill_owner = rt.alloc_text_owned(fill);
1573    // The fill's `Text` and the value parsed out of it are both live across
1574    // every row: the value is a slice of the owner, and the padding cells all
1575    // share it.
1576    scope.root(fill_owner);
1577    // SAFETY: `alloc_text_owned` just produced a live Text.
1578    let fill_input = unsafe { Input::new(fill_owner) }
1579        .ok_or_else(|| ParseFail::at(region.start().offset(), 0, "grid fill"))?;
1580    let fill_region = fill_input.whole();
1581    // SAFETY: ctx is valid.
1582    let fill_value =
1583        unsafe { walk_exact(rt, &fill_input, plan, child, fill_region, ExactBound::Fill)? };
1584    scope.root(fill_value);
1585    // Rows are parsed first and padded second: a ragged grid's width is the
1586    // widest row's **cell count**, which is not known until the cell parser has
1587    // read them.
1588    let mut items = Vec::new();
1589    let mut rows = Vec::with_capacity(lines.len());
1590    let blank_run = trailing_blank_run(i, &lines);
1591    for (n, line) in lines.iter().enumerate() {
1592        // SAFETY: ctx is valid.
1593        let cells = unsafe { walk_grid_row(rt, i, plan, child, *line, &mut items, &scope)? };
1594        // The same rule uniform `grid` answers from: a trailing blank line the
1595        // cell parser reads no cell in is nobody's, and would otherwise be a
1596        // zero-cell row padded out to the full width with `fill`.
1597        if cells == 0 && n >= blank_run {
1598            continue;
1599        }
1600        rows.push(cells);
1601    }
1602    let width = rows.iter().copied().max().unwrap_or(0);
1603    // Pad each short row out to the width, from the back forwards so the
1604    // earlier rows' offsets stay valid while we insert.
1605    let mut at = items.len();
1606    for (n, cells) in rows.iter().enumerate().rev() {
1607        at -= cells;
1608        for _ in *cells..width {
1609            items.insert(at + cells, fill_value);
1610        }
1611        let _ = n;
1612    }
1613    let elem_desc = child_descriptor(plan, child);
1614    alloc_grid(rt, elem_desc, items, width, region.end())
1615}
1616
1617/// Allocate a `Grid` from element refs + width (shared by grid/matrix/ragged).
1618/// `next` is the position the constructor stopped at.
1619fn alloc_grid(
1620    rt: &Rt,
1621    elem_desc: &'static crate::TypeDescriptor,
1622    items: Vec<GcRef>,
1623    width: usize,
1624    next: Cursor,
1625) -> WalkResult {
1626    let payload = crate::collections::GridPayload {
1627        element_descriptor: elem_desc,
1628        items,
1629        width,
1630    };
1631    let (heap, safepoint) = rt.safepoint();
1632    // SAFETY: GridPayload is GRID's payload type.
1633    let grid_ref = unsafe { heap.alloc_payload(safepoint, &crate::collections::GRID, payload) };
1634    Ok(Walked {
1635        value: grid_ref,
1636        next,
1637    })
1638}
1639
1640fn walk_csv(
1641    rt: &Rt,
1642    i: &Input<'_>,
1643    plan: &ParserPlan,
1644    child: u32,
1645    region: ByteRegion,
1646) -> WalkResult {
1647    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1648    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1649    // scope opened deeper covers everything its callers hold too.
1650    // SAFETY: ctx is live and outlives this scope.
1651    let scope = unsafe { NativeScope::new(rt.ctx) };
1652    let text = region_str(i, region, "csv")?;
1653    let mut items = Vec::new();
1654    for token in csv_tokens(region, text) {
1655        // The field's own region, consumed exactly.
1656        // SAFETY: ctx is valid.
1657        let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Field)? };
1658        scope.root(value);
1659        items.push(value);
1660    }
1661    let elem_desc = child_descriptor(plan, child);
1662    Ok(Walked {
1663        value: rt.alloc_vec(elem_desc, items),
1664        next: region.end(),
1665    })
1666}
1667
1668/// Walk `ws(P)` (§7.5): split on whitespace and apply `P` to each token.
1669///
1670/// **A whitespace-delimited token contains no whitespace.** §7.5 says `ws`
1671/// splits "on one or more spaces or tabs", which names the *separator*; it does
1672/// not say a `\n` may sit inside a token, and nothing could want it to.
1673/// Splitting on spaces and tabs alone would run a token through a line ending,
1674/// making `read ws(int)` over `"1 2\n3 4\n"` three tokens — `1`, `2\n3`, `4\n`
1675/// — the middle of which faults. A line terminator is not `ws`'s separator but
1676/// it is still a token terminator, which is the rule [`whitespace_tokens`]
1677/// applies for `matrix`; sharing that splitter is what stops the two
1678/// whitespace-token constructors disagreeing about one file.
1679fn walk_ws(
1680    rt: &Rt,
1681    i: &Input<'_>,
1682    plan: &ParserPlan,
1683    child: u32,
1684    region: ByteRegion,
1685) -> WalkResult {
1686    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1687    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1688    // scope opened deeper covers everything its callers hold too.
1689    // SAFETY: ctx is live and outlives this scope.
1690    let scope = unsafe { NativeScope::new(rt.ctx) };
1691    let text = region_str(i, region, "whitespace-separated tokens")?;
1692    let mut items = Vec::new();
1693    for token in whitespace_tokens(region, text) {
1694        // SAFETY: ctx is valid.
1695        let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
1696        scope.root(value);
1697        items.push(value);
1698    }
1699    let elem_desc = child_descriptor(plan, child);
1700    Ok(Walked {
1701        value: rt.alloc_vec(elem_desc, items),
1702        next: region.end(),
1703    })
1704}
1705
1706fn walk_sep(
1707    rt: &Rt,
1708    i: &Input<'_>,
1709    plan: &ParserPlan,
1710    child: u32,
1711    sep: &str,
1712    region: ByteRegion,
1713) -> WalkResult {
1714    let bytes = region.bytes(i);
1715    let base = region.start();
1716    let sep_bytes = sep.as_bytes();
1717    // The loop below advances by `sep_bytes.len()` on a match, and
1718    // `starts_with(&[])` is unconditionally true — so an empty separator is an
1719    // infinite loop that allocates a value per iteration. The compiler makes
1720    // that unrepresentable (`praxis_input_parser::Separator`); this records
1721    // what the loop is relying on.
1722    debug_assert!(
1723        !sep_bytes.is_empty(),
1724        "Separator::new refuses an empty separator (IP-10): the loop below cannot advance past one"
1725    );
1726    if sep_bytes.is_empty() {
1727        return Err(ParseFail::at(base.offset(), 0, "a non-empty separator"));
1728    }
1729    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1730    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1731    // scope opened deeper covers everything its callers hold too.
1732    // SAFETY: ctx is live and outlives this scope.
1733    let scope = unsafe { NativeScope::new(rt.ctx) };
1734    let mut items = Vec::new();
1735    let mut token_start = 0usize;
1736    let mut pos = 0usize;
1737    while pos < bytes.len() {
1738        if bytes[pos..].starts_with(sep_bytes) {
1739            let token = region.subregion(base.advance(token_start), base.advance(pos));
1740            // SAFETY: ctx is valid.
1741            let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
1742            scope.root(value);
1743            items.push(value);
1744            pos += sep_bytes.len();
1745            token_start = pos;
1746        } else {
1747            pos += 1;
1748        }
1749    }
1750    // Parse the final token.
1751    if token_start < bytes.len() {
1752        let token = region.subregion(base.advance(token_start), region.end());
1753        // SAFETY: ctx is valid.
1754        let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
1755        scope.root(value);
1756        items.push(value);
1757    }
1758    let elem_desc = child_descriptor(plan, child);
1759    Ok(Walked {
1760        value: rt.alloc_vec(elem_desc, items),
1761        next: region.end(),
1762    })
1763}
1764
1765fn walk_grid(
1766    rt: &Rt,
1767    i: &Input<'_>,
1768    plan: &ParserPlan,
1769    child: u32,
1770    region: ByteRegion,
1771) -> WalkResult {
1772    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1773    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1774    // scope opened deeper covers everything its callers hold too.
1775    // SAFETY: ctx is live and outlives this scope.
1776    let scope = unsafe { NativeScope::new(rt.ctx) };
1777    let lines = split_lines(i, region);
1778    let blank_run = trailing_blank_run(i, &lines);
1779    let mut items = Vec::new();
1780    let mut width: Option<usize> = None;
1781    for (n, line) in lines.iter().enumerate() {
1782        // SAFETY: ctx is valid.
1783        let cells = unsafe { walk_grid_row(rt, i, plan, child, *line, &mut items, &scope)? };
1784        // **A trailing blank line is a row if the cell parser reads cells in
1785        // it.** `char` does, so `grid(char)` over `"ab\ncd\n  \n"` is 2x3 —
1786        // which is the same answer that makes `"ab\ncd \n"` ragged, rather than
1787        // an exception to it. `digit`/`int` read no cell there, so the line
1788        // belongs to nobody and the grid is 2x2.
1789        if cells == 0 && n >= blank_run {
1790            continue;
1791        }
1792        // Grid rows must be uniform (§7.5); uneven rows are `walk_grid_ragged`'s
1793        // job. Uniform in **cells**, which is the only measure that means the
1794        // same thing for every cell parser: `grid(char)` counts characters and
1795        // `grid(int)` counts integer tokens. That choice of unit is grid's own;
1796        // where the fault points is the shared rule, and `uniform_row_width`
1797        // owns it.
1798        width = Some(uniform_row_width(
1799            width,
1800            cells,
1801            *line,
1802            "a grid row of the same cell count as the first",
1803        )?);
1804    }
1805    let width = width.unwrap_or(0);
1806    let elem_desc = child_descriptor(plan, child);
1807    alloc_grid(rt, elem_desc, items, width, region.end())
1808}
1809
1810// ---- templates (§7.2, §7.3) -----------------------------------------------
1811
1812/// The run of template parts a capture must stop before: every part from
1813/// `index + 1` up to (not including) the next capture.
1814///
1815/// **The whole run, not its first constraining member.** §7.4 says `text`
1816/// "minimally consumes text until the following template literal can match";
1817/// what has to be able to match is everything before the next capture. That is
1818/// the only reading under which two spellings of one policy agree: §7.9 lowers
1819/// `\\s+` to its own empty-text part, so bounding by the first part alone would
1820/// stop `` lines(`{a:text}\\s+bar`) `` at the first space, where `bar` is not,
1821/// while `` lines(`{a:text} bar`) `` reads the same bytes as `a = "x y"`.
1822///
1823/// **A literal's trailing run is one of those parts.** The scanner emits the
1824/// run at a literal's *trailing* end as an empty literal carrying `SpaceRun` —
1825/// a literal has one policy slot and it sits in front of the text — so
1826/// `` `Card {id:int}: {body:rest}` `` bounds `id` by the two-part run
1827/// `[":" with no policy, "" with `SpaceRun`]`, and `body` starts after the
1828/// space rather than on it. Taking the *whole* run is what makes that work:
1829/// bounding by the `":"` alone would stop `id` in the right place and then hand
1830/// `body` the space the template wrote.
1831///
1832/// `None` means the run constrains nothing — it is empty (the next part is a
1833/// capture), or every member matches the empty string (`\\s*` and a literal
1834/// with no run in front of it: `WsPolicy::ZeroOrMore`, `WsPolicy::None`, with
1835/// no text). A capture with nothing to stop before takes the rest of its
1836/// region, which is the documented answer for a template that asks for
1837/// zero-or-more.
1838fn following_bound(
1839    parts: &[praxis_input_parser::TemplatePartNode],
1840    index: usize,
1841) -> Option<&[praxis_input_parser::TemplatePartNode]> {
1842    use praxis_input_parser::{TemplatePartNode, WsPolicy};
1843    let rest = &parts[index + 1..];
1844    let len = rest
1845        .iter()
1846        .take_while(|p| matches!(p, TemplatePartNode::Literal { .. }))
1847        .count();
1848    let run = &rest[..len];
1849    let constrains = run.iter().any(|p| match p {
1850        TemplatePartNode::Literal { text, ws } => {
1851            !text.is_empty() || !matches!(ws, WsPolicy::None | WsPolicy::ZeroOrMore)
1852        }
1853        _ => false,
1854    });
1855    constrains.then_some(run)
1856}
1857
1858/// Match the literal run `run` at `at`, returning where it ends, or `None`.
1859///
1860/// Exactly what `walk_template`'s own `Literal` arm does, in the form the bound
1861/// scan needs: a lookahead that answers "could the rest of this template's
1862/// fixed text start here?" without committing.
1863fn match_literal_run(
1864    i: &Input<'_>,
1865    region: ByteRegion,
1866    base: Cursor,
1867    bytes: &[u8],
1868    at: Cursor,
1869    run: &[praxis_input_parser::TemplatePartNode],
1870) -> Option<Cursor> {
1871    let mut cursor = at;
1872    for part in run {
1873        let praxis_input_parser::TemplatePartNode::Literal { text, ws } = part else {
1874            // `following_bound` only ever hands us literals.
1875            return None;
1876        };
1877        cursor = base.advance(consume_ws(bytes, cursor.delta_from(base), *ws)?);
1878        if !region.from(cursor).bytes(i).starts_with(text.as_bytes()) {
1879            return None;
1880        }
1881        cursor = cursor.advance(text.len());
1882    }
1883    Some(cursor)
1884}
1885
1886/// The earliest position at or after `cursor` where `run` can match — i.e.
1887/// where the capture before it must stop.
1888///
1889/// "Earliest" is what makes `text` non-greedy, and taking the position *before*
1890/// the run's leading whitespace policy runs is what keeps that whitespace out of
1891/// the capture: `` `{name:text} {v:int}` `` on `"foo 3"` stops `name` at the
1892/// space rather than inside it, because the run is a literal with empty text
1893/// and `WsPolicy::SpaceRun` whose earliest match is byte 3.
1894///
1895/// It does **not** follow that the child fills its region. For
1896/// `{a:int},{b:int}` on `"12 ,34"` the comma carries `WsPolicy::None` — a
1897/// template that writes nothing in front of a literal gets no run in front of
1898/// it — so the bound is the comma at byte 3, `a` is handed `"12 "`, and the
1899/// space is forgiven by `walk_exact` because it is whitespace `int` did not
1900/// read (ADR-078). Removing `walk_exact`'s forgiveness would make that program
1901/// fault at `2..3`.
1902///
1903/// `None` means the run does not occur in the rest of the region at all, which
1904/// is a mismatch the parts themselves will report.
1905fn capture_bound(
1906    i: &Input<'_>,
1907    region: ByteRegion,
1908    base: Cursor,
1909    cursor: Cursor,
1910    run: &[praxis_input_parser::TemplatePartNode],
1911) -> Option<Cursor> {
1912    let bytes = region.bytes(i);
1913    let mut at = cursor;
1914    loop {
1915        if match_literal_run(i, region, base, bytes, at, run).is_some() {
1916            return Some(at);
1917        }
1918        // Step by scalar, so a bound never lands inside a multi-byte character.
1919        at = region.next_scalar(i, at)?;
1920    }
1921}
1922
1923/// Interpret a backtick template against `region` (§7.2, §7.3).
1924///
1925/// Walks the `parts` in order: a `Literal` part matches its bytes (honoring the
1926/// whitespace policy), a `Capture` part recursively walks its child parser to
1927/// extract one value. Which of §7.3's four results those captures assemble into
1928/// is [`TemplateShape::of`]'s answer, read from the same `parts` — this
1929/// function does not classify them itself, and neither does
1930/// [`template_result_descriptor`], which tags the same value inside a
1931/// collection. Classifying in two places lets the two disagree (ADR-092).
1932fn walk_template(
1933    rt: &Rt,
1934    i: &Input<'_>,
1935    plan: &ParserPlan,
1936    parts: &[praxis_input_parser::TemplatePartNode],
1937    field_order: &'static [&'static str],
1938    region: ByteRegion,
1939) -> WalkResult {
1940    // Every `GcRef` this helper holds is rooted here. A `NativeScope` claims the
1941    // tail of `ctx.native_roots`, and `RuntimeRoots` scans the whole store, so a
1942    // scope opened deeper covers everything its callers hold too.
1943    // SAFETY: ctx is live and outlives this scope.
1944    let scope = unsafe { NativeScope::new(rt.ctx) };
1945    let base = region.start();
1946    let bytes = region.bytes(i);
1947    let mut cursor = base;
1948    // Capture values in field-index order. Each entry is (name, child_node,
1949    // value): the child node is kept so a multi-anon-capture tuple can build its
1950    // TupleSchema from the child result descriptors.
1951    let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
1952
1953    for (index, part) in parts.iter().enumerate() {
1954        match part {
1955            praxis_input_parser::TemplatePartNode::Literal { text, ws } => {
1956                // Honor the whitespace policy before matching the literal.
1957                let Some(after) = consume_ws(bytes, cursor.delta_from(base), *ws) else {
1958                    return Err(ParseFail::at(cursor.offset(), 0, "whitespace"));
1959                };
1960                cursor = base.advance(after);
1961                // Match the literal bytes verbatim, within the region.
1962                let lit = text.as_bytes();
1963                if !region.from(cursor).bytes(i).starts_with(lit) {
1964                    return Err(ParseFail::at(
1965                        cursor.offset(),
1966                        lit.len(),
1967                        format!("literal {:?}", text),
1968                    ));
1969                }
1970                cursor = cursor.advance(lit.len());
1971            }
1972            praxis_input_parser::TemplatePartNode::Capture {
1973                child,
1974                field_index: _,
1975                name,
1976            } => {
1977                // **The child is offered the bytes at the cursor, whitespace
1978                // and all** — a capture answers from the one rule like every
1979                // other construct (ADR-078's amended §, §7.5). The cursor is
1980                // *not* advanced past leading horizontal whitespace here:
1981                // trimming would decide about whitespace without asking the
1982                // child, so the same child on the same bytes would answer one
1983                // way as `lines(char)` and another as ``lines(`{a:char}`)``,
1984                // and a `{a:text}`/`{a:rest}` capture would lose bytes its
1985                // child reads. `walk_atomic` already puts §7.4's "surrounding
1986                // horizontal space handled by caller" where it belongs — it
1987                // trims for the numeric atomics and deliberately does not for
1988                // `char`, `text` and `rest` — so a trim here would re-impose it
1989                // one level up for exactly the children that forbid it.
1990                //
1991                // The skip applies as a *lookahead offset for the bound scan
1992                // only* (`search`, below). That is a bound question, not a
1993                // whitespace-reading one: a capture may not be bounded by its
1994                // own leading whitespace, or `` `{a:text} {v:int}` `` over
1995                // `"  foo 3"` would stop `a` at byte 0 — the following literal
1996                // run is `SpaceRun` + empty text, which matches the indent
1997                // itself — and hand `int` the word.
1998                let search = base.advance(skip_capture_ws(bytes, cursor.delta_from(base)));
1999                // **Bound the capture by the literal that follows it.** §7.4
2000                // says `text` "minimally consumes text until the following
2001                // template literal can match"; unbounded, `pre{body:text}post`
2002                // would eat its own suffix and no template with a trailing
2003                // literal could match. Done here rather than in `walk_atomic`
2004                // because it is uniform: every capture is bounded, not only the
2005                // `text` ones, which is also what stops a `word` at a `-`
2006                // without adding `-` to `word`'s delimiter set.
2007                match following_bound(parts, index) {
2008                    Some(bound) => {
2009                        match capture_bound(i, region, base, search, bound) {
2010                            Some(bound) => {
2011                                // SAFETY: ctx is valid.
2012                                let value = unsafe {
2013                                    walk_exact(
2014                                        rt,
2015                                        i,
2016                                        plan,
2017                                        *child,
2018                                        region.subregion(cursor, bound),
2019                                        ExactBound::Capture,
2020                                    )?
2021                                };
2022                                scope.root(value);
2023                                cursor = bound;
2024                                captures.push((*name, *child, value));
2025                            }
2026                            None => {
2027                                // The following part does not occur at all. Let
2028                                // the capture parse naturally so *that part*
2029                                // reports the mismatch, at the position where it
2030                                // was actually looked for.
2031                                // SAFETY: ctx is valid.
2032                                let walked =
2033                                    unsafe { walk(rt.ctx, i, plan, *child, region.from(cursor))? };
2034                                scope.root(walked.value);
2035                                cursor = walked.next;
2036                                captures.push((*name, *child, walked.value));
2037                            }
2038                        }
2039                    }
2040                    None => {
2041                        // Nothing follows, so there is nothing to stop before:
2042                        // the capture takes the rest of the region and keeps
2043                        // its own cursor. Requiring exhaustion here would fault
2044                        // every root-level template on its input's trailing
2045                        // newline; whether the region must be filled is the
2046                        // *parent's* question.
2047                        // SAFETY: ctx is valid.
2048                        let walked = unsafe { walk(rt.ctx, i, plan, *child, region.from(cursor))? };
2049                        scope.root(walked.value);
2050                        cursor = walked.next;
2051                        captures.push((*name, *child, walked.value));
2052                    }
2053                }
2054            }
2055        }
2056    }
2057
2058    // Assemble the result per §7.3, returning the position where matching
2059    // stopped so a `block(...)` parent can advance item by item (§7.5).
2060    let value = match (TemplateShape::of(parts), captures.as_slice()) {
2061        // Named captures → Record. Build the schema at runtime.
2062        (TemplateShape::Record, _) => alloc_record(rt, &captures, field_order),
2063        // One anonymous capture → the captured value itself.
2064        (TemplateShape::Scalar { .. }, [(_, _, only)]) => *only,
2065        // Two or more anonymous captures → Tuple. The schema comes from the
2066        // child result descriptors, the payload from the captured values.
2067        (TemplateShape::Tuple, _) => {
2068            let children: Vec<u32> = captures.iter().map(|(_, c, _)| *c).collect();
2069            let values: Vec<GcRef> = captures.iter().map(|(_, _, v)| *v).collect();
2070            alloc_tuple(rt, &children, plan, values)
2071        }
2072        // No captures → Unit, and so is `Scalar` paired with anything other
2073        // than exactly one captured value — a combination the classifier
2074        // cannot produce, since it counts the same captures this loop pushed.
2075        // Bound by slice pattern rather than bridged with `expect`: this runs
2076        // beneath an `extern "C"` entry point, where a panic is undefined
2077        // behaviour.
2078        _ => alloc_unit(rt),
2079    };
2080    Ok(Walked {
2081        value,
2082        next: cursor,
2083    })
2084}
2085
2086/// Where a capture's **bound scan** starts: past zero or more spaces or tabs.
2087/// Returns that position as an offset into `bytes`.
2088///
2089/// This is **not** a [`WsPolicy`](praxis_input_parser::WsPolicy): `SpaceRun` is
2090/// the one-or-more policy, and a capture may not demand leading whitespace.
2091///
2092/// It offsets the bound scan and nothing else — never the **cursor**, which is
2093/// what the child is offered. Moving the cursor would decide about whitespace
2094/// without asking the child, and `walk_atomic` already answers that question
2095/// per atomic, trimming for the numeric ones and deliberately not for `char`,
2096/// `text` and `rest`. The earliest place the following literal run may match is
2097/// *after* the capture's own leading whitespace, or a run that can match a
2098/// space run would bound every indented capture at its first byte.
2099fn skip_capture_ws(bytes: &[u8], cursor: usize) -> usize {
2100    let Some(rest) = bytes.get(cursor..) else {
2101        return cursor;
2102    };
2103    cursor + horizontal_ws_run(rest)
2104}
2105
2106/// Consume bytes at `cursor` per `ws`, returning the new cursor or `None` if the
2107/// policy is not satisfied (§7.2).
2108fn consume_ws(bytes: &[u8], cursor: usize, ws: praxis_input_parser::WsPolicy) -> Option<usize> {
2109    use praxis_input_parser::WsPolicy;
2110    let rest = bytes.get(cursor..)?;
2111    let mut i = 0;
2112    match ws {
2113        WsPolicy::None => {
2114            // The template wrote no run in front of this literal, so no run is
2115            // consumed. Without this variant every literal would claim
2116            // `SpaceRun`, and `SpaceRun` would have to accept an empty run to
2117            // compensate.
2118        }
2119        WsPolicy::SpaceRun => {
2120            // **One or more** spaces or tabs — the flexible §7.2 default, as
2121            // `WsPolicy`'s own definition states it. A literal the template
2122            // wrote no run in front of carries `None`, not this policy, so
2123            // requiring a run here cannot make a template that starts with a
2124            // literal unmatchable.
2125            i = horizontal_ws_run(rest);
2126            if i == 0 {
2127                return None;
2128            }
2129        }
2130        WsPolicy::ZeroOrMore => {
2131            i = ascii_ws_run(rest);
2132        }
2133        WsPolicy::OneOrMore => {
2134            // Literally `ZeroOrMore` plus a non-empty check, sharing the same
2135            // run so the two cannot drift apart.
2136            i = ascii_ws_run(rest);
2137            if i == 0 {
2138                return None;
2139            }
2140        }
2141        WsPolicy::ExactSpace => {
2142            if rest.first() == Some(&b' ') {
2143                i = 1;
2144            } else {
2145                return None;
2146            }
2147        }
2148        WsPolicy::Newline => {
2149            // Match `\n`, optionally preceded by `\r`.
2150            if rest.first() == Some(&b'\r') {
2151                i = 1;
2152            }
2153            if rest.get(i) == Some(&b'\n') {
2154                i += 1;
2155            } else {
2156                return None;
2157            }
2158        }
2159        WsPolicy::Tab => {
2160            if rest.first() == Some(&b'\t') {
2161                i = 1;
2162            } else {
2163                return None;
2164            }
2165        }
2166    }
2167    Some(cursor + i)
2168}
2169
2170/// Allocate a `Unit` sentinel.
2171fn alloc_unit(rt: &Rt) -> GcRef {
2172    // SAFETY: ctx is valid.
2173    unsafe { (*rt.ctx).unit_ref }
2174}
2175
2176/// Allocate a record from named captures (§7.3). Builds (and caches) the
2177/// `RecordSchema` from the capture names + the child result descriptors, and
2178/// fills the payload with the captured values. The schema is owned by the cache
2179/// below, not leaked.
2180fn alloc_record(
2181    rt: &Rt,
2182    captures: &[(Option<&'static str>, u32, GcRef)],
2183    field_order: &'static [&'static str],
2184) -> GcRef {
2185    // **The record is laid out in `field_order`, not in capture order** (§5.6,
2186    // ADR-152). An anonymous record's identity is its field-name set, so a
2187    // second parser naming the same fields in another order builds the *same
2188    // type* — and a field read compiles to a slot index against that one type's
2189    // definition. Assembling in capture order would put `w` in `h`'s slot for
2190    // whichever spelling the compiler did not make canonical, and the read
2191    // would answer the wrong field with no error anywhere.
2192    //
2193    // Build the schema fields. Named captures only (the record case requires
2194    // every capture to have a name in well-formed input; anonymous ones in a
2195    // named template are a parser-validation concern, treated as `_` here).
2196    //
2197    // Each field's descriptor is taken from the CAPTURED VALUE's own header
2198    // (`value.descriptor()`). record_equals/format/hash dispatch through the
2199    // schema's per-field descriptor (records.rs), so it must match the value's
2200    // real type — hardcoding INT here miscompares/misformats/segsfaults on any
2201    // non-Int field (Text, Char, nested record, …) because the INT callback
2202    // reinterprets the foreign payload as an i64.
2203    let ordered = canonical_captures(captures, field_order);
2204    let fields: Vec<crate::records::RecordField> = ordered
2205        .iter()
2206        .map(|(name, _child, value)| crate::records::RecordField {
2207            name: name.unwrap_or("_"),
2208            descriptor: value.descriptor(),
2209        })
2210        .collect();
2211    let schema = record_schema_for(fields);
2212    let items: Vec<GcRef> = ordered.iter().map(|(_, _, v)| *v).collect();
2213    let payload = crate::records::RecordPayload { schema, items };
2214    let (heap, safepoint) = rt.safepoint();
2215    // SAFETY: RecordPayload is RECORD's payload type.
2216    unsafe { heap.alloc_payload(safepoint, &crate::records::RECORD, payload) }
2217}
2218
2219/// `captures` permuted into `field_order`, borrowed unchanged when they already
2220/// agree.
2221///
2222/// They almost always do: a program with one spelling of a shape gets its own
2223/// order back, which is the whole of `SourceOrder`'s case and nearly all of a
2224/// compile's. So the common path is one name comparison per field and no
2225/// allocation, and the copy is paid only by the spelling that lost.
2226///
2227/// An empty `field_order` means the plan node builds no record — a tuple or
2228/// scalar template — and the captures stand as they are.
2229fn canonical_captures<'a>(
2230    captures: &'a [(Option<&'static str>, u32, GcRef)],
2231    field_order: &'static [&'static str],
2232) -> std::borrow::Cow<'a, [(Option<&'static str>, u32, GcRef)]> {
2233    let agrees = field_order.len() == captures.len()
2234        && captures
2235            .iter()
2236            .zip(field_order)
2237            .all(|((name, _, _), want)| *name == Some(*want));
2238    if agrees || field_order.is_empty() {
2239        return std::borrow::Cow::Borrowed(captures);
2240    }
2241    // A name in `field_order` that no capture carries cannot happen — the order
2242    // was computed from these same names — but the walk is written to be total
2243    // rather than to assert: this runs beneath an `extern "C"` entry point,
2244    // where a panic is undefined behaviour. A capture left over keeps its place
2245    // at the end, so no field is ever dropped.
2246    let mut ordered: Vec<(Option<&'static str>, u32, GcRef)> = Vec::with_capacity(captures.len());
2247    for want in field_order {
2248        if let Some(c) = captures
2249            .iter()
2250            .find(|(name, _, _)| *name == Some(*want) && !ordered.iter().any(|o| o.0 == *name))
2251        {
2252            ordered.push(*c);
2253        }
2254    }
2255    for c in captures {
2256        if !ordered.iter().any(|o| o.0 == c.0) {
2257            ordered.push(*c);
2258        }
2259    }
2260    std::borrow::Cow::Owned(ordered)
2261}
2262
2263/// Allocate a tuple from positional capture values (§7.3). Builds (and caches)
2264/// the `TupleSchema` from the element descriptors and fills the payload. The
2265/// schema is owned by the cache below, not leaked.
2266fn alloc_tuple(rt: &Rt, elements: &[u32], plan: &ParserPlan, values: Vec<GcRef>) -> GcRef {
2267    let descriptors: Vec<*const crate::TypeDescriptor> = elements
2268        .iter()
2269        .map(|&e| child_descriptor(plan, e) as *const _)
2270        .collect();
2271    let schema = tuple_schema_for(descriptors);
2272    let payload = crate::tuples::TuplePayload {
2273        schema,
2274        items: values,
2275    };
2276    let (heap, safepoint) = rt.safepoint();
2277    // SAFETY: TuplePayload is TUPLE's payload type.
2278    unsafe { heap.alloc_payload(safepoint, &crate::tuples::TUPLE, payload) }
2279}
2280
2281// ---- parser-built schemas --------------------------------------------------
2282//
2283// A named-capture template produces an anonymous record, and an anonymous
2284// multi-capture template produces a tuple. Both need a schema, and the
2285// interpreter is the only thing that knows the field descriptors — it learns
2286// them from the values the child plans produced. So the schemas are built here,
2287// at runtime, and cached by shape so repeated parses of one template share one.
2288//
2289// **These entries own their storage.** `Box::leak`ing them would not be merely
2290// a leak: a `RecordField::name` is a `&'static str` *borrowed from plan
2291// storage*, so a cache that outlives the plans holds dangling names. Owning
2292// them lets `retire_schemas` drop the schemas in the same breath as the plans,
2293// which is what makes reclaiming either one sound.
2294
2295/// One cached record schema and everything it points at.
2296struct RecordSchemaEntry {
2297    /// `(field name, descriptor address)` — the shape this schema serves.
2298    key: Vec<(&'static str, usize)>,
2299    /// The fields the schema borrows. Boxed so the address is stable across the
2300    /// registry `Vec`'s reallocations. Never read directly.
2301    #[allow(dead_code)]
2302    fields: Box<[crate::records::RecordField]>,
2303    schema: Box<crate::records::RecordSchema>,
2304}
2305
2306/// One cached tuple schema and everything it points at.
2307struct TupleSchemaEntry {
2308    /// The descriptor-address sequence this schema serves.
2309    key: Vec<usize>,
2310    /// The descriptors the schema borrows. See [`RecordSchemaEntry::fields`].
2311    #[allow(dead_code)]
2312    descriptors: Box<[*const crate::TypeDescriptor]>,
2313    schema: Box<crate::tuples::TupleSchema>,
2314}
2315
2316/// One cached enum schema and everything it points at.
2317struct EnumSchemaEntry {
2318    /// The case-name sequence this schema serves.
2319    key: Vec<&'static str>,
2320    /// The variant shapes the schema borrows. See [`RecordSchemaEntry::fields`].
2321    #[allow(dead_code)]
2322    variants: Box<[crate::enums::EnumVariantShape]>,
2323    /// The one-slot payload arrays each variant shape borrows.
2324    #[allow(dead_code)]
2325    payloads: Box<[*const crate::TypeDescriptor]>,
2326    schema: Box<crate::enums::EnumSchema>,
2327}
2328
2329/// The parser interpreter's schema cache.
2330#[derive(Default)]
2331struct ParserSchemas {
2332    records: Vec<RecordSchemaEntry>,
2333    tuples: Vec<TupleSchemaEntry>,
2334    enums: Vec<EnumSchemaEntry>,
2335}
2336
2337// SAFETY: the entries hold raw `*const TypeDescriptor`s into process-static
2338// descriptor data and `&'static str`s into plan storage. Nothing is mutated
2339// after construction, and every access goes through the mutex below.
2340unsafe impl Send for ParserSchemas {}
2341
2342static SCHEMAS: std::sync::Mutex<Option<ParserSchemas>> = std::sync::Mutex::new(None);
2343
2344/// Drop every schema the parser interpreter has built.
2345///
2346/// # Safety
2347/// Every schema pointer handed out must be dead — no live `RecordPayload` or
2348/// `TuplePayload` may still name one. `retire_parser_plans` is the only
2349/// intended caller and holds the [`HeapDrained`](crate::HeapDrained) proof of
2350/// exactly that.
2351pub(crate) unsafe fn retire_schemas() {
2352    *SCHEMAS
2353        .lock()
2354        .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
2355}
2356
2357/// Run `f` against the schema cache, created on first use.
2358///
2359/// The three `*_schema_for` builders below all start here, and it is the only
2360/// thing that locks: "every access goes through the mutex" is the sentence
2361/// [`ParserSchemas`]' `unsafe impl Send` rests on.
2362fn with_schemas<R>(f: impl FnOnce(&mut ParserSchemas) -> R) -> R {
2363    let mut guard = SCHEMAS
2364        .lock()
2365        .unwrap_or_else(std::sync::PoisonError::into_inner);
2366    f(guard.get_or_insert_with(ParserSchemas::default))
2367}
2368
2369/// Re-borrow cache-owned data as `'static`, which is what the schema structs
2370/// below declare their borrows to be.
2371///
2372/// # Safety
2373/// The slice must live in a box the cache entry being built owns, so that it
2374/// outlives every schema pointer handed out from that entry. [`retire_schemas`]
2375/// is what discharges the obligation.
2376unsafe fn erase_lifetime<T: 'static>(slice: &[T]) -> &'static [T] {
2377    // SAFETY: per the contract above, the owning entry outlives the borrow.
2378    unsafe { &*(slice as *const [T]) }
2379}
2380
2381/// The `RecordSchema` for a template shape, built once and shared afterwards.
2382///
2383/// Cached by the `(field-name, descriptor)` sequence. The descriptor half is
2384/// load-bearing: two templates with the same field *names* but different
2385/// capture types (e.g. `{x:int}` vs `{x:word}`) must NOT share a schema —
2386/// `alloc_record` records each field's real descriptor, and
2387/// `record_equals`/`record_format`/`record_hash` dispatch through the schema's
2388/// per-field descriptor, so a name-only cache would hand the second template
2389/// the first template's descriptor and recompare/reformat via the wrong
2390/// callback.
2391fn record_schema_for(
2392    fields: Vec<crate::records::RecordField>,
2393) -> *const crate::records::RecordSchema {
2394    with_schemas(|cache| {
2395        let key: Vec<(&'static str, usize)> = fields
2396            .iter()
2397            .map(|f| (f.name, f.descriptor as usize))
2398            .collect();
2399        if let Some(entry) = cache.records.iter().find(|e| e.key == key) {
2400            return &*entry.schema as *const _;
2401        }
2402        let fields: Box<[crate::records::RecordField]> = fields.into_boxed_slice();
2403        // SAFETY: `RecordSchema::fields` declares `&'static`, and the slice
2404        // lives in the boxed `fields` this entry owns.
2405        let borrowed = unsafe { erase_lifetime(&fields) };
2406        // A named-capture template produces an *anonymous* structural record
2407        // (§5.6): its identity is its shape, so two templates with the same
2408        // fields yield records that compare equal.
2409        let schema = Box::new(crate::records::RecordSchema {
2410            identity: crate::records::SchemaIdentity::Anonymous,
2411            fields: borrowed,
2412        });
2413        let raw: *const crate::records::RecordSchema = &*schema;
2414        cache.records.push(RecordSchemaEntry {
2415            key,
2416            fields,
2417            schema,
2418        });
2419        raw
2420    })
2421}
2422
2423/// The `EnumSchema` for a `choice`'s case list, built once and shared
2424/// afterwards, so two parses of one template produce values that compare equal.
2425///
2426/// `choice(Name: P, …)` synthesizes an **anonymous** enum (§7.5,
2427/// `synthesize::ParserAst::Choice`), so its identity is its case-name shape and
2428/// the key is that sequence.
2429///
2430/// Every payload slot is **null** — unknown. The interpreter learns a case's
2431/// value type from the value the child plan produced, never from a static type,
2432/// and a null slot says exactly that: the value's own descriptor answers, and
2433/// it is read off the object's header, so it is never wrong. The arity is still
2434/// exact (one payload per case), which is what sizes the payload.
2435fn enum_schema_for(cases: &'static [(&'static str, u32)]) -> *const crate::enums::EnumSchema {
2436    with_schemas(|cache| {
2437        let key: Vec<&'static str> = cases.iter().map(|(name, _)| *name).collect();
2438        if let Some(entry) = cache.enums.iter().find(|e| e.key == key) {
2439            return &*entry.schema as *const _;
2440        }
2441        // One unknown slot per case, in one owned array the variant shapes
2442        // borrow disjoint single-element windows of.
2443        let payloads: Box<[*const crate::TypeDescriptor]> =
2444            vec![std::ptr::null(); cases.len()].into_boxed_slice();
2445        let variants: Box<[crate::enums::EnumVariantShape]> = key
2446            .iter()
2447            .enumerate()
2448            .map(|(i, name)| {
2449                // SAFETY: the window lives in the boxed `payloads` this
2450                // entry owns.
2451                let slot = unsafe { erase_lifetime(&payloads[i..=i]) };
2452                crate::enums::EnumVariantShape {
2453                    name,
2454                    payload: slot,
2455                }
2456            })
2457            .collect::<Vec<_>>()
2458            .into_boxed_slice();
2459        // SAFETY: as the slot above, for the boxed `variants`.
2460        let borrowed = unsafe { erase_lifetime(&variants) };
2461        let schema = Box::new(crate::enums::EnumSchema {
2462            identity: crate::records::SchemaIdentity::Anonymous,
2463            variants: borrowed,
2464        });
2465        let raw: *const crate::enums::EnumSchema = &*schema;
2466        cache.enums.push(EnumSchemaEntry {
2467            key,
2468            variants,
2469            payloads,
2470            schema,
2471        });
2472        raw
2473    })
2474}
2475
2476/// The `TupleSchema` for a descriptor sequence, built once and shared
2477/// afterwards, so same-shaped tuples compare structurally equal.
2478fn tuple_schema_for(
2479    descriptors: Vec<*const crate::TypeDescriptor>,
2480) -> *const crate::tuples::TupleSchema {
2481    with_schemas(|cache| {
2482        let key: Vec<usize> = descriptors.iter().map(|p| *p as usize).collect();
2483        if let Some(entry) = cache.tuples.iter().find(|e| e.key == key) {
2484            return &*entry.schema as *const _;
2485        }
2486        let descriptors: Box<[*const crate::TypeDescriptor]> = descriptors.into_boxed_slice();
2487        // SAFETY: the slice lives in the boxed `descriptors` this entry owns.
2488        let borrowed = unsafe { erase_lifetime(&descriptors) };
2489        let schema = Box::new(crate::tuples::TupleSchema {
2490            descriptors: borrowed,
2491        });
2492        let raw: *const crate::tuples::TupleSchema = &*schema;
2493        cache.tuples.push(TupleSchemaEntry {
2494            key,
2495            descriptors,
2496            schema,
2497        });
2498        raw
2499    })
2500}
2501
2502// ---- byte-splitting helpers -----------------------------------------------
2503//
2504// `split_lines` and `split_sections` live in `cursor.rs`, because they produce
2505// positions and positions are that module's business.
2506
2507/// Skip leading horizontal whitespace (spaces and tabs).
2508fn trim_leading_ws(bytes: &[u8]) -> &[u8] {
2509    &bytes[horizontal_ws_run(bytes)..]
2510}
2511
2512/// Take a run of integer characters (optional `-` + digits), returning the text
2513/// and the byte length consumed.
2514///
2515/// `pub(crate)` for `Text.int()` (ADR-136), which is the second caller and the
2516/// reason this is a shared function rather than a local one: `parse(t, int)` and
2517/// `t.int()` are two spellings of "read a number out of text", and a program
2518/// that gets different answers from them has found a defect in one of us. The
2519/// method requires the run to cover the *whole* trimmed text; the atomic stops
2520/// where the run stops and hands the rest to the template.
2521pub(crate) fn take_int_run(bytes: &[u8]) -> (&str, usize) {
2522    let mut end = 0;
2523    if end < bytes.len() && bytes[end] == b'-' {
2524        end += 1;
2525    }
2526    while end < bytes.len() && bytes[end].is_ascii_digit() {
2527        end += 1;
2528    }
2529    // SAFETY: ASCII digits are valid UTF-8.
2530    let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
2531    (s, end)
2532}
2533
2534/// Take a run of decimal floating-point characters (optional `-`, digits, an
2535/// optional `.` and fraction, an optional `e±NN` exponent), returning the text
2536/// and the byte length consumed (§7.4 `float`).
2537///
2538/// `pub(crate)` for `Text.float()`, for the reason [`take_int_run`] gives.
2539///
2540/// Note that this accepts a leading `+` and [`take_int_run`] does not. That
2541/// asymmetry is §7.4's as implemented, and it is carried into the two methods
2542/// rather than papered over there: changing an atomic's accepted set is a
2543/// change to the input language, and it wants its own decision.
2544pub(crate) fn take_float_run(bytes: &[u8]) -> (&str, usize) {
2545    let mut end = 0;
2546    if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
2547        end += 1;
2548    }
2549    let int_start = end;
2550    while end < bytes.len() && bytes[end].is_ascii_digit() {
2551        end += 1;
2552    }
2553    let mut saw_digit = end > int_start;
2554    if end < bytes.len() && bytes[end] == b'.' {
2555        let after_dot = end + 1;
2556        let mut frac = after_dot;
2557        while frac < bytes.len() && bytes[frac].is_ascii_digit() {
2558            frac += 1;
2559        }
2560        // A trailing `.` with no fraction is not part of the number: `1.` in
2561        // `1.` is a `1` followed by a literal `.` the template may need.
2562        if frac > after_dot {
2563            saw_digit = true;
2564            end = frac;
2565        }
2566    }
2567    if !saw_digit {
2568        return ("", 0);
2569    }
2570    // An exponent only counts if it is complete; `1e` is `1` followed by `e`.
2571    if end < bytes.len() && (bytes[end] == b'e' || bytes[end] == b'E') {
2572        let mut exp = end + 1;
2573        if exp < bytes.len() && (bytes[exp] == b'-' || bytes[exp] == b'+') {
2574            exp += 1;
2575        }
2576        let digits_start = exp;
2577        while exp < bytes.len() && bytes[exp].is_ascii_digit() {
2578            exp += 1;
2579        }
2580        if exp > digits_start {
2581            end = exp;
2582        }
2583    }
2584    // SAFETY: every byte accepted above is ASCII.
2585    let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
2586    (s, end)
2587}
2588
2589/// Take an identifier run under §4.1's **one** character class, returning the
2590/// byte length consumed (§7.4 `identifier`).
2591///
2592/// Zero if the run does not start one. Invalid UTF-8 simply ends the run —
2593/// `identifier` produces a source-slice `Text`, whose invariant is that its
2594/// bytes are valid UTF-8.
2595fn take_ident_run(bytes: &[u8]) -> usize {
2596    // Scan as far as the input decodes; a bad byte simply ends the run.
2597    let s = match std::str::from_utf8(bytes) {
2598        Ok(s) => s,
2599        Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or_default(),
2600    };
2601    praxis_syntax::ident::ident_run_len(s)
2602}
2603
2604/// Take a run of word characters (non-whitespace, non-delimiter).
2605fn take_word_run(bytes: &[u8]) -> (&str, usize) {
2606    let mut end = 0;
2607    while end < bytes.len()
2608        && !is_ws(bytes[end])
2609        && bytes[end] != b','
2610        && bytes[end] != b'\n'
2611        && bytes[end] != b'\r'
2612    {
2613        end += 1;
2614    }
2615    let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
2616    (s, end)
2617}
2618
2619/// Horizontal whitespace: space and tab, and nothing else.
2620fn is_ws(b: u8) -> bool {
2621    b == b' ' || b == b'\t'
2622}
2623
2624/// Length of the leading run of horizontal whitespace in `bytes`.
2625///
2626/// The one spelling of that run for the whole module — `skip_line_boundary`,
2627/// `skip_chars`'s `Whitespace` arm, `skip_capture_ws`, `consume_ws`'s
2628/// `SpaceRun` arm and `trim_leading_ws` all ask here. They are one lexical
2629/// class, not five policies, and the policies differ only in what they *do*
2630/// with the count (advance a cursor, require it non-empty, slice at it).
2631///
2632/// Byte-wise on purpose: space and tab are single-byte scalars and cannot occur
2633/// inside a multi-byte one, so scanning bytes can never land mid-scalar. Every
2634/// caller depends on that. (The cell and scan loops step by scalar because
2635/// *they* can.)
2636fn horizontal_ws_run(bytes: &[u8]) -> usize {
2637    bytes.iter().take_while(|&&b| is_ws(b)).count()
2638}
2639
2640/// Length of the leading run of ASCII whitespace in `bytes` — [`is_ws`]'s class
2641/// *plus* line endings, and so always at least [`horizontal_ws_run`].
2642///
2643/// That inclusion is the point: `SkipPolicy::Newlines` is the broader policy and
2644/// `WsPolicy::{ZeroOrMore, OneOrMore}` the broader runs, which is easy to get
2645/// backwards from the names alone. See [`skip_chars`] for the full note and the
2646/// test that pins the ordering.
2647fn ascii_ws_run(bytes: &[u8]) -> usize {
2648    bytes.iter().take_while(|b| b.is_ascii_whitespace()).count()
2649}
2650
2651/// Determine the element descriptor for a child plan node's *result* type. A
2652/// constructor `lines(P)` produces `Vec[result(P)]`, so its element descriptor is
2653/// the descriptor of `result(P)`.
2654///
2655/// Because the collection descriptors (`VEC`, `GRID`) are **uniform** — the
2656/// per-instance element type lives in the payload, not the descriptor — a nested
2657/// constructor's result descriptor is just `VEC`/`GRID` regardless of how deep
2658/// the nesting goes. The payload chain carries the inner element types, so
2659/// `vec_format`/`vec_equals`/`vec_hash` recurse correctly through it. Collapsing
2660/// the subtree to its leaf atomic instead would mis-tag every intermediate
2661/// Vec/Grid — a silent mis-dispatch in any nested-collection format/eq/hash.
2662///
2663/// `RECORD`, `ENUM` and `TUPLE` are uniform in exactly the same way: one
2664/// descriptor for every shape, with the `RecordSchema`/`EnumSchema`/
2665/// `TupleSchema` in the payload. So every arm below answers a fixed descriptor
2666/// or recurses; none of them ever needs to *construct* one.
2667fn child_descriptor(plan: &ParserPlan, child: u32) -> &'static crate::TypeDescriptor {
2668    match &plan.nodes[child as usize] {
2669        // Atomics produce their scalar.
2670        PlanNode::Atomic { kind } => atomic_descriptor(*kind),
2671        // Collection constructors produce a Vec (lines/sections/csv/ws/sep) or a
2672        // Grid. Uniform descriptors — the element type is in the payload.
2673        PlanNode::Lines { .. }
2674        | PlanNode::Sections { .. }
2675        | PlanNode::Csv { .. }
2676        | PlanNode::Ws { .. }
2677        | PlanNode::Sep { .. }
2678        | PlanNode::Scan { .. } => &crate::collections::VEC,
2679        PlanNode::Grid { .. } => &crate::collections::GRID,
2680        // Named sections produce an anonymous record (uniform descriptor; the
2681        // schema is in the payload, built at runtime by `walk_sections_named`).
2682        PlanNode::SectionsNamed { .. } => &crate::records::RECORD,
2683        // A block produces a flattened anonymous record (uniform descriptor).
2684        PlanNode::Block { .. } => &crate::records::RECORD,
2685        // choice/optional produce an enum (uniform descriptor; tag + payload).
2686        PlanNode::Choice { .. } | PlanNode::Optional { .. } => &crate::enums::ENUM,
2687        // one_of produces a Char; chars produces a Vec[Char].
2688        PlanNode::OneOf { .. } => &scalars::CHAR,
2689        PlanNode::Characters { .. } => &crate::collections::VEC,
2690        // matrix / ragged grid produce a Grid.
2691        PlanNode::Matrix { .. } | PlanNode::GridRagged { .. } => &crate::collections::GRID,
2692        // A template's result is one of §7.3's four shapes, decided by the same
2693        // classifier that assembles the value.
2694        PlanNode::Template { parts, .. } => template_result_descriptor(plan, parts),
2695    }
2696}
2697
2698/// The scalar descriptor for an atomic kind.
2699///
2700/// Which kinds share a descriptor is [`AtomicClass::of`]'s decision, not this
2701/// function's, and it is the same decision `synthesize::atomic_type` reads for
2702/// the *static* type — `uint` is an `Int` on both sides because
2703/// `ScalarType::UInt` has no runtime object to describe (§7.4). Two copies of
2704/// that grouping are precisely how a descriptor comes to disagree with the type
2705/// behind it.
2706fn atomic_descriptor(kind: AtomicKind) -> &'static crate::TypeDescriptor {
2707    match AtomicClass::of(kind) {
2708        AtomicClass::Int => &scalars::INT,
2709        AtomicClass::Float => &scalars::FLOAT,
2710        AtomicClass::Byte => &scalars::BYTE,
2711        AtomicClass::Char => &scalars::CHAR,
2712        AtomicClass::Text => &crate::text::TEXT,
2713    }
2714}
2715
2716/// The descriptor of a template's *result*, which is the tag a collection built
2717/// from that template carries for its elements. One arm per §7.3 shape, from
2718/// the same [`TemplateShape::of`] that decides the value in [`walk_template`].
2719///
2720/// **The tuple arm is a fixed descriptor, not a constructed one** (ADR-092).
2721/// There is nothing to construct: `TUPLE` is uniform like `VEC` and `RECORD`,
2722/// and the per-shape `TupleSchema` lives in the payload (`tuples.rs`), where
2723/// `alloc_tuple` interns it. Tagging ``read lines(`{int},{int}`)``'s elements
2724/// anything else would hold real tuples in a mistagged `Vec`, print them
2725/// through the wrong format callback, and compare them unequal to the same Vec
2726/// built with `push`, because `vec_equals` bails on unequal element tags before
2727/// comparing an element.
2728fn template_result_descriptor(
2729    plan: &ParserPlan,
2730    parts: &[praxis_input_parser::TemplatePartNode],
2731) -> &'static crate::TypeDescriptor {
2732    match TemplateShape::of(parts) {
2733        TemplateShape::Unit => &scalars::UNIT,
2734        // One anonymous capture → the child's own result descriptor.
2735        //
2736        // Not a guessed default: this is the tag a *collection* carries for its
2737        // elements, and `vec_format`, `vec_equals` and `vec_hash` dispatch
2738        // through exactly that tag (ADR-078 Decision 5). A fixed `&scalars::INT`
2739        // here would give `lines(`{word}`)` a `Vec` of `Text` objects whose
2740        // element descriptor says `Int`, rendering a `Text` payload through the
2741        // `Int` callback.
2742        //
2743        // Deriving it is correct because a capture names its own parser body.
2744        TemplateShape::Scalar { child } => child_descriptor(plan, child),
2745        TemplateShape::Record => &crate::records::RECORD,
2746        TemplateShape::Tuple => &crate::tuples::TUPLE,
2747    }
2748}
2749
2750#[cfg(test)]
2751mod tests {
2752    use super::*;
2753
2754    fn test_plan(nodes: Vec<PlanNode>, root: u32) -> ParserPlan {
2755        ParserPlan {
2756            nodes: Box::leak(nodes.into_boxed_slice()),
2757            template_parts: &[],
2758            literals: &[],
2759            root,
2760        }
2761    }
2762
2763    // `split_lines`/`split_sections` are covered by `cursor.rs`'s own tests:
2764    // they produce `ByteRegion`s over an `Input`, so their gates live beside
2765    // the types whose invariants they establish.
2766
2767    /// **ADR-136.** `t.int()`/`t.float()` and `parse(t, int)`/`parse(t, float)`
2768    /// read the same set, because they run the same scanner.
2769    ///
2770    /// This is the gate on that claim, and it is written as the *difference from
2771    /// the obvious implementation*: every row below is a text where
2772    /// `i64::from_str`/`f64::from_str` disagrees with §7.4's atomic, so a
2773    /// rewrite in terms of `from_str` turns each of them red.
2774    ///
2775    /// `abi::whole_trimmed` is the method side; it requires the run to cover the
2776    /// whole trimmed text, which is the only difference between a method and an
2777    /// atomic (an atomic hands the rest of the line to its template).
2778    #[test]
2779    fn a_method_and_an_atomic_read_the_same_number() {
2780        fn whole(s: &str, run: fn(&[u8]) -> (&str, usize)) -> bool {
2781            let t = s.trim();
2782            let (text, len) = run(t.as_bytes());
2783            !text.is_empty() && len == t.len()
2784        }
2785
2786        // `from_str` takes these; §7.4 does not, so neither does the method.
2787        for s in ["+5", "1_000"] {
2788            assert!(s.parse::<i64>().is_ok() || s == "1_000");
2789            assert!(!whole(s, take_int_run), "`{s}` is not an `int`");
2790        }
2791        for s in ["1.", "inf", "infinity", "nan", "NaN", "-inf"] {
2792            assert!(s.parse::<f64>().is_ok(), "`{s}` is a Rust float");
2793            assert!(!whole(s, take_float_run), "`{s}` is not a §7.4 `float`");
2794        }
2795
2796        // …and the ordinary spellings are read by both, trimmed.
2797        for s in ["12", " -7 ", "0"] {
2798            assert!(whole(s, take_int_run), "`{s}` is an `int`");
2799        }
2800        for s in ["1.5", " -2 ", "+5.0", "1e10", "3"] {
2801            assert!(whole(s, take_float_run), "`{s}` is a `float`");
2802        }
2803
2804        // A run that stops short is a rejection for the method and a *partial*
2805        // read for the atomic — the one place the two differ, stated directly.
2806        assert_eq!(take_int_run(b"12abc"), ("12", 2));
2807        assert!(!whole("12abc", take_int_run));
2808    }
2809
2810    #[test]
2811    fn take_int_run_parses_negative() {
2812        let (s, len) = take_int_run(b"-42abc");
2813        assert_eq!(s, "-42");
2814        assert_eq!(len, 3);
2815    }
2816
2817    /// §7.4's ten atomic parsers all exist at runtime: every kind parses
2818    /// something and has a descriptor, and `uint`, `float`, `byte` and
2819    /// `identifier` mean what §7.4 says they mean.
2820    ///
2821    /// The type half is in `praxis-input-parser`'s `synthesize`; the closed-set
2822    /// half is `atomic_round_trips_keywords` in its `ast.rs`.
2823    #[test]
2824    fn every_atomic_the_design_requires_has_a_parser_and_a_type() {
2825        /// Parse `input` with one atomic and return the consumed length, or
2826        /// `None` on a parse failure.
2827        fn parse_one(kind: AtomicKind, input: &str) -> Option<(crate::Runtime, GcRef, usize)> {
2828            let mut rt = crate::Runtime::new();
2829            let text = rt.alloc_text(input);
2830            let mut ctx = rt.context();
2831            ctx.input_source = text;
2832            let plan = test_plan(vec![PlanNode::Atomic { kind }], 0);
2833            let i = unsafe { Input::new(text) }.expect("a Text is UTF-8");
2834            let out = unsafe { walk(&mut ctx, &i, &plan, plan.root, i.whole()) };
2835            out.ok().map(|w| (rt, w.value, w.next.offset()))
2836        }
2837
2838        // Every kind has a descriptor. Exhaustive by `ALL`, so a new atomic
2839        // cannot be added without one.
2840        for kind in AtomicKind::ALL {
2841            let _ = atomic_descriptor(*kind);
2842        }
2843
2844        // `uint` is an Int and refuses a leading `-` — the non-negativity is
2845        // the parse rule, because `ScalarType::UInt` has no runtime object.
2846        let (_rt, v, consumed) = parse_one(AtomicKind::UInt, "42rest").expect("uint reads 42");
2847        assert_eq!(v.as_int(), 42);
2848        assert_eq!(consumed, 2);
2849        assert!(
2850            parse_one(AtomicKind::UInt, "-1").is_none(),
2851            "`uint` refuses a negative"
2852        );
2853        // …and `int` still accepts it, so the two are different rules.
2854        let (_rt, v, _) = parse_one(AtomicKind::Int, "-1").expect("int reads -1");
2855        assert_eq!(v.as_int(), -1);
2856
2857        // `float`.
2858        for (input, expected, consumed) in [
2859            ("3.5", 3.5_f64, 3),
2860            ("-0.25x", -0.25, 5),
2861            ("2", 2.0, 1),
2862            ("1e3", 1000.0, 3),
2863            ("1.5e-2", 0.015, 6),
2864            // A trailing `.` is not part of the number: the template may need it.
2865            ("7.", 7.0, 1),
2866        ] {
2867            let (_rt, v, got) = parse_one(AtomicKind::Float, input)
2868                .unwrap_or_else(|| panic!("float reads {input}"));
2869            assert_eq!(v.as_float(), expected, "for {input}");
2870            assert_eq!(got, consumed, "for {input}");
2871        }
2872        assert!(parse_one(AtomicKind::Float, "x").is_none());
2873
2874        // `byte` is a decimal integer in 0..=255 — not a raw input byte, which
2875        // could not be re-sliced as Text without breaking the UTF-8 invariant.
2876        let (_rt, v, _) = parse_one(AtomicKind::Byte, "255").expect("byte reads 255");
2877        assert_eq!(v.as_byte(), 255);
2878        assert!(
2879            parse_one(AtomicKind::Byte, "256").is_none(),
2880            "256 is not a byte"
2881        );
2882        assert!(
2883            parse_one(AtomicKind::Byte, "-1").is_none(),
2884            "-1 is not a byte"
2885        );
2886
2887        // `identifier` uses §4.1's one class, so a Unicode name is a name, and
2888        // the run stops where an identifier stops.
2889        for (input, expected) in [
2890            ("name rest", "name"),
2891            ("λx-1", "λx"),
2892            ("_x9=2", "_x9"),
2893            ("日本語:", "日本語"),
2894        ] {
2895            let (_rt, v, _) = parse_one(AtomicKind::Identifier, input)
2896                .unwrap_or_else(|| panic!("identifier reads {input}"));
2897            assert_eq!(v.as_text(), expected, "for {input}");
2898        }
2899        assert!(
2900            parse_one(AtomicKind::Identifier, "9x").is_none(),
2901            "a digit does not start an identifier"
2902        );
2903    }
2904
2905    #[test]
2906    fn text_slices_in_later_sections_point_at_their_actual_source_bytes() {
2907        let mut rt = crate::Runtime::new();
2908        let input = rt.alloc_text("first\n\nsecond");
2909        let mut ctx = rt.context();
2910        ctx.input_source = input;
2911        let plan = test_plan(
2912            vec![
2913                PlanNode::Atomic {
2914                    kind: AtomicKind::Word,
2915                },
2916                PlanNode::Sections { child: 0 },
2917            ],
2918            1,
2919        );
2920
2921        let result =
2922            unsafe { run_root(&mut ctx, &plan, input) }.expect("sections(word) should parse");
2923        let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
2924
2925        assert_eq!(values, vec!["first", "second"]);
2926    }
2927
2928    /// **The owner of a slice is the buffer that was *parsed*,** not whatever
2929    /// the context happens to call its input: `parse(text, P)` hands the
2930    /// interpreter a `Text` that is not `ctx.input_source`.
2931    #[test]
2932    fn a_parse_of_a_non_input_text_owns_its_slices() {
2933        let mut rt = crate::Runtime::new();
2934        // The context's input is one buffer…
2935        let stdin_buffer = rt.alloc_text("XXXXXXXXXXXXXXXX");
2936        // …and the thing being parsed is a different one.
2937        let subject = rt.alloc_text("alpha beta");
2938        let mut ctx = rt.context();
2939        ctx.input_source = stdin_buffer;
2940        let plan = test_plan(
2941            vec![
2942                PlanNode::Atomic {
2943                    kind: AtomicKind::Word,
2944                },
2945                PlanNode::Ws { child: 0 },
2946            ],
2947            1,
2948        );
2949
2950        let result = unsafe { run_root(&mut ctx, &plan, subject) }.expect("ws(word) should parse");
2951        let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
2952
2953        assert_eq!(
2954            values,
2955            vec!["alpha", "beta"],
2956            "a parse's slices must be views of the text it parsed, not of ctx.input_source"
2957        );
2958    }
2959
2960    /// **A parse of a slice does not extend the owner chain.**
2961    ///
2962    /// `parse(t, P)` takes its owner from the argument, and that argument may
2963    /// itself be a slice. Naming it directly makes every produced `Text` one
2964    /// link longer than the last, and `text_bytes` walks the chain on every
2965    /// read — so `t = parse(t, rest)` in a loop would go quadratic and
2966    /// eventually overflow the stack. `Input::new` resolves to the root owned
2967    /// `Text` and carries the base offset, so a slice of a slice is not
2968    /// constructible from here however deep the argument was.
2969    #[test]
2970    fn a_parse_of_a_slice_does_not_extend_the_owner_chain() {
2971        let mut rt = crate::Runtime::new();
2972        let owned = rt.alloc_text("XXalpha betaXX");
2973        // The subject is a *slice* of the owned text: bytes [2, 12).
2974        // SAFETY: `owned` is the live Text allocated above.
2975        let subject = unsafe { rt.alloc_text_slice(owned, 2, 10) }.expect("[2, 12) is in range");
2976        assert_eq!(subject.as_text(), "alpha beta");
2977        let mut ctx = rt.context();
2978        ctx.input_source = owned;
2979        let plan = test_plan(
2980            vec![
2981                PlanNode::Atomic {
2982                    kind: AtomicKind::Word,
2983                },
2984                PlanNode::Ws { child: 0 },
2985            ],
2986            1,
2987        );
2988
2989        let result = unsafe { run_root(&mut ctx, &plan, subject) }.expect("ws(word) should parse");
2990        let items: Vec<GcRef> = result.as_vec().to_vec();
2991        let values: Vec<&str> = items.iter().map(GcRef::as_text).collect();
2992        assert_eq!(
2993            values,
2994            vec!["alpha", "beta"],
2995            "the base offset must be applied, or the slices name the wrong bytes"
2996        );
2997
2998        for item in items {
2999            // SAFETY: each item is a live Text produced by the parse.
3000            let payload = unsafe {
3001                &*(item.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload)
3002            };
3003            let crate::text::TextPayload::Slice(slice) = payload else {
3004                panic!("a `word` is a source slice");
3005            };
3006            // SAFETY: a slice's owner is a live Text.
3007            let owner = unsafe {
3008                &*(slice.owner().payload::<crate::text::TextPayload>()
3009                    as *const crate::text::TextPayload)
3010            };
3011            assert!(
3012                owner.is_owned(),
3013                "a parse of a slice must still name the ROOT owned text, not another slice"
3014            );
3015        }
3016    }
3017
3018    #[test]
3019    fn unicode_grid_cells_are_parsed_once_per_scalar() {
3020        let mut rt = crate::Runtime::new();
3021        let input = rt.alloc_text("é");
3022        let mut ctx = rt.context();
3023        ctx.input_source = input;
3024        let plan = test_plan(
3025            vec![
3026                PlanNode::Atomic {
3027                    kind: AtomicKind::Char,
3028                },
3029                PlanNode::Grid { child: 0 },
3030            ],
3031            1,
3032        );
3033
3034        let grid = unsafe { run_root(&mut ctx, &plan, input) }
3035            .expect("one Unicode scalar is one valid grid cell");
3036        let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
3037
3038        assert_eq!(payload.width, 1);
3039        assert_eq!(payload.items.len(), 1);
3040    }
3041
3042    /// **ADR-107, the parser half.** `read grid(char)` is the shape the interning
3043    /// was written for: the `char` atomic runs once per cell, so uninterned a
3044    /// 140×140 AoC map boxes 19,600 `Char`s with at most 128 distinct values.
3045    ///
3046    /// The assertion is a *count*, not a spot check, because the property is
3047    /// "the parse allocates nothing per cell" and only a count can say that. One
3048    /// object is allocated by the whole parse — the `Grid` itself, whose cells
3049    /// live in a Rust `Vec<GcRef>` rather than in the heap — and that number is
3050    /// independent of how many cells there are, which the second grid below is
3051    /// what proves. A per-cell allocation makes the first delta 10 and the
3052    /// second 26.
3053    #[test]
3054    fn a_grid_of_chars_interns_its_ascii_cells() {
3055        let mut rt = crate::Runtime::new();
3056        let input = rt.alloc_text("#.#\n.#.\n#.#");
3057        let mut ctx = rt.context();
3058        ctx.input_source = input;
3059        let plan = test_plan(
3060            vec![
3061                PlanNode::Atomic {
3062                    kind: AtomicKind::Char,
3063                },
3064                PlanNode::Grid { child: 0 },
3065            ],
3066            1,
3067        );
3068
3069        let before = rt.heap().stats().live_count;
3070        let grid = unsafe { run_root(&mut ctx, &plan, input) }.expect("a 3×3 ASCII grid parses");
3071        let after = rt.heap().stats().live_count;
3072        assert_eq!(
3073            after - before,
3074            1,
3075            "nine cells, one allocation: the Grid object and no Chars at all"
3076        );
3077
3078        let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
3079        assert_eq!(payload.items.len(), 9);
3080        let hash = rt.immortals().small_char('#' as u32).expect("ASCII");
3081        let dot = rt.immortals().small_char('.' as u32).expect("ASCII");
3082        for (n, cell) in payload.items.iter().enumerate() {
3083            // Every cell is one of exactly two objects, and they are the
3084            // runtime's own table entries rather than a cache of the parser's.
3085            let expected = if n % 2 == 0 { hash } else { dot };
3086            assert_eq!(cell.as_ptr(), expected.as_ptr(), "cell {n}");
3087        }
3088
3089        // The same shape at a different size costs the same: the delta is the
3090        // Grid, not the cells.
3091        let bigger = rt.alloc_text("#.#.#\n.#.#.\n#.#.#\n.#.#.\n#.#.#");
3092        let mut ctx = rt.context();
3093        ctx.input_source = bigger;
3094        let before = rt.heap().stats().live_count;
3095        let _ = unsafe { run_root(&mut ctx, &plan, bigger) }.expect("a 5×5 ASCII grid parses");
3096        assert_eq!(
3097            rt.heap().stats().live_count - before,
3098            1,
3099            "twenty-five cells cost exactly what nine did"
3100        );
3101    }
3102
3103    /// The branch a regression would delete. A cell outside the interned range
3104    /// is still a fresh object per cell, and still holds its own scalar.
3105    #[test]
3106    fn a_non_ascii_grid_cell_is_still_a_fresh_object() {
3107        let mut rt = crate::Runtime::new();
3108        let input = rt.alloc_text("éé");
3109        let mut ctx = rt.context();
3110        ctx.input_source = input;
3111        let plan = test_plan(
3112            vec![
3113                PlanNode::Atomic {
3114                    kind: AtomicKind::Char,
3115                },
3116                PlanNode::Grid { child: 0 },
3117            ],
3118            1,
3119        );
3120
3121        let before = rt.heap().stats().live_count;
3122        let grid = unsafe { run_root(&mut ctx, &plan, input) }.expect("two scalars, one row");
3123        assert_eq!(
3124            rt.heap().stats().live_count - before,
3125            3,
3126            "the Grid and one Char per cell — `é` is outside the interned range"
3127        );
3128
3129        let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
3130        assert_eq!(payload.items.len(), 2);
3131        assert_ne!(payload.items[0].as_ptr(), payload.items[1].as_ptr());
3132        assert_eq!(payload.items[0].as_char(), 'é');
3133        assert_eq!(payload.items[1].as_char(), 'é');
3134    }
3135
3136    /// `one_of` is the parser's *other* door to `Rt::alloc_char`, and the grid
3137    /// test does not reach it — `walk_one_of` builds its `Char` itself rather
3138    /// than through the `char` atomic.
3139    #[test]
3140    fn one_of_answers_the_interned_char() {
3141        let mut rt = crate::Runtime::new();
3142        let input = rt.alloc_text("<");
3143        let mut ctx = rt.context();
3144        ctx.input_source = input;
3145        let literals: &'static [&'static str] = Box::leak(vec!["<>^v"].into_boxed_slice());
3146        let nodes: &'static [PlanNode] =
3147            Box::leak(vec![PlanNode::OneOf { chars_index: 0 }].into_boxed_slice());
3148        let plan = ParserPlan {
3149            nodes,
3150            template_parts: &[],
3151            literals,
3152            root: 0,
3153        };
3154
3155        let before = rt.heap().stats().live_count;
3156        let value = unsafe { run_root(&mut ctx, &plan, input) }.expect("`<` is one of \"<>^v\"");
3157        assert_eq!(
3158            rt.heap().stats().live_count,
3159            before,
3160            "an interned Char never enters the live registry"
3161        );
3162        assert_eq!(
3163            value.as_ptr(),
3164            rt.immortals()
3165                .small_char('<' as u32)
3166                .expect("ASCII")
3167                .as_ptr()
3168        );
3169        assert_eq!(value.as_char(), '<');
3170    }
3171
3172    #[test]
3173    fn csv_rest_parser_is_bounded_to_each_token() {
3174        let mut rt = crate::Runtime::new();
3175        let input = rt.alloc_text("a,b");
3176        let mut ctx = rt.context();
3177        ctx.input_source = input;
3178        let plan = test_plan(
3179            vec![
3180                PlanNode::Atomic {
3181                    kind: AtomicKind::Rest,
3182                },
3183                PlanNode::Csv { child: 0 },
3184            ],
3185            1,
3186        );
3187
3188        let result = unsafe { run_root(&mut ctx, &plan, input) }.expect("csv(rest) should parse");
3189        let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
3190
3191        assert_eq!(values, vec!["a", "b"]);
3192    }
3193
3194    /// **An empty csv field is an empty `Text`, not a panic.** This runs inside
3195    /// `extern "C"`, where a panic is undefined behaviour, and `"10,20,"` is
3196    /// all it takes to produce a field that trims to nothing.
3197    #[test]
3198    fn an_empty_csv_field_does_not_panic() {
3199        let mut rt = crate::Runtime::new();
3200        let input = rt.alloc_text("10,20,");
3201        let mut ctx = rt.context();
3202        ctx.input_source = input;
3203        let plan = test_plan(
3204            vec![
3205                PlanNode::Atomic {
3206                    kind: AtomicKind::Rest,
3207                },
3208                PlanNode::Csv { child: 0 },
3209            ],
3210            1,
3211        );
3212
3213        let result = unsafe { run_root(&mut ctx, &plan, input) }
3214            .expect("an empty csv field is an empty Text, not an abort");
3215        let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
3216        assert_eq!(
3217            values,
3218            vec!["10", "20", ""],
3219            "the field after the last comma is empty, and being empty is not a panic"
3220        );
3221    }
3222
3223    // --- whitespace matcher (§7.2) -------------------------------------------
3224
3225    /// A failed `choice` reports the deepest case failure, not a generic
3226    /// `"any choice case"` at its own offset.
3227    ///
3228    /// The case that got furthest is the case the input was trying to be, and
3229    /// its own message is the one worth showing; a generic message would name
3230    /// the outermost construct and point at a byte where nothing went wrong.
3231    #[test]
3232    fn a_failed_choice_reports_the_deepest_case_failure() {
3233        fn lit(text: &'static str) -> praxis_input_parser::TemplatePartNode {
3234            praxis_input_parser::TemplatePartNode::Literal {
3235                text,
3236                ws: praxis_input_parser::WsPolicy::None,
3237            }
3238        }
3239        fn capture(child: u32) -> praxis_input_parser::TemplatePartNode {
3240            praxis_input_parser::TemplatePartNode::Capture {
3241                child,
3242                field_index: None,
3243                name: None,
3244            }
3245        }
3246
3247        let mut rt = crate::Runtime::new();
3248        // `a{int}` fails at byte 1; `ab{int}` gets one byte further and fails
3249        // at byte 2. The second is the one to report.
3250        let input = rt.alloc_text("abz");
3251        let mut ctx = rt.context();
3252        ctx.input_source = input;
3253        let short: &'static [praxis_input_parser::TemplatePartNode] =
3254            Box::leak(vec![lit("a"), capture(0)].into_boxed_slice());
3255        let long: &'static [praxis_input_parser::TemplatePartNode] =
3256            Box::leak(vec![lit("ab"), capture(0)].into_boxed_slice());
3257        let cases: &'static [(&'static str, u32)] =
3258            Box::leak(vec![("Short", 1u32), ("Long", 2u32)].into_boxed_slice());
3259        let plan = test_plan(
3260            vec![
3261                PlanNode::Atomic {
3262                    kind: AtomicKind::Int,
3263                },
3264                PlanNode::Template {
3265                    parts: short,
3266                    field_order: &[],
3267                },
3268                PlanNode::Template {
3269                    parts: long,
3270                    field_order: &[],
3271                },
3272                PlanNode::Choice { cases },
3273            ],
3274            3,
3275        );
3276
3277        let fail = unsafe { run_root(&mut ctx, &plan, input) }
3278            .expect_err("neither case can read `z` as an int");
3279        assert_eq!(
3280            fail.expected, "int",
3281            "the deepest case's own expectation, not \"any choice case\""
3282        );
3283        assert_eq!(
3284            fail.input_span.0, 2,
3285            "byte 2 is where the case that got furthest actually broke"
3286        );
3287    }
3288
3289    /// A ragged row's fault names *the row that broke it*, in both constructors
3290    /// that have the rule.
3291    ///
3292    /// **Both halves are asserted in one test on purpose.** The gate is on the
3293    /// *pair* stating one rule, so neither constructor can drift into naming
3294    /// the whole region it was handed and stay green.
3295    #[test]
3296    fn a_ragged_row_fault_names_the_row_in_grid_and_in_matrix() {
3297        let mut rt = crate::Runtime::new();
3298
3299        // `"1 2\n  \n3 4\n"`: the interior blank line is a zero-token row, and
3300        // its own bytes are 4..6.
3301        let input = rt.alloc_text("1 2\n  \n3 4\n");
3302        let mut ctx = rt.context();
3303        ctx.input_source = input;
3304        let plan = test_plan(
3305            vec![
3306                PlanNode::Atomic {
3307                    kind: AtomicKind::Int,
3308                },
3309                PlanNode::Matrix { child: 0 },
3310            ],
3311            1,
3312        );
3313        let fail = unsafe { run_root(&mut ctx, &plan, input) }
3314            .expect_err("a zero-token row is not two tokens wide");
3315        assert_eq!(fail.expected, "rectangular matrix row");
3316        assert_eq!(
3317            fail.input_span,
3318            (4, 6),
3319            "the blank line's own bytes, not the region matrix was handed"
3320        );
3321
3322        // The analogous grid, which must answer the same way.
3323        // `"12\n  \n34\n"`: the blank line is 3..5.
3324        let input = rt.alloc_text("12\n  \n34\n");
3325        let mut ctx = rt.context();
3326        ctx.input_source = input;
3327        let plan = test_plan(
3328            vec![
3329                PlanNode::Atomic {
3330                    kind: AtomicKind::Digit,
3331                },
3332                PlanNode::Grid { child: 0 },
3333            ],
3334            1,
3335        );
3336        let fail = unsafe { run_root(&mut ctx, &plan, input) }
3337            .expect_err("a zero-cell row is not two cells wide");
3338        assert_eq!(
3339            fail.expected,
3340            "a grid row of the same cell count as the first"
3341        );
3342        assert_eq!(fail.input_span, (3, 5), "the blank line's own bytes");
3343    }
3344
3345    /// **The `chars` skip policies, ordered by what they skip.**
3346    ///
3347    /// `Whitespace` is spaces and tabs; `Newlines` is those **and** line
3348    /// endings. The names imply the opposite containment — "whitespace" reads
3349    /// like the superset — which invites the conclusion that `skip: whitespace`
3350    /// can absorb an input file's trailing newline. It cannot. The sets are
3351    /// deliberately kept as they are (they are the ones §7.5's
3352    /// `chars(one_of("^v<>"), skip: whitespace)` example needs, and swapping
3353    /// them would change what every existing `skip: newlines` program accepts),
3354    /// so what has to exist instead is this: a test that states the inclusion,
3355    /// and fails if anyone quietly swaps the arms to make the names read
3356    /// straight.
3357    #[test]
3358    fn the_skip_policies_are_ordered_by_what_they_skip() {
3359        use praxis_input_parser::SkipPolicy;
3360        let rt = crate::Runtime::new();
3361        let owner = rt.alloc_text(" \t\n\r x");
3362        // SAFETY: `owner` is a Text allocated just above and `rt` outlives `i`.
3363        let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
3364        let region = i.whole();
3365        let skipped = |p| skip_chars(&i, region, region.start(), p).offset();
3366
3367        assert_eq!(skipped(SkipPolicy::None), 0, "`none` skips nothing");
3368        assert_eq!(
3369            skipped(SkipPolicy::Whitespace),
3370            2,
3371            "`whitespace` is HORIZONTAL whitespace: it stops at the newline"
3372        );
3373        assert_eq!(
3374            skipped(SkipPolicy::Newlines),
3375            5,
3376            "`newlines` is horizontal whitespace AND line endings — the broader policy"
3377        );
3378        assert!(
3379            skipped(SkipPolicy::Newlines) > skipped(SkipPolicy::Whitespace),
3380            "`newlines` must skip a superset of `whitespace`, however the two are named"
3381        );
3382        // The one description both the diagnostic and the runtime comment
3383        // quote, so the names are never the only thing a reader is given.
3384        assert_eq!(SkipPolicy::Whitespace.skips(), "spaces and tabs");
3385        assert_eq!(
3386            SkipPolicy::Newlines.skips(),
3387            "spaces, tabs and line endings"
3388        );
3389        // Sweep the closed list, so a fourth policy cannot arrive without a
3390        // description and a position in the ordering.
3391        let mut previous = 0usize;
3392        for policy in SkipPolicy::ALL.iter().copied() {
3393            assert!(
3394                !policy.skips().is_empty(),
3395                "every skip policy states what it skips"
3396            );
3397            let n = skipped(policy);
3398            assert!(
3399                n >= previous,
3400                "SkipPolicy::ALL is ordered from narrowest to broadest; {policy:?} skips {n}"
3401            );
3402            previous = n;
3403        }
3404    }
3405
3406    /// **`chars` that cannot read its whole region faults**, rather than
3407    /// returning `Ok` at the first child failure and silently dropping the
3408    /// rest: `chars(digit, skip: none)` over `"12x34"` is a parse failure, not
3409    /// `[1, 2]`.
3410    ///
3411    /// The rule §7.5 wants falls out of running the skip policy once more after
3412    /// the last match: whatever the skip does not absorb, the child must read.
3413    #[test]
3414    fn chars_that_cannot_read_the_whole_region_is_a_parse_failure() {
3415        fn parse(input: &str, skip: praxis_input_parser::SkipPolicy) -> Option<Vec<i64>> {
3416            let mut rt = crate::Runtime::new();
3417            let text = rt.alloc_text(input);
3418            let mut ctx = rt.context();
3419            ctx.input_source = text;
3420            let plan = test_plan(
3421                vec![
3422                    PlanNode::Atomic {
3423                        kind: AtomicKind::Digit,
3424                    },
3425                    PlanNode::Characters { child: 0, skip },
3426                ],
3427                1,
3428            );
3429            unsafe { run_root(&mut ctx, &plan, text) }
3430                .ok()
3431                .map(|v| v.as_vec().iter().map(GcRef::as_int).collect())
3432        }
3433
3434        use praxis_input_parser::SkipPolicy;
3435        assert_eq!(parse("1234", SkipPolicy::None), Some(vec![1, 2, 3, 4]));
3436        assert_eq!(
3437            parse("12x34", SkipPolicy::None),
3438            None,
3439            "a child failure inside the region is the parse's failure, not a short answer"
3440        );
3441        // The skip policy is what a trailing run is for, and it is applied
3442        // after the last match as well as between matches.
3443        assert_eq!(
3444            parse("1 2 3 \t", SkipPolicy::Whitespace),
3445            Some(vec![1, 2, 3])
3446        );
3447        assert_eq!(parse("1 2\n", SkipPolicy::Newlines), Some(vec![1, 2]));
3448        assert_eq!(
3449            parse("1\n2", SkipPolicy::None),
3450            None,
3451            "`skip: none` absorbs nothing, so an interior newline is a mismatch"
3452        );
3453        // **The byte at the end of an input file is the file's terminator, not
3454        // a byte the program asked any parser to read.** It IS inside the
3455        // region — the root region is the whole buffer — and `walk_characters`
3456        // forgives it because it is whitespace the child declined
3457        // (`ByteRegion::is_all_whitespace`, the bound half of `cursor`'s rule).
3458        // No skip policy has to absorb it and no root trim has to hide it;
3459        // requiring `chars` to consume it would fault every newline-terminated
3460        // file, §7.5's own `chars(one_of("^v<>"), skip: whitespace)` example
3461        // included. `parse("1\n2", None)` above states the other half: a
3462        // newline *inside* the data is still a mismatch under `skip: none`.
3463        assert_eq!(
3464            parse("12\n", SkipPolicy::None),
3465            Some(vec![1, 2]),
3466            "the file's own terminator is whitespace the child declined"
3467        );
3468    }
3469
3470    /// **A `grid` cell is whatever the cell parser reads**, so `grid(int)` reads
3471    /// one integer **token** per cell and `grid(digit)` reads one digit. §7.5's
3472    /// two examples are `grid(char)` and `grid(digit)`, and `digit` exists *for*
3473    /// the one-digit case — if `grid(int)` meant that too, `digit` would name
3474    /// nothing.
3475    ///
3476    /// Measuring width in bytes and walking the child once per byte answers
3477    /// neither semantics: over `"12\n34\n"` it yields **four** cells
3478    /// `[12, 2, 34, 4]`, the token and then the token's tail.
3479    #[test]
3480    fn a_grid_cell_is_whatever_its_cell_parser_reads() {
3481        fn cells(kind: AtomicKind, input: &str) -> Option<(usize, Vec<i64>)> {
3482            let mut rt = crate::Runtime::new();
3483            let text = rt.alloc_text(input);
3484            let mut ctx = rt.context();
3485            ctx.input_source = text;
3486            let plan = test_plan(
3487                vec![PlanNode::Atomic { kind }, PlanNode::Grid { child: 0 }],
3488                1,
3489            );
3490            let grid = unsafe { run_root(&mut ctx, &plan, text) }.ok()?;
3491            let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
3492            Some((
3493                payload.width,
3494                payload.items.iter().map(|r| r.as_int()).collect(),
3495            ))
3496        }
3497
3498        // `int` is an integer token, so each row of `"12\n34\n"` is one cell.
3499        assert_eq!(
3500            cells(AtomicKind::Int, "12\n34\n"),
3501            Some((1, vec![12, 34])),
3502            "one token per cell — not [12, 2, 34, 4], and not [1, 2, 3, 4] either"
3503        );
3504        // …and a row of several tokens is several cells.
3505        assert_eq!(
3506            cells(AtomicKind::Int, "1 2\n3 4\n"),
3507            Some((2, vec![1, 2, 3, 4]))
3508        );
3509        // `digit` is the per-digit parser, which is what it is for.
3510        assert_eq!(
3511            cells(AtomicKind::Digit, "12\n34\n"),
3512            Some((2, vec![1, 2, 3, 4])),
3513            "`digit` names the one-digit-per-cell case, so `int` must not"
3514        );
3515        // Rows must agree in **cells**, which is the only measure that means
3516        // the same thing for every cell parser.
3517        assert_eq!(
3518            cells(AtomicKind::Int, "1 2\n3\n"),
3519            None,
3520            "two cells then one is not a rectangle"
3521        );
3522    }
3523
3524    /// **`scan` advances one scalar at a time, not one byte**, so it never
3525    /// attempts a match at a continuation byte — a position that is not a
3526    /// character at all.
3527    ///
3528    /// Over `"ééé"` there are exactly three scalar starts and three
3529    /// continuation bytes. A byte-stepping `scan` visits six positions; a
3530    /// scalar-stepping one visits three, and `one_of("é")` matches at each.
3531    #[test]
3532    fn scan_advances_by_scalar_across_a_multibyte_run() {
3533        let mut rt = crate::Runtime::new();
3534        let input = rt.alloc_text("ééé");
3535        let mut ctx = rt.context();
3536        ctx.input_source = input;
3537        let literals: &'static [&'static str] = Box::leak(vec!["é"].into_boxed_slice());
3538        let nodes: &'static [PlanNode] = Box::leak(
3539            vec![
3540                PlanNode::OneOf { chars_index: 0 },
3541                PlanNode::Scan { child: 0 },
3542            ]
3543            .into_boxed_slice(),
3544        );
3545        let plan = ParserPlan {
3546            nodes,
3547            template_parts: &[],
3548            literals,
3549            root: 1,
3550        };
3551
3552        let result = unsafe { run_root(&mut ctx, &plan, input) }.expect("scan never fails");
3553        let chars: Vec<char> = result
3554            .as_vec()
3555            .iter()
3556            .map(|r| char::from_u32(unsafe { *r.payload::<u32>() }).expect("a Char"))
3557            .collect();
3558        assert_eq!(
3559            chars,
3560            vec!['é', 'é', 'é'],
3561            "three scalars, and no attempt at the three continuation bytes between them"
3562        );
3563    }
3564
3565    #[test]
3566    fn consume_ws_space_run_requires_one_or_more_spaces_or_tabs() {
3567        use praxis_input_parser::WsPolicy;
3568        assert_eq!(consume_ws(b"  ,x", 0, WsPolicy::SpaceRun), Some(2));
3569        assert_eq!(consume_ws(b"\t\t,x", 0, WsPolicy::SpaceRun), Some(2));
3570        assert_eq!(
3571            consume_ws(b"x", 0, WsPolicy::SpaceRun),
3572            None,
3573            "SpaceRun is the one-or-more policy; absence of whitespace must not match"
3574        );
3575    }
3576
3577    #[test]
3578    fn consume_ws_one_or_more_requires_at_least_one() {
3579        use praxis_input_parser::WsPolicy;
3580        assert_eq!(consume_ws(b"  x", 0, WsPolicy::OneOrMore), Some(2));
3581        assert_eq!(consume_ws(b"x", 0, WsPolicy::OneOrMore), None);
3582    }
3583
3584    #[test]
3585    fn consume_ws_exact_space_matches_one() {
3586        use praxis_input_parser::WsPolicy;
3587        assert_eq!(consume_ws(b" x", 0, WsPolicy::ExactSpace), Some(1));
3588        assert_eq!(consume_ws(b"\tx", 0, WsPolicy::ExactSpace), None);
3589    }
3590
3591    #[test]
3592    fn consume_ws_newline_matches_crlf_and_lf() {
3593        use praxis_input_parser::WsPolicy;
3594        assert_eq!(consume_ws(b"\r\nx", 0, WsPolicy::Newline), Some(2));
3595        assert_eq!(consume_ws(b"\nx", 0, WsPolicy::Newline), Some(1));
3596        assert_eq!(consume_ws(b"x", 0, WsPolicy::Newline), None);
3597    }
3598
3599    #[test]
3600    fn single_anonymous_template_capture_uses_its_child_descriptor() {
3601        let parts: &'static [praxis_input_parser::TemplatePartNode] = Box::leak(
3602            vec![praxis_input_parser::TemplatePartNode::Capture {
3603                child: 0,
3604                field_index: None,
3605                name: None,
3606            }]
3607            .into_boxed_slice(),
3608        );
3609        let nodes: &'static [PlanNode] = Box::leak(
3610            vec![
3611                PlanNode::Atomic {
3612                    kind: AtomicKind::Word,
3613                },
3614                PlanNode::Template {
3615                    parts,
3616                    field_order: &[],
3617                },
3618            ]
3619            .into_boxed_slice(),
3620        );
3621        let plan = ParserPlan {
3622            nodes,
3623            template_parts: &[],
3624            literals: &[],
3625            root: 1,
3626        };
3627
3628        assert_eq!(
3629            child_descriptor(&plan, plan.root).id(),
3630            crate::text::TEXT.id(),
3631            "lines(`{{word}}`) must carry Text as its Vec element descriptor"
3632        );
3633    }
3634
3635    /// The sibling of the test above: that one gates the *one*-capture tag,
3636    /// this one gates the *many*-capture tag (ADR-092).
3637    ///
3638    /// `Int` and `Word`, not `Int` and `Int`, on purpose: an implementation
3639    /// that reached for the first child's descriptor — the shape the
3640    /// one-capture arm has — would answer `INT` and stay red here.
3641    #[test]
3642    fn multi_anonymous_template_captures_are_a_tuple() {
3643        let parts: &'static [praxis_input_parser::TemplatePartNode] = Box::leak(
3644            vec![
3645                praxis_input_parser::TemplatePartNode::Capture {
3646                    child: 0,
3647                    field_index: Some(0),
3648                    name: None,
3649                },
3650                praxis_input_parser::TemplatePartNode::Literal {
3651                    text: ",",
3652                    ws: praxis_input_parser::WsPolicy::None,
3653                },
3654                praxis_input_parser::TemplatePartNode::Capture {
3655                    child: 1,
3656                    field_index: Some(1),
3657                    name: None,
3658                },
3659            ]
3660            .into_boxed_slice(),
3661        );
3662        let nodes: &'static [PlanNode] = Box::leak(
3663            vec![
3664                PlanNode::Atomic {
3665                    kind: AtomicKind::Int,
3666                },
3667                PlanNode::Atomic {
3668                    kind: AtomicKind::Word,
3669                },
3670                PlanNode::Template {
3671                    parts,
3672                    field_order: &[],
3673                },
3674            ]
3675            .into_boxed_slice(),
3676        );
3677        let plan = ParserPlan {
3678            nodes,
3679            template_parts: &[],
3680            literals: &[],
3681            root: 2,
3682        };
3683
3684        assert_eq!(
3685            child_descriptor(&plan, plan.root).id(),
3686            crate::tuples::TUPLE.id(),
3687            "lines(`{{int}},{{word}}`) must carry Tuple as its Vec element descriptor"
3688        );
3689    }
3690}