1#![feature(default_field_values)]
7
8mod prelude;
10#[cfg(test)]
11mod tests;
12
13use self::prelude::{
15 LazyLock,
16 Regex,
17 Map,
18 AddAssign,
19 Borrow,
20 Set
21};
22
23
24static COLLAPSE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\(([^()]*)\)").unwrap());
30
31static POSTFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<atom>[A-Za-z0-9_$]+)(?P<operator>[*+?])").unwrap());
33
34
35struct Locales {
41 parentheses: Map<String, usize>,
42 more: Map<String, usize>,
43 multiple: Map<String, usize>,
44 optional: Map<String, usize>,
45 names: Map<usize, String>
46} impl Locales {pub fn new() -> Self {return Self {
47 parentheses: Map::new(),
48 more: Map::new(),
49 multiple: Map::new(),
50 optional: Map::new(),
51 names: Map::new()
52}}}
53
54#[derive(Clone, Copy)]
56pub enum Delimiter {
57 Arrow,
58 Colon,
59 Definition
60} impl Into<&'static str> for Delimiter {fn into(self) -> &'static str {match self {
61 Delimiter::Arrow => "->",
62 Delimiter::Colon => ":",
63 Delimiter::Definition => "::="
64}}}
65
66#[derive(Clone, Copy)]
68pub enum Comment {
69 Hashtag,
70 DoubleSlash
71} impl Into<&'static str> for Comment {fn into(self) -> &'static str {match self {
72 Comment::Hashtag => "#",
73 Comment::DoubleSlash => "//"
74}}}
75
76#[derive(Clone, Copy)]
78pub enum Naming {
79 Descriptive,
80 Verbose,
81 Numeral
82}
83
84#[derive(Clone, Copy)]
86pub struct Options {
87 input_delimiter: Delimiter = Delimiter::Definition,
88 output_delimiter: Delimiter = Delimiter::Definition,
89 comment: Comment = Comment::DoubleSlash,
90 naming: Naming = Naming::Descriptive
91}
92
93pub fn convert(ebnf: &str, options: Options) -> Option<String> {
95 let mut rules = Set::new();
96 let mut locales = Locales::new();
97 let mut counter = 0;
98 for line in ebnf.lines().map(str::trim) {
99 if line.is_empty() || line.starts_with::<&'static str>(options.comment.into()) {rules.insert(line.to_string()); continue}
100 let (rule, productions) = line.split_once::<&'static str>(options.input_delimiter.into()).map(|data| (data.0.trim(), data.1.trim()))?;
101 let body = expand(productions, &mut rules, &mut locales, &mut counter, &options);
102 rules.insert(format!("{rule} {} {body}", Into::<&'static str>::into(options.output_delimiter)));
103 };
104 return Some(Vec::from_iter(rules.into_iter()).join("\n"));
105}
106
107fn expand(productions: &str, rules: &mut Set<String>, locales: &mut Locales, counter: &mut usize, options: &Options) -> String {
109 let mut productions = productions.to_string();
110 while COLLAPSE.is_match(&productions) {productions = collapse(&productions, rules, locales, counter, options)};
111 return postfix(&productions, rules, locales, counter, options);
112}
113
114fn collapse(productions: &str, rules: &mut Set<String>, locales: &mut Locales, counter: &mut usize, options: &Options) -> String {
116 let hit = COLLAPSE.find(productions).unwrap();
117 let inside = &productions[hit.start() + 1 .. hit.end() - 1];
118 let symbol = namify(*locales.parentheses.entry(inside.to_string()).or_insert_with(|| {counter.add_assign(1); *counter}), productions, locales, options).to_string();
119 let expanded = expand(inside, rules, locales, counter, options);
120 rules.insert(format!("{symbol} {} {expanded}", Into::<&'static str>::into(options.output_delimiter)));
121 return format!("{}{symbol}{}", &productions[..hit.start()], &productions[hit.end()..]);
122}
123
124fn postfix(productions: &str, rules: &mut Set<String>, locales: &mut Locales, counter: &mut usize, options: &Options) -> String {
126 let mut result = productions.to_string();
127 while let Some(hit) = POSTFIX.captures(&result) {
128 let atom = hit.name("atom").unwrap().as_str();
129 let operator = hit.name("operator").unwrap().as_str();
130 let symbol = namify(*match operator {
131 "+" => locales.more.entry(atom.to_string()).or_insert_with(|| {counter.add_assign(1); *counter}),
132 "*" => locales.multiple.entry(atom.to_string()).or_insert_with(|| {counter.add_assign(1); *counter}),
133 "?" => locales.optional.entry(atom.to_string()).or_insert_with(|| {counter.add_assign(1); *counter}),
134 _ => unreachable!()
135 }, productions, locales, options);
136 rules.insert(match operator {
137 "+" => format!("{symbol} {} {atom} {symbol} | {atom}", Into::<&'static str>::into(options.output_delimiter)),
138 "*" => format!("{symbol} {} {atom} {symbol} |", Into::<&'static str>::into(options.output_delimiter)),
139 "?" => format!("{symbol} {} {atom} |", Into::<&'static str>::into(options.output_delimiter)),
140 _ => unreachable!()
141 });
142 let group = hit.get(0).unwrap();
143 result = format!("{}{symbol}{}", &result[..group.start()], &result[group.end()..]);
144 };
145 return result;
146}
147
148fn namify<'source>(symbol: usize, productions: &str, locales: &'source mut Locales, options: &Options) -> &'source str {return locales.names.entry(symbol).or_insert_with(|| match options.naming {
150 Naming::Verbose => productions.replace(" ", "_").replace("(", "open_").replace(")", "_close").replace("+", "_more").replace("*", "_multiple").replace("?", "_optional"),
151 Naming::Descriptive => productions.replace(" ", "_").replace("(", "op_").replace(")", "_cl").replace("+", "_mor").replace("*", "_mul").replace("?", "_opt"),
152 Naming::Numeral => format!("${symbol}")
153})}
154
155
156pub trait ToBNF {
162 fn to_bnf(&self) -> Option<String>;
163 fn to_bnf_with_options(&self, options: Options) -> Option<String>;
164}
165
166impl<Type: Borrow<str>> ToBNF for Type {
168 fn to_bnf(&self) -> Option<String> {convert(self.borrow(), Options {..})}
169 fn to_bnf_with_options(&self, options: Options) -> Option<String> {convert(self.borrow(), options)}
170}