use praxis_source::Span;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AtomicKind {
Int,
UInt,
Float,
Byte,
Char,
Digit,
Word,
Identifier,
Text,
Rest,
}
impl AtomicKind {
pub fn keyword(self) -> &'static str {
match self {
AtomicKind::Int => "int",
AtomicKind::UInt => "uint",
AtomicKind::Float => "float",
AtomicKind::Byte => "byte",
AtomicKind::Char => "char",
AtomicKind::Digit => "digit",
AtomicKind::Word => "word",
AtomicKind::Identifier => "identifier",
AtomicKind::Text => "text",
AtomicKind::Rest => "rest",
}
}
pub fn doc(self) -> &'static str {
match self {
AtomicKind::Int => {
"Signed decimal integer. Surrounding horizontal space is the \
caller's, not the atomic's."
}
AtomicKind::UInt => "Non-negative decimal integer; a leading `-` is refused.",
AtomicKind::Float => "Decimal floating-point number.",
AtomicKind::Byte => "A decimal integer in `0..=255` — a number, not a raw input byte.",
AtomicKind::Char => "One Unicode scalar value, whitespace included where offered.",
AtomicKind::Digit => "One decimal digit.",
AtomicKind::Word => {
"A non-empty run excluding whitespace and parser-delimiter punctuation."
}
AtomicKind::Identifier => "An identifier, by the language's own identifier rule.",
AtomicKind::Text => {
"Consumes as little as possible until the literal run that \
follows can match."
}
AtomicKind::Rest => "The remainder of the current region.",
}
}
pub fn from_keyword(name: &str) -> Option<Self> {
Some(match name {
"int" => AtomicKind::Int,
"uint" => AtomicKind::UInt,
"float" => AtomicKind::Float,
"byte" => AtomicKind::Byte,
"char" => AtomicKind::Char,
"digit" => AtomicKind::Digit,
"word" => AtomicKind::Word,
"identifier" => AtomicKind::Identifier,
"text" => AtomicKind::Text,
"rest" => AtomicKind::Rest,
_ => return None,
})
}
pub const ALL: &'static [AtomicKind] = &[
AtomicKind::Int,
AtomicKind::UInt,
AtomicKind::Float,
AtomicKind::Byte,
AtomicKind::Char,
AtomicKind::Digit,
AtomicKind::Word,
AtomicKind::Identifier,
AtomicKind::Text,
AtomicKind::Rest,
];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WsPolicy {
None,
SpaceRun,
ZeroOrMore,
OneOrMore,
ExactSpace,
Newline,
Tab,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaptureName(Box<str>);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvalidCaptureName;
impl CaptureName {
pub fn parse(text: &str) -> Result<Self, InvalidCaptureName> {
if praxis_syntax::ident::is_ident(text) {
Ok(CaptureName(text.into()))
} else {
Err(InvalidCaptureName)
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CaptureName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug)]
pub enum TemplatePart {
Literal {
text: String,
ws: WsPolicy,
span: Span,
},
Capture {
name: Option<CaptureName>,
parser: Box<ParserAst>,
span: Span,
name_span: Option<Span>,
},
}
impl TemplatePart {
#[must_use]
pub fn span(&self) -> Span {
match self {
TemplatePart::Literal { span, .. } | TemplatePart::Capture { span, .. } => *span,
}
}
}
#[derive(Clone, Debug)]
pub enum ParserAst {
Atomic { kind: AtomicKind, span: Span },
Template {
parts: Vec<TemplatePart>,
span: Span,
},
Lines { child: Box<ParserAst>, span: Span },
Sections { child: Box<ParserAst>, span: Span },
SectionsNamed {
fields: Vec<SectionItem>,
repeated_tail: Option<(String, Box<ParserAst>)>,
span: Span,
},
Csv { child: Box<ParserAst>, span: Span },
Ws { child: Box<ParserAst>, span: Span },
Sep {
separator: Separator,
child: Box<ParserAst>,
span: Span,
},
Grid { child: Box<ParserAst>, span: Span },
Block { items: Vec<BlockItem>, span: Span },
Choice {
cases: Vec<(String, ParserAst)>,
span: Span,
},
Optional { child: Box<ParserAst>, span: Span },
Scan { child: Box<ParserAst>, span: Span },
OneOf { chars: String, span: Span },
Characters {
child: Box<ParserAst>,
skip: SkipPolicy,
span: Span,
},
Matrix { child: Box<ParserAst>, span: Span },
GridRagged {
child: Box<ParserAst>,
fill: String,
span: Span,
},
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Separator(Box<str>);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EmptySeparator;
impl std::fmt::Display for EmptySeparator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a `sep` separator may not be empty: it could never advance")
}
}
impl std::error::Error for EmptySeparator {}
impl Separator {
pub fn new(text: &str) -> Result<Self, EmptySeparator> {
if text.is_empty() {
return Err(EmptySeparator);
}
Ok(Separator(text.into()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Separator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RepeatCount(std::num::NonZeroU32);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvalidRepeatCount {
NotPositive,
TooLarge,
}
impl std::fmt::Display for InvalidRepeatCount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
InvalidRepeatCount::NotPositive => {
"a `repeated` count must be at least 1: a group of no sections parses nothing"
}
InvalidRepeatCount::TooLarge => "a `repeated` count must fit in 32 bits",
})
}
}
impl std::error::Error for InvalidRepeatCount {}
impl RepeatCount {
pub fn new(n: i64) -> Result<Self, InvalidRepeatCount> {
if n <= 0 {
return Err(InvalidRepeatCount::NotPositive);
}
let n = u32::try_from(n).map_err(|_| InvalidRepeatCount::TooLarge)?;
std::num::NonZeroU32::new(n)
.map(RepeatCount)
.ok_or(InvalidRepeatCount::NotPositive)
}
#[must_use]
pub fn get(self) -> u32 {
self.0.get()
}
}
impl std::fmt::Display for RepeatCount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.get())
}
}
#[derive(Clone, Debug)]
pub enum SectionItem {
One { name: String, parser: ParserAst },
Counted {
name: String,
count: RepeatCount,
parser: ParserAst,
},
}
impl SectionItem {
#[must_use]
pub fn name(&self) -> &str {
match self {
SectionItem::One { name, .. } | SectionItem::Counted { name, .. } => name,
}
}
#[must_use]
pub fn parser(&self) -> &ParserAst {
match self {
SectionItem::One { parser, .. } | SectionItem::Counted { parser, .. } => parser,
}
}
pub fn parser_mut(&mut self) -> &mut ParserAst {
match self {
SectionItem::One { parser, .. } | SectionItem::Counted { parser, .. } => parser,
}
}
#[must_use]
pub fn sections_wanted(&self) -> usize {
match self {
SectionItem::One { .. } => 1,
SectionItem::Counted { count, .. } => count.get() as usize,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SkipPolicy {
None,
Whitespace,
Newlines,
}
impl SkipPolicy {
pub fn from_keyword(name: &str) -> Option<Self> {
Some(match name {
"none" => SkipPolicy::None,
"whitespace" => SkipPolicy::Whitespace,
"newlines" => SkipPolicy::Newlines,
_ => return None,
})
}
pub fn skips(self) -> &'static str {
match self {
SkipPolicy::None => "nothing",
SkipPolicy::Whitespace => "spaces and tabs",
SkipPolicy::Newlines => "spaces, tabs and line endings",
}
}
pub const ALL: &'static [SkipPolicy] = &[
SkipPolicy::None,
SkipPolicy::Whitespace,
SkipPolicy::Newlines,
];
}
#[derive(Clone, Debug)]
pub enum BlockItem {
Positional(ParserAst),
Named { name: String, parser: ParserAst },
}
impl ParserAst {
pub fn span(&self) -> Span {
match self {
ParserAst::Atomic { span, .. }
| ParserAst::Template { span, .. }
| ParserAst::Lines { span, .. }
| ParserAst::Sections { span, .. }
| ParserAst::SectionsNamed { span, .. }
| ParserAst::Csv { span, .. }
| ParserAst::Ws { span, .. }
| ParserAst::Sep { span, .. }
| ParserAst::Grid { span, .. }
| ParserAst::Block { span, .. }
| ParserAst::Choice { span, .. }
| ParserAst::Optional { span, .. }
| ParserAst::Scan { span, .. }
| ParserAst::OneOf { span, .. }
| ParserAst::Characters { span, .. }
| ParserAst::Matrix { span, .. }
| ParserAst::GridRagged { span, .. } => *span,
}
}
pub fn shift_spans(&mut self, delta: u32) {
match self {
ParserAst::Atomic { span, .. } | ParserAst::OneOf { span, .. } => {
*span = span.shifted(delta);
}
ParserAst::Template { parts, span } => {
*span = span.shifted(delta);
shift_part_spans(parts, delta);
}
ParserAst::Lines { child, span }
| ParserAst::Sections { child, span }
| ParserAst::Csv { child, span }
| ParserAst::Ws { child, span }
| ParserAst::Grid { child, span }
| ParserAst::Sep { child, span, .. }
| ParserAst::Optional { child, span }
| ParserAst::Scan { child, span }
| ParserAst::Matrix { child, span }
| ParserAst::GridRagged { child, span, .. }
| ParserAst::Characters { child, span, .. } => {
*span = span.shifted(delta);
child.shift_spans(delta);
}
ParserAst::SectionsNamed {
fields,
repeated_tail,
span,
} => {
*span = span.shifted(delta);
for item in fields {
item.parser_mut().shift_spans(delta);
}
if let Some((_, tail)) = repeated_tail {
tail.shift_spans(delta);
}
}
ParserAst::Block { items, span } => {
*span = span.shifted(delta);
for item in items {
match item {
BlockItem::Positional(p) | BlockItem::Named { parser: p, .. } => {
p.shift_spans(delta);
}
}
}
}
ParserAst::Choice { cases, span } => {
*span = span.shifted(delta);
for (_, p) in cases {
p.shift_spans(delta);
}
}
}
}
}
pub fn shift_part_spans(parts: &mut [TemplatePart], delta: u32) {
for part in parts {
match part {
TemplatePart::Literal { span, .. } => *span = span.shifted(delta),
TemplatePart::Capture {
parser,
span,
name_span,
..
} => {
*span = span.shifted(delta);
if let Some(n) = name_span {
*n = n.shifted(delta);
}
parser.shift_spans(delta);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Constructor {
Lines,
Sections,
Csv,
Ws,
Sep,
Grid,
Matrix,
Chars,
OneOf,
Block,
Choice,
Optional,
Scan,
Repeated,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArgShape {
Positional(usize),
StringThenParser,
OneString,
ParserWithSkip,
ParserWithOptionalCount,
GridMaybeRagged,
OnePositionalOrNamed,
Items,
NamedOnly { at_least: usize },
}
impl Constructor {
pub fn from_keyword(name: &str) -> Option<Self> {
Some(match name {
"lines" => Constructor::Lines,
"sections" => Constructor::Sections,
"csv" => Constructor::Csv,
"ws" => Constructor::Ws,
"sep" => Constructor::Sep,
"grid" => Constructor::Grid,
"matrix" => Constructor::Matrix,
"chars" => Constructor::Chars,
"one_of" => Constructor::OneOf,
"block" => Constructor::Block,
"choice" => Constructor::Choice,
"optional" => Constructor::Optional,
"scan" => Constructor::Scan,
"repeated" => Constructor::Repeated,
_ => return None,
})
}
pub fn keyword(self) -> &'static str {
match self {
Constructor::Lines => "lines",
Constructor::Sections => "sections",
Constructor::Csv => "csv",
Constructor::Ws => "ws",
Constructor::Sep => "sep",
Constructor::Grid => "grid",
Constructor::Matrix => "matrix",
Constructor::Chars => "chars",
Constructor::OneOf => "one_of",
Constructor::Block => "block",
Constructor::Choice => "choice",
Constructor::Optional => "optional",
Constructor::Scan => "scan",
Constructor::Repeated => "repeated",
}
}
pub const ALL: &'static [Constructor] = &[
Constructor::Lines,
Constructor::Sections,
Constructor::Csv,
Constructor::Ws,
Constructor::Sep,
Constructor::Grid,
Constructor::Matrix,
Constructor::Chars,
Constructor::OneOf,
Constructor::Block,
Constructor::Choice,
Constructor::Optional,
Constructor::Scan,
Constructor::Repeated,
];
pub fn doc(self) -> &'static str {
match self {
Constructor::Lines => {
"Split the region into lines and apply the parser to each. Every \
line must be consumed whole."
}
Constructor::Sections => {
"Split the region on blank lines and apply the parser to each \
section. With named arguments, parses fixed sections in order \
into a record."
}
Constructor::Csv => {
"Split the region on commas. Whitespace around a comma is \
forgiven, because the field's own parser does not read it."
}
Constructor::Ws => {
"Split on runs of whitespace — line endings included, so a token \
never spans a line."
}
Constructor::Sep => "Split on an exact separator string, with no implicit trimming.",
Constructor::Grid => {
"Parse rectangular lines into a `Grid[T]`, one cell per parser \
application. `ragged` with `fill:` permits uneven rows."
}
Constructor::Matrix => {
"Parse lines of whitespace-separated elements into a `Grid[T]`. \
Unlike `lines(ws(P))`, a row with no tokens is not a row."
}
Constructor::Chars => {
"Apply a parser repeatedly to characters. `skip:` says what is \
passed over between matches: `none`, `whitespace`, `newlines`."
}
Constructor::OneOf => "Match one character from a literal set.",
Constructor::Block => {
"Apply parsers in sequence within one region. A positional item \
contributes its captures; a named one contributes a field."
}
Constructor::Choice => {
"Parse one of several alternatives into an anonymous enum, one \
variant per named case."
}
Constructor::Optional => {
"Return `Option[T]`. A failure consumes no input — this is \
parser-level optionality, not recovery."
}
Constructor::Scan => {
"Find repeated matches inside otherwise irrelevant text, for \
input that embeds its data in noise."
}
Constructor::Repeated => {
"A repeating group of sections in a heterogeneous `sections`. \
`repeated(P, N)` takes exactly N and may be followed; \
`repeated(P)` takes every section left, so it must be last."
}
}
}
pub fn keyword_arg(self) -> Option<&'static str> {
match self {
Constructor::Chars => Some("skip"),
Constructor::Grid => Some("fill"),
_ => None,
}
}
pub fn flag_arg(self) -> Option<&'static str> {
match self {
Constructor::Grid => Some("ragged"),
_ => None,
}
}
pub fn arg_shape(self) -> ArgShape {
match self {
Constructor::Lines
| Constructor::Csv
| Constructor::Ws
| Constructor::Matrix
| Constructor::Optional
| Constructor::Scan => ArgShape::Positional(1),
Constructor::Repeated => ArgShape::ParserWithOptionalCount,
Constructor::Sections => ArgShape::OnePositionalOrNamed,
Constructor::Sep => ArgShape::StringThenParser,
Constructor::OneOf => ArgShape::OneString,
Constructor::Chars => ArgShape::ParserWithSkip,
Constructor::Grid => ArgShape::GridMaybeRagged,
Constructor::Block => ArgShape::Items,
Constructor::Choice => ArgShape::NamedOnly { at_least: 1 },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atomic_round_trips_keywords() {
for kind in AtomicKind::ALL {
assert_eq!(AtomicKind::from_keyword(kind.keyword()), Some(*kind));
match kind {
AtomicKind::Int
| AtomicKind::UInt
| AtomicKind::Float
| AtomicKind::Byte
| AtomicKind::Char
| AtomicKind::Digit
| AtomicKind::Word
| AtomicKind::Identifier
| AtomicKind::Text
| AtomicKind::Rest => {}
}
}
assert_eq!(AtomicKind::from_keyword("nope"), None);
let names: Vec<&str> = AtomicKind::ALL.iter().map(|k| k.keyword()).collect();
assert_eq!(
names,
vec![
"int",
"uint",
"float",
"byte",
"char",
"digit",
"word",
"identifier",
"text",
"rest"
]
);
for not_an_atomic in ["uint8", "integer", "string", "line", "lines", "sep"] {
assert_eq!(AtomicKind::from_keyword(not_an_atomic), None);
}
}
#[test]
fn constructor_round_trips_keywords_and_states_its_shape() {
for ctor in Constructor::ALL {
assert_eq!(
Constructor::from_keyword(ctor.keyword()),
Some(*ctor),
"`{}` must round-trip through the table",
ctor.keyword()
);
match ctor {
Constructor::Lines
| Constructor::Sections
| Constructor::Csv
| Constructor::Ws
| Constructor::Sep
| Constructor::Grid
| Constructor::Matrix
| Constructor::Chars
| Constructor::OneOf
| Constructor::Block
| Constructor::Choice
| Constructor::Optional
| Constructor::Scan
| Constructor::Repeated => {}
}
}
assert_eq!(Constructor::from_keyword("frobnicate"), None);
assert_eq!(Constructor::Lines.arg_shape(), ArgShape::Positional(1));
assert_eq!(Constructor::Optional.arg_shape(), ArgShape::Positional(1));
assert_eq!(Constructor::Sep.arg_shape(), ArgShape::StringThenParser);
assert_eq!(Constructor::OneOf.arg_shape(), ArgShape::OneString);
assert_eq!(Constructor::Chars.arg_shape(), ArgShape::ParserWithSkip);
assert_eq!(Constructor::Grid.arg_shape(), ArgShape::GridMaybeRagged);
assert_eq!(
Constructor::Sections.arg_shape(),
ArgShape::OnePositionalOrNamed
);
assert_eq!(Constructor::Block.arg_shape(), ArgShape::Items);
assert_eq!(
Constructor::Choice.arg_shape(),
ArgShape::NamedOnly { at_least: 1 }
);
assert_eq!(
Constructor::Repeated.arg_shape(),
ArgShape::ParserWithOptionalCount
);
for ctor in Constructor::ALL {
let expected = (*ctor == Constructor::Grid).then_some("ragged");
assert_eq!(ctor.flag_arg(), expected, "`{}`", ctor.keyword());
}
}
#[test]
fn a_repeat_count_is_positive_by_construction() {
assert_eq!(RepeatCount::new(0), Err(InvalidRepeatCount::NotPositive));
assert_eq!(RepeatCount::new(-3), Err(InvalidRepeatCount::NotPositive));
assert_eq!(
RepeatCount::new(1 << 33),
Err(InvalidRepeatCount::TooLarge),
"the plan node holds a u32, so the refusal happens where the span is"
);
assert_eq!(RepeatCount::new(1).expect("one section").get(), 1);
assert_eq!(RepeatCount::new(6).expect("six sections").get(), 6);
assert_eq!(
RepeatCount::new(i64::from(u32::MAX))
.expect("the largest count the plan can hold")
.get(),
u32::MAX
);
}
#[test]
fn a_section_items_appetite_is_its_count() {
let atom = || ParserAst::Atomic {
kind: AtomicKind::Int,
span: Span::at(0),
};
let one = SectionItem::One {
name: "regions".to_string(),
parser: atom(),
};
let counted = SectionItem::Counted {
name: "shapes".to_string(),
count: RepeatCount::new(6).expect("six sections"),
parser: atom(),
};
assert_eq!(one.sections_wanted(), 1);
assert_eq!(counted.sections_wanted(), 6);
assert_eq!(one.name(), "regions");
assert_eq!(counted.name(), "shapes");
}
}