use std::collections::{BTreeMap, BTreeSet};
use super::{EmitReport, GrammarEmitError, unsupported_error};
use crate::grammar::{CharClassItem, Grammar, GrammarExpr, GrammarFormat};
pub(super) const MAX_REPEAT_COPIES: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Precedence {
Choice,
Sequence,
Labeled,
Postfix,
Prefix,
Atom,
}
pub(super) type Rendered = (String, Precedence);
pub(super) fn wrap((text, precedence): Rendered, parent: Precedence) -> String {
if precedence < parent {
format!("({text})")
} else {
text
}
}
pub(super) fn join_sequence(items: Vec<Rendered>) -> Rendered {
let mut parts = items
.into_iter()
.filter(|(text, _precedence)| !text.is_empty())
.collect::<Vec<_>>();
match parts.len() {
0 => (String::new(), Precedence::Sequence),
1 => parts.remove(0),
_ => (
parts
.into_iter()
.map(|(text, precedence)| {
if precedence == Precedence::Sequence {
text
} else {
wrap((text, precedence), Precedence::Labeled)
}
})
.collect::<Vec<_>>()
.join(" "),
Precedence::Sequence,
),
}
}
pub(super) fn join_choice(alternatives: Vec<Rendered>) -> Rendered {
if alternatives
.iter()
.all(|(text, _precedence)| text.is_empty())
{
return (String::new(), Precedence::Sequence);
}
if alternatives.len() == 1 {
return alternatives.into_iter().next().expect("one alternative");
}
let mut output = String::new();
for (index, (text, _precedence)) in alternatives.into_iter().enumerate() {
if index > 0 {
if !output.is_empty() {
output.push(' ');
}
output.push('|');
}
if !text.is_empty() {
if !output.is_empty() {
output.push(' ');
}
output.push_str(&text);
}
}
(output, Precedence::Choice)
}
pub(super) fn lower_repeat(
format: GrammarFormat,
expr: &GrammarExpr,
min: usize,
max: Option<usize>,
) -> Result<GrammarExpr, GrammarEmitError> {
check_repeat_bounds(format, min, max)?;
let copies = max.unwrap_or(min);
if copies > MAX_REPEAT_COPIES {
return Err(unsupported_error(
format,
format!(
"counted repetition {} expands to {copies} copies",
repeat_bounds(min, max)
),
));
}
let mut items = Vec::new();
match max {
None if min == 0 => return Ok(GrammarExpr::zero_or_more(expr.clone())),
None => {
items.extend(std::iter::repeat_n(expr.clone(), min - 1));
items.push(GrammarExpr::one_or_more(expr.clone()));
}
Some(max) => {
items.extend(std::iter::repeat_n(expr.clone(), min));
let mut tail = GrammarExpr::Empty;
for _ in min..max {
tail = GrammarExpr::optional(if tail == GrammarExpr::Empty {
expr.clone()
} else {
GrammarExpr::Sequence(vec![expr.clone(), tail])
});
}
if tail != GrammarExpr::Empty {
items.push(tail);
}
}
}
Ok(match items.len() {
0 => GrammarExpr::Empty,
1 => items.remove(0),
_ => GrammarExpr::Sequence(items),
})
}
pub(super) fn check_repeat_bounds(
format: GrammarFormat,
min: usize,
max: Option<usize>,
) -> Result<(), GrammarEmitError> {
match max {
Some(max) if max < min => Err(unsupported_error(
format,
format!("repeat maximum {max} is below minimum {min}"),
)),
_ => Ok(()),
}
}
pub(super) fn repeat_bounds(min: usize, max: Option<usize>) -> String {
max.map_or_else(|| format!("{{{min},}}"), |max| format!("{{{min},{max}}}"))
}
pub(super) fn char_set_items(expr: &GrammarExpr) -> Option<(bool, Vec<CharClassItem>)> {
match expr {
GrammarExpr::CharClass { negated, items } if !items.is_empty() => {
Some((*negated, items.clone()))
}
GrammarExpr::CharRange(start, end) if start <= end => {
Some((false, vec![CharClassItem::Range(*start, *end)]))
}
GrammarExpr::Terminal(value) => {
let mut chars = value.chars();
match (chars.next(), chars.next()) {
(Some(character), None) => Some((false, vec![CharClassItem::Char(character)])),
_ => None,
}
}
_ => None,
}
}
pub(super) fn negated_lookahead_set(
items: &[GrammarExpr],
index: usize,
) -> Option<(bool, Vec<CharClassItem>)> {
let GrammarExpr::Not(inner) = items.get(index)? else {
return None;
};
if items.get(index + 1) != Some(&GrammarExpr::AnyChar) {
return None;
}
char_set_items(inner).map(|(negated, class)| (!negated, class))
}
pub fn case_variants(character: char) -> Option<Vec<char>> {
let mut lower = character.to_lowercase();
let mut upper = character.to_uppercase();
let (Some(lower_char), None, Some(upper_char), None) =
(lower.next(), lower.next(), upper.next(), upper.next())
else {
return None;
};
if lower_char == upper_char {
return None;
}
let mut variants = vec![lower_char, upper_char];
if character != lower_char && character != upper_char {
variants.push(character);
}
Some(variants)
}
pub(super) fn collect_references<'expr>(expr: &'expr GrammarExpr, names: &mut Vec<&'expr str>) {
match expr {
GrammarExpr::NonTerminal(name) => {
if !names.contains(&name.as_str()) {
names.push(name);
}
}
GrammarExpr::Choice { alternatives, .. } => {
for alternative in alternatives {
collect_references(alternative, names);
}
}
GrammarExpr::Sequence(items) => {
for item in items {
collect_references(item, names);
}
}
GrammarExpr::Optional(inner)
| GrammarExpr::ZeroOrMore(inner)
| GrammarExpr::OneOrMore(inner)
| GrammarExpr::And(inner)
| GrammarExpr::Not(inner)
| GrammarExpr::Repeat { expr: inner, .. }
| GrammarExpr::Capture { expr: inner, .. } => collect_references(inner, names),
GrammarExpr::Empty
| GrammarExpr::Terminal(_)
| GrammarExpr::TerminalInsensitive(_)
| GrammarExpr::CharRange(_, _)
| GrammarExpr::CharClass { .. }
| GrammarExpr::Feature(_)
| GrammarExpr::AnyChar => {}
}
}
#[derive(Clone, Debug)]
pub(super) struct NamePlan {
rule_names: Vec<String>,
by_source: BTreeMap<String, String>,
}
impl NamePlan {
pub(super) fn new(
grammar: &Grammar,
label: &str,
candidates: &[String],
fixed: &BTreeSet<usize>,
reference: impl Fn(&str) -> String,
report: &mut EmitReport,
) -> Self {
let rules = grammar.rules();
let mut used = BTreeSet::new();
let mut names: Vec<Option<String>> = vec![None; rules.len()];
for &index in fixed {
used.insert(candidates[index].clone());
names[index] = Some(candidates[index].clone());
}
for (index, rule) in rules.iter().enumerate() {
if names[index].is_none()
&& candidates[index] == rule.name
&& used.insert(candidates[index].clone())
{
names[index] = Some(candidates[index].clone());
}
}
for (index, name) in names.iter_mut().enumerate() {
if name.is_none() {
let unique = unique_name(&candidates[index], &used);
used.insert(unique.clone());
*name = Some(unique);
}
}
let rule_names = names
.into_iter()
.map(|name| name.expect("every rule is named"))
.collect::<Vec<_>>();
let mut by_source = BTreeMap::new();
for (rule, emitted) in rules.iter().zip(&rule_names) {
if rule.name != *emitted {
report.add_lossy(format!(
"{label} renamed rule {:?} to {emitted:?}",
rule.name
));
}
by_source
.entry(rule.name.clone())
.or_insert_with(|| emitted.clone());
}
for source in grammar.undefined_nonterminals() {
let candidate = reference(&source);
let emitted = unique_name(&candidate, &used);
used.insert(emitted.clone());
if emitted != source {
report.add_lossy(format!(
"{label} renamed non-terminal reference {source:?} to {emitted:?}"
));
}
by_source.insert(source, emitted);
}
Self {
rule_names,
by_source,
}
}
pub(super) fn rule_name(&self, index: usize) -> &str {
&self.rule_names[index]
}
pub(super) fn resolve<'name>(&'name self, name: &'name str) -> &'name str {
self.by_source.get(name).map_or(name, String::as_str)
}
}
fn unique_name(candidate: &str, used: &BTreeSet<String>) -> String {
if !used.contains(candidate) {
return candidate.to_string();
}
(2..=used.len() + 2)
.map(|suffix| format!("{candidate}_{suffix}"))
.find(|name| !used.contains(name))
.expect("one of used.len() + 1 suffixes is unused")
}
pub(super) fn ascii_identifier(name: &str) -> String {
name.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || character == '_' {
character
} else {
'_'
}
})
.collect()
}
pub(super) fn snake_case(name: &str) -> String {
let mut output = String::new();
let mut previous: Option<char> = None;
for character in name.chars() {
if character.is_ascii_uppercase() {
if previous
.is_some_and(|previous| previous.is_ascii_lowercase() || previous.is_ascii_digit())
{
output.push('_');
}
output.push(character.to_ascii_lowercase());
} else if character.is_ascii_alphanumeric() || character == '_' {
output.push(character);
} else {
output.push('_');
}
previous = Some(character);
}
output
}
pub(super) fn doc_lines(text: &str) -> impl Iterator<Item = &str> {
text.lines().map(str::trim).filter(|line| !line.is_empty())
}