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 §ions[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 §ions[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}