use std::collections::{BTreeMap, BTreeSet};
use petgraph::algo::tarjan_scc;
use petgraph::graph::DiGraph;
use super::action::{ActionReferenceKind, action_references};
use super::model::{
Alternative, AlternativeId, Block, Element, ElementKind, GrammarKind, GrammarUnit,
ModelIdAllocator, ModelNodeId, OptionDecl, Quantifier, Rule, RuleCall, RuleId, RuleKind,
Terminal,
};
use super::provenance::{Origin, ProvenanceIndex, SyntheticReason};
pub(crate) fn eliminate_mutual_left_recursion(
units: &mut [GrammarUnit],
ids: &mut ModelIdAllocator,
provenance: &mut ProvenanceIndex,
) -> bool {
let mut changed = false;
for unit in units.iter_mut() {
if unit.kind == GrammarKind::Parser {
changed |= eliminate_in_unit(unit, ids, provenance);
}
}
changed
}
type Cycle = Vec<RuleId>;
#[derive(Clone, Copy)]
struct Grammar<'a> {
names: &'a BTreeMap<String, RuleId>,
nullable: &'a BTreeSet<RuleId>,
}
impl Grammar<'_> {
fn target(self, call: &RuleCall) -> Option<RuleId> {
self.names.get(&call.name).copied()
}
}
fn eliminate_in_unit(
unit: &mut GrammarUnit,
ids: &mut ModelIdAllocator,
provenance: &mut ProvenanceIndex,
) -> bool {
let mut changed = false;
loop {
let names = rule_names(unit);
let nullable = nullable_rules(unit, &names);
let grammar = Grammar {
names: &names,
nullable: &nullable,
};
let Some(plan) = left_corner_cycles(unit, grammar)
.iter()
.find_map(|cycle| plan_cycle(unit, cycle, grammar))
else {
return changed;
};
apply_plan(unit, &plan, ids, provenance);
changed = true;
}
}
#[derive(Debug)]
struct CyclePlan {
hub: RuleId,
alternatives: Vec<PlannedAlternative>,
removable: BTreeSet<RuleId>,
}
#[derive(Debug)]
struct PlannedAlternative {
label_from: AlternativeId,
options: Vec<OptionDecl>,
origin: AlternativeId,
elements: Vec<Element>,
verbatim: bool,
}
struct Requirements;
enum Corner {
Bare { index: usize, target: RuleId },
OutsideCycle,
Unusable,
}
fn classify_corner(
alternative: &Alternative,
cycle_set: &BTreeSet<RuleId>,
grammar: Grammar<'_>,
) -> Corner {
for (index, element) in alternative.elements.iter().enumerate() {
match &element.kind {
ElementKind::Action { .. } | ElementKind::Predicate { .. } | ElementKind::Epsilon => {}
ElementKind::RuleCall(call) => {
let Some(target) = grammar.target(call) else {
return Corner::OutsideCycle;
};
if !cycle_set.contains(&target) {
return if grammar.nullable.contains(&target) {
Corner::Unusable
} else {
Corner::OutsideCycle
};
}
return if element.quantifier == Quantifier::One && bare_reference(element, call) {
Corner::Bare { index, target }
} else {
Corner::Unusable
};
}
_ => return Corner::OutsideCycle,
}
}
Corner::OutsideCycle
}
fn corner_enters_cycle(
alternative: &Alternative,
cycle_set: &BTreeSet<RuleId>,
grammar: Grammar<'_>,
) -> bool {
!matches!(
classify_corner(alternative, cycle_set, grammar),
Corner::OutsideCycle
)
}
const fn bare_reference(element: &Element, call: &RuleCall) -> bool {
element.label.is_none() && call.arguments.is_none() && element.options.is_empty()
}
fn plan_cycle(unit: &GrammarUnit, cycle: &Cycle, grammar: Grammar<'_>) -> Option<CyclePlan> {
let rules = rules_by_id(unit);
if cycle.iter().any(|member| !rules.contains_key(member)) {
return None;
}
let cycle_set: BTreeSet<RuleId> = cycle.iter().copied().collect();
let hub_id = choose_hub(unit, cycle, &cycle_set, grammar)?;
if rules[&hub_id].arguments.is_some() {
return None;
}
for member in cycle.iter().filter(|member| **member != hub_id) {
if !satellite_is_inlinable(rules[member]) {
return None;
}
}
let mut planned: Vec<PlannedAlternative> = rules[&hub_id]
.block
.alternatives
.iter()
.map(|alternative| PlannedAlternative {
label_from: alternative.id,
options: alternative.options.clone(),
origin: alternative.id,
elements: alternative.elements.clone(),
verbatim: true,
})
.collect();
let budget = substitution_budget(&cycle_set, &rules);
let mut steps: usize = 0;
while let Some(position) = planned.iter().position(|candidate| {
!matches!(
planned_corner(candidate, hub_id, &cycle_set, grammar),
PlannedCorner::Settled
)
}) {
steps += 1;
if steps > budget {
return None;
}
let replacement = match planned_corner(&planned[position], hub_id, &cycle_set, grammar) {
PlannedCorner::Optional { index, target } => {
if remaining_actions_reference(
&planned[position].elements,
index,
&rules[&target].name,
) {
return None;
}
split_optional(&planned[position], index)
}
PlannedCorner::Satellite { index, target } => {
splice_satellite(&planned[position], index, rules[&target])?
}
PlannedCorner::Blocked => return None,
PlannedCorner::Settled => unreachable!("position was found to need work"),
};
planned.splice(position..=position, replacement);
}
if steps == 0 {
return None;
}
if !planned_hub_is_directly_rewritable(&planned, hub_id, &cycle_set, grammar) {
return None;
}
let removable = removable_satellites(unit, cycle, hub_id, &planned, grammar.names);
Some(CyclePlan {
hub: hub_id,
alternatives: planned,
removable,
})
}
enum PlannedCorner {
Optional { index: usize, target: RuleId },
Satellite { index: usize, target: RuleId },
Blocked,
Settled,
}
fn planned_corner(
candidate: &PlannedAlternative,
hub_id: RuleId,
cycle_set: &BTreeSet<RuleId>,
grammar: Grammar<'_>,
) -> PlannedCorner {
for (index, element) in candidate.elements.iter().enumerate() {
match &element.kind {
ElementKind::Action { .. } | ElementKind::Predicate { .. } | ElementKind::Epsilon => {}
ElementKind::RuleCall(call) => {
let Some(target) = grammar.target(call) else {
return PlannedCorner::Settled;
};
if !cycle_set.contains(&target) {
return PlannedCorner::Settled;
}
if matches!(element.quantifier, Quantifier::Optional { greedy: true })
&& bare_reference(element, call)
{
return PlannedCorner::Optional { index, target };
}
if target == hub_id && element.quantifier == Quantifier::One {
return PlannedCorner::Settled;
}
if element.quantifier == Quantifier::One && bare_reference(element, call) {
return PlannedCorner::Satellite { index, target };
}
return PlannedCorner::Blocked;
}
_ => return PlannedCorner::Settled,
}
}
PlannedCorner::Settled
}
fn remaining_actions_reference(elements: &[Element], skip: usize, rule_name: &str) -> bool {
elements.iter().enumerate().any(|(index, element)| {
if index == skip {
return false;
}
element_actions_reference(element, rule_name)
})
}
fn element_actions_reference(element: &Element, rule_name: &str) -> bool {
let mut bodies: Vec<&str> = Vec::new();
match &element.kind {
ElementKind::Action { body, .. } => bodies.push(body),
ElementKind::Predicate { body, fail, .. } => {
bodies.push(body);
if let Some(fail) = fail.as_deref() {
bodies.push(fail);
}
}
ElementKind::Block(block) => {
return block.alternatives.iter().any(|alternative| {
alternative
.elements
.iter()
.any(|nested| element_actions_reference(nested, rule_name))
});
}
_ => {}
}
bodies.into_iter().any(|body| {
action_references(body)
.iter()
.any(|reference| match reference.kind {
ActionReferenceKind::Attribute { name, .. } => name == rule_name,
ActionReferenceKind::Qualified { name, .. } => name == rule_name,
ActionReferenceKind::NonLocal { rule, .. } => rule == rule_name,
})
})
}
fn split_optional(candidate: &PlannedAlternative, index: usize) -> Vec<PlannedAlternative> {
let mut present = candidate.elements.clone();
if let Some(element) = present.get_mut(index) {
element.quantifier = Quantifier::One;
}
let mut absent = candidate.elements.clone();
absent.remove(index);
vec![
PlannedAlternative {
label_from: candidate.label_from,
options: candidate.options.clone(),
origin: candidate.origin,
elements: present,
verbatim: false,
},
PlannedAlternative {
label_from: candidate.label_from,
options: candidate.options.clone(),
origin: candidate.origin,
elements: absent,
verbatim: false,
},
]
}
fn splice_satellite(
candidate: &PlannedAlternative,
index: usize,
satellite: &Rule,
) -> Option<Vec<PlannedAlternative>> {
if remaining_actions_reference(&candidate.elements, index, &satellite.name) {
return None;
}
let prefix = &candidate.elements[..index];
let suffix = &candidate.elements[index + 1..];
let mut expansions = Vec::with_capacity(satellite.block.alternatives.len());
for source in &satellite.block.alternatives {
if labels_collide(prefix, suffix, &source.elements) {
return None;
}
if implicit_bindings_collide(prefix, suffix, &source.elements) {
return None;
}
let mut options = candidate.options.clone();
for option in &source.options {
match options
.iter()
.find(|existing| existing.name.value == option.name.value)
{
Some(existing) if existing.value.value != option.value.value => return None,
Some(_) => {}
None => options.push(option.clone()),
}
}
let mut elements = Vec::with_capacity(prefix.len() + source.elements.len() + suffix.len());
elements.extend(prefix.iter().cloned());
elements.extend(source.elements.iter().cloned());
elements.extend(suffix.iter().cloned());
expansions.push(PlannedAlternative {
label_from: candidate.label_from,
options,
origin: source.id,
elements,
verbatim: false,
});
}
Some(expansions)
}
fn labels_collide(prefix: &[Element], suffix: &[Element], spliced: &[Element]) -> bool {
let mut caller = BTreeSet::new();
collect_labels(prefix, &mut caller);
collect_labels(suffix, &mut caller);
let mut satellite = BTreeSet::new();
collect_labels(spliced, &mut satellite);
!caller.is_disjoint(&satellite)
}
fn collect_labels<'a>(elements: &'a [Element], out: &mut BTreeSet<&'a str>) {
for element in elements {
if let Some(label) = &element.label {
out.insert(label.name.as_str());
}
if let ElementKind::Block(nested) = &element.kind {
for alternative in &nested.alternatives {
collect_labels(&alternative.elements, out);
}
}
}
}
fn implicit_bindings_collide(prefix: &[Element], suffix: &[Element], spliced: &[Element]) -> bool {
let mut caller_refs = BTreeSet::new();
action_reference_names(prefix, &mut caller_refs);
action_reference_names(suffix, &mut caller_refs);
let mut satellite_intro = BTreeSet::new();
bindable_names(spliced, &mut satellite_intro);
if !caller_refs.is_disjoint(&satellite_intro) {
return true;
}
let mut satellite_refs = BTreeSet::new();
action_reference_names(spliced, &mut satellite_refs);
let mut caller_intro = BTreeSet::new();
bindable_names(prefix, &mut caller_intro);
bindable_names(suffix, &mut caller_intro);
!satellite_refs.is_disjoint(&caller_intro)
}
fn action_reference_names<'a>(elements: &'a [Element], out: &mut BTreeSet<&'a str>) {
for element in elements {
let mut bodies: Vec<&str> = Vec::new();
match &element.kind {
ElementKind::Action { body, .. } => bodies.push(body),
ElementKind::Predicate { body, fail, .. } => {
bodies.push(body);
if let Some(fail) = fail.as_deref() {
bodies.push(fail);
}
}
ElementKind::Block(nested) => {
for alternative in &nested.alternatives {
action_reference_names(&alternative.elements, out);
}
}
_ => {}
}
for body in bodies {
for reference in action_references(body) {
match reference.kind {
ActionReferenceKind::Attribute { name, .. }
| ActionReferenceKind::Qualified { name, .. } => {
out.insert(name);
}
ActionReferenceKind::NonLocal { .. } => {}
}
}
}
}
}
fn bindable_names<'a>(elements: &'a [Element], out: &mut BTreeSet<&'a str>) {
for element in elements {
if let Some(label) = &element.label {
out.insert(label.name.as_str());
}
match &element.kind {
ElementKind::RuleCall(call) => {
out.insert(call.name.as_str());
}
ElementKind::Terminal(Terminal::Token(token)) => {
out.insert(token.as_str());
}
ElementKind::Block(nested) => {
for alternative in &nested.alternatives {
bindable_names(&alternative.elements, out);
}
}
_ => {}
}
}
}
fn satellite_is_inlinable(satellite: &Rule) -> bool {
satellite.kind == RuleKind::Parser
&& satellite.arguments.is_none()
&& satellite.returns.is_none()
&& satellite.locals.is_none()
&& satellite.throws.is_empty()
&& satellite.actions.is_empty()
&& satellite.catches.is_empty()
&& satellite.finally_action.is_none()
&& satellite.options.is_empty()
&& satellite
.block
.alternatives
.iter()
.all(|alternative| alternative.label.is_none())
&& !satellite_has_embedded_semantics(satellite)
}
fn satellite_has_embedded_semantics(satellite: &Rule) -> bool {
fn elements_have_semantics(elements: &[Element]) -> bool {
elements.iter().any(|element| match &element.kind {
ElementKind::Action { .. } | ElementKind::Predicate { .. } => true,
ElementKind::Block(nested) => nested
.alternatives
.iter()
.any(|alternative| elements_have_semantics(&alternative.elements)),
_ => false,
})
}
satellite
.block
.alternatives
.iter()
.any(|alternative| elements_have_semantics(&alternative.elements))
}
fn planned_hub_is_directly_rewritable(
planned: &[PlannedAlternative],
hub_id: RuleId,
cycle_set: &BTreeSet<RuleId>,
grammar: Grammar<'_>,
) -> bool {
let mut has_primary = false;
let mut has_recursive = false;
for candidate in planned {
let elements = &candidate.elements;
if elements.iter().any(|element| {
hub_call(element, hub_id, grammar).is_some_and(|call| call.arguments.is_some())
}) {
return false;
}
let Some(last_significant) = elements.iter().rposition(|e| !is_epsilon_only(e)) else {
return false;
};
if elements
.first()
.is_some_and(|element| is_hub_call(element, hub_id, grammar))
{
if last_significant == 0 {
return false;
}
if elements[1..]
.iter()
.all(|element| element_nullable(element, grammar.names, grammar.nullable))
{
return false;
}
has_recursive = true;
} else {
let mut corners = BTreeSet::new();
collect_left_corner_calls(elements, grammar, &mut corners);
if corners.iter().any(|corner| cycle_set.contains(corner)) {
return false;
}
has_primary = true;
}
}
has_primary && has_recursive
}
const fn is_epsilon_only(element: &Element) -> bool {
matches!(
element.kind,
ElementKind::Action { .. } | ElementKind::Predicate { .. } | ElementKind::Epsilon
)
}
fn is_hub_call(element: &Element, hub_id: RuleId, grammar: Grammar<'_>) -> bool {
hub_call(element, hub_id, grammar).is_some()
}
fn hub_call<'a>(
element: &'a Element,
hub_id: RuleId,
grammar: Grammar<'_>,
) -> Option<&'a RuleCall> {
match &element.kind {
ElementKind::RuleCall(call)
if element.quantifier == Quantifier::One && grammar.target(call) == Some(hub_id) =>
{
Some(call)
}
_ => None,
}
}
fn apply_plan(
unit: &mut GrammarUnit,
plan: &CyclePlan,
ids: &mut ModelIdAllocator,
provenance: &mut ProvenanceIndex,
) {
let hub_index = unit
.rules
.iter()
.position(|rule| rule.id == plan.hub)
.expect("planned hub exists");
let attributes = collect_alternative_attributes(unit);
let template = unit.rules[hub_index].block.alternatives.first().cloned();
let alternatives = plan
.alternatives
.iter()
.map(|planned| {
let label_source = attributes
.get(&planned.label_from)
.or(template.as_ref())
.expect("hub has at least one alternative");
let id = if planned.verbatim {
planned.origin
} else {
let fresh = ids.alternative();
provenance.record_model(
ModelNodeId::Alternative(fresh),
[Origin::Synthetic {
reason: SyntheticReason::RuleBoundary,
owner: ModelNodeId::Alternative(planned.origin),
}],
);
fresh
};
let elements = if planned.verbatim {
planned.elements.clone()
} else {
renumber_elements(planned.elements.clone(), ids, provenance)
};
Alternative {
id,
elements,
label: label_source.label.clone(),
options: planned.options.clone(),
commands: label_source.commands.clone(),
syntax: label_source.syntax,
span: label_source.span.clone(),
}
})
.collect::<Vec<_>>();
let hub = &mut unit.rules[hub_index];
hub.block = Block {
alternatives,
options: hub.block.options.clone(),
syntax: hub.block.syntax,
span: hub.block.span.clone(),
};
provenance.record_model(
ModelNodeId::Rule(plan.hub),
[Origin::Synthetic {
reason: SyntheticReason::RuleBoundary,
owner: ModelNodeId::Rule(plan.hub),
}],
);
unit.rules.retain(|rule| !plan.removable.contains(&rule.id));
}
fn collect_alternative_attributes(unit: &GrammarUnit) -> BTreeMap<AlternativeId, Alternative> {
let mut index = BTreeMap::new();
for rule in &unit.rules {
collect_block_alternatives(&rule.block, &mut index);
}
index
}
fn collect_block_alternatives(block: &Block, index: &mut BTreeMap<AlternativeId, Alternative>) {
for alternative in &block.alternatives {
index.insert(alternative.id, alternative.clone());
for element in &alternative.elements {
if let ElementKind::Block(nested) = &element.kind {
collect_block_alternatives(nested, index);
}
}
}
}
fn choose_hub(
unit: &GrammarUnit,
cycle: &Cycle,
cycle_set: &BTreeSet<RuleId>,
grammar: Grammar<'_>,
) -> Option<RuleId> {
let external = externally_referenced(unit, cycle_set, grammar.names);
let rules = rules_by_id(unit);
let candidates = cycle
.iter()
.copied()
.filter(|id| {
rules.get(id).is_some_and(|rule| {
rule.block
.alternatives
.iter()
.any(|alternative| !corner_enters_cycle(alternative, cycle_set, grammar))
})
})
.collect::<Vec<_>>();
candidates
.iter()
.copied()
.filter(|id| external.contains(id))
.min()
.or_else(|| candidates.into_iter().min())
}
fn externally_referenced(
unit: &GrammarUnit,
cycle_set: &BTreeSet<RuleId>,
names: &BTreeMap<String, RuleId>,
) -> BTreeSet<RuleId> {
let mut external = BTreeSet::new();
for rule in &unit.rules {
if cycle_set.contains(&rule.id) {
continue;
}
collect_calls_into(&rule.block, names, &mut |target| {
if cycle_set.contains(&target) {
external.insert(target);
}
});
}
external
}
fn removable_satellites(
unit: &GrammarUnit,
cycle: &Cycle,
hub_id: RuleId,
planned: &[PlannedAlternative],
names: &BTreeMap<String, RuleId>,
) -> BTreeSet<RuleId> {
let mut removable = cycle
.iter()
.copied()
.filter(|member| *member != hub_id)
.collect::<BTreeSet<_>>();
loop {
let mut referenced = BTreeSet::new();
{
let mut sink = |target: RuleId| {
if removable.contains(&target) {
referenced.insert(target);
}
};
for candidate in planned {
collect_calls_in_elements(&candidate.elements, names, &mut sink);
}
for rule in &unit.rules {
if removable.contains(&rule.id) || rule.id == hub_id {
continue;
}
collect_calls_into(&rule.block, names, &mut sink);
}
}
if referenced.is_empty() {
return removable;
}
for target in referenced {
removable.remove(&target);
}
}
}
fn substitution_budget(cycle_set: &BTreeSet<RuleId>, rules: &BTreeMap<RuleId, &Rule>) -> usize {
let alternatives: usize = cycle_set
.iter()
.filter_map(|id| rules.get(id))
.map(|rule| rule.block.alternatives.len())
.sum();
alternatives.saturating_mul(alternatives).max(64)
}
fn left_corner_cycles(unit: &GrammarUnit, grammar: Grammar<'_>) -> Vec<Cycle> {
let mut graph = DiGraph::<RuleId, ()>::new();
let nodes = unit
.rules
.iter()
.map(|rule| (rule.id, graph.add_node(rule.id)))
.collect::<BTreeMap<_, _>>();
for rule in &unit.rules {
let mut corners = BTreeSet::new();
for alternative in &rule.block.alternatives {
collect_left_corner_calls(&alternative.elements, grammar, &mut corners);
}
for target in corners {
if let (Some(source), Some(target)) = (nodes.get(&rule.id), nodes.get(&target)) {
graph.add_edge(*source, *target, ());
}
}
}
let mut cycles = tarjan_scc(&graph)
.into_iter()
.filter_map(|component| {
(component.len() > 1).then(|| {
let mut rules = component
.into_iter()
.map(|node| graph[node])
.collect::<Cycle>();
rules.sort_unstable();
rules
})
})
.collect::<Vec<_>>();
cycles.sort();
cycles
}
fn collect_left_corner_calls(
elements: &[Element],
grammar: Grammar<'_>,
result: &mut BTreeSet<RuleId>,
) {
for element in elements {
match &element.kind {
ElementKind::Action { .. } | ElementKind::Predicate { .. } | ElementKind::Epsilon => {}
ElementKind::RuleCall(call) => {
let Some(target) = grammar.target(call) else {
return;
};
result.insert(target);
let skippable = matches!(
element.quantifier,
Quantifier::Optional { .. } | Quantifier::ZeroOrMore { .. }
) || grammar.nullable.contains(&target);
if !skippable {
return;
}
}
ElementKind::Block(block) => {
for nested in &block.alternatives {
collect_left_corner_calls(&nested.elements, grammar, result);
}
let skippable = matches!(
element.quantifier,
Quantifier::Optional { .. } | Quantifier::ZeroOrMore { .. }
) || block_is_nullable(block, grammar.names, grammar.nullable);
if !skippable {
return;
}
}
_ => return,
}
}
}
fn collect_calls_into(
block: &Block,
names: &BTreeMap<String, RuleId>,
sink: &mut impl FnMut(RuleId),
) {
for alternative in &block.alternatives {
collect_calls_in_elements(&alternative.elements, names, sink);
}
}
fn collect_calls_in_elements(
elements: &[Element],
names: &BTreeMap<String, RuleId>,
sink: &mut impl FnMut(RuleId),
) {
for element in elements {
match &element.kind {
ElementKind::RuleCall(call) => {
if let Some(target) = names.get(&call.name) {
sink(*target);
}
}
ElementKind::Block(nested) => collect_calls_into(nested, names, sink),
_ => {}
}
}
}
fn rules_by_id(unit: &GrammarUnit) -> BTreeMap<RuleId, &Rule> {
unit.rules.iter().map(|rule| (rule.id, rule)).collect()
}
fn rule_names(unit: &GrammarUnit) -> BTreeMap<String, RuleId> {
unit.rules
.iter()
.map(|rule| (rule.name.clone(), rule.id))
.collect()
}
fn nullable_rules(unit: &GrammarUnit, names: &BTreeMap<String, RuleId>) -> BTreeSet<RuleId> {
let rules = rules_by_id(unit);
let mut nullable = BTreeSet::new();
loop {
let previous = nullable.len();
for (id, rule) in &rules {
if rule.block.alternatives.iter().any(|alternative| {
alternative
.elements
.iter()
.all(|element| element_nullable(element, names, &nullable))
}) {
nullable.insert(*id);
}
}
if nullable.len() == previous {
return nullable;
}
}
}
fn element_nullable(
element: &Element,
names: &BTreeMap<String, RuleId>,
nullable: &BTreeSet<RuleId>,
) -> bool {
if matches!(
element.quantifier,
Quantifier::Optional { .. } | Quantifier::ZeroOrMore { .. }
) {
return true;
}
match &element.kind {
ElementKind::Epsilon | ElementKind::Action { .. } | ElementKind::Predicate { .. } => true,
ElementKind::RuleCall(call) => names
.get(&call.name)
.is_some_and(|target| nullable.contains(target)),
ElementKind::Block(block) => block_is_nullable(block, names, nullable),
_ => false,
}
}
fn block_is_nullable(
block: &Block,
names: &BTreeMap<String, RuleId>,
nullable: &BTreeSet<RuleId>,
) -> bool {
block.alternatives.iter().any(|alternative| {
alternative
.elements
.iter()
.all(|element| element_nullable(element, names, nullable))
})
}
fn renumber_elements(
elements: Vec<Element>,
ids: &mut ModelIdAllocator,
provenance: &mut ProvenanceIndex,
) -> Vec<Element> {
elements
.into_iter()
.map(|element| renumber_element(element, ids, provenance))
.collect()
}
fn renumber_element(
mut element: Element,
ids: &mut ModelIdAllocator,
provenance: &mut ProvenanceIndex,
) -> Element {
let original = element.id;
element.id = ids.element();
record_clone(
provenance,
ModelNodeId::Element(element.id),
ModelNodeId::Element(original),
);
if let Some(label) = element.label.as_mut() {
let previous = label.id;
label.id = ids.label();
record_clone(
provenance,
ModelNodeId::Label(label.id),
ModelNodeId::Label(previous),
);
}
element.kind = match element.kind {
ElementKind::Block(block) => ElementKind::Block(Block {
alternatives: block
.alternatives
.into_iter()
.map(|mut alternative| {
let previous = alternative.id;
alternative.id = ids.alternative();
record_clone(
provenance,
ModelNodeId::Alternative(alternative.id),
ModelNodeId::Alternative(previous),
);
alternative.elements = renumber_elements(alternative.elements, ids, provenance);
alternative
})
.collect(),
options: block.options,
syntax: block.syntax,
span: block.span,
}),
ElementKind::Action { id, body } => {
let fresh = ids.action();
record_clone(
provenance,
ModelNodeId::Action(fresh),
ModelNodeId::Action(id),
);
ElementKind::Action { id: fresh, body }
}
ElementKind::Predicate {
id,
body,
fail,
precedence,
} => {
let fresh = ids.predicate();
record_clone(
provenance,
ModelNodeId::Predicate(fresh),
ModelNodeId::Predicate(id),
);
ElementKind::Predicate {
id: fresh,
body,
fail,
precedence,
}
}
kind => kind,
};
element
}
fn record_clone(provenance: &mut ProvenanceIndex, fresh: ModelNodeId, original: ModelNodeId) {
let mut origins = provenance.origins(original).to_vec();
origins.push(Origin::Synthetic {
reason: SyntheticReason::BlockBoundary,
owner: original,
});
provenance.record_model(fresh, origins);
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)] mod tests {
use super::*;
use crate::grammar::frontend::{SourceId, parse_source};
use crate::grammar::left_recursion::rewrite_immediate_left_recursion;
use crate::grammar::model::{GrammarId, Terminal};
use crate::grammar::syntax::parse_grammar_unit;
struct Fixture {
unit: GrammarUnit,
ids: ModelIdAllocator,
provenance: ProvenanceIndex,
}
fn parse(text: &str) -> Fixture {
let file = parse_source(SourceId::new(0), "P.g4", text).expect("valid grammar");
let mut ids = ModelIdAllocator::after_loaded_grammars(1);
let mut provenance = ProvenanceIndex::default();
let unit = parse_grammar_unit(&file, GrammarId::new(0), &mut ids, &mut provenance);
Fixture {
unit,
ids,
provenance,
}
}
fn run(text: &str) -> (String, String, bool) {
let mut fixture = parse(text);
let before = render(&fixture.unit);
let changed = eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
);
let after = render(&fixture.unit);
(before, after, changed)
}
fn rewritten(text: &str) -> GrammarUnit {
let mut fixture = parse(text);
assert!(
eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
),
"expected the cycle to be rewritten"
);
fixture.unit
}
fn render(unit: &GrammarUnit) -> String {
let mut out = String::new();
for rule in &unit.rules {
out.push_str(&rule.name);
out.push_str(":\n");
for alternative in &rule.block.alternatives {
out.push_str(" | ");
if let Some(assoc) = alternative
.options
.iter()
.find(|option| option.name.value == "assoc")
{
use std::fmt::Write as _;
let _ = write!(out, "<assoc={}> ", assoc.value.value);
}
out.push_str(&render_elements(&alternative.elements));
if let Some(label) = &alternative.label {
use std::fmt::Write as _;
let _ = write!(out, " #{}", label.value);
}
out.push('\n');
}
}
out
}
fn render_elements(elements: &[Element]) -> String {
elements
.iter()
.map(render_element)
.collect::<Vec<_>>()
.join(" ")
}
fn render_element(element: &Element) -> String {
let quantifier = match element.quantifier {
Quantifier::One => "",
Quantifier::Optional { .. } => "?",
Quantifier::ZeroOrMore { .. } => "*",
Quantifier::OneOrMore { .. } => "+",
};
let body = match &element.kind {
ElementKind::RuleCall(call) => call.name.clone(),
ElementKind::Terminal(Terminal::Literal(text)) => format!("'{text}'"),
ElementKind::Terminal(Terminal::Token(name)) => name.clone(),
ElementKind::Terminal(_) => "<terminal>".to_owned(),
ElementKind::Set { .. } => "<set>".to_owned(),
ElementKind::Block(_) => "<block>".to_owned(),
ElementKind::Range(..) => "<range>".to_owned(),
ElementKind::Action { .. } => "<action>".to_owned(),
ElementKind::Predicate { .. } => "<pred>".to_owned(),
ElementKind::Epsilon => "<eps>".to_owned(),
};
let label = element
.label
.as_ref()
.map(|label| format!("{}=", label.name))
.unwrap_or_default();
format!("{label}{body}{quantifier}")
}
fn rule<'a>(unit: &'a GrammarUnit, name: &str) -> &'a Rule {
unit.rules
.iter()
.find(|rule| rule.name == name)
.unwrap_or_else(|| panic!("rule {name} exists"))
}
fn assert_declined(text: &str) {
let (before, after, changed) = run(text);
assert!(
!changed,
"expected a decline, but the pass reported a change"
);
assert_eq!(
before, after,
"a declined cycle must leave the model untouched"
);
}
#[test]
fn collapses_two_rule_name_cycle_into_the_hub() {
let unit = rewritten(
"parser grammar P; \
name : qualified_name | simple_name ; \
qualified_name : name '.' simple_name ; \
simple_name : ID ;",
);
assert!(
unit.rules.iter().all(|rule| rule.name != "qualified_name"),
"hub-only satellite is removed"
);
insta::assert_snapshot!("name_cycle_collapsed", render(&unit));
}
#[test]
fn collapsed_hub_is_then_rewritten_by_the_direct_pass() {
let mut fixture = parse(
"parser grammar P; \
name : qualified_name | simple_name ; \
qualified_name : name '.' simple_name ; \
simple_name : ID ;",
);
assert!(eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
));
let diagnostics = rewrite_immediate_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
);
assert!(diagnostics.is_empty(), "{diagnostics:?}");
assert!(
rule(&fixture.unit, "name").left_recursion.is_some(),
"collapsed hub is now a direct-left-recursion precedence rule"
);
}
#[test]
fn splits_optional_from_inlined_satellite() {
let unit = rewritten(
"parser grammar P; \
e : e '+' e | r | ID ; \
r : e? '..' e? ;",
);
insta::assert_snapshot!("optional_from_satellite", render(&unit));
}
#[test]
fn expands_consecutive_leading_optionals_to_a_fixpoint() {
let unit = rewritten(
"parser grammar P; \
e : e '+' e | r | ID ; \
r : e? e? '..' ;",
);
insta::assert_snapshot!("consecutive_optionals", render(&unit));
}
#[test]
fn range_operator_hub_is_accepted_by_the_direct_pass() {
let mut fixture = parse(
"parser grammar P; \
e : e '+' e | r | ID ; \
r : e? '..' e? ;",
);
assert!(eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
));
let diagnostics = rewrite_immediate_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
);
assert!(diagnostics.is_empty(), "{diagnostics:?}");
assert!(rule(&fixture.unit, "e").left_recursion.is_some());
}
#[test]
fn retains_satellite_referenced_from_outside_the_cycle() {
let unit = rewritten(
"parser grammar P; \
t : arr | t '?' | ID ; \
arr : t '[' ']' ; \
new_arr : 'new' arr ;",
);
assert!(
unit.rules.iter().any(|rule| rule.name == "arr"),
"externally-referenced satellite is retained"
);
insta::assert_snapshot!("external_satellite_retained", render(&unit));
}
#[test]
fn preserves_satellite_alternative_associativity() {
let unit = rewritten(
"parser grammar P; \
expr : power | ID ; \
power : <assoc=right> expr '^' expr ;",
);
insta::assert_snapshot!("assoc_right_preserved", render(&unit));
}
#[test]
fn chained_alias_splice_carries_operator_options() {
let unit = rewritten(
"parser grammar P; \
e : a | ID ; \
a : <assoc=right> b '^' e ; \
b : e ;",
);
insta::assert_snapshot!("chained_assoc_carried", render(&unit));
}
#[test]
fn declines_conflicting_options_along_a_splice_chain() {
assert_declined(
"parser grammar P; \
e : a | ID ; \
a : <assoc=right> b '^' e ; \
b : <assoc=left> e ;",
);
}
#[test]
fn preserves_declared_alternative_order() {
let unit = rewritten(
"parser grammar P; \
e : s | e '+' e | ID ; \
s : e '*' e ;",
);
insta::assert_snapshot!("declared_order_preserved", render(&unit));
}
#[test]
fn ignores_grammar_without_mutual_recursion() {
let (_, _, changed) = run("parser grammar P; \
e : e '+' t | t ; \
t : ID ;");
assert!(
!changed,
"direct-only left recursion is left for the direct pass"
);
}
#[test]
fn declines_cycle_without_a_token_consuming_operator() {
assert_declined("parser grammar P; a : b ; b : c ; c : a | X ;");
}
#[test]
fn declines_argument_bearing_recursion() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e '+' e[3] ;",
);
}
#[test]
fn declines_argument_bearing_satellite_call() {
assert_declined(
"parser grammar P; \
e : s[3] | ID ; \
s[int x] : e '+' ID ;",
);
}
#[test]
fn declines_quantified_corner() {
assert_declined(
"parser grammar P; \
a : b* 'x' | 'a' ; \
b : a 'b' ;",
);
}
#[test]
fn declines_corner_behind_a_nullable_prefix() {
assert_declined(
"parser grammar P; \
a : n b | 'a' ; \
b : a 'b' ; \
n : ;",
);
}
#[test]
fn declines_labelled_corner() {
assert_declined(
"parser grammar P; \
e : x=s | ID ; \
s : e '+' ID ;",
);
}
#[test]
fn declines_satellite_with_rule_level_action() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s @init { let _x = 1; } : e '+' ID ;",
);
}
#[test]
fn declines_satellite_with_labelled_alternatives() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e '+' ID # Add ;",
);
}
#[test]
fn declines_when_caller_and_satellite_labels_collide() {
assert_declined(
"parser grammar P; \
e : s x=ID | ID ; \
s : e '+' x=ID ;",
);
}
#[test]
fn terminates_and_declines_when_no_corner_is_reducible() {
assert_declined(
"parser grammar P; \
e : (s | ID) | e '+' e ; \
s : e '*' e ;",
);
}
#[test]
fn retains_satellite_still_referenced_by_the_planned_hub() {
let unit = rewritten(
"parser grammar P; \
e : s s | ID ; \
s : e '+' ID ;",
);
assert!(
unit.rules.iter().any(|rule| rule.name == "s"),
"satellite referenced by the planned hub body is retained"
);
insta::assert_snapshot!("suffix_satellite_retained", render(&unit));
}
#[test]
fn retains_satellite_referenced_from_an_unspliced_alternative() {
let unit = rewritten(
"parser grammar P; \
t : arr | t '?' arr | ID ; \
arr : t '[' ']' ;",
);
assert!(
unit.rules.iter().any(|rule| rule.name == "arr"),
"satellite referenced by an unspliced alternative is retained"
);
insta::assert_snapshot!("verbatim_alt_satellite_retained", render(&unit));
}
#[test]
fn preserves_the_caller_alternative_label() {
let unit = rewritten(
"parser grammar P; \
e : s # ViaSatellite | ID # Atom ; \
s : e '+' ID ;",
);
insta::assert_snapshot!("caller_alt_label_preserved", render(&unit));
}
#[test]
fn splitting_an_optional_keeps_the_label_on_both_products() {
let unit = rewritten(
"parser grammar P; \
e : e '+' e # Add | r # Range | ID # Atom ; \
r : e? '..' ;",
);
insta::assert_snapshot!("split_label_on_both_products", render(&unit));
}
#[test]
fn declines_predicate_prefixed_satellite_alternative() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : {true}? e '+' ID ;",
);
}
#[test]
fn declines_nongreedy_optional_corner() {
assert_declined(
"parser grammar P; \
e : r | ID ; \
r : e?? '..' ;",
);
}
#[test]
fn declines_when_a_surviving_action_references_the_satellite() {
assert_declined(
"parser grammar P; \
e : s { let _x = $s.text; } | ID ; \
s : e '+' ID ;",
);
}
#[test]
fn declines_parameterized_hub() {
assert_declined(
"parser grammar P; \
e[i32 x] : s | ID ; \
s : e '+' ID ;",
);
}
#[test]
fn declines_satellite_action_bound_to_its_rule_context() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e '+' ID { let _r = $ctx; } ;",
);
}
#[test]
fn declines_when_a_splice_would_capture_an_implicit_reference() {
assert_declined(
"parser grammar P; \
e : s { let _t = $ID.text; } ID | INT ; \
s : e '+' ID ;",
);
}
#[test]
fn declines_any_satellite_with_embedded_semantics() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e '+' i=ID { let _t = $i.text; } ;",
);
}
#[test]
fn declines_nullable_recursive_tail() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e n ; \
n : ;",
);
}
#[test]
fn declines_when_a_split_absent_branch_is_a_bare_self_loop() {
assert_declined(
"parser grammar P; \
e : s | ID ; \
s : e? ;",
);
}
#[test]
fn leaves_lexer_grammars_untouched() {
let mut fixture = parse(
"lexer grammar L; \
A : B 'a' | 'x' ; \
B : A 'b' ;",
);
let before = render(&fixture.unit);
let changed = eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
);
assert!(!changed, "lexer grammars must not be rewritten");
assert_eq!(before, render(&fixture.unit));
}
#[test]
fn declining_consumes_no_ids_and_writes_no_provenance() {
let mut fixture = parse("parser grammar P; a : b ; b : c ; c : a | X ;");
let ids_before = format!("{:?}", fixture.ids);
let provenance_before = format!("{:?}", fixture.provenance);
assert!(!eliminate_mutual_left_recursion(
std::slice::from_mut(&mut fixture.unit),
&mut fixture.ids,
&mut fixture.provenance,
));
assert_eq!(
ids_before,
format!("{:?}", fixture.ids),
"a declined cycle must not consume model IDs"
);
assert_eq!(
provenance_before,
format!("{:?}", fixture.provenance),
"a declined cycle must not record provenance"
);
}
}