rley 0.7.08 → 0.8.03

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 (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