ebnf 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,8 +19,8 @@ module EBNF
19
19
 
20
20
  # Consolodate equivalent terminal rules
21
21
  to_rewrite = {}
22
- new_ast.select {|r| r.kind == :terminal}.each do |src_rule|
23
- new_ast.select {|r| r.kind == :terminal}.each do |dst_rule|
22
+ new_ast.select {|r| r.terminal?}.each do |src_rule|
23
+ new_ast.select {|r| r.terminal?}.each do |dst_rule|
24
24
  if src_rule.equivalent?(dst_rule) && src_rule != dst_rule
25
25
  debug("make_bnf") {"equivalent rules: #{src_rule.inspect} and #{dst_rule.inspect}"}
26
26
  (to_rewrite[src_rule] ||= []) << dst_rule
@@ -48,143 +48,104 @@ module EBNF
48
48
  start_rule.start = true
49
49
  end
50
50
 
51
- # Comprehnsion rule, create shorter versions of all non-terminal sequences
51
+ # Comprehnsion rule, create shorter versions of all non-terminal sequences. This is necessary as the FF rules reference w', which is a comprehension.
52
52
  comprehensions = []
53
53
  ittr = 0
54
54
  depth do
55
55
  begin
56
56
  comprehensions = []
57
- ast.select {|r| r.seq? && r.kind == :rule && r.expr.length > 2}.each do |rule|
57
+ ast.select {|r| r.rule? && r.seq? && r.comp.nil? && r.expr.length > 2}.each do |rule|
58
58
  new_expr = rule.expr[2..-1].unshift(:seq)
59
- unless ast.any? {|r| r.expr == new_expr}
60
- debug("FF.c") {"(#{ittr}) add comprehension rule for #{rule.sym} => #{new_expr.inspect}"}
59
+ if new_rule = ast.detect {|r| r.expr == new_expr}
60
+ # Link to existing comprehension used for another rules
61
+ debug("FF.c") {"(#{ittr}) link comprehension rule for #{rule.sym} => #{new_rule.sym}[#{new_expr.inspect}]"}
62
+ else
61
63
  new_rule = rule.build(new_expr)
62
- rule.comp = new_rule
64
+ debug("FF.c") {"(#{ittr}) add comprehension rule for #{rule.sym} => #{new_rule.sym}[#{new_expr.inspect}]"}
63
65
  comprehensions << new_rule
64
66
  end
67
+ rule.comp = new_rule
65
68
  end
66
-
69
+
67
70
  @ast += comprehensions
68
71
  progress("FF.c") {"(#{ittr}) comprehensions #{comprehensions.length}"}
72
+ #require 'debugger'; breakpoint
69
73
  ittr += 1
70
74
  end while !comprehensions.empty?
71
75
 
72
- # Fi(a w' ) = { a } for every terminal a
73
- # For each rule who's expr's first element of a seq a terminal, or having any element of alt a terminal, add that terminal to the first set for this rule
74
- each(:rule) do |rule|
75
- each(:terminal) do |terminal|
76
- if rule.starts_with?(terminal.sym)
77
- debug("FF.t") {"(0) add first #{terminal.sym} to #{rule.sym}"}
78
- rule.add_first([terminal.sym])
79
- end
80
- end
81
-
82
- # Add strings to first for strings which are start elements
83
- start_strs = rule.starts_with?(String)
84
- if start_strs
85
- debug("FF.t") {"(0) add firsts #{start_strs.join(", ")} to #{rule.sym}"}
86
- rule.add_first(start_strs)
87
- end
88
- end
89
-
90
- # # Fi(ε) = { ε }
91
- # Add _eps as a first of _empty
92
- find_rule(:_empty).add_first([:_eps])
93
-
94
- # Loop until no more first elements are added
95
- firsts, follows, ittr = 0, 0, 0
76
+ ittr = 0
96
77
  begin
97
78
  firsts, follows = 0, 0
