rley 0.7.08 → 0.8.03

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -5
  3. data/CHANGELOG.md +28 -4
  4. data/README.md +4 -5
  5. data/examples/NLP/nano_eng/nano_en_demo.rb +7 -11
  6. data/examples/NLP/nano_eng/nano_grammar.rb +18 -18
  7. data/examples/data_formats/JSON/json_ast_builder.rb +9 -18
  8. data/examples/data_formats/JSON/json_demo.rb +1 -2
  9. data/examples/data_formats/JSON/json_grammar.rb +11 -11
  10. data/examples/general/calc_iter1/calc_grammar.rb +5 -4
  11. data/examples/general/calc_iter2/calc_grammar.rb +9 -9
  12. data/examples/general/left.rb +1 -1
  13. data/examples/general/right.rb +1 -1
  14. data/lib/rley/base/dotted_item.rb +5 -0
  15. data/lib/rley/base/grm_items_builder.rb +6 -0
  16. data/lib/rley/constants.rb +1 -1
  17. data/lib/rley/engine.rb +2 -2
  18. data/lib/rley/interface.rb +16 -0
  19. data/lib/rley/notation/all_notation_nodes.rb +4 -0
  20. data/lib/rley/notation/ast_builder.rb +185 -0
  21. data/lib/rley/notation/ast_node.rb +44 -0
  22. data/lib/rley/notation/ast_visitor.rb +115 -0
  23. data/lib/rley/notation/grammar.rb +49 -0
  24. data/lib/rley/notation/grammar_builder.rb +505 -0
  25. data/lib/rley/notation/grouping_node.rb +23 -0
  26. data/lib/rley/notation/parser.rb +56 -0
  27. data/lib/rley/notation/sequence_node.rb +35 -0
  28. data/lib/rley/notation/symbol_node.rb +29 -0
  29. data/lib/rley/notation/tokenizer.rb +180 -0
  30. data/lib/rley/parse_rep/ast_base_builder.rb +44 -0
  31. data/lib/rley/parser/gfg_chart.rb +101 -6
  32. data/lib/rley/parser/gfg_earley_parser.rb +1 -1
  33. data/lib/rley/parser/gfg_parsing.rb +5 -3
  34. data/lib/rley/parser/parse_entry_set.rb +1 -1
  35. data/lib/rley/syntax/{grammar_builder.rb → base_grammar_builder.rb} +53 -15
  36. data/lib/rley/syntax/grm_symbol.rb +1 -1
  37. data/lib/rley/syntax/match_closest.rb +43 -0
  38. data/lib/rley/syntax/production.rb +6 -0
  39. data/lib/rley.rb +1 -1
  40. data/spec/rley/engine_spec.rb +6 -6
  41. data/spec/rley/gfg/grm_flow_graph_spec.rb +2 -2
  42. data/spec/rley/notation/grammar_builder_spec.rb +302 -0
  43. data/spec/rley/notation/parser_spec.rb +183 -0
  44. data/spec/rley/notation/tokenizer_spec.rb +364 -0
  45. data/spec/rley/parse_rep/ast_builder_spec.rb +0 -1
  46. data/spec/rley/parse_rep/groucho_spec.rb +1 -1
  47. data/spec/rley/parse_rep/parse_forest_builder_spec.rb +1 -1
  48. data/spec/rley/parse_rep/parse_forest_factory_spec.rb +2 -2
  49. data/spec/rley/parse_rep/parse_tree_factory_spec.rb +1 -1
  50. data/spec/rley/parser/dangling_else_spec.rb +447 -0
  51. data/spec/rley/parser/gfg_earley_parser_spec.rb +118 -10
  52. data/spec/rley/parser/gfg_parsing_spec.rb +2 -1
  53. data/spec/rley/parser/parse_walker_factory_spec.rb +2 -2
  54. data/spec/rley/support/ambiguous_grammar_helper.rb +2 -2
  55. data/spec/rley/support/grammar_abc_helper.rb +2 -2
  56. data/spec/rley/support/grammar_ambig01_helper.rb +2 -2
  57. data/spec/rley/support/grammar_arr_int_helper.rb +2 -2
  58. data/spec/rley/support/grammar_b_expr_helper.rb +2 -2
  59. data/spec/rley/support/grammar_int_seq_helper.rb +51 -0
  60. data/spec/rley/support/grammar_l0_helper.rb +2 -2
  61. data/spec/rley/support/grammar_pb_helper.rb +2 -2
  62. data/spec/rley/support/grammar_sppf_helper.rb +2 -2
  63. data/spec/rley/syntax/{grammar_builder_spec.rb → base_grammar_builder_spec.rb} +29 -11
  64. data/spec/rley/syntax/match_closest_spec.rb +46 -0
  65. data/spec/rley/syntax/production_spec.rb +4 -0
  66. metadata +29 -14
  67. data/lib/rley/parser/parse_state.rb +0 -78
  68. data/lib/rley/parser/parse_state_tracker.rb +0 -59
  69. data/lib/rley/parser/state_set.rb +0 -100
  70. data/spec/rley/parser/parse_state_spec.rb +0 -125
  71. data/spec/rley/parser/parse_tracer_spec.rb +0 -200
  72. data/spec/rley/parser/state_set_spec.rb +0 -130
