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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +26 -0
- data/Steepfile +9 -0
- data/examples/calc.rb +29 -0
- data/examples/json.rb +55 -0
- data/examples/mini_lang.rb +52 -0
- data/exe/rusa +6 -0
- data/lib/rusa/analysis/automaton.rb +60 -0
- data/lib/rusa/analysis/conflict_resolver.rb +211 -0
- data/lib/rusa/analysis/first_follow.rb +106 -0
- data/lib/rusa/analysis/item.rb +51 -0
- data/lib/rusa/analysis/item_set.rb +64 -0
- data/lib/rusa/analysis/lalr_table.rb +460 -0
- data/lib/rusa/analysis/parse_action.rb +81 -0
- data/lib/rusa/cli.rb +188 -0
- data/lib/rusa/errors.rb +12 -0
- data/lib/rusa/generator/code_generator.rb +334 -0
- data/lib/rusa/grammar/action_capture.rb +128 -0
- data/lib/rusa/grammar/dsl.rb +123 -0
- data/lib/rusa/grammar/grammar.rb +212 -0
- data/lib/rusa/grammar/precedence.rb +29 -0
- data/lib/rusa/grammar/rule.rb +55 -0
- data/lib/rusa/grammar/symbol.rb +71 -0
- data/lib/rusa/version.rb +5 -0
- data/lib/rusa.rb +31 -0
- data/sig/generated/rusa/analysis/automaton.rbs +25 -0
- data/sig/generated/rusa/analysis/conflict_resolver.rbs +57 -0
- data/sig/generated/rusa/analysis/first_follow.rbs +33 -0
- data/sig/generated/rusa/analysis/item.rbs +35 -0
- data/sig/generated/rusa/analysis/item_set.rbs +31 -0
- data/sig/generated/rusa/analysis/lalr_table.rbs +182 -0
- data/sig/generated/rusa/analysis/parse_action.rbs +58 -0
- data/sig/generated/rusa/cli.rbs +68 -0
- data/sig/generated/rusa/errors.rbs +24 -0
- data/sig/generated/rusa/generator/code_generator.rbs +82 -0
- data/sig/generated/rusa/grammar/action_capture.rbs +46 -0
- data/sig/generated/rusa/grammar/dsl.rbs +62 -0
- data/sig/generated/rusa/grammar/grammar.rbs +103 -0
- data/sig/generated/rusa/grammar/precedence.rbs +23 -0
- data/sig/generated/rusa/grammar/rule.rbs +35 -0
- data/sig/generated/rusa/grammar/symbol.rbs +51 -0
- data/sig/generated/rusa/version.rbs +5 -0
- data/sig/generated/rusa.rbs +6 -0
- data/test/test_automaton.rb +27 -0
- data/test/test_code_generator.rb +74 -0
- data/test/test_dsl.rb +77 -0
- data/test/test_e2e.rb +134 -0
- data/test/test_first_follow.rb +70 -0
- data/test/test_grammar_model.rb +60 -0
- data/test/test_helper.rb +6 -0
- data/test/test_lalr_table.rb +64 -0
- metadata +96 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Generated from lib/rusa/grammar/grammar.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Rusa
|
|
4
|
+
module Grammar
|
|
5
|
+
# Grammar keeps the normalized internal representation of the DSL.
|
|
6
|
+
class Grammar
|
|
7
|
+
AUGMENTED_START: ::Symbol
|
|
8
|
+
|
|
9
|
+
END_OF_INPUT: ::Symbol
|
|
10
|
+
|
|
11
|
+
attr_accessor start_symbol: Symbol?
|
|
12
|
+
|
|
13
|
+
attr_reader terminals: Hash[Symbol, TerminalSymbol]
|
|
14
|
+
|
|
15
|
+
attr_reader nonterminals: Hash[Symbol, NonterminalSymbol]
|
|
16
|
+
|
|
17
|
+
attr_reader productions: Array[Production]
|
|
18
|
+
|
|
19
|
+
attr_reader precedences: Hash[Symbol, Precedence]
|
|
20
|
+
|
|
21
|
+
attr_reader skip_patterns: Array[Regexp]
|
|
22
|
+
|
|
23
|
+
attr_reader warnings: Array[String]
|
|
24
|
+
|
|
25
|
+
# : () -> void
|
|
26
|
+
def initialize: () -> void
|
|
27
|
+
|
|
28
|
+
# : (Symbol | String, String | Regexp) -> TerminalSymbol
|
|
29
|
+
def add_terminal: (Symbol | String, String | Regexp) -> TerminalSymbol
|
|
30
|
+
|
|
31
|
+
# : (Symbol | String) -> NonterminalSymbol
|
|
32
|
+
def add_nonterminal: (Symbol | String) -> NonterminalSymbol
|
|
33
|
+
|
|
34
|
+
# : (Production) -> Production
|
|
35
|
+
def add_production: (Production) -> Production
|
|
36
|
+
|
|
37
|
+
# : (Regexp) -> Regexp
|
|
38
|
+
def add_skip_pattern: (Regexp) -> Regexp
|
|
39
|
+
|
|
40
|
+
# : (Symbol | String, Integer, Symbol) -> Precedence
|
|
41
|
+
def set_precedence: (Symbol | String, Integer, Symbol) -> Precedence
|
|
42
|
+
|
|
43
|
+
# : (Symbol | String) -> Precedence?
|
|
44
|
+
def precedence_for: (Symbol | String) -> Precedence?
|
|
45
|
+
|
|
46
|
+
# : (Symbol | String) -> Array[Production]
|
|
47
|
+
def productions_for: (Symbol | String) -> Array[Production]
|
|
48
|
+
|
|
49
|
+
# : () -> self
|
|
50
|
+
def augment!: () -> self
|
|
51
|
+
|
|
52
|
+
# : () -> self
|
|
53
|
+
def validate!: () -> self
|
|
54
|
+
|
|
55
|
+
# : () -> Symbol
|
|
56
|
+
def augmented_start: () -> Symbol
|
|
57
|
+
|
|
58
|
+
# : () -> Production
|
|
59
|
+
def augmented_production: () -> Production
|
|
60
|
+
|
|
61
|
+
# : () -> Hash[Symbol, TerminalSymbol | NonterminalSymbol]
|
|
62
|
+
def symbols: () -> Hash[Symbol, TerminalSymbol | NonterminalSymbol]
|
|
63
|
+
|
|
64
|
+
# : (Symbol | String) -> bool
|
|
65
|
+
def terminal?: (Symbol | String) -> bool
|
|
66
|
+
|
|
67
|
+
# : (Symbol | String) -> bool
|
|
68
|
+
def nonterminal?: (Symbol | String) -> bool
|
|
69
|
+
|
|
70
|
+
# : () -> String
|
|
71
|
+
def dump: () -> String
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# : (Symbol) -> String
|
|
76
|
+
def duplicate_nonterminal_message: (Symbol) -> String
|
|
77
|
+
|
|
78
|
+
# : () -> Symbol
|
|
79
|
+
def required_start_symbol: () -> Symbol
|
|
80
|
+
|
|
81
|
+
# : () -> Array[Symbol]
|
|
82
|
+
def undefined_symbols: () -> Array[Symbol]
|
|
83
|
+
|
|
84
|
+
# : (Production) -> Array[Symbol]
|
|
85
|
+
def undefined_symbols_in: (Production) -> Array[Symbol]
|
|
86
|
+
|
|
87
|
+
# : (Symbol) -> bool
|
|
88
|
+
def defined_symbol?: (Symbol) -> bool
|
|
89
|
+
|
|
90
|
+
# : () -> Array[String]
|
|
91
|
+
def unreachable_warnings: () -> Array[String]
|
|
92
|
+
|
|
93
|
+
# : () -> Array[Production]
|
|
94
|
+
def reindex_productions!: () -> Array[Production]
|
|
95
|
+
|
|
96
|
+
# : () -> Array[Symbol]
|
|
97
|
+
def unreachable_nonterminals: () -> Array[Symbol]
|
|
98
|
+
|
|
99
|
+
# : (Symbol, Array[Symbol]) -> Array[Symbol]
|
|
100
|
+
def next_reachable_nonterminals: (Symbol, Array[Symbol]) -> Array[Symbol]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated from lib/rusa/grammar/precedence.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Rusa
|
|
4
|
+
module Grammar
|
|
5
|
+
# Operator precedence metadata used to resolve parser conflicts.
|
|
6
|
+
class Precedence
|
|
7
|
+
attr_reader level: Integer
|
|
8
|
+
|
|
9
|
+
attr_reader associativity: Symbol
|
|
10
|
+
|
|
11
|
+
# : (Integer, Symbol) -> void
|
|
12
|
+
def initialize: (Integer, Symbol) -> void
|
|
13
|
+
|
|
14
|
+
# : (Object) -> bool
|
|
15
|
+
def ==: (Object) -> bool
|
|
16
|
+
|
|
17
|
+
alias eql? ==
|
|
18
|
+
|
|
19
|
+
# : () -> Integer
|
|
20
|
+
def hash: () -> Integer
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated from lib/rusa/grammar/rule.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Rusa
|
|
4
|
+
module Grammar
|
|
5
|
+
# Production rules connect one nonterminal with a sequence of symbols.
|
|
6
|
+
class Production
|
|
7
|
+
attr_accessor id: Integer?
|
|
8
|
+
|
|
9
|
+
attr_accessor lhs: Symbol
|
|
10
|
+
|
|
11
|
+
attr_accessor rhs: Array[Symbol]
|
|
12
|
+
|
|
13
|
+
attr_accessor action: Proc?
|
|
14
|
+
|
|
15
|
+
attr_accessor context_precedence: Symbol?
|
|
16
|
+
|
|
17
|
+
attr_accessor action_source: String?
|
|
18
|
+
|
|
19
|
+
# : (Symbol | String, Array[Symbol | String], ?Proc?, ?context_precedence: Symbol | String?, ?action_source: String?) -> void
|
|
20
|
+
def initialize: (Symbol | String, Array[Symbol | String], ?Proc?, ?context_precedence: Symbol | String?, ?action_source: String?) -> void
|
|
21
|
+
|
|
22
|
+
# : (Grammar) -> Symbol?
|
|
23
|
+
def precedence_token: (Grammar) -> Symbol?
|
|
24
|
+
|
|
25
|
+
# : (Grammar) -> Precedence?
|
|
26
|
+
def precedence: (Grammar) -> Precedence?
|
|
27
|
+
|
|
28
|
+
# : () -> bool
|
|
29
|
+
def empty?: () -> bool
|
|
30
|
+
|
|
31
|
+
# : () -> String
|
|
32
|
+
def to_s: () -> String
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Generated from lib/rusa/grammar/symbol.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Rusa
|
|
4
|
+
module Grammar
|
|
5
|
+
# Terminal symbols are matched by the generated tokenizer.
|
|
6
|
+
class TerminalSymbol
|
|
7
|
+
attr_reader name: Symbol
|
|
8
|
+
|
|
9
|
+
attr_reader pattern: Regexp
|
|
10
|
+
|
|
11
|
+
# : (Symbol | String, String | Regexp) -> void
|
|
12
|
+
def initialize: (Symbol | String, String | Regexp) -> void
|
|
13
|
+
|
|
14
|
+
# : () -> bool
|
|
15
|
+
def terminal?: () -> bool
|
|
16
|
+
|
|
17
|
+
# : () -> bool
|
|
18
|
+
def nonterminal?: () -> bool
|
|
19
|
+
|
|
20
|
+
# : (Object) -> bool
|
|
21
|
+
def ==: (Object) -> bool
|
|
22
|
+
|
|
23
|
+
alias eql? ==
|
|
24
|
+
|
|
25
|
+
# : () -> Integer
|
|
26
|
+
def hash: () -> Integer
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Nonterminal symbols are expanded by grammar productions.
|
|
30
|
+
class NonterminalSymbol
|
|
31
|
+
attr_reader name: Symbol
|
|
32
|
+
|
|
33
|
+
# : (Symbol | String) -> void
|
|
34
|
+
def initialize: (Symbol | String) -> void
|
|
35
|
+
|
|
36
|
+
# : () -> bool
|
|
37
|
+
def terminal?: () -> bool
|
|
38
|
+
|
|
39
|
+
# : () -> bool
|
|
40
|
+
def nonterminal?: () -> bool
|
|
41
|
+
|
|
42
|
+
# : (Object) -> bool
|
|
43
|
+
def ==: (Object) -> bool
|
|
44
|
+
|
|
45
|
+
alias eql? ==
|
|
46
|
+
|
|
47
|
+
# : () -> Integer
|
|
48
|
+
def hash: () -> Integer
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class AutomatonTest < Minitest::Test
|
|
6
|
+
def test_builds_lr0_states_for_balanced_parens_grammar
|
|
7
|
+
grammar = Rusa.grammar do
|
|
8
|
+
token :LPAREN, "("
|
|
9
|
+
token :RPAREN, ")"
|
|
10
|
+
token :ID, "id"
|
|
11
|
+
|
|
12
|
+
start :s
|
|
13
|
+
|
|
14
|
+
rule :s do
|
|
15
|
+
alt(:LPAREN, :s, :RPAREN)
|
|
16
|
+
alt(:ID)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
automaton = Rusa::Analysis::Automaton.new(grammar)
|
|
21
|
+
|
|
22
|
+
assert_equal 6, automaton.states.length
|
|
23
|
+
assert_equal({ s: 1, LPAREN: 2, ID: 3 }, automaton.transitions[0])
|
|
24
|
+
assert automaton.states.any? { |state| state.items.any? { |item| item.to_s == "s -> LPAREN s · RPAREN" } }
|
|
25
|
+
assert automaton.states.any? { |state| state.items.any? { |item| item.to_s == "s -> ID ·" } }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "test_helper"
|
|
5
|
+
|
|
6
|
+
class CodeGeneratorTest < Minitest::Test
|
|
7
|
+
def test_generates_parser_from_file_backed_block_actions
|
|
8
|
+
Dir.mktmpdir do |dir|
|
|
9
|
+
grammar_path = File.join(dir, "calc.rb")
|
|
10
|
+
File.write(grammar_path, <<~RUBY)
|
|
11
|
+
Rusa.grammar do
|
|
12
|
+
token :NUMBER,(/\\d+/)
|
|
13
|
+
token :PLUS, "+"
|
|
14
|
+
token :STAR, "*"
|
|
15
|
+
skip(/\\s+/)
|
|
16
|
+
|
|
17
|
+
left :PLUS
|
|
18
|
+
left :STAR
|
|
19
|
+
|
|
20
|
+
start :expr
|
|
21
|
+
|
|
22
|
+
rule :expr do
|
|
23
|
+
alt(:expr, :PLUS, :expr) { |left, _, right| [:add, left, right] }
|
|
24
|
+
alt(:expr, :STAR, :expr) { |left, _, right| [:mul, left, right] }
|
|
25
|
+
alt(:NUMBER) { |number| number.to_i }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
RUBY
|
|
29
|
+
|
|
30
|
+
grammar = Rusa::CLI.new.send(:load_grammar, grammar_path)
|
|
31
|
+
table = Rusa::Analysis::LALRTable.new(grammar)
|
|
32
|
+
code = Rusa::Generator::CodeGenerator.new(grammar, table, class_name: "GeneratedCalcParser").generate
|
|
33
|
+
parser = build_parser("GeneratedCalcParser", code)
|
|
34
|
+
|
|
35
|
+
assert_equal [:add, 3, [:mul, 4, 5]], parser.parse("3 + 4 * 5")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_generates_compact_parser_from_action_strings
|
|
40
|
+
grammar = Rusa.grammar do
|
|
41
|
+
token :NUMBER,(/\d+/)
|
|
42
|
+
token :PLUS, "+"
|
|
43
|
+
skip(/\s+/)
|
|
44
|
+
|
|
45
|
+
left :PLUS
|
|
46
|
+
start :expr
|
|
47
|
+
|
|
48
|
+
rule :expr do
|
|
49
|
+
alt(:expr, :PLUS, :expr).action("[:add, _1, _3]")
|
|
50
|
+
alt(:NUMBER).action("_1.to_i")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
table = Rusa::Analysis::LALRTable.new(grammar)
|
|
55
|
+
code = Rusa::Generator::CodeGenerator.new(
|
|
56
|
+
grammar,
|
|
57
|
+
table,
|
|
58
|
+
class_name: "CompactExprParser",
|
|
59
|
+
compact: true
|
|
60
|
+
).generate
|
|
61
|
+
parser = build_parser("CompactExprParser", code)
|
|
62
|
+
|
|
63
|
+
assert_includes code, "Base64.decode64"
|
|
64
|
+
assert_equal [:add, [:add, 1, 2], 3], parser.parse("1 + 2 + 3")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def build_parser(class_name, code)
|
|
70
|
+
Object.send(:remove_const, class_name.to_sym) if Object.const_defined?(class_name, false)
|
|
71
|
+
Object.class_eval(code)
|
|
72
|
+
Object.const_get(class_name).new
|
|
73
|
+
end
|
|
74
|
+
end
|
data/test/test_dsl.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class DslTest < Minitest::Test
|
|
6
|
+
def test_dsl_builds_grammar_and_augments_it
|
|
7
|
+
grammar = Rusa.grammar do
|
|
8
|
+
token :NUMBER,(/\d+/)
|
|
9
|
+
token :PLUS, "+"
|
|
10
|
+
skip(/\s+/)
|
|
11
|
+
left :PLUS
|
|
12
|
+
|
|
13
|
+
start :expr
|
|
14
|
+
|
|
15
|
+
rule :expr do
|
|
16
|
+
alt(:expr, :PLUS, :expr) { |l, _, r| [:add, l, r] }
|
|
17
|
+
alt(:NUMBER) { |n| n.to_i }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
assert grammar.terminals.key?(:NUMBER)
|
|
22
|
+
assert grammar.terminals.key?(:PLUS)
|
|
23
|
+
assert grammar.nonterminals.key?(:expr)
|
|
24
|
+
assert grammar.nonterminals.key?(:"$start")
|
|
25
|
+
assert_equal 3, grammar.productions.length
|
|
26
|
+
assert_equal Rusa::Grammar::Precedence.new(1, :left), grammar.precedence_for(:PLUS)
|
|
27
|
+
assert_equal :expr, grammar.start_symbol
|
|
28
|
+
assert_equal 1, grammar.skip_patterns.length
|
|
29
|
+
assert_match(/\Alambda /, grammar.productions[1].action_source)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_keyword_literals_are_bounded_and_regexes_are_anchored
|
|
33
|
+
grammar = Rusa.grammar do
|
|
34
|
+
token :IF, "if"
|
|
35
|
+
token :IDENT,(/[a-z_]\w*/)
|
|
36
|
+
skip(/\s+/)
|
|
37
|
+
start :expr
|
|
38
|
+
|
|
39
|
+
rule :expr do
|
|
40
|
+
alt(:IDENT) { |value| value }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
assert_equal "\\Aif\\b", grammar.terminals[:IF].pattern.source
|
|
45
|
+
assert_equal "\\A(?:[a-z_]\\w*)", grammar.terminals[:IDENT].pattern.source
|
|
46
|
+
assert_equal "\\A(?:\\s+)", grammar.skip_patterns.first.source
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_action_capture_returns_nil_for_non_file_backed_proc
|
|
50
|
+
captured = Rusa::Grammar::ActionCapture.capture(eval("proc { |value| value }"))
|
|
51
|
+
|
|
52
|
+
assert_nil captured
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_action_capture_does_not_swallow_unexpected_internal_errors
|
|
56
|
+
action = proc { |value| value }
|
|
57
|
+
action_capture = Rusa::Grammar::ActionCapture
|
|
58
|
+
singleton_class = action_capture.singleton_class
|
|
59
|
+
singleton_class.alias_method(:__extract_block_before_test__, :extract_block)
|
|
60
|
+
singleton_class.send(:remove_method, :extract_block)
|
|
61
|
+
singleton_class.define_method(:extract_block) { |_source| raise "boom" }
|
|
62
|
+
|
|
63
|
+
error = assert_raises(RuntimeError) do
|
|
64
|
+
action_capture.capture(action)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
assert_equal "boom", error.message
|
|
68
|
+
ensure
|
|
69
|
+
if singleton_class.method_defined?(:extract_block)
|
|
70
|
+
singleton_class.send(:remove_method, :extract_block)
|
|
71
|
+
end
|
|
72
|
+
if singleton_class.method_defined?(:__extract_block_before_test__)
|
|
73
|
+
singleton_class.alias_method(:extract_block, :__extract_block_before_test__)
|
|
74
|
+
singleton_class.send(:remove_method, :__extract_block_before_test__)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/test/test_e2e.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "test_helper"
|
|
5
|
+
|
|
6
|
+
class E2ETest < Minitest::Test
|
|
7
|
+
def test_cli_generates_calc_parser_and_parses_examples
|
|
8
|
+
Dir.mktmpdir do |dir|
|
|
9
|
+
output_path = File.join(dir, "calc_parser.rb")
|
|
10
|
+
status, = run_cli(
|
|
11
|
+
"generate",
|
|
12
|
+
"examples/calc.rb",
|
|
13
|
+
"-o",
|
|
14
|
+
output_path,
|
|
15
|
+
"--class",
|
|
16
|
+
"ExampleCalcParser"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert_equal 0, status
|
|
20
|
+
parser = load_generated_parser("ExampleCalcParser", output_path)
|
|
21
|
+
|
|
22
|
+
assert_equal [:add, 1, 2], parser.parse("1 + 2")
|
|
23
|
+
assert_equal [:add, 1, [:mul, 2, 3]], parser.parse("1 + 2 * 3")
|
|
24
|
+
assert_equal [:mul, [:add, 1, 2], 3], parser.parse("(1 + 2) * 3")
|
|
25
|
+
assert_equal [:add, [:add, 1, 2], 3], parser.parse("1 + 2 + 3")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_generated_parser_reports_parse_and_lex_errors
|
|
30
|
+
Dir.mktmpdir do |dir|
|
|
31
|
+
output_path = File.join(dir, "calc_parser.rb")
|
|
32
|
+
run_cli(
|
|
33
|
+
"generate",
|
|
34
|
+
"examples/calc.rb",
|
|
35
|
+
"-o",
|
|
36
|
+
output_path,
|
|
37
|
+
"--class",
|
|
38
|
+
"ErrorCalcParser"
|
|
39
|
+
)
|
|
40
|
+
parser = load_generated_parser("ErrorCalcParser", output_path)
|
|
41
|
+
|
|
42
|
+
parse_error = assert_raises(ErrorCalcParser::ParseError) { parser.parse("1 + + 2") }
|
|
43
|
+
lex_error = assert_raises(ErrorCalcParser::ParseError) { parser.parse("1 @ 2") }
|
|
44
|
+
|
|
45
|
+
assert_includes parse_error.message, "line 1, column 5"
|
|
46
|
+
assert_includes lex_error.message, "Unexpected character"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_epsilon_grammar_parses_optional_prefix
|
|
51
|
+
Dir.mktmpdir do |dir|
|
|
52
|
+
grammar_path = File.join(dir, "optional.rb")
|
|
53
|
+
parser_path = File.join(dir, "optional_parser.rb")
|
|
54
|
+
|
|
55
|
+
File.write(grammar_path, <<~RUBY)
|
|
56
|
+
Rusa.grammar do
|
|
57
|
+
token :A, "a"
|
|
58
|
+
token :B, "b"
|
|
59
|
+
skip(/\\s+/)
|
|
60
|
+
|
|
61
|
+
start :s
|
|
62
|
+
|
|
63
|
+
rule :s do
|
|
64
|
+
alt(:a, :B) { |prefix, b| [prefix, b] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
rule :a do
|
|
68
|
+
alt(:A) { |a| a }
|
|
69
|
+
alt
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
RUBY
|
|
73
|
+
|
|
74
|
+
run_cli(
|
|
75
|
+
"generate",
|
|
76
|
+
grammar_path,
|
|
77
|
+
"-o",
|
|
78
|
+
parser_path,
|
|
79
|
+
"--class",
|
|
80
|
+
"OptionalParser"
|
|
81
|
+
)
|
|
82
|
+
parser = load_generated_parser("OptionalParser", parser_path)
|
|
83
|
+
|
|
84
|
+
assert_equal ["a", "b"], parser.parse("a b")
|
|
85
|
+
assert_equal [nil, "b"], parser.parse("b")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_report_command_prints_conflict_information
|
|
90
|
+
Dir.mktmpdir do |dir|
|
|
91
|
+
grammar_path = File.join(dir, "dangling_else.rb")
|
|
92
|
+
File.write(grammar_path, <<~RUBY)
|
|
93
|
+
Rusa.grammar do
|
|
94
|
+
token :IF, "if"
|
|
95
|
+
token :THEN, "then"
|
|
96
|
+
token :ELSE, "else"
|
|
97
|
+
token :ID,(/[a-z]+/)
|
|
98
|
+
skip(/\\s+/)
|
|
99
|
+
|
|
100
|
+
start :stmt
|
|
101
|
+
|
|
102
|
+
rule :stmt do
|
|
103
|
+
alt(:IF, :ID, :THEN, :stmt, :ELSE, :stmt)
|
|
104
|
+
alt(:IF, :ID, :THEN, :stmt)
|
|
105
|
+
alt(:ID)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
|
|
110
|
+
status, stdout = run_cli("report", grammar_path)
|
|
111
|
+
|
|
112
|
+
assert_equal 0, status
|
|
113
|
+
assert_includes stdout, "Conflict report:"
|
|
114
|
+
assert_includes stdout, "default shift"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def load_generated_parser(class_name, path)
|
|
121
|
+
Object.send(:remove_const, class_name.to_sym) if Object.const_defined?(class_name, false)
|
|
122
|
+
Object.class_eval(File.read(path), path)
|
|
123
|
+
Object.const_get(class_name).new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def run_cli(*argv)
|
|
127
|
+
status = nil
|
|
128
|
+
stdout, stderr = capture_io do
|
|
129
|
+
status = Rusa::CLI.new.run(argv)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
[status, stdout, stderr]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class FirstFollowTest < Minitest::Test
|
|
6
|
+
def test_expression_first_and_follow_sets
|
|
7
|
+
grammar = Rusa.grammar do
|
|
8
|
+
token :ID, "id"
|
|
9
|
+
token :PLUS, "+"
|
|
10
|
+
token :STAR, "*"
|
|
11
|
+
token :LPAREN, "("
|
|
12
|
+
token :RPAREN, ")"
|
|
13
|
+
|
|
14
|
+
start :e
|
|
15
|
+
|
|
16
|
+
rule :e do
|
|
17
|
+
alt(:e, :PLUS, :t)
|
|
18
|
+
alt(:t)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
rule :t do
|
|
22
|
+
alt(:t, :STAR, :f)
|
|
23
|
+
alt(:f)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
rule :f do
|
|
27
|
+
alt(:LPAREN, :e, :RPAREN)
|
|
28
|
+
alt(:ID)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
first_follow = Rusa::Analysis::FirstFollow.new(grammar)
|
|
33
|
+
|
|
34
|
+
assert_equal Set[:LPAREN, :ID], first_follow.first_sets[:f]
|
|
35
|
+
assert_equal Set[:LPAREN, :ID], first_follow.first_sets[:t]
|
|
36
|
+
assert_equal Set[:LPAREN, :ID], first_follow.first_sets[:e]
|
|
37
|
+
assert_equal Set[:$end, :PLUS, :RPAREN], first_follow.follow_sets[:e]
|
|
38
|
+
assert_equal Set[:$end, :PLUS, :STAR, :RPAREN], first_follow.follow_sets[:t]
|
|
39
|
+
assert_equal Set[:$end, :PLUS, :STAR, :RPAREN], first_follow.follow_sets[:f]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_epsilon_productions_flow_through_sequence_first_sets
|
|
43
|
+
grammar = Rusa.grammar do
|
|
44
|
+
token :A, "a"
|
|
45
|
+
token :B, "b"
|
|
46
|
+
|
|
47
|
+
start :s
|
|
48
|
+
|
|
49
|
+
rule :s do
|
|
50
|
+
alt(:a, :b)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
rule :a do
|
|
54
|
+
alt(:A)
|
|
55
|
+
alt
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
rule :b do
|
|
59
|
+
alt(:B)
|
|
60
|
+
alt
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
first_follow = Rusa::Analysis::FirstFollow.new(grammar)
|
|
65
|
+
|
|
66
|
+
assert_equal Set[:A, :B, Rusa::Analysis::FirstFollow::EMPTY], first_follow.first_sets[:s]
|
|
67
|
+
assert_equal Set[:B, :$end], first_follow.follow_sets[:a]
|
|
68
|
+
assert_equal Set[:$end], first_follow.follow_sets[:b]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class GrammarModelTest < Minitest::Test
|
|
6
|
+
def test_symbol_types_compare_by_name_and_type
|
|
7
|
+
number_a = Rusa::Grammar::TerminalSymbol.new(:NUMBER, /\d+/)
|
|
8
|
+
number_b = Rusa::Grammar::TerminalSymbol.new(:NUMBER, /\d+/)
|
|
9
|
+
expr = Rusa::Grammar::NonterminalSymbol.new(:expr)
|
|
10
|
+
|
|
11
|
+
assert_equal number_a, number_b
|
|
12
|
+
refute_equal number_a, expr
|
|
13
|
+
assert number_a.terminal?
|
|
14
|
+
refute number_a.nonterminal?
|
|
15
|
+
assert expr.nonterminal?
|
|
16
|
+
refute expr.terminal?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_production_uses_context_precedence_or_rightmost_terminal
|
|
20
|
+
grammar = Rusa::Grammar::Grammar.new
|
|
21
|
+
grammar.add_terminal(:PLUS, /\+/)
|
|
22
|
+
grammar.add_terminal(:STAR, /\*/)
|
|
23
|
+
|
|
24
|
+
plain = Rusa::Grammar::Production.new(:expr, [:expr, :PLUS, :expr, :STAR])
|
|
25
|
+
explicit = Rusa::Grammar::Production.new(:expr, [:MINUS, :expr], context_precedence: :UMINUS)
|
|
26
|
+
|
|
27
|
+
assert_equal :STAR, plain.precedence_token(grammar)
|
|
28
|
+
assert_equal :UMINUS, explicit.precedence_token(grammar)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_grammar_tracks_rules_and_validation
|
|
32
|
+
grammar = Rusa::Grammar::Grammar.new
|
|
33
|
+
grammar.add_terminal(:NUMBER, /\d+/)
|
|
34
|
+
grammar.add_terminal(:PLUS, /\+/)
|
|
35
|
+
grammar.start_symbol = :expr
|
|
36
|
+
grammar.add_nonterminal(:expr)
|
|
37
|
+
grammar.add_nonterminal(:unused)
|
|
38
|
+
grammar.add_production(Rusa::Grammar::Production.new(:expr, [:expr, :PLUS, :expr]))
|
|
39
|
+
grammar.add_production(Rusa::Grammar::Production.new(:expr, [:NUMBER]))
|
|
40
|
+
|
|
41
|
+
grammar.augment!
|
|
42
|
+
grammar.validate!
|
|
43
|
+
|
|
44
|
+
assert_equal 3, grammar.productions.length
|
|
45
|
+
assert_equal :expr, grammar.start_symbol
|
|
46
|
+
assert_includes grammar.warnings, "nonterminal unused is unreachable from expr"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_validation_raises_for_undefined_symbols
|
|
50
|
+
grammar = Rusa::Grammar::Grammar.new
|
|
51
|
+
grammar.add_terminal(:NUMBER, /\d+/)
|
|
52
|
+
grammar.start_symbol = :expr
|
|
53
|
+
grammar.add_nonterminal(:expr)
|
|
54
|
+
grammar.add_production(Rusa::Grammar::Production.new(:expr, [:NUMBER, :MISSING]))
|
|
55
|
+
grammar.augment!
|
|
56
|
+
|
|
57
|
+
error = assert_raises(Rusa::UndefinedSymbolError) { grammar.validate! }
|
|
58
|
+
assert_includes error.message, "MISSING"
|
|
59
|
+
end
|
|
60
|
+
end
|