mod cursor;
use crate::GcRef;
use crate::context::RuntimeContext;
use crate::parse_detail::ParseFail;
use crate::roots::{NativeScope, RuntimeRoots};
use crate::scalars;
use crate::text::TextPayload;
use cursor::{ByteRegion, Cursor, Input, Walked, split_lines, split_sections, trailing_blank_run};
use praxis_input_parser::synthesize::AtomicClass;
use praxis_input_parser::{AtomicKind, ParserPlan, PlanNode, SectionItemNode, TemplateShape};
pub unsafe fn run_plan_by_id(ctx: *mut RuntimeContext, raw_id: i64, input: GcRef) -> Option<GcRef> {
let id = u32::try_from(raw_id)
.ok()
.and_then(praxis_input_parser::PlanId::from_raw)?;
let plan = praxis_input_parser::get_plan(id)?;
Some(unsafe { run_plan(ctx, plan, input) })
}
unsafe fn run_plan(ctx: *mut RuntimeContext, plan: &ParserPlan, input: GcRef) -> GcRef {
let Some(i) = (unsafe { Input::new(input) }) else {
unsafe { clear_parse_detail(ctx) };
return unsafe { fault_sentinel(ctx) };
};
let region = i.whole();
let scope = unsafe { NativeScope::new(ctx) };
let _input = scope.root(input);
unsafe { clear_parse_detail(ctx) };
let result = unsafe { walk(ctx, &i, plan, plan.root, region) };
match result {
Ok(walked) => walked.value,
Err(fail) => {
unsafe { record_fail(ctx, fail, i.whole().bytes(&i)) };
unsafe { fault_sentinel(ctx) }
}
}
}
#[cfg(test)]
unsafe fn run_root(
ctx: *mut RuntimeContext,
plan: &ParserPlan,
input: GcRef,
) -> Result<GcRef, ParseFail> {
let i = unsafe { Input::new(input) }.expect("the test's input is a Text");
let region = i.whole();
let scope = unsafe { NativeScope::new(ctx) };
let _input = scope.root(input);
unsafe { walk(ctx, &i, plan, plan.root, region) }.map(|w| w.value)
}
unsafe fn fault_sentinel(ctx: *mut RuntimeContext) -> GcRef {
unsafe { set_parse_fault(ctx) };
unsafe { (*ctx).unit_ref }
}
unsafe fn set_parse_fault(ctx: *mut RuntimeContext) {
let fault = unsafe { &mut *(*ctx).pending_fault };
fault.set(crate::context::RaisedFault::PARSE_FAILED);
}
pub(crate) unsafe fn clear_parse_detail(ctx: *mut RuntimeContext) {
if unsafe { (*ctx).parse_detail.is_null() } {
return;
}
unsafe { (*(*ctx).parse_detail).clear() };
}
unsafe fn record_fail(ctx: *mut RuntimeContext, fail: ParseFail, input: &[u8]) {
if unsafe { (*ctx).parse_detail.is_null() } {
return;
}
unsafe { (*(*ctx).parse_detail).consider(fail, input) };
}
type WalkResult = Result<Walked, ParseFail>;
struct Rt {
ctx: *mut RuntimeContext,
}
unsafe fn heap_ref<'a>(ctx: *mut RuntimeContext) -> &'a crate::Heap {
unsafe { &*(*ctx).heap }
}
impl Rt {
fn safepoint(&self) -> (&crate::Heap, crate::heap::Safepoint<'_>) {
let heap = unsafe { heap_ref(self.ctx) };
let roots = unsafe { RuntimeRoots::from_context(self.ctx) };
let safepoint = heap.pace(&roots);
(heap, safepoint)
}
fn alloc_int(&self, value: i64) -> GcRef {
let (heap, safepoint) = self.safepoint();
match crate::small_int::index_of(value) {
Some(i) => {
drop(safepoint);
unsafe { *(*self.ctx).small_ints.add(i) }
}
None => heap.alloc(safepoint, scalars::INT_PAYLOAD, value),
}
}
fn alloc_char(&self, value: u32) -> GcRef {
let (heap, safepoint) = self.safepoint();
match crate::small_char::index_of(value) {
Some(i) => {
drop(safepoint);
unsafe { *(*self.ctx).small_chars.add(i) }
}
None => heap.alloc(safepoint, scalars::CHAR_PAYLOAD, value),
}
}
fn alloc_float(&self, value: f64) -> GcRef {
let (heap, safepoint) = self.safepoint();
heap.alloc(safepoint, scalars::FLOAT_PAYLOAD, value)
}
fn alloc_byte(&self, value: u8) -> GcRef {
let (heap, safepoint) = self.safepoint();
heap.alloc(safepoint, scalars::BYTE_PAYLOAD, value)
}
fn alloc_text_slice(&self, owner: GcRef, start: usize, len: usize) -> Option<GcRef> {
let slice = unsafe { crate::text::SourceSlice::new(owner, start, len) }?;
let payload = TextPayload::Slice(slice);
let (heap, safepoint) = self.safepoint();
Some(unsafe { heap.alloc_payload(safepoint, &crate::text::TEXT, payload) })
}
fn alloc_text_owned(&self, s: &str) -> GcRef {
let payload = TextPayload::owned(s);
let (heap, safepoint) = self.safepoint();
unsafe { heap.alloc_payload(safepoint, &crate::text::TEXT, payload) }
}
fn alloc_vec(
&self,
element_descriptor: &'static crate::TypeDescriptor,
items: Vec<GcRef>,
) -> GcRef {
let payload = crate::collections::VecPayload {
element_descriptor,
items: items.into(),
};
let (heap, safepoint) = self.safepoint();
unsafe { heap.alloc_payload(safepoint, &crate::collections::VEC, payload) }
}
fn alloc_enum(
&self,
schema: *const crate::enums::EnumSchema,
tag: u32,
items: Vec<GcRef>,
) -> GcRef {
let payload = crate::enums::EnumPayload { schema, tag, items };
let (heap, safepoint) = self.safepoint();
unsafe { heap.alloc_payload(safepoint, &crate::enums::ENUM, payload) }
}
}
unsafe fn walk(
ctx: *mut RuntimeContext,
i: &Input<'_>,
plan: &ParserPlan,
node: u32,
region: ByteRegion,
) -> WalkResult {
let rt = Rt { ctx };
let node = &plan.nodes[node as usize];
match node {
PlanNode::Atomic { kind } => walk_atomic(&rt, i, *kind, region),
PlanNode::Lines { child } => walk_lines(&rt, i, plan, *child, region),
PlanNode::Sections { child } => walk_sections(&rt, i, plan, *child, region),
PlanNode::SectionsNamed {
fields,
repeated_tail,
field_order,
} => walk_sections_named(&rt, i, plan, fields, *repeated_tail, field_order, region),
PlanNode::Block { items, field_order } => {
walk_block(&rt, i, plan, items, field_order, region)
}
PlanNode::Choice { cases } => walk_choice(&rt, i, plan, cases, region),
PlanNode::Optional { child } => walk_optional(&rt, i, plan, *child, region),
PlanNode::Scan { child } => walk_scan(&rt, i, plan, *child, region),
PlanNode::OneOf { chars_index } => {
let chars = plan.literals[*chars_index as usize];
walk_one_of(&rt, i, chars, region)
}
PlanNode::Characters { child, skip } => {
walk_characters(&rt, i, plan, *child, *skip, region)
}
PlanNode::Matrix { child } => walk_matrix(&rt, i, plan, *child, region),
PlanNode::GridRagged { child, fill_index } => {
let fill = plan.literals[*fill_index as usize];
walk_grid_ragged(&rt, i, plan, *child, fill, region)
}
PlanNode::Csv { child } => walk_csv(&rt, i, plan, *child, region),
PlanNode::Ws { child } => walk_ws(&rt, i, plan, *child, region),
PlanNode::Sep {
separator_index,
child,
} => {
let sep = plan.literals[*separator_index as usize];
walk_sep(&rt, i, plan, *child, sep, region)
}
PlanNode::Grid { child } => walk_grid(&rt, i, plan, *child, region),
PlanNode::Template { parts, field_order } => {
walk_template(&rt, i, plan, parts, field_order, region)
}
}
}
fn walk_atomic(rt: &Rt, i: &Input<'_>, kind: AtomicKind, region: ByteRegion) -> WalkResult {
let rest = region.bytes(i);
let s = trim_leading_ws(rest);
let at = region.start().advance(rest.len() - s.len());
let what = kind.keyword();
match kind {
AtomicKind::Int => {
let (digits, len) = take_int_run(s);
if digits.is_empty() {
return Err(ParseFail::at(at.offset(), 0, what));
}
let value: i64 = digits
.parse()
.map_err(|_| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: rt.alloc_int(value),
next: at.advance(len),
})
}
AtomicKind::Digit => {
let Some(&b) = s.first() else {
return Err(ParseFail::at(at.offset(), 0, what));
};
if !b.is_ascii_digit() {
return Err(ParseFail::at(at.offset(), 1, what));
}
let value = (b - b'0') as i64;
Ok(Walked {
value: rt.alloc_int(value),
next: at.advance(1),
})
}
AtomicKind::Char => {
let at = region.start();
let Some(next) = region.next_scalar(i, at) else {
return Err(ParseFail::at(at.offset(), 0, what));
};
let text = region
.subregion(at, next)
.str(i)
.ok_or_else(|| ParseFail::at(at.offset(), 0, what))?;
let ch = text
.chars()
.next()
.ok_or_else(|| ParseFail::at(at.offset(), 0, what))?;
Ok(Walked {
value: rt.alloc_char(ch as u32),
next,
})
}
AtomicKind::Word => {
let (word, len) = take_word_run(s);
if word.is_empty() {
return Err(ParseFail::at(at.offset(), 0, what));
}
let slice = rt
.alloc_text_slice(i.owner(), i.owner_offset(at.offset()), len)
.ok_or_else(|| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: slice,
next: at.advance(len),
})
}
AtomicKind::UInt => {
if s.first() == Some(&b'-') {
return Err(ParseFail::at(at.offset(), 1, what));
}
let (digits, len) = take_int_run(s);
if digits.is_empty() {
return Err(ParseFail::at(at.offset(), 0, what));
}
let value: i64 = digits
.parse()
.map_err(|_| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: rt.alloc_int(value),
next: at.advance(len),
})
}
AtomicKind::Float => {
let (text, len) = take_float_run(s);
if text.is_empty() {
return Err(ParseFail::at(at.offset(), 0, what));
}
let value: f64 = text
.parse()
.map_err(|_| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: rt.alloc_float(value),
next: at.advance(len),
})
}
AtomicKind::Byte => {
let (digits, len) = take_int_run(s);
if digits.is_empty() {
return Err(ParseFail::at(at.offset(), 0, what));
}
let value: u8 = digits
.parse()
.map_err(|_| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: rt.alloc_byte(value),
next: at.advance(len),
})
}
AtomicKind::Identifier => {
let len = take_ident_run(s);
if len == 0 {
return Err(ParseFail::at(at.offset(), 0, what));
}
let slice = rt
.alloc_text_slice(i.owner(), i.owner_offset(at.offset()), len)
.ok_or_else(|| ParseFail::at(at.offset(), len, what))?;
Ok(Walked {
value: slice,
next: at.advance(len),
})
}
AtomicKind::Text | AtomicKind::Rest => {
let start = region.start();
let len = region.end().delta_from(start);
let slice = rt
.alloc_text_slice(i.owner(), i.owner_offset(start.offset()), len)
.ok_or_else(|| ParseFail::at(start.offset(), len, what))?;
Ok(Walked {
value: slice,
next: region.end(),
})
}
}
}
#[derive(Clone, Copy)]
enum ExactBound {
Line,
Section,
Token,
Field,
Capture,
Fill,
}
impl ExactBound {
const fn describe(self) -> &'static str {
match self {
ExactBound::Line => "the rest of the line",
ExactBound::Section => "the rest of the section",
ExactBound::Token => "the rest of the token",
ExactBound::Field => "the rest of the field",
ExactBound::Capture => "the rest of the capture",
ExactBound::Fill => "the rest of the fill",
}
}
}
unsafe fn walk_exact(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
node: u32,
region: ByteRegion,
what: ExactBound,
) -> Result<GcRef, ParseFail> {
let walked = unsafe { walk(rt.ctx, i, plan, node, region)? };
if walked.next != region.end() && !region.from(walked.next).is_all_whitespace(i) {
return Err(ParseFail::at(
walked.next.offset(),
region.end().delta_from(walked.next),
what.describe(),
));
}
Ok(walked.value)
}
fn region_str<'a>(
i: &Input<'a>,
region: ByteRegion,
what: &'static str,
) -> Result<&'a str, ParseFail> {
region
.str(i)
.ok_or_else(|| ParseFail::at(region.start().offset(), region.len(), what))
}
fn whitespace_tokens(region: ByteRegion, s: &str) -> Vec<ByteRegion> {
let base = region.start();
let mut out = Vec::new();
let mut start: Option<usize> = None;
for (idx, ch) in s.char_indices() {
if ch.is_whitespace() {
if let Some(st) = start.take() {
out.push(region.subregion(base.advance(st), base.advance(idx)));
}
} else if start.is_none() {
start = Some(idx);
}
}
if let Some(st) = start {
out.push(region.subregion(base.advance(st), region.end()));
}
out
}
fn csv_tokens(region: ByteRegion, s: &str) -> Vec<ByteRegion> {
let base = region.start();
let mut out = Vec::new();
let mut field_start = 0usize;
for (idx, ch) in s.char_indices() {
if ch == ',' {
out.push(region.subregion(base.advance(field_start), base.advance(idx)));
field_start = idx + ch.len_utf8();
}
}
out.push(region.subregion(base.advance(field_start), region.end()));
out
}
unsafe fn walk_grid_row(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
line: ByteRegion,
items: &mut Vec<GcRef>,
scope: &NativeScope<'_>,
) -> Result<usize, ParseFail> {
let mut cells = 0usize;
let mut cursor = line.start();
while cursor < line.end() {
let walked = match unsafe { walk(rt.ctx, i, plan, child, line.from(cursor)) } {
Ok(walked) => walked,
Err(fail) => {
if line.from(cursor).is_all_whitespace(i) {
break;
}
return Err(fail);
}
};
if walked.next <= cursor {
return Err(ParseFail::at(cursor.offset(), 0, "a cell that reads input"));
}
scope.root(walked.value);
items.push(walked.value);
cursor = walked.next;
cells += 1;
}
Ok(cells)
}
fn uniform_row_width(
first: Option<usize>,
count: usize,
line: ByteRegion,
expected: &'static str,
) -> Result<usize, ParseFail> {
match first {
None => Ok(count),
Some(w) if w != count => Err(ParseFail::at(line.start().offset(), line.len(), expected)),
Some(w) => Ok(w),
}
}
fn walk_lines(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut items = Vec::new();
let lines = split_lines(i, region);
let blank_run = trailing_blank_run(i, &lines);
for (n, line) in lines.iter().enumerate() {
match unsafe { walk_exact(rt, i, plan, child, *line, ExactBound::Line) } {
Ok(value) => {
scope.root(value);
items.push(value);
}
Err(fail) => {
if n < blank_run {
return Err(fail);
}
}
}
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_sections(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut items = Vec::new();
for section in split_sections(i, region) {
let value = unsafe { walk_exact(rt, i, plan, child, section, ExactBound::Section)? };
scope.root(value);
items.push(value);
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_sections_named(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
fields: &'static [SectionItemNode],
repeated_tail: Option<(&'static str, u32)>,
field_order: &'static [&'static str],
region: ByteRegion,
) -> WalkResult {
let sections = split_sections(i, region);
let required: usize = fields.iter().map(SectionItemNode::sections_wanted).sum();
if sections.len() < required {
return Err(ParseFail::at(
region.start().offset(),
region.len(),
sections_shortfall(fields, sections.len()),
));
}
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
let mut at = 0usize;
for item in fields {
match item {
SectionItemNode::One { name, child } => {
let value =
unsafe { walk_exact(rt, i, plan, *child, sections[at], ExactBound::Section)? };
scope.root(value);
captures.push((Some(*name), *child, value));
}
SectionItemNode::Counted { name, child, count } => {
let mut group = Vec::with_capacity(*count as usize);
for section in §ions[at..at + *count as usize] {
let value =
unsafe { walk_exact(rt, i, plan, *child, *section, ExactBound::Section)? };
scope.root(value);
group.push(value);
}
let elem_desc = child_descriptor(plan, *child);
let group_vec = rt.alloc_vec(elem_desc, group);
scope.root(group_vec);
captures.push((Some(*name), *child, group_vec));
}
}
at += item.sections_wanted();
}
if let Some((tail_name, tail_child)) = repeated_tail {
let mut tail_items = Vec::new();
for section in §ions[at..] {
let value =
unsafe { walk_exact(rt, i, plan, tail_child, *section, ExactBound::Section)? };
scope.root(value);
tail_items.push(value);
}
let elem_desc = child_descriptor(plan, tail_child);
let tail_vec = rt.alloc_vec(elem_desc, tail_items);
scope.root(tail_vec);
captures.push((Some(tail_name), tail_child, tail_vec));
}
let record = alloc_record(rt, &captures, field_order);
Ok(Walked {
value: record,
next: region.end(),
})
}
fn sections_shortfall(fields: &'static [SectionItemNode], available: usize) -> String {
let mut at = 0usize;
for item in fields {
let wanted = item.sections_wanted();
if at + wanted > available {
if let SectionItemNode::Counted { name, count, .. } = item {
return format!("{count} sections for `{name}`");
}
break;
}
at += wanted;
}
"section header".to_string()
}
fn walk_block(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
items: &'static [praxis_input_parser::BlockItemNode],
field_order: &'static [&'static str],
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut cursor = region.start();
let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
for (n, item) in items.iter().enumerate() {
if n > 0 {
cursor = skip_line_boundary(i, region, cursor);
}
match item {
praxis_input_parser::BlockItemNode::Positional { child } => {
let walked = unsafe {
walk(
rt.ctx,
i,
plan,
*child,
block_item_window(i, plan, *child, region, cursor),
)?
};
scope.root(walked.value);
cursor = walked.next;
if std::ptr::eq(walked.value.descriptor(), &crate::records::RECORD) {
flatten_record_into(rt, walked.value, &mut captures);
}
}
praxis_input_parser::BlockItemNode::Named { name, child } => {
let walked = unsafe {
walk(
rt.ctx,
i,
plan,
*child,
block_item_window(i, plan, *child, region, cursor),
)?
};
scope.root(walked.value);
cursor = walked.next;
captures.push((Some(name), *child, walked.value));
}
}
}
let record = alloc_record(rt, &captures, field_order);
Ok(Walked {
value: record,
next: cursor,
})
}
fn block_item_window(
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
cursor: Cursor,
) -> ByteRegion {
let PlanNode::Template { parts, .. } = &plan.nodes[child as usize] else {
return region.from(cursor);
};
let extra = parts
.iter()
.filter(|p| {
matches!(
p,
praxis_input_parser::TemplatePartNode::Literal {
ws: praxis_input_parser::WsPolicy::Newline,
..
}
)
})
.count();
region.subregion(cursor, cursor::line_window_end(i, region, cursor, extra))
}
fn skip_line_boundary(i: &Input<'_>, region: ByteRegion, cursor: Cursor) -> Cursor {
let tail = region.from(cursor);
let bytes = tail.bytes(i);
let mut n = horizontal_ws_run(bytes);
if bytes.get(n) == Some(&b'\r') {
n += 1;
}
if bytes.get(n) == Some(&b'\n') {
n += 1;
}
cursor.advance(n)
}
fn flatten_record_into(
_rt: &Rt,
record_ref: GcRef,
captures: &mut Vec<(Option<&'static str>, u32, GcRef)>,
) {
let payload = record_ref.payload::<u8>() as *const crate::records::RecordPayload;
let (schema, items) = unsafe {
let p = &*payload;
(p.schema, &p.items)
};
let schema = unsafe { &*schema };
for (n, field) in schema.fields.iter().enumerate() {
if let Some(value) = items.get(n) {
captures.push((Some(field.name), u32::MAX, *value));
}
}
}
fn walk_choice(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
cases: &'static [(&'static str, u32)],
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut deepest: Option<ParseFail> = None;
for (tag, (_name, child)) in cases.iter().enumerate() {
match unsafe { walk(rt.ctx, i, plan, *child, region) } {
Ok(walked) => {
scope.root(walked.value);
let schema = enum_schema_for(cases);
let enum_ref = rt.alloc_enum(schema, tag as u32, vec![walked.value]);
return Ok(Walked {
value: enum_ref,
next: walked.next,
});
}
Err(inner) => {
let deeper = match &deepest {
None => true,
Some(best) => inner.input_span.0 > best.input_span.0,
};
if deeper {
deepest = Some(inner);
}
}
}
}
Err(deepest.unwrap_or_else(|| ParseFail::at(region.start().offset(), 0, "any choice case")))
}
fn walk_optional(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
match unsafe { walk(rt.ctx, i, plan, child, region) } {
Ok(walked) => {
scope.root(walked.value);
let some_ref = rt.alloc_enum(crate::enums::option_schema(), 0, vec![walked.value]);
Ok(Walked {
value: some_ref,
next: walked.next,
})
}
Err(_) => {
let none_ref = rt.alloc_enum(crate::enums::option_schema(), 1, Vec::new());
Ok(Walked {
value: none_ref,
next: region.start(),
})
}
}
}
fn walk_scan(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut items = Vec::new();
let mut cursor = region.start();
while cursor < region.end() {
match unsafe { walk(rt.ctx, i, plan, child, region.from(cursor)) } {
Ok(walked) => {
scope.root(walked.value);
items.push(walked.value);
cursor = if walked.next > cursor {
walked.next
} else {
match region.next_scalar(i, cursor) {
Some(next) => next,
None => break,
}
};
}
Err(_) => {
cursor = match region.next_scalar(i, cursor) {
Some(next) => next,
None => break,
};
}
}
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_one_of(rt: &Rt, i: &Input<'_>, chars: &str, region: ByteRegion) -> WalkResult {
let at = region.start();
let Some(next) = region.next_scalar(i, at) else {
return Err(ParseFail::at(at.offset(), 0, "char"));
};
let ch = region
.subregion(at, next)
.str(i)
.and_then(|t| t.chars().next())
.ok_or_else(|| ParseFail::at(at.offset(), 0, "char"))?;
if !chars.contains(ch) {
return Err(ParseFail::at(
at.offset(),
ch.len_utf8(),
format!("one of \"{chars}\""),
));
}
Ok(Walked {
value: rt.alloc_char(ch as u32),
next,
})
}
fn walk_characters(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
skip: praxis_input_parser::SkipPolicy,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut items = Vec::new();
let mut cursor = region.start();
loop {
cursor = skip_chars(i, region, cursor, skip);
if cursor >= region.end() {
break;
}
let walked = match unsafe { walk(rt.ctx, i, plan, child, region.from(cursor)) } {
Ok(walked) => walked,
Err(fail) => {
if region.from(cursor).is_all_whitespace(i) {
break;
}
return Err(fail);
}
};
cursor = if walked.next > cursor {
walked.next
} else {
match region.next_scalar(i, cursor) {
Some(next) => next,
None => break,
}
};
scope.root(walked.value);
items.push(walked.value);
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn skip_chars(
i: &Input<'_>,
region: ByteRegion,
cursor: Cursor,
skip: praxis_input_parser::SkipPolicy,
) -> Cursor {
use praxis_input_parser::SkipPolicy;
let bytes = region.from(cursor).bytes(i);
let n = match skip {
SkipPolicy::None => 0,
SkipPolicy::Whitespace => horizontal_ws_run(bytes),
SkipPolicy::Newlines => ascii_ws_run(bytes),
};
cursor.advance(n)
}
fn walk_matrix(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let lines = split_lines(i, region);
let blank_run = trailing_blank_run(i, &lines);
let mut items = Vec::with_capacity(lines.len());
let mut width: Option<usize> = None;
for (n, line) in lines.iter().enumerate() {
let text = region_str(i, *line, "matrix row")?;
let tokens = whitespace_tokens(*line, text);
if tokens.is_empty() && n >= blank_run {
continue;
}
width = Some(uniform_row_width(
width,
tokens.len(),
*line,
"rectangular matrix row",
)?);
for token in &tokens {
let value = unsafe { walk_exact(rt, i, plan, child, *token, ExactBound::Token)? };
scope.root(value);
items.push(value);
}
}
let width = width.unwrap_or(0);
let elem_desc = child_descriptor(plan, child);
alloc_grid(rt, elem_desc, items, width, region.end())
}
fn walk_grid_ragged(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
fill: &str,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let lines = split_lines(i, region);
let fill_owner = rt.alloc_text_owned(fill);
scope.root(fill_owner);
let fill_input = unsafe { Input::new(fill_owner) }
.ok_or_else(|| ParseFail::at(region.start().offset(), 0, "grid fill"))?;
let fill_region = fill_input.whole();
let fill_value =
unsafe { walk_exact(rt, &fill_input, plan, child, fill_region, ExactBound::Fill)? };
scope.root(fill_value);
let mut items = Vec::new();
let mut rows = Vec::with_capacity(lines.len());
let blank_run = trailing_blank_run(i, &lines);
for (n, line) in lines.iter().enumerate() {
let cells = unsafe { walk_grid_row(rt, i, plan, child, *line, &mut items, &scope)? };
if cells == 0 && n >= blank_run {
continue;
}
rows.push(cells);
}
let width = rows.iter().copied().max().unwrap_or(0);
let mut at = items.len();
for (n, cells) in rows.iter().enumerate().rev() {
at -= cells;
for _ in *cells..width {
items.insert(at + cells, fill_value);
}
let _ = n;
}
let elem_desc = child_descriptor(plan, child);
alloc_grid(rt, elem_desc, items, width, region.end())
}
fn alloc_grid(
rt: &Rt,
elem_desc: &'static crate::TypeDescriptor,
items: Vec<GcRef>,
width: usize,
next: Cursor,
) -> WalkResult {
let payload = crate::collections::GridPayload {
element_descriptor: elem_desc,
items,
width,
};
let (heap, safepoint) = rt.safepoint();
let grid_ref = unsafe { heap.alloc_payload(safepoint, &crate::collections::GRID, payload) };
Ok(Walked {
value: grid_ref,
next,
})
}
fn walk_csv(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let text = region_str(i, region, "csv")?;
let mut items = Vec::new();
for token in csv_tokens(region, text) {
let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Field)? };
scope.root(value);
items.push(value);
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_ws(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let text = region_str(i, region, "whitespace-separated tokens")?;
let mut items = Vec::new();
for token in whitespace_tokens(region, text) {
let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
scope.root(value);
items.push(value);
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_sep(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
sep: &str,
region: ByteRegion,
) -> WalkResult {
let bytes = region.bytes(i);
let base = region.start();
let sep_bytes = sep.as_bytes();
debug_assert!(
!sep_bytes.is_empty(),
"Separator::new refuses an empty separator (IP-10): the loop below cannot advance past one"
);
if sep_bytes.is_empty() {
return Err(ParseFail::at(base.offset(), 0, "a non-empty separator"));
}
let scope = unsafe { NativeScope::new(rt.ctx) };
let mut items = Vec::new();
let mut token_start = 0usize;
let mut pos = 0usize;
while pos < bytes.len() {
if bytes[pos..].starts_with(sep_bytes) {
let token = region.subregion(base.advance(token_start), base.advance(pos));
let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
scope.root(value);
items.push(value);
pos += sep_bytes.len();
token_start = pos;
} else {
pos += 1;
}
}
if token_start < bytes.len() {
let token = region.subregion(base.advance(token_start), region.end());
let value = unsafe { walk_exact(rt, i, plan, child, token, ExactBound::Token)? };
scope.root(value);
items.push(value);
}
let elem_desc = child_descriptor(plan, child);
Ok(Walked {
value: rt.alloc_vec(elem_desc, items),
next: region.end(),
})
}
fn walk_grid(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
child: u32,
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let lines = split_lines(i, region);
let blank_run = trailing_blank_run(i, &lines);
let mut items = Vec::new();
let mut width: Option<usize> = None;
for (n, line) in lines.iter().enumerate() {
let cells = unsafe { walk_grid_row(rt, i, plan, child, *line, &mut items, &scope)? };
if cells == 0 && n >= blank_run {
continue;
}
width = Some(uniform_row_width(
width,
cells,
*line,
"a grid row of the same cell count as the first",
)?);
}
let width = width.unwrap_or(0);
let elem_desc = child_descriptor(plan, child);
alloc_grid(rt, elem_desc, items, width, region.end())
}
fn following_bound(
parts: &[praxis_input_parser::TemplatePartNode],
index: usize,
) -> Option<&[praxis_input_parser::TemplatePartNode]> {
use praxis_input_parser::{TemplatePartNode, WsPolicy};
let rest = &parts[index + 1..];
let len = rest
.iter()
.take_while(|p| matches!(p, TemplatePartNode::Literal { .. }))
.count();
let run = &rest[..len];
let constrains = run.iter().any(|p| match p {
TemplatePartNode::Literal { text, ws } => {
!text.is_empty() || !matches!(ws, WsPolicy::None | WsPolicy::ZeroOrMore)
}
_ => false,
});
constrains.then_some(run)
}
fn match_literal_run(
i: &Input<'_>,
region: ByteRegion,
base: Cursor,
bytes: &[u8],
at: Cursor,
run: &[praxis_input_parser::TemplatePartNode],
) -> Option<Cursor> {
let mut cursor = at;
for part in run {
let praxis_input_parser::TemplatePartNode::Literal { text, ws } = part else {
return None;
};
cursor = base.advance(consume_ws(bytes, cursor.delta_from(base), *ws)?);
if !region.from(cursor).bytes(i).starts_with(text.as_bytes()) {
return None;
}
cursor = cursor.advance(text.len());
}
Some(cursor)
}
fn capture_bound(
i: &Input<'_>,
region: ByteRegion,
base: Cursor,
cursor: Cursor,
run: &[praxis_input_parser::TemplatePartNode],
) -> Option<Cursor> {
let bytes = region.bytes(i);
let mut at = cursor;
loop {
if match_literal_run(i, region, base, bytes, at, run).is_some() {
return Some(at);
}
at = region.next_scalar(i, at)?;
}
}
fn walk_template(
rt: &Rt,
i: &Input<'_>,
plan: &ParserPlan,
parts: &[praxis_input_parser::TemplatePartNode],
field_order: &'static [&'static str],
region: ByteRegion,
) -> WalkResult {
let scope = unsafe { NativeScope::new(rt.ctx) };
let base = region.start();
let bytes = region.bytes(i);
let mut cursor = base;
let mut captures: Vec<(Option<&'static str>, u32, GcRef)> = Vec::new();
for (index, part) in parts.iter().enumerate() {
match part {
praxis_input_parser::TemplatePartNode::Literal { text, ws } => {
let Some(after) = consume_ws(bytes, cursor.delta_from(base), *ws) else {
return Err(ParseFail::at(cursor.offset(), 0, "whitespace"));
};
cursor = base.advance(after);
let lit = text.as_bytes();
if !region.from(cursor).bytes(i).starts_with(lit) {
return Err(ParseFail::at(
cursor.offset(),
lit.len(),
format!("literal {:?}", text),
));
}
cursor = cursor.advance(lit.len());
}
praxis_input_parser::TemplatePartNode::Capture {
child,
field_index: _,
name,
} => {
let search = base.advance(skip_capture_ws(bytes, cursor.delta_from(base)));
match following_bound(parts, index) {
Some(bound) => {
match capture_bound(i, region, base, search, bound) {
Some(bound) => {
let value = unsafe {
walk_exact(
rt,
i,
plan,
*child,
region.subregion(cursor, bound),
ExactBound::Capture,
)?
};
scope.root(value);
cursor = bound;
captures.push((*name, *child, value));
}
None => {
let walked =
unsafe { walk(rt.ctx, i, plan, *child, region.from(cursor))? };
scope.root(walked.value);
cursor = walked.next;
captures.push((*name, *child, walked.value));
}
}
}
None => {
let walked = unsafe { walk(rt.ctx, i, plan, *child, region.from(cursor))? };
scope.root(walked.value);
cursor = walked.next;
captures.push((*name, *child, walked.value));
}
}
}
}
}
let value = match (TemplateShape::of(parts), captures.as_slice()) {
(TemplateShape::Record, _) => alloc_record(rt, &captures, field_order),
(TemplateShape::Scalar { .. }, [(_, _, only)]) => *only,
(TemplateShape::Tuple, _) => {
let children: Vec<u32> = captures.iter().map(|(_, c, _)| *c).collect();
let values: Vec<GcRef> = captures.iter().map(|(_, _, v)| *v).collect();
alloc_tuple(rt, &children, plan, values)
}
_ => alloc_unit(rt),
};
Ok(Walked {
value,
next: cursor,
})
}
fn skip_capture_ws(bytes: &[u8], cursor: usize) -> usize {
let Some(rest) = bytes.get(cursor..) else {
return cursor;
};
cursor + horizontal_ws_run(rest)
}
fn consume_ws(bytes: &[u8], cursor: usize, ws: praxis_input_parser::WsPolicy) -> Option<usize> {
use praxis_input_parser::WsPolicy;
let rest = bytes.get(cursor..)?;
let mut i = 0;
match ws {
WsPolicy::None => {
}
WsPolicy::SpaceRun => {
i = horizontal_ws_run(rest);
if i == 0 {
return None;
}
}
WsPolicy::ZeroOrMore => {
i = ascii_ws_run(rest);
}
WsPolicy::OneOrMore => {
i = ascii_ws_run(rest);
if i == 0 {
return None;
}
}
WsPolicy::ExactSpace => {
if rest.first() == Some(&b' ') {
i = 1;
} else {
return None;
}
}
WsPolicy::Newline => {
if rest.first() == Some(&b'\r') {
i = 1;
}
if rest.get(i) == Some(&b'\n') {
i += 1;
} else {
return None;
}
}
WsPolicy::Tab => {
if rest.first() == Some(&b'\t') {
i = 1;
} else {
return None;
}
}
}
Some(cursor + i)
}
fn alloc_unit(rt: &Rt) -> GcRef {
unsafe { (*rt.ctx).unit_ref }
}
fn alloc_record(
rt: &Rt,
captures: &[(Option<&'static str>, u32, GcRef)],
field_order: &'static [&'static str],
) -> GcRef {
let ordered = canonical_captures(captures, field_order);
let fields: Vec<crate::records::RecordField> = ordered
.iter()
.map(|(name, _child, value)| crate::records::RecordField {
name: name.unwrap_or("_"),
descriptor: value.descriptor(),
})
.collect();
let schema = record_schema_for(fields);
let items: Vec<GcRef> = ordered.iter().map(|(_, _, v)| *v).collect();
let payload = crate::records::RecordPayload { schema, items };
let (heap, safepoint) = rt.safepoint();
unsafe { heap.alloc_payload(safepoint, &crate::records::RECORD, payload) }
}
fn canonical_captures<'a>(
captures: &'a [(Option<&'static str>, u32, GcRef)],
field_order: &'static [&'static str],
) -> std::borrow::Cow<'a, [(Option<&'static str>, u32, GcRef)]> {
let agrees = field_order.len() == captures.len()
&& captures
.iter()
.zip(field_order)
.all(|((name, _, _), want)| *name == Some(*want));
if agrees || field_order.is_empty() {
return std::borrow::Cow::Borrowed(captures);
}
let mut ordered: Vec<(Option<&'static str>, u32, GcRef)> = Vec::with_capacity(captures.len());
for want in field_order {
if let Some(c) = captures
.iter()
.find(|(name, _, _)| *name == Some(*want) && !ordered.iter().any(|o| o.0 == *name))
{
ordered.push(*c);
}
}
for c in captures {
if !ordered.iter().any(|o| o.0 == c.0) {
ordered.push(*c);
}
}
std::borrow::Cow::Owned(ordered)
}
fn alloc_tuple(rt: &Rt, elements: &[u32], plan: &ParserPlan, values: Vec<GcRef>) -> GcRef {
let descriptors: Vec<*const crate::TypeDescriptor> = elements
.iter()
.map(|&e| child_descriptor(plan, e) as *const _)
.collect();
let schema = tuple_schema_for(descriptors);
let payload = crate::tuples::TuplePayload {
schema,
items: values,
};
let (heap, safepoint) = rt.safepoint();
unsafe { heap.alloc_payload(safepoint, &crate::tuples::TUPLE, payload) }
}
struct RecordSchemaEntry {
key: Vec<(&'static str, usize)>,
#[allow(dead_code)]
fields: Box<[crate::records::RecordField]>,
schema: Box<crate::records::RecordSchema>,
}
struct TupleSchemaEntry {
key: Vec<usize>,
#[allow(dead_code)]
descriptors: Box<[*const crate::TypeDescriptor]>,
schema: Box<crate::tuples::TupleSchema>,
}
struct EnumSchemaEntry {
key: Vec<&'static str>,
#[allow(dead_code)]
variants: Box<[crate::enums::EnumVariantShape]>,
#[allow(dead_code)]
payloads: Box<[*const crate::TypeDescriptor]>,
schema: Box<crate::enums::EnumSchema>,
}
#[derive(Default)]
struct ParserSchemas {
records: Vec<RecordSchemaEntry>,
tuples: Vec<TupleSchemaEntry>,
enums: Vec<EnumSchemaEntry>,
}
unsafe impl Send for ParserSchemas {}
static SCHEMAS: std::sync::Mutex<Option<ParserSchemas>> = std::sync::Mutex::new(None);
pub(crate) unsafe fn retire_schemas() {
*SCHEMAS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
fn with_schemas<R>(f: impl FnOnce(&mut ParserSchemas) -> R) -> R {
let mut guard = SCHEMAS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
f(guard.get_or_insert_with(ParserSchemas::default))
}
unsafe fn erase_lifetime<T: 'static>(slice: &[T]) -> &'static [T] {
unsafe { &*(slice as *const [T]) }
}
fn record_schema_for(
fields: Vec<crate::records::RecordField>,
) -> *const crate::records::RecordSchema {
with_schemas(|cache| {
let key: Vec<(&'static str, usize)> = fields
.iter()
.map(|f| (f.name, f.descriptor as usize))
.collect();
if let Some(entry) = cache.records.iter().find(|e| e.key == key) {
return &*entry.schema as *const _;
}
let fields: Box<[crate::records::RecordField]> = fields.into_boxed_slice();
let borrowed = unsafe { erase_lifetime(&fields) };
let schema = Box::new(crate::records::RecordSchema {
identity: crate::records::SchemaIdentity::Anonymous,
fields: borrowed,
});
let raw: *const crate::records::RecordSchema = &*schema;
cache.records.push(RecordSchemaEntry {
key,
fields,
schema,
});
raw
})
}
fn enum_schema_for(cases: &'static [(&'static str, u32)]) -> *const crate::enums::EnumSchema {
with_schemas(|cache| {
let key: Vec<&'static str> = cases.iter().map(|(name, _)| *name).collect();
if let Some(entry) = cache.enums.iter().find(|e| e.key == key) {
return &*entry.schema as *const _;
}
let payloads: Box<[*const crate::TypeDescriptor]> =
vec![std::ptr::null(); cases.len()].into_boxed_slice();
let variants: Box<[crate::enums::EnumVariantShape]> = key
.iter()
.enumerate()
.map(|(i, name)| {
let slot = unsafe { erase_lifetime(&payloads[i..=i]) };
crate::enums::EnumVariantShape {
name,
payload: slot,
}
})
.collect::<Vec<_>>()
.into_boxed_slice();
let borrowed = unsafe { erase_lifetime(&variants) };
let schema = Box::new(crate::enums::EnumSchema {
identity: crate::records::SchemaIdentity::Anonymous,
variants: borrowed,
});
let raw: *const crate::enums::EnumSchema = &*schema;
cache.enums.push(EnumSchemaEntry {
key,
variants,
payloads,
schema,
});
raw
})
}
fn tuple_schema_for(
descriptors: Vec<*const crate::TypeDescriptor>,
) -> *const crate::tuples::TupleSchema {
with_schemas(|cache| {
let key: Vec<usize> = descriptors.iter().map(|p| *p as usize).collect();
if let Some(entry) = cache.tuples.iter().find(|e| e.key == key) {
return &*entry.schema as *const _;
}
let descriptors: Box<[*const crate::TypeDescriptor]> = descriptors.into_boxed_slice();
let borrowed = unsafe { erase_lifetime(&descriptors) };
let schema = Box::new(crate::tuples::TupleSchema {
descriptors: borrowed,
});
let raw: *const crate::tuples::TupleSchema = &*schema;
cache.tuples.push(TupleSchemaEntry {
key,
descriptors,
schema,
});
raw
})
}
fn trim_leading_ws(bytes: &[u8]) -> &[u8] {
&bytes[horizontal_ws_run(bytes)..]
}
pub(crate) fn take_int_run(bytes: &[u8]) -> (&str, usize) {
let mut end = 0;
if end < bytes.len() && bytes[end] == b'-' {
end += 1;
}
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
(s, end)
}
pub(crate) fn take_float_run(bytes: &[u8]) -> (&str, usize) {
let mut end = 0;
if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
end += 1;
}
let int_start = end;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
let mut saw_digit = end > int_start;
if end < bytes.len() && bytes[end] == b'.' {
let after_dot = end + 1;
let mut frac = after_dot;
while frac < bytes.len() && bytes[frac].is_ascii_digit() {
frac += 1;
}
if frac > after_dot {
saw_digit = true;
end = frac;
}
}
if !saw_digit {
return ("", 0);
}
if end < bytes.len() && (bytes[end] == b'e' || bytes[end] == b'E') {
let mut exp = end + 1;
if exp < bytes.len() && (bytes[exp] == b'-' || bytes[exp] == b'+') {
exp += 1;
}
let digits_start = exp;
while exp < bytes.len() && bytes[exp].is_ascii_digit() {
exp += 1;
}
if exp > digits_start {
end = exp;
}
}
let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
(s, end)
}
fn take_ident_run(bytes: &[u8]) -> usize {
let s = match std::str::from_utf8(bytes) {
Ok(s) => s,
Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or_default(),
};
praxis_syntax::ident::ident_run_len(s)
}
fn take_word_run(bytes: &[u8]) -> (&str, usize) {
let mut end = 0;
while end < bytes.len()
&& !is_ws(bytes[end])
&& bytes[end] != b','
&& bytes[end] != b'\n'
&& bytes[end] != b'\r'
{
end += 1;
}
let s = std::str::from_utf8(&bytes[..end]).unwrap_or("");
(s, end)
}
fn is_ws(b: u8) -> bool {
b == b' ' || b == b'\t'
}
fn horizontal_ws_run(bytes: &[u8]) -> usize {
bytes.iter().take_while(|&&b| is_ws(b)).count()
}
fn ascii_ws_run(bytes: &[u8]) -> usize {
bytes.iter().take_while(|b| b.is_ascii_whitespace()).count()
}
fn child_descriptor(plan: &ParserPlan, child: u32) -> &'static crate::TypeDescriptor {
match &plan.nodes[child as usize] {
PlanNode::Atomic { kind } => atomic_descriptor(*kind),
PlanNode::Lines { .. }
| PlanNode::Sections { .. }
| PlanNode::Csv { .. }
| PlanNode::Ws { .. }
| PlanNode::Sep { .. }
| PlanNode::Scan { .. } => &crate::collections::VEC,
PlanNode::Grid { .. } => &crate::collections::GRID,
PlanNode::SectionsNamed { .. } => &crate::records::RECORD,
PlanNode::Block { .. } => &crate::records::RECORD,
PlanNode::Choice { .. } | PlanNode::Optional { .. } => &crate::enums::ENUM,
PlanNode::OneOf { .. } => &scalars::CHAR,
PlanNode::Characters { .. } => &crate::collections::VEC,
PlanNode::Matrix { .. } | PlanNode::GridRagged { .. } => &crate::collections::GRID,
PlanNode::Template { parts, .. } => template_result_descriptor(plan, parts),
}
}
fn atomic_descriptor(kind: AtomicKind) -> &'static crate::TypeDescriptor {
match AtomicClass::of(kind) {
AtomicClass::Int => &scalars::INT,
AtomicClass::Float => &scalars::FLOAT,
AtomicClass::Byte => &scalars::BYTE,
AtomicClass::Char => &scalars::CHAR,
AtomicClass::Text => &crate::text::TEXT,
}
}
fn template_result_descriptor(
plan: &ParserPlan,
parts: &[praxis_input_parser::TemplatePartNode],
) -> &'static crate::TypeDescriptor {
match TemplateShape::of(parts) {
TemplateShape::Unit => &scalars::UNIT,
TemplateShape::Scalar { child } => child_descriptor(plan, child),
TemplateShape::Record => &crate::records::RECORD,
TemplateShape::Tuple => &crate::tuples::TUPLE,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_plan(nodes: Vec<PlanNode>, root: u32) -> ParserPlan {
ParserPlan {
nodes: Box::leak(nodes.into_boxed_slice()),
template_parts: &[],
literals: &[],
root,
}
}
#[test]
fn a_method_and_an_atomic_read_the_same_number() {
fn whole(s: &str, run: fn(&[u8]) -> (&str, usize)) -> bool {
let t = s.trim();
let (text, len) = run(t.as_bytes());
!text.is_empty() && len == t.len()
}
for s in ["+5", "1_000"] {
assert!(s.parse::<i64>().is_ok() || s == "1_000");
assert!(!whole(s, take_int_run), "`{s}` is not an `int`");
}
for s in ["1.", "inf", "infinity", "nan", "NaN", "-inf"] {
assert!(s.parse::<f64>().is_ok(), "`{s}` is a Rust float");
assert!(!whole(s, take_float_run), "`{s}` is not a §7.4 `float`");
}
for s in ["12", " -7 ", "0"] {
assert!(whole(s, take_int_run), "`{s}` is an `int`");
}
for s in ["1.5", " -2 ", "+5.0", "1e10", "3"] {
assert!(whole(s, take_float_run), "`{s}` is a `float`");
}
assert_eq!(take_int_run(b"12abc"), ("12", 2));
assert!(!whole("12abc", take_int_run));
}
#[test]
fn take_int_run_parses_negative() {
let (s, len) = take_int_run(b"-42abc");
assert_eq!(s, "-42");
assert_eq!(len, 3);
}
#[test]
fn every_atomic_the_design_requires_has_a_parser_and_a_type() {
fn parse_one(kind: AtomicKind, input: &str) -> Option<(crate::Runtime, GcRef, usize)> {
let mut rt = crate::Runtime::new();
let text = rt.alloc_text(input);
let mut ctx = rt.context();
ctx.input_source = text;
let plan = test_plan(vec![PlanNode::Atomic { kind }], 0);
let i = unsafe { Input::new(text) }.expect("a Text is UTF-8");
let out = unsafe { walk(&mut ctx, &i, &plan, plan.root, i.whole()) };
out.ok().map(|w| (rt, w.value, w.next.offset()))
}
for kind in AtomicKind::ALL {
let _ = atomic_descriptor(*kind);
}
let (_rt, v, consumed) = parse_one(AtomicKind::UInt, "42rest").expect("uint reads 42");
assert_eq!(v.as_int(), 42);
assert_eq!(consumed, 2);
assert!(
parse_one(AtomicKind::UInt, "-1").is_none(),
"`uint` refuses a negative"
);
let (_rt, v, _) = parse_one(AtomicKind::Int, "-1").expect("int reads -1");
assert_eq!(v.as_int(), -1);
for (input, expected, consumed) in [
("3.5", 3.5_f64, 3),
("-0.25x", -0.25, 5),
("2", 2.0, 1),
("1e3", 1000.0, 3),
("1.5e-2", 0.015, 6),
("7.", 7.0, 1),
] {
let (_rt, v, got) = parse_one(AtomicKind::Float, input)
.unwrap_or_else(|| panic!("float reads {input}"));
assert_eq!(v.as_float(), expected, "for {input}");
assert_eq!(got, consumed, "for {input}");
}
assert!(parse_one(AtomicKind::Float, "x").is_none());
let (_rt, v, _) = parse_one(AtomicKind::Byte, "255").expect("byte reads 255");
assert_eq!(v.as_byte(), 255);
assert!(
parse_one(AtomicKind::Byte, "256").is_none(),
"256 is not a byte"
);
assert!(
parse_one(AtomicKind::Byte, "-1").is_none(),
"-1 is not a byte"
);
for (input, expected) in [
("name rest", "name"),
("λx-1", "λx"),
("_x9=2", "_x9"),
("日本語:", "日本語"),
] {
let (_rt, v, _) = parse_one(AtomicKind::Identifier, input)
.unwrap_or_else(|| panic!("identifier reads {input}"));
assert_eq!(v.as_text(), expected, "for {input}");
}
assert!(
parse_one(AtomicKind::Identifier, "9x").is_none(),
"a digit does not start an identifier"
);
}
#[test]
fn text_slices_in_later_sections_point_at_their_actual_source_bytes() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("first\n\nsecond");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Word,
},
PlanNode::Sections { child: 0 },
],
1,
);
let result =
unsafe { run_root(&mut ctx, &plan, input) }.expect("sections(word) should parse");
let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
assert_eq!(values, vec!["first", "second"]);
}
#[test]
fn a_parse_of_a_non_input_text_owns_its_slices() {
let mut rt = crate::Runtime::new();
let stdin_buffer = rt.alloc_text("XXXXXXXXXXXXXXXX");
let subject = rt.alloc_text("alpha beta");
let mut ctx = rt.context();
ctx.input_source = stdin_buffer;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Word,
},
PlanNode::Ws { child: 0 },
],
1,
);
let result = unsafe { run_root(&mut ctx, &plan, subject) }.expect("ws(word) should parse");
let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
assert_eq!(
values,
vec!["alpha", "beta"],
"a parse's slices must be views of the text it parsed, not of ctx.input_source"
);
}
#[test]
fn a_parse_of_a_slice_does_not_extend_the_owner_chain() {
let mut rt = crate::Runtime::new();
let owned = rt.alloc_text("XXalpha betaXX");
let subject = unsafe { rt.alloc_text_slice(owned, 2, 10) }.expect("[2, 12) is in range");
assert_eq!(subject.as_text(), "alpha beta");
let mut ctx = rt.context();
ctx.input_source = owned;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Word,
},
PlanNode::Ws { child: 0 },
],
1,
);
let result = unsafe { run_root(&mut ctx, &plan, subject) }.expect("ws(word) should parse");
let items: Vec<GcRef> = result.as_vec().to_vec();
let values: Vec<&str> = items.iter().map(GcRef::as_text).collect();
assert_eq!(
values,
vec!["alpha", "beta"],
"the base offset must be applied, or the slices name the wrong bytes"
);
for item in items {
let payload = unsafe {
&*(item.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload)
};
let crate::text::TextPayload::Slice(slice) = payload else {
panic!("a `word` is a source slice");
};
let owner = unsafe {
&*(slice.owner().payload::<crate::text::TextPayload>()
as *const crate::text::TextPayload)
};
assert!(
owner.is_owned(),
"a parse of a slice must still name the ROOT owned text, not another slice"
);
}
}
#[test]
fn unicode_grid_cells_are_parsed_once_per_scalar() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("é");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Char,
},
PlanNode::Grid { child: 0 },
],
1,
);
let grid = unsafe { run_root(&mut ctx, &plan, input) }
.expect("one Unicode scalar is one valid grid cell");
let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
assert_eq!(payload.width, 1);
assert_eq!(payload.items.len(), 1);
}
#[test]
fn a_grid_of_chars_interns_its_ascii_cells() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("#.#\n.#.\n#.#");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Char,
},
PlanNode::Grid { child: 0 },
],
1,
);
let before = rt.heap().stats().live_count;
let grid = unsafe { run_root(&mut ctx, &plan, input) }.expect("a 3×3 ASCII grid parses");
let after = rt.heap().stats().live_count;
assert_eq!(
after - before,
1,
"nine cells, one allocation: the Grid object and no Chars at all"
);
let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
assert_eq!(payload.items.len(), 9);
let hash = rt.immortals().small_char('#' as u32).expect("ASCII");
let dot = rt.immortals().small_char('.' as u32).expect("ASCII");
for (n, cell) in payload.items.iter().enumerate() {
let expected = if n % 2 == 0 { hash } else { dot };
assert_eq!(cell.as_ptr(), expected.as_ptr(), "cell {n}");
}
let bigger = rt.alloc_text("#.#.#\n.#.#.\n#.#.#\n.#.#.\n#.#.#");
let mut ctx = rt.context();
ctx.input_source = bigger;
let before = rt.heap().stats().live_count;
let _ = unsafe { run_root(&mut ctx, &plan, bigger) }.expect("a 5×5 ASCII grid parses");
assert_eq!(
rt.heap().stats().live_count - before,
1,
"twenty-five cells cost exactly what nine did"
);
}
#[test]
fn a_non_ascii_grid_cell_is_still_a_fresh_object() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("éé");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Char,
},
PlanNode::Grid { child: 0 },
],
1,
);
let before = rt.heap().stats().live_count;
let grid = unsafe { run_root(&mut ctx, &plan, input) }.expect("two scalars, one row");
assert_eq!(
rt.heap().stats().live_count - before,
3,
"the Grid and one Char per cell — `é` is outside the interned range"
);
let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
assert_eq!(payload.items.len(), 2);
assert_ne!(payload.items[0].as_ptr(), payload.items[1].as_ptr());
assert_eq!(payload.items[0].as_char(), 'é');
assert_eq!(payload.items[1].as_char(), 'é');
}
#[test]
fn one_of_answers_the_interned_char() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("<");
let mut ctx = rt.context();
ctx.input_source = input;
let literals: &'static [&'static str] = Box::leak(vec!["<>^v"].into_boxed_slice());
let nodes: &'static [PlanNode] =
Box::leak(vec![PlanNode::OneOf { chars_index: 0 }].into_boxed_slice());
let plan = ParserPlan {
nodes,
template_parts: &[],
literals,
root: 0,
};
let before = rt.heap().stats().live_count;
let value = unsafe { run_root(&mut ctx, &plan, input) }.expect("`<` is one of \"<>^v\"");
assert_eq!(
rt.heap().stats().live_count,
before,
"an interned Char never enters the live registry"
);
assert_eq!(
value.as_ptr(),
rt.immortals()
.small_char('<' as u32)
.expect("ASCII")
.as_ptr()
);
assert_eq!(value.as_char(), '<');
}
#[test]
fn csv_rest_parser_is_bounded_to_each_token() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("a,b");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Rest,
},
PlanNode::Csv { child: 0 },
],
1,
);
let result = unsafe { run_root(&mut ctx, &plan, input) }.expect("csv(rest) should parse");
let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
assert_eq!(values, vec!["a", "b"]);
}
#[test]
fn an_empty_csv_field_does_not_panic() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("10,20,");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Rest,
},
PlanNode::Csv { child: 0 },
],
1,
);
let result = unsafe { run_root(&mut ctx, &plan, input) }
.expect("an empty csv field is an empty Text, not an abort");
let values: Vec<&str> = result.as_vec().iter().map(GcRef::as_text).collect();
assert_eq!(
values,
vec!["10", "20", ""],
"the field after the last comma is empty, and being empty is not a panic"
);
}
#[test]
fn a_failed_choice_reports_the_deepest_case_failure() {
fn lit(text: &'static str) -> praxis_input_parser::TemplatePartNode {
praxis_input_parser::TemplatePartNode::Literal {
text,
ws: praxis_input_parser::WsPolicy::None,
}
}
fn capture(child: u32) -> praxis_input_parser::TemplatePartNode {
praxis_input_parser::TemplatePartNode::Capture {
child,
field_index: None,
name: None,
}
}
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("abz");
let mut ctx = rt.context();
ctx.input_source = input;
let short: &'static [praxis_input_parser::TemplatePartNode] =
Box::leak(vec![lit("a"), capture(0)].into_boxed_slice());
let long: &'static [praxis_input_parser::TemplatePartNode] =
Box::leak(vec![lit("ab"), capture(0)].into_boxed_slice());
let cases: &'static [(&'static str, u32)] =
Box::leak(vec![("Short", 1u32), ("Long", 2u32)].into_boxed_slice());
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Int,
},
PlanNode::Template {
parts: short,
field_order: &[],
},
PlanNode::Template {
parts: long,
field_order: &[],
},
PlanNode::Choice { cases },
],
3,
);
let fail = unsafe { run_root(&mut ctx, &plan, input) }
.expect_err("neither case can read `z` as an int");
assert_eq!(
fail.expected, "int",
"the deepest case's own expectation, not \"any choice case\""
);
assert_eq!(
fail.input_span.0, 2,
"byte 2 is where the case that got furthest actually broke"
);
}
#[test]
fn a_ragged_row_fault_names_the_row_in_grid_and_in_matrix() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("1 2\n \n3 4\n");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Int,
},
PlanNode::Matrix { child: 0 },
],
1,
);
let fail = unsafe { run_root(&mut ctx, &plan, input) }
.expect_err("a zero-token row is not two tokens wide");
assert_eq!(fail.expected, "rectangular matrix row");
assert_eq!(
fail.input_span,
(4, 6),
"the blank line's own bytes, not the region matrix was handed"
);
let input = rt.alloc_text("12\n \n34\n");
let mut ctx = rt.context();
ctx.input_source = input;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Digit,
},
PlanNode::Grid { child: 0 },
],
1,
);
let fail = unsafe { run_root(&mut ctx, &plan, input) }
.expect_err("a zero-cell row is not two cells wide");
assert_eq!(
fail.expected,
"a grid row of the same cell count as the first"
);
assert_eq!(fail.input_span, (3, 5), "the blank line's own bytes");
}
#[test]
fn the_skip_policies_are_ordered_by_what_they_skip() {
use praxis_input_parser::SkipPolicy;
let rt = crate::Runtime::new();
let owner = rt.alloc_text(" \t\n\r x");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let region = i.whole();
let skipped = |p| skip_chars(&i, region, region.start(), p).offset();
assert_eq!(skipped(SkipPolicy::None), 0, "`none` skips nothing");
assert_eq!(
skipped(SkipPolicy::Whitespace),
2,
"`whitespace` is HORIZONTAL whitespace: it stops at the newline"
);
assert_eq!(
skipped(SkipPolicy::Newlines),
5,
"`newlines` is horizontal whitespace AND line endings — the broader policy"
);
assert!(
skipped(SkipPolicy::Newlines) > skipped(SkipPolicy::Whitespace),
"`newlines` must skip a superset of `whitespace`, however the two are named"
);
assert_eq!(SkipPolicy::Whitespace.skips(), "spaces and tabs");
assert_eq!(
SkipPolicy::Newlines.skips(),
"spaces, tabs and line endings"
);
let mut previous = 0usize;
for policy in SkipPolicy::ALL.iter().copied() {
assert!(
!policy.skips().is_empty(),
"every skip policy states what it skips"
);
let n = skipped(policy);
assert!(
n >= previous,
"SkipPolicy::ALL is ordered from narrowest to broadest; {policy:?} skips {n}"
);
previous = n;
}
}
#[test]
fn chars_that_cannot_read_the_whole_region_is_a_parse_failure() {
fn parse(input: &str, skip: praxis_input_parser::SkipPolicy) -> Option<Vec<i64>> {
let mut rt = crate::Runtime::new();
let text = rt.alloc_text(input);
let mut ctx = rt.context();
ctx.input_source = text;
let plan = test_plan(
vec![
PlanNode::Atomic {
kind: AtomicKind::Digit,
},
PlanNode::Characters { child: 0, skip },
],
1,
);
unsafe { run_root(&mut ctx, &plan, text) }
.ok()
.map(|v| v.as_vec().iter().map(GcRef::as_int).collect())
}
use praxis_input_parser::SkipPolicy;
assert_eq!(parse("1234", SkipPolicy::None), Some(vec![1, 2, 3, 4]));
assert_eq!(
parse("12x34", SkipPolicy::None),
None,
"a child failure inside the region is the parse's failure, not a short answer"
);
assert_eq!(
parse("1 2 3 \t", SkipPolicy::Whitespace),
Some(vec![1, 2, 3])
);
assert_eq!(parse("1 2\n", SkipPolicy::Newlines), Some(vec![1, 2]));
assert_eq!(
parse("1\n2", SkipPolicy::None),
None,
"`skip: none` absorbs nothing, so an interior newline is a mismatch"
);
assert_eq!(
parse("12\n", SkipPolicy::None),
Some(vec![1, 2]),
"the file's own terminator is whitespace the child declined"
);
}
#[test]
fn a_grid_cell_is_whatever_its_cell_parser_reads() {
fn cells(kind: AtomicKind, input: &str) -> Option<(usize, Vec<i64>)> {
let mut rt = crate::Runtime::new();
let text = rt.alloc_text(input);
let mut ctx = rt.context();
ctx.input_source = text;
let plan = test_plan(
vec![PlanNode::Atomic { kind }, PlanNode::Grid { child: 0 }],
1,
);
let grid = unsafe { run_root(&mut ctx, &plan, text) }.ok()?;
let payload = unsafe { &*grid.payload::<crate::collections::GridPayload>() };
Some((
payload.width,
payload.items.iter().map(|r| r.as_int()).collect(),
))
}
assert_eq!(
cells(AtomicKind::Int, "12\n34\n"),
Some((1, vec![12, 34])),
"one token per cell — not [12, 2, 34, 4], and not [1, 2, 3, 4] either"
);
assert_eq!(
cells(AtomicKind::Int, "1 2\n3 4\n"),
Some((2, vec![1, 2, 3, 4]))
);
assert_eq!(
cells(AtomicKind::Digit, "12\n34\n"),
Some((2, vec![1, 2, 3, 4])),
"`digit` names the one-digit-per-cell case, so `int` must not"
);
assert_eq!(
cells(AtomicKind::Int, "1 2\n3\n"),
None,
"two cells then one is not a rectangle"
);
}
#[test]
fn scan_advances_by_scalar_across_a_multibyte_run() {
let mut rt = crate::Runtime::new();
let input = rt.alloc_text("ééé");
let mut ctx = rt.context();
ctx.input_source = input;
let literals: &'static [&'static str] = Box::leak(vec!["é"].into_boxed_slice());
let nodes: &'static [PlanNode] = Box::leak(
vec![
PlanNode::OneOf { chars_index: 0 },
PlanNode::Scan { child: 0 },
]
.into_boxed_slice(),
);
let plan = ParserPlan {
nodes,
template_parts: &[],
literals,
root: 1,
};
let result = unsafe { run_root(&mut ctx, &plan, input) }.expect("scan never fails");
let chars: Vec<char> = result
.as_vec()
.iter()
.map(|r| char::from_u32(unsafe { *r.payload::<u32>() }).expect("a Char"))
.collect();
assert_eq!(
chars,
vec!['é', 'é', 'é'],
"three scalars, and no attempt at the three continuation bytes between them"
);
}
#[test]
fn consume_ws_space_run_requires_one_or_more_spaces_or_tabs() {
use praxis_input_parser::WsPolicy;
assert_eq!(consume_ws(b" ,x", 0, WsPolicy::SpaceRun), Some(2));
assert_eq!(consume_ws(b"\t\t,x", 0, WsPolicy::SpaceRun), Some(2));
assert_eq!(
consume_ws(b"x", 0, WsPolicy::SpaceRun),
None,
"SpaceRun is the one-or-more policy; absence of whitespace must not match"
);
}
#[test]
fn consume_ws_one_or_more_requires_at_least_one() {
use praxis_input_parser::WsPolicy;
assert_eq!(consume_ws(b" x", 0, WsPolicy::OneOrMore), Some(2));
assert_eq!(consume_ws(b"x", 0, WsPolicy::OneOrMore), None);
}
#[test]
fn consume_ws_exact_space_matches_one() {
use praxis_input_parser::WsPolicy;
assert_eq!(consume_ws(b" x", 0, WsPolicy::ExactSpace), Some(1));
assert_eq!(consume_ws(b"\tx", 0, WsPolicy::ExactSpace), None);
}
#[test]
fn consume_ws_newline_matches_crlf_and_lf() {
use praxis_input_parser::WsPolicy;
assert_eq!(consume_ws(b"\r\nx", 0, WsPolicy::Newline), Some(2));
assert_eq!(consume_ws(b"\nx", 0, WsPolicy::Newline), Some(1));
assert_eq!(consume_ws(b"x", 0, WsPolicy::Newline), None);
}
#[test]
fn single_anonymous_template_capture_uses_its_child_descriptor() {
let parts: &'static [praxis_input_parser::TemplatePartNode] = Box::leak(
vec![praxis_input_parser::TemplatePartNode::Capture {
child: 0,
field_index: None,
name: None,
}]
.into_boxed_slice(),
);
let nodes: &'static [PlanNode] = Box::leak(
vec![
PlanNode::Atomic {
kind: AtomicKind::Word,
},
PlanNode::Template {
parts,
field_order: &[],
},
]
.into_boxed_slice(),
);
let plan = ParserPlan {
nodes,
template_parts: &[],
literals: &[],
root: 1,
};
assert_eq!(
child_descriptor(&plan, plan.root).id(),
crate::text::TEXT.id(),
"lines(`{{word}}`) must carry Text as its Vec element descriptor"
);
}
#[test]
fn multi_anonymous_template_captures_are_a_tuple() {
let parts: &'static [praxis_input_parser::TemplatePartNode] = Box::leak(
vec![
praxis_input_parser::TemplatePartNode::Capture {
child: 0,
field_index: Some(0),
name: None,
},
praxis_input_parser::TemplatePartNode::Literal {
text: ",",
ws: praxis_input_parser::WsPolicy::None,
},
praxis_input_parser::TemplatePartNode::Capture {
child: 1,
field_index: Some(1),
name: None,
},
]
.into_boxed_slice(),
);
let nodes: &'static [PlanNode] = Box::leak(
vec![
PlanNode::Atomic {
kind: AtomicKind::Int,
},
PlanNode::Atomic {
kind: AtomicKind::Word,
},
PlanNode::Template {
parts,
field_order: &[],
},
]
.into_boxed_slice(),
);
let plan = ParserPlan {
nodes,
template_parts: &[],
literals: &[],
root: 2,
};
assert_eq!(
child_descriptor(&plan, plan.root).id(),
crate::tuples::TUPLE.id(),
"lines(`{{int}},{{word}}`) must carry Tuple as its Vec element descriptor"
);
}
}