@@ -51,7 +51,7 @@ module Rley # This module is used as a namespace
51
51
  result.chart[index].each do |entry|
52
52
  # Is entry of the form? [A => alpha . B beta, k]...
53
53
  next_symbol = entry.next_symbol
54
- if next_symbol&.kind_of?(Syntax::NonTerminal)
54
+ if next_symbol.kind_of?(Syntax::NonTerminal)
55
55
  # ...apply the Call rule
56
56
  call_rule(result, entry, index)
57
57
  end
@@ -280,8 +280,8 @@ module Rley # This module is used as a namespace
280
280
 
281
281
  def apply_rule(antecedentEntry, aVertex, anOrigin, aPosition, aRuleId)
282
282
  consequent = push_entry(aVertex, anOrigin, aPosition, aRuleId)
283
-
284
- antecedence[consequent] << antecedentEntry
283
+ if consequent
284
+ antecedence[consequent] << antecedentEntry
285
285
 
286
286
  =begin
287
287
  # Invariant checks
@@ -319,7 +319,9 @@ module Rley # This module is used as a namespace
319
319
  end
320
320
  end
321
321
  =end
322
- consequent.add_antecedent(antecedentEntry)
322
+ consequent.add_antecedent(antecedentEntry)
323
+ end
324
+
323
325
  consequent
324
326
  end
325
327
 
@@ -82,7 +82,7 @@ module Rley # This module is used as a namespace
82
82
 
83
83
  # Group parse entries by lhs symbol and origin
84
84
  groupings = complete_entries.group_by do |entry|
85
- entry.vertex.dotted_rule.lhs.object_id.to_s
85
+ entry.vertex.dotted_item.lhs.object_id.to_s
86
86
  end
87
87
 
88
88
  # Retain the groups having more than one element.
@@ -13,7 +13,7 @@ module Rley # This module is used as a namespace
13
13
  # Builder GoF pattern. Builder builds a complex object
14
14
  # (say, a grammar) from simpler objects (terminals and productions)
15
15
  # and using a step by step approach.
16
- class GrammarBuilder
16
+ class BaseGrammarBuilder
17
17
  # @return [Hash{String, GrmSymbol}] The mapping of grammar symbol names
18
18
  # to the matching grammar symbol object.
19
19
  attr_reader(:symbols)
@@ -56,6 +56,14 @@ module Rley # This module is used as a namespace
56
56
  symbols.merge!(new_symbs)
57
57
  end
58
58
 
59
+ # Add the given marker symbol to the grammar of the language
60
+ # @param aMarkerSymbol [Syntax::Marker] A mazker symbol
61
+ # @return [void]
62
+ def add_marker(aMarkerSymbol)
63
+ new_symb = build_symbol(Marker, aMarkerSymbol)
64
+ symbols[aMarkerSymbol.name] = new_symb
65
+ end
66
+
59
67
  # Add a production rule in the grammar given one
60
68
  # key-value pair of the form: String => Array.
61
69
  # Where the key is the name of the non-terminal appearing in the
@@ -72,13 +80,13 @@ module Rley # This module is used as a namespace
72
80
  # @return [Production] The created Production instance
73
81
  def add_production(aProductionRepr)
