Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/lrama/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/lrama/reporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Expand All @@ -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
Expand Down
165 changes: 165 additions & 0 deletions lib/lrama/reporter/ielr.rb
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions sig/generated/lrama/reporter/ielr.rbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions spec/lrama/option_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"])
Expand Down
94 changes: 94 additions & 0 deletions spec/lrama/reporter/ielr_spec.rb
Original file line number Diff line number Diff line change
@@ -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