Skip to main content

meta_language/grammar/
fidelity.rs

1//! Per-format capability profiles for grammar round-trip fidelity.
2//!
3//! Each supported grammar notation exposes a [`LanguageProfile`] over the
4//! grammar IR construct vocabulary. Constructs are classified as lossless or
5//! equivalent when the target notation represents them without a documented
6//! lossy fallback, and lossy when the emitter must degrade, synthesize helper
7//! productions, drop metadata, or reject the construct with an explicit
8//! unsupported-construct error.
9
10use std::collections::{BTreeMap, BTreeSet};
11
12use crate::language_profile::LanguageProfile;
13use crate::link_network::LinkType;
14
15/// Grammar formats with a fidelity profile.
16///
17/// The matrix starts with BNF because it is the first import/emission pair this
18/// issue depends on. Later importer/emitter issues can add rows by extending
19/// this list and adding a profile function branch.
20pub const GRAMMAR_FORMATS: &[&str] = &["bnf"];
21
22/// The grammar IR construct vocabulary classified by the fidelity matrix.
23///
24/// Every [`GrammarFormatProfile`] must classify each construct as either
25/// lossless/equivalent support or exactly one documented lossy fallback.
26pub 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/// Round-trip fidelity level for one construct in one grammar format.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum GrammarFidelityLevel {
52    /// The construct is represented natively and survives same-format
53    /// import/emit/re-import without a semantic fallback.
54    Lossless,
55    /// The construct is represented by a semantically equivalent spelling or
56    /// normalization.
57    Equivalent,
58    /// The construct requires a documented fallback, metadata drop, helper
59    /// expansion, or explicit unsupported-construct error.
60    Lossy,
61}
62
63impl GrammarFidelityLevel {
64    /// Markdown cell symbol used by `docs/grammar/fidelity.md`.
65    #[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/// Capability profile for one grammar notation.
76///
77/// The embedded [`LanguageProfile`] carries the support-or-fallback invariant
78/// used by the document fidelity matrix. `equivalent_constructs` marks
79/// supported constructs whose round trip is semantically equivalent but not
80/// byte/canonical-form lossless.
81#[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    /// Creates a grammar format profile around a [`LanguageProfile`].
90    #[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    /// Returns this profile with a lossless construct.
100    #[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    /// Returns this profile with an equivalent construct.
107    #[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    /// Returns this profile with a documented lossy fallback for a construct.
116    #[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    /// Canonical format label for this profile.
127    #[must_use]
128    pub const fn format(&self) -> &'static str {
129        self.format
130    }
131
132    /// Underlying language profile.
133    #[must_use]
134    pub const fn language_profile(&self) -> &LanguageProfile {
135        &self.profile
136    }
137
138    /// Lossy fallback table keyed by construct id.
139    #[must_use]
140    pub const fn fallbacks(&self) -> &BTreeMap<String, String> {
141        self.profile.fallbacks()
142    }
143
144    /// Constructs represented through equivalent spelling or normalization.
145    #[must_use]
146    pub const fn equivalent_constructs(&self) -> &BTreeSet<String> {
147        &self.equivalent_constructs
148    }
149
150    /// Whether this format represents a construct without a lossy fallback.
151    #[must_use]
152    pub fn supports_construct(&self, construct: &str) -> bool {
153        self.profile.supports_concept(construct)
154    }
155
156    /// Documented lossy fallback for a construct this format cannot represent
157    /// natively.
158    #[must_use]
159    pub fn construct_fallback(&self, construct: &str) -> Option<&str> {
160        self.profile.concept_fallback(construct)
161    }
162
163    /// Fidelity level for a construct, or `None` when the construct is outside
164    /// this profile's vocabulary.
165    #[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/// Returns the capability profile for a grammar `format`, or `None` when the
182/// format has no fidelity row yet.
183#[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/// Canonicalizes a grammar format label to one of [`GRAMMAR_FORMATS`].
193#[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}