74
82
  aProductionRepr.each_pair do |(lhs_name, rhs_repr)|
75
- lhs = get_nonterminal(lhs_name)
83
+ lhs = get_grm_symbol(lhs_name)
76
84
  case rhs_repr
77
85
  when Array
78
- rhs_members = rhs_repr.map { |name| get_nonterminal(name) }
86
+ rhs_members = rhs_repr.map { |name| get_grm_symbol(name) }
79
87
  when String
80
88
  rhs_lexemes = rhs_repr.scan(/\S+/)
81
- rhs_members = rhs_lexemes.map { |name| get_nonterminal(name) }
89
+ rhs_members = rhs_lexemes.map { |name| get_grm_symbol(name) }
82
90
  when Terminal
83
91
  rhs_members = [rhs_repr]
84
92
  end
@@ -122,6 +130,20 @@ module Rley # This module is used as a namespace
122
130
  return @grammar
123
131
  end
124
132
 
133
+ # When a symbol, say symb, in a rhs is followed by a '+' modifier,
134
+ # then a rule will be generated with a lhs named symb + suffix_plus
135
+ def suffix_plus
136
+ '_plus'
137
+ end
138
+
139
+ def suffix_plus_more
140
+ 'base_plus_more'
141
+ end
142
+
143
+ def suffix_plus_last
144
+ 'base_plus_last'
145
+ end
146
+
125
147
  alias rule add_production
126
148
 
127
149
  private
@@ -149,24 +171,40 @@ module Rley # This module is used as a namespace
149
171
  # @param aSymbolArg [GrmSymbol-like or String]
150
172
  # @return [Array] list of grammar symbols
151
173
  def build_symbol(aClass, aSymbolArg)
152
- a_symbol = if aSymbolArg.kind_of?(GrmSymbol)
153
- aSymbolArg
154
- else
155
- aClass.new(aSymbolArg)
156
- end
157
-
158
- return a_symbol
174
+ if aSymbolArg.kind_of?(GrmSymbol)
175
+ aSymbolArg
176
+ else
177
+ aClass.new(aSymbolArg)
178
+ end
159
179
  end
160
180
 
161
181
  # Retrieve the non-terminal symbol with given name.
162
182
  # If it doesn't exist yet, then it is created on the fly.
163
183
  # @param aSymbolName [String] the name of the grammar symbol to retrieve
164
184
  # @return [NonTerminal]
165
- def get_nonterminal(aSymbolName)
166
- unless symbols.include? aSymbolName
167
- symbols[aSymbolName] = NonTerminal.new(aSymbolName)
185
+ def get_grm_symbol(aSymbolName)
186
+ unless aSymbolName.end_with?('+') && aSymbolName.length > 1
187
+ name = aSymbolName
188
+ else
189
+ name = aSymbolName.chop
190
+ case aSymbolName[-1]
191
+ when '+'
192
+ name_modified = "#{name}#{suffix_plus}"
193
+ unless symbols.include? name_modified
194
+ symbols[name_modified] = NonTerminal.new(name_modified)
195
+ rule(name_modified => [name_modified, name]).as suffix_plus_more
196
+ rule(name_modified => name).as suffix_plus_last
197
+ end
198
+ name = name_modified
199
+ else
200
+ err_msg = "Unknown symbol modifier #{aSymbolName[-1]}"
201
+ raise NotImplementedError, err_msg
202
+ end
168
203
  end
169
- symbols[aSymbolName]
204
+
205
+ symbols[name] = NonTerminal.new(name) unless symbols.include? name
206
+
207
+ symbols[name]
170
208
  end
171
209
  end # class
172
210
  end # module
@@ -5,7 +5,7 @@ module Rley # This module is used as a namespace
5
5
  # Abstract class for grammar symbols.
6
6
  # A grammar symbol is an element that appears in grammar rules.
7
7
  class GrmSymbol
8
- # The name of the grammar symbol
8
+ # @return [String] The name of the grammar symbol
9
9
  attr_reader(:name)
10
10
 
