1use 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#[derive(Clone, Copy)]
22pub struct GrammarReverseConversion<'a> {
23 pub import: GrammarImportFn<'a>,
25 pub emit: GrammarEmitFn<'a>,
27 pub accepts: &'a [String],
29 pub rejects: &'a [String],
31}
32
33impl<'a> GrammarReverseConversion<'a> {
34 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum GrammarReverseStatus {
57 Equivalent,
59 Different,
61}
62
63impl GrammarReverseStatus {
64 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum GrammarReverseFailureKind {
77 LinksNotFaithful,
79 LossyExport,
81 RulesChanged,
83 DocChanged,
85 SampleRejected,
87 SampleAccepted,
89}
90
91impl GrammarReverseFailureKind {
92 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum GrammarReverseStage {
109 Imported,
111 Links,
113 Exported,
115 Reimported,
117}
118
119impl GrammarReverseStage {
120 #[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#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct GrammarReverseFailure {
135 pub kind: GrammarReverseFailureKind,
137 pub stage: GrammarReverseStage,
139 pub detail: String,
141}
142
143#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct GrammarReverseReport {
146 pub status: GrammarReverseStatus,
148 pub failures: Vec<GrammarReverseFailure>,
150 pub links: String,
152 pub exported: String,
154 pub reimported_links: String,
156}
157
158pub 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}