use std::error::Error;
use std::fmt;
use super::emit::{EmitReport, GrammarEmitError};
use super::import::GrammarImportError;
use super::merge::{GrammarMergeError, normalized_rule_definition};
use super::runtime::GrammarParser;
use super::{CharClassItem, Grammar, GrammarExpr, GrammarFormat, GrammarRule};
pub const GRAMMAR_ROUND_TRIP_MARKER: &str = "round-trip-mutation";
pub type GrammarImportFn<'a> = &'a dyn Fn(&str) -> Result<Grammar, GrammarImportError>;
pub type GrammarEmitFn<'a> = &'a dyn Fn(&Grammar) -> Result<(String, EmitReport), GrammarEmitError>;
#[derive(Clone, Copy)]
pub struct GrammarRoundTrip<'a> {
pub import: GrammarImportFn<'a>,
pub emit: GrammarEmitFn<'a>,
pub marker: &'a str,
pub accepts: &'a [String],
pub rejects: &'a [String],
}
impl<'a> GrammarRoundTrip<'a> {
#[must_use]
pub const fn new(import: GrammarImportFn<'a>, emit: GrammarEmitFn<'a>) -> Self {
Self {
import,
emit,
marker: GRAMMAR_ROUND_TRIP_MARKER,
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 GrammarRoundTripStatus {
Preserved,
Broken,
}
impl GrammarRoundTripStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Preserved => "preserved",
Self::Broken => "broken",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarRoundTripFailureKind {
SampleRejected,
SampleAccepted,
MarkerAlreadyAccepted,
LossyExport,
MutationNotExported,
RulesChanged,
MutationNotVisible,
ExportNotStable,
}
impl GrammarRoundTripFailureKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SampleRejected => "sample-rejected",
Self::SampleAccepted => "sample-accepted",
Self::MarkerAlreadyAccepted => "marker-already-accepted",
Self::LossyExport => "lossy-export",
Self::MutationNotExported => "mutation-not-exported",
Self::RulesChanged => "rules-changed",
Self::MutationNotVisible => "mutation-not-visible",
Self::ExportNotStable => "export-not-stable",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarRoundTripStage {
Imported,
Exported,
Reimported,
}
impl GrammarRoundTripStage {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Imported => "imported",
Self::Exported => "exported",
Self::Reimported => "reimported",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarRoundTripFailure {
pub kind: GrammarRoundTripFailureKind,
pub stage: GrammarRoundTripStage,
pub detail: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarRoundTripReport {
pub status: GrammarRoundTripStatus,
pub failures: Vec<GrammarRoundTripFailure>,
pub mutated: Grammar,
pub exported: String,
pub reimported: Grammar,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GrammarRoundTripError {
Import(GrammarImportError),
Emit(GrammarEmitError),
Normalize(GrammarMergeError),
MissingStartRule,
}
impl fmt::Display for GrammarRoundTripError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Import(error) => write!(formatter, "{error}"),
Self::Emit(error) => write!(formatter, "{error}"),
Self::Normalize(error) => write!(formatter, "{error}"),
Self::MissingStartRule => {
formatter.write_str("the grammar has no start rule to mutate")
}
}
}
}
impl Error for GrammarRoundTripError {}
impl From<GrammarImportError> for GrammarRoundTripError {
fn from(error: GrammarImportError) -> Self {
Self::Import(error)
}
}
impl From<GrammarEmitError> for GrammarRoundTripError {
fn from(error: GrammarEmitError) -> Self {
Self::Emit(error)
}
}
impl From<GrammarMergeError> for GrammarRoundTripError {
fn from(error: GrammarMergeError) -> Self {
Self::Normalize(error)
}
}
pub fn mutate_grammar_start_rule(
grammar: &Grammar,
marker: &str,
) -> Result<Grammar, GrammarRoundTripError> {
let start = grammar
.start_rule()
.ok_or(GrammarRoundTripError::MissingStartRule)?
.name
.clone();
let ordered = grammar.source_format() == Some(GrammarFormat::Peg);
let mut mutated = Grammar::new();
for rule in grammar.rules() {
let mut rule = rule.clone();
if rule.name == start {
rule.expr = GrammarExpr::Choice {
ordered,
alternatives: vec![rule.expr, GrammarExpr::Terminal(marker.to_owned())],
};
}
mutated.add_rule(rule);
}
if let Some(name) = grammar.start() {
mutated.set_start(name);
}
if let Some(format) = grammar.source_format() {
mutated.set_source_format(format);
}
Ok(mutated)
}
pub fn check_grammar_round_trip(
source: &str,
round_trip: &GrammarRoundTrip<'_>,
) -> Result<GrammarRoundTripReport, GrammarRoundTripError> {
let mut failures = Vec::new();
let imported = (round_trip.import)(source)?;
check_samples(
&mut failures,
GrammarRoundTripStage::Imported,
&imported,
round_trip,
);
if accepts(&imported, round_trip.marker) {
failures.push(failure(
GrammarRoundTripFailureKind::MarkerAlreadyAccepted,
GrammarRoundTripStage::Imported,
round_trip.marker,
));
}
let mutated = mutate_grammar_start_rule(&imported, round_trip.marker)?;
let (unmutated, _) = (round_trip.emit)(&imported)?;
let (exported, report) = (round_trip.emit)(&mutated)?;
for note in &report.lossy {
failures.push(failure(
GrammarRoundTripFailureKind::LossyExport,
GrammarRoundTripStage::Exported,
note,
));
}
if exported == unmutated || !exported.contains(round_trip.marker) {
failures.push(failure(
GrammarRoundTripFailureKind::MutationNotExported,
GrammarRoundTripStage::Exported,
round_trip.marker,
));
}
let reimported = (round_trip.import)(&exported)?;
compare_rules(&mut failures, &mutated, &reimported)?;
if !accepts(&reimported, round_trip.marker) {
failures.push(failure(
GrammarRoundTripFailureKind::MutationNotVisible,
GrammarRoundTripStage::Reimported,
round_trip.marker,
));
}
check_samples(
&mut failures,
GrammarRoundTripStage::Reimported,
&reimported,
round_trip,
);
let (again, _) = (round_trip.emit)(&reimported)?;
let settled = (round_trip.import)(&again)?;
if definitions(&mutated)? != definitions(&settled)? || (round_trip.emit)(&settled)?.0 != again {
failures.push(failure(
GrammarRoundTripFailureKind::ExportNotStable,
GrammarRoundTripStage::Reimported,
"a second export does not settle",
));
}
Ok(GrammarRoundTripReport {
status: if failures.is_empty() {
GrammarRoundTripStatus::Preserved
} else {
GrammarRoundTripStatus::Broken
},
failures,
mutated,
exported,
reimported,
})
}
fn failure(
kind: GrammarRoundTripFailureKind,
stage: GrammarRoundTripStage,
detail: &str,
) -> GrammarRoundTripFailure {
GrammarRoundTripFailure {
kind,
stage,
detail: detail.to_owned(),
}
}
fn compare_rules(
failures: &mut Vec<GrammarRoundTripFailure>,
expected: &Grammar,
actual: &Grammar,
) -> Result<(), GrammarRoundTripError> {
let expected_names = expected.rule_names();
let actual_names = actual.rule_names();
if expected_names != actual_names {
failures.push(failure(
GrammarRoundTripFailureKind::RulesChanged,
GrammarRoundTripStage::Reimported,
&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) {
failures.push(failure(
GrammarRoundTripFailureKind::RulesChanged,
GrammarRoundTripStage::Reimported,
"the start rule changed",
));
}
for rule in expected.rules() {
if let Some(other) = actual.rule(&rule.name)
&& canonical_rule_definition(other)? != canonical_rule_definition(rule)?
{
failures.push(failure(
GrammarRoundTripFailureKind::RulesChanged,
GrammarRoundTripStage::Reimported,
&format!("rule {} changed its definition", rule.name),
));
}
}
Ok(())
}
fn definitions(grammar: &Grammar) -> Result<Vec<String>, GrammarRoundTripError> {
grammar
.rules()
.iter()
.map(|rule| {
Ok(format!(
"{}={}",
rule.name,
canonical_rule_definition(rule)?
))
})
.collect()
}
pub fn canonical_rule_definition(rule: &GrammarRule) -> Result<String, GrammarRoundTripError> {
let canonical = GrammarRule {
expr: canonical_characters(&rule.expr),
..rule.clone()
};
Ok(normalized_rule_definition(&canonical)?)
}
fn canonical_characters(expr: &GrammarExpr) -> GrammarExpr {
if let Some(mut set) = character_set(expr) {
return if set.len() == 1 {
set.remove(0)
} else {
GrammarExpr::Choice {
ordered: false,
alternatives: set,
}
};
}
let boxed = |inner: &GrammarExpr| Box::new(canonical_characters(inner));
match expr {
GrammarExpr::Choice {
ordered,
alternatives,
} => GrammarExpr::Choice {
ordered: *ordered,
alternatives: alternatives.iter().map(canonical_characters).collect(),
},
GrammarExpr::Sequence(items) => {
GrammarExpr::Sequence(items.iter().map(canonical_characters).collect())
}
GrammarExpr::Optional(inner) => GrammarExpr::Optional(boxed(inner)),
GrammarExpr::ZeroOrMore(inner) => GrammarExpr::ZeroOrMore(boxed(inner)),
GrammarExpr::OneOrMore(inner) => GrammarExpr::OneOrMore(boxed(inner)),
GrammarExpr::Repeat { expr, min, max } => GrammarExpr::Repeat {
expr: boxed(expr),
min: *min,
max: *max,
},
GrammarExpr::And(inner) => GrammarExpr::And(boxed(inner)),
GrammarExpr::Not(inner) => GrammarExpr::Not(boxed(inner)),
GrammarExpr::Capture { label, expr } => GrammarExpr::Capture {
label: label.clone(),
expr: boxed(expr),
},
other => other.clone(),
}
}
fn character_set(expr: &GrammarExpr) -> Option<Vec<GrammarExpr>> {
match expr {
GrammarExpr::Terminal(value) if value.chars().count() == 1 => Some(vec![expr.clone()]),
GrammarExpr::CharRange(..) => Some(vec![expr.clone()]),
GrammarExpr::CharClass {
negated: false,
items,
} => Some(
items
.iter()
.map(|item| match item {
CharClassItem::Char(value) => GrammarExpr::Terminal(value.to_string()),
CharClassItem::Range(start, end) => GrammarExpr::CharRange(*start, *end),
})
.collect(),
),
GrammarExpr::Choice { alternatives, .. } => alternatives
.iter()
.map(character_set)
.collect::<Option<Vec<_>>>()
.map(|sets| sets.into_iter().flatten().collect()),
_ => None,
}
}
fn check_samples(
failures: &mut Vec<GrammarRoundTripFailure>,
stage: GrammarRoundTripStage,
grammar: &Grammar,
round_trip: &GrammarRoundTrip<'_>,
) {
let parser = GrammarParser::new(grammar.clone());
for text in round_trip.accepts {
if !parser.accepts(text) {
failures.push(failure(
GrammarRoundTripFailureKind::SampleRejected,
stage,
text,
));
}
}
for text in round_trip.rejects {
if parser.accepts(text) {
failures.push(failure(
GrammarRoundTripFailureKind::SampleAccepted,
stage,
text,
));
}
}
}
#[must_use]
pub fn accepts_text(grammar: &Grammar, text: &str) -> bool {
super::feature_runtime::parse_with_grammar(
grammar,
text.as_bytes(),
&super::feature_runtime::FeatureParseOptions::default(),
)
.is_ok()
}
fn accepts(grammar: &Grammar, text: &str) -> bool {
GrammarParser::new(grammar.clone()).accepts(text)
}