11
11
  # An indicator that tells whether the grammar symbol can generate a
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rley # This module is used as a namespace
4
+ module Syntax # This module is used as a namespace
5
+ # A constraint that indicates that a given rhs member must
6
+ # match the closest given terminal symbol in that rhs
7
+ class MatchClosest
8
+ # @return [Integer] index of constrained symbol to match
9
+ attr_reader(:idx_symbol)
10
+
11
+ # @return [String] name of closest preceding symbol to pair
12
+ attr_reader(:closest_symb)
13
+
14
+ # @return [NilClass, Array<Parser::ParseEntry>] set of entries with closest symbol
15
+ attr_accessor(:entries)
16
+
17
+ # @param aSymbolSeq [Rley::Syntax::SymbolSeq] a sequence of grammar symbols
18
+ # @param idxSymbol [Integer] index of symbol
19
+ # @param nameClosest [String] Terminal symbol name
20
+ def initialize(aSymbolSeq, idxSymbol, nameClosest)
21
+ @idx_symbol = valid_idx_symbol(idxSymbol, aSymbolSeq)
22
+ @closest_symb = valid_name_closest(nameClosest)
23
+ end
24
+
25
+ private
26
+
27
+ # Check that the provided index is within plausible bounds
28
+ def valid_idx_symbol(idxSymbol, aSymbolSeq)
29
+ bounds = 0..aSymbolSeq.size - 1
30
+ err_msg_outbound = 'Index of symbol out of bound'
31
+ raise StandardError, err_msg_outbound unless bounds.include? idxSymbol
32
+
33
+ idxSymbol
34
+ end
35
+
36
+ def valid_name_closest(nameClosest)
37
+ nameClosest
38
+ end
39
+ end # class
40
+ end # module
41
+ end # module
42
+
43
+ # End of file
@@ -31,6 +31,9 @@ module Rley # This module is used as a namespace
31
31
  # rhs members are nullable.
32
32
  attr_writer(:nullable)
33
33
 
34
+ # @return [Array<Syntax::MatchClosest>] A list of constraints between rhs members
35
+ attr_accessor(:constraints)
36
+
34
37
  # Provide common alternate names to lhs and rhs accessors
35
38
 
36
39
  alias body rhs
@@ -42,6 +45,7 @@ module Rley # This module is used as a namespace
42
45
  def initialize(aNonTerminal, theSymbols)
43
46
  @lhs = valid_lhs(aNonTerminal)
44
47
  @rhs = valid_rhs(theSymbols)
48
+ @constraints = []
45
49
  end
46
50
 
47
51
  # Is the rhs empty?
@@ -81,6 +85,8 @@ module Rley # This module is used as a namespace
81
85
  @name = aName
82
86
  end
83
87
 
88
+ alias tag as
89
+
84
90
  private
85
91
 
86
92
  # Validation method. Return the validated input argument or
data/lib/rley.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # for a Rley client.
6
6
 
7
7
  require_relative './rley/constants'
8
- require_relative './rley/syntax/grammar_builder'
8
+ require_relative './rley/interface'
9
9
  require_relative './rley/lexical/token'
10
10
  require_relative './rley/parser/gfg_earley_parser'
11
11
  require_relative './rley/parse_rep/ast_base_builder'
@@ -35,9 +35,9 @@ module Rley # Open this namespace to avoid module qualifier prefixes
35
35
  it 'should build grammar' do
36
36
  subject.build_grammar do
37
37
  add_terminals('a', 'b', 'c')
38
- add_production('S' => ['A'])
39
- add_production('A' => %w[a A c])
40
- add_production('A' => ['b'])
38
+ add_production('S' => 'A')
39
+ add_production('A' => 'a A c')
40
+ add_production('A' => 'b')
41
41
  end
42
42
 
43
43
  expect(subject.grammar).to be_kind_of(Rley::Syntax::Grammar)
@@ -71,9 +71,9 @@ module Rley # Open this namespace to avoid module qualifier prefixes
71
71
  def add_sample_grammar(anEngine)
72
72
  anEngine.build_grammar do
73
73
  add_terminals('a', 'b', 'c')
74
- add_production('S' => ['A'])
75
- add_production('A' => %w[a A c])
76
- add_production('A' => ['b'])
74
+ add_production('S' => 'A')
75
+ add_production('A' => 'a A c')
76
+ add_production('A' => 'b')
77
77
  end
78
78
  end
79
79
 
@@ -112,7 +112,7 @@ module Rley # Open this namespace to avoid module qualifier prefixes
112
112
  end