98
- each(:rule) do |rule|
99
- each(:rule) do |first_rule|
100
- next if first_rule == rule || first_rule.first.nil?
101
-
102
- # Fi(A w' ) = Fi(A) for every nonterminal A with ε not in Fi(A)
103
- # For each rule that starts with another rule having firsts which don't include _eps, add the firsts of that rule to this rule, unless it already has those terminals in its first.
104
- # Note that it's simpler to promote all fi(A) to fi(A w') and exclude _eps, as this covers corner cases of the following rule.
105
- if rule.starts_with?(first_rule.sym) && first_rule.first != [:_eps]
106
- debug("FF.1") {"(#{ittr}) add first #{first_rule.first.inspect} from #{first_rule.sym} to #{rule.sym}"}
107
- firsts += rule.add_first(first_rule.first - [:_eps])
108
- end
109
-
110
- # Fi(A w' ) = Fi(A) \ { ε } ∪ Fi(w' ) for every nonterminal A with ε in Fi(A)
111
- # For each rule starting with eps, add the terminals for the comprehension of this rule
112
- if rule.seq? &&
113
- rule.expr.fetch(1, nil) == first_rule.sym &&
114
- first_rule.first_includes_eps? &&
115
- (comp = rule.comp) &&
116
- comp.first &&
117
- !(comp.first - [:_eps]).empty?
79
+ # add Fi(wi) to Fi(Ai) for every rule Ai → wi
80
+ #
81
+ # For sequences, this is the first rule in the sequence.
82
+ # For alts, this is every rule in the sequence
83
+ each(:rule) do |ai|
84
+ # Fi(a w' ) = { a } for every terminal a
85
+ ai.terminals(ast).each do |t|
86
+ debug("Fi.2.1") {"(#{ittr}) add terminal #{t} to #{ai.sym}"}
87
+ firsts += ai.add_first([t])
88
+ end
118
89
 
119
- to_add = comp.first - [:_eps]
120
- debug("FF.2") {"(#{ittr}) add first #{to_add.inspect} from #{comp.sym} to #{rule.sym}"}
121
- firsts += rule.add_first(to_add)
90
+ ai.non_terminals(ast).select(&:first).each do |a|
91
+ if !a.first_includes_eps?
92
+ # Fi(A w' ) = Fi(A) for every nonterminal A with ε not in Fi(A)
93
+ debug("Fi.2.2") {"(#{ittr}) add first from #{a.sym} to #{ai.sym}: #{a.first.inspect}"}
94
+ firsts += ai.add_first(a.first)
95
+ else
96
+ # Fi(A w' ) = Fi(A) \ { ε } ∪ Fi(w' ) for every nonterminal A with ε in Fi(A)
97
+ if ai.seq?
98
+ # w' is either comprehnsion of ai, or empty, if there is no comprehension
99
+ comp = ai.comp || find_rule(:_empty)
100
+
101
+ fi = a.first - [:_eps] + (comp.first || [])
102
+ debug("Fi.2.3a") {"(#{ittr}) add first #{fi.inspect} from #{a.sym} and #{comp.sym} to #{ai.sym}"}
103
+ firsts += ai.add_first(fi)
104
+ else
105
+ # ai is an alt, so there are no comprehensions of non-terminals, add Fi(A) including ε
106
+ debug("Fi.2.3b") {"(#{ittr}) add first #{a.first} from #{a.sym} to #{ai.sym}"}
107
+ firsts += ai.add_first(a.first)
108
+ end
122
109
  end
123
110
  end
111
+ end
124
112
 
