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",
28 "sequence",
29 "ordered-choice",
30 "unordered-choice",
31 "optional",
32 "zero-or-more",
33 "one-or-more",
34 "repeat-range",
35 "char-range",
36 "char-class",
37 "any-char",
38 "terminal",
39 "case-insensitive-terminal",
40 "non-terminal",
41 "and-predicate",
42 "not-predicate",
43 "capture",
44 "rule-kind-atomic",
45 "rule-kind-silent",
46 "rule-kind-token",
47];
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum GrammarFidelityLevel {
52 Lossless,
55 Equivalent,
58 Lossy,
61}
62
63impl GrammarFidelityLevel {
64 #[must_use]
66 pub const fn symbol(self) -> &'static str {
67 match self {
68 Self::Lossless => "✅",
69 Self::Equivalent => "≈",
70 Self::Lossy => "⚠️",
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct GrammarFormatProfile {
83 format: &'static str,
84 profile: LanguageProfile,
85 equivalent_constructs: BTreeSet<String>,
86}
87
88impl GrammarFormatProfile {
89 #[must_use]
91 pub const fn new(format: &'static str, profile: LanguageProfile) -> Self {
92 Self {
93 format,
94 profile,
95 equivalent_constructs: BTreeSet::new(),
96 }
97 }
98
99 #[must_use]
101 pub fn with_lossless_construct(mut self, construct: impl Into<String>) -> Self {
102 self.profile = self.profile.with_concept(construct);
103 self
104 }
105
106 #[must_use]
108 pub fn with_equivalent_construct(mut self, construct: impl Into<String>) -> Self {
109 let construct = construct.into();
110 self.profile = self.profile.with_concept(construct.clone());
111 self.equivalent_constructs.insert(construct);
112 self
113 }
114
115 #[must_use]
117 pub fn with_lossy_fallback(
118 mut self,
119 construct: impl Into<String>,
120 fallback: impl Into<String>,
121 ) -> Self {
122 self.profile = self.profile.with_concept_fallback(construct, fallback);
123 self
124 }
125
126 #[must_use]
128 pub const fn format(&self) -> &'static str {
129 self.format
130 }
131
132 #[must_use]
134 pub const fn language_profile(&self) -> &LanguageProfile {
135 &self.profile
136 }
137
138 #[must_use]
140 pub const fn fallbacks(&self) -> &BTreeMap<String, String> {
141 self.profile.fallbacks()
142 }
143
144 #[must_use]
146 pub const fn equivalent_constructs(&self) -> &BTreeSet<String> {
147 &self.equivalent_constructs
148 }
149
150 #[must_use]
152 pub fn supports_construct(&self, construct: &str) -> bool {
153 self.profile.supports_concept(construct)
154 }
155
156 #[must_use]
159 pub fn construct_fallback(&self, construct: &str) -> Option<&str> {
160 self.profile.concept_fallback(construct)
161 }
162
163 #[must_use]
166 pub fn construct_fidelity(&self, construct: &str) -> Option<GrammarFidelityLevel> {
167 if self.supports_construct(construct) {
168 if self.equivalent_constructs.contains(construct) {
169 Some(GrammarFidelityLevel::Equivalent)
170 } else {
171 Some(GrammarFidelityLevel::Lossless)
172 }
173 } else if self.construct_fallback(construct).is_some() {
174 Some(GrammarFidelityLevel::Lossy)
175 } else {
176 None
177 }
178 }
179}
180
181#[must_use]
184pub fn grammar_format_profile(format: &str) -> Option<GrammarFormatProfile> {
185 let canonical = canonical_grammar_format(format)?;
186 Some(match canonical {
187 "bnf" => bnf_profile(),
188 _ => unreachable!("canonical_grammar_format only yields known formats"),
189 })
190}
191
192#[must_use]
194pub fn canonical_grammar_format(format: &str) -> Option<&'static str> {
195 match format.to_ascii_lowercase().as_str() {
196 "bnf" | "classic-bnf" | "classic bnf" | "backus-naur form" | "backus naur form" => {
197 Some("bnf")
198 }
199 _ => None,
200 }
201}
202
203fn base_profile(format: &'static str, name: &'static str) -> GrammarFormatProfile {
204 GrammarFormatProfile::new(
205 format,
206 LanguageProfile::new(name, format)
207 .with_link_type(LinkType::Grammar)
208 .with_link_type(LinkType::Concept)
209 .with_link_type(LinkType::Token),
210 )
211}
212
213fn with_lossless_constructs<'a>(
214 mut profile: GrammarFormatProfile,
215 constructs: impl IntoIterator<Item = &'a str>,
216) -> GrammarFormatProfile {
217 for construct in constructs {
218 profile = profile.with_lossless_construct(construct);
219 }
220 profile
221}
222
223fn bnf_profile() -> GrammarFormatProfile {
224 with_lossless_constructs(
225 base_profile("bnf", "Backus-Naur Form"),
226 [
227 "empty",
228 "sequence",
229 "unordered-choice",
230 "terminal",
231 "non-terminal",
232 ],
233 )
234 .with_lossy_fallback(
235 "ordered-choice",
236 "emitted as an unordered BNF alternative; priority semantics are not preserved",
237 )
238 .with_lossy_fallback(
239 "optional",
240 "emitted through a synthetic helper production with an empty alternative",
241 )
242 .with_lossy_fallback(
243 "zero-or-more",
244 "emitted through a recursive synthetic helper production with an empty alternative",
245 )
246 .with_lossy_fallback(
247 "one-or-more",
248 "emitted through a recursive synthetic helper production plus one required item",
249 )
250 .with_lossy_fallback(
251 "repeat-range",
252 "emitted as required occurrences plus optional or recursive synthetic helper productions",
253 )
254 .with_lossy_fallback(
255 "char-range",
256 "expanded to a synthetic helper production enumerating each character when the range is bounded",
257 )
258 .with_lossy_fallback(
259 "char-class",
260 "expanded to a synthetic helper production for finite non-negated classes; unsupported classes are rejected",
261 )
262 .with_lossy_fallback(
263 "any-char",
264 "unsupported by BNF emission and rejected instead of silently broadening the language",
265 )
266 .with_lossy_fallback(
267 "case-insensitive-terminal",
268 "emitted as a case-sensitive literal and reported as lossy",
269 )
270 .with_lossy_fallback(
271 "and-predicate",
272 "unsupported by BNF emission and rejected because lookahead has no BNF equivalent",
273 )
274 .with_lossy_fallback(
275 "not-predicate",
276 "unsupported by BNF emission and rejected because lookahead has no BNF equivalent",
277 )
278 .with_lossy_fallback(
279 "capture",
280 "emitted as the captured expression while dropping the capture label",
281 )
282 .with_lossy_fallback(
283 "rule-kind-atomic",
284 "emitted as a normal BNF production; rule-kind metadata is dropped",
285 )
286 .with_lossy_fallback(
287 "rule-kind-silent",
288 "emitted as a normal BNF production; rule-kind metadata is dropped",
289 )
290 .with_lossy_fallback(
291 "rule-kind-token",
292 "emitted as a normal BNF production; rule-kind metadata is dropped",
293 )
294}