rusa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +175 -0
  4. data/Rakefile +26 -0
  5. data/Steepfile +9 -0
  6. data/examples/calc.rb +29 -0
  7. data/examples/json.rb +55 -0
  8. data/examples/mini_lang.rb +52 -0
  9. data/exe/rusa +6 -0
  10. data/lib/rusa/analysis/automaton.rb +60 -0
  11. data/lib/rusa/analysis/conflict_resolver.rb +211 -0
  12. data/lib/rusa/analysis/first_follow.rb +106 -0
  13. data/lib/rusa/analysis/item.rb +51 -0
  14. data/lib/rusa/analysis/item_set.rb +64 -0
  15. data/lib/rusa/analysis/lalr_table.rb +460 -0
  16. data/lib/rusa/analysis/parse_action.rb +81 -0
  17. data/lib/rusa/cli.rb +188 -0
  18. data/lib/rusa/errors.rb +12 -0
  19. data/lib/rusa/generator/code_generator.rb +334 -0
  20. data/lib/rusa/grammar/action_capture.rb +128 -0
  21. data/lib/rusa/grammar/dsl.rb +123 -0
  22. data/lib/rusa/grammar/grammar.rb +212 -0
  23. data/lib/rusa/grammar/precedence.rb +29 -0
  24. data/lib/rusa/grammar/rule.rb +55 -0
  25. data/lib/rusa/grammar/symbol.rb +71 -0
  26. data/lib/rusa/version.rb +5 -0
  27. data/lib/rusa.rb +31 -0
  28. data/sig/generated/rusa/analysis/automaton.rbs +25 -0
  29. data/sig/generated/rusa/analysis/conflict_resolver.rbs +57 -0
  30. data/sig/generated/rusa/analysis/first_follow.rbs +33 -0
  31. data/sig/generated/rusa/analysis/item.rbs +35 -0
  32. data/sig/generated/rusa/analysis/item_set.rbs +31 -0
  33. data/sig/generated/rusa/analysis/lalr_table.rbs +182 -0
  34. data/sig/generated/rusa/analysis/parse_action.rbs +58 -0
  35. data/sig/generated/rusa/cli.rbs +68 -0
  36. data/sig/generated/rusa/errors.rbs +24 -0
  37. data/sig/generated/rusa/generator/code_generator.rbs +82 -0
  38. data/sig/generated/rusa/grammar/action_capture.rbs +46 -0
  39. data/sig/generated/rusa/grammar/dsl.rbs +62 -0
  40. data/sig/generated/rusa/grammar/grammar.rbs +103 -0
  41. data/sig/generated/rusa/grammar/precedence.rbs +23 -0
  42. data/sig/generated/rusa/grammar/rule.rbs +35 -0
  43. data/sig/generated/rusa/grammar/symbol.rbs +51 -0
  44. data/sig/generated/rusa/version.rbs +5 -0
  45. data/sig/generated/rusa.rbs +6 -0
  46. data/test/test_automaton.rb +27 -0
  47. data/test/test_code_generator.rb +74 -0
  48. data/test/test_dsl.rb +77 -0
  49. data/test/test_e2e.rb +134 -0
  50. data/test/test_first_follow.rb +70 -0
  51. data/test/test_grammar_model.rb +60 -0
  52. data/test/test_helper.rb +6 -0
  53. data/test/test_lalr_table.rb +64 -0
  54. metadata +96 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c28ea4a3cf8a916002f8824535cd4ff420762aa9ce83480852b58e1abc094dc