113
113
 
114
114
  it 'should handle empty productions' do
115
- builder = Rley::Syntax::GrammarBuilder.new
115
+ builder = Rley::Syntax::BaseGrammarBuilder.new
116
116
  builder.add_terminals('a')
117
117
  builder.add_production('S' => 'A')
118
118
  builder.add_production('A' => 'a')
@@ -162,7 +162,7 @@ module Rley # Open this namespace to avoid module qualifier prefixes
162
162
  let(:problematic_grammar) do
163
163
  # Based on grammar example in book
164
164
  # C. Fisher, R. LeBlanc, "Crafting a Compiler"; page 98
165
- builder = Rley::Syntax::GrammarBuilder.new
165
+ builder = Rley::Syntax::BaseGrammarBuilder.new
166
166
  builder.add_terminals('a', 'b', 'c')
167
167
  builder.add_production('S' => 'A')
168
168
  builder.add_production('S' => 'B')
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ # Load the class under test
6
+ require_relative '../../../lib/rley/notation/grammar_builder'
7
+
8
+ module Rley # Open this namespace to avoid module qualifier prefixes
9
+ module Notation # Open this namespace to avoid module qualifier prefixes
10
+ describe GrammarBuilder do
11
+ context 'Initialization without argument:' do
12
+ it 'could be created without argument' do
13
+ expect { GrammarBuilder.new }.not_to raise_error
14
+ end
15
+
16
+ it 'should have no grammar symbols at start' do
17
+ expect(subject.symbols).to be_empty
18
+ end
19
+
20
+ it 'should have no productions at start' do
21
+ expect(subject.productions).to be_empty
22
+ end
23
+ end # context
24
+
25
+ context 'Initialization with argument:' do
26
+ it 'could be created with a block argument' do
27
+ expect do
28
+ GrammarBuilder.new { nil }
29
+ end.not_to raise_error
30
+ end
31
+
32
+ it 'could have grammar symbols from block argument' do
33
+ instance = GrammarBuilder.new do
34
+ add_terminals('a', 'b', 'c')
35
+ end
36
+ expect(instance.symbols.size).to eq(3)
37
+ end
38
+
39
+ it 'should have no productions at start' do
40
+ expect(subject.productions).to be_empty
41
+ end
42
+ end # context
43
+
44
+ context 'Adding symbols:' do
45
+ it 'should build terminals from their names' do
46
+ subject.add_terminals('a', 'b', 'c')
47
+ expect(subject.symbols.size).to eq(3)
48
+ expect(subject.symbols['a']).to be_kind_of(Syntax::Terminal)
49
+ expect(subject.symbols['a'].name).to eq('a')
50
+ expect(subject.symbols['b']).to be_kind_of(Syntax::Terminal)
51
+ expect(subject.symbols['b'].name).to eq('b')
52
+ expect(subject.symbols['c']).to be_kind_of(Syntax::Terminal)
53
+ expect(subject.symbols['c'].name).to eq('c')
54
+ end
55
+
56
+ it 'should accept already built terminals' do
57
+ a = Syntax::Terminal.new('a')
58
+ b = Syntax::VerbatimSymbol.new('b')
59
+ c = Syntax::Literal.new('c', /c/)
60
+
61
+ subject.add_terminals(a, b, c)
62
+ expect(subject.symbols.size).to eq(3)
63
+ expect(subject.symbols['a']).to eq(a)
64
+ expect(subject.symbols['b']).to eq(b)
65
+ expect(subject.symbols['c']).to eq(c)
66
+ end
67
+ end # context
68
+
69
+ context 'Adding productions:' do
70
+ subject do
71
+ instance = GrammarBuilder.new
72
+ instance.add_terminals('a', 'b', 'c')
73
+ instance
74
+ end
75
+
76
+ it 'should add a valid production' do
77
+ # Case of a rhs representation that consists of one name only
78
+ expect { subject.add_production('S' => 'A') }.not_to raise_error
79
+ expect(subject.productions.size).to eq(1)
80
+ new_prod = subject.productions[0]
81
+ expect(new_prod.lhs).to eq(subject['S'])
82
+ expect(new_prod.rhs[0]).not_to be_nil
83
+ expect(new_prod.rhs[0]).to eq(subject['A'])
84
+
85
+ # Case of rhs with multiple symbols
86
+ subject.add_production('A' => 'a A c')
87
+ expect(subject.productions.size).to eq(2)
88
+ new_prod = subject.productions.last
89
+ expect(new_prod.lhs).to eq(subject['A'])
90
+ expect_rhs = [subject['a'], subject['A'], subject['c']]
91
+ expect(new_prod.rhs.members).to eq(expect_rhs)
92
+
93
+ # GrammarBuilder#rule is an alias of add_production
94
+ subject.rule('A' => 'b')
95
+ expect(subject.productions.size).to eq(3)
96
+ new_prod = subject.productions.last
97
+ expect(new_prod.lhs).to eq(subject['A'])
98
+ expect(new_prod.rhs[0]).to eq(subject['b'])
99
+ end
100
+
101
+ it 'should accept annotated terminals' do
102
+ subject.rule('A' => "a b {match_closest: 'IF' } c")
103
+ expect(subject.productions.size).to eq(1)
104
+ new_prod = subject.productions.last
105
+ expect(new_prod.lhs).to eq(subject['A'])
106
+ expect(new_prod.rhs[0].name).to eq('a')
107
+ expect(new_prod.rhs[0]).to eq(subject['a'])
108
+ expect(new_prod.rhs[1].name).to eq('b')
109
+ expect(new_prod.rhs[2].name).to eq('c')
110
+ expect(new_prod.constraints.size).to eq(1)
111
+ expect(new_prod.constraints[0]).to be_kind_of(Syntax::MatchClosest)
112
+ expect(new_prod.constraints[0].idx_symbol).to eq(1) # b is on position 1
113
+ expect(new_prod.constraints[0].closest_symb).to eq('IF')
114
+ end
115
+
116
+ it 'should support optional symbol' do
117
+ instance = GrammarBuilder.new
118
+ instance.add_terminals('LPAREN', 'RPAREN')
119
+
120
+ instance.rule 'argument_list' => 'LPAREN arguments? RPAREN'
121
+ instance.grammar_complete!
122
+
123
+ # implicitly called: rule('arguments_qmark' => 'arguments').tag suffix_qmark_one
124
+ # implicitly called: rule('arguments_qmark' => '').tag suffix_qmark_none
125
+ expect(instance.productions.size).to eq(3)
126
+ prod_star = instance.productions.select { |prod| prod.lhs.name == 'arguments_qmark' }
127
+ expect(prod_star.size).to eq(2)
128
+ first_prod = instance.productions.first
129
+ expect(first_prod.lhs.name).to eq('argument_list')
130
+ expect(first_prod.rhs.members[1].name).to eq('arguments_qmark')
131
+ end
132
+
133
+ it "should support Kleene's star" do
134
+ instance = GrammarBuilder.new
135
+ instance.add_terminals('EOF')
136
+
137
+ instance.rule 'program' => 'declaration* EOF'
138
+ instance.grammar_complete!
139
+
140
+ # implicitly called: rule('declaration_star' => 'declaration_star declaration').tag suffix_star_more
141
+ # implicitly called: rule('declaration_star' => '').tag suffix_star_last
142
+ expect(instance.productions.size).to eq(3)
143
+ prod_star = instance.productions.select { |prod| prod.lhs.name == 'declaration_star' }
144
+ expect(prod_star.size).to eq(2)
145
+ first_prod = instance.productions.first
146
+ expect(first_prod.lhs.name).to eq('program')
147
+ expect(first_prod.rhs.members[0].name).to eq('declaration_star')
148
+ end
149
+
150
+ it "should support symbols decorated with Kleene's plus" do
151
+ instance = GrammarBuilder.new
152
+ instance.add_terminals('plus', 'minus', 'digit')
153
+
154
+ instance.rule 'integer' => 'value'
155
+ instance.rule 'integer' => 'sign value'
156
+ instance.rule 'sign' => 'plus'
157
+ instance.rule 'sign' => 'minus'
158
+ instance.rule 'value' => 'digit+'
159
+ expect(instance.productions.size).to eq(5)
160
+ instance.grammar_complete!
161
+ expect(instance.productions.size).to eq(7)
162
+
163
+ # implicitly called: rule('digit_plus' => 'digit_plus digit').tag suffix_plus_more
164
+ # implicitly called: rule('digit_plus' => 'digit').tag suffix_plus_last
165
+ expect(instance.productions.size).to eq(7) # Two additional rules generated
166
+ prod_plus = instance.productions.select { |prod| prod.lhs.name == 'digit_plus' }
167
+ expect(prod_plus.size).to eq(2)
168
+ val_prod = instance.productions[4]
169
+ expect(val_prod.lhs.name).to eq('value')
170
+ expect(val_prod.rhs.members[0].name).to eq('digit_plus')
171
+ end
172
+
173
+ it 'should support optional grouping' do
174
+ instance = GrammarBuilder.new
175
+ instance.add_terminals('EQUAL', 'IDENTIFIER', 'VAR')
176
+
177
+ instance.rule 'var_decl' => 'VAR IDENTIFIER (EQUAL expression)?'
178
+ instance.grammar_complete!
179
+
180
+ # implicitly called: rule('seq_EQUAL_expression' => 'EQUAL expression').tag 'return_children'
181
+ # implicitly called: rule('seq_EQUAL_expression_qmark' => 'seq_EQUAL_expression').tag suffix_qmark_one
182
+ # implicitly called: rule('seq_EQUAL_expression_qmark' => '').tag suffix_qmark_none
183
+ expect(instance.productions.size).to eq(4)
184
+ first_prod = instance.productions.first
185
+ expect(first_prod.lhs.name).to eq('var_decl')
186
+ expect(first_prod.rhs.members[2].name).to eq('seq_EQUAL_expression_qmark')
187
+ (p0, p1, p2) = instance.productions[1..3]
188
+ expect(p0.lhs.name).to eq('seq_EQUAL_expression')
189
+ expect(p0.rhs[0].name).to eq('EQUAL')
190
+ expect(p0.rhs[1].name).to eq('expression')
191
+ expect(p0.name).to eq('return_children')
192
+
193
+ expect(p1.lhs.name).to eq('seq_EQUAL_expression_qmark')
194
+ expect(p1.rhs[0].name).to eq('seq_EQUAL_expression')
195
+ expect(p1.name).to eq('_qmark_one')
196
+
197
+ expect(p2.lhs.name).to eq('seq_EQUAL_expression_qmark')
198
+ expect(p2.rhs).to be_empty
199
+ expect(p2.name).to eq('_qmark_none')
200
+ end
201
+
202
+ it 'should support grouping with star modifier' do
203
+ instance = GrammarBuilder.new
204
+ instance.add_terminals('OR')
205
+
206
+ instance.rule 'logic_or' => 'logic_and (OR logic_and)*'
207
+ instance.grammar_complete!
208
+
209
+ # implicitly called: rule('seq_OR_logic_and' => 'OR logic_and').tag 'return_children'
210
+ # implicitly called: rule('seq_EQUAL_expression_star' => 'seq_EQUAL_expression_star seq_EQUAL_expression').tag suffix_star_more
211
+ # implicitly called: rule('seq_EQUAL_expression_star' => '').tag suffix_star_none
212
+ expect(instance.productions.size).to eq(4)
213
+ first_prod = instance.productions.first
214
+ expect(first_prod.lhs.name).to eq('logic_or')
215
+ expect(first_prod.rhs.members[1].name).to eq('seq_OR_logic_and_star')
216
+
217
+ (p0, p1, p2) = instance.productions[1..3]
218
+ expect(p0.lhs.name).to eq('seq_OR_logic_and')
219
+ expect(p0.rhs[0].name).to eq('OR')
220
+ expect(p0.rhs[1].name).to eq('logic_and')
221
+ expect(p0.name).to eq('return_children')
222
+
223
+ expect(p1.lhs.name).to eq('seq_OR_logic_and_star')
224
+ expect(p1.rhs[0].name).to eq('seq_OR_logic_and_star')
225
+ expect(p1.rhs[1].name).to eq('seq_OR_logic_and')
226
+ expect(p1.name).to eq('_star_more')
227
+
228
+ expect(p2.lhs.name).to eq('seq_OR_logic_and_star')
229
+ expect(p2.rhs).to be_empty
230
+ expect(p2.name).to eq('_star_none')
231
+ end
232
+
233
+ it 'should support grouping with plus modifier' do
234
+ instance = GrammarBuilder.new
235
+ instance.add_terminals('POINT TO SEMI_COLON')
236
+
237
+ instance.rule 'path' => 'POINT (TO POINT)+ SEMI_COLON'
238
+ instance.grammar_complete!
239
+
240
+ # implicitly called: rule('seq_TO_POINT' => 'TO POINT').tag 'return_children'
241
+ # implicitly called: rule('seq_TO_POINT_plus' => 'seq_TO_POINT_plus seq_TO_POINT').tag suffix_plus_more
242
+ # implicitly called: rule('seq_TO_POINT_plus' => 'seq_TO_POINT').tag suffix_plus_one
243
+ expect(instance.productions.size).to eq(4)
244
+ first_prod = instance.productions.first
245
+ expect(first_prod.lhs.name).to eq('path')
246
+ expect(first_prod.rhs.members[1].name).to eq('seq_TO_POINT_plus')
247
+
248
+ (p0, p1, p2) = instance.productions[1..3]
249
+ expect(p0.lhs.name).to eq('seq_TO_POINT')
250
+ expect(p0.rhs[0].name).to eq('TO')
251
+ expect(p0.rhs[1].name).to eq('POINT')
252
+ expect(p0.name).to eq('return_children')
253
+
254
+ expect(p1.lhs.name).to eq('seq_TO_POINT_plus')
255
+ expect(p1.rhs[0].name).to eq('seq_TO_POINT_plus')
256
+ expect(p1.rhs[1].name).to eq('seq_TO_POINT')
257
+ expect(p1.name).to eq('_plus_more')
258
+
259
+ expect(p2.lhs.name).to eq('seq_TO_POINT_plus')
260
+ expect(p2.rhs[0].name).to eq('seq_TO_POINT')
261
+ expect(p2.name).to eq('_plus_one')
262
+ end
263
+
264
+ it 'should support grouping with nested annotation' do
265
+ instance = GrammarBuilder.new
266
+ instance.add_terminals('IF ELSE LPAREN RPAREN')
267
+ st = "IF LPAREN expr RPAREN stmt (ELSE { match_closest: 'IF' } stmt)?"
268
+ instance.rule('if_stmt' => st)
269
+ instance.grammar_complete!
270
+
271
+ # implicitly called: rule('seq_ELSE_stmt' => 'ELSE stmt').tag 'return_children'
272
+ # implicitly called: rule('seq_ELSE_stmt_qmark' => 'seq_ELSE_stmt ').tag suffix_plus_more
273
+ # implicitly called: rule('seq_ELSE_stmt_qmark' => '').tag suffix_plus_one
274
+ expect(instance.productions.size).to eq(4)
275
+ first_prod = instance.productions.first
276
+ expect(first_prod.lhs.name).to eq('if_stmt')
277
+ expect(first_prod.rhs.members[5].name).to eq('seq_ELSE_stmt_qmark')
278
+
279
+ (p0, p1, p2) = instance.productions[1..3]
280
+ expect(p0.lhs.name).to eq('seq_ELSE_stmt')
281
+ expect(p0.rhs[0].name).to eq('ELSE')
282
+ expect(p0.rhs[1].name).to eq('stmt')
283
+ expect(p0.name).to eq('return_children')
284
+ expect(p0.constraints.size).to eq(1)
285
+ expect(p0.constraints[0]).to be_kind_of(Syntax::MatchClosest)
286
+ expect(p0.constraints[0].idx_symbol).to eq(0) # ELSE is on position 0
287
+ expect(p0.constraints[0].closest_symb).to eq('IF')
288
+
289
+ expect(p1.lhs.name).to eq('seq_ELSE_stmt_qmark')
290
+ expect(p1.rhs[0].name).to eq('seq_ELSE_stmt')
291
+ expect(p1.name).to eq('_qmark_one')
292
+
293
+ expect(p2.lhs.name).to eq('seq_ELSE_stmt_qmark')
294
+ expect(p2.rhs).to be_empty
295
+ expect(p2.name).to eq('_qmark_none')
296
+ end
297
+ end # context
298
+ end # describe
299
+ end # module
300
+ end # module
301
+
302
+ # End of file