Skip to main content

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}