125
- # Only run these rules if the rule is a sequence having two or more elements, whos first element is also a sequence and first_rule is the comprehension of rule
126
- if rule.seq? && (comp = rule.comp)
127
- #if there is a rule of the form Aj → wAiw' , then
128
- #
129
- if (ai = find_rule(rule.expr[1])) && ai.kind == :rule && comp.first
130
- # * if the terminal a is in Fi(w' ), then add a to Fo(Ai)
131
- #
132
- # Add follow terminals based on the first terminals
133
- # of a comprehension of this rule (having the same
134
- # sequence other than the first rule in the sequence)
135
- #
136
- # @example
137
- # rule: (seq a b c)
138
- # first_rule: (seq b c)
139
- # if first_rule.first == [T]
140
- # => a.follow += [T]
141
- debug("FF.3") {"(#{ittr}) add follow #{comp.first.inspect} from #{comp.sym} to #{ai.sym}"}
113
+ # # Fi(ε) = { ε }
114
+ # Add _eps as a first of _empty
115
+ find_rule(:_empty).add_first([:_eps])
116
+
117
+ # Add follows
118
+ # if there is a rule of the form Aj wAiw' , then
119
+ # First do this for the case when Ai is the first rule
120
+ each(:rule) do |aj|
121
+ comp = aj.comp || find_rule(:_empty)
122
+ aj.non_terminals(ast).reject {|r| r.sym == :_empty}.each do |ai|
123
+ # if the terminal a is in Fi(w' ), then add a to Fo(Ai)
124
+ # Basically, this says that the firsts of a comprehension of a rule are the follows of the first non-terminal in the rule.
125
+ if comp.first
126
+ debug("Fo.2.1") {"(#{ittr}) add follow #{comp.first.inspect} from #{comp.sym} to #{ai.sym}"}
142
127
  follows += ai.add_follow(comp.first)
143
128
  end
144
129
 
145
- # Follows of a rule are also follows of the comprehension of the rule.
146
- if rule.follow
147
- debug("FF.4") {"(#{ittr}) add follow #{rule.follow.inspect} to from #{rule.sym} #{comp.sym}"}
148
- follows += comp.add_follow(rule.follow)
130
+ # If there is no comprehension of this rule (meaning, it is a sequence of one non-terminal), then the follows of the non-terminal include the follows of the rule. This handles rules with multiple sequences because it will have a comprehension that includes the last element in the sequence
131
+ #require 'debugger'; breakpoint if ai.sym == :_predicateObjectList_1 && aj.sym == :_predicateObjectList_7
132
+ if !aj.comp && aj.follow
133
+ debug("Fo.2.1a") {"(#{ittr}) add follow #{aj.follow.inspect} from #{aj.sym} to #{ai.sym}"}
134
+ follows += ai.add_follow(aj.follow)
149
135
  end
150
136
 
151
- # * if ε is in Fi(w' ), then add Fo(Aj) to Fo(Ai)
152
- #
153
- # If the comprehension of a sequence has an _eps first, then the follows of the rule also become the follows of the first member of the rule
154
- if comp.first && comp.first.include?(:_eps) && rule.first &&
155
- (member = find_rule(rule.expr.fetch(1, nil))) &&
156
- member.kind == :rule
157
-
158
- debug("FF.5") {"(#{ittr}) add follow #{rule.follow.inspect} from #{rule.sym} to #{member.sym}"}
159
- follows += member.add_follow(rule.first)
160
- end
161
- end
162
-
163
- # Firsts of elements of an alt are firsts of the alt
164
- if rule.alt?
165
- rule.expr[1..-1].map {|s| find_rule(s)}.compact.select(&:first).each do |mem|
166
- debug("FF.6") {"(#{ittr}) add first #{mem.first.inspect} from #{mem.sym} to #{rule.sym}"}
167
- rule.add_first(mem.first)
137
+ # if ε is in Fi(w' ), then add Fo(Aj) to Fo(Ai)
138
+ if comp.first_includes_eps? && aj.follow
139
+ debug("Fo.2.2") {"(#{ittr}) add follow #{aj.follow.inspect} from #{aj.sym} to #{ai.sym}"}
140
+ follows += ai.add_follow(aj.follow)
168
141
  end
169
142
  end
170
143
 
171
- # Follows of a rule are also follows of the last production in the rule
172
- if rule.seq? && rule.follow &&
173
- (member = find_rule(rule.expr.last)) &&
174
- member.kind == :rule
175
-
176
- debug("FF.7") {"(#{ittr}) add follow #{rule.follow.inspect} to #{member.sym}"}
177
- follows += member.add_follow(rule.follow)
178
- end
179
-
180
- # For alts, anything that follows the rule follows each member of the rule
181
- if rule.alt? && rule.follow
182
- rule.expr[1..-1].map {|s| find_rule(s)}.each do |mem|
183
- if mem && mem.kind == :rule
184
- debug("FF.8") {"(#{ittr}) add follow #{rule.first.inspect} to #{mem.sym}"}
185
- follows += mem.add_follow(rule.follow)
186
- end
187
- end
144
+ # Since the rules are of the form wAiw', and we've handled the case which is just Aiw', this leaves those cases that have rules prior to Ai. This basically says that the follows of a rule are added to the follows of the comprehension of the rule
145
+ #require 'debugger'; breakpoint if aj.sym == :_predicateObjectList_6 && aj.follow
146
+ if aj.comp && aj.follow
147
+ debug("Fo.2.3") {"(#{ittr}) add follow #{aj.follow.inspect} from #{aj.sym} to #{aj.comp.sym}"}
148
+ follows += aj.comp.add_follow(aj.follow)
188
149
  end
