use super::Grammar;
use super::interchange::{parse_grammar_links, render_grammar_links};
use super::round_trip::{
GrammarEmitFn, GrammarImportFn, GrammarRoundTripError, canonical_rule_definition,
};
use super::runtime::GrammarParser;
#[derive(Clone, Copy)]
pub struct GrammarReverseConversion<'a> {
pub import: GrammarImportFn<'a>,
pub emit: GrammarEmitFn<'a>,
pub accepts: &'a [String],
pub rejects: &'a [String],
}
impl<'a> GrammarReverseConversion<'a> {
#[must_use]
pub const fn new(import: GrammarImportFn<'a>, emit: GrammarEmitFn<'a>) -> Self {
Self {
import,
emit,
accepts: &[],
rejects: &[],
}
}
#[must_use]
pub const fn with_samples(mut self, accepts: &'a [String], rejects: &'a [String]) -> Self {
self.accepts = accepts;
self.rejects = rejects;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarReverseStatus {
Equivalent,
Different,
}
impl GrammarReverseStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Equivalent => "equivalent",
Self::Different => "different",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarReverseFailureKind {
LinksNotFaithful,
LossyExport,
RulesChanged,
DocChanged,
SampleRejected,
SampleAccepted,
}
impl GrammarReverseFailureKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::LinksNotFaithful => "links-not-faithful",
Self::LossyExport => "lossy-export",
Self::RulesChanged => "rules-changed",
Self::DocChanged => "doc-changed",
Self::SampleRejected => "sample-rejected",
Self::SampleAccepted => "sample-accepted",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarReverseStage {
Imported,
Links,
Exported,
Reimported,
}
impl GrammarReverseStage {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Imported => "imported",
Self::Links => "links",
Self::Exported => "exported",
Self::Reimported => "reimported",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarReverseFailure {
pub kind: GrammarReverseFailureKind,
pub stage: GrammarReverseStage,
pub detail: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarReverseReport {
pub status: GrammarReverseStatus,
pub failures: Vec<GrammarReverseFailure>,
pub links: String,
pub exported: String,
pub reimported_links: String,
}
pub fn check_grammar_reverse_conversion(
source: &str,
conversion: &GrammarReverseConversion<'_>,
) -> Result<GrammarReverseReport, GrammarRoundTripError> {
let mut failures = Vec::new();
let imported = (conversion.import)(source)?;
sample_failures(
&mut failures,
GrammarReverseStage::Imported,
&imported,
conversion,
);
let links = render_grammar_links(&imported);
let decoded = parse_grammar_links(&links)?;
if render_grammar_links(&decoded) != links {
failures.push(failure(
GrammarReverseFailureKind::LinksNotFaithful,
GrammarReverseStage::Links,
"reading the links back writes different links",
));
}
let (exported, report) = (conversion.emit)(&decoded)?;
for note in &report.lossy {
failures.push(failure(
GrammarReverseFailureKind::LossyExport,
GrammarReverseStage::Exported,
note,
));
}
let reimported = (conversion.import)(&exported)?;
let reimported_links = render_grammar_links(&reimported);
compare_grammars(&mut failures, &imported, &reimported)?;
sample_failures(
&mut failures,
GrammarReverseStage::Reimported,
&reimported,
conversion,
);
Ok(GrammarReverseReport {
status: if failures.is_empty() {
GrammarReverseStatus::Equivalent
} else {
GrammarReverseStatus::Different
},
failures,
links,
exported,
reimported_links,
})
}
fn failure(
kind: GrammarReverseFailureKind,
stage: GrammarReverseStage,
detail: &str,
) -> GrammarReverseFailure {
GrammarReverseFailure {
kind,
stage,
detail: detail.to_owned(),
}
}
fn compare_grammars(
failures: &mut Vec<GrammarReverseFailure>,
expected: &Grammar,
actual: &Grammar,
) -> Result<(), GrammarRoundTripError> {
let mut changed = |detail: &str| {
failures.push(failure(
GrammarReverseFailureKind::RulesChanged,
GrammarReverseStage::Reimported,
detail,
));
};
let expected_names = expected.rule_names();
let actual_names = actual.rule_names();
if expected_names != actual_names {
changed(&format!(
"rule names [{}] became [{}]",
expected_names.join(", "),
actual_names.join(", ")
));
}
let start = |grammar: &Grammar| grammar.start_rule().map(|rule| rule.name.clone());
if start(expected) != start(actual) {
changed("the start rule changed");
}
for rule in expected.rules() {
let Some(other) = actual.rule(&rule.name) else {
continue;
};
if canonical_rule_definition(other)? != canonical_rule_definition(rule)? {
failures.push(failure(
GrammarReverseFailureKind::RulesChanged,
GrammarReverseStage::Reimported,
&format!("rule {} changed its definition", rule.name),
));
}
if other.doc() != rule.doc() {
failures.push(failure(
GrammarReverseFailureKind::DocChanged,
GrammarReverseStage::Reimported,
&format!("rule {} changed its documentation", rule.name),
));
}
}
Ok(())
}
fn sample_failures(
failures: &mut Vec<GrammarReverseFailure>,
stage: GrammarReverseStage,
grammar: &Grammar,
conversion: &GrammarReverseConversion<'_>,
) {
let parser = GrammarParser::new(grammar.clone());
for text in conversion.accepts {
if !parser.accepts(text) {
failures.push(failure(
GrammarReverseFailureKind::SampleRejected,
stage,
text,
));
}
}
for text in conversion.rejects {
if parser.accepts(text) {
failures.push(failure(
GrammarReverseFailureKind::SampleAccepted,
stage,
text,
));
}
}
}