use std::collections::BTreeSet;
use super::{GrammarImportError, parse_error};
use crate::grammar::{Grammar, GrammarExpr, GrammarFormat, GrammarRule};
const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
pub(super) const fn is_space(character: char) -> bool {
matches!(
character,
'\t' | '\n' | '\u{b}' | '\u{c}' | '\r' | ' ' | '\u{a0}' | '\u{1680}' | '\u{2000}'
..='\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}
pub(super) const fn is_line_terminator(character: char) -> bool {
matches!(character, '\n' | '\r' | '\u{2028}' | '\u{2029}')
}
pub(super) fn trim(text: &str) -> &str {
text.trim_matches(is_space)
}
pub(super) fn split_lines(text: &str) -> impl Iterator<Item = &str> {
text.split('\n')
.map(|line| line.strip_suffix('\r').unwrap_or(line))
}
pub(super) fn json_quote(value: &str) -> String {
serde_json::Value::String(value.to_owned()).to_string()
}
pub(super) fn strip_line_comment(line: &str, marker: char, backslash_escapes: bool) -> &str {
let mut quote = None;
let mut escaped = false;
for (index, character) in line.char_indices() {
if escaped {
escaped = false;
continue;
}
if quote.is_some() && backslash_escapes && character == '\\' {
escaped = true;
continue;
}
if quote == Some(character) {
quote = None;
} else if quote.is_none() && matches!(character, '"' | '\'') {
quote = Some(character);
} else if quote.is_none() && character == marker {
return &line[..index];
}
}
line
}
pub(super) use crate::grammar::builder::{choice, sequence};
pub(super) fn alternatives_choice(alternatives: Vec<GrammarExpr>) -> GrammarExpr {
if alternatives
.iter()
.all(|alternative| *alternative == GrammarExpr::Empty)
{
GrammarExpr::Empty
} else {
choice(alternatives, false)
}
}
pub(super) fn terminal_or_empty(value: String) -> GrammarExpr {
if value.is_empty() {
GrammarExpr::Empty
} else {
GrammarExpr::Terminal(value)
}
}
pub(super) fn canonical_repeat(
format: GrammarFormat,
expr: GrammarExpr,
min: u64,
max: Option<u64>,
) -> Result<GrammarExpr, GrammarImportError> {
crate::grammar::builder::canonical_repeat(expr, min, max)
.map_err(|error| parse_error(format, error.to_string()))
}
pub(super) fn decimal(digits: &str) -> u64 {
digits.bytes().fold(0, |value: u64, digit| {
value
.saturating_mul(10)
.saturating_add(u64::from(digit - b'0'))
.min(MAX_SAFE_INTEGER + 1)
})
}
pub(super) fn grammar_from_rules(
format: GrammarFormat,
rules: Vec<GrammarRule>,
allowed: &[&str],
) -> Result<Grammar, GrammarImportError> {
if rules.is_empty() {
return Err(parse_error(format, "grammar contains no rules"));
}
let mut names = BTreeSet::new();
for rule in &rules {
if !names.insert(rule.name.as_str()) {
return Err(parse_error(format, format!("duplicate rule {}", rule.name)));
}
}
let mut grammar = Grammar::new().with_source_format(format);
for rule in rules {
grammar.add_rule(rule);
}
if let Some(missing) = grammar
.undefined_nonterminals()
.into_iter()
.find(|name| !allowed.contains(&name.as_str()))
{
return Err(parse_error(
format,
format!("undefined non-terminal {missing}"),
));
}
Ok(grammar)
}
pub(super) fn collect_references(expr: &GrammarExpr, names: &mut Vec<String>) {
match expr {
GrammarExpr::NonTerminal(name) => {
if !names.contains(name) {
names.push(name.clone());
}
}
GrammarExpr::Choice {
alternatives: items,
..
}
| GrammarExpr::Sequence(items) => {
for item in items {
collect_references(item, names);
}
}
GrammarExpr::Optional(expr)
| GrammarExpr::ZeroOrMore(expr)
| GrammarExpr::OneOrMore(expr)
| GrammarExpr::And(expr)
| GrammarExpr::Not(expr)
| GrammarExpr::Capture { expr, .. }
| GrammarExpr::Repeat { expr, .. } => collect_references(expr, names),
GrammarExpr::Feature(feature) => {
feature.for_each_expression(&mut |inner| collect_references(inner, names));
}
GrammarExpr::Empty
| GrammarExpr::Terminal(_)
| GrammarExpr::TerminalInsensitive(_)
| GrammarExpr::CharRange(_, _)
| GrammarExpr::CharClass { .. }
| GrammarExpr::AnyChar => {}
}
}
pub(super) type Escapes = fn(char, char, GrammarFormat) -> Result<String, GrammarImportError>;
#[allow(clippy::unnecessary_wraps)]
pub(super) fn default_escapes(
escaped: char,
_quote: char,
_format: GrammarFormat,
) -> Result<String, GrammarImportError> {
Ok(match escaped {
'n' => "\n".to_string(),
'r' => "\r".to_string(),
't' => "\t".to_string(),
other => other.to_string(),
})
}
#[derive(Clone, Debug)]
pub(super) struct Cursor<'source> {
pub(super) source: &'source str,
pub(super) format: GrammarFormat,
pub(super) offset: usize,
comments: bool,
}
impl<'source> Cursor<'source> {
pub(super) const fn new(source: &'source str, format: GrammarFormat) -> Self {
Self {
source,
format,
offset: 0,
comments: false,
}
}
pub(super) const fn with_comments(source: &'source str, format: GrammarFormat) -> Self {
Self {
source,
format,
offset: 0,
comments: true,
}
}
pub(super) const fn eof(&self) -> bool {
self.offset >= self.source.len()
}
pub(super) fn rest(&self) -> &'source str {
self.source.get(self.offset..).unwrap_or("")
}
pub(super) fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
pub(super) fn starts_with(&self, value: &str) -> bool {
self.rest().starts_with(value)
}
pub(super) const fn advance(&mut self, bytes: usize) {
self.offset += bytes;
}
pub(super) fn take(&mut self) -> Option<char> {
let character = self.peek();
self.offset += character.map_or(1, char::len_utf8);
character
}
pub(super) fn skip_space(&mut self) -> Result<(), GrammarImportError> {
loop {
while self.peek().is_some_and(is_space) {
self.take();
}
if !self.comments {
return Ok(());
}
if self.starts_with("//") {
self.offset = self.rest()[2..]
.find('\n')
.map_or(self.source.len(), |end| self.offset + 2 + end + 1);
} else if self.starts_with("/*") {
let Some(end) = self.rest()[2..].find("*/") else {
return Err(self.error("unterminated block comment"));
};
self.offset += 2 + end + 2;
} else {
return Ok(());
}
}
}
pub(super) fn consume(&mut self, value: &str) -> Result<(), GrammarImportError> {
if self.try_consume(value)? {
Ok(())
} else {
Err(parse_error(
self.format,
format!(
"expected {} at offset {}",
json_quote(value),
self.position()
),
))
}
}
pub(super) fn try_consume(&mut self, value: &str) -> Result<bool, GrammarImportError> {
self.skip_space()?;
if !self.starts_with(value) {
return Ok(false);
}
self.offset += value.len();
Ok(true)
}
pub(super) fn identifier_with(
&mut self,
first: fn(char) -> bool,
rest: fn(char) -> bool,
) -> Result<String, GrammarImportError> {
self.skip_space()?;
let text = self.rest();
if !text.chars().next().is_some_and(first) {
return Err(parse_error(
self.format,
format!("expected identifier at offset {}", self.position()),
));
}
let length = text
.char_indices()
.skip(1)
.find(|&(_, character)| !rest(character))
.map_or(text.len(), |(index, _)| index);
self.offset += length;
Ok(text[..length].to_string())
}
pub(super) fn identifier(&mut self) -> Result<String, GrammarImportError> {
self.identifier_with(
|character| character.is_ascii_alphabetic() || character == '_',
|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'),
)
}
pub(super) fn quoted(&mut self, decode: Option<Escapes>) -> Result<String, GrammarImportError> {
self.skip_space()?;
let start = self.position();
let quote = self.take();
let Some(quote @ ('"' | '\'')) = quote else {
return Err(parse_error(
self.format,
format!("expected quoted literal at offset {start}"),
));
};
let mut result = String::new();
while let Some(character) = self.take() {
if character == quote {
return Ok(result);
}
let Some(decode) = decode.filter(|_| character == '\\') else {
result.push(character);
continue;
};
let Some(escaped) = self.take() else {
return Err(parse_error(self.format, "unterminated string escape"));
};
result.push_str(&decode(escaped, quote, self.format)?);
}
Err(parse_error(self.format, "unterminated string literal"))
}
pub(super) fn position(&self) -> usize {
let within = self.offset.min(self.source.len());
self.source[..within].encode_utf16().count() + (self.offset - within)
}
pub(super) fn error(&self, message: &str) -> GrammarImportError {
parse_error(
self.format,
format!("{message} at offset {}", self.position()),
)
}
}