use crate::collections::{Set, set};
use crate::grammar::repr::*;
use crate::lr1::core::*;
use crate::lr1::example::{Example, ExampleStyles, ExampleSymbol};
use crate::lr1::first::FirstSets;
use crate::lr1::lookahead::{Token, TokenSet};
use crate::lr1::trace::Tracer;
use crate::message::Message;
use crate::message::builder::{BodyCharacter, Builder, Character, MessageBuilder};
use crate::tls::Tls;
use itertools::Itertools;
#[cfg(test)]
mod test;
pub fn report_error<E>(
grammar: &Grammar,
error: &Lr1TableConstructionError<'_>,
reporter: impl FnMut(Message) -> Result<(), E>,
) -> Result<(), E> {
let mut cx = ErrorReportingCx::new(grammar, &error.states, &error.conflicts);
cx.report_errors(reporter)
}
struct ErrorReportingCx<'cx, 'grammar> {
grammar: &'grammar Grammar,
first_sets: FirstSets,
states: &'cx [Lr1State<'grammar>],
conflicts: &'cx [Lr1Conflict<'grammar>],
}
#[derive(Debug)]
enum ConflictClassification {
Ambiguity { action: Example, reduce: Example },
Precedence {
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
},
SuggestInline {
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
},
SuggestQuestion {
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
symbol: Symbol,
},
AmbiguousReduction {
reduce: Example,
span1: Span,
span2: Span,
},
InsufficientLookahead { action: Example, reduce: Example },
Naive,
}
type TokenConflict<'grammar> = Conflict<'grammar, Token>;
impl<'cx, 'grammar> ErrorReportingCx<'cx, 'grammar> {
fn new(
grammar: &'grammar Grammar,
states: &'cx [Lr1State<'grammar>],
conflicts: &'cx [Lr1Conflict<'grammar>],
) -> Self {
ErrorReportingCx {
grammar,
first_sets: FirstSets::new(grammar),
states,
conflicts,
}
}
fn report_errors<E>(
&mut self,
mut reporter: impl FnMut(Message) -> Result<(), E>,
) -> Result<(), E> {
for conflict_group in &token_conflicts(self.conflicts) {
let (mut naive_conflicts, better_conflicts): (Vec<_>, Vec<_>) = conflict_group
.iter()
.map(|c| (c, self.classify(c)))
.partition(|c| matches!(c.1, ConflictClassification::Naive));
let conflicts = if better_conflicts.is_empty() {
if matches!(
naive_conflicts.first().map(|c| &c.0.action),
Some(&Action::Reduce(_))
) {
naive_conflicts.truncate(1);
}
naive_conflicts
} else {
better_conflicts
};
for (conflict, conflict_class) in conflicts {
reporter(self.report_error(conflict, conflict_class))?
}
}
Ok(())
}
fn report_error(
&mut self,
conflict: &TokenConflict<'grammar>,
conflict_class: ConflictClassification,
) -> Message {
match conflict_class {
ConflictClassification::Ambiguity { action, reduce } => {
self.report_error_ambiguity(conflict, action, reduce)
}
ConflictClassification::Precedence {
shift,
reduce,
nonterminal,
} => self.report_error_precedence(conflict, shift, reduce, nonterminal),
ConflictClassification::SuggestInline {
shift,
reduce,
nonterminal,
} => self.report_error_suggest_inline(conflict, shift, reduce, nonterminal),
ConflictClassification::SuggestQuestion {
shift,
reduce,
nonterminal,
symbol,
} => self.report_error_suggest_question(conflict, shift, reduce, nonterminal, symbol),
ConflictClassification::AmbiguousReduction {
reduce,
span1,
span2,
} => self.report_error_ambiguous_reduction(reduce, span1, span2),
ConflictClassification::InsufficientLookahead { action, reduce } => {
self.report_error_insufficient_lookahead(conflict, action, reduce)
}
ConflictClassification::Naive => self.report_error_naive(conflict),
}
}
fn report_error_ambiguity_core(
&self,
conflict: &TokenConflict<'grammar>,
shift: Example,
reduce: Example,
) -> Builder<BodyCharacter> {
let styles = ExampleStyles::ambig();
MessageBuilder::new(conflict.production.span)
.heading()
.text("Ambiguous grammar detected")
.end()
.body()
.begin_lines()
.wrap_text("The following symbols can be reduced in two ways:")
.push(reduce.to_symbol_list(reduce.symbols.len(), styles))
.end()
.begin_lines()
.wrap_text("They could be reduced like so:")
.push(reduce.into_picture(styles))
.end()
.begin_lines()
.wrap_text("Alternatively, they could be reduced like so:")
.push(shift.into_picture(styles))
.end()
}
fn report_error_ambiguity(
&self,
conflict: &TokenConflict<'grammar>,
shift: Example,
reduce: Example,
) -> Message {
self.report_error_ambiguity_core(conflict, shift, reduce)
.wrap_text(
"LALRPOP does not yet support ambiguous grammars. \
See the LALRPOP manual for advice on \
making your grammar unambiguous.",
)
.end()
.end()
}
fn report_error_precedence(
&self,
conflict: &TokenConflict<'grammar>,
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
) -> Message {
self.report_error_ambiguity_core(conflict, shift, reduce)
.begin_wrap()
.text("Hint:")
.styled(Tls::session().hint_text)
.text("This looks like a precedence error related to")
.push(nonterminal)
.verbatimed()
.punctuated(".")
.text("See the LALRPOP manual for advice on encoding precedence.")
.end()
.end()
.end()
}
fn report_error_not_lr1_core(
&self,
conflict: &TokenConflict<'grammar>,
action: Example,
reduce: Example,
) -> Builder<BodyCharacter> {
let styles = ExampleStyles::new();
let builder = MessageBuilder::new(conflict.production.span)
.heading()
.text("Local ambiguity detected")
.end()
.body();
let builder = builder
.begin_lines()
.begin_wrap()
.text("The problem arises after having observed the following symbols")
.text("in the input:")
.end()
.push(if action.cursor >= reduce.cursor {
action.to_symbol_list(action.cursor, styles)
} else {
reduce.to_symbol_list(reduce.cursor, styles)
})
.begin_wrap();
let builder = match conflict.lookahead {
Token::Terminal(ref term) => builder
.text("At that point, if the next token is a")
.push(term.clone())
.verbatimed()
.styled(Tls::session().cursor_symbol)
.punctuated(","),
Token::Error => builder.text("If an error has been found,"),
Token::Eof => builder.text("If the end of the input is reached,"),
};
let builder = builder
.text("then the parser can proceed in two different ways.")
.end()
.end();
let builder = self.describe_reduce(builder, styles, conflict.production, reduce, "First");
match conflict.action {
Action::Shift(ref lookahead, _) => {
self.describe_shift(builder, styles, lookahead.clone(), action, "Alternatively")
}
Action::Reduce(production) => {
self.describe_reduce(builder, styles, production, action, "Alternatively")
}
}
}
fn describe_shift<C: Character>(
&self,
builder: Builder<C>,
styles: ExampleStyles,
lookahead: TerminalString,
example: Example,
intro_word: &str,
) -> Builder<C> {
let nt1 = example.reductions[0].nonterminal.clone();
builder
.begin_lines()
.begin_wrap()
.text(intro_word)
.punctuated(",")
.text("the parser could shift the")
.push(lookahead)
.verbatimed()
.text("token and later use it to construct a")
.push(nt1)
.verbatimed()
.punctuated(".")
.text("This might then yield a parse tree like")
.end()
.push(example.into_picture(styles))
.end()
}
fn describe_reduce<C: Character>(
&self,
builder: Builder<C>,
styles: ExampleStyles,
production: &Production,
example: Example,
intro_word: &str,
) -> Builder<C> {
builder
.begin_lines()
.begin_wrap()
.text(intro_word)
.punctuated(",")
.text("the parser could execute the production at")
.push(production.span)
.punctuated(",")
.text("which would consume the top")
.text(production.symbols.len())
.text("token(s) from the stack")
.text("and produce a")
.push(production.nonterminal.clone())
.verbatimed()
.punctuated(".")
.text("This might then yield a parse tree like")
.end()
.push(example.into_picture(styles))
.end()
}
fn report_error_suggest_inline(
&self,
conflict: &TokenConflict<'grammar>,
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
) -> Message {
let builder = self.report_error_not_lr1_core(conflict, shift, reduce);
builder
.begin_wrap()
.text("Hint:")
.styled(Tls::session().hint_text)
.text("It appears you could resolve this problem by adding")
.text("the attribute `#[inline]` to the definition of")
.push(nonterminal)
.verbatimed()
.punctuated(".")
.text("For more information, see the section on inlining")
.text("in the LALRPOP manual.")
.end()
.end()
.end()
}
fn report_error_suggest_question(
&self,
conflict: &TokenConflict<'grammar>,
shift: Example,
reduce: Example,
nonterminal: NonterminalString,
symbol: Symbol,
) -> Message {
let builder = self.report_error_not_lr1_core(conflict, shift, reduce);
builder
.begin_wrap()
.text("Hint:")
.styled(Tls::session().hint_text)
.text("It appears you could resolve this problem by replacing")
.text("uses of")
.push(nonterminal.clone())
.verbatimed()
.text("with")
.text(symbol) .adjacent_text("`", "?`")
.text(
"(or, alternatively, by adding the attribute `#[inline]` \
to the definition of",
)
.push(nonterminal)
.punctuated(").")
.text("For more information, see the section on inlining")
.text("in the LALRPOP manual.")
.end()
.end()
.end()
}
fn report_error_ambiguous_reduction(
&self,
reduce: Example,
span1: Span,
span2: Span,
) -> Message {
let file_text = Tls::file_text();
let styles = ExampleStyles::new();
let span1_str = file_text.span_text(span1);
let span2_str = file_text.span_text(span2);
let span1_line = file_text.line_col(span1.0).0 + 1;
let span2_line = file_text.line_col(span2.0).0 + 1;
MessageBuilder::new(span1)
.heading()
.text("Multiple productions for the same reduction")
.end()
.body()
.begin_lines()
.wrap_text(format!(
"The following symbols can be reduced into a {} in two ways",
reduce.reductions.first().unwrap().nonterminal
))
.push(reduce.to_symbol_list(reduce.symbols.len(), styles))
.wrap_text(format!(
"They could be reduced using the production on line {span1_line}:"
))
.wrap_text(span1_str)
.wrap_text(format!("...or using the production on line {span2_line}:"))
.wrap_text(span2_str)
.end()
.end()
.end()
}
fn report_error_insufficient_lookahead(
&self,
conflict: &TokenConflict<'grammar>,
action: Example,
reduce: Example,
) -> Message {
let builder = self.report_error_not_lr1_core(conflict, action, reduce);
builder
.wrap_text(
"See the LALRPOP manual for advice on \
making your grammar LR(1).",
)
.end()
.end()
}
fn report_error_naive(&self, conflict: &TokenConflict<'grammar>) -> Message {
let mut builder = MessageBuilder::new(conflict.production.span)
.heading()
.text("Conflict detected")
.end()
.body()
.begin_lines()
.wrap_text("when in this state:")
.indented();
for item in self.states[conflict.state.0].items.vec.iter() {
builder = builder.text(format!("{item:?}"));
}
let mut builder = builder
.end()
.begin_wrap()
.text(format!("and looking at a token `{:?}`", conflict.lookahead))
.text("we can reduce to a")
.push(conflict.production.nonterminal.clone())
.verbatimed();
builder = match conflict.action {
Action::Shift(..) => builder.text("but we can also shift"),
Action::Reduce(prod) => builder
.text("but we can also reduce to a")
.text(prod.nonterminal.clone())
.verbatimed(),
};
builder.end().end().end()
}
fn classify(&mut self, conflict: &TokenConflict<'grammar>) -> ConflictClassification {
let mut action_examples = match conflict.action {
Action::Shift(..) => self.shift_examples(conflict),
Action::Reduce(production) => {
self.reduce_examples(conflict.state, production, conflict.lookahead.clone())
}
};
let mut reduce_examples = self.reduce_examples(
conflict.state,
conflict.production,
conflict.lookahead.clone(),
);
action_examples.sort_by(|e, f| e.symbols.len().cmp(&f.symbols.len()));
reduce_examples.sort_by(|e, f| e.symbols.len().cmp(&f.symbols.len()));
if action_examples.is_empty() || reduce_examples.is_empty() {
return ConflictClassification::Naive;
}
if let Some(classification) =
self.try_classify_ambiguity(conflict, &action_examples, &reduce_examples)
{
return classification;
}
if let Some(classification) =
self.try_classify_question(conflict, &action_examples, &reduce_examples)
{
return classification;
}
if let Some(classification) =
self.try_classify_inline(conflict, &action_examples, &reduce_examples)
{
return classification;
}
action_examples
.into_iter()
.zip(reduce_examples)
.next()
.map(
|(action, reduce)| ConflictClassification::InsufficientLookahead { action, reduce },
)
.unwrap_or(ConflictClassification::Naive)
}
fn try_classify_ambiguity(
&self,
conflict: &TokenConflict<'grammar>,
action_examples: &[Example],
reduce_examples: &[Example],
) -> Option<ConflictClassification> {
action_examples
.iter()
.cartesian_product(reduce_examples)
.filter(|&(action, reduce)| action.symbols == reduce.symbols)
.filter(|&(action, reduce)| action.cursor == reduce.cursor)
.map(|(action, reduce)| {
if let Action::Shift(ref term, _) = conflict.action {
let nt = &conflict.production.nonterminal;
if conflict.production.symbols.len() == 3
&& conflict.production.symbols[0] == Symbol::Nonterminal(nt.clone())
&& conflict.production.symbols[1] == Symbol::Terminal(term.clone())
&& conflict.production.symbols[2] == Symbol::Nonterminal(nt.clone())
{
return ConflictClassification::Precedence {
shift: action.clone(),
reduce: reduce.clone(),
nonterminal: nt.clone(),
};
}
} else if let Action::Reduce(prod) = conflict.action {
if (action.reductions.first().map(|r| &r.nonterminal)
== reduce.reductions.first().map(|r| &r.nonterminal))
{
return ConflictClassification::AmbiguousReduction {
reduce: action.clone(),
span1: conflict.production.span,
span2: prod.span,
};
}
}
ConflictClassification::Ambiguity {
action: action.clone(),
reduce: reduce.clone(),
}
})
.next()
}
fn try_classify_question(
&self,
conflict: &TokenConflict<'grammar>,
action_examples: &[Example],
reduce_examples: &[Example],
) -> Option<ConflictClassification> {
if let Action::Reduce(_) = conflict.action {
return None;
}
debug!(
"try_classify_question: action_examples={:?}",
action_examples
);
debug!(
"try_classify_question: reduce_examples={:?}",
reduce_examples
);
let nt = &conflict.production.nonterminal;
let nt_productions = self.grammar.productions_for(nt);
if nt_productions.len() == 2 {
for &(i, j) in &[(0, 1), (1, 0)] {
if nt_productions[i].symbols.is_empty() && nt_productions[j].symbols.len() == 1 {
return Some(ConflictClassification::SuggestQuestion {
shift: action_examples[0].clone(),
reduce: reduce_examples[0].clone(),
nonterminal: nt.clone(),
symbol: nt_productions[j].symbols[0].clone(),
});
}
}
}
None
}
fn try_classify_inline(
&self,
conflict: &TokenConflict<'grammar>,
action_examples: &[Example],
reduce_examples: &[Example],
) -> Option<ConflictClassification> {
if let Action::Reduce(_) = conflict.action {
return None;
}
action_examples
.iter()
.cartesian_product(reduce_examples)
.filter_map(|(shift, reduce)| {
if self.try_classify_inline_example(shift, reduce) {
let nt = &reduce.reductions[0].nonterminal;
Some(ConflictClassification::SuggestInline {
shift: shift.clone(),
reduce: reduce.clone(),
nonterminal: nt.clone(),
})
} else {
None
}
})
.next()
}
fn try_classify_inline_example(&self, shift: &Example, reduce: &Example) -> bool {
debug!("try_classify_inline_example({:?}, {:?})", shift, reduce);
let shift_upcoming = &shift.symbols[shift.cursor + 1..shift.reductions[0].end];
debug!(
"try_classify_inline_example: shift_upcoming={:?}",
shift_upcoming
);
let r0_end = reduce.reductions[0].end;
let i = reduce.reductions.iter().position(|r| r.end != r0_end);
let i = match i {
Some(v) => v,
None => return false,
};
let ri = &reduce.reductions[i];
let reduce_upcoming = &reduce.symbols[r0_end..ri.end];
debug!(
"try_classify_inline_example: reduce_upcoming={:?} i={:?}",
reduce_upcoming, i
);
if i != 1 {
return false;
}
let mut duplicates = set();
if reduce.reductions[0..=i]
.iter()
.any(|r| !duplicates.insert(r.nonterminal.clone()))
{
return false;
}
shift_upcoming
.iter()
.zip(reduce_upcoming)
.find_map(|(shift_sym, reduce_sym)| match (shift_sym, reduce_sym) {
(ExampleSymbol::Symbol(shift_sym), ExampleSymbol::Symbol(reduce_sym)) => {
if shift_sym == reduce_sym {
None
} else {
let shift_first = self.first_sets.first0(std::iter::once(shift_sym));
let reduce_first = self.first_sets.first0(std::iter::once(reduce_sym));
Some(shift_first.is_disjoint(&reduce_first))
}
}
_ => {
Some(false)
}
})
.unwrap_or(false)
}
fn shift_examples(&self, conflict: &TokenConflict<'grammar>) -> Vec<Example> {
log!(Tls::session(), Verbose, "Gathering shift examples");
let state = &self.states[conflict.state.0];
let conflicting_items = self.conflicting_shift_items(state, conflict);
conflicting_items
.into_iter()
.flat_map(|item| {
let tracer = Tracer::new(&self.first_sets, self.states);
let shift_trace = tracer.backtrace_shift(conflict.state, item);
let local_examples: Vec<Example> = shift_trace.lr0_examples(item).collect();
local_examples
})
.collect()
}
fn reduce_examples(
&self,
state: StateIndex,
production: &'grammar Production,
lookahead: Token,
) -> Vec<Example> {
log!(Tls::session(), Verbose, "Gathering reduce examples");
let item = Item {
production,
index: production.symbols.len(),
lookahead: TokenSet::from(lookahead),
};
let tracer = Tracer::new(&self.first_sets, self.states);
let reduce_trace = tracer.backtrace_reduce(state, item.to_lr0());
reduce_trace.lr1_examples(&self.first_sets, &item).collect()
}
fn conflicting_shift_items(
&self,
state: &Lr1State<'grammar>,
conflict: &TokenConflict<'grammar>,
) -> Set<Lr0Item<'grammar>> {
let lookahead = Symbol::Terminal(conflict.lookahead.unwrap_terminal().clone());
state
.items
.vec
.iter()
.filter(|i| i.can_shift())
.filter(|i| i.production.symbols[i.index] == lookahead)
.map(Item::to_lr0)
.collect()
}
}
fn token_conflicts<'grammar>(
conflicts: &[Conflict<'grammar, TokenSet>],
) -> Vec<Vec<TokenConflict<'grammar>>> {
conflicts
.iter()
.map(|conflict| {
conflict
.lookahead
.iter()
.map(move |token| Conflict {
state: conflict.state,
lookahead: token,
production: conflict.production,
action: conflict.action.clone(),
})
.collect()
})
.collect()
}