ebnf 1.1.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +218 -196
  3. data/UNLICENSE +1 -1
  4. data/VERSION +1 -1
  5. data/bin/ebnf +40 -21
  6. data/etc/abnf-core.ebnf +52 -0
  7. data/etc/abnf.abnf +121 -0
  8. data/etc/abnf.ebnf +124 -0
  9. data/etc/abnf.sxp +45 -0
  10. data/etc/doap.ttl +13 -12
  11. data/etc/ebnf.ebnf +21 -33
  12. data/etc/ebnf.html +171 -160
  13. data/etc/{ebnf.rb → ebnf.ll1.rb} +30 -107
  14. data/etc/ebnf.ll1.sxp +182 -183
  15. data/etc/ebnf.peg.rb +90 -0
  16. data/etc/ebnf.peg.sxp +84 -0
  17. data/etc/ebnf.sxp +40 -41
  18. data/etc/iso-ebnf.ebnf +140 -0
  19. data/etc/iso-ebnf.isoebnf +138 -0
  20. data/etc/iso-ebnf.sxp +65 -0
  21. data/etc/sparql.ebnf +4 -4
  22. data/etc/sparql.html +1603 -1751
  23. data/etc/sparql.ll1.sxp +7372 -7372
  24. data/etc/sparql.peg.rb +532 -0
  25. data/etc/sparql.peg.sxp +597 -0
  26. data/etc/sparql.sxp +363 -362
  27. data/etc/turtle.ebnf +3 -3
  28. data/etc/turtle.html +465 -517
  29. data/etc/{turtle.rb → turtle.ll1.rb} +3 -4
  30. data/etc/turtle.ll1.sxp +425 -425
  31. data/etc/turtle.peg.rb +182 -0
  32. data/etc/turtle.peg.sxp +199 -0
  33. data/etc/turtle.sxp +103 -101
  34. data/lib/ebnf.rb +7 -2
  35. data/lib/ebnf/abnf.rb +301 -0
  36. data/lib/ebnf/abnf/core.rb +23 -0
  37. data/lib/ebnf/abnf/meta.rb +111 -0
  38. data/lib/ebnf/base.rb +128 -87
  39. data/lib/ebnf/bnf.rb +1 -26
  40. data/lib/ebnf/ebnf/meta.rb +90 -0
  41. data/lib/ebnf/isoebnf.rb +229 -0
  42. data/lib/ebnf/isoebnf/meta.rb +75 -0
  43. data/lib/ebnf/ll1.rb +140 -8
  44. data/lib/ebnf/ll1/lexer.rb +37 -32
  45. data/lib/ebnf/ll1/parser.rb +113 -73
  46. data/lib/ebnf/ll1/scanner.rb +84 -51
  47. data/lib/ebnf/native.rb +320 -0
  48. data/lib/ebnf/parser.rb +285 -302
  49. data/lib/ebnf/peg.rb +39 -0
  50. data/lib/ebnf/peg/parser.rb +554 -0
  51. data/lib/ebnf/peg/rule.rb +241 -0
  52. data/lib/ebnf/rule.rb +453 -163
  53. data/lib/ebnf/terminals.rb +21 -0
  54. data/lib/ebnf/writer.rb +554 -85
  55. metadata +98 -20
  56. data/etc/sparql.rb +0 -45773
