diff --git a/lib/lrama/option_parser.rb b/lib/lrama/option_parser.rb index 5a15d59c..023fee64 100644 --- a/lib/lrama/option_parser.rb +++ b/lib/lrama/option_parser.rb @@ -97,6 +97,7 @@ def parse_by_option_parser(argv) o.on_tail ' lookaheads explicitly associate lookahead tokens to items' o.on_tail ' solved describe shift/reduce conflicts solving' o.on_tail ' counterexamples, cex generate conflict counterexamples' + o.on_tail ' ielr show where IELR splits LALR states, with lookahead diffs and split reasons' o.on_tail ' rules list unused rules' o.on_tail ' terms list unused terminals' o.on_tail ' verbose report detailed internal state and analysis results' @@ -141,7 +142,7 @@ def parse_by_option_parser(argv) end ALIASED_REPORTS = { cex: :counterexamples }.freeze #: Hash[Symbol, Symbol] - VALID_REPORTS = %i[states itemsets lookaheads solved counterexamples rules terms verbose].freeze #: Array[Symbol] + VALID_REPORTS = %i[states itemsets lookaheads solved counterexamples ielr rules terms verbose].freeze #: Array[Symbol] # @rbs (Array[String]) -> Hash[Symbol, bool] def validate_report(report) diff --git a/lib/lrama/reporter.rb b/lib/lrama/reporter.rb index ed25cc7f..6a452d4b 100644 --- a/lib/lrama/reporter.rb +++ b/lib/lrama/reporter.rb @@ -3,6 +3,7 @@ require_relative 'reporter/conflicts' require_relative 'reporter/grammar' +require_relative 'reporter/ielr' require_relative 'reporter/precedences' require_relative 'reporter/profile' require_relative 'reporter/rules' @@ -21,6 +22,7 @@ def initialize(**options) @conflicts = Conflicts.new @precedences = Precedences.new @grammar = Grammar.new(**options) + @ielr = Ielr.new(**options) @states = States.new(**options) end @@ -32,6 +34,7 @@ def report(io, states) report_duration(:report_conflicts) { @conflicts.report(io, states) } report_duration(:report_precedences) { @precedences.report(io, states) } report_duration(:report_grammar) { @grammar.report(io, states) } + report_duration(:report_ielr) { @ielr.report(io, states) } report_duration(:report_states) { @states.report(io, states, ielr: states.ielr_defined?) } end end diff --git a/lib/lrama/reporter/ielr.rb b/lib/lrama/reporter/ielr.rb new file mode 100644 index 00000000..d7ed7c0f --- /dev/null +++ b/lib/lrama/reporter/ielr.rb @@ -0,0 +1,165 @@ +# rbs_inline: enabled +# frozen_string_literal: true + +module Lrama + class Reporter + class Ielr + # @rbs (?ielr: bool, **bool _) -> void + def initialize(ielr: false, **_) + @enabled = ielr + end + + # @rbs (IO io, Lrama::States states) -> void + def report(io, states) + return unless @enabled && states.ielr_defined? + + groups = split_groups(states) + return if groups.empty? + + @incoming_index = build_incoming_index(states) + + io << "IELR State Splits\n\n" + + groups.each do |core| + report_group(io, core) + end + ensure + @incoming_index = nil + end + + private + + # @rbs (Lrama::States states) -> Array[Lrama::State] + def split_groups(states) + states.states.select do |state| + !state.split_state? && state.ielr_isocores.size > 1 + end + end + + # @rbs (Lrama::States states) -> Hash[Lrama::State, Array[Lrama::State::Action::Shift | Lrama::State::Action::Goto]] + def build_incoming_index(states) + index = Hash.new { |h, k| h[k] = [] } #: Hash[Lrama::State, Array[Lrama::State::Action::Shift | Lrama::State::Action::Goto]] + states.states.each do |state| + state.transitions.each do |transition| + index[transition.to_state] << transition + end + end + index + end + + # @rbs (IO io, Lrama::State core) -> void + def report_group(io, core) + variants = core.ielr_isocores.sort_by(&:id) + + io << " LALR state #{core.id} splits into IELR states #{variants.map(&:id).join(', ')}\n\n" + report_incoming_transitions(io, variants) + report_lookahead_differences(io, variants) + report_split_reasons(io, variants) + io << "\n" + end + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_incoming_transitions(io, variants) + io << " Incoming transitions\n" + + variants.each do |variant| + @incoming_index[variant] + .sort_by { |t| [t.from_state.id, t.next_sym.number] } + .each do |transition| + io << " state #{transition.from_state.id} -- #{transition.next_sym.display_name} --> state #{variant.id} #{state_role(variant)}\n" + end + end + + io << "\n" + end + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_lookahead_differences(io, variants) + differing_items = variants.first.kernels.select do |item| + variants.map { |state| lookahead_signature(state.item_lookahead_set[item]) }.uniq.size > 1 + end + return if differing_items.empty? + + io << " Lookahead differences\n" + + differing_items.each do |item| + io << " #{item.display_name}\n" + + variants.each do |state| + io << " state #{state.id} #{state_role(state)}: #{format_lookaheads(state.item_lookahead_set[item])}\n" + end + end + + io << "\n" + end + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_split_reasons(io, variants) + core = variants.first.lalr_isocore + different_annotations = [] #: Array[[Lrama::State::InadequacyAnnotation, Hash[Lrama::State, String]]] + + core.annotation_list.each do |annotation| + labels_by_state = {} #: Hash[Lrama::State, String] + variants.each do |state| + labels_by_state[state] = dominant_actions(state, annotation) + end + next if labels_by_state.values.uniq.size <= 1 + + different_annotations << [annotation, labels_by_state] + end + return if different_annotations.empty? + + io << " Why it split\n" + + different_annotations.each do |annotation, labels_by_state| + io << " token #{annotation.token.display_name}\n" + + variants.each do |state| + io << " state #{state.id} #{state_role(state)}: #{labels_by_state[state]}\n" + end + end + + io << "\n" + end + + # @rbs (Array[Lrama::Grammar::Symbol] syms) -> Array[Integer] + def lookahead_signature(syms) + syms.map(&:number).sort + end + + # @rbs (Array[Lrama::Grammar::Symbol] syms) -> String + def format_lookaheads(syms) + values = syms.sort_by(&:number).map(&:display_name) + "[#{values.join(', ')}]" + end + + # @rbs (Lrama::State state, Lrama::State::InadequacyAnnotation annotation) -> String + def dominant_actions(state, annotation) + actions = annotation.dominant_contribution(state.item_lookahead_set) + return "no dominant action" if actions.nil? || actions.empty? + + actions.map { |action| format_action(state, action) }.join(", ") + end + + # @rbs (Lrama::State state, Lrama::State::Action::Shift | Lrama::State::Action::Reduce action) -> String + def format_action(state, action) + case action + when Lrama::State::Action::Shift + current_shift = state.term_transitions.find { |shift| shift.next_sym == action.next_sym } + destination = current_shift ? current_shift.to_state.id : action.to_state.id + "shift and go to state #{destination}" + when Lrama::State::Action::Reduce + rule = action.item.rule + "reduce using rule #{rule.id} (#{rule.lhs.display_name})" + else + raise "Unsupported action #{action.class}" + end + end + + # @rbs (Lrama::State state) -> String + def state_role(state) + state.split_state? ? "[IELR split]" : "[LALR core]" + end + end + end +end diff --git a/sig/generated/lrama/reporter/ielr.rbs b/sig/generated/lrama/reporter/ielr.rbs new file mode 100644 index 00000000..e675a4eb --- /dev/null +++ b/sig/generated/lrama/reporter/ielr.rbs @@ -0,0 +1,48 @@ +# Generated from lib/lrama/reporter/ielr.rb with RBS::Inline + +module Lrama + class Reporter + class Ielr + # @rbs (?ielr: bool, **bool _) -> void + def initialize: (?ielr: bool, **bool _) -> void + + # @rbs (IO io, Lrama::States states) -> void + def report: (IO io, Lrama::States states) -> void + + private + + # @rbs (Lrama::States states) -> Array[Lrama::State] + def split_groups: (Lrama::States states) -> Array[Lrama::State] + + # @rbs (Lrama::States states) -> Hash[Lrama::State, Array[Lrama::State::Action::Shift | Lrama::State::Action::Goto]] + def build_incoming_index: (Lrama::States states) -> Hash[Lrama::State, Array[Lrama::State::Action::Shift | Lrama::State::Action::Goto]] + + # @rbs (IO io, Lrama::State core) -> void + def report_group: (IO io, Lrama::State core) -> void + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_incoming_transitions: (IO io, Array[Lrama::State] variants) -> void + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_lookahead_differences: (IO io, Array[Lrama::State] variants) -> void + + # @rbs (IO io, Array[Lrama::State] variants) -> void + def report_split_reasons: (IO io, Array[Lrama::State] variants) -> void + + # @rbs (Array[Lrama::Grammar::Symbol] syms) -> Array[Integer] + def lookahead_signature: (Array[Lrama::Grammar::Symbol] syms) -> Array[Integer] + + # @rbs (Array[Lrama::Grammar::Symbol] syms) -> String + def format_lookaheads: (Array[Lrama::Grammar::Symbol] syms) -> String + + # @rbs (Lrama::State state, Lrama::State::InadequacyAnnotation annotation) -> String + def dominant_actions: (Lrama::State state, Lrama::State::InadequacyAnnotation annotation) -> String + + # @rbs (Lrama::State state, Lrama::State::Action::Shift | Lrama::State::Action::Reduce action) -> String + def format_action: (Lrama::State state, Lrama::State::Action::Shift | Lrama::State::Action::Reduce action) -> String + + # @rbs (Lrama::State state) -> String + def state_role: (Lrama::State state) -> String + end + end +end diff --git a/spec/lrama/option_parser_spec.rb b/spec/lrama/option_parser_spec.rb index 7675c4da..9c324588 100644 --- a/spec/lrama/option_parser_spec.rb +++ b/spec/lrama/option_parser_spec.rb @@ -80,6 +80,7 @@ lookaheads explicitly associate lookahead tokens to items solved describe shift/reduce conflicts solving counterexamples, cex generate conflict counterexamples + ielr show where IELR splits LALR states, with lookahead diffs and split reasons rules list unused rules terms list unused terminals verbose report detailed internal state and analysis results @@ -134,11 +135,19 @@ expect(opts).to eq({ grammar: true, states: true, itemsets: true, lookaheads: true, solved: true, counterexamples: true, + ielr: true, rules: true, terms: true, verbose: true }) end end + context "when ielr is passed" do + it "returns option hash ielr flag enabled" do + opts = option_parser.send(:validate_report, ["ielr"]) + expect(opts).to eq({grammar: true, ielr: true}) + end + end + context "when none is passed" do it "returns empty option hash" do opts = option_parser.send(:validate_report, ["none"]) diff --git a/spec/lrama/reporter/ielr_spec.rb b/spec/lrama/reporter/ielr_spec.rb new file mode 100644 index 00000000..5b81875e --- /dev/null +++ b/spec/lrama/reporter/ielr_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.describe Lrama::Reporter::Ielr do + describe "#report" do + it "reports LALR vs IELR split diagnostics" do + y = <<~INPUT + %{ + // Prologue + %} + + %define lr.type ielr + + %token a + %token b + %token c + + %% + + S: S2 + ; + + S2: a A1 a + | a A2 b + | b A1 b + | b A2 a + ; + + A1: c + ; + + A2: c + ; + + %% + INPUT + + grammar = Lrama::Parser.new(y, "ielr_diff.y").parse + grammar.prepare + grammar.validate! + states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new)) + states.compute + states.compute_ielr + + io = StringIO.new + described_class.new(ielr: true).report(io, states) + + expect(io.string).to eq(<<~STR) + IELR State Splits + + LALR state 5 splits into IELR states 5, 15 + + Incoming transitions + state 1 -- c --> state 5 [LALR core] + state 2 -- c --> state 15 [IELR split] + + Lookahead differences + c • (rule 6) + state 5 [LALR core]: [a] + state 15 [IELR split]: [b] + c • (rule 7) + state 5 [LALR core]: [b] + state 15 [IELR split]: [a] + + Why it split + token a + state 5 [LALR core]: reduce using rule 6 (A1) + state 15 [IELR split]: reduce using rule 7 (A2) + token b + state 5 [LALR core]: reduce using rule 7 (A2) + state 15 [IELR split]: reduce using rule 6 (A1) + + + STR + end + + it "reports shift destinations from the current split state" do + grammar = Lrama::Parser.new(File.read(fixture_path("integration/ielr.y")), fixture_path("integration/ielr.y")).parse + grammar.prepare + grammar.validate! + states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new)) + states.compute + states.compute_ielr + + io = StringIO.new + described_class.new(ielr: true).report(io, states) + + expect(io.string).to include("state 19 [IELR split]: shift and go to state 8") + expect(io.string).to include("state 20 [IELR split]: shift and go to state 8") + expect(io.string).to include("state 21 [IELR split]: shift and go to state 17") + expect(io.string).not_to include("state 19 [IELR split]: shift and go to state 17") + expect(io.string).not_to include("state 20 [IELR split]: shift and go to state 17") + end + end +end