Skip to main content

meta_language/grammar/
reverse.rs

1//! Reverse conversion of a grammar back to its source format.
2//!
3//! The checked cycle is source grammar -> native links -> exported grammar ->
4//! native links: the source is imported, written as the native links form and
5//! read back from those links alone, so the emitter is handed a grammar decoded
6//! from links and never the source text. The export is re-imported and
7//! compared with the imported grammar rule by rule (names, start rule, kinds,
8//! definitions modulo the documented canonical spelling of single-character
9//! sets, and rule documentation, the provenance an importer keeps from
10//! comments), and both grammars are run on independent accept and reject
11//! samples. It mirrors `js/src/grammar-reverse.js`.
12
13use super::Grammar;
14use super::interchange::{parse_grammar_links, render_grammar_links};
15use super::round_trip::{
16    GrammarEmitFn, GrammarImportFn, GrammarRoundTripError, canonical_rule_definition,
17};
18use super::runtime::GrammarParser;
19
20/// The importer/emitter pair under test and the independent samples.
21#[derive(Clone, Copy)]
22pub struct GrammarReverseConversion<'a> {
23    /// Importer of the notation.
24    pub import: GrammarImportFn<'a>,
25    /// Emitter of the same notation.
26    pub emit: GrammarEmitFn<'a>,
27    /// Texts the grammar must accept.
28    pub accepts: &'a [String],
29    /// Texts the grammar must reject.
30    pub rejects: &'a [String],
31}
32
33impl<'a> GrammarReverseConversion<'a> {
34    /// Builds a reverse conversion through `import` and `emit` with no samples.
35    #[must_use]
36    pub const fn new(import: GrammarImportFn<'a>, emit: GrammarEmitFn<'a>) -> Self {
37        Self {
38            import,
39            emit,
40            accepts: &[],
41            rejects: &[],
42        }
43    }
44
45    /// Returns this conversion with independent accept and reject samples.
46    #[must_use]
47    pub const fn with_samples(mut self, accepts: &'a [String], rejects: &'a [String]) -> Self {
48        self.accepts = accepts;
49        self.rejects = rejects;
50        self
51    }
52}
53
54/// Whether the cycle kept the grammar.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum GrammarReverseStatus {
57    /// Every check held.
58    Equivalent,
59    /// At least one check failed.
60    Different,
61}
62
63impl GrammarReverseStatus {
64    /// Stable tag shared with the JavaScript runtime.
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::Equivalent => "equivalent",
69            Self::Different => "different",
70        }
71    }
72}
73
74/// What a failed check found.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum GrammarReverseFailureKind {
77    /// Reading the links back writes different links.
78    LinksNotFaithful,
79    /// The emitter reported a lossy conversion.
80    LossyExport,
81    /// The re-imported rules differ from the imported ones.
82    RulesChanged,
83    /// A re-imported rule lost or changed its documentation.
84    DocChanged,
85    /// A text the grammar must accept was rejected.
86    SampleRejected,
87    /// A text the grammar must reject was accepted.
88    SampleAccepted,
89}
90
91impl GrammarReverseFailureKind {
92    /// Stable tag shared with the JavaScript runtime.
93    #[must_use]
94    pub const fn as_str(self) -> &'static str {
95        match self {
96            Self::LinksNotFaithful => "links-not-faithful",
97            Self::LossyExport => "lossy-export",
98            Self::RulesChanged => "rules-changed",
99            Self::DocChanged => "doc-changed",
100            Self::SampleRejected => "sample-rejected",
101            Self::SampleAccepted => "sample-accepted",
102        }
103    }
104}
105
106/// The step of the cycle a failed check looked at.
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum GrammarReverseStage {
109    /// The grammar imported from the source.
110    Imported,
111    /// The native links of the imported grammar.
112    Links,
113    /// The text exported from the grammar decoded from links.
114    Exported,
115    /// The grammar re-imported from the exported text.
116    Reimported,
117}
118
119impl GrammarReverseStage {
120    /// Stable tag shared with the JavaScript runtime.
121    #[must_use]
122    pub const fn as_str(self) -> &'static str {
123        match self {
124            Self::Imported => "imported",
125            Self::Links => "links",
126            Self::Exported => "exported",
127            Self::Reimported => "reimported",
128        }
129    }
130}
131
132/// One failed check.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct GrammarReverseFailure {
135    /// What the check found.
136    pub kind: GrammarReverseFailureKind,
137    /// The step it looked at.
138    pub stage: GrammarReverseStage,
139    /// The sample, note or rule involved.
140    pub detail: String,
141}
142
143/// The outcome of [`check_grammar_reverse_conversion`].
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct GrammarReverseReport {
146    /// Whether every check held.
147    pub status: GrammarReverseStatus,
148    /// Every failed check, in the order they ran.
149    pub failures: Vec<GrammarReverseFailure>,
150    /// The native links of the imported grammar.
151    pub links: String,
152    /// The text exported from the grammar decoded from those links.
153    pub exported: String,
154    /// The native links of the grammar re-imported from the export.
155    pub reimported_links: String,
156}
157
158/// Runs the reverse conversion cycle on `source` and reports every failure.
159///
160/// # Errors
161///
162/// Returns the importer's error for a malformed source, the links reader's
163/// error, the emitter's error, or a normalization error.
164pub fn check_grammar_reverse_conversion(
165    source: &str,
166    conversion: &GrammarReverseConversion<'_>,
167) -> Result<GrammarReverseReport, GrammarRoundTripError> {
168    let mut failures = Vec::new();
169    let imported = (conversion.import)(source)?;
170    sample_failures(
171        &mut failures,
172        GrammarReverseStage::Imported,
173        &imported,
174        conversion,
175    );
176
177    let links = render_grammar_links(&imported);
178    let decoded = parse_grammar_links(&links)?;
179    if render_grammar_links(&decoded) != links {
180        failures.push(failure(
181            GrammarReverseFailureKind::LinksNotFaithful,
182            GrammarReverseStage::Links,
183            "reading the links back writes different links",
184        ));
185    }
186
187    let (exported, report) = (conversion.emit)(&decoded)?;
188    for note in &report.lossy {
189        failures.push(failure(
190            GrammarReverseFailureKind::LossyExport,
191            GrammarReverseStage::Exported,
192            note,
193        ));
194    }
195
196    let reimported = (conversion.import)(&exported)?;
197    let reimported_links = render_grammar_links(&reimported);
198    compare_grammars(&mut failures, &imported, &reimported)?;
199    sample_failures(
200        &mut failures,
201        GrammarReverseStage::Reimported,
202        &reimported,
203        conversion,
204    );
205
206    Ok(GrammarReverseReport {
207        status: if failures.is_empty() {
208            GrammarReverseStatus::Equivalent
209        } else {
210            GrammarReverseStatus::Different
211        },
212        failures,
213        links,
214        exported,
215        reimported_links,
216    })
217}
218
219fn failure(
220    kind: GrammarReverseFailureKind,
221    stage: GrammarReverseStage,
222    detail: &str,
223) -> GrammarReverseFailure {
224    GrammarReverseFailure {
225        kind,
226        stage,
227        detail: detail.to_owned(),
228    }
229}
230
231fn compare_grammars(
232    failures: &mut Vec<GrammarReverseFailure>,
233    expected: &Grammar,
234    actual: &Grammar,
235) -> Result<(), GrammarRoundTripError> {
236    let mut changed = |detail: &str| {
237        failures.push(failure(
238            GrammarReverseFailureKind::RulesChanged,
239            GrammarReverseStage::Reimported,
240            detail,
241        ));
242    };
243    let expected_names = expected.rule_names();
244    let actual_names = actual.rule_names();
245    if expected_names != actual_names {
246        changed(&format!(
247            "rule names [{}] became [{}]",
248            expected_names.join(", "),
249            actual_names.join(", ")
250        ));
251    }
252    let start = |grammar: &Grammar| grammar.start_rule().map(|rule| rule.name.clone());
253    if start(expected) != start(actual) {
254        changed("the start rule changed");
255    }
256    for rule in expected.rules() {
257        let Some(other) = actual.rule(&rule.name) else {
258            continue;
259        };
260        if canonical_rule_definition(other)? != canonical_rule_definition(rule)? {
261            failures.push(failure(
262                GrammarReverseFailureKind::RulesChanged,
263                GrammarReverseStage::Reimported,
264                &format!("rule {} changed its definition", rule.name),
265            ));
266        }
267        if other.doc() != rule.doc() {
268            failures.push(failure(
269                GrammarReverseFailureKind::DocChanged,
270                GrammarReverseStage::Reimported,
271                &format!("rule {} changed its documentation", rule.name),
272            ));
273        }
274    }
275    Ok(())
276}
277
278fn sample_failures(
279    failures: &mut Vec<GrammarReverseFailure>,
280    stage: GrammarReverseStage,
281    grammar: &Grammar,
282    conversion: &GrammarReverseConversion<'_>,
283) {
284    let parser = GrammarParser::new(grammar.clone());
285    for text in conversion.accepts {
286        if !parser.accepts(text) {
287            failures.push(failure(
288                GrammarReverseFailureKind::SampleRejected,
289                stage,
290                text,
291            ));
292        }
293    }
294    for text in conversion.rejects {
295        if parser.accepts(text) {
296            failures.push(failure(
297                GrammarReverseFailureKind::SampleAccepted,
298                stage,
299                text,
300            ));
301        }
302    }
303}