1use std::collections::{BTreeMap, BTreeSet};
11
12use crate::language_profile::LanguageProfile;
13use crate::link_network::LinkType;
14
15pub const GRAMMAR_FORMATS: &[&str] = &["bnf"];
21
22pub const GRAMMAR_CONSTRUCTS: &[&str] = &[
27 "empty-expression",
28 "sequence",
29 "ordered-choice",
30 "unordered-choice",
31 "optional-expression",
32 "zero-or-more-repetition",
33 "one-or-more-repetition",
34 "counted-repetition",
35 "character-range",
36 "character-class",
37 "any-character",
38 "terminal",
39 "case-insensitive-terminal",
40 "nonterminal",
41 "positive-predicate",
42 "negative-predicate",
43 "capture",
44 "atomic-rule",
45 "silent-rule",
46 "token-rule",
47];
48
49pub const FORMER_GRAMMAR_CONSTRUCTS: &[(&str, &str)] = &[
52 ("empty", "empty-expression"),
53 ("optional", "optional-expression"),
54 ("zero-or-more", "zero-or-more-repetition"),
55 ("one-or-more", "one-or-more-repetition"),
56 ("repeat-range", "counted-repetition"),
57 ("char-range", "character-range"),
58 ("char-class", "character-class"),
59 ("any-char", "any-character"),
60 ("non-terminal", "nonterminal"),
61 ("and-predicate", "positive-predicate"),
62 ("not-predicate", "negative-predicate"),
63 ("rule-kind-atomic", "atomic-rule"),
64 ("rule-kind-silent", "silent-rule"),
65 ("rule-kind-token", "token-rule"),
66];
67
68#[must_use]
70pub fn current_grammar_construct(construct: &str) -> &str {
71 FORMER_GRAMMAR_CONSTRUCTS
72 .iter()
73 .find(|(former, _)| *former == construct)
74 .map_or(construct, |(_, current)| current)
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum GrammarFidelityLevel {
80 Lossless,
83 Equivalent,
86 Lossy,
89}
90
91impl GrammarFidelityLevel {
92 #[must_use]
94 pub const fn symbol(self) -> &'static str {
95 match self {
96 Self::Lossless => "✅",
97 Self::Equivalent => "≈",
98 Self::Lossy => "⚠️",
99 }
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct GrammarFormatProfile {
111 format: &'static str,
112 profile: LanguageProfile,
113 equivalent_constructs: BTreeSet<String>,
114}
115
116impl GrammarFormatProfile {
117 #[must_use]
119 pub const fn new(format: &'static str, profile: LanguageProfile) -> Self {
120 Self {
121 format,
122 profile,
123 equivalent_constructs: BTreeSet::new(),
124 }
125 }
126
127 #[must_use]
129 pub fn with_lossless_construct(mut self, construct: impl Into<String>) -> Self {
130 self.profile = self.profile.with_concept(construct);
131 self
132 }
133
134 #[must_use]
136 pub fn with_equivalent_construct(mut self, construct: impl Into<String>) -> Self {
137 let construct = construct.into();
138 self.profile = self.profile.with_concept(construct.clone());
139 self.equivalent_constructs.insert(construct);
140 self
141 }
142
143 #[must_use]
145 pub fn with_lossy_fallback(
146 mut self,
147 construct: impl Into<String>,
148 fallback: impl Into<String>,
149 ) -> Self {
150 self.profile = self.profile.with_concept_fallback(construct, fallback);
151 self
152 }
153
154 #[must_use]
156 pub const fn format(&self) -> &'static str {
157 self.format
158 }
159
160 #[must_use]
162 pub const fn language_profile(&self) -> &LanguageProfile {
163 &self.profile
164 }
165
166 #[must_use]
168 pub const fn fallbacks(&self) -> &BTreeMap<String, String> {
169 self.profile.fallbacks()
170 }
171
172 #[must_use]
174 pub const fn equivalent_constructs(&self) -> &BTreeSet<String> {
175 &self.equivalent_constructs
176 }
177
178 #[must_use]
180 pub fn supports_construct(&self, construct: &str) -> bool {
181 self.profile
182 .supports_concept(current_grammar_construct(construct))
183 }
184
185 #[must_use]
188 pub fn construct_fallback(&self, construct: &str) -> Option<&str> {
189 self.profile
190 .concept_fallback(current_grammar_construct(construct))
191 }
192
193 #[must_use]
196 pub fn construct_fidelity(&self, construct: &str) -> Option<GrammarFidelityLevel> {
197 if self.supports_construct(construct) {
198 if self
199 .equivalent_constructs
200 .contains(current_grammar_construct(construct))
201 {
202 Some(GrammarFidelityLevel::Equivalent)
203 } else {
204 Some(GrammarFidelityLevel::Lossless)
205 }
206 } else if self.construct_fallback(construct).is_some() {
207 Some(GrammarFidelityLevel::Lossy)
208 } else {
209 None
210 }
211 }
212}
213
214#[must_use]
217pub fn grammar_format_profile(format: &str) -> Option<GrammarFormatProfile> {
218 let canonical = canonical_grammar_format(format)?;
219 Some(match canonical {
220 "bnf" => bnf_profile(),
221 _ => unreachable!("canonical_grammar_format only yields known formats"),
222 })
223}
224
225#[must_use]
227pub fn canonical_grammar_format(format: &str) -> Option<&'static str> {
228 match format.to_ascii_lowercase().as_str() {
229 "bnf" | "classic-bnf" | "classic bnf" | "backus-naur form" | "backus naur form" => {
230 Some("bnf")
231 }
232 _ => None,
233 }
234}
235
236fn base_profile(format: &'static str, name: &'static str) -> GrammarFormatProfile {
237 GrammarFormatProfile::new(
238 format,
239 LanguageProfile::new(name, format)
240 .with_link_type(LinkType::Grammar)
241 .with_link_type(LinkType::Concept)
242 .with_link_type(LinkType::Token),
243 )
244}
245
246fn with_lossless_constructs<'a>(
247 mut profile: GrammarFormatProfile,
248 constructs: impl IntoIterator<Item = &'a str>,
249) -> GrammarFormatProfile {
250 for construct in constructs {
251 profile = profile.with_lossless_construct(construct);
252 }
253 profile
254}
255
256fn bnf_profile() -> GrammarFormatProfile {
257 with_lossless_constructs(
258 base_profile("bnf", "Backus-Naur Form"),
259 [
260 "empty-expression",
261 "sequence",
262 "unordered-choice",
263 "terminal",
264 "nonterminal",
265 ],
266 )
267 .with_lossy_fallback(
268 "ordered-choice",
269 "emitted as an unordered BNF alternative; priority semantics are not preserved",
270 )
271 .with_lossy_fallback(
272 "optional-expression",
273 "emitted through a synthetic helper production with an empty alternative",
274 )
275 .with_lossy_fallback(
276 "zero-or-more-repetition",
277 "emitted through a recursive synthetic helper production with an empty alternative",
278 )
279 .with_lossy_fallback(
280 "one-or-more-repetition",
281 "emitted through a recursive synthetic helper production plus one required item",
282 )
283 .with_lossy_fallback(
284 "counted-repetition",
285 "emitted as required occurrences plus optional or recursive synthetic helper productions",
286 )
287 .with_lossy_fallback(
288 "character-range",
289 "expanded to a synthetic helper production enumerating each character when the range is bounded",
290 )
291 .with_lossy_fallback(
292 "character-class",
293 "expanded to a synthetic helper production for finite non-negated classes; unsupported classes are rejected",
294 )
295 .with_lossy_fallback(
296 "any-character",
297 "unsupported by BNF emission and rejected instead of silently broadening the language",
298 )
299 .with_lossy_fallback(
300 "case-insensitive-terminal",
301 "emitted as a case-sensitive literal and reported as lossy",
302 )
303 .with_lossy_fallback(
304 "positive-predicate",
305 "unsupported by BNF emission and rejected because lookahead has no BNF equivalent",
306 )
307 .with_lossy_fallback(
308 "negative-predicate",
309 "unsupported by BNF emission and rejected because lookahead has no BNF equivalent",
310 )
311 .with_lossy_fallback(
312 "capture",
313 "emitted as the captured expression while dropping the capture label",
314 )
315 .with_lossy_fallback(
316 "atomic-rule",
317 "emitted as a normal BNF production; rule-kind metadata is dropped",
318 )
319 .with_lossy_fallback(
320 "silent-rule",
321 "emitted as a normal BNF production; rule-kind metadata is dropped",
322 )
323 .with_lossy_fallback(
324 "token-rule",
325 "emitted as a normal BNF production; rule-kind metadata is dropped",
326 )
327}