praxis_input_parser/plan.rs
1//! Parser plans: the flat runtime representation of a parser AST.
2//!
3//! After validation and type synthesis, a [`ParserAst`] is lowered into a
4//! [`ParserPlan`] — a flat arena of [`PlanNode`]s that the runtime interpreter
5//! (`praxis-runtime::parser`) walks against the input buffer. The compiled plan
6//! is registered in a process-wide arena and identified by a
7//! [`PlanId`]; MIR passes that id as an `i64` immediate (no
8//! pointer-as-immediate needed).
9//!
10//! Design constraints:
11//! - **Flat and self-contained**: nodes reference children by **index** (no
12//! `Box`, no owned `String` on the hot path). Separators and template
13//! literals are interned into a parallel `&'static [&'static str]` slice so
14//! the runtime reads them without dereferencing Rust owned data.
15//! - Record schemas for named-capture templates are built at **runtime** (the
16//! interpreter knows the field descriptors from the child plans' result
17//! types); the plan stores only field names as `&'static str`.
18//!
19//! **Not `#[repr(C)]`.** These are ordinary Rust enums and slices with the
20//! default representation, and nothing here crosses an FFI boundary: the plan
21//! is consumed by `praxis-runtime`, which is Rust and links against this crate,
22//! and only the plan *id* is passed as a JIT immediate. If a plan ever does
23//! need to be read by generated code, that is a real representation change
24//! (explicit `#[repr(C)]`, no enums with payloads, no `&str` fat pointers) and
25//! not something to assume from this comment.
26//!
27//! # Ownership
28//!
29//! Each [`CompiledPlan`] owns a `bumpalo` arena holding everything the plan's
30//! `&'static` fields point into, so a plan is reclaimable at all; registration
31//! is bounded and checked, so a long enough compile cannot wrap the index and
32//! hand the runtime a *different* plan; and a [`PlanId`] is a `NonZeroU32`, so
33//! "no plan" is not spelled `0` — it is not spellable at all.
34//!
35//! Reclamation has the same ordering obligation as the JIT generation arena:
36//! record schemas the runtime builds for named-capture templates borrow their
37//! field names from plan storage, so plans may only be retired once the heap
38//! is drained. `praxis_runtime::retire_parser_plans` is the gate; see
39//! [`retire_all_plans`].
40
41use std::num::NonZeroU32;
42use std::sync::Mutex;
43
44use bumpalo::Bump;
45
46use crate::ast::{AtomicKind, ParserAst, SkipPolicy, TemplatePart, WsPolicy};
47
48// ===========================================================================
49// The flat plan node arena.
50// ===========================================================================
51
52/// One node in the flattened parser plan. Index-based: children refer to other
53/// nodes by their position in the `ParserPlan::nodes` slice.
54#[derive(Debug)]
55pub enum PlanNode {
56 /// An atomic parser.
57 Atomic { kind: AtomicKind },
58 /// `lines(P)`.
59 Lines { child: u32 },
60 /// `sections(P)` (homogeneous).
61 Sections { child: u32 },
62 /// Named heterogeneous `sections(name: P, ..., tail: repeated(P))`.
63 /// `fields` are the named arguments in source order, each contributing one
64 /// record field and consuming
65 /// [`SectionItemNode::sections_wanted`] sections; `repeated_tail` is the
66 /// unbounded tail's `(name, child_index)`, if present, and it consumes
67 /// every section the fields left.
68 SectionsNamed {
69 fields: &'static [SectionItemNode],
70 repeated_tail: Option<(&'static str, u32)>,
71 /// The result record's canonical field order (see [`FieldOrder`]).
72 field_order: &'static [&'static str],
73 },
74 /// `block(item, ...)` (§7.5). Sequential parsers within one region;
75 /// positional named-capture templates flatten their fields into the result
76 /// record, named items contribute one field each.
77 Block {
78 items: &'static [BlockItemNode],
79 /// The result record's canonical field order (see [`FieldOrder`]).
80 field_order: &'static [&'static str],
81 },
82 /// `choice(Name: P, ...)` (§7.5). Try each case in source order; the
83 /// first match wins and its value becomes the variant's payload. `cases`
84 /// are `(name, child_index)` pairs.
85 Choice {
86 cases: &'static [(&'static str, u32)],
87 },
88 /// `optional(P)` (§7.5). Parse `P`; on success return Some(value)
89 /// (Option tag 0), on failure consume nothing and return None (tag 1).
90 Optional { child: u32 },
91 /// `scan(P)` (§7.5). Slide a cursor; at each position try `P`; collect
92 /// matches in source order, ignoring unmatched text.
93 Scan { child: u32 },
94 /// `one_of("LR")` (§7.5). `chars_index` is into `ParserPlan::literals`.
95 OneOf { chars_index: u32 },
96 /// `chars(P, skip:)` (§7.5). Apply a char-parser repeatedly.
97 Characters { child: u32, skip: SkipPolicy },
98 /// `matrix(P)` (§7.5, ADR-030). Whitespace-tokenized rectangular Grid.
99 Matrix { child: u32 },
100 /// Ragged `grid(P, ragged, fill:)` (§7.5). `fill_index` into literals.
101 GridRagged { child: u32, fill_index: u32 },
102 /// `csv(P)`.
103 Csv { child: u32 },
104 /// `ws(P)`.
105 Ws { child: u32 },
106 /// `sep(separator_index, P)`.
107 Sep { separator_index: u32, child: u32 },
108 /// `grid(P)`.
109 Grid { child: u32 },
110 /// A backtick template. `parts` are indices into [`ParserPlan::template_parts`].
111 ///
112 /// **Every template shape lowers to this**, multi-anonymous-capture tuples
113 /// included; see [`TemplateShape`]. There is deliberately no `Tuple` node
114 /// beside it — see that type's doc for why one cannot exist.
115 Template {
116 parts: &'static [TemplatePartNode],
117 /// The result record's canonical field order (see [`FieldOrder`]), and
118 /// **empty for the shapes that are not records** — a tuple's element
119 /// order is its capture order and nothing reorders it, so there is no
120 /// second opinion for this to carry.
121 field_order: &'static [&'static str],
122 },
123}
124
125/// One part of a template, in plan form.
126#[derive(Debug)]
127pub enum TemplatePartNode {
128 /// A literal match.
129 Literal { text: &'static str, ws: WsPolicy },
130 /// A capture whose value comes from the child plan node. `field_index` is the
131 /// position in the resulting record/tuple (None for single-capture scalars).
132 Capture {
133 child: u32,
134 field_index: Option<u16>,
135 name: Option<&'static str>,
136 },
137}
138
139/// What a lowered template's parts add up to (§7.3).
140///
141/// §7.3: named captures produce an anonymous record; anonymous captures produce
142/// a scalar when there is one and a tuple when there are several. A template
143/// with no captures matches literally and produces `Unit`.
144///
145/// **Stated here, next to the parts it classifies, and asked rather than
146/// re-derived** (ADR-092). The interpreter assembles a template's value
147/// (`walk_template`) and tags a collection built from it
148/// (`template_result_descriptor`) through this one function, because answering
149/// the question separately is how the two drift: a tag that says `Unit` for the
150/// tuple shape makes ``read lines(`{int},{int}`)`` print `[Unit, Unit]` and
151/// compare unequal to an identical `Vec` built with `push`, while
152/// `praxis check` types it `Vec[(Int, Int)]` throughout.
153///
154/// **There is no `PlanNode::Tuple`, and that is not an omission.** A variant
155/// carrying only child indices cannot represent a multi-capture template,
156/// because the template's separators are `TemplatePartNode::Literal`s between
157/// the captures — `` `{int},{int}` `` would lose its comma. Widening it to hold
158/// the literals makes it `PlanNode::Template` again. So the tuple shape is a
159/// property of a `Template`'s parts, which is what this type reads, and the
160/// state "a tuple node" is unnameable rather than merely unreachable.
161///
162/// `synthesize::template_type` answers the same question for the *type*, over
163/// AST `TemplatePart`s rather than lowered ones. Keep the two in step.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum TemplateShape {
166 /// No captures: the template matches literally and produces `Unit`.
167 Unit,
168 /// One anonymous capture: the result is that capture's value, and its type
169 /// is the child parser's own result type. `child` is the child node index.
170 Scalar { child: u32 },
171 /// At least one named capture: an anonymous record, one field per capture.
172 /// Mixing named and anonymous captures in one template is rejected before
173 /// lowering (§7.3), so "any named" and "all named" are the same set here.
174 Record,
175 /// Two or more captures, none named: a tuple, one element per capture in
176 /// source order.
177 Tuple,
178}
179
180impl TemplateShape {
181 /// Classify a lowered template's parts.
182 pub fn of(parts: &[TemplatePartNode]) -> TemplateShape {
183 let mut captures = 0usize;
184 let mut any_named = false;
185 let mut sole_anonymous: Option<u32> = None;
186 for part in parts {
187 if let TemplatePartNode::Capture { child, name, .. } = part {
188 captures += 1;
189 match name {
190 Some(_) => any_named = true,
191 None => sole_anonymous = Some(*child),
192 }
193 }
194 }
195 match (any_named, captures) {
196 (true, _) => TemplateShape::Record,
197 (false, 0) => TemplateShape::Unit,
198 (false, 1) => match sole_anonymous {
199 Some(child) => TemplateShape::Scalar { child },
200 // Not reachable: one capture with none named *is* that one
201 // anonymous capture. Answering `Unit` rather than panicking
202 // keeps this total, which matters because the interpreter that
203 // calls it runs under `extern "C"`, where a panic is undefined
204 // behaviour.
205 None => TemplateShape::Unit,
206 },
207 (false, _) => TemplateShape::Tuple,
208 }
209 }
210}
211
212/// One named argument of a heterogeneous `sections(...)` other than its
213/// unbounded tail (§7.5), in plan form.
214///
215/// The count is a plain `u32` and not the [`crate::ast::RepeatCount`] newtype:
216/// the invariant is discharged upstream, where the source span is still in hand
217/// to report the violation against, and the plan is a flat `&'static` repr the
218/// runtime reads without unwrapping anything.
219#[derive(Debug)]
220pub enum SectionItemNode {
221 /// `name: P` — one section.
222 One { name: &'static str, child: u32 },
223 /// `name: repeated(P, N)` — exactly `count` consecutive sections, collected
224 /// into one `Vec` field. Never zero.
225 Counted {
226 name: &'static str,
227 child: u32,
228 count: u32,
229 },
230}
231
232impl SectionItemNode {
233 /// The record field this item contributes.
234 #[must_use]
235 pub fn name(&self) -> &'static str {
236 match self {
237 SectionItemNode::One { name, .. } | SectionItemNode::Counted { name, .. } => name,
238 }
239 }
240
241 /// How many sections this item consumes — the number the runtime's section
242 /// cursor advances by, and the number the shortfall check sums.
243 #[must_use]
244 pub fn sections_wanted(&self) -> usize {
245 match self {
246 SectionItemNode::One { .. } => 1,
247 SectionItemNode::Counted { count, .. } => *count as usize,
248 }
249 }
250}
251
252/// One item of a `block(...)` (§7.5), in plan form.
253#[derive(Debug)]
254pub enum BlockItemNode {
255 /// A positional parser. A named-capture template's fields flatten into the
256 /// block record (the runtime reads the record's fields from the produced
257 /// value); any other positional must have been named (validation rejects).
258 Positional { child: u32 },
259 /// A named item contributing one field.
260 Named { name: &'static str, child: u32 },
261}
262
263/// A compiled parser plan: the node arena plus auxiliary interned data.
264pub struct ParserPlan {
265 /// The flat node arena, indexed by `PlanNode` child references.
266 pub nodes: &'static [PlanNode],
267 /// Template literal/capture parts, referenced by `PlanNode::Template`.
268 pub template_parts: &'static [TemplatePartNode],
269 /// Interned string literals (separators, template literals), so the runtime
270 /// reads `&'static str` without touching Rust owned data.
271 pub literals: &'static [&'static str],
272 /// The root node index (entry point).
273 pub root: u32,
274}
275
276impl std::fmt::Debug for ParserPlan {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("ParserPlan")
279 .field("nodes", &self.nodes)
280 .field("template_parts_len", &self.template_parts.len())
281 .field("literals", &self.literals)
282 .field("root", &self.root)
283 .finish()
284 }
285}
286
287// ===========================================================================
288// Lowering: ParserAst → &'static ParserPlan
289// ===========================================================================
290
291/// A builder that accumulates plan nodes into owned Vecs, then moves them into
292/// the compiled plan's arena in [`PlanBuilder::finish`].
293struct PlanBuilder<'a> {
294 arena: &'a Bump,
295 nodes: Vec<PlanNode>,
296 template_parts: Vec<TemplatePartNode>,
297 literals: Vec<&'static str>,
298 order: &'a mut dyn FieldOrder,
299}
300
301impl<'a> PlanBuilder<'a> {
302 fn new(arena: &'a Bump, order: &'a mut dyn FieldOrder) -> Self {
303 PlanBuilder {
304 arena,
305 nodes: Vec::new(),
306 template_parts: Vec::new(),
307 literals: Vec::new(),
308 order,
309 }
310 }
311
312 /// The canonical order of an anonymous record shape's fields, allocated in
313 /// this plan's arena so the runtime reads `&'static str`.
314 ///
315 /// `names` are the fields in the order this parser *writes* them, which is
316 /// the order the value is assembled in; the answer is the order the value
317 /// must be **laid out** in. The two differ only when some other spelling of
318 /// the same shape got there first — see [`FieldOrder`].
319 fn canonical_order(&mut self, names: &[&str]) -> &'static [&'static str] {
320 let canonical = self.order.canonical(names);
321 let entries: Vec<&'static str> = canonical.iter().map(|n| self.alloc_str(n)).collect();
322 self.alloc_slice(entries)
323 }
324
325 /// Push a node and return its index.
326 fn push_node(&mut self, node: PlanNode) -> u32 {
327 let idx = self.nodes.len() as u32;
328 self.nodes.push(node);
329 idx
330 }
331
332 /// Intern a literal string, returning its index. The input `s` must already
333 /// live in this plan's arena (see [`PlanBuilder::alloc_str`]).
334 fn intern_literal(&mut self, s: &'static str) -> u32 {
335 let idx = self.literals.len() as u32;
336 self.literals.push(s);
337 idx
338 }
339
340 /// Copy `s` into the plan's arena.
341 fn alloc_str(&self, s: &str) -> &'static str {
342 alloc_str(self.arena, s)
343 }
344
345 /// Move a `Vec<T>` into the plan's arena.
346 fn alloc_slice<T>(&self, v: Vec<T>) -> &'static [T] {
347 alloc_slice(self.arena, v)
348 }
349
350 /// Move the accumulated data into the arena and return the plan.
351 fn finish(self, root: u32) -> &'static ParserPlan {
352 let PlanBuilder {
353 arena,
354 nodes,
355 template_parts,
356 literals,
357 order: _,
358 } = self;
359 let plan: &ParserPlan = arena.alloc(ParserPlan {
360 nodes: alloc_slice(arena, nodes),
361 template_parts: alloc_slice(arena, template_parts),
362 literals: alloc_slice(arena, literals),
363 root,
364 });
365 // SAFETY: see `alloc_str`.
366 unsafe { &*(plan as *const ParserPlan) }
367 }
368}
369
370/// Copy `s` into `arena`, erasing the lifetime to `'static`.
371///
372/// The `'static` is a lie the plan's own field types force: `PlanNode` and
373/// `ParserPlan` declare `&'static str`. The truth is *arena* lifetime, and
374/// [`CompiledPlan`] is what keeps the arena alive exactly as long as the plan.
375/// This function and [`alloc_slice`] are the only two places the erasure
376/// happens, and [`retire_all_plans`]'s safety contract is what discharges it.
377fn alloc_str(arena: &Bump, s: &str) -> &'static str {
378 let stored: &str = arena.alloc_str(s);
379 // SAFETY: the bytes live in the arena the `CompiledPlan` owns alongside the
380 // plan, and both are released together by `retire_all`.
381 unsafe { &*(stored as *const str) }
382}
383
384/// Move a `Vec<T>` into `arena`. Same lifetime erasure as [`alloc_str`].
385fn alloc_slice<T>(arena: &Bump, v: Vec<T>) -> &'static [T] {
386 let stored: &[T] = arena.alloc_slice_fill_iter(v);
387 // SAFETY: as `alloc_str`.
388 unsafe { &*(stored as *const [T]) }
389}
390
391/// A lowered plan together with the arena that owns everything it points at.
392///
393/// Self-referential by construction — `plan` addresses storage inside `arena` —
394/// which is sound because `bumpalo` keeps its chunks on the heap: moving a
395/// `CompiledPlan` moves the `Bump` *handle*, never the bytes.
396pub struct CompiledPlan {
397 /// Owns the nodes, template parts, literals and every interned string. It
398 /// is never read — its whole job is to keep `plan`'s storage alive and to
399 /// release it on drop, which is what makes a plan reclaimable at all.
400 #[allow(dead_code)]
401 arena: Bump,
402 plan: *const ParserPlan,
403}
404
405impl CompiledPlan {
406 /// The compiled plan.
407 pub fn plan(&self) -> &ParserPlan {
408 // SAFETY: `plan` was allocated in `self.arena`, which this borrow keeps
409 // alive, and is never mutated after construction.
410 unsafe { &*self.plan }
411 }
412}
413
414impl std::fmt::Debug for CompiledPlan {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 self.plan().fmt(f)
417 }
418}
419
420// SAFETY: `CompiledPlan` owns everything it points at, and nothing inside is
421// shared with another thread until it is registered behind the arena's mutex.
422// The raw pointer is what makes the auto-impl fail; the data is plain,
423// immutable, and self-contained.
424unsafe impl Send for CompiledPlan {}
425
426/// Lower a validated `ParserAst` into a self-owning [`CompiledPlan`].
427///
428/// The caller registers it with [`register_plan`] to get the [`PlanId`] MIR
429/// embeds.
430///
431/// # Panics
432/// Only on an internal inconsistency (the AST should have passed validation).
433pub fn lower_to_plan(ast: &ParserAst, order: &mut dyn FieldOrder) -> CompiledPlan {
434 let arena = Bump::new();
435 let plan = {
436 let mut b = PlanBuilder::new(&arena, order);
437 let root = lower_node(&mut b, ast);
438 b.finish(root) as *const ParserPlan
439 };
440 CompiledPlan { arena, plan }
441}
442
443/// Who decides the **layout order** of the anonymous record a named-capture
444/// parser builds (§5.6, ADR-152).
445///
446/// An anonymous record's identity is its field-name set, so two parsers that
447/// name the same fields in different orders produce one type — and a type has
448/// exactly one field order, because a field read compiles to a slot index. The
449/// order therefore cannot be a property of the parser that happens to be
450/// building the value; it has to come from whatever knows about *every*
451/// spelling in the program. During a compile that is the
452/// [`TypeDb`](praxis_typeck::TypeDb), which registered a definition for each.
453///
454/// The plan stores the answer per record-producing node so the runtime places
455/// fields with an index rather than a name lookup.
456pub trait FieldOrder {
457 /// The canonical order of the shape whose fields are `names`. The answer is
458 /// a permutation of `names` — same set, possibly reordered.
459 fn canonical(&mut self, names: &[&str]) -> Vec<String>;
460}
461
462/// The order the parser wrote them in — [`FieldOrder`] for a plan lowered
463/// outside a compilation, where there is no other spelling to agree with.
464///
465/// Correct on its own terms: with one spelling of a shape, the first one *is*
466/// the canonical one. It is what the teardown and plan tests use, and what
467/// makes those tests independent of the type arena.
468pub struct SourceOrder;
469
470impl FieldOrder for SourceOrder {
471 fn canonical(&mut self, names: &[&str]) -> Vec<String> {
472 names.iter().map(|n| (*n).to_string()).collect()
473 }
474}
475
476impl FieldOrder for praxis_typeck::TypeDb {
477 fn canonical(&mut self, names: &[&str]) -> Vec<String> {
478 self.canonical_field_order(names).to_vec()
479 }
480}
481
482// ===========================================================================
483// The plan arena: maps `PlanId`s (passed as an i64 immediate through MIR) to
484// their compiled plans. Lives here (not in HIR) so both HIR (which registers)
485// and the runtime interpreter (which looks up) depend on this crate without
486// creating a dependency cycle.
487// ===========================================================================
488
489/// The identity of a registered parser plan.
490///
491/// `NonZeroU32` on purpose: a `0` meaning "parser analysis failed" would be
492/// indistinguishable from the first successfully registered plan. No `u32`
493/// means "no plan", so that encoding is unwritable — a failed analysis lowers
494/// to an error expression instead.
495#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
496pub struct PlanId(NonZeroU32);
497
498impl PlanId {
499 /// The raw id, for embedding as a MIR immediate.
500 pub fn get(self) -> u32 {
501 self.0.get()
502 }
503
504 /// Recover a `PlanId` from a raw value, rejecting `0`.
505 ///
506 /// The runtime calls this on the integer it reads back out of a boxed `Int`
507 /// argument: a value that never named a plan must become a parse fault, not
508 /// an index into the arena.
509 pub fn from_raw(raw: u32) -> Option<PlanId> {
510 NonZeroU32::new(raw).map(PlanId)
511 }
512}
513
514/// Registration refused: the process has compiled more parser plans than the
515/// arena admits.
516///
517/// This is a compile-time registration bound, not a language limit — one plan
518/// per `read`/`parse` expression per compile. Reaching it means something is
519/// recompiling in a loop, and saying so beats wrapping the index.
520#[derive(Clone, Copy, PartialEq, Eq, Debug)]
521pub struct TooManyPlans {
522 /// The bound that was hit.
523 pub limit: usize,
524}
525
526impl std::fmt::Display for TooManyPlans {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 write!(
529 f,
530 "too many parser plans registered in one process (limit {})",
531 self.limit
532 )
533 }
534}
535
536impl std::error::Error for TooManyPlans {}
537
538/// The registration bound. Chosen so an id always fits a `u32` with room to
539/// spare, and so a runaway registration loop is caught long before the
540/// narrowing could matter.
541pub const MAX_PLANS: usize = 1 << 20;
542
543/// The narrowing `register_plan` performs is checked *and* provable: an id is
544/// `len()` after a push, which the bound keeps strictly inside `u32`.
545const _: () = assert!(MAX_PLANS < u32::MAX as usize);
546
547/// The process-wide store. Index `i` holds the plan with id `i + 1`, which is
548/// what keeps [`PlanId`] non-zero.
549static PLAN_ARENA: Mutex<Vec<CompiledPlan>> = Mutex::new(Vec::new());
550
551/// Register a compiled plan, returning its id.
552///
553/// # Errors
554/// [`TooManyPlans`] once [`MAX_PLANS`] plans have been registered.
555pub fn register_plan(plan: CompiledPlan) -> Result<PlanId, TooManyPlans> {
556 register_with_limit(plan, MAX_PLANS)
557}
558
559/// [`register_plan`] with the bound as a parameter, so a test can reach the
560/// refusal without registering a million plans.
561fn register_with_limit(plan: CompiledPlan, limit: usize) -> Result<PlanId, TooManyPlans> {
562 let mut arena = PLAN_ARENA
563 .lock()
564 .unwrap_or_else(std::sync::PoisonError::into_inner);
565 if arena.len() >= limit {
566 // Refuse *before* pushing, so a rejected registration leaves the arena
567 // exactly as it found it.
568 return Err(TooManyPlans { limit });
569 }
570 arena.push(plan);
571 let raw = u32::try_from(arena.len()).map_err(|_| TooManyPlans { limit })?;
572 Ok(PlanId(
573 NonZeroU32::new(raw).expect("len is at least 1 after the push"),
574 ))
575}
576
577/// Look up a plan by id. `None` if the id names no registered plan (the caller
578/// treats that as a parse fault).
579pub fn get_plan(id: PlanId) -> Option<&'static ParserPlan> {
580 let arena = PLAN_ARENA
581 .lock()
582 .unwrap_or_else(std::sync::PoisonError::into_inner);
583 let plan: *const ParserPlan = arena.get(id.get() as usize - 1)?.plan;
584 // SAFETY: registered plans are never removed except by `retire_all_plans`, whose
585 // caller has proven the heap is drained; the storage lives in the
586 // `CompiledPlan`'s arena, on the heap, so the address is stable across the
587 // `Vec`'s own reallocations.
588 Some(unsafe { &*plan })
589}
590
591/// How many plans are registered. For tests and diagnostics.
592pub fn plan_count() -> usize {
593 PLAN_ARENA
594 .lock()
595 .unwrap_or_else(std::sync::PoisonError::into_inner)
596 .len()
597}
598
599/// Drop every registered plan, releasing its arena.
600///
601/// # Safety
602/// Every `&'static str` a plan handed out must be dead. In particular the
603/// runtime builds record schemas for named-capture templates whose field names
604/// *borrow from plan storage*, and caches them; those caches must be cleared in
605/// the same breath. `praxis_runtime::retire_parser_plans` is the only intended
606/// caller and does exactly that, behind a `HeapDrained` proof.
607pub unsafe fn retire_all_plans() {
608 PLAN_ARENA
609 .lock()
610 .unwrap_or_else(std::sync::PoisonError::into_inner)
611 .clear();
612}
613
614/// Lower one node, returning its index in the plan arena.
615fn lower_node(b: &mut PlanBuilder<'_>, ast: &ParserAst) -> u32 {
616 /// Lower the one child and wrap it — the entire body of every constructor
617 /// whose plan node is `{ child }` and nothing else, and whose plan variant
618 /// shares its name with the AST variant.
619 ///
620 /// The arms written out below are the ones that need more. `Atomic` and
621 /// `OneOf` have no child; `Template` has its own function; the rest carry
622 /// payload beside the child that has to be allocated or interned *here*, in
623 /// the plan's arena — a literal (`Sep`, `OneOf`, `GridRagged`), a skip
624 /// policy (`Characters`), or a slice of entries (`SectionsNamed`, `Block`,
625 /// `Choice`).
626 ///
627 /// A macro rather than a helper `fn` because what varies is a *variant
628 /// name*, not a value; and invoked in body position rather than expanded
629 /// into the arms themselves because macros cannot expand to match arms.
630 macro_rules! unary {
631 ($node:ident, $child:expr_2021) => {{
632 let c = lower_node(b, $child);
633 b.push_node(PlanNode::$node { child: c })
634 }};
635 }
636
637 match ast {
638 ParserAst::Atomic { kind, .. } => b.push_node(PlanNode::Atomic { kind: *kind }),
639 ParserAst::Lines { child, .. } => unary!(Lines, child),
640 ParserAst::Sections { child, .. } => unary!(Sections, child),
641 ParserAst::SectionsNamed {
642 fields,
643 repeated_tail,
644 ..
645 } => {
646 // Lower each named argument's child, keeping source order: a
647 // counted group's position among the fields is the position the
648 // runtime consumes its sections at.
649 let field_entries: Vec<SectionItemNode> = fields
650 .iter()
651 .map(|item| {
652 let name = b.alloc_str(item.name());
653 let child = lower_node(b, item.parser());
654 match item {
655 crate::ast::SectionItem::One { .. } => SectionItemNode::One { name, child },
656 crate::ast::SectionItem::Counted { count, .. } => {
657 SectionItemNode::Counted {
658 name,
659 child,
660 count: count.get(),
661 }
662 }
663 }
664 })
665 .collect();
666 let tail_entry = repeated_tail.as_ref().map(|(name, p)| {
667 let n = b.alloc_str(name);
668 let c = lower_node(b, p);
669 (n, c)
670 });
671 // The record's fields are the named arguments in source order, then
672 // the unbounded tail, which is the order the runtime assembles them
673 // in and therefore the order to ask about.
674 let mut names: Vec<&str> = field_entries.iter().map(|f| f.name()).collect();
675 if let Some((tail_name, _)) = tail_entry {
676 names.push(tail_name);
677 }
678 let field_order = b.canonical_order(&names);
679 let field_slice = b.alloc_slice(field_entries);
680 b.push_node(PlanNode::SectionsNamed {
681 fields: field_slice,
682 repeated_tail: tail_entry,
683 field_order,
684 })
685 }
686 ParserAst::Csv { child, .. } => unary!(Csv, child),
687 ParserAst::Ws { child, .. } => unary!(Ws, child),
688 ParserAst::Sep {
689 separator, child, ..
690 } => {
691 let sep_static: &'static str = b.alloc_str(separator.as_str());
692 let sep_idx = b.intern_literal(sep_static);
693 let c = lower_node(b, child);
694 b.push_node(PlanNode::Sep {
695 separator_index: sep_idx,
696 child: c,
697 })
698 }
699 ParserAst::Grid { child, .. } => unary!(Grid, child),
700 ParserAst::Block { items, .. } => {
701 // The block record's fields, in the order the runtime assembles
702 // them: a named item contributes its own name, and a positional
703 // template contributes each of its captures' names, flattened in
704 // place (§7.5). Read off the *AST* because that is where a
705 // positional's capture names still are — the lowered child is a
706 // node index, and its parts are the runtime's to walk.
707 let mut names: Vec<&str> = Vec::new();
708 for item in items {
709 match item {
710 crate::ast::BlockItem::Positional(p) => names.extend(template_field_names(p)),
711 crate::ast::BlockItem::Named { name, .. } => names.push(name),
712 }
713 }
714 let field_order = b.canonical_order(&names);
715 let item_nodes: Vec<BlockItemNode> = items
716 .iter()
717 .map(|item| match item {
718 crate::ast::BlockItem::Positional(p) => BlockItemNode::Positional {
719 child: lower_node(b, p),
720 },
721 crate::ast::BlockItem::Named { name, parser } => BlockItemNode::Named {
722 name: b.alloc_str(name),
723 child: lower_node(b, parser),
724 },
725 })
726 .collect();
727 let items_slice = b.alloc_slice(item_nodes);
728 b.push_node(PlanNode::Block {
729 items: items_slice,
730 field_order,
731 })
732 }
733 ParserAst::Choice { cases, .. } => {
734 let case_entries: Vec<(&'static str, u32)> = cases
735 .iter()
736 .map(|(name, p)| {
737 let n = b.alloc_str(name);
738 let c = lower_node(b, p);
739 (n, c)
740 })
741 .collect();
742 let cases_slice = b.alloc_slice(case_entries);
743 b.push_node(PlanNode::Choice { cases: cases_slice })
744 }
745 ParserAst::Optional { child, .. } => unary!(Optional, child),
746 ParserAst::Scan { child, .. } => unary!(Scan, child),
747 ParserAst::OneOf { chars, .. } => {
748 let chars_static = b.alloc_str(chars);
749 let idx = b.intern_literal(chars_static);
750 b.push_node(PlanNode::OneOf { chars_index: idx })
751 }
752 ParserAst::Characters { child, skip, .. } => {
753 let c = lower_node(b, child);
754 b.push_node(PlanNode::Characters {
755 child: c,
756 skip: *skip,
757 })
758 }
759 ParserAst::Matrix { child, .. } => unary!(Matrix, child),
760 ParserAst::GridRagged { child, fill, .. } => {
761 let c = lower_node(b, child);
762 let fill_static = b.alloc_str(fill);
763 let fill_idx = b.intern_literal(fill_static);
764 b.push_node(PlanNode::GridRagged {
765 child: c,
766 fill_index: fill_idx,
767 })
768 }
769 ParserAst::Template { parts, .. } => lower_template(b, parts),
770 }
771}
772
773/// Lower a template into a `PlanNode::Template`.
774///
775/// **Every shape, one node.** Scalar, tuple and record templates all lower to
776/// this, because all three need the literal parts between the captures kept —
777/// they are the separators the runtime matches, and they are what makes
778/// `` `{int},{int}` `` a pair rather than a comma-less run of digits. Which of
779/// the three a given template *is* is [`TemplateShape::of`]'s answer, read from
780/// these same parts by the interpreter; this function does not decide it.
781/// `captures` is collected only for the field indices [`lower_template_parts`]
782/// assigns.
783fn lower_template(b: &mut PlanBuilder<'_>, parts: &[TemplatePart]) -> u32 {
784 // Capture positions, which is what assigns each capture its field index in
785 // the resulting record or tuple.
786 let captures: Vec<(usize, &TemplatePart)> = parts
787 .iter()
788 .enumerate()
789 .filter(|(_, p)| matches!(p, TemplatePart::Capture { .. }))
790 .collect();
791 // A record shape only — a tuple has no field names to reorder by, and
792 // `TemplateShape::of` reads the same "is any capture named?" question off
793 // the lowered parts.
794 let names = template_field_names_of(parts);
795 let field_order = if names.is_empty() {
796 &[][..]
797 } else {
798 b.canonical_order(&names)
799 };
800 let part_indices = lower_template_parts(b, parts, &captures);
801 b.push_node(PlanNode::Template {
802 parts: part_indices,
803 field_order,
804 })
805}
806
807/// The field names a *template* parser contributes to a record, in source
808/// order, or empty when it builds no record.
809///
810/// The two callers ask for different reasons — one is lowering the template
811/// itself, one is a `block(...)` flattening it (§7.5) — and both need the same
812/// answer, which is the one `TemplateShape::Record` is defined by: a template
813/// is a record when any capture is named.
814fn template_field_names_of(parts: &[TemplatePart]) -> Vec<&str> {
815 let names: Vec<&str> = parts
816 .iter()
817 .filter_map(|p| match p {
818 TemplatePart::Capture { name, .. } => name.as_ref().map(|n| n.as_str()),
819 TemplatePart::Literal { .. } => None,
820 })
821 .collect();
822 names
823}
824
825/// [`template_field_names_of`] for a parser that *may* be a template. A
826/// positional `block(...)` item that is not one contributes no fields —
827/// validation has already refused it (I026).
828fn template_field_names(ast: &ParserAst) -> Vec<&str> {
829 match ast {
830 ParserAst::Template { parts, .. } => template_field_names_of(parts),
831 _ => Vec::new(),
832 }
833}
834
835/// Lower template parts into the `template_parts` arena, returning a static
836/// slice reference. `captures` lets us assign field indices.
837fn lower_template_parts(
838 b: &mut PlanBuilder<'_>,
839 parts: &[TemplatePart],
840 captures: &[(usize, &TemplatePart)],
841) -> &'static [TemplatePartNode] {
842 let mut nodes = Vec::new();
843 for part in parts {
844 match part {
845 TemplatePart::Literal { text, ws, .. } => {
846 let text_static = b.alloc_str(text);
847 nodes.push(TemplatePartNode::Literal {
848 text: text_static,
849 ws: *ws,
850 });
851 }
852 TemplatePart::Capture { name, parser, .. } => {
853 let child = lower_node(b, parser);
854 let field_index = captures
855 .iter()
856 .position(|(_, p)| std::ptr::eq(*p, part))
857 .map(|i| i as u16);
858 let name_static = name.as_ref().map(|n| b.alloc_str(n.as_str()));
859 nodes.push(TemplatePartNode::Capture {
860 child,
861 field_index,
862 name: name_static,
863 });
864 }
865 }
866 }
867 b.alloc_slice(nodes)
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use crate::ast::{AtomicKind, Separator, TemplatePart, WsPolicy};
874 use praxis_source::Span;
875
876 #[test]
877 fn atomic_lower_to_plan() {
878 let ast = ParserAst::Atomic {
879 kind: AtomicKind::Int,
880 span: Span::at(0),
881 };
882 let compiled = lower_to_plan(&ast, &mut SourceOrder);
883 let plan = compiled.plan();
884 assert_eq!(plan.root, 0);
885 assert!(matches!(
886 plan.nodes[0],
887 PlanNode::Atomic {
888 kind: AtomicKind::Int
889 }
890 ));
891 }
892
893 #[test]
894 fn lines_of_int_lower_to_plan() {
895 let ast = ParserAst::Lines {
896 child: Box::new(ParserAst::Atomic {
897 kind: AtomicKind::Int,
898 span: Span::at(0),
899 }),
900 span: Span::at(0),
901 };
902 let compiled = lower_to_plan(&ast, &mut SourceOrder);
903 let plan = compiled.plan();
904 // Children are lowered first (lower index); the parent (Lines) is root.
905 assert!(matches!(
906 plan.nodes[0],
907 PlanNode::Atomic {
908 kind: AtomicKind::Int
909 }
910 ));
911 assert!(matches!(
912 plan.nodes[plan.root as usize],
913 PlanNode::Lines { child: 0 }
914 ));
915 }
916
917 #[test]
918 fn sep_lower_interns_separator() {
919 let ast = ParserAst::Sep {
920 separator: Separator::new(" -> ").expect("a non-empty separator"),
921 child: Box::new(ParserAst::Atomic {
922 kind: AtomicKind::Word,
923 span: Span::at(0),
924 }),
925 span: Span::at(0),
926 };
927 let compiled = lower_to_plan(&ast, &mut SourceOrder);
928 let plan = compiled.plan();
929 match plan.nodes[plan.root as usize] {
930 PlanNode::Sep {
931 separator_index, ..
932 } => {
933 assert_eq!(plan.literals[separator_index as usize], " -> ");
934 }
935 _ => panic!("expected Sep at root"),
936 }
937 }
938
939 /// A `PlanId` cannot be zero, so a `0` failure sentinel has no encoding.
940 #[test]
941 fn zero_is_not_a_plan_id() {
942 assert!(PlanId::from_raw(0).is_none());
943 assert_eq!(PlanId::from_raw(1).map(PlanId::get), Some(1));
944 }
945
946 /// Registration hands out non-zero ids that round-trip through the raw
947 /// `u32` MIR embeds, and each one resolves to the plan it named.
948 #[test]
949 fn registered_plans_round_trip_through_their_raw_id() {
950 let first = register_plan(lower_to_plan(
951 &ParserAst::Atomic {
952 kind: AtomicKind::Int,
953 span: Span::at(0),
954 },
955 &mut SourceOrder,
956 ))
957 .expect("the arena is far from full");
958 let second = register_plan(lower_to_plan(
959 &ParserAst::Atomic {
960 kind: AtomicKind::Word,
961 span: Span::at(0),
962 },
963 &mut SourceOrder,
964 ))
965 .expect("the arena is far from full");
966 assert_ne!(first, second);
967 for (id, expected) in [(first, AtomicKind::Int), (second, AtomicKind::Word)] {
968 let raw = id.get();
969 assert!(raw > 0, "a plan id is never zero");
970 let recovered = PlanId::from_raw(raw).expect("a registered id is non-zero");
971 let plan = get_plan(recovered).expect("a registered plan resolves");
972 assert!(
973 matches!(plan.nodes[plan.root as usize], PlanNode::Atomic { kind } if kind == expected)
974 );
975 }
976 }
977
978 /// Registration is bounded and refuses cleanly: the caller gets a
979 /// diagnostic instead of a wrapped index.
980 ///
981 /// The bound is a parameter here only so the test need not register a
982 /// million plans; a limit of zero refuses deterministically regardless of
983 /// what else this test binary has registered in parallel.
984 #[test]
985 fn registration_past_the_bound_is_refused() {
986 let atom = || ParserAst::Atomic {
987 kind: AtomicKind::Int,
988 span: Span::at(0),
989 };
990 let refused = register_with_limit(lower_to_plan(&atom(), &mut SourceOrder), 0)
991 .expect_err("a zero limit admits no plans at all");
992 assert_eq!(refused.limit, 0);
993 assert!(refused.to_string().contains("too many parser plans"));
994 // The refusal happens before the push, so it consumed nothing: an
995 // ordinary registration still succeeds and still yields a usable id.
996 let accepted = register_plan(lower_to_plan(&atom(), &mut SourceOrder))
997 .expect("the real arena has room");
998 assert!(get_plan(accepted).is_some());
999 }
1000
1001 /// An id past the end of the arena resolves to nothing rather than
1002 /// indexing. The runtime turns that `None` into a parse fault.
1003 #[test]
1004 fn an_unregistered_id_resolves_to_nothing() {
1005 let beyond = PlanId::from_raw(u32::MAX).expect("non-zero");
1006 assert!(get_plan(beyond).is_none());
1007 }
1008
1009 /// The plan's storage really is owned: an interned literal survives being
1010 /// read back out of the arena, and the `CompiledPlan` is what keeps it
1011 /// alive.
1012 #[test]
1013 fn a_compiled_plan_owns_its_interned_strings() {
1014 let compiled = lower_to_plan(
1015 &ParserAst::Sep {
1016 separator: Separator::new(" -> ").expect("a non-empty separator"),
1017 child: Box::new(ParserAst::Atomic {
1018 kind: AtomicKind::Word,
1019 span: Span::at(0),
1020 }),
1021 span: Span::at(0),
1022 },
1023 &mut SourceOrder,
1024 );
1025 assert_eq!(compiled.plan().literals, &[" -> "]);
1026 }
1027
1028 /// A named-capture template lowers with the record's **canonical** field
1029 /// order, not its own (§5.6, ADR-152).
1030 ///
1031 /// The order is the [`FieldOrder`]'s answer and the plan carries it, which
1032 /// is how the runtime lays two spellings of one shape out the same way. The
1033 /// oracle here stands in for the type arena's "the first spelling of this
1034 /// shape wrote `w` before `h`"; the template writes them the other way.
1035 #[test]
1036 fn a_named_template_carries_the_canonical_field_order_and_not_its_own() {
1037 struct WThenH;
1038 impl FieldOrder for WThenH {
1039 fn canonical(&mut self, _names: &[&str]) -> Vec<String> {
1040 vec!["w".to_string(), "h".to_string()]
1041 }
1042 }
1043 let named = |name: &str| TemplatePart::Capture {
1044 name: Some(crate::ast::CaptureName::parse(name).expect("a legal name")),
1045 parser: Box::new(ParserAst::Atomic {
1046 kind: AtomicKind::Int,
1047 span: Span::at(0),
1048 }),
1049 span: Span::at(0),
1050 name_span: None,
1051 };
1052 let ast = ParserAst::Template {
1053 parts: vec![
1054 named("h"),
1055 TemplatePart::Literal {
1056 text: "x".to_string(),
1057 ws: WsPolicy::SpaceRun,
1058 span: Span::at(0),
1059 },
1060 named("w"),
1061 ],
1062 span: Span::at(0),
1063 };
1064 let compiled = lower_to_plan(&ast, &mut WThenH);
1065 let plan = compiled.plan();
1066 let PlanNode::Template { parts, field_order } = &plan.nodes[plan.root as usize] else {
1067 panic!("a named-capture template lowers to a Template node");
1068 };
1069 assert_eq!(TemplateShape::of(parts), TemplateShape::Record);
1070 assert_eq!(*field_order, &["w", "h"]);
1071
1072 // A shape nobody wrote first keeps its own order, which is what
1073 // `SourceOrder` is: the first spelling *is* the canonical one.
1074 let compiled = lower_to_plan(&ast, &mut SourceOrder);
1075 let PlanNode::Template { field_order, .. } =
1076 &compiled.plan().nodes[compiled.plan().root as usize]
1077 else {
1078 panic!("a named-capture template lowers to a Template node");
1079 };
1080 assert_eq!(*field_order, &["h", "w"]);
1081 }
1082
1083 /// A template that builds no record carries no order to disagree about.
1084 #[test]
1085 fn a_tuple_template_carries_no_field_order() {
1086 let anonymous = || TemplatePart::Capture {
1087 name: None,
1088 parser: Box::new(ParserAst::Atomic {
1089 kind: AtomicKind::Int,
1090 span: Span::at(0),
1091 }),
1092 span: Span::at(0),
1093 name_span: None,
1094 };
1095 let ast = ParserAst::Template {
1096 parts: vec![
1097 anonymous(),
1098 TemplatePart::Literal {
1099 text: ",".to_string(),
1100 ws: WsPolicy::SpaceRun,
1101 span: Span::at(0),
1102 },
1103 anonymous(),
1104 ],
1105 span: Span::at(0),
1106 };
1107 let compiled = lower_to_plan(&ast, &mut SourceOrder);
1108 let PlanNode::Template { field_order, .. } =
1109 &compiled.plan().nodes[compiled.plan().root as usize]
1110 else {
1111 panic!("a template lowers to a Template node");
1112 };
1113 assert!(field_order.is_empty(), "a tuple has no fields to order");
1114 }
1115
1116 /// Two anonymous captures lower to a `Template` node — **not** to a tuple
1117 /// node, because there is none and cannot be one (ADR-092).
1118 ///
1119 /// This assertion is the standing proof that the tuple shape takes the
1120 /// `Template` path. The literals between the captures are preserved on that
1121 /// path, which is exactly what a `Tuple { elements: &[u32] }` would have
1122 /// nowhere to put; the interpreter reads the shape back off the parts with
1123 /// [`TemplateShape::of`] and assembles the tuple from the captured values.
1124 #[test]
1125 fn template_literal_lower_to_plan() {
1126 let ast = ParserAst::Template {
1127 parts: vec![
1128 TemplatePart::Capture {
1129 name: None,
1130 parser: Box::new(ParserAst::Atomic {
1131 kind: AtomicKind::Int,
1132 span: Span::at(0),
1133 }),
1134 span: Span::at(0),
1135 name_span: None,
1136 },
1137 TemplatePart::Literal {
1138 text: ",".to_string(),
1139 ws: WsPolicy::SpaceRun,
1140 span: Span::at(0),
1141 },
1142 TemplatePart::Capture {
1143 name: None,
1144 parser: Box::new(ParserAst::Atomic {
1145 kind: AtomicKind::Int,
1146 span: Span::at(0),
1147 }),
1148 span: Span::at(0),
1149 name_span: None,
1150 },
1151 ],
1152 span: Span::at(0),
1153 };
1154 let compiled = lower_to_plan(&ast, &mut SourceOrder);
1155 let plan = compiled.plan();
1156 // The root is the last-pushed node.
1157 let PlanNode::Template { parts, .. } = &plan.nodes[plan.root as usize] else {
1158 panic!("a two-anonymous-capture template lowers to a Template node");
1159 };
1160 // And the shape the interpreter reads back off those parts is the tuple
1161 // one, which is the half the node kind alone does not say.
1162 assert_eq!(TemplateShape::of(parts), TemplateShape::Tuple);
1163 }
1164
1165 /// **A counted group keeps its position and its count in the plan.** The
1166 /// runtime walks `fields` in order with one section cursor, so a counted
1167 /// item lowered out of order — or lowered without its count — would read a
1168 /// different span of sections than the source named. The plan is the last
1169 /// place source order still exists.
1170 #[test]
1171 fn a_counted_item_keeps_its_count_and_its_position_in_the_plan() {
1172 use crate::ast::{RepeatCount, SectionItem};
1173
1174 let ast = ParserAst::SectionsNamed {
1175 fields: vec![
1176 SectionItem::Counted {
1177 name: "shapes".to_string(),
1178 count: RepeatCount::new(6).expect("six sections"),
1179 parser: ParserAst::Lines {
1180 child: Box::new(ParserAst::Atomic {
1181 kind: AtomicKind::Int,
1182 span: Span::at(0),
1183 }),
1184 span: Span::at(0),
1185 },
1186 },
1187 SectionItem::One {
1188 name: "regions".to_string(),
1189 parser: ParserAst::Atomic {
1190 kind: AtomicKind::Char,
1191 span: Span::at(0),
1192 },
1193 },
1194 ],
1195 repeated_tail: None,
1196 span: Span::at(0),
1197 };
1198 let compiled = lower_to_plan(&ast, &mut SourceOrder);
1199 let plan = compiled.plan();
1200 let PlanNode::SectionsNamed {
1201 fields,
1202 repeated_tail,
1203 ..
1204 } = &plan.nodes[plan.root as usize]
1205 else {
1206 panic!("a named `sections` lowers to a SectionsNamed node");
1207 };
1208 assert!(repeated_tail.is_none(), "a counted group is not the tail");
1209 assert_eq!(fields.len(), 2);
1210 match &fields[0] {
1211 SectionItemNode::Counted { name, child, count } => {
1212 assert_eq!(*name, "shapes");
1213 assert_eq!(*count, 6);
1214 assert!(matches!(
1215 plan.nodes[*child as usize],
1216 PlanNode::Lines { .. }
1217 ));
1218 }
1219 other => panic!("the first field is the counted group, got {other:?}"),
1220 }
1221 match &fields[1] {
1222 SectionItemNode::One { name, .. } => assert_eq!(*name, "regions"),
1223 other => panic!("the second field follows the counted group, got {other:?}"),
1224 }
1225 // Two sections for `regions` to start at is what the count buys, and
1226 // the runtime reads it from here.
1227 assert_eq!(fields[0].sections_wanted(), 6);
1228 assert_eq!(fields[1].sections_wanted(), 1);
1229 }
1230}