4
+ data.tar.gz: a69bd45ee7cb9b135b46904cedd492956fe8f8ccf02777632f17b8d0d336c810
5
+ SHA512:
6
+ metadata.gz: 7a26465250a4347e9645b78638dad53b71460a923fe52c366ebce6c9ef258fa7768cadb18040d24b674a406d8c1494a13a1bd2453094abd7c0ae6f2ceae4d52a
7
+ data.tar.gz: 4445972fbf14b5163f5b096903129248f16ca14cf01cb25b13478cb57bb818587dd77e4c9f9424d4e07c100ab979e360ea0ecd53d25f931abc9ef40ad4368f95
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Rusa
2
+
3
+ Rusa is a pure Ruby LALR(1) parser generator. You define a grammar with a Ruby DSL, and Rusa generates a standalone Ruby parser with no runtime gem dependency.
4
+
5
+ ## Features
6
+
7
+ - Pure Ruby implementation
8
+ - Ruby-first DSL for tokens, precedence, and productions
9
+ - Standalone parser output as a single `.rb` file
10
+ - Conflict reporting for shift/reduce and reduce/reduce cases
11
+ - Line and column tracking in generated parse errors
12
+
13
+ ## Installation
14
+
15
+ Add the gem to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "rusa"
19
+ ```
20
+
21
+ Or install it directly:
22
+
23
+ ```bash
24
+ gem install rusa
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ Define a grammar:
30
+
31
+ ```ruby
32
+ Rusa.grammar do
33
+ token :NUMBER, /\d+/
34
+ token :PLUS, "+"
35
+ token :STAR, "*"
36
+ skip /\s+/
37
+
38
+ left :PLUS
39
+ left :STAR
40
+
41
+ start :expr
42
+
43
+ rule :expr do
44
+ alt(:expr, :PLUS, :expr) { |left, _, right| [:add, left, right] }
45
+ alt(:expr, :STAR, :expr) { |left, _, right| [:mul, left, right] }
46
+ alt(:NUMBER) { |number| number.to_i }
47
+ end
48
+ end
49
+ ```
50
+
51
+ Generate a parser:
52
+
53
+ ```bash
54
+ exe/rusa generate examples/calc.rb -o calc_parser.rb --class CalcParser
55
+ ```
56
+
57
+ Use the generated parser:
58
+
59
+ ```ruby
60
+ require_relative "calc_parser"
61
+
62
+ parser = CalcParser.new
63
+ parser.parse("1 + 2 * 3")
64
+ # => [:add, 1, [:mul, 2, 3]]
65
+ ```
66
+
67
+ ## DSL Reference
68
+
69
+ ### Tokens and Skips
70
+
71
+ ```ruby
72
+ token :NUMBER, /\d+/
73
+ token :PLUS, "+"
74
+ skip /\s+/
75
+ ```
76
+
77
+ - `token(name, pattern)` accepts a literal string or regular expression.
78
+ - String literals are escaped and anchored automatically.
79
+ - Identifier-like keywords such as `"if"` are emitted with a trailing word boundary.
80
+
81
+ ### Precedence
82
+
83
+ ```ruby
84
+ left :PLUS, :MINUS
85
+ left :STAR, :SLASH
86
+ right :UMINUS
87
+ nonassoc :EQ, :NEQ
88
+ ```
89
+
90
+ Each call creates one precedence level. Later calls bind tighter.
91
+
92
+ ### Rules
93
+
94
+ ```ruby
95
+ start :expr
96
+
97
+ rule :expr do
98
+ alt(:expr, :PLUS, :expr) { |left, _, right| [:add, left, right] }
99
+ alt(:NUMBER) { |number| number.to_i }
100
+ end
101
+ ```
102
+
103
+ Empty productions are written as `alt`.
104
+
105
+ ### Semantic Actions
106
+
107
+ File-backed grammar blocks are embedded into generated code automatically:
108
+
109
+ ```ruby
110
+ alt(:NUMBER) { |number| number.to_i }
111
+ ```
112
+
113
+ For REPL or eval-driven grammars, use string actions:
114
+
115
+ ```ruby
116
+ alt(:NUMBER).action("_1.to_i")
117
+ alt(:expr, :PLUS, :expr).action("[:add, _1, _3]")
118
+ ```
119
+
120
+ Context precedence is available with `prec`:
121
+
122
+ ```ruby
123
+ alt(:MINUS, :expr).prec(:UMINUS).action("[:neg, _2]")
124
+ ```
125
+
126
+ ## CLI
127
+
128
+ Generate a parser:
129
+
130
+ ```bash
131
+ exe/rusa generate GRAMMAR.rb [-o OUTPUT.rb] [--class NAME] [--compact] [--verbose] [--no-line-tracking]
132
+ ```
133
+
134
+ Inspect conflicts and warnings:
135
+
136
+ ```bash
137
+ exe/rusa report GRAMMAR.rb
138
+ ```
139
+
140
+ Validate a grammar:
141
+
142
+ ```bash
143
+ exe/rusa check GRAMMAR.rb
144
+ ```
145
+
146
+ ## Examples
147
+
148
+ - `examples/calc.rb`: arithmetic expressions with precedence
149
+ - `examples/json.rb`: JSON parser returning Ruby objects
150
+ - `examples/mini_lang.rb`: small statement language with `if`, `while`, and assignment
151
+
152
+ ## Development
153
+
154
+ Run the test suite:
155
+
156
+ ```bash
157
+ rake test
158
+ ```
159
+
160
+ Refresh generated signatures and run the type checker:
161
+
162
+ ```bash
163
+ rake sig:generate
164
+ bundle exec steep check
165
+ ```
166
+
167
+ Or directly:
168
+
169
+ ```bash
170
+ ruby -Ilib -Itest -e "Dir['test/test_*.rb'].sort.each { |f| require_relative f }"
171
+ ```
172
+
173
+ ## License
174
+
175
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |task|
7
+ task.libs << "lib"
8
+ task.libs << "test"
9
+ task.pattern = "test/test_*.rb"
10
+ task.warning = true
11
+ end
12
+
13
+ desc "Generate RBS files from inline annotations"
14
+ task :"sig:generate" do
15
+ sh "rbs-inline", "--opt-out", "--output", "sig/generated", "lib"
16
+ end
17
+
18
+ begin
19
+ require "steep/rake_task"
20
+
21
+ Steep::RakeTask.new(:steep)
22
+ rescue LoadError
23
+ nil
24
+ end
25
+
26
+ task default: :test
data/Steepfile ADDED
@@ -0,0 +1,9 @@
1
+ target :rusa do
2
+ check "lib"
3
+ signature "sig"
4
+
5
+ library "base64"
6
+ library "erb"
7
+ library "optparse"
8
+ library "ripper"
9
+ end
data/examples/calc.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/rusa"
4
+
5
+ Rusa.grammar do
6
+ token :NUMBER,(/\d+(?:\.\d+)?/)
7
+ token :PLUS, "+"
8
+ token :MINUS, "-"
9
+ token :STAR, "*"
10
+ token :SLASH, "/"
11
+ token :LPAREN, "("
12
+ token :RPAREN, ")"
13
+
14
+ skip(/\s+/)
15
+
16
+ left :PLUS, :MINUS
17
+ left :STAR, :SLASH
18
+
19
+ start :expr
20
+
21
+ rule :expr do
22
+ alt(:expr, :PLUS, :expr) { |left, _, right| [:add, left, right] }
23
+ alt(:expr, :MINUS, :expr) { |left, _, right| [:sub, left, right] }
24
+ alt(:expr, :STAR, :expr) { |left, _, right| [:mul, left, right] }
25
+ alt(:expr, :SLASH, :expr) { |left, _, right| [:div, left, right] }
26
+ alt(:LPAREN, :expr, :RPAREN) { |_, expression, _| expression }
27
+ alt(:NUMBER) { |number| number.include?(".") ? number.to_f : number.to_i }
28
+ end
29
+ end
data/examples/json.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/rusa"
4
+
5
+ Rusa.grammar do
6
+ token :LBRACE, "{"
7
+ token :RBRACE, "}"
8
+ token :LBRACKET, "["
9
+ token :RBRACKET, "]"
10
+ token :COLON, ":"
11
+ token :COMMA, ","
12
+ token :TRUE, "true"
13
+ token :FALSE, "false"
14
+ token :NULL, "null"
15
+ token :STRING,(/"(?:\\.|[^"\\])*"/)
16
+ token :NUMBER,(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)
17
+
18
+ skip(/\s+/)
19
+
20
+ start :value
21
+
22
+ rule :value do
23
+ alt(:object).action("_1")
24
+ alt(:array).action("_1")
25
+ alt(:STRING).action("_1[1..-2]")
26
+ alt(:NUMBER).action("_1.match?(/[.eE]/) ? _1.to_f : _1.to_i")
27
+ alt(:TRUE).action("true")
28
+ alt(:FALSE).action("false")
29
+ alt(:NULL).action("nil")
30
+ end
31
+
32
+ rule :object do
33
+ alt(:LBRACE, :members, :RBRACE).action("_2")
34
+ alt(:LBRACE, :RBRACE).action("{}")
35
+ end
36
+
37
+ rule :members do
38
+ alt(:pair).action("{ _1[0] => _1[1] }")
39
+ alt(:members, :COMMA, :pair).action("_1.merge(_3[0] => _3[1])")
40
+ end
41
+
42
+ rule :pair do
43
+ alt(:STRING, :COLON, :value).action("[_1[1..-2], _3]")
44
+ end
45
+
46
+ rule :array do
47
+ alt(:LBRACKET, :elements, :RBRACKET).action("_2")
48
+ alt(:LBRACKET, :RBRACKET).action("[]")
49
+ end
50
+
51
+ rule :elements do
52
+ alt(:value).action("[_1]")
53
+ alt(:elements, :COMMA, :value).action("_1 + [_3]")
54
+ end
55
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/rusa"
4
+
5
+ Rusa.grammar do
6
+ token :IF, "if"
7
+ token :THEN, "then"
8
+ token :ELSE, "else"
9
+ token :WHILE, "while"
10
+ token :DO, "do"
11
+ token :END, "end"
12
+ token :IDENT,(/[a-z_]\w*/)
13
+ token :NUMBER,(/\d+/)
14
+ token :ASSIGN, "="
15
+ token :SEMI, ";"
16
+ token :PLUS, "+"
17
+ token :STAR, "*"
18
+ token :LPAREN, "("
19
+ token :RPAREN, ")"
20
+
21
+ skip(/\s+/)
22
+
23
+ left :PLUS
24
+ left :STAR
25
+
26
+ start :program
27
+
28
+ rule :program do
29
+ alt(:stmts).action("[:program, _1]")
30
+ end
31
+
32
+ rule :stmts do
33
+ alt(:stmt).action("[_1]")
34
+ alt(:stmts, :SEMI, :stmt).action("_1 + [_3]")
35
+ end
36
+
37
+ rule :stmt do
38
+ alt(:IDENT, :ASSIGN, :expr).action("[:assign, _1, _3]")
39
+ alt(:IF, :expr, :THEN, :stmts, :END).action("[:if, _2, _4]")
40
+ alt(:IF, :expr, :THEN, :stmts, :ELSE, :stmts, :END).action("[:if, _2, _4, _6]")
41
+ alt(:WHILE, :expr, :DO, :stmts, :END).action("[:while, _2, _4]")
42
+ alt(:expr).action("_1")
43
+ end
44
+
45
+ rule :expr do
46
+ alt(:expr, :PLUS, :expr).action("[:add, _1, _3]")
47
+ alt(:expr, :STAR, :expr).action("[:mul, _1, _3]")
48
+ alt(:LPAREN, :expr, :RPAREN).action("_2")
49
+ alt(:NUMBER).action("_1.to_i")
50
+ alt(:IDENT).action("[:var, _1]")
51
+ end
52
+ end
data/exe/rusa ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/rusa"
5
+
6
+ exit(Rusa::CLI.new.run(ARGV))
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rusa
4
+ module Analysis
5
+ # Automaton builds the canonical LR(0) state graph.
6
+ class Automaton
7
+ attr_reader :states #: Array[ItemSet]
8
+ attr_reader :transitions #: Hash[Integer, Hash[Symbol, Integer]]
9
+
10
+ #: (Grammar::Grammar) -> void
11
+ def initialize(grammar)
12
+ @grammar = grammar
13
+ @states = []
14
+ @transitions = Hash.new do |hash, key|
15
+ hash[key] = {} #: Hash[Symbol, Integer]
16
+ end
17
+ build
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :grammar #: Grammar::Grammar
23
+
24
+ #: () -> void
25
+ def build
26
+ start_item = Item.new(grammar.augmented_production, 0)
27
+ state0 = ItemSet.new(0, [start_item], grammar)
28
+ @states << state0
29
+
30
+ known_states = { kernel_key(state0.kernel_items) => state0 }
31
+ worklist = [state0]
32
+
33
+ until worklist.empty?
34
+ current = worklist.shift
35
+ symbols = current.items.map(&:next_symbol).compact.uniq
36
+
37
+ symbols.each do |symbol|
38
+ kernel_items = current.items.select { |item| item.next_symbol == symbol }.map(&:advance)
39
+ key = kernel_key(kernel_items)
40
+ target = known_states[key]
41
+
42
+ unless target
43
+ target = ItemSet.new(states.length, kernel_items, grammar)
44
+ known_states[key] = target
45
+ states << target
46
+ worklist << target
47
+ end
48
+
49
+ transitions[current.id][symbol] = target.id
50
+ end
51
+ end
52
+ end
53
+
54
+ #: (Enumerable[Item]) -> Array[Array[Integer?]]
55
+ def kernel_key(items)
56
+ items.map { |item| [item.production.id, item.dot] }.sort.freeze
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rusa
4
+ module Analysis
5
+ class ConflictReport
6
+ attr_reader :state_id #: Integer
7
+ attr_reader :lookahead #: Symbol
8
+ attr_reader :existing_action #: Shift | Reduce | Accept
9
+ attr_reader :new_action #: Shift | Reduce | Accept
10
+ attr_reader :resolved_action #: Shift | Reduce | Accept | nil
11
+ attr_reader :message #: String
12
+
13
+ #: (state_id: Integer, lookahead: Symbol, existing_action: Shift | Reduce | Accept, new_action: Shift | Reduce | Accept, resolved_action: Shift | Reduce | Accept | nil, message: String) -> void
14
+ def initialize(
15
+ state_id:,
16
+ lookahead:,
17
+ existing_action:,
18
+ new_action:,
19
+ resolved_action:,
20
+ message:
21
+ )
22
+ @state_id = state_id
23
+ @lookahead = lookahead
24
+ @existing_action = existing_action
25
+ @new_action = new_action
26
+ @resolved_action = resolved_action
27
+ @message = message
28
+ freeze
29
+ end
30
+ end
31
+
32
+ # ConflictResolver applies precedence and associativity rules to ACTION conflicts.
33
+ class ConflictResolver
34
+ #: (Shift | Reduce | Accept, Shift | Reduce | Accept, Symbol, Grammar::Grammar, state_id: Integer, productions_by_id: Hash[Integer?, Grammar::Production]) -> [Shift | Reduce | Accept | nil, ConflictReport]
35
+ def resolve(existing_action, new_action, lookahead, grammar, state_id:, productions_by_id:)
36
+ report = build_report(
37
+ existing_action,
38
+ new_action,
39
+ lookahead,
40
+ grammar,
41
+ state_id: state_id,
42
+ productions_by_id: productions_by_id
43
+ )
44
+
45
+ [report.resolved_action, report]
46
+ end
47
+
48
+ private
49
+
50
+ #: (Shift | Reduce | Accept, Shift | Reduce | Accept, Symbol, Grammar::Grammar, state_id: Integer, productions_by_id: Hash[Integer?, Grammar::Production]) -> ConflictReport
51
+ def build_report(
52
+ existing_action,
53
+ new_action,
54
+ lookahead,
55
+ grammar,
56
+ state_id:,
57
+ productions_by_id:
58
+ )
59
+ return resolve_shift_reduce(
60
+ existing_action,
61
+ new_action,
62
+ lookahead,
63
+ grammar,
64
+ productions_by_id,
65
+ state_id
66
+ ) if existing_action.is_a?(Shift) && new_action.is_a?(Reduce)
67
+
68
+ return resolve_shift_reduce(
69
+ new_action,
70
+ existing_action,
71
+ lookahead,
72
+ grammar,
73
+ productions_by_id,
74
+ state_id
75
+ ) if existing_action.is_a?(Reduce) && new_action.is_a?(Shift)
76
+
77
+ return resolve_reduce_reduce(
78
+ existing_action,
79
+ new_action,
80
+ lookahead,
81
+ state_id
82
+ ) if existing_action.is_a?(Reduce) && new_action.is_a?(Reduce)
83
+
84
+ duplicate_action_report(
85
+ state_id,
86
+ lookahead,
87
+ existing_action,
88
+ new_action
89
+ )
90
+ end
91
+
92
+ #: (Shift, Reduce, Symbol, Grammar::Grammar, Hash[Integer?, Grammar::Production], Integer) -> ConflictReport
93
+ def resolve_shift_reduce(
94
+ shift_action,
95
+ reduce_action,
96
+ lookahead,
97
+ grammar,
98
+ productions_by_id,
99
+ state_id
100
+ )
101
+ shift_precedence = grammar.precedence_for(lookahead)
102
+ reduce_production = productions_by_id.fetch(reduce_action.production_id)
103
+ reduce_precedence = reduce_production.precedence(grammar)
104
+
105
+ resolved_action, message = if shift_precedence && reduce_precedence
106
+ compare_precedences(
107
+ shift_action,
108
+ reduce_action,
109
+ shift_precedence,
110
+ reduce_precedence
111
+ )
112
+ else
113
+ default_shift_resolution(shift_action, lookahead)
114
+ end
115
+
116
+ build_conflict_report(
117
+ state_id: state_id,
118
+ lookahead: lookahead,
119
+ existing_action: shift_action,
120
+ new_action: reduce_action,
121
+ resolved_action: resolved_action,
122
+ message: message
123
+ )
124
+ end
125
+
126
+ #: (Shift, Reduce, Grammar::Precedence, Grammar::Precedence) -> [Shift | Reduce | nil, String]
127
+ def compare_precedences(shift_action, reduce_action, shift_precedence, reduce_precedence)
128
+ if shift_precedence.level > reduce_precedence.level
129
+ return higher_token_precedence_result(shift_action)
130
+ end
131
+
132
+ if shift_precedence.level < reduce_precedence.level
133
+ return higher_production_precedence_result(reduce_action)
134
+ end
135
+
136
+ case shift_precedence.associativity
137
+ when :left
138
+ [reduce_action, "shift/reduce conflict resolved by left associativity"]
139
+ when :right
140
+ [shift_action, "shift/reduce conflict resolved by right associativity"]
141
+ when :nonassoc
142
+ [nil, "shift/reduce conflict resolved to syntax error by nonassociativity"]
143
+ else
144
+ [shift_action, "shift/reduce conflict resolved by default shift"]
145
+ end
146
+ end
147
+
148
+ #: (Reduce, Reduce, Symbol, Integer) -> ConflictReport
149
+ def resolve_reduce_reduce(existing_action, new_action, lookahead, state_id)
150
+ resolved_action = [existing_action, new_action].min_by do |action|
151
+ action.production_id || -1
152
+ end
153
+
154
+ build_conflict_report(
155
+ state_id: state_id,
156
+ lookahead: lookahead,
157
+ existing_action: existing_action,
158
+ new_action: new_action,
159
+ resolved_action: resolved_action,
160
+ message: "reduce/reduce conflict on #{lookahead} resolved by earlier production"
161
+ )
162
+ end
163
+
164
+ #: (Integer, Symbol, Shift | Reduce | Accept, Shift | Reduce | Accept) -> ConflictReport
165
+ def duplicate_action_report(state_id, lookahead, existing_action, new_action)
166
+ build_conflict_report(
167
+ state_id: state_id,
168
+ lookahead: lookahead,
169
+ existing_action: existing_action,
170
+ new_action: new_action,
171
+ resolved_action: existing_action,
172
+ message: "duplicate action kept for #{lookahead}"
173
+ )
174
+ end
175
+
176
+ #: (Shift, Symbol) -> [Shift, String]
177
+ def default_shift_resolution(shift_action, lookahead)
178
+ [shift_action, "shift/reduce conflict on #{lookahead} resolved by default shift"]
179
+ end
180
+
181
+ #: (Shift) -> [Shift, String]
182
+ def higher_token_precedence_result(shift_action)
183
+ [shift_action, "shift/reduce conflict resolved by higher token precedence"]
184
+ end
185
+
186
+ #: (Reduce) -> [Reduce, String]
187
+ def higher_production_precedence_result(reduce_action)
188
+ [reduce_action, "shift/reduce conflict resolved by higher production precedence"]
189
+ end
190
+
191
+ #: (state_id: Integer, lookahead: Symbol, existing_action: Shift | Reduce | Accept, new_action: Shift | Reduce | Accept, resolved_action: Shift | Reduce | Accept | nil, message: String) -> ConflictReport
192
+ def build_conflict_report(
193
+ state_id:,
194
+ lookahead:,
195
+ existing_action:,
196
+ new_action:,
197
+ resolved_action:,
198
+ message:
199
+ )
200
+ ConflictReport.new(
201
+ state_id: state_id,
202
+ lookahead: lookahead,
203
+ existing_action: existing_action,
204
+ new_action: new_action,
205
+ resolved_action: resolved_action,
206
+ message: message
207
+ )
208
+ end
209
+ end
210
+ end
211
+ end