diff --git a/lib/lrama/command.rb b/lib/lrama/command.rb index 17aad1a1..c1a8e1b7 100644 --- a/lib/lrama/command.rb +++ b/lib/lrama/command.rb @@ -10,7 +10,7 @@ def initialize(argv) @options = OptionParser.parse(argv) @tracer = Tracer.new(STDERR, **@options.trace_opts) @reporter = Reporter.new(**@options.report_opts) - @warnings = Warnings.new(@logger, @options.warnings) + @warnings = Warnings.new(@logger, @options.warnings, @options.warning_opts || {}) rescue => e abort format_error_message(e.message) end diff --git a/lib/lrama/counterexamples.rb b/lib/lrama/counterexamples.rb index 60d830d0..799b1544 100644 --- a/lib/lrama/counterexamples.rb +++ b/lib/lrama/counterexamples.rb @@ -59,14 +59,7 @@ def compute(conflict_state) # to avoid one of example's path to be nil. next if @exceed_cumulative_time_limit - case conflict.type - when :shift_reduce - # @type var conflict: State::ShiftReduceConflict - shift_reduce_example(conflict_state, conflict) - when :reduce_reduce - # @type var conflict: State::ReduceReduceConflict - reduce_reduce_examples(conflict_state, conflict) - end + conflict_examples(conflict_state, conflict) rescue Timeout::Error => e STDERR.puts "Counterexamples calculation for state #{conflict_state.id} #{e.message} with #{@iterate_count} iteration" increment_total_duration(PathSearchTimeLimit) @@ -76,6 +69,39 @@ def compute(conflict_state) private + # @rbs (State conflict_state, State::conflict conflict) -> Array[Example] + def conflict_examples(conflict_state, conflict) + examples = conflict.symbols.map do |conflict_symbol| + case conflict.type + when :shift_reduce + # @type var conflict: State::ShiftReduceConflict + shift_reduce_example(conflict_state, conflict, conflict_symbol) + when :reduce_reduce + # @type var conflict: State::ReduceReduceConflict + reduce_reduce_example(conflict_state, conflict, conflict_symbol) + end + end + + merge_examples(examples) + end + + # @rbs (Array[Example]) -> Array[Example] + def merge_examples(examples) + merged = {} #: Hash[Array[untyped], Example] + + examples.each do |example| + key = example.merge_key + + if merged[key] + merged[key].merge_conflict_symbols!(example.conflict_symbols) + else + merged[key] = example + end + end + + merged.values + end + # @rbs (State state, State::Item item) -> StateItem def get_state_item(state, item) @state_items[[state, item]] @@ -176,9 +202,8 @@ def get_triple(state_item, precise_lookahead_set) @triples[key] ||= Triple.new(state_item, precise_lookahead_set) end - # @rbs (State conflict_state, State::ShiftReduceConflict conflict) -> Example - def shift_reduce_example(conflict_state, conflict) - conflict_symbol = conflict.symbols.first + # @rbs (State conflict_state, State::ShiftReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example + def shift_reduce_example(conflict_state, conflict, conflict_symbol) # @type var shift_conflict_item: ::Lrama::State::Item shift_conflict_item = conflict_state.items.find { |item| item.next_sym == conflict_symbol } path2 = with_timeout("#shortest_path:") do @@ -191,9 +216,8 @@ def shift_reduce_example(conflict_state, conflict) Example.new(path1, path2, conflict, conflict_symbol, self) end - # @rbs (State conflict_state, State::ReduceReduceConflict conflict) -> Example - def reduce_reduce_examples(conflict_state, conflict) - conflict_symbol = conflict.symbols.first + # @rbs (State conflict_state, State::ReduceReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example + def reduce_reduce_example(conflict_state, conflict, conflict_symbol) path1 = with_timeout("#shortest_path:") do shortest_path(conflict_state, conflict.reduce1.item, conflict_symbol) end diff --git a/lib/lrama/counterexamples/derivation.rb b/lib/lrama/counterexamples/derivation.rb index a2b74767..e3d4133a 100644 --- a/lib/lrama/counterexamples/derivation.rb +++ b/lib/lrama/counterexamples/derivation.rb @@ -36,6 +36,11 @@ def render_for_report render_strings_for_report.join("\n") end + # @rbs (?Derivation derivation) -> Array[String] + def render_symbols_for_example(derivation = self) + _render_symbols_for_example(derivation) + end + private # @rbs (Derivation derivation, Integer offset, Array[String] strings, Integer index) -> Integer @@ -71,6 +76,36 @@ def _render_for_report(derivation, offset, strings, index) return str.length end + + # @rbs (Derivation derivation) -> Array[String] + def _render_symbols_for_example(derivation) + item = derivation.item + result = item.symbols_before_dot.map do |symbol| + normalize_symbol_for_example(symbol.display_name) + end + + if derivation.left + result.concat(_render_symbols_for_example(derivation.left)) + else + result << "•" + result.concat(item.symbols_after_dot.map { |symbol| normalize_symbol_for_example(symbol.display_name) }) + return result + end + + if (right = derivation.right&.left) + result.concat(_render_symbols_for_example(right)) + tail = item.symbols_after_dot.drop(2) + else + tail = item.symbols_after_dot.drop(1) + end + + result.concat(tail.map { |symbol| normalize_symbol_for_example(symbol.display_name) }) + end + + # @rbs (String name) -> String + def normalize_symbol_for_example(name) + name == '"end of file"' ? "$end" : name + end end end end diff --git a/lib/lrama/counterexamples/example.rb b/lib/lrama/counterexamples/example.rb index c007f45a..30504748 100644 --- a/lib/lrama/counterexamples/example.rb +++ b/lib/lrama/counterexamples/example.rb @@ -12,6 +12,7 @@ class Example # @path1: ::Array[StateItem] # @path2: ::Array[StateItem] # @conflict: State::conflict + # @conflict_symbols: ::Array[Grammar::Symbol] # @conflict_symbol: Grammar::Symbol # @counterexamples: Counterexamples # @derivations1: Derivation @@ -20,16 +21,18 @@ class Example attr_reader :path1 #: ::Array[StateItem] attr_reader :path2 #: ::Array[StateItem] attr_reader :conflict #: State::conflict + attr_reader :conflict_symbols #: ::Array[Grammar::Symbol] attr_reader :conflict_symbol #: Grammar::Symbol # path1 is shift conflict when S/R conflict # path2 is always reduce conflict # - # @rbs (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples) -> void - def initialize(path1, path2, conflict, conflict_symbol, counterexamples) + # @rbs (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples, ?conflict_symbols: Array[Grammar::Symbol]) -> void + def initialize(path1, path2, conflict, conflict_symbol, counterexamples, conflict_symbols: [conflict_symbol]) @path1 = path1 @path2 = path2 @conflict = conflict + @conflict_symbols = conflict_symbols @conflict_symbol = conflict_symbol @counterexamples = counterexamples end @@ -59,6 +62,70 @@ def derivations2 @derivations2 ||= _derivations(path2) end + # @rbs () -> String + def example1 + (shared_example_symbols || full_example_symbols1).join(" ") + end + + # @rbs () -> String + def example2 + (shared_example_symbols || full_example_symbols2).join(" ") + end + + # @rbs () -> bool + def same_example? + example1 == example2 + end + + # @rbs () -> String + def example1_label + same_example? ? "Example" : "First example" + end + + # @rbs () -> String + def example2_label + same_example? ? "Example" : "Second example" + end + + # @rbs () -> String + def derivation_label1 + type == :shift_reduce ? "Shift derivation" : "First Reduce derivation" + end + + # @rbs () -> String + def derivation_label2 + type == :shift_reduce ? "Reduce derivation" : "Second Reduce derivation" + end + + # @rbs () -> String + def conflict_label + labels = conflict_symbols.map { |symbol| normalize_symbol_for_example(symbol.display_name) } + prefix = labels.size == 1 ? "token" : "tokens" + + "#{prefix} #{labels.join(", ")}" + end + + # @rbs (Array[Grammar::Symbol]) -> Example + def merge_conflict_symbols!(symbols) + @conflict_symbols |= symbols + self + end + + # @rbs () -> Array[untyped] + def merge_key + [ + type, + path1.map(&:id), + path2.map(&:id), + example1_label, + example1, + derivations1.render_for_report, + example2_label, + example2, + derivations2.render_for_report + ] + end + private # @rbs (Array[StateItem] state_items) -> Derivation @@ -149,6 +216,57 @@ def find_derivation_for_symbol(state_item, sym) end end end + + # @rbs (String name) -> String + def normalize_symbol_for_example(name) + name == '"end of file"' ? "$end" : name + end + + # @rbs () -> Array[String] + def full_example_symbols1 + derivations1.render_symbols_for_example + end + + # @rbs () -> Array[String] + def full_example_symbols2 + derivations2.render_symbols_for_example + end + + # @rbs () -> Array[String]? + def shared_example_symbols + return @shared_example_symbols if instance_variable_defined?(:@shared_example_symbols) + + @shared_example_symbols = build_shared_example_symbols + end + + # @rbs () -> Array[String]? + def build_shared_example_symbols + return full_example_symbols1 if full_example_symbols1 == full_example_symbols2 + return nil unless type == :shift_reduce + + common = common_prefix(full_example_symbols1, full_example_symbols2) + dot_index = common.index("•") + return nil unless dot_index + + shared_after_dot_length = common.length - dot_index - 1 + return nil if shared_after_dot_length < path1_item.symbols_after_dot.length + + common + end + + # @rbs (Array[String] a, Array[String] b) -> Array[String] + def common_prefix(a, b) + prefix = [] #: Array[String] + + a.zip(b) do |left, right| + break unless left && right + break unless left == right + + prefix << left + end + + prefix + end end end end diff --git a/lib/lrama/logger.rb b/lib/lrama/logger.rb index 291eea52..16d2227e 100644 --- a/lib/lrama/logger.rb +++ b/lib/lrama/logger.rb @@ -23,6 +23,11 @@ def warn(message) @out << 'warning: ' << message << "\n" end + # @rbs (String message) -> void + def note(message) + @out << 'note: ' << message << "\n" + end + # @rbs (String message) -> void def error(message) @out << 'error: ' << message << "\n" diff --git a/lib/lrama/option_parser.rb b/lib/lrama/option_parser.rb index 5a15d59c..69df9868 100644 --- a/lib/lrama/option_parser.rb +++ b/lib/lrama/option_parser.rb @@ -10,6 +10,8 @@ class OptionParser # @options: Lrama::Options # @trace: Array[String] # @report: Array[String] + # @warning: Array[String] + # @warnings_option_specified: bool # @profile: Array[String] # @rbs (Array[String]) -> Lrama::Options @@ -22,6 +24,8 @@ def initialize @options = Options.new @trace = [] @report = [] + @warning = [] + @warnings_option_specified = false @profile = [] end @@ -31,6 +35,8 @@ def parse(argv) @options.trace_opts = validate_trace(@trace) @options.report_opts = validate_report(@report) + @options.warning_opts = validate_warning(@warning) + @options.warnings = warnings_enabled? @options.profile_opts = validate_profile(@profile) @options.grammar_file = argv.shift @@ -127,7 +133,15 @@ def parse_by_option_parser(argv) o.on('-v', '--verbose', "same as '--report=state'") {|_v| @report << 'states' } o.separator '' o.separator 'Diagnostics:' - o.on('-W', '--warnings', 'report the warnings') {|v| @options.warnings = true } + o.on('-W', '--warnings[=CATEGORY]', Array, 'report the warnings') do |v| + @warnings_option_specified = true + @warning.concat(v || []) + end + o.on_tail '' + o.on_tail 'CATEGORY can include:' + o.on_tail ' counterexamples, cex generate conflict counterexamples' + o.on_tail ' all enable all warnings' + o.on_tail ' none disable all warnings' o.separator '' o.separator 'Error Recovery:' o.on('-e', 'enable error recovery') {|v| @options.error_recovery = true } @@ -142,6 +156,8 @@ def parse_by_option_parser(argv) ALIASED_REPORTS = { cex: :counterexamples }.freeze #: Hash[Symbol, Symbol] VALID_REPORTS = %i[states itemsets lookaheads solved counterexamples rules terms verbose].freeze #: Array[Symbol] + ALIASED_WARNINGS = { cex: :counterexamples }.freeze #: Hash[Symbol, Symbol] + VALID_WARNINGS = %i[counterexamples].freeze #: Array[Symbol] # @rbs (Array[String]) -> Hash[Symbol, bool] def validate_report(report) @@ -170,6 +186,41 @@ def aliased_report_option(opt) (ALIASED_REPORTS[opt.to_sym] || opt).to_sym end + # @rbs (Array[String]) -> Hash[Symbol, bool] + def validate_warning(warning) + h = {} #: Hash[Symbol, bool] + return h if warning.empty? + return h if warning == ['none'] + if warning == ['all'] + VALID_WARNINGS.each { |w| h[w] = true } + return h + end + + warning.each do |w| + aliased = aliased_warning_option(w) + if VALID_WARNINGS.include?(aliased) + h[aliased] = true + else + raise "Invalid warning option \"#{w}\"." + end + end + + h + end + + # @rbs (String) -> Symbol + def aliased_warning_option(opt) + (ALIASED_WARNINGS[opt.to_sym] || opt).to_sym + end + + # @rbs () -> bool + def warnings_enabled? + return false unless @warnings_option_specified + return false if @warning == ['none'] + + true + end + VALID_TRACES = %w[ locations scan parse automaton bitsets closure grammar rules only-explicit-rules actions resource diff --git a/lib/lrama/options.rb b/lib/lrama/options.rb index 87aec624..da4943c5 100644 --- a/lib/lrama/options.rb +++ b/lib/lrama/options.rb @@ -14,6 +14,7 @@ class Options attr_accessor :grammar_file #: String attr_accessor :trace_opts #: Hash[Symbol, bool]? attr_accessor :report_opts #: Hash[Symbol, bool]? + attr_accessor :warning_opts #: Hash[Symbol, bool]? attr_accessor :warnings #: bool attr_accessor :y #: IO attr_accessor :debug #: bool @@ -35,6 +36,7 @@ def initialize @grammar_file = '' @trace_opts = nil @report_opts = nil + @warning_opts = nil @warnings = false @y = STDIN @debug = false diff --git a/lib/lrama/reporter/states.rb b/lib/lrama/reporter/states.rb index d152d051..0163792e 100644 --- a/lib/lrama/reporter/states.rb +++ b/lib/lrama/reporter/states.rb @@ -228,20 +228,25 @@ def report_counterexamples(io, state, cex) examples.each do |example| is_shift_reduce = example.type == :shift_reduce - label0 = is_shift_reduce ? "shift/reduce" : "reduce/reduce" - label1 = is_shift_reduce ? "Shift derivation" : "First Reduce derivation" - label2 = is_shift_reduce ? "Reduce derivation" : "Second Reduce derivation" + items = if is_shift_reduce + [example.path2_item, example.path1_item] + else + [example.path1_item, example.path2_item] + end.uniq - io << " #{label0} conflict on token #{example.conflict_symbol.id.s_value}:\n" - io << " #{example.path1_item}\n" - io << " #{example.path2_item}\n" - io << " #{label1}\n" + io << " #{example.type.to_s.tr('_', '/')} conflict on #{example.conflict_label}:\n" + items.each do |item| + io << " #{item}\n" + end + io << " #{example.example1_label}: #{example.example1}\n" + io << " #{example.derivation_label1}\n" example.derivations1.render_strings_for_report.each do |str| io << " #{str}\n" end - io << " #{label2}\n" + io << " #{example.example2_label}: #{example.example2}\n" + io << " #{example.derivation_label2}\n" example.derivations2.render_strings_for_report.each do |str| io << " #{str}\n" diff --git a/lib/lrama/warnings.rb b/lib/lrama/warnings.rb index 52f09144..6154c35c 100644 --- a/lib/lrama/warnings.rb +++ b/lib/lrama/warnings.rb @@ -10,9 +10,9 @@ module Lrama class Warnings - # @rbs (Logger logger, bool warnings) -> void - def initialize(logger, warnings) - @conflicts = Conflicts.new(logger, warnings) + # @rbs (Logger logger, bool warnings, Hash[Symbol, bool] warning_opts) -> void + def initialize(logger, warnings, warning_opts = {}) + @conflicts = Conflicts.new(logger, warnings, counterexamples: warning_opts[:counterexamples] || false) @implicit_empty = ImplicitEmpty.new(logger, warnings) @name_conflicts = NameConflicts.new(logger, warnings) @redefined_rules = RedefinedRules.new(logger, warnings) diff --git a/lib/lrama/warnings/conflicts.rb b/lib/lrama/warnings/conflicts.rb index 6ba0de6f..0d38a137 100644 --- a/lib/lrama/warnings/conflicts.rb +++ b/lib/lrama/warnings/conflicts.rb @@ -4,10 +4,11 @@ module Lrama class Warnings class Conflicts - # @rbs (Lrama::Logger logger, bool warnings) -> void - def initialize(logger, warnings) + # @rbs (Lrama::Logger logger, bool warnings, counterexamples: bool) -> void + def initialize(logger, warnings, counterexamples: false) @logger = logger @warnings = warnings + @counterexamples = counterexamples end # @rbs (Lrama::States states) -> void @@ -21,7 +22,44 @@ def warn(states) if states.rr_conflicts_count != 0 @logger.warn("reduce/reduce conflicts: #{states.rr_conflicts_count} found") end + + return if states.sr_conflicts_count == 0 && states.rr_conflicts_count == 0 + + if @counterexamples + warn_counterexamples(states) + else + @logger.note("rerun with option '-Wcounterexamples' to generate conflict counterexamples") + end end + + private + + # @rbs (Lrama::States states) -> void + def warn_counterexamples(states) + cex = Lrama::Counterexamples.new(states) + first = true + + states.states.each do |state| + next unless state.has_conflicts? + + cex.compute(state).each do |example| + @logger.line_break if first + first = false + @logger.warn("#{example.type.to_s.tr('_', '/')} conflict on #{example.conflict_label} [-Wcounterexamples]") + @logger.trace(" #{example.example1_label}: #{example.example1}") + @logger.trace(" #{example.derivation_label1}") + example.derivations1.render_strings_for_report.each do |line| + @logger.trace(" #{line}") + end + @logger.trace(" #{example.example2_label}: #{example.example2}") + @logger.trace(" #{example.derivation_label2}") + example.derivations2.render_strings_for_report.each do |line| + @logger.trace(" #{line}") + end + end + end + end + end end end diff --git a/sig/generated/lrama/counterexamples.rbs b/sig/generated/lrama/counterexamples.rbs index f73bec3d..ccff2dbb 100644 --- a/sig/generated/lrama/counterexamples.rbs +++ b/sig/generated/lrama/counterexamples.rbs @@ -47,6 +47,12 @@ module Lrama private + # @rbs (State conflict_state, State::conflict conflict) -> Array[Example] + def conflict_examples: (State conflict_state, State::conflict conflict) -> Array[Example] + + # @rbs (Array[Example]) -> Array[Example] + def merge_examples: (Array[Example]) -> Array[Example] + # @rbs (State state, State::Item item) -> StateItem def get_state_item: (State state, State::Item item) -> StateItem @@ -71,11 +77,11 @@ module Lrama # @rbs (StateItem state_item, Bitmap::bitmap precise_lookahead_set) -> Triple def get_triple: (StateItem state_item, Bitmap::bitmap precise_lookahead_set) -> Triple - # @rbs (State conflict_state, State::ShiftReduceConflict conflict) -> Example - def shift_reduce_example: (State conflict_state, State::ShiftReduceConflict conflict) -> Example + # @rbs (State conflict_state, State::ShiftReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example + def shift_reduce_example: (State conflict_state, State::ShiftReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example - # @rbs (State conflict_state, State::ReduceReduceConflict conflict) -> Example - def reduce_reduce_examples: (State conflict_state, State::ReduceReduceConflict conflict) -> Example + # @rbs (State conflict_state, State::ReduceReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example + def reduce_reduce_example: (State conflict_state, State::ReduceReduceConflict conflict, Grammar::Symbol conflict_symbol) -> Example # @rbs (Array[StateItem]? reduce_state_items, State conflict_state, State::Item conflict_item) -> Array[StateItem] def find_shift_conflict_shortest_path: (Array[StateItem]? reduce_state_items, State conflict_state, State::Item conflict_item) -> Array[StateItem] diff --git a/sig/generated/lrama/counterexamples/derivation.rbs b/sig/generated/lrama/counterexamples/derivation.rbs index 746103f5..2392ed10 100644 --- a/sig/generated/lrama/counterexamples/derivation.rbs +++ b/sig/generated/lrama/counterexamples/derivation.rbs @@ -27,10 +27,19 @@ module Lrama # @rbs () -> String def render_for_report: () -> String + # @rbs (?Derivation derivation) -> Array[String] + def render_symbols_for_example: (?Derivation derivation) -> Array[String] + private # @rbs (Derivation derivation, Integer offset, Array[String] strings, Integer index) -> Integer def _render_for_report: (Derivation derivation, Integer offset, Array[String] strings, Integer index) -> Integer + + # @rbs (Derivation derivation) -> Array[String] + def _render_symbols_for_example: (Derivation derivation) -> Array[String] + + # @rbs (String name) -> String + def normalize_symbol_for_example: (String name) -> String end end end diff --git a/sig/generated/lrama/counterexamples/example.rbs b/sig/generated/lrama/counterexamples/example.rbs index fb251cdb..e65f50d7 100644 --- a/sig/generated/lrama/counterexamples/example.rbs +++ b/sig/generated/lrama/counterexamples/example.rbs @@ -9,6 +9,8 @@ module Lrama @conflict: State::conflict + @conflict_symbols: ::Array[Grammar::Symbol] + @conflict_symbol: Grammar::Symbol @counterexamples: Counterexamples @@ -23,13 +25,15 @@ module Lrama attr_reader conflict: State::conflict + attr_reader conflict_symbols: ::Array[Grammar::Symbol] + attr_reader conflict_symbol: Grammar::Symbol # path1 is shift conflict when S/R conflict # path2 is always reduce conflict # - # @rbs (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples) -> void - def initialize: (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples) -> void + # @rbs (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples, ?conflict_symbols: Array[Grammar::Symbol]) -> void + def initialize: (Array[StateItem]? path1, Array[StateItem]? path2, State::conflict conflict, Grammar::Symbol conflict_symbol, Counterexamples counterexamples, ?conflict_symbols: Array[Grammar::Symbol]) -> void # @rbs () -> (:shift_reduce | :reduce_reduce) def type: () -> (:shift_reduce | :reduce_reduce) @@ -46,6 +50,36 @@ module Lrama # @rbs () -> Derivation def derivations2: () -> Derivation + # @rbs () -> String + def example1: () -> String + + # @rbs () -> String + def example2: () -> String + + # @rbs () -> bool + def same_example?: () -> bool + + # @rbs () -> String + def example1_label: () -> String + + # @rbs () -> String + def example2_label: () -> String + + # @rbs () -> String + def derivation_label1: () -> String + + # @rbs () -> String + def derivation_label2: () -> String + + # @rbs () -> String + def conflict_label: () -> String + + # @rbs (Array[Grammar::Symbol]) -> Example + def merge_conflict_symbols!: (Array[Grammar::Symbol]) -> Example + + # @rbs () -> Array[untyped] + def merge_key: () -> Array[untyped] + private # @rbs (Array[StateItem] state_items) -> Derivation @@ -53,6 +87,24 @@ module Lrama # @rbs (StateItem state_item, Grammar::Symbol sym) -> Derivation? def find_derivation_for_symbol: (StateItem state_item, Grammar::Symbol sym) -> Derivation? + + # @rbs (String name) -> String + def normalize_symbol_for_example: (String name) -> String + + # @rbs () -> Array[String] + def full_example_symbols1: () -> Array[String] + + # @rbs () -> Array[String] + def full_example_symbols2: () -> Array[String] + + # @rbs () -> Array[String]? + def shared_example_symbols: () -> Array[String]? + + # @rbs () -> Array[String]? + def build_shared_example_symbols: () -> Array[String]? + + # @rbs (Array[String] a, Array[String] b) -> Array[String] + def common_prefix: (Array[String] a, Array[String] b) -> Array[String] end end end diff --git a/sig/generated/lrama/logger.rbs b/sig/generated/lrama/logger.rbs index 6bac90d3..611f2c9a 100644 --- a/sig/generated/lrama/logger.rbs +++ b/sig/generated/lrama/logger.rbs @@ -14,6 +14,9 @@ module Lrama # @rbs (String message) -> void def warn: (String message) -> void + # @rbs (String message) -> void + def note: (String message) -> void + # @rbs (String message) -> void def error: (String message) -> void end diff --git a/sig/generated/lrama/option_parser.rbs b/sig/generated/lrama/option_parser.rbs index 56b0cb19..1af71a1b 100644 --- a/sig/generated/lrama/option_parser.rbs +++ b/sig/generated/lrama/option_parser.rbs @@ -9,6 +9,10 @@ module Lrama @report: Array[String] + @warning: Array[String] + + @warnings_option_specified: bool + @profile: Array[String] # @rbs (Array[String]) -> Lrama::Options @@ -29,12 +33,25 @@ module Lrama VALID_REPORTS: Array[Symbol] + ALIASED_WARNINGS: Hash[Symbol, Symbol] + + VALID_WARNINGS: Array[Symbol] + # @rbs (Array[String]) -> Hash[Symbol, bool] def validate_report: (Array[String]) -> Hash[Symbol, bool] # @rbs (String) -> Symbol def aliased_report_option: (String) -> Symbol + # @rbs (Array[String]) -> Hash[Symbol, bool] + def validate_warning: (Array[String]) -> Hash[Symbol, bool] + + # @rbs (String) -> Symbol + def aliased_warning_option: (String) -> Symbol + + # @rbs () -> bool + def warnings_enabled?: () -> bool + VALID_TRACES: Array[String] NOT_SUPPORTED_TRACES: Array[String] diff --git a/sig/generated/lrama/options.rbs b/sig/generated/lrama/options.rbs index 48ece486..9fdc33d2 100644 --- a/sig/generated/lrama/options.rbs +++ b/sig/generated/lrama/options.rbs @@ -23,6 +23,8 @@ module Lrama attr_accessor report_opts: Hash[Symbol, bool]? + attr_accessor warning_opts: Hash[Symbol, bool]? + attr_accessor warnings: bool attr_accessor y: IO diff --git a/sig/generated/lrama/warnings.rbs b/sig/generated/lrama/warnings.rbs index 70342f24..b22c0b2c 100644 --- a/sig/generated/lrama/warnings.rbs +++ b/sig/generated/lrama/warnings.rbs @@ -2,8 +2,8 @@ module Lrama class Warnings - # @rbs (Logger logger, bool warnings) -> void - def initialize: (Logger logger, bool warnings) -> void + # @rbs (Logger logger, bool warnings, Hash[Symbol, bool] warning_opts) -> void + def initialize: (Logger logger, bool warnings, Hash[Symbol, bool] warning_opts) -> void # @rbs (Lrama::Grammar grammar, Lrama::States states) -> void def warn: (Lrama::Grammar grammar, Lrama::States states) -> void diff --git a/sig/generated/lrama/warnings/conflicts.rbs b/sig/generated/lrama/warnings/conflicts.rbs index fac91ffa..e4e26c6c 100644 --- a/sig/generated/lrama/warnings/conflicts.rbs +++ b/sig/generated/lrama/warnings/conflicts.rbs @@ -3,11 +3,16 @@ module Lrama class Warnings class Conflicts - # @rbs (Lrama::Logger logger, bool warnings) -> void - def initialize: (Lrama::Logger logger, bool warnings) -> void + # @rbs (Lrama::Logger logger, bool warnings, counterexamples: bool) -> void + def initialize: (Lrama::Logger logger, bool warnings, counterexamples: bool) -> void # @rbs (Lrama::States states) -> void def warn: (Lrama::States states) -> void + + private + + # @rbs (Lrama::States states) -> void + def warn_counterexamples: (Lrama::States states) -> void end end end diff --git a/spec/lrama/counterexamples_output_spec.rb b/spec/lrama/counterexamples_output_spec.rb new file mode 100644 index 00000000..2541a65c --- /dev/null +++ b/spec/lrama/counterexamples_output_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require "stringio" + +RSpec.describe "counterexamples output" do + let(:grammar_source) do + <<~Y + %token ID ',' + + %% + + s: a ID + ; + + a: expr + ; + + expr: %empty + | expr ID ',' + ; + Y + end + + let(:grammar) do + grammar = Lrama::Parser.new(grammar_source, "ids.y").parse + grammar.prepare + grammar.validate! + grammar + end + + let(:states) do + states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new)) + states.compute + states + end + + describe "warning output" do + it "suggests rerunning with -Wcounterexamples when conflicts exist" do + io = StringIO.new + logger = Lrama::Logger.new(io) + + Lrama::Warnings.new(logger, true, {}).warn(grammar, states) + + expect(io.string).to include("warning: shift/reduce conflicts: 1 found\n") + expect(io.string).to include("note: rerun with option '-Wcounterexamples' to generate conflict counterexamples\n") + end + + it "renders nonunifying counterexamples with first and second examples" do + io = StringIO.new + logger = Lrama::Logger.new(io) + + Lrama::Warnings.new(logger, true, { counterexamples: true }).warn(grammar, states) + + expect(io.string).to include("warning: shift/reduce conflict on token ID [-Wcounterexamples]\n") + expect(io.string).to include(" First example: expr • ID ',' ID $end\n") + expect(io.string).to include(" Second example: expr • ID $end\n") + expect(io.string).to include(" Shift derivation\n") + expect(io.string).to include(" Reduce derivation\n") + end + end + + describe "report output" do + it "renders counterexample example lines inside the state report" do + io = StringIO.new + + Lrama::Reporter.new(states: true, counterexamples: true).report(io, states) + + expect(io.string).to include("shift/reduce conflict on token ID:\n") + expect(io.string).to include(" First example: expr • ID ',' ID $end\n") + expect(io.string).to include(" Second example: expr • ID $end\n") + expect(io.string).to include(" Shift derivation\n") + expect(io.string).to include(" Reduce derivation\n") + end + + it "collapses ambiguous arithmetic conflicts to a shared example" do + grammar = Lrama::Parser.new(<<~Y, "calc.y").parse + %token NUM + %% + exp: + exp '+' exp + | exp '-' exp + | exp '*' exp + | exp '/' exp + | NUM + ; + Y + grammar.prepare + grammar.validate! + + states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new)) + states.compute + + io = StringIO.new + Lrama::Reporter.new(states: true, counterexamples: true).report(io, states) + + expect(io.string).to include("shift/reduce conflict on token '/':\n") + expect(io.string).to include(" Example: exp '+' exp • '/' exp\n") + expect(io.string).not_to include(" First example: exp '+' exp • '/' exp\n") + expect(io.string).not_to include(" Second example: exp '+' exp • '/' exp\n") + end + + it "keeps separate reduce/reduce counterexamples when the lookahead changes the witness" do + grammar = Lrama::Parser.new(<<~Y, "multi_rr.y").parse + %token X Y + %% + s: p q ; + p: a | b ; + a: %empty ; + b: %empty ; + q: x | y ; + x: X ; + y: Y ; + Y + grammar.prepare + grammar.validate! + + states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new)) + states.compute + + io = StringIO.new + Lrama::Reporter.new(states: true, counterexamples: true).report(io, states) + + expect(io.string).to include(" reduce/reduce conflict on token X:\n") + expect(io.string).to include(" Example: • • X $end\n") + expect(io.string).to include(" reduce/reduce conflict on token Y:\n") + expect(io.string).to include(" Example: • • Y $end\n") + expect(io.string).not_to include(" reduce/reduce conflict on tokens X, Y:\n") + end + end +end diff --git a/spec/lrama/logger_spec.rb b/spec/lrama/logger_spec.rb index 6020e711..0c1b2340 100644 --- a/spec/lrama/logger_spec.rb +++ b/spec/lrama/logger_spec.rb @@ -28,6 +28,15 @@ end end + describe "#note" do + it "prints a note message" do + out = StringIO.new + logger = described_class.new(out) + logger.note("This is a note message.") + expect(out.string).to eq("note: This is a note message.\n") + end + end + describe "#error" do it "prints an error message" do out = StringIO.new diff --git a/spec/lrama/option_parser_spec.rb b/spec/lrama/option_parser_spec.rb index 7675c4da..ab5247dd 100644 --- a/spec/lrama/option_parser_spec.rb +++ b/spec/lrama/option_parser_spec.rb @@ -65,7 +65,7 @@ -v, --verbose same as '--report=state' Diagnostics: - -W, --warnings report the warnings + -W, --warnings[=CATEGORY] report the warnings Error Recovery: -e enable error recovery @@ -100,6 +100,11 @@ call-stack use sampling call-stack profiler (stackprof gem) memory use memory profiler (memory_profiler gem) + CATEGORY can include: + counterexamples, cex generate conflict counterexamples + all enable all warnings + none disable all warnings + HELP end end @@ -214,6 +219,51 @@ end end + describe "#validate_warning" do + let(:option_parser) { Lrama::OptionParser.new } + + context "when no options are passed" do + it "returns empty option hash" do + opts = option_parser.send(:validate_warning, []) + expect(opts).to eq({}) + end + end + + context "when counterexamples is passed" do + it "returns option hash counterexamples flag enabled" do + opts = option_parser.send(:validate_warning, ["counterexamples"]) + expect(opts).to eq({counterexamples: true}) + end + end + + context "when cex is passed" do + it "returns option hash counterexamples flag enabled" do + opts = option_parser.send(:validate_warning, ["cex"]) + expect(opts).to eq({counterexamples: true}) + end + end + + context "when all is passed" do + it "returns option hash all flags enabled" do + opts = option_parser.send(:validate_warning, ["all"]) + expect(opts).to eq({counterexamples: true}) + end + end + + context "when none is passed" do + it "returns empty option hash" do + opts = option_parser.send(:validate_warning, ["none"]) + expect(opts).to eq({}) + end + end + + describe "invalid options are passed" do + it "raises error" do + expect { option_parser.send(:validate_warning, ["invalid"]) }.to raise_error(/Invalid warning option/) + end + end + end + describe "@grammar_file" do context "file is specified" do it "@grammar_file is file name" do @@ -343,6 +393,46 @@ expect(options.warnings).to be true end end + + context "when -Wcounterexamples is passed" do + it "returns counterexamples warning option" do + option_parser = Lrama::OptionParser.new + option_parser.send(:parse, ["-Wcounterexamples", fixture_path("command/basic.y")]) + options = option_parser.instance_variable_get(:@options) + expect(options.warnings).to be true + expect(options.warning_opts).to eq({counterexamples: true}) + end + end + + context "when -Wcex is passed" do + it "returns counterexamples warning option" do + option_parser = Lrama::OptionParser.new + option_parser.send(:parse, ["-Wcex", fixture_path("command/basic.y")]) + options = option_parser.instance_variable_get(:@options) + expect(options.warnings).to be true + expect(options.warning_opts).to eq({counterexamples: true}) + end + end + + context "when -Wnone is passed" do + it "disables warnings" do + option_parser = Lrama::OptionParser.new + option_parser.send(:parse, ["-Wnone", fixture_path("command/basic.y")]) + options = option_parser.instance_variable_get(:@options) + expect(options.warnings).to be false + expect(options.warning_opts).to eq({}) + end + end + + context "when --warnings=none is passed" do + it "disables warnings" do + option_parser = Lrama::OptionParser.new + option_parser.send(:parse, ["--warnings=none", fixture_path("command/basic.y")]) + options = option_parser.instance_variable_get(:@options) + expect(options.warnings).to be false + expect(options.warning_opts).to eq({}) + end + end end end