@@ -0,0 +1,241 @@
1
+ module EBNF::PEG
2
+ # Behaviior for parsing a PEG rule
3
+ module Rule
4
+ ##
5
+ # Initialized by parser when loading rules.
6
+ # Used for finding rules and invoking elements of the parse process.
7
+ #
8
+ # @return [EBNF::PEG::Parser] parser
9
+ attr_accessor :parser
10
+
11
+ ##
12
+ # Parse a rule or terminal, invoking callbacks, as appropriate
13
+
14
+ # If there is are `start_production` and/or `production`,
15
+ # they are invoked with a `prod_data` stack, the input stream and offset.
16
+ # Otherwise, the results are added as an array value
17
+ # to a hash indexed by the rule name.
18
+ #
19
+ # If matched, the input position is updated and the results returned in a Hash.
20
+ #
21
+ # * `alt`: returns the value of the matched production or `:unmatched`.
22
+ # * `diff`: returns the value matched, or `:unmatched`.
23
+ # * `hex`: returns a string composed of the matched hex character, or `:unmatched`.
24
+ # * `opt`: returns the value matched, or `nil` if unmatched.
25
+ # * `plus`: returns an array of the values matched for the specified production, or `:unmatched`, if none are matched. For Terminals, these are concatenated into a single string.
26
+ # * `range`: returns a string composed of the values matched, or `:unmatched`, if less than `min` are matched.
27
+ # * `seq`: returns an array composed of single-entry hashes for each matched production indexed by the production name, or `:unmatched` if any production fails to match. For Terminals, returns a string created by concatenating these values. Via option in a `production` or definition, the result can be a single hash with values for each matched production; note that this is not always possible due to the possibility of repeated productions within the sequence.
28
+ # * `star`: returns an array of the values matched for the specified production. For Terminals, these are concatenated into a single string.
29
+ #
30
+ # @param [Scanner] input
31
+ # @return [Hash{Symbol => Object}, :unmatched] A hash with keys for matched component of the expression. Returns :unmatched if the input does not match the production.
32
+ def parse(input)
33
+ # Save position and linenumber for backtracking
34
+ pos, lineno = input.pos, input.lineno
35
+
36
+ parser.packrat[sym] ||= {}
37
+ if parser.packrat[sym][pos]
38
+ parser.debug("#{sym}(:memo)", lineno: lineno) { "#{parser.packrat[sym][pos].inspect}(@#{pos})"}
39
+ input.pos, input.lineno = parser.packrat[sym][pos][:pos], parser.packrat[sym][pos][:lineno]
40
+ return parser.packrat[sym][pos][:result]
41
+ end
42
+
43
+ if terminal?
44
+ # If the terminal is defined with a regular expression,
45
+ # use that to match the input,
46
+ # otherwise,
47
+ if regexp = parser.find_terminal_regexp(sym)
48
+ matched = input.scan(regexp)
49
+ result = parser.onTerminal(sym, (matched ? matched : :unmatched))
50
+ # Update furthest failure for strings and terminals
51
+ parser.update_furthest_failure(input.pos, input.lineno, sym) if result == :unmatched
52
+ parser.packrat[sym][pos] = {
53
+ pos: input.pos,
54
+ lineno: input.lineno,
55
+ result: result
56
+ }
57
+ return parser.packrat[sym][pos][:result]
58
+ end
59
+ else
60
+ eat_whitespace(input)
61
+ end
62
+ start_options = parser.onStart(sym)
63
+
64
+ result = case expr.first
65
+ when :alt
66
+ # Return the first expression to match.
67
+ # Result is either :unmatched, or the value of the matching rule
68
+ alt = :unmatched
69
+ expr[1..-1].each do |prod|
70
+ alt = case prod
71
+ when Symbol
72
+ rule = parser.find_rule(prod)
73
+ raise "No rule found for #{prod}" unless rule
74
+ rule.parse(input)
75
+ when String
76
+ input.scan(Regexp.new(Regexp.quote(prod))) || :unmatched
77
+ end
78
+ if alt == :unmatched
79
+ # Update furthest failure for strings and terminals
80
+ parser.update_furthest_failure(input.pos, input.lineno, prod) if prod.is_a?(String) || rule.terminal?
81
+ else
82
+ break
83
+ end
84
+ end
85
+ alt
86
+ when :diff
87
+ # matches any string that matches A but does not match B.
88
+ # (Note, this is only used for Terminal rules, non-terminals will use :not)
89
+ raise "Diff used on non-terminal #{prod}" unless terminal?
90
+ re1, re2 = Regexp.new(translate_codepoints(expr[1])), Regexp.new(translate_codepoints(expr[2]))
91
+ matched = input.scan(re1)
92
+ if !matched || re2.match?(matched)
93
+ # Update furthest failure for terminals
94
+ parser.update_furthest_failure(input.pos, input.lineno, sym)
95
+ :unmatched
96
+ else
97
+ matched
98
+ end
99
+ when :hex
100
+ # Matches the given hex character if expression matches the character whose number (code point) in ISO/IEC 10646 is N. The number of leading zeros in the #xN form is insignificant.
101
+ input.scan(to_regexp) || begin
102
+ # Update furthest failure for terminals
103
+ parser.update_furthest_failure(input.pos, input.lineno, expr.last)
104
+ :unmatched
105
+ end
106
+ when :not
107
+ # matches any string that does not match B.
108
+ res = case prod = expr[1]
109
+ when Symbol
110
+ rule = parser.find_rule(prod)
111
+ raise "No rule found for #{prod}" unless rule
112
+ rule.parse(input)
113
+ when String
114
+ input.scan(Regexp.new(Regexp.quote(prod))) || :unmatched
115
+ end
116
+ if res != :unmatched
117
+ # Update furthest failure for terminals
118
+ parser.update_furthest_failure(input.pos, input.lineno, sym) if terminal?
119
+ :unmatched
120
+ else
121
+ nil
122
+ end
123
+ when :opt
124
+ # Result is the matched value or nil
125
+ opt = rept(input, 0, 1, expr[1])
126
+
127
+ # Update furthest failure for strings and terminals
128
+ parser.update_furthest_failure(input.pos, input.lineno, expr[1]) if terminal?
129
+ opt.first
130
+ when :plus
131
+ # Result is an array of all expressions while they match,
132
+ # at least one must match
133
+ plus = rept(input, 1, '*', expr[1])
134
+
135
+ # Update furthest failure for strings and terminals
136
+ parser.update_furthest_failure(input.pos, input.lineno, expr[1]) if terminal?
137
+ plus.is_a?(Array) && terminal? ? plus.join("") : plus
138
+ when :range, :istr
139
+ # Matches the specified character range
140
+ input.scan(to_regexp) || begin
141
+ # Update furthest failure for strings and terminals
142
+ parser.update_furthest_failure(input.pos, input.lineno, expr[1])
143
+ :unmatched
144
+ end
145
+ when :seq
146
+ # Evaluate each expression into an array of hashes where each hash contains a key from the associated production and the value is the parsed value of that production. Returns :unmatched if the input does not match the production. Value ordering is ensured by native Hash ordering.
147
+ seq = expr[1..-1].each_with_object([]) do |prod, accumulator|
148
+ eat_whitespace(input) unless accumulator.empty? || terminal?
149
+ res = case prod
150
+ when Symbol
151
+ rule = parser.find_rule(prod)
152
+ raise "No rule found for #{prod}" unless rule
153
+ rule.parse(input)
154
+ when String
155
+ input.scan(Regexp.new(Regexp.quote(prod))) || :unmatched
156
+ end
157
+ if res == :unmatched
158
+ # Update furthest failure for strings and terminals
159
+ parser.update_furthest_failure(input.pos, input.lineno, prod)
160
+ break :unmatched
161
+ end
162
+ accumulator << {prod.to_sym => res}
163
+ end
164
+ if seq == :unmatched
165
+ :unmatched
166
+ elsif terminal?
167
+ seq.map(&:values).compact.join("") # Concat values for terminal production
168
+ elsif start_options[:as_hash]
169
+ seq.inject {|memo, h| memo.merge(h)}
170
+ else
171
+ seq
172
+ end
173
+ when :star
174
+ # Result is an array of all expressions while they match,
175
+ # an empty array of none match
176
+ star = rept(input, 0, '*', expr[1])
177
+
178
+ # Update furthest failure for strings and terminals
179
+ parser.update_furthest_failure(input.pos, input.lineno, expr[1]) if terminal?
180
+ star.is_a?(Array) && terminal? ? star.join("") : star
181
+ else
182
+ raise "attempt to parse unknown rule type: #{expr.first}"
183
+ end
184
+
185
+ if result == :unmatched
186
+ input.pos, input.lineno = pos, lineno
187
+ end
188
+
189
+ result = parser.onFinish(result)
190
+ (parser.packrat[sym] ||= {})[pos] = {
191
+ pos: input.pos,
192
+ lineno: input.lineno,
193
+ result: result
194
+ }
195
+ return parser.packrat[sym][pos][:result]
196
+ end
197
+
198
+ ##
199
+ # Repitition, 0-1, 0-n, 1-n, ...
200
+ #
201
+ # Note, nil results are removed from the result, but count towards min/max calculations
202
+ #
203
+ # @param [Scanner] input
204
+ # @param [Integer] min
205
+ # @param [Integer] max
206
+ # If it is an integer, it stops matching after max entries.
207
+ # @param [Symbol, String] prod
208
+ # @return [:unmatched, Array]
209
+ def rept(input, min, max, prod)
210
+ result = []
211
+
212
+ case prod
213
+ when Symbol
214
+ rule = parser.find_rule(prod)
215
+ raise "No rule found for #{prod}" unless rule
216
+ while (max == '*' || result.length < max) && (res = rule.parse(input)) != :unmatched
217
+ eat_whitespace(input) unless terminal?
218
+ result << res
219
+ end
220
+ when String
221
+ while (res = input.scan(Regexp.new(Regexp.quote(prod)))) && (max == '*' || result.length < max)
222
+ eat_whitespace(input) unless terminal?
223
+ result << res
224
+ end
225
+ end
226
+
227
+ result.length < min ? :unmatched : result.compact
228
+ end
229
+
230
+ ##
231
+ # Eat whitespace between non-terminal rules
232
+ def eat_whitespace(input)
233
+ if parser.whitespace.is_a?(Regexp)
234
+ # Eat whitespace before a non-terminal
235
+ input.skip(parser.whitespace)
236
+ elsif parser.whitespace.is_a?(Rule)
237
+ parser.whitespace.parse(input) # throw away result
238
+ end
239
+ end
240
+ end
241
+ end
@@ -1,15 +1,33 @@
1
+ require 'scanf'
2
+ require 'strscan'
3
+
1
4
  module EBNF
