dendroid 0.1.00 → 0.2.01

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +6 -0
  4. data/lib/dendroid/formatters/ascii_tree.rb +142 -0
  5. data/lib/dendroid/formatters/base_formatter.rb +24 -0
  6. data/lib/dendroid/formatters/bracket_notation.rb +50 -0
  7. data/lib/dendroid/grm_analysis/dotted_item.rb +45 -30
  8. data/lib/dendroid/grm_analysis/grm_analyzer.rb +10 -4
  9. data/lib/dendroid/grm_analysis/{choice_items.rb → rule_items.rb} +10 -10
  10. data/lib/dendroid/grm_dsl/base_grm_builder.rb +3 -4
  11. data/lib/dendroid/parsing/and_node.rb +54 -0
  12. data/lib/dendroid/parsing/chart_walker.rb +301 -0
  13. data/lib/dendroid/parsing/composite_parse_node.rb +21 -0
  14. data/lib/dendroid/parsing/empty_rule_node.rb +28 -0
  15. data/lib/dendroid/parsing/or_node.rb +46 -0
  16. data/lib/dendroid/parsing/parse_node.rb +26 -0
  17. data/lib/dendroid/parsing/parse_tree_visitor.rb +127 -0
  18. data/lib/dendroid/parsing/parser.rb +185 -0
  19. data/lib/dendroid/parsing/terminal_node.rb +32 -0
  20. data/lib/dendroid/parsing/walk_progress.rb +121 -0
  21. data/lib/dendroid/recognizer/chart.rb +3 -0
  22. data/lib/dendroid/recognizer/e_item.rb +21 -2
  23. data/lib/dendroid/recognizer/item_set.rb +7 -2
  24. data/lib/dendroid/recognizer/recognizer.rb +42 -23
  25. data/lib/dendroid/syntax/grammar.rb +5 -1
  26. data/lib/dendroid/syntax/rule.rb +71 -13
  27. data/spec/dendroid/grm_analysis/dotted_item_spec.rb +59 -47
  28. data/spec/dendroid/grm_analysis/{choice_items_spec.rb → rule_items_spec.rb} +5 -6
  29. data/spec/dendroid/parsing/chart_walker_spec.rb +250 -0
  30. data/spec/dendroid/parsing/terminal_node_spec.rb +36 -0
  31. data/spec/dendroid/recognizer/e_item_spec.rb +5 -5
  32. data/spec/dendroid/recognizer/item_set_spec.rb +16 -8
  33. data/spec/dendroid/recognizer/recognizer_spec.rb +56 -5
  34. data/spec/dendroid/support/sample_grammars.rb +2 -2
  35. data/spec/dendroid/syntax/grammar_spec.rb +16 -21
  36. data/spec/dendroid/syntax/rule_spec.rb +56 -7
  37. data/version.txt +1 -1
  38. metadata +20 -13
  39. data/lib/dendroid/grm_analysis/alternative_item.rb +0 -70
  40. data/lib/dendroid/grm_analysis/production_items.rb +0 -55
  41. data/lib/dendroid/syntax/choice.rb +0 -95
  42. data/lib/dendroid/syntax/production.rb +0 -82
  43. data/spec/dendroid/grm_analysis/alternative_item_spec.rb +0 -12
  44. data/spec/dendroid/grm_analysis/production_items_spec.rb +0 -68
  45. data/spec/dendroid/syntax/choice_spec.rb +0 -68
  46. data/spec/dendroid/syntax/production_spec.rb +0 -92
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walk_progress'
4
+
5
+ module Dendroid
6
+ module Parsing
7
+ class ChartWalker
8
+ attr_reader :chart
9
+ attr_reader :last_item
10
+
11
+ # rubocop: disable Metrics/AbcSize
12
+ # rubocop: disable Metrics/CyclomaticComplexity
13
+ # rubocop: disable Metrics/PerceivedComplexity
14
+
15
+ def initialize(theChart)
16
+ @chart = theChart
17
+ end
18
+
19
+ def walk(start_item)
20
+ curr_rank = chart.size - 1
21
+
22
+ parents = []
23
+ progress = WalkProgress.new(curr_rank, start_item, parents)
24
+ paths = [progress]
25
+
26
+ if start_item.predecessors.size > 1
27
+ # Create n times start_item as predecessors, then for each path initialize to its unique own predecessor
28
+ forerunners = disambiguate(progress, start_item.predecessors)
29
+ if forerunners.size == 1
30
+ parents << ANDNode.new(start_item, curr_rank)
31
+ else
32
+ preds = sort_predecessors(forerunners)
33
+ if start_item.rule.rhs.size == 1
34
+ parents << ANDNode.new(start_item, curr_rank)
35
+ progress.push_or_node(start_item.origin, preds.size)
36
+ else
37
+ parents << OrNode.new(start_item.lhs, start_item.origin, curr_rank, preds.size)
38
+ end
39
+ progress.curr_item = start_item
40
+ fork(progress, paths, preds)
41
+ end
42
+ else
43
+ parents << ANDNode.new(start_item, curr_rank)
44
+ end
45
+ token2node = {}
46
+ entry2node = {}
47
+ sharing = {}
48
+ or_nodes_crossed = {}
49
+
50
+ loop do # Iterate over rank values
51
+ pass = :primary
52
+ loop do # Iterate over paths until all are ready for previous rank
53
+ paths.each do |prg|
54
+ next if prg.state == :Complete || prg.state == :Delegating
55
+ next if pass == :secondary && prg.state == :Waiting
56
+
57
+ step_back(prg, paths, token2node, entry2node, sharing, or_nodes_crossed)
58
+ end
59
+ # TODO: handle path removal
60
+ break if paths.none? { |pg| pg.state == :Running || pg.state == :Forking }
61
+
62
+ pass = :secondary
63
+ end
64
+ break if paths.all? { |prg| prg.state == :Complete }
65
+
66
+ entry2node.clear
67
+ end
68
+
69
+ parents[0]
70
+ end
71
+
72
+ def step_back(walk_progress, paths, token2node, entry2node, sharing, or_nodes_crossed)
73
+ loop do
74
+ case walk_progress.state
75
+ when :Waiting, :New
76
+ predecessors = predecessors_of(walk_progress.curr_item, walk_progress.parents)
77
+ last_parent = walk_progress.parents.last
78
+ if sharing.include? last_parent
79
+ delegating = sharing[last_parent]
80
+ unless delegating.include? walk_progress
81
+ delegating.each do |dlg|
82
+ dlg.curr_rank = walk_progress.curr_rank
83
+ dlg.curr_item = walk_progress.curr_item
84
+ dlg.state = :Waiting
85
+ end
86
+ sharing.delete(last_parent)
87
+ end
88
+ end
89
+ walk_progress.state = :Running
90
+
91
+ when :Running
92
+ predecessors = predecessors_of(walk_progress.curr_item, walk_progress.parents)
93
+
94
+ when :Forking
95
+ # predecessors = [walk_progress.curr_item]
96
+ predecessors = [walk_progress.predecessor]
97
+ walk_progress.predecessor = nil
98
+ walk_progress.state = :Running
99
+ end
100
+
101
+ if predecessors.empty?
102
+ walk_progress.state = :Complete
103
+ break
104
+ end
105
+
106
+ case walk_progress.curr_item.algo
107
+ when :completer
108
+ completer_backwards(walk_progress, paths, entry2node, sharing, predecessors)
109
+ break if walk_progress.state == :Delegating
110
+
111
+ when :scanner
112
+ curr_token = chart.tokens[walk_progress.curr_rank - 1]
113
+ if token2node.include? curr_token
114
+ walk_progress.add_child_node(token2node[curr_token])
115
+ walk_progress.curr_rank -= 1
116
+ else
117
+ new_node = walk_progress.add_terminal_node(chart.tokens[walk_progress.curr_rank - 1])
118
+ token2node[curr_token] = new_node
119
+ end
120
+ if predecessors.size == 1
121
+ walk_progress.curr_item = predecessors[0]
122
+ walk_progress.state = :Waiting
123
+ break
124
+ else
125
+ # TODO: challenge assumption single predecessor
126
+ raise StandardError
127
+ end
128
+
129
+ when :predictor
130
+ unless walk_progress.parents.last.partial?
131
+ last_parent = walk_progress.parents.pop
132
+ if sharing.include? last_parent
133
+ delegating = sharing[last_parent]
134
+ unless delegating.include? walk_progress
135
+ delegating.each do |dlg|
136
+ dlg.curr_rank = walk_progress.curr_rank
137
+ dlg.curr_item = walk_progress.curr_item
138
+ dlg.state = :Running
139
+ end
140
+ sharing.delete(last_parent)
141
+ end
142
+ end
143
+ if last_parent.is_a?(OrNode)
144
+ if or_nodes_crossed.include?(last_parent)
145
+ walk_progress.state = :Complete
146
+ break
147
+ else
148
+ or_nodes_crossed[last_parent] = true
149
+ end
150
+ end
151
+ end
152
+ index_empty = predecessors.find_index { |entry| entry.dotted_item.empty? }
153
+ if index_empty
154
+ entry_empty = predecessors.delete_at(index_empty)
155
+ walk_progress.add_node_empty(entry_empty)
156
+ raise StandardError unless predecessors.empty? # Uncovered case
157
+
158
+ walk_progress.curr_item = entry_empty
159
+ next
160
+ end
161
+ if predecessors.size == 1
162
+ walk_progress.curr_item = predecessors[0]
163
+ else
164
+ # curr_item has multiple predecessors from distinct rules
165
+ # look in lineage the latest entry that matches one of the ancestors AND
166
+ # has a free slot for the current symbol
167
+ matches = walk_progress.match_parent?(predecessors, true)
168
+ if matches.empty?
169
+ walk_progress.state = :Complete
170
+ break
171
+ end
172
+ (matching_pred, stack_offset) = matches.first
173
+ walk_progress.curr_item = matching_pred
174
+ unless stack_offset.zero?
175
+ removed = walk_progress.parents.pop(stack_offset)
176
+ if removed.is_a?(Array)
177
+ or_nodes = removed.select { |entry| entry.is_a?(OrNode) }
178
+ unless or_nodes.empty?
179
+ or_nodes.reverse_each do |or_nd|
180
+ if or_nodes_crossed.include?(or_nd)
181
+ walk_progress.state = :Complete
182
+ break
183
+ else
184
+ or_nodes_crossed[or_nd] = true
185
+ end
186
+ end
187
+ break if walk_progress.state == :Complete
188
+
189
+ end
190
+ elsif removed.is_a?(OrNode)
191
+ if or_nodes_crossed.include?(removed)
192
+ walk_progress.state = :Complete
193
+ break
194
+ else
195
+ or_nodes_crossed[removed] = true
196
+ end
197
+ end
198
+ end
199
+ end
200
+ else
201
+ raise StandardError
202
+ end
203
+ end
204
+
205
+ walk_progress
206
+ end
207
+
208
+ def disambiguate(_progress, predecessors)
209
+ predecessors
210
+ end
211
+
212
+ def sort_predecessors(predecessors)
213
+ predecessors
214
+ end
215
+
216
+ def predecessors_of(anEItem, parents)
217
+ # Rule: if anEItem has itself as predecessor AND parents contains
218
+ # only a start item, then remove anEItem from its own predecessor(s).
219
+ if (parents.size == 1) && anEItem.predecessors.include?(anEItem)
220
+ # raise StandardError unless parents[0].match(anEItem)
221
+ unless parents[0].match(anEItem)
222
+ raise StandardError
223
+
224
+ end
225
+ preds = anEItem.predecessors.dup
226
+ preds.delete(anEItem)
227
+ preds
228
+ else
229
+ anEItem.predecessors
230
+ end
231
+ end
232
+
233
+ def completer_backwards(walk_progress, paths, entry2node, sharing, predecessors)
234
+ # Trying to remove some predecessors with some disambiguation technique
235
+ forerunners = disambiguate(walk_progress, predecessors)
236
+
237
+ if forerunners.size == 1
238
+ pred = forerunners[0]
239
+ if entry2node.include? pred
240
+ shared_node = entry2node[pred]
241
+ if sharing.include? shared_node
242
+ sharing[shared_node] << walk_progress
243
+ else
244
+ sharing[shared_node] = [walk_progress]
245
+ end
246
+ walk_progress.add_child_node(shared_node)
247
+ walk_progress.parents.push(shared_node)
248
+ walk_progress.state = :Delegating
249
+
250
+ elsif pred.predecessors.size == 1
251
+ new_node = walk_progress.push_and_node(pred)
252
+ walk_progress.curr_item = pred
253
+ entry2node[pred] = new_node
254
+ else
255
+ pre_forerunners = disambiguate(walk_progress, pred.predecessors)
256
+ index_empty = pre_forerunners.find_index { |entry| entry.dotted_item.empty? }
257
+ if index_empty
258
+ entry_empty = pre_forerunners.delete_at(index_empty)
259
+ walk_progress.add_node_empty(entry_empty)
260
+ walk_progress.curr_item = entry_empty.predecessors[0] # Assuming only one predecessor
261
+ end
262
+ if pre_forerunners.size == 1
263
+ pred = forerunners[0]
264
+ new_node = walk_progress.push_and_node(pre_forerunners[0])
265
+ walk_progress.curr_item = pred
266
+ entry2node[pred] = new_node
267
+ else
268
+ prepreds = sort_predecessors(pre_forerunners)
269
+ new_node = walk_progress.push_or_node(pred.origin, prepreds.size)
270
+ walk_progress.curr_item = pred
271
+ entry2node[pred] = new_node
272
+ fork(walk_progress, paths, prepreds)
273
+ end
274
+ end
275
+ else
276
+ # AMBIGUITY: multiple valid predecessors
277
+ preds = sort_predecessors(forerunners)
278
+ walk_progress.push_or_node(preds)
279
+ fork(walk_progress, paths, preds)
280
+ end
281
+ end
282
+
283
+ def fork(walk_progress, paths, sorted_predecessors)
284
+ progs = [walk_progress]
285
+ walk_progress.fork(sorted_predecessors[0])
286
+ sorted_predecessors[1..].each do |prd|
287
+ alternate = walk_progress.dup
288
+ alternate.fork(prd)
289
+ paths << alternate
290
+ progs << alternate
291
+ end
292
+
293
+ progs.each { |pg| pg.push_and_node(pg.curr_item) }
294
+ end
295
+ end # class
296
+
297
+ # rubocop: enable Metrics/AbcSize
298
+ # rubocop: enable Metrics/CyclomaticComplexity
299
+ # rubocop: enable Metrics/PerceivedComplexity
300
+ end # module
301
+ end # module
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parse_node'
4
+
5
+ module Dendroid
6
+ module Parsing
7
+ class CompositeParseNode < ParseNode
8
+ attr_reader :range
9
+ attr_reader :children
10
+
11
+ def initialize(lowerBound, upperBound, child_count)
12
+ super(lowerBound, upperBound)
13
+ @children = Array.new(child_count, nil)
14
+ end
15
+
16
+ def add_child(child_node, index)
17
+ children[index] = child_node
18
+ end
19
+ end # class
20
+ end # module
21
+ end # module
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parse_node'
4
+
5
+ module Dendroid
6
+ module Parsing
7
+ class EmptyRuleNode < ParseNode
8
+ attr_reader :rule
9
+ attr_reader :alt_index
10
+
11
+ def initialize(anEItem, rank)
12
+ super(rank, rank)
13
+ @rule = WeakRef.new(anEItem.dotted_item.rule)
14
+ @alt_index = anEItem.dotted_item.alt_index
15
+ end
16
+
17
+ def to_s
18
+ "_ #{super}"
19
+ end
20
+
21
+ # Part of the 'visitee' role in Visitor design pattern.
22
+ # @param aVisitor[ParseTreeVisitor] the visitor
23
+ def accept(aVisitor)
24
+ aVisitor.visit_empty_rule_node(self)
25
+ end
26
+ end # class
27
+ end # module
28
+ end # module
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'composite_parse_node'
4
+
5
+ module Dendroid
6
+ module Parsing
7
+ class OrNode < CompositeParseNode
8
+ attr_reader :symbol
9
+
10
+ def initialize(sym, lower, upper, arity)
11
+ @symbol = sym
12
+ super(lower, upper, arity)
13
+ end
14
+
15
+ def add_child(child_node, _index)
16
+ idx = children.find_index(&:nil?)
17
+ raise StandardError unless idx
18
+
19
+ # Use first found available slot...
20
+ super(child_node, idx)
21
+ end
22
+
23
+ def match(anEItem)
24
+ return false if range[0] != anEItem.origin
25
+
26
+ dotted = anEItem.dotted_item
27
+ (symbol == dotted.rule.lhs) && children.any? { |ch| ch.match(anEItem) }
28
+ end
29
+
30
+ def partial?
31
+ # children.any?(&:nil?)
32
+ false
33
+ end
34
+
35
+ def to_s
36
+ "OR: #{symbol.name} #{range}"
37
+ end
38
+
39
+ # Part of the 'visitee' role in Visitor design pattern.
40
+ # @param aVisitor[ParseTreeVisitor] the visitor
41
+ def accept(aVisitor)
42
+ aVisitor.visit_or_node(self)
43
+ end
44
+ end # class
45
+ end # module
46
+ end # module
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dendroid
4
+ module Parsing
5
+ class ParseNode
6
+ # @return [Array<Integer>] The range of input tokens that match this node.
7
+ attr_reader :range
8
+
9
+ def initialize(lowerBound, upperBound)
10
+ @range = valid_range(lowerBound, upperBound)
11
+ end
12
+
13
+ def to_s
14
+ "[#{range[0]}, #{range[1]}]"
15
+ end
16
+
17
+ private
18
+
19
+ def valid_range(lowerBound, upperBound)
20
+ raise StandardError unless lowerBound.is_a?(Integer) && upperBound.is_a?(Integer)
21
+
22
+ [lowerBound, upperBound]
23
+ end
24
+ end # class
25
+ end # module
26
+ end # module
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ParseTreeVisitor
4
+ # Link to the result root node of the tree (forest)
5
+ attr_reader(:root)
6
+
7
+ # List of objects that subscribed to the visit event notification.
8
+ attr_reader(:subscribers)
9
+
10
+ # Indicates the kind of tree traversal to perform: :post_order, :pre-order
11
+ attr_reader(:traversal)
12
+
13
+ # Build a visitor for the given root.
14
+ # @param aParseTree [ParseTree] the parse tree to visit.
15
+ def initialize(aParseTree, aTraversalStrategy = :post_order)
16
+ raise StandardError if aParseTree.nil?
17
+
18
+ @root = aParseTree
19
+ @subscribers = []
20
+ @traversal = aTraversalStrategy
21
+ end
22
+
23
+ # Add a subscriber for the visit event notifications.
24
+ # @param aSubscriber [Object]
25
+ def subscribe(aSubscriber)
26
+ subscribers << aSubscriber
27
+ end
28
+
29
+ # Remove the given object from the subscription list.
30
+ # The object won't be notified of visit events.
31
+ # @param aSubscriber [Object]
32
+ def unsubscribe(aSubscriber)
33
+ subscribers.delete_if { |entry| entry == aSubscriber }
34
+ end
35
+
36
+ # The signal to begin the visit of the parse tree.
37
+ def start
38
+ root.accept(self)
39
+ end
40
+
41
+ # Visit event. The visitor is about to visit the root.
42
+ # @param aParseTree [ParseTree] the root to visit.
43
+ def start_visit_root(aParseTree)
44
+ broadcast(:before_root, aParseTree)
45
+ end
46
+
47
+ # Visit event. The visitor is about to visit the given non terminal node.
48
+ # @param aNonTerminalNode [ANDNode] the node to visit.
49
+ def visit_and_node(aNonTerminalNode)
50
+ if @traversal == :post_order
51
+ broadcast(:before_and_node, aNonTerminalNode)
52
+ traverse_subnodes(aNonTerminalNode)
53
+ else
54
+ traverse_subnodes(aNonTerminalNode)
55
+ broadcast(:before_and_node, aNonTerminalNode)
56
+ end
57
+ broadcast(:after_and_node, aNonTerminalNode)
58
+ end
59
+
60
+ # Visit event. The visitor is about to visit the given non terminal node.
61
+ # @param aNonTerminalNode [OrNode] the node to visit.
62
+ def visit_or_node(aNonTerminalNode)
63
+ if @traversal == :post_order
64
+ broadcast(:before_or_node, aNonTerminalNode)
65
+ traverse_subnodes(aNonTerminalNode)
66
+ else
67
+ traverse_subnodes(aNonTerminalNode)
68
+ broadcast(:before_or_node, aNonTerminalNode)
69
+ end
70
+ broadcast(:after_or_node, aNonTerminalNode)
71
+ end
72
+
73
+ # Visit event. The visitor is visiting the
74
+ # given terminal node.
75
+ # @param anEmptyRuleNode [EmptyRuleNode] the node to visit.
76
+ def visit_empty_rule_node(anEmptyRuleNode)
77
+ broadcast(:before_empty_rule_node, anEmptyRuleNode)
78
+ broadcast(:after_empty_rule_node, anEmptyRuleNode)
79
+ end
80
+
81
+ # Visit event. The visitor is visiting the
82
+ # given terminal node.
83
+ # @param aTerminalNode [TerminalNode] the terminal to visit.
84
+ def visit_terminal(aTerminalNode)
85
+ broadcast(:before_terminal, aTerminalNode)
86
+ broadcast(:after_terminal, aTerminalNode)
87
+ end
88
+
89
+ # Visit event. The visitor has completed its visit of the given
90
+ # non-terminal node.
91
+ # @param aNonTerminalNode [NonTerminalNode] the node to visit.
92
+ def end_visit_nonterminal(aNonTerminalNode)
93
+ broadcast(:after_and_node, aNonTerminalNode)
94
+ end
95
+
96
+ # Visit event. The visitor has completed the visit of the root.
97
+ # @param aParseTree [ParseTree] the root to visit.
98
+ def end_visit_root(aParseTree)
99
+ broadcast(:after_root, aParseTree)
100
+ end
101
+
102
+ private
103
+
104
+ # Visit event. The visitor is about to visit the subnodes of a non
105
+ # terminal node.
106
+ # @param aParentNode [NonTeminalNode] the (non-terminal) parent node.
107
+ def traverse_subnodes(aParentNode)
108
+ subnodes = aParentNode.children
109
+ broadcast(:before_subnodes, aParentNode, subnodes)
110
+
111
+ # Let's proceed with the visit of subnodes
112
+ subnodes.each { |a_node| a_node.accept(self) }
113
+
114
+ broadcast(:after_subnodes, aParentNode, subnodes)
115
+ end
116
+
117
+ # Send a notification to all subscribers.
118
+ # @param msg [Symbol] event to notify
119
+ # @param args [Array] arguments of the notification.
120
+ def broadcast(msg, *args)
121
+ subscribers.each do |subscr|
122
+ next unless subscr.respond_to?(msg) || subscr.respond_to?(:accept_all)
123
+
124
+ subscr.send(msg, *args)
125
+ end
126
+ end
127
+ end # class