189
150
  end
190
151
 
@@ -198,8 +159,8 @@ module EBNF
198
159
  # Generate parser tables, {#branch}, {#first}, {#follow}, and {#terminals}
199
160
  def build_tables
200
161
  progress("build_tables") {
201
- "Terminals: #{ast.count {|r| r.kind == :terminal}} " +
202
- "Non-Terminals: #{ast.count {|r| r.kind == :rule}}"
162
+ "Terminals: #{ast.count {|r| r.terminal?}} " +
163
+ "Non-Terminals: #{ast.count {|r| r.rule?}}"
203
164
  }
204
165
 
205
166
  @first = ast.
@@ -211,13 +172,13 @@ module EBNF
211
172
  @follow = ast.
212
173
  select(&:follow).
213
174
  inject({}) {|memo, r|
214
- memo[r.sym] = r.first if r.first
175
+ memo[r.sym] = r.follow if r.follow
215
176
  memo
216
177
  }
217
178
  @terminals = ast.map do |r|
218
179
  (r.first || []) + (r.follow || [])
219
180
  end.flatten.uniq
220
- @terminals = (@terminals - [:_eps, :_eof, :_empty]).sort_by(&:inspect)
181
+ @terminals = (@terminals - [:_eps, :_eof]).sort_by{|t| t.to_s.sub(/^_/, '')}
221
182
 
222
183
  @branch = {}
223
184
  @already = []
@@ -250,14 +211,14 @@ module EBNF
250
211
 
251
212
  if table.is_a?(Hash)
252
213
  io.puts "#{ind0}#{name} = {"
253
- table.keys.sort_by(&:inspect).each do |prod|
214
+ table.keys.sort_by{|t| t.to_s.sub(/^_/, '')}.each do |prod|
254
215
  case table[prod]
255
216
  when Array
256
217
  list = table[prod].map(&:inspect).join(",\n#{ind2}")
257
218
  io.puts "#{ind1}#{prod.inspect} => [\n#{ind2}#{list}],"
258
219
  when Hash
259
220
  io.puts "#{ind1}#{prod.inspect} => {"
260
- table[prod].keys.sort_by(&:inspect).each do |term|
221
+ table[prod].keys.sort_by{|t| t.to_s.sub(/^_/, '')}.each do |term|
261
222
  list = table[prod][term].map(&:inspect).join(", ")
262
223
  io.puts "#{ind2}#{term.inspect} => [#{list}],"
263
224
  end
@@ -269,7 +230,7 @@ module EBNF
269
230
  io.puts "#{ind0}}.freeze\n"
270
231
  else
271
232
  io.puts "#{ind0}#{name} = [\n#{ind1}" +
272
- table.sort_by(&:inspect).map(&:inspect).join(",\n#{ind1}") +
233
+ table.sort_by{|t| t.to_s.sub(/^_/, '')}.map(&:inspect).join(",\n#{ind1}") +
273
234
  "\n#{ind0}].freeze\n"
274
235
  end
275
236
  end
@@ -277,7 +238,7 @@ module EBNF
277
238
  private
278
239
  def do_production(lhs)
279
240
  rule = find_rule(lhs)
280
- if rule.nil? || rule.kind != :rule || rule.sym == :_empty
241
+ if rule.nil? || !rule.rule? || rule.sym == :_empty
281
242
  progress("prod") {"Skip: #{lhs.inspect}"}
282
243
  return
283
244
  end
@@ -302,12 +263,12 @@ module EBNF
302
263
  @agenda << prod unless @already.include?(prod) || @agenda.include?(prod)
303
264
  if prod == :_empty
304
265
  debug(" empty")
305
- branchDict[prod] = []
266
+ # Skip empty, rules added bellow for follows
306
267
  elsif prod_rule.nil? || prod_rule.first.nil?
307
268
  debug(" no first =>", prod)
308
269
  branchDict[prod] = [prod]
309
270
  else
310
- prod_rule.first.each do |f|
271
+ prod_rule.first.reject{|f| f == :_eps}.each do |f|
311
272
  if branchDict.has_key?(f)
312
273
  error("First/First Conflict: #{f} is also the condition for #{branchDict[f]}")
313
274
  end
@@ -321,8 +282,12 @@ module EBNF
321
282
  debug(" Seq", rule)
322
283
  # Entries for each first element referencing the sequence
323
284
  (rule.first || []).each do |f|
324
- debug(" seq") {"[#{f}] => #{rule.expr[1..-1].inspect}"}
325
- branchDict[f] = rule.expr[1..-1]
285
+ if [:_eps, :_eof].include?(f)
286
+ # Skip eps/eof, rules added below for follows
287
+ else
288
+ debug(" seq") {"[#{f}] => #{rule.expr[1..-1].inspect}"}
289
+ branchDict[f] = rule.expr[1..-1]
290
+ end
326
291
  end
327
292
 
328
293
  # Add each production to the agenda
@@ -330,11 +295,13 @@ module EBNF
330
295
  @agenda << prod unless @already.include?(prod) || @agenda.include?(prod)
331
296
  end
332
297
  end
333
-
334
- # Add follow rules
335
- (rule.follow || []).each do |f|
336
- debug(" Follow") {f.inspect}
337
- branchDict[f] ||= []
298
+
299
+ # Add follow rules, if first includes eps
300
+ if rule.first_includes_eps?
301
+ (rule.follow || []).reject {|f| f == :_eof}.each do |f|
302
+ debug(" Follow") {f.inspect}
303
+ branchDict[f] ||= []
304
+ end
338
305
  end
339
306
 
340
307
  @branch[lhs] = branchDict
@@ -282,7 +282,7 @@ module EBNF::LL1
282
282
  debug("parse(token)") {"token #{token.inspect}, term #{term.inspect}"}
283
283
  onToken(term, token)
284
284
  elsif terminals.include?(term)
285
- # If term is a terminal, then it is an error of token does not
285
+ # If term is a terminal, then it is an error if token does not
286
286
  # match it
287
287
  skip_until_valid(todo_stack)
288
288
  else
@@ -301,7 +301,7 @@ module EBNF::LL1
301
301
  while !pushed &&
302
302
  !todo_stack.empty? &&
303
303
  ( (terms = todo_stack.last.fetch(:terms, [])).empty? ||
304
- (@recovering && @follow.fetch(terms.last, []).none? {|t| token == t}))
304
+ (@recovering && @follow.fetch(terms.last, []).none? {|t| (token || :_eps) == t}))
305
305
  debug("parse(pop)", :level => 2) {"todo #{todo_stack.last.inspect}, depth #{depth}"}
306
306
  if terms.empty?
307
307
  prod = todo_stack.last[:prod]
@@ -403,7 +403,6 @@ module EBNF::LL1
403
403
  self.class.eval_with_binding(self) {
404
404
  handler.call(@prod_data.last, data, @parse_callback)
405
405
  }
406
- #require 'debugger'; breakpoint
407
406
  progress("#{prod}(:finish):#{@prod_data.length}") {@prod_data.last}
408
407
  else
409
408
  progress("#{prod}(:finish)", "recovering: #{@recovering.inspect}")
@@ -440,18 +439,20 @@ module EBNF::LL1
440
439
  cur_prod = todo_stack.last[:prod]
441
440
  token = get_token
442
441
  first = @first[cur_prod] || []
443
-
442
+ expected = @branch.fetch(cur_prod, {}).keys
443
+ expected << :_eps if first.include?(:_eps) # Helps when testing
444
+
445
+ # If we've reached EOF, token is nil. This is fine, if _eof is in @follow
446
+ return if token.nil? && @follow.fetch(cur_prod, []).include?(:_eof)
447
+
444
448
  # If this token can be used by the top production, return it
445
449
  # Otherwise, if the banch table allows empty, also return the token
446
- return token if !@recovering && (
447
- (@branch[cur_prod] && @branch[cur_prod].has_key?(:_empty)) ||
448
- first.any? {|t| token === t})
449
-
450
+ return token if !@recovering && (expected.any? {|t| (token || :_eps) === t})
451
+
450
452
  # Otherwise, it's an error condition, and skip either until
451
453
  # we find a valid token for this production, or until we find
452
454
  # something that can follow this production
453
- expected = first.map {|v| v.inspect}.join(", ")
454
- error("skip_until_valid", "expected one of #{expected}",
455
+ error("skip_until_valid", "expected one of #{expected.map(&:inspect).join(", ")}, found #{token.inspect}",
455
456
  :production => cur_prod, :token => token)
456
457
 
457
458
  debug("recovery", "stack follows:")
@@ -495,6 +496,7 @@ module EBNF::LL1
495
496
  @error_log << message unless @recovering
496
497
  @recovering = true
497
498
  debug(node, message, options.merge(:level => 0))
499
+ $stderr.puts("[#{@lineno}]#{' ' * depth}#{node}: #{message}")
498
500
  end
499
501
 
500
502
  ##
@@ -17,6 +17,9 @@ module EBNF
17
17
  cur_lineno += s.count("\n")
18
18
  #debug("eachRule(ws)") { "[#{cur_lineno}] #{s.inspect}" }
19
19
  when s = scanner.scan(%r(/\*([^\*]|\*[^\/])*\*/)m)
20
+ # Eat comments
21
+ debug("eachRule(comment)") { "[#{cur_lineno}] #{s.inspect}" }
22
+ when s = scanner.scan(%r((#|//).*$))
20
23
  # Eat comments
21
24
  cur_lineno += s.count("\n")
22
25
  debug("eachRule(comment)") { "[#{cur_lineno}] #{s.inspect}" }
@@ -32,7 +35,7 @@ module EBNF
32
35
  yield r unless r.empty?
33
36
  @lineno = cur_lineno
34
37
  r = s
35
- when s = scanner.scan(/\[(?=\w+\])/)
38
+ when s = scanner.scan(/\[(?=[\w\.]+\])/)
36
39
  # Found rule start, if we've already collected a rule, yield it
37
40
  yield r unless r.empty?
38
41
  #debug("eachRule(rule)") { "[#{cur_lineno}] #{s.inspect}" }
@@ -268,7 +271,7 @@ module EBNF
268
271
  s.match(/(#\w+)(.*)$/)
269
272
  l, s = $1, $2
270
273
  [[:hex, l], s]
271
- when /[[:alpha:]]/
274
+ when /[\w\.]/
272
275
  s.match(/(\w+)(.*)$/)
273
276
  l, s = $1, $2
274
277
  [l.to_sym, s]
@@ -290,7 +293,7 @@ module EBNF
290
293
  [[m.to_sym], s[1..-1]]
291
294
  else
292
295
  error("terminal", "unrecognized terminal: #{s.inspect}")
293
- raise "Syntax Error"
296
+ raise "Syntax Error, unrecognized terminal: #{s.inspect}"
294
297
  end
295
298
  end
296
299
  end
@@ -113,7 +113,7 @@ module EBNF
113
113
  %{ rdfs:comment #{comment.inspect};},
114
114
  ]
115
115
 
116
- statements += ttl_expr(expr, kind == :terminal ? "re" : "g", 1, false)
116
+ statements += ttl_expr(expr, terminal? ? "re" : "g", 1, false)
117
117
  "\n" + statements.join("\n")
118
118
  end
119
119
 
@@ -128,7 +128,7 @@ module EBNF
128
128
  # * Transform (a rule (plus b)) into (a rule (seq b (star b)
129
129
  # @return [Array<Rule>]
130
130
  def to_bnf
131
- return [self] unless kind == :rule
131
+ return [self] unless rule?
132
132
  new_rules = []
133
133
 
134
134
  # Look for rules containing recursive definition and rewrite to multiple rules. If `expr` contains elements which are in array form, where the first element of that array is a symbol, create a new rule for it.
@@ -170,7 +170,7 @@ module EBNF
170
170
  new_rules << self
171
171
  elsif [:diff, :hex, :range].include?(expr.first)
172
172
  # This rules are fine, the just need to be terminals
173
- raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.kind == :terminal
173
+ raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.terminal?
174
174
  new_rules << self
175
175
  else
176
176
  # Some case we didn't think of
@@ -180,6 +180,42 @@ module EBNF
180
180
  return new_rules
181
181
  end
182
182
 
183
+ # Return the non-terminals for this rule. For seq, this is the first
184
+ # non-terminals in the seq. For alt, this is every non-terminal ni the alt
185
+ # @param [Array<Rule>] ast
186
+ # The set of rules, used to turn symbols into rules
187
+ # @return [Array<Rule>]
188
+ def non_terminals(ast)
189
+ @non_terms ||= (alt? ? expr[1..-1] : expr[1,1]).map do |sym|
190
+ case sym
191
+ when Symbol
192
+ r = ast.detect {|r| r.sym == sym}
193
+ r if r && r.rule?
194
+ else
195
+ nil
196
+ end
197
+ end.compact
198
+ end
199
+
200
+ # Return the terminals for this rule. For seq, this is the first
201
+ # terminals or strings in the seq. For alt, this is every non-terminal ni the alt
202
+ # @param [Array<Rule>] ast
203
+ # The set of rules, used to turn symbols into rules
204
+ # @return [Array<Rule>]
205
+ def terminals(ast)
206
+ @terms ||= (alt? ? expr[1..-1] : expr[1,1]).map do |sym|
207
+ case sym
208
+ when Symbol
209
+ r = ast.detect {|r| r.sym == sym}
210
+ r if r && r.terminal?
211
+ when String
212
+ sym
213
+ else
214
+ nil
215
+ end
216
+ end.compact
217
+ end
218
+
183
219
  # Does this rule start with a sym? It does if expr is that sym,
184
220
  # expr starts with alt and contains that sym, or
185
221
  # expr starts with seq and the next element is that sym
@@ -203,22 +239,22 @@ module EBNF
203
239
  end
204
240
 
205
241
  # Add terminal as proceding this rule
206
- # @param [Array<Rule>] terminals
242
+ # @param [Array<Rule, Symbol, String>] terminals
207
243
  # @return [Integer] if number of terminals added
208
244
  def add_first(terminals)
209
245
  @first ||= []
210
- terminals -= @first # Remove those already in first
246
+ terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - @first
211
247
  @first += terminals
212
248
  terminals.length
213
249
  end
214
250
 
215
251
  # Add terminal as following this rule. Don't add _eps as a follow
216
252
  #
217
- # @param [Array<Rule>] terminals
253
+ # @param [Array<Rule, Symbol, String>] terminals
218
254
  # @return [Integer] if number of terminals added
219
255
  def add_follow(terminals)
220
- terminals -= @follow || [] # Remove those already in first
221
- terminals -= [:_eps] # Special case, don't add empty string as a follow terminal
256
+ # Remove terminals already in follows, and empty string
257
+ terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - (@follow || []) - [:_eps]
222
258
  unless terminals.empty?
223
259
  @follow ||= []
224
260
  @follow += terminals
@@ -226,6 +262,23 @@ module EBNF
226
262
  terminals.length
227
263
  end
228
264
 
265
+ # Is this a terminal?
266
+ # @return [Boolean]
267
+ def terminal?
268
+ kind == :terminal
269
+ end
270
+
271
+ # Is this a rule?
272
+ # @return [Boolean]
273
+ def rule?
274
+ kind == :rule
275
+ end
276
+
277
+ # Is this rule of the form (alt ...)?
278
+ def alt?
279
+ expr.is_a?(Array) && expr.first == :alt
280
+ end
281
+
229
282
  # Is this rule of the form (seq ...)?
230
283
  def seq?
231
284
  expr.is_a?(Array) && expr.first == :seq