mod declarations;
mod group;
mod names;
mod normalize;
mod reconcile;
mod rename;
use group::{
Prepared, edition_alternatives, group_fingerprint, group_key, merge_group, name_label,
normalize_samples, prepare_sources, required_failures, source_of,
};
use normalize::normalize;
pub use rename::{rename_grammar_rule, restore_source_names};
use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt;
use serde::Serialize;
use super::{Grammar, GrammarRule};
use crate::decorators::{DecoratorLevel, DecoratorSet, decorator_record, record_field};
pub const GRAMMAR_MERGE_METHOD: &str = "recursive-structural-bisimulation";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarMergeSource {
pub id: String,
pub language: String,
pub edition: String,
pub precedence: u32,
pub grammar: Grammar,
}
impl GrammarMergeSource {
#[must_use]
pub fn new(id: impl Into<String>, language: impl Into<String>, grammar: Grammar) -> Self {
Self {
id: id.into(),
language: language.into(),
edition: String::new(),
precedence: 0,
grammar,
}
}
#[must_use]
pub fn with_edition(mut self, edition: impl Into<String>) -> Self {
self.edition = edition.into();
self
}
#[must_use]
pub const fn with_precedence(mut self, precedence: u32) -> Self {
self.precedence = precedence;
self
}
}
#[derive(Clone, Debug, Default)]
pub struct GrammarMergeOptions<'a> {
pub samples: BTreeMap<String, Vec<String>>,
pub required_equivalences: Vec<(String, String)>,
pub previous: Option<&'a GrammarMergeResult>,
pub decorators: DecoratorSet,
pub reconcile: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeDecisionKind {
Merged,
Reconciled,
KeptUnique,
RenamedForCollision,
HomonymKeptDistinct,
Uncertain,
DeclarationConflict,
}
impl GrammarMergeDecisionKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Merged => "merged",
Self::Reconciled => "reconciled",
Self::KeptUnique => "kept-unique",
Self::RenamedForCollision => "renamed-for-collision",
Self::HomonymKeptDistinct => "homonym-kept-distinct",
Self::Uncertain => "uncertain",
Self::DeclarationConflict => "declaration-conflict",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
[
Self::Merged,
Self::Reconciled,
Self::KeptUnique,
Self::RenamedForCollision,
Self::HomonymKeptDistinct,
Self::Uncertain,
Self::DeclarationConflict,
]
.into_iter()
.find(|kind| kind.as_str() == text)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct GrammarMergeDecision {
pub kind: GrammarMergeDecisionKind,
pub name: String,
pub members: Vec<String>,
pub basis: String,
pub definition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concept: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeNominationBasis {
NameSimilarity,
IdenticalSamples,
}
impl GrammarMergeNominationBasis {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NameSimilarity => "name-similarity",
Self::IdenticalSamples => "identical-samples",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeNominationOutcome {
Proven,
Reconciled,
Unproven,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct GrammarMergeNomination {
pub basis: GrammarMergeNominationBasis,
pub members: [String; 2],
pub outcome: GrammarMergeNominationOutcome,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeAlternativeReason {
StartRule,
DistinctMeaning,
UncertainMatch,
Edition,
DeclarationConflict,
}
impl GrammarMergeAlternativeReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::StartRule => "start-rule",
Self::DistinctMeaning => "distinct-meaning",
Self::UncertainMatch => "uncertain-match",
Self::Edition => "edition",
Self::DeclarationConflict => "declaration-conflict",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct GrammarMergeAlternative {
pub reason: GrammarMergeAlternativeReason,
pub name: String,
pub options: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeFailureKind {
UnresolvedRequiredEquivalence,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarMergeFailureReason {
UnknownRule,
DifferentLanguageOrEdition,
NotProven,
}
impl GrammarMergeFailureReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::UnknownRule => "unknown-rule",
Self::DifferentLanguageOrEdition => "different-language-or-edition",
Self::NotProven => "not-proven",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct GrammarMergeFailure {
pub kind: GrammarMergeFailureKind,
pub members: [String; 2],
pub reason: GrammarMergeFailureReason,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MergedGrammarGroup {
pub key: String,
pub language: String,
pub edition: String,
pub fingerprint: String,
pub sources: Vec<String>,
pub grammar: Grammar,
pub identities: BTreeMap<String, String>,
pub decisions: Vec<GrammarMergeDecision>,
pub nominations: Vec<GrammarMergeNomination>,
pub alternatives: Vec<GrammarMergeAlternative>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarMergeResult {
pub groups: Vec<MergedGrammarGroup>,
pub alternatives: Vec<GrammarMergeAlternative>,
pub failures: Vec<GrammarMergeFailure>,
pub reused: Vec<String>,
pub recomputed: Vec<String>,
}
impl GrammarMergeResult {
#[must_use]
pub const fn status(&self) -> &'static str {
if self.failures.is_empty() {
"complete"
} else {
"incomplete"
}
}
#[must_use]
pub fn group(&self, key: &str) -> Option<&MergedGrammarGroup> {
self.groups.iter().find(|group| group.key == key)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarMergeError {
message: String,
failures: Vec<GrammarMergeFailure>,
concatenated: Vec<String>,
}
impl GrammarMergeError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
failures: Vec::new(),
concatenated: Vec::new(),
}
}
#[must_use]
pub fn failures(&self) -> &[GrammarMergeFailure] {
&self.failures
}
#[must_use]
pub fn concatenated(&self) -> &[String] {
&self.concatenated
}
}
impl fmt::Display for GrammarMergeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for GrammarMergeError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrammarRenameErrorKind {
InvalidName,
UnknownRule,
Collision,
}
impl GrammarRenameErrorKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidName => "invalid-name",
Self::UnknownRule => "unknown-rule",
Self::Collision => "collision",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrammarRenameError {
kind: GrammarRenameErrorKind,
message: String,
}
impl GrammarRenameError {
fn new(kind: GrammarRenameErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
#[must_use]
pub const fn kind(&self) -> GrammarRenameErrorKind {
self.kind
}
}
impl fmt::Display for GrammarRenameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for GrammarRenameError {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RuleAlias {
pub canonical: String,
pub original: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenamedGrammar {
pub grammar: Grammar,
pub aliases: Vec<RuleAlias>,
}
pub fn merge_grammars(
sources: &[GrammarMergeSource],
options: &GrammarMergeOptions<'_>,
) -> Result<GrammarMergeResult, GrammarMergeError> {
let prepared = prepare_sources(sources)?;
let samples = normalize_samples(&options.samples);
let required: BTreeSet<(String, String)> = options
.required_equivalences
.iter()
.map(|(first, second)| {
if first <= second {
(first.clone(), second.clone())
} else {
(second.clone(), first.clone())
}
})
.collect();
let previous_groups: BTreeMap<&str, &MergedGrammarGroup> = options
.previous
.map(|previous| {
previous
.groups
.iter()
.map(|group| (group.key.as_str(), group))
.collect()
})
.unwrap_or_default();
let mut grouped: BTreeMap<(&str, &str), Vec<&Prepared<'_>>> = BTreeMap::new();
for source in &prepared {
grouped
.entry((source.language, source.edition))
.or_default()
.push(source);
}
let mut groups = Vec::new();
let mut reused = Vec::new();
let mut recomputed = Vec::new();
for ((language, edition), entry) in grouped {
let key = group_key(language, edition);
let ids: BTreeSet<&str> = entry.iter().map(|source| source.id).collect();
let group_required: Vec<&(String, String)> = required
.iter()
.filter(|(first, second)| {
ids.contains(source_of(first)) || ids.contains(source_of(second))
})
.collect();
let fingerprint = group_fingerprint(
language,
edition,
&entry,
&group_required,
&samples,
&options.decorators,
options.reconcile,
)?;
let previous = previous_groups.get(key.as_str()).copied();
if let Some(previous) = previous.filter(|previous| previous.fingerprint == fingerprint) {
groups.push(previous.clone());
reused.push(key);
continue;
}
let empty = BTreeMap::new();
let previous_identities = previous.map_or(&empty, |previous| &previous.identities);
let mut group = merge_group(
language,
edition,
&entry,
fingerprint,
&samples,
previous_identities,
options.reconcile,
)?;
group.decisions = decorate_decisions(group.decisions, &options.decorators)?;
groups.push(group);
recomputed.push(key);
}
let alternatives = edition_alternatives(&groups);
let failures = required_failures(&required, &groups);
Ok(GrammarMergeResult {
groups,
alternatives,
failures,
reused,
recomputed,
})
}
fn decorate_decisions(
decisions: Vec<GrammarMergeDecision>,
decorators: &DecoratorSet,
) -> Result<Vec<GrammarMergeDecision>, GrammarMergeError> {
if !decorators.has(DecoratorLevel::MergeDecision) {
return Ok(decisions);
}
let mut kept = Vec::with_capacity(decisions.len());
for mut decision in decisions {
let record = decorator_record([
("kind", decision.kind.as_str()),
("name", &decision.name),
("members", &decision.members.join(" ")),
("basis", &decision.basis),
(
"definition",
decision.definition.as_deref().unwrap_or_default(),
),
]);
let Some(decorated) = decorators.decorate(DecoratorLevel::MergeDecision, &record) else {
continue;
};
let kind = record_field(&decorated, "kind");
decision.kind = GrammarMergeDecisionKind::parse(kind).ok_or_else(|| {
GrammarMergeError::new(format!(
"a decorator set the unknown decision kind {kind:?}"
))
})?;
record_field(&decorated, "basis").clone_into(&mut decision.basis);
kept.push(decision);
}
Ok(kept)
}
pub fn assert_merge_complete(
result: &GrammarMergeResult,
) -> Result<&GrammarMergeResult, GrammarMergeError> {
if result.failures.is_empty() {
return Ok(result);
}
let detail = result
.failures
.iter()
.map(|failure| {
format!(
"{} = {} ({})",
failure.members[0],
failure.members[1],
failure.reason.as_str()
)
})
.collect::<Vec<_>>()
.join("; ");
Err(GrammarMergeError {
message: format!("unresolved required equivalence: {detail}"),
failures: result.failures.clone(),
concatenated: Vec::new(),
})
}
#[must_use]
pub fn shared_rule_decisions(group: &MergedGrammarGroup) -> Vec<&GrammarMergeDecision> {
group
.decisions
.iter()
.filter(|decision| {
matches!(
decision.kind,
GrammarMergeDecisionKind::Merged | GrammarMergeDecisionKind::Reconciled
) && decision
.members
.iter()
.map(|member| source_of(member))
.collect::<BTreeSet<_>>()
.len()
> 1
})
.collect()
}
pub fn assert_merge_shares(
result: &GrammarMergeResult,
) -> Result<&GrammarMergeResult, GrammarMergeError> {
let concatenated: Vec<&MergedGrammarGroup> = result
.groups
.iter()
.filter(|group| group.sources.len() > 1 && shared_rule_decisions(group).is_empty())
.collect();
if concatenated.is_empty() {
return Ok(result);
}
let detail = concatenated
.iter()
.map(|group| {
format!(
"{} ({}: {} rules)",
group.key,
group.sources.join(", "),
group.grammar.rules().len()
)
})
.collect::<Vec<_>>()
.join("; ");
Err(GrammarMergeError {
message: format!("the merge only concatenates its sources, sharing no rule: {detail}"),
failures: Vec::new(),
concatenated: concatenated.iter().map(|group| group.key.clone()).collect(),
})
}
pub fn normalized_rule_definition(rule: &GrammarRule) -> Result<String, GrammarMergeError> {
Ok(format!(
"{}:{}",
rule.kind.as_str(),
normalize(&rule.expr, &name_label)?.text
))
}