use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::io;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MemberKind {
Int,
Stack,
}
impl MemberKind {
pub(crate) fn parse(value: &str) -> io::Result<Self> {
match value {
"int" | "integer" | "bool" | "boolean" => Ok(Self::Int),
"stack" => Ok(Self::Stack),
other => Err(invalid_data(format!("unknown member slot kind {other:?}"))),
}
}
const fn describe(self) -> &'static str {
match self {
Self::Int => "int",
Self::Stack => "stack",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MemberScope {
Lexer,
Parser,
Both,
}
impl MemberScope {
pub(crate) fn parse(value: &str) -> io::Result<Self> {
match value {
"lexer" => Ok(Self::Lexer),
"parser" => Ok(Self::Parser),
"both" => Ok(Self::Both),
other => Err(invalid_data(format!("unknown member slot scope {other:?}"))),
}
}
const fn covers(self, recognizer: Self) -> bool {
matches!(
(self, recognizer),
(Self::Both, _) | (Self::Lexer, Self::Lexer) | (Self::Parser, Self::Parser)
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct MemberDeclaration {
pub(crate) name: String,
pub(crate) kind: MemberKind,
pub(crate) scope: MemberScope,
pub(crate) init: Option<i64>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct MemberSlots {
slots: BTreeMap<String, (MemberKind, usize)>,
scalar_inits: BTreeMap<usize, i64>,
}
impl MemberSlots {
pub(crate) fn assign_scoped(
declarations: &[MemberDeclaration],
recognizer: MemberScope,
) -> io::Result<Self> {
let mut slots = BTreeMap::new();
let mut scalar_inits = BTreeMap::new();
let mut next_int = 0;
let mut next_stack = 0;
for declaration in declarations
.iter()
.filter(|declaration| declaration.scope.covers(recognizer))
{
let slot = match declaration.kind {
MemberKind::Int => {
let slot = next_int;
next_int += 1;
slot
}
MemberKind::Stack => {
let slot = next_stack;
next_stack += 1;
slot
}
};
if slots
.insert(declaration.name.clone(), (declaration.kind, slot))
.is_some()
{
return Err(invalid_data(format!(
"duplicate member slot declaration {:?}",
declaration.name
)));
}
match (declaration.kind, declaration.init) {
(MemberKind::Stack, Some(_)) => {
return Err(invalid_data(format!(
"member slot {:?} is a stack and cannot declare an `init` value",
declaration.name
)));
}
(MemberKind::Int, Some(init)) if init != 0 => {
scalar_inits.insert(slot, init);
}
_ => {}
}
}
Ok(Self {
slots,
scalar_inits,
})
}
fn resolve(&self, name: &str, expected: MemberKind) -> io::Result<usize> {
let (kind, slot) = self.slots.get(name).copied().ok_or_else(|| {
invalid_data(format!(
"unknown member slot {name:?}; declare it with a [[member]] entry"
))
})?;
if kind != expected {
return Err(invalid_data(format!(
"member slot {name:?} is declared {} but used as {}",
kind.describe(),
expected.describe()
)));
}
Ok(slot)
}
pub(crate) fn scalar_inits(&self) -> impl Iterator<Item = (usize, i64)> + '_ {
self.scalar_inits
.iter()
.map(|(slot, value)| (*slot, *value))
}
#[cfg(test)]
pub(crate) fn entries(&self) -> impl Iterator<Item = (&str, MemberKind, usize)> + '_ {
self.slots
.iter()
.map(|(name, (kind, slot))| (name.as_str(), *kind, *slot))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum MemberExpr {
Bool(bool),
Int(i64),
Member(usize),
MemberTop(usize),
MemberLen(usize),
Not(Box<Self>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum MemberStmt {
Set(usize, MemberExpr),
Add(usize, MemberExpr),
Push(usize, MemberExpr),
Pop(usize),
Seq(Vec<Self>),
}
pub(crate) fn parse_member_expr(
lower: &str,
slots: &MemberSlots,
) -> Option<io::Result<MemberExpr>> {
let (name, _) = split_call(lower.trim())?;
if matches!(name, "bool" | "int") {
return None;
}
parse_member_operand(lower, slots)
}
fn parse_member_operand(lower: &str, slots: &MemberSlots) -> Option<io::Result<MemberExpr>> {
let lower = lower.trim();
let (name, body) = split_call(lower)?;
match name {
"bool" => Some(match body.trim() {
"true" => Ok(MemberExpr::Bool(true)),
"false" => Ok(MemberExpr::Bool(false)),
other => Err(invalid_data(format!("invalid bool literal {other:?}"))),
}),
"int" => Some(
body.trim()
.parse()
.map(MemberExpr::Int)
.map_err(|error| invalid_data(format!("invalid int literal {body:?}: {error}"))),
),
"member" => Some(
slots
.resolve(body.trim(), MemberKind::Int)
.map(MemberExpr::Member),
),
"member_top" => Some(
slots
.resolve(body.trim(), MemberKind::Stack)
.map(MemberExpr::MemberTop),
),
"member_len" => Some(
slots
.resolve(body.trim(), MemberKind::Stack)
.map(MemberExpr::MemberLen),
),
"not" => Some(
parse_member_operand(body, slots)
.unwrap_or_else(|| Err(invalid_data(format!("invalid not() operand {body:?}"))))
.map(|inner| MemberExpr::Not(Box::new(inner))),
),
_ => None,
}
}
pub(crate) fn parse_member_stmt(
lower: &str,
slots: &MemberSlots,
) -> Option<io::Result<MemberStmt>> {
let lower = lower.trim();
let (name, body) = split_call(lower)?;
match name {
"pop_member" => Some(
slots
.resolve(body.trim(), MemberKind::Stack)
.map(MemberStmt::Pop),
),
"set_member" | "add_member" | "push_member" => Some(parse_slot_and_expr(name, body, slots)),
"seq" => Some(parse_seq(body, slots)),
_ => None,
}
}
fn parse_slot_and_expr(name: &str, body: &str, slots: &MemberSlots) -> io::Result<MemberStmt> {
let (slot_name, value) = split_argument(body)
.ok_or_else(|| invalid_data(format!("{name}() needs a slot name and a value: {body:?}")))?;
let kind = if name == "push_member" {
MemberKind::Stack
} else {
MemberKind::Int
};
let slot = slots.resolve(slot_name.trim(), kind)?;
let value = parse_member_operand(&value, slots)
.unwrap_or_else(|| Err(invalid_data(format!("invalid {name}() value {value:?}"))))?;
Ok(match name {
"set_member" => MemberStmt::Set(slot, value),
"add_member" => MemberStmt::Add(slot, value),
_ => MemberStmt::Push(slot, value),
})
}
fn parse_seq(body: &str, slots: &MemberSlots) -> io::Result<MemberStmt> {
let mut statements = Vec::new();
let mut rest = body.trim().to_owned();
while !rest.is_empty() {
let (head, tail) = split_argument(&rest).unwrap_or_else(|| (rest.clone(), String::new()));
if head.trim().is_empty() {
return Err(invalid_data(format!("empty seq() element in {body:?}")));
}
statements.push(
parse_member_stmt(&head, slots)
.unwrap_or_else(|| Err(invalid_data(format!("invalid seq() element {head:?}"))))?,
);
tail.trim().clone_into(&mut rest);
}
if statements.is_empty() {
return Err(invalid_data(
"seq() needs at least one statement".to_owned(),
));
}
Ok(MemberStmt::Seq(statements))
}
fn split_call(lower: &str) -> Option<(&str, &str)> {
let open = lower.find('(')?;
if !lower.ends_with(')') {
return None;
}
let name = lower[..open].trim();
if name.is_empty() || !name.bytes().all(|b| b == b'_' || b.is_ascii_alphanumeric()) {
return None;
}
Some((name, &lower[open + 1..lower.len() - 1]))
}
fn split_argument(body: &str) -> Option<(String, String)> {
let mut depth = 0_usize;
for (index, byte) in body.bytes().enumerate() {
match byte {
b'(' => depth += 1,
b')' => depth = depth.checked_sub(1)?,
b',' if depth == 0 => {
return Some((body[..index].to_owned(), body[index + 1..].to_owned()));
}
_ => {}
}
}
None
}
pub(crate) fn render_member_expr(expr: &MemberExpr, out: &mut String, next: &mut usize) -> String {
let node = match expr {
MemberExpr::Bool(value) => format!("antlr4_runtime::semir::PExpr::Bool({value})"),
MemberExpr::Int(value) => format!("antlr4_runtime::semir::PExpr::Int({value})"),
MemberExpr::Member(slot) => format!("antlr4_runtime::semir::PExpr::Member({slot})"),
MemberExpr::MemberTop(slot) => format!("antlr4_runtime::semir::PExpr::MemberTop({slot})"),
MemberExpr::MemberLen(slot) => format!("antlr4_runtime::semir::PExpr::MemberLen({slot})"),
MemberExpr::Not(inner) => {
let inner = render_member_expr(inner, out, next);
format!("antlr4_runtime::semir::PExpr::Not({inner})")
}
};
let name = format!("__member_expr_{next}");
*next += 1;
writeln!(out, " let {name} = ir.expr({node});")
.expect("writing to a string cannot fail");
name
}
pub(crate) fn render_member_stmt(stmt: &MemberStmt, out: &mut String, next: &mut usize) -> String {
let node = match stmt {
MemberStmt::Set(slot, value) => {
let value = render_member_expr(value, out, next);
format!("antlr4_runtime::semir::AStmt::SetMember({slot}, {value})")
}
MemberStmt::Add(slot, value) => {
let value = render_member_expr(value, out, next);
format!("antlr4_runtime::semir::AStmt::AddMember({slot}, {value})")
}
MemberStmt::Push(slot, value) => {
let value = render_member_expr(value, out, next);
format!("antlr4_runtime::semir::AStmt::PushMember({slot}, {value})")
}
MemberStmt::Pop(slot) => format!("antlr4_runtime::semir::AStmt::PopMember({slot})"),
MemberStmt::Seq(statements) => {
let ids = statements
.iter()
.map(|stmt| render_member_stmt(stmt, out, next))
.collect::<Vec<_>>()
.join(", ");
format!("antlr4_runtime::semir::AStmt::Seq([{ids}].into())")
}
};
let name = format!("__member_stmt_{next}");
*next += 1;
writeln!(out, " let {name} = ir.stmt({node});")
.expect("writing to a string cannot fail");
name
}
fn invalid_data(message: String) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)] mod tests {
use super::{
MemberDeclaration, MemberKind, MemberScope, MemberSlots, parse_member_expr,
parse_member_stmt, render_member_stmt,
};
fn csharp_slots() -> MemberSlots {
MemberSlots::assign_scoped(
&[
MemberDeclaration {
name: "interpolatedStringLevel".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: None,
},
MemberDeclaration {
name: "verbatium".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: None,
},
MemberDeclaration {
name: "interpolatedVerbatiums".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Both,
init: None,
},
MemberDeclaration {
name: "curlyLevels".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Both,
init: None,
},
],
MemberScope::Lexer,
)
.expect("declarations should assign")
}
#[test]
fn slots_number_scalars_and_stacks_in_separate_namespaces() {
let slots = csharp_slots();
insta::assert_debug_snapshot!(
"csharp_member_slot_assignment",
slots.entries().collect::<Vec<_>>()
);
}
#[test]
fn duplicate_slot_declaration_is_rejected() {
let error = MemberSlots::assign_scoped(
&[
MemberDeclaration {
name: "x".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: None,
},
MemberDeclaration {
name: "x".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Both,
init: None,
},
],
MemberScope::Lexer,
)
.expect_err("duplicate slot must fail");
insta::assert_snapshot!("duplicate_member_slot_error", error.to_string());
}
#[test]
fn declared_scalar_initializers_are_recorded_as_seeds() {
let slots = MemberSlots::assign_scoped(
&[
MemberDeclaration {
name: "enabled".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: Some(1),
},
MemberDeclaration {
name: "level".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: Some(7),
},
MemberDeclaration {
name: "explicitZero".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: Some(0),
},
MemberDeclaration {
name: "undeclared".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: None,
},
],
MemberScope::Lexer,
)
.expect("declarations should assign");
insta::assert_compact_debug_snapshot!(
slots.scalar_inits().collect::<Vec<_>>(),
@"[(0, 1), (1, 7)]"
);
}
#[test]
fn lexer_and_parser_inventories_are_independent() {
let declarations = [
MemberDeclaration {
name: "level".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Parser,
init: Some(2),
},
MemberDeclaration {
name: "level".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Lexer,
init: None,
},
MemberDeclaration {
name: "shared".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Both,
init: Some(9),
},
];
let lexer = MemberSlots::assign_scoped(&declarations, MemberScope::Lexer)
.expect("lexer inventory should assign");
let parser = MemberSlots::assign_scoped(&declarations, MemberScope::Parser)
.expect("parser inventory should assign");
insta::assert_compact_debug_snapshot!(
lexer.entries().collect::<Vec<_>>(),
@r#"[("level", Stack, 0), ("shared", Int, 0)]"#
);
insta::assert_compact_debug_snapshot!(
parser.entries().collect::<Vec<_>>(),
@r#"[("level", Int, 0), ("shared", Int, 1)]"#
);
insta::assert_compact_debug_snapshot!(
lexer.scalar_inits().collect::<Vec<_>>(),
@"[(0, 9)]"
);
insta::assert_compact_debug_snapshot!(
parser.scalar_inits().collect::<Vec<_>>(),
@"[(0, 2), (1, 9)]"
);
}
#[test]
fn duplicate_within_one_scope_is_still_rejected() {
let error = MemberSlots::assign_scoped(
&[
MemberDeclaration {
name: "x".to_owned(),
kind: MemberKind::Int,
scope: MemberScope::Lexer,
init: None,
},
MemberDeclaration {
name: "x".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Both,
init: None,
},
],
MemberScope::Lexer,
)
.expect_err("two declarations visible to one recognizer must collide");
insta::assert_snapshot!(
error.to_string(),
@r#"duplicate member slot declaration "x""#
);
}
#[test]
fn stack_slots_reject_an_init_value() {
let error = MemberSlots::assign_scoped(
&[MemberDeclaration {
name: "depths".to_owned(),
kind: MemberKind::Stack,
scope: MemberScope::Both,
init: Some(1),
}],
MemberScope::Lexer,
)
.expect_err("a stack init must be rejected");
insta::assert_snapshot!(
error.to_string(),
@r#"member slot "depths" is a stack and cannot declare an `init` value"#
);
}
#[test]
fn expressions_resolve_declared_slots() {
let slots = csharp_slots();
let parsed = [
"member(verbatium)",
"member_top(interpolatedVerbatiums)",
"member_len(curlyLevels)",
"not(member_top(interpolatedVerbatiums))",
"not(member(verbatium))",
]
.into_iter()
.map(|lower| {
parse_member_expr(lower, &slots)
.expect("member expression should match")
.expect("member expression should resolve")
})
.collect::<Vec<_>>();
insta::assert_debug_snapshot!("member_expression_lowerings", parsed);
}
#[test]
fn bare_literal_lowerings_are_left_to_the_constant_templates() {
let slots = csharp_slots();
assert!(parse_member_expr("bool(true)", &slots).is_none());
assert!(parse_member_expr("bool(false)", &slots).is_none());
assert!(parse_member_expr("int(3)", &slots).is_none());
let nested = parse_member_stmt("push_member(interpolatedVerbatiums, bool(true))", &slots)
.expect("statement should match")
.expect("literal operand should resolve");
insta::assert_compact_debug_snapshot!(nested, @"Push(0, Bool(true))");
}
#[test]
fn csharp_interpolation_bodies_lower_to_statements() {
let slots = csharp_slots();
let lowered = [
"seq(add_member(interpolatedStringLevel, int(1)), push_member(interpolatedVerbatiums, bool(false)), set_member(verbatium, bool(false)))",
"seq(add_member(interpolatedStringLevel, int(1)), push_member(interpolatedVerbatiums, bool(true)), set_member(verbatium, bool(true)))",
"push_member(curlyLevels, int(1))",
"seq(add_member(interpolatedStringLevel, int(-1)), pop_member(interpolatedVerbatiums), set_member(verbatium, member_top(interpolatedVerbatiums)))",
"pop_member(curlyLevels)",
]
.into_iter()
.map(|lower| {
parse_member_stmt(lower, &slots)
.expect("member statement should match")
.expect("member statement should resolve")
})
.collect::<Vec<_>>();
insta::assert_debug_snapshot!("csharp_interpolation_statement_lowerings", lowered);
}
#[test]
fn unknown_and_mistyped_slots_are_named_in_the_error() {
let slots = csharp_slots();
let unknown = parse_member_expr("member(nope)", &slots)
.expect("member() should match")
.expect_err("unknown slot must fail");
let mistyped = parse_member_expr("member_top(verbatium)", &slots)
.expect("member_top() should match")
.expect_err("scalar used as stack must fail");
insta::assert_snapshot!(
"member_slot_resolution_errors",
format!("{unknown}\n{mistyped}")
);
}
#[test]
fn non_member_lowerings_decline_so_other_patterns_can_match() {
let slots = csharp_slots();
assert!(parse_member_expr("cmp(ne, la(1), token(X))", &slots).is_none());
assert!(parse_member_stmt("hook", &slots).is_none());
assert!(parse_member_stmt("true", &slots).is_none());
}
#[test]
fn statements_render_semir_builder_calls() {
let slots = csharp_slots();
let stmt = parse_member_stmt(
"seq(add_member(interpolatedStringLevel, int(-1)), pop_member(interpolatedVerbatiums), set_member(verbatium, member_top(interpolatedVerbatiums)))",
&slots,
)
.expect("statement should match")
.expect("statement should resolve");
let mut out = String::new();
let mut next = 0;
let root = render_member_stmt(&stmt, &mut out, &mut next);
insta::assert_snapshot!(
"csharp_double_quote_inside_rendered",
format!("{out} // root: {root}")
);
}
}