2
5
  # Represent individual parsed rules
3
6
  class Rule
4
- # Operations which are flattened to seprate rules in to_bnf
7
+ # Operations which are flattened to seprate rules in to_bnf.
5
8
  BNF_OPS = %w{
6
- alt opt plus seq star
9
+ alt diff not opt plus rept seq star
7
10
  }.map(&:to_sym).freeze
8
11
 
9
12
  TERM_OPS = %w{
10
- diff hex range
13
+ hex istr range
11
14
  }.map(&:to_sym).freeze
12
15
 
16
+ # The number of arguments expected per operator. `nil` for unspecified
17
+ OP_ARGN = {
18
+ alt: nil,
19
+ diff: 2,
20
+ hex: 1,
21
+ istr: 1,
22
+ not: 1,
23
+ opt: 1,
24
+ plus: 1,
25
+ range: 1,
26
+ rept: 3,
27
+ seq: nil,
28
+ star: 1
29
+ }
30
+
13
31
  # Symbol of rule
14
32
  #
15
33
  # @return [Symbol]
@@ -26,7 +44,7 @@ module EBNF
26
44
 
27
45
  # Kind of rule
28
46
  #
29
- # @return [:rule, :terminal, or :pass]
47
+ # @return [:rule, :terminal, :terminals, or :pass]
30
48
  attr_accessor :kind
31
49
 
32
50
  # Rule expression
@@ -57,45 +75,92 @@ module EBNF
57
75
  # Determines preparation and cleanup rules for reconstituting EBNF ? * + from BNF
58
76
  attr_accessor :cleanup
59
77
 
60
- # @param [Integer] id
61
- # @param [Symbol] sym
78
+ # @param [Symbol, nil] sym
79
+ # `nil` is allowed only for @pass or @terminals
80
+ # @param [Integer, nil] id
62
81
  # @param [Array] expr
63
- # @param [Hash{Symbol => Object}] options
64
- # @option options [Symbol] :kind
65
- # @option options [String] :ebnf
66
- # @option options [Array] :first
67
- # @option options [Array] :follow
68
- # option options [Boolean] :start
69
- def initialize(sym, id, expr, options = {})
82
+ # The expression is an internal-representation of an S-Expression with one of the following oparators:
83
+ #
84
+ # * `alt` A list of alternative rules, which are attempted in order. It terminates with the first matching rule, or is terminated as unmatched, if no such rule is found.
85
+ # * `diff` matches any string that matches `A` but does not match `B`.
86
+ # * `hex` A single character represented using the hexadecimal notation `#xnn`.
87
+ # * `istr` A string which matches in a case-insensitive manner, so that `(istr "fOo")` will match either of the strings `"foo"`, `"FOO"` or any other combination.
88
+ # * `opt` An optional rule or terminal. It either results in the matching rule or returns `nil`.
89
+ # * `plus` – A sequence of one or more of the matching rule. If there is no such rule, it is terminated as unmatched; otherwise, the result is an array containing all matched input.
90
+ # * `range` – A range of characters, possibly repeated, of the form `(range "a-z")`. May also use hexadecimal notation.
91
+ # * `rept m n` – A sequence of at lest `m` and at most `n` of the matching rule. It will always return an array.
92
+ # * `seq` – A sequence of rules or terminals. If any (other than `opt` or `star`) to not parse, the rule is terminated as unmatched.
93
+ # * `star` – A sequence of zero or more of the matching rule. It will always return an array.
94
+ # @param [:rule, :terminal, :terminals, :pass] kind (nil)
95
+ # @param [String] ebnf (nil)
96
+ # When parsing, records the EBNF string used to create the rule.
97
+ # @param [Array] first (nil)
98
+ # Recorded set of terminals that can proceed this rule (LL(1))
99
+ # @param [Array] follow (nil)
100
+ # Recorded set of terminals that can follow this rule (LL(1))
101
+ # @param [Boolean] start (nil)
102
+ # Is this the starting rule for the grammar?
103
+ # @param [Rule] top_rule (nil)
104
+ # The top-most rule. All expressed rules are top-rules, derived rules have the original rule as their top-rule.
105
+ # @param [Boolean] cleanup (nil)
106
+ # Records information useful for cleaning up converted :plus, and :star expansions (LL(1)).
107
+ def initialize(sym, id, expr, kind: nil, ebnf: nil, first: nil, follow: nil, start: nil, top_rule: nil, cleanup: nil)
70
108
  @sym, @id = sym, id
71
- @expr = expr.is_a?(Array) ? expr : [:seq, expr]
72
- @ebnf = options[:ebnf]
73
- @top_rule = options.fetch(:top_rule, self)
74
- @first = options[:first]
75
- @follow = options[:follow]
76
- @start = options[:start]
77
- @cleanup = options[:cleanup]
78
- @kind = case
79
- when options[:kind] then options[:kind]
109
+ @expr = expr.is_a?(Array) ? expr : [:seq, expr].compact
110
+ @ebnf, @kind, @first, @follow, @start, @cleanup, @top_rule = ebnf, kind, first, follow, start, cleanup, top_rule
111
+ @top_rule ||= self
112
+ @kind ||= case
80
113
  when sym.to_s == sym.to_s.upcase then :terminal
81
114
  when !BNF_OPS.include?(@expr.first) then :terminal
82
115
  else :rule
83
116
  end
117
+
118
+ # Allow @pass and @terminals to not be named
119
+ @sym ||= :_pass if @kind == :pass
120
+ @sym ||= :_terminals if @kind == :terminals
121
+
122
+ raise ArgumentError, "Rule sym must be a symbol, was #{@sym.inspect}" unless @sym.is_a?(Symbol)
123
+ raise ArgumentError, "Rule id must be a string or nil, was #{@id.inspect}" unless (@id || "").is_a?(String)
124
+ raise ArgumentError, "Rule kind must be one of :rule, :terminal, :terminals, or :pass, was #{@kind.inspect}" unless
125
+ @kind.is_a?(Symbol) && %w(rule terminal terminals pass).map(&:to_sym).include?(@kind)
126
+
127
+ case @expr.first
128
+ when :alt
129
+ raise ArgumentError, "#{@expr.first} operation must have at least one operand, had #{@expr.length - 1}" unless @expr.length > 1
130
+ when :diff
131
+ raise ArgumentError, "#{@expr.first} operation must have exactly two operands, had #{@expr.length - 1}" unless @expr.length == 3
132
+ when :hex, :istr, :not, :opt, :plus, :range, :star
133
+ raise ArgumentError, "#{@expr.first} operation must have exactly one operand, had #{@expr.length - 1}" unless @expr.length == 2
134
+ when :rept
135
+ raise ArgumentError, "#{@expr.first} operation must have exactly three, had #{@expr.length - 1}" unless @expr.length == 4
136
+ raise ArgumentError, "#{@expr.first} operation must an non-negative integer minimum, was #{@expr[1]}" unless
137
+ @expr[1].is_a?(Integer) && @expr[1] >= 0
138
+ raise ArgumentError, "#{@expr.first} operation must an non-negative integer maximum or '*', was #{@expr[2]}" unless
139
+ @expr[2] == '*' || @expr[2].is_a?(Integer) && @expr[2] >= 0
140
+ when :seq
141
+ # It's legal to have a zero-length sequence
142
+ else
143
+ raise ArgumentError, "Rule expression must be an array using a known operator, was #{@expr.first}"
144
+ end
84
145
  end
85
146
 
86
147
  ##
87
148
  # Return a rule from its SXP representation:
88
149
  #
89
150
  # @example inputs
90
- # (pass (plus (range "#x20\\t\\r\\n")))
151
+ # (pass _pass (plus (range "#x20\\t\\r\\n")))
91
152
  # (rule ebnf "1" (star (alt declaration rule)))
92
- # (terminal O_ENUM "17" (seq "[^" (plus CHAR) "]"))
153
+ # (terminal R_CHAR "19" (diff CHAR (alt "]" "-")))
93
154
  #
94
- # Also may have (first ...), (follow ...), or (start #t)
155
+ # Also may have `(first ...)`, `(follow ...)`, or `(start #t)`.
95
156
  #
96
- # @param [Array] sxp
157
+ # @param [String, Array] sxp
97
158
  # @return [Rule]
98
159
  def self.from_sxp(sxp)
160
+ if sxp.is_a?(String)
161
+ require 'sxp' unless defined?(SXP)
162
+ sxp = SXP.parse(sxp)
163
+ end
99
164
  expr = sxp.detect {|e| e.is_a?(Array) && ![:first, :follow, :start].include?(e.first.to_sym)}
100
165
  first = sxp.detect {|e| e.is_a?(Array) && e.first.to_sym == :first}
101
166
  first = first[1..-1] if first
@@ -106,27 +171,28 @@ module EBNF
106
171
  start = sxp.any? {|e| e.is_a?(Array) && e.first.to_sym == :start}
107
172
  sym = sxp[1] if sxp[1].is_a?(Symbol)
108
173
  id = sxp[2] if sxp[2].is_a?(String)
109
- Rule.new(sym, id, expr, kind: sxp.first, first: first, follow: follow, cleanup: cleanup, start: start)
174
+ self.new(sym, id, expr, kind: sxp.first, first: first, follow: follow, cleanup: cleanup, start: start)
110
175
  end
111
176
 
112
177
  # Build a new rule creating a symbol and numbering from the current rule
113
- # Symbol and number creation is handled by the top-most rule in such a chain
178
+ # Symbol and number creation is handled by the top-most rule in such a chain.
114
179
  #
115
180
  # @param [Array] expr
181
+ # @param [Symbol] kind (nil)
182
+ # @param [Hash{Symbol => Symbol}] cleanup (nil)
116
183
  # @param [Hash{Symbol => Object}] options
117
- # @option options [Symbol] :kind
118
- # @option options [String] :ebnf EBNF instance (used for messages)
119
- def build(expr, options = {})
120
- new_sym, new_id = (@top_rule ||self).send(:make_sym_id)
121
- Rule.new(new_sym, new_id, expr, {
122
- kind: options[:kind],
123
- ebnf: @ebnf,
124
- top_rule: @top_rule || self,
125
- cleanup: options[:cleanup],
126
- }.merge(options))
127
- end
128
-
129
- # Return representation for building S-Expressions
184
+ def build(expr, kind: nil, cleanup: nil, **options)
185
+ new_sym, new_id = @top_rule.send(:make_sym_id)
186
+ self.class.new(new_sym, new_id, expr,
187
+ kind: kind,
188
+ ebnf: @ebnf,
189
+ top_rule: @top_rule,
190
+ cleanup: cleanup,
191
+ **options)
192
+ end
193
+
194
+ # Return representation for building S-Expressions.
195
+ #
130
196
  # @return [Array]
131
197
  def for_sxp
132
198
  elements = [kind, sym]
@@ -142,40 +208,51 @@ module EBNF
142
208
  # Return SXP representation of this rule
143
209
  # @return [String]
144
210
  def to_sxp
211
+ require 'sxp' unless defined?(SXP)
145
212
  for_sxp.to_sxp
146
213
  end
147
214
 
148
215
  alias_method :to_s, :to_sxp
149
216
 
150
- # Serializes this rule to an Turtle
217
+ # Serializes this rule to an Turtle.
218
+ #
151
219
  # @return [String]
152
220
  def to_ttl
153
221
  @ebnf.debug("to_ttl") {inspect} if @ebnf
154
- comment = orig.strip.
155
- gsub(/"""/, '\"\"\"').
156
- gsub("\\", "\\\\").
157
- sub(/^\"/, '\"').
158
- sub(/\"$/m, '\"')
159
- statements = [
160
- %{:#{id} rdfs:label "#{id}"; rdf:value "#{sym}";},
161
- %{ rdfs:comment #{comment.inspect};},
162
- ]
222
+ statements = [%{:#{sym} rdfs:label "#{sym}";}]
223
+ if orig
224
+ comment = orig.to_s.strip.
225
+ gsub(/"""/, '\"\"\"').
226
+ gsub("\\", "\\\\").
227
+ sub(/^\"/, '\"').
228
+ sub(/\"$/m, '\"')
229
+ statements << %{ rdfs:comment #{comment.inspect};}
230
+ end
231
+ statements << %{ dc:identifier "#{id}";} if id
163
232
 
164
233
  statements += ttl_expr(expr, terminal? ? "re" : "g", 1, false)
165
234
  "\n" + statements.join("\n")
166
235
  end
167
236
 
237
+ # Return a Ruby representation of this rule
238
+ # @return [String]
239
+ def to_ruby
240
+ "EBNF::Rule.new(#{sym.inspect}, #{id.inspect}, #{expr.inspect}#{', kind: ' + kind.inspect unless kind == :rule})"
241
+ end
242
+
168
243
  ##
169
244
  # Transform EBNF rule to BNF rules:
170
245
  #
171
- # * Transform (a [n] rule (op1 (op2))) into two rules:
172
- # (a [n] rule (op1 _a_1))
173
- # (_a_1 [n.1] rule (op2))
174
- # * Transform (a rule (opt b)) into (a rule (alt _empty b))
175
- # * Transform (a rule (star b)) into (a rule (alt _empty (seq b a)))
176
- # * Transform (a rule (plus b)) into (a rule (seq b (star b)
246
+ # * Transform `(rule a "n" (op1 (op2)))` into two rules:
247
+ #
248
+ # (rule a "n" (op1 _a_1))
249
+ # (rule _a_1 "n.1" (op2))
250
+ # * Transform `(rule a (opt b))` into `(rule a (alt _empty b))`
251
+ # * Transform `(rule a (star b))` into `(rule a (alt _empty (seq b a)))`
252
+ # * Transform `(rule a (plus b))` into `(rule a (seq b (star b)`
253
+ #
254
+ # Transformation includes information used to re-construct non-transformed.
177
255
  #
178
- # Transformation includes information used to re-construct non-transformed
179
256
  # AST representation
180
257
  # @return [Array<Rule>]
181
258
  def to_bnf
@@ -202,19 +279,19 @@ module EBNF
202
279
  new_rules = new_rules.map {|r| r.to_bnf}.flatten
203
280
  elsif expr.first == :opt
204
281
  this = dup
205
- # * Transform (a rule (opt b)) into (a rule (alt _empty b))
282
+ # * Transform (rule a (opt b)) into (rule a (alt _empty b))
206
283
  this.expr = [:alt, :_empty, expr.last]
207
284
  this.cleanup = :opt
208
285
  new_rules = this.to_bnf
209
286
  elsif expr.first == :star
210
- # * Transform (a rule (star b)) into (a rule (alt _empty (seq b a)))
287
+ # * Transform (rule a (star b)) into (rule a (alt _empty (seq b a)))
211
288
  this = dup
212
289
  this.cleanup = :star
213
290
  new_rule = this.build([:seq, expr.last, this.sym], cleanup: :merge)
214
291
  this.expr = [:alt, :_empty, new_rule.sym]
215
292
  new_rules = [this] + new_rule.to_bnf
216
293
  elsif expr.first == :plus
217
- # * Transform (a rule (plus b)) into (a rule (seq b (star b)
294
+ # * Transform (rule a (plus b)) into (rule a (seq b (star b)
218
295
  this = dup
219
296
  this.cleanup = :plus
220
297
  this.expr = [:seq, expr.last, [:star, expr.last]]
@@ -223,7 +300,7 @@ module EBNF
223
300
  # Otherwise, no further transformation necessary
224
301
  new_rules << self
225
302
  elsif [:diff, :hex, :range].include?(expr.first)
226
- # This rules are fine, the just need to be terminals
303
+ # This rules are fine, they just need to be terminals
227
304
  raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.terminal?
228
305
  new_rules << self
229
306
  else
@@ -234,89 +311,73 @@ module EBNF
234
311
  return new_rules
235
312
  end
236
313
 
237
- # Return the non-terminals for this rule. For seq, this is the first
238
- # non-terminals in the seq. For alt, this is every non-terminal ni the alt
239
- # @param [Array<Rule>] ast
240
- # The set of rules, used to turn symbols into rules
314
+ ##
315
+ # Transform EBNF rule for PEG:
316
+ #
317
+ # * Transform `(rule a "n" (op1 ... (op2 y) ...z))` into two rules:
318
+ #
319
+ # (rule a "n" (op1 ... _a_1 ... z))
320
+ # (rule _a_1 "n.1" (op2 y))
321
+ # * Transform `(rule a "n" (diff op1 op2))` into two rules:
322
+ #
323
+ # (rule a "n" (seq _a_1 op1))
324
+ # (rule _a_1 "n.1" (not op1))
325
+ #
241
326
  # @return [Array<Rule>]
242
- def non_terminals(ast)
243
- @non_terms ||= (alt? ? expr[1..-1] : expr[1,1]).map do |sym|
244
- case sym
245
- when Symbol
246
- r = ast.detect {|r| r.sym == sym}
247
- r if r && r.rule?
248
- else
249
- nil
250
- end
251
- end.compact
252
- end
327
+ def to_peg
328
+ new_rules = []
253
329
 
254
- # Return the terminals for this rule. For seq, this is the first
255
- # terminals or strings in the seq. For alt, this is every non-terminal ni the alt
256
- # @param [Array<Rule>] ast
257
- # The set of rules, used to turn symbols into rules
258
- # @return [Array<Rule>]
259
- def terminals(ast)
260
- @terms ||= (alt? ? expr[1..-1] : expr[1,1]).map do |sym|
261
- case sym
262
- when Symbol
263
- r = ast.detect {|r| r.sym == sym}
264
- r if r && r.terminal?
265
- when String
266
- sym
267
- else
268
- nil
330
+ # Look for rules containing sub-sequences
331
+ if expr.any? {|e| e.is_a?(Array) && e.first.is_a?(Symbol)}
332
+ # duplicate ourselves for rewriting
333
+ this = dup
334
+ new_rules << this
335
+
336
+ expr.each_with_index do |e, index|
337
+ next unless e.is_a?(Array) && e.first.is_a?(Symbol)
338
+ new_rule = build(e)
339
+ this.expr[index] = new_rule.sym
340
+ new_rules << new_rule
269
341
  end
270
- end.compact
271
- end
272
342
 
273
- # Does this rule start with a sym? It does if expr is that sym,
274
- # expr starts with alt and contains that sym, or
275
- # expr starts with seq and the next element is that sym
276
- # @param [Symbol, class] sym
277
- # Symbol matching any start element, or if it is String, any start element which is a String
278
- # @return [Array<Symbol, String>] list of symbol (singular), or strings which are start symbol, or nil if there are none
279
- def starts_with?(sym)
280
- if seq? && sym === (v = expr.fetch(1, nil))
281
- [v]
282
- elsif alt? && expr.any? {|e| sym === e}
283
- expr.select {|e| sym === e}
343
+ # Return new rules after recursively applying #to_bnf
344
+ new_rules = new_rules.map {|r| r.to_peg}.flatten
345
+ elsif expr.first == :diff && !terminal?
346
+ this = dup
347
+ new_rule = build([:not, expr[2]])
348
+ this.expr = [:seq, new_rule.sym, expr[1]]
349
+ new_rules << this
350
+ new_rules << new_rule
351
+ elsif [:hex, :istr, :range].include?(expr.first)
352
+ # This rules are fine, they just need to be terminals
353
+ raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.terminal?
354
+ new_rules << self
284
355
  else
285
- nil
356
+ new_rules << self
286
357
  end
358
+
359
+ return new_rules.map {|r| r.extend(EBNF::PEG::Rule)}
287
360
  end
288
361
 
289
- # Do the firsts of this rule include the empty string?
290
- # @return [Boolean]
291
- def first_includes_eps?
292
- @first && @first.include?(:_eps)
293
- end
294
-
295
- # Add terminal as proceding this rule
296
- # @param [Array<Rule, Symbol, String>] terminals
297
- # @return [Integer] if number of terminals added
298
- def add_first(terminals)
299
- @first ||= []
300
- terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - @first
301
- @first += terminals
302
- terminals.length
303
- end
304
-
305
- # Add terminal as following this rule. Don't add _eps as a follow
362
+ ##
363
+ # For :hex or :range, create a regular expression.
306
364
  #
307
- # @param [Array<Rule, Symbol, String>] terminals
308
- # @return [Integer] if number of terminals added
309
- def add_follow(terminals)
310
- # Remove terminals already in follows, and empty string
311
- terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - (@follow || []) - [:_eps]
312
- unless terminals.empty?
313
- @follow ||= []
314
- @follow += terminals
365
+ # @return [Regexp]
366
+ def to_regexp
367
+ case expr.first
368
+ when :hex
369
+ Regexp.new(translate_codepoints(expr[1]))
370
+ when :istr
371
+ /#{expr.last}/ui
372
+ when :range
373
+ Regexp.new("[#{translate_codepoints(expr[1])}]")
374
+ else
375
+ raise "Can't turn #{expr.inspect} into a regexp"
315
376
  end
316
- terminals.length
317
377
  end
318
378
 
319
379
  # Is this a terminal?
380
+ #
320
381
  # @return [Boolean]
321
382
  def terminal?
322
383
  kind == :terminal
@@ -344,18 +405,14 @@ module EBNF
344
405
  expr.is_a?(Array) && expr.first == :seq
345
406
  end
346
407
 
347
- # Is this rule of the form (alt ...)?
348
- def alt?
349
- expr.is_a?(Array) && expr.first == :alt
350
- end
351
-
352
408
  def inspect
353
409
  "#<EBNF::Rule:#{object_id} " +
354
410
  {sym: sym, id: id, kind: kind, expr: expr}.inspect +
355
411
  ">"
356
412
  end
357
413
 
358
- # Two rules are equal if they have the same {#sym}, {#kind} and {#expr}
414
+ # Two rules are equal if they have the same {#sym}, {#kind} and {#expr}.
415
+ #
359
416
  # @param [Rule] other
360
417
  # @return [Boolean]
361
418
  def ==(other)
@@ -364,42 +421,264 @@ module EBNF
364
421
  expr == other.expr
365
422
  end
366
423
 
367
- # Two rules are equivalent if they have the same {#expr}
424
+ # Two rules are equivalent if they have the same {#expr}.
425
+ #
368
426
  # @param [Rule] other
369
427
  # @return [Boolean]
370
- def equivalent?(other)
371
- expr == other.expr
428
+ def eql?(other)
429
+ expr == other.expr
372
430
  end
373
431
 
374
- # Rewrite the rule substituting src_rule for dst_rule wherever
375
- # it is used in the production (first level only).
376
- # @param [Rule] src_rule
377
- # @param [Rule] dst_rule
378
- # @return [Rule]
379
- def rewrite(src_rule, dst_rule)
380
- case @expr
381
- when Array
382
- @expr = @expr.map {|e| e == src_rule.sym ? dst_rule.sym : e}
432
+ # Rules compare using their ids
433
+ def <=>(other)
434
+ if id && other.id
435
+ if id == other.id
436
+ id.to_s <=> other.id.to_s
437
+ else
438
+ id.to_f <=> other.id.to_f
439
+ end
383
440
  else
384
- @expr = dst_rule.sym if @expr == src_rule.sym
441
+ sym.to_s <=> other.sym.to_s
385
442
  end
386
- self
387
443
  end
388
444
 
389
- # Rules compare using their ids
390
- def <=>(other)
391
- if id.to_i == other.id.to_i
392
- id <=> other.id
445
+ ##
446
+ # Utility function to translate code points of the form '#xN' into ruby unicode characters
447
+ def translate_codepoints(str)
448
+ str.gsub(/#x\h+/) {|c| c[2..-1].scanf("%x").first.chr(Encoding::UTF_8)}
449
+ end
450
+
451
+ # Return the non-terminals for this rule.
452
+ #
453
+ # * `alt` => this is every non-terminal.
454
+ # * `diff` => this is every non-terminal.
455
+ # * `hex` => nil
456
+ # * `istr` => nil
457
+ # * `not` => this is the last expression, if any.
458
+ # * `opt` => this is the last expression, if any.
459
+ # * `plus` => this is the last expression, if any.
460
+ # * `range` => nil
461
+ # * `rept` => this is the last expression, if any.
462
+ # * `seq` => this is the first expression in the sequence, if any.
463
+ # * `star` => this is the last expression, if any.
464
+ #
465
+ # @param [Array<Rule>] ast
466
+ # The set of rules, used to turn symbols into rules
467
+ # @param [Array<Symbol,String,Array>] expr (@expr)
468
+ # The expression to check, defaults to the rule expression.
469
+ # Typically, if the expression is recursive, the embedded expression is called recursively.
470
+ # @return [Array<Rule>]
471
+ # @note this is used for LL(1) tansformation, so rule types are limited
472
+ def non_terminals(ast, expr = @expr)
473
+ ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).map do |sym|
474
+ case sym
475
+ when Symbol
476
+ r = ast.detect {|r| r.sym == sym}
477
+ r if r && r.rule?
478
+ when Array
479
+ non_terminals(ast, sym)
480
+ else
481
+ nil
482
+ end
483
+ end.flatten.compact.uniq
484
+ end
485
+
486
+ # Return the terminals for this rule.
487
+ #
488
+ # * `alt` => this is every terminal.
489
+ # * `diff` => this is every terminal.
490
+ # * `hex` => nil
491
+ # * `istr` => nil
492
+ # * `not` => this is the last expression, if any.
493
+ # * `opt` => this is the last expression, if any.
494
+ # * `plus` => this is the last expression, if any.
495
+ # * `range` => nil
496
+ # * `rept` => this is the last expression, if any.
497
+ # * `seq` => this is the first expression in the sequence, if any.
498
+ # * `star` => this is the last expression, if any.
499
+ #
500
+ # @param [Array<Rule>] ast
501
+ # The set of rules, used to turn symbols into rules
502
+ # @param [Array<Symbol,String,Array>] expr (@expr)
503
+ # The expression to check, defaults to the rule expression.
504
+ # Typically, if the expression is recursive, the embedded expression is called recursively.
505
+ # @return [Array<Rule>]
506
+ # @note this is used for LL(1) tansformation, so rule types are limited
507
+ def terminals(ast, expr = @expr)
508
+ ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).map do |sym|
509
+ case sym
510
+ when Symbol
511
+ r = ast.detect {|r| r.sym == sym}
512
+ r if r && r.terminal?
513
+ when String
514
+ sym
515
+ when Array
516
+ terminals(ast, sym)
517
+ end
518
+ end.flatten.compact.uniq
519
+ end
520
+
521
+ # Return the symbols used in the rule.
522
+ #
523
+ # @param [Array<Symbol,String,Array>] expr (@expr)
524
+ # The expression to check, defaults to the rule expression.
525
+ # Typically, if the expression is recursive, the embedded expression is called recursively.
526
+ # @return [Array<Rule>]
527
+ def symbols(expr = @expr)
528
+ expr[1..-1].map do |sym|
529
+ case sym
530
+ when Symbol
531
+ sym
532
+ when Array
533
+ symbols(sym)
534
+ end
535
+ end.flatten.compact.uniq
536
+ end
537
+
538
+ ##
539
+ # The following are used for LL(1) transformation.
540
+ ##
541
+
542
+ # Does this rule start with `sym`? It does if expr is that sym,
543
+ # expr starts with alt and contains that sym,
544
+ # or expr starts with seq and the next element is that sym.
545
+ #
546
+ # @param [Symbol, class] sym
547
+ # Symbol matching any start element, or if it is String, any start element which is a String
548
+ # @return [Array<Symbol, String>] list of symbol (singular), or strings which are start symbol, or nil if there are none
549
+ def starts_with?(sym)
550
+ if seq? && sym === (v = expr.fetch(1, nil))
551
+ [v]
552
+ elsif alt? && expr.any? {|e| sym === e}
553
+ expr.select {|e| sym === e}
393
554
  else
394
- id.to_i <=> other.id.to_i
555
+ nil
395
556
  end
396
557
  end
397
558
 
559
+ ##
560
+ # Validate the rule, with respect to an AST.
561
+ #
562
+ # @param [Array<Rule>] ast
563
+ # The set of rules, used to turn symbols into rules
564
+ # @param [Array<Symbol,String,Array>] expr (@expr)
565
+ # The expression to check, defaults to the rule expression.
566
+ # Typically, if the expression is recursive, the embedded expression is called recursively.
567
+ # @raise [RangeError]
568
+ def validate!(ast, expr = @expr)
569
+ op = expr.first
570
+ raise SyntaxError, "Unknown operator: #{op}" unless OP_ARGN.key?(op)
571
+ raise SyntaxError, "Argument count missmatch on operator #{op}, had #{expr.length - 1} expected #{OP_ARGN[op]}" if
572
+ OP_ARGN[op] && OP_ARGN[op] != expr.length - 1
573
+
574
+ # rept operator needs min and max
575
+ if op == :alt
576
+ raise SyntaxError, "alt operation must have at least one operand, had #{expr.length - 1}" unless expr.length > 1
577
+ elsif op == :rept
578
+ raise SyntaxError, "rept operation must an non-negative integer minimum, was #{expr[1]}" unless
579
+ expr[1].is_a?(Integer) && expr[1] >= 0
580
+ raise SyntaxError, "rept operation must an non-negative integer maximum or '*', was #{expr[2]}" unless
581
+ expr[2] == '*' || expr[2].is_a?(Integer) && expr[2] >= 0
582
+ end
583
+
584
+ case op
585
+ when :hex
586
+ raise SyntaxError, "Hex operand must be of form '#xN+': #{sym}" unless expr.last.match?(/^#x\h+$/)
587
+ when :range
588
+ str = expr.last.dup
589
+ str = str[1..-1] if str.start_with?('^')
590
+ str = str[0..-2] if str.end_with?('-') # Allowed at end of range
591
+ scanner = StringScanner.new(str)
592
+ hex = rchar = in_range = false
593
+ while !scanner.eos?
594
+ begin
595
+ if scanner.scan(Terminals::HEX)
596
+ raise SyntaxError if in_range && rchar
597
+ rchar = in_range = false
598
+ hex = true
599
+ elsif scanner.scan(Terminals::R_CHAR)
600
+ raise SyntaxError if in_range && hex
601
+ hex = in_range = false
602
+ rchar = true
603
+ else
604
+ raise(SyntaxError, "Range contains illegal components at offset #{scanner.pos}: was #{expr.last}")
605
+ end
606
+
607
+ if scanner.scan(/\-/)
608
+ raise SyntaxError if in_range
609
+ in_range = true
610
+ end
611
+ rescue SyntaxError
612
+ raise(SyntaxError, "Range contains illegal components at offset #{scanner.pos}: was #{expr.last}")
613
+ end
614
+ end
615
+ else
616
+ ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).each do |sym|
617
+ case sym
618
+ when Symbol
619
+ r = ast.detect {|r| r.sym == sym}
620
+ raise SyntaxError, "No rule found for #{sym}" unless r
621
+ when Array
622
+ validate!(ast, sym)
623
+ when String
624
+ raise SyntaxError, "String must be of the form CHAR*" unless sym.match?(/^#{Terminals::CHAR}*$/)
625
+ end
626
+ end
627
+ end
628
+ end
629
+
630
+ ##
631
+ # Validate the rule, with respect to an AST.
632
+ #
633
+ # Uses `#validate!` and catches `RangeError`
634
+ #
635
+ # @param [Array<Rule>] ast
636
+ # The set of rules, used to turn symbols into rules
637
+ # @return [Boolean]
638
+ def valid?(ast)
639
+ validate!(ast)
640
+ true
641
+ rescue SyntaxError
642
+ false
643
+ end
644
+
645
+ # Do the firsts of this rule include the empty string?
646
+ #
647
+ # @return [Boolean]
648
+ def first_includes_eps?
649
+ @first && @first.include?(:_eps)
650
+ end
651
+
652
+ # Add terminal as proceding this rule.
653
+ #
654
+ # @param [Array<Rule, Symbol, String>] terminals
655
+ # @return [Integer] if number of terminals added
656
+ def add_first(terminals)
657
+ @first ||= []
658
+ terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - @first
659
+ @first += terminals
660
+ terminals.length
661
+ end
662
+
663
+ # Add terminal as following this rule. Don't add _eps as a follow
664
+ #
665
+ # @param [Array<Rule, Symbol, String>] terminals
666
+ # @return [Integer] if number of terminals added
667
+ def add_follow(terminals)
668
+ # Remove terminals already in follows, and empty string
669
+ terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - (@follow || []) - [:_eps]
670
+ unless terminals.empty?
671
+ @follow ||= []
672
+ @follow += terminals
673
+ end
674
+ terminals.length
675
+ end
676
+
398
677
  private
399
678
  def ttl_expr(expr, pfx, depth, is_obj = true)
400
679
  indent = ' ' * depth
401
- @ebnf.debug("ttl_expr", depth: depth) {expr.inspect}
402
- op = expr.shift if expr.is_a?(Array)
680
+ @ebnf.debug("ttl_expr", depth: depth) {expr.inspect} if @ebnf
681
+ op, *expr = expr if expr.is_a?(Array)
403
682
  statements = []
404
683
 
405
684
  if is_obj
@@ -410,17 +689,28 @@ module EBNF
410
689
 
411
690
  case op
412
691
  when :seq, :alt, :diff
692
+ # Multiple operands
413
693
  statements << %{#{indent}#{bra}#{pfx}:#{op} (}
414
694
  expr.each {|a| statements += ttl_expr(a, pfx, depth + 1)}
415
695
  statements << %{#{indent} )#{ket}}
416
- when :opt, :plus, :star
696
+ when :opt, :plus, :star, :not
697
+ # Single operand
417
698
  statements << %{#{indent}#{bra}#{pfx}:#{op} }
418
699
  statements += ttl_expr(expr.first, pfx, depth + 1)
419
700
  statements << %{#{indent} #{ket}} unless ket.empty?
420
- when :_empty, :_eps, :_empty
701
+ when :rept
702
+ # Three operands (min, max and expr)
703
+ statements << %{ #{indent}#{pfx}:min #{expr[0].inspect};}
704
+ statements << %{ #{indent}#{pfx}:max #{expr[1].inspect};}
705
+ statements << %{#{indent}#{bra}#{pfx}:#{op} }
706
+ statements += ttl_expr(expr.last, pfx, depth + 1)
707
+ statements << %{#{indent} #{ket}} unless ket.empty?
708
+ when :_empty, :_eps
421
709
  statements << %{#{indent}"g:#{op.to_s[1..-1]}"}
422
710
  when :"'"
423
711
  statements << %{#{indent}"#{esc(expr)}"}
712
+ when :istr
713
+ statements << %{#{indent}#{bra} re:matches #{expr.first.inspect} #{ket}}
424
714
  when :range
425
715
  statements << %{#{indent}#{bra} re:matches #{cclass(expr.first).inspect} #{ket}}
426
716
  when :hex
@@ -435,7 +725,7 @@ module EBNF
435
725
  end
436
726
 
437
727
  statements.last << " ." unless is_obj
438
- @ebnf.debug("statements", depth: depth) {statements.join("\n")}
728
+ @ebnf.debug("statements", depth: depth) {statements.join("\n")} if @ebnf
439
729
  statements
440
730
  end
441
731
 
@@ -476,7 +766,7 @@ module EBNF
476
766
  def make_sym_id(variation = nil)
477
767
  @id_seq ||= 0
478
768
  @id_seq += 1
479
- ["_#{@sym}_#{@id_seq}#{variation}".to_sym, "#{@id}.#{@id_seq}#{variation}"]
769
+ ["_#{@sym}_#{@id_seq}#{variation}".to_sym, ("#{@id}.#{@id_seq}#{variation}" if @id)]
480
770
  end
481
771
  end
482
772
  end