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,39 @@
1
+ module EBNF
2
+ module PEG
3
+ autoload :Parser, 'ebnf/peg/parser'
4
+ autoload :Rule, 'ebnf/peg/rule'
5
+
6
+ ##
7
+ # Transform EBNF Rule set for PEG parsing:
8
+ #
9
+ # * Transform each rule into a set of sub-rules extracting unnamed sequences into new rules, using {Rule#to_peg}.
10
+ # @return [ENBF] self
11
+ def make_peg
12
+ progress("make_peg") {"Start: #{@ast.length} rules"}
13
+ new_ast = []
14
+
15
+ ast.each do |rule|
16
+ debug("make_peg") {"expand from: #{rule.inspect}"}
17
+ new_rules = rule.to_peg
18
+ debug(" => ") {new_rules.map(&:sym).join(', ')}
19
+ new_ast += new_rules
20
+ end
21
+
22
+ @ast = new_ast
23
+ progress("make_peg") {"End: #{@ast.length} rules"}
24
+ self
25
+ end
26
+
27
+ ##
28
+ # Output Ruby parser files for PEG parsing
29
+ #
30
+ # @param [IO, StringIO] output
31
+ def to_ruby_peg(output, **options)
32
+ output.puts " RULES = ["
33
+ ast.each do |rule|
34
+ output.puts " " + rule.to_ruby + (rule.is_a?(EBNF::PEG::Rule) ? '.extend(EBNF::PEG::Rule)' : '') + ','
35
+ end
36
+ output.puts " ]"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,554 @@
1
+ module EBNF::PEG
2
+ ##
3
+ # A Generic PEG parser using the parsed rules modified for PEG parseing.
4
+ module Parser
5
+ ##
6
+ # @return [Regexp, Rule] how to remove inter-rule whitespace
7
+ attr_reader :whitespace
8
+
9
+ ##
10
+ # @return [Scanner] used for scanning input.
11
+ attr_reader :scanner
12
+
13
+ ##
14
+ # A Hash structure used for memoizing rule results for a given input location.
15
+ #
16
+ # @example Partial structure for memoizing results for a particular rule
17
+ #
18
+ # {
19
+ # rule: {
20
+ # 86: {
21
+ # pos:
22
+ # result: [<EBNF::Rule:80 {
23
+ # sym: :ebnf,
24
+ # id: "1",
25
+ # kind: :rule,
26
+ # expr: [:star, [:alt, :declaration, :rule]]}>],
27
+ # }
28
+ # 131: [<EBNF::Rule:80 {sym: :ebnf,
29
+ # id: "1",
30
+ # kind: :rule,
31
+ # expr: [:star, [:alt, :declaration, :rule]]}>,
32
+ # <EBNF::Rule:100 {
33
+ # sym: :declaration,
34
+ # id: "2",
35
+ # kind: :rule,
36
+ # expr: [:alt, "@terminals", :pass]}>]
37
+ # },
38
+ # POSTFIX: {
39
+ # 80: "*",
40
+ # 368: "*",
41
+ # 399: "+"
42
+ # }
43
+ # }
44
+ # @return [Hash{Integer => Hash{Symbol => Object}}]
45
+ attr_reader :packrat
46
+
47
+ def self.included(base)
48
+ base.extend(ClassMethods)
49
+ end
50
+
51
+ # DSL for creating terminals and productions
52
+ module ClassMethods
53
+ def start_handlers; (@start_handlers ||= {}); end
54
+ def start_options; (@start_hoptions ||= {}); end
55
+ def production_handlers; (@production_handlers ||= {}); end
56
+ def terminal_handlers; (@terminal_handlers ||= {}); end
57
+ def terminal_regexps; (@terminal_regexps ||= {}); end
58
+
59
+ ##
60
+ # Defines the pattern for a terminal node and a block to be invoked
61
+ # when ther terminal is encountered. If the block is missing, the
62
+ # value of the terminal will be placed on the input hash to be returned
63
+ # to a previous production. Block is called in an evaluation block from
64
+ # the enclosing parser.
65
+ #
66
+ # If no block is provided, then the value which would have been passed to the block is used as the result directly.
67
+ #
68
+ # @param [Symbol] term
69
+ # The terminal name.
70
+ # @param [Regexp] regexp (nil)
71
+ # Pattern used to scan for this terminal,
72
+ # defaults to the expression defined in the associated rule.
73
+ # If unset, the terminal rule is used for matching.
74
+ # @param [Hash] options
75
+ # @option options [Hash{String => String}] :map ({})
76
+ # A mapping from terminals, in lower-case form, to
77
+ # their canonical value
78
+ # @option options [Boolean] :unescape
79
+ # Cause strings and codepoints to be unescaped.
80
+ # @yield [value, prod]
81
+ # @yieldparam [String] value
82
+ # The scanned terminal value.
83
+ # @yieldparam [Symbol] prod
84
+ # A symbol indicating the production which referenced this terminal
85
+ # @yieldparam [Proc] block
86
+ # Block passed to initialization for yielding to calling parser.
87
+ # Should conform to the yield specs for #initialize
88
+ def terminal(term, regexp = nil, **options, &block)
89
+ terminal_regexps[term] = regexp if regexp
90
+ terminal_handlers[term] = block if block_given?
91
+ end
92
+
93
+ ##
94
+ # Defines a production called at the beggining of a particular production
95
+ # with data from previous production along with data defined for the
96
+ # current production. Block is called in an evaluation block from
97
+ # the enclosing parser.
98
+ #
99
+ # @param [Symbol] term
100
+ # The rule name
101
+ # @param [Hash{Symbol => Object}] options
102
+ # Options which are returned from {Parser#onStart}.
103
+ # @option options [Boolean] :as_hash (false)
104
+ # If the production is a `seq`, causes the value to be represented as a single hash, rather than an array of individual hashes for each sub-production. Note that this is not always advisable due to the possibility of repeated productions within the sequence.
105
+ # @yield [data, block]
106
+ # @yieldparam [Hash] data
107
+ # A Hash defined for the current production, during :start
108
+ # may be initialized with data to pass to further productions,
109
+ # during :finish, it contains data placed by earlier productions
110
+ # @yieldparam [Proc] block
111
+ # Block passed to initialization for yielding to calling parser.
112
+ # Should conform to the yield specs for #initialize
113
+ # Yield to generate a triple
114
+ def start_production(term, **options, &block)
115
+ start_handlers[term] = block
116
+ start_options[term] = options.freeze
117
+ end
118
+
119
+ ##
120
+ # Defines a production called when production of associated
121
+ # non-terminals has completed
122
+ # with data from previous production along with data defined for the
123
+ # current production. Block is called in an evaluation block from
124
+ # the enclosing parser.
125
+ #
126
+ # @param [Symbol] term
127
+ # Term which is a key in the branch table
128
+ # @param [Boolean] clear_packrat (false)
129
+ # Clears the packrat state on completion to reduce memory requirements of parser. Use only on a top-level rule when it is determined that no further backtracking is necessary.
130
+ # @yield [result, data, block]
131
+ # @yieldparam [Object] result
132
+ # The result from sucessfully parsing the production.
133
+ # @yieldparam [Hash] data
134
+ # A Hash defined for the current production, during :start
135
+ # may be initialized with data to pass to further productions,
136
+ # during :finish, it contains data placed by earlier productions
137
+ # @yieldparam [Proc] block
138
+ # Block passed to initialization for yielding to calling parser.
139
+ # Should conform to the yield specs for #initialize
140
+ # @yieldreturn [Object] the result of this production.
141
+ # Yield to generate a triple
142
+ def production(term, clear_packrat: false, &block)
143
+ production_handlers[term] = [block, clear_packrat]
144
+ end
145
+
146
+ # Evaluate a handler, delegating to the specified object.
147
+ # This is necessary so that handlers can operate within the
148
+ # binding context of the parser in which they're invoked.
149
+ # @param [Object] object
150
+ # @return [Object]
151
+ def eval_with_binding(object)
152
+ @delegate = object
153
+ object.instance_eval {yield}
154
+ end
155
+
156
+ private
157
+
158
+ def method_missing(method, *args, &block)
159
+ if @delegate ||= nil
160
+ # special handling when last arg is **options
161
+ params = @delegate.method(method).parameters
162
+ if params.any? {|t, _| t == :keyrest} && args.last.is_a?(Hash)
163
+ opts = args.pop
164
+ @delegate.send(method, *args, **opts, &block)
165
+ else
166
+ @delegate.send(method, *args, &block)
167
+ end
168
+ else
169
+ super
170
+ end
171
+ end
172
+ end
173
+
174
+ ##
175
+ # Initializes a new parser instance.
176
+ #
177
+ # @param [String, #to_s] input
178
+ # @param [Symbol, #to_s] start
179
+ # The starting production for the parser. It may be a URI from the grammar, or a symbol representing the local_name portion of the grammar URI.
180
+ # @param [Array<EBNF::PEG::Rule>] rules
181
+ # The parsed rules, which control parsing sequence.
182
+ # Identify the symbol of the starting rule with `start`.
183
+ # @param [Hash{Symbol => Object}] options
184
+ # @option options[Integer] :high_water passed to lexer
185
+ # @option options [Logger] :logger for errors/progress/debug.
186
+ # @option options[Integer] :low_water passed to lexer
187
+ # @option options [Symbol, Regexp] :whitespace
188
+ # Symbol of whitespace rule (defaults to `@pass`), or a regular expression
189
+ # for eating whitespace between non-terminal rules (strongly encouraged).
190
+ # @yield [context, *data]
191
+ # Yields to return data to parser
192
+ # @yieldparam [:statement, :trace] context
193
+ # Context for block
194
+ # @yieldparam [Symbol] *data
195
+ # Data specific to the call
196
+ # @return [Object] AST resulting from parse
197
+ # @raise [Exception] Raises exceptions for parsing errors
198
+ # or errors raised during processing callbacks. Internal
199
+ # errors are raised using {Error}.
200
+ def parse(input = nil, start = nil, rules = nil, **options, &block)
201
+ start ||= options[:start]
202
+ rules ||= options[:rules] || []
203
+ @rules = rules.inject({}) {|memo, rule| memo.merge(rule.sym => rule)}
204
+ @packrat = {}
205
+
206
+ # Add parser reference to each rule
207
+ @rules.each_value {|rule| rule.parser = self}
208
+
209
+ # Take whitespace from options, a named rule, a `pass` rule, a rule named :WS, or a default
210
+ @whitespace = case options[:whitespace]
211
+ when Regexp then options[:whitespace]
212
+ when Symbol then @rules[options[:whitespace]]
213
+ else options[:whitespace]
214
+ end ||
215
+ @rules.values.detect(&:pass?) ||
216
+ /(?:\s|(?:#[^x][^\n\r]*))+/m.freeze
217
+
218
+ @options = options.dup
219
+ @productions = []
220
+ @parse_callback = block
221
+ @error_log = []
222
+ @prod_data = []
223
+
224
+ @scanner = EBNF::LL1::Scanner.new(input)
225
+ start = start.split('#').last.to_sym unless start.is_a?(Symbol)
226
+ start_rule = @rules[start]
227
+ raise Error, "Starting production #{start.inspect} not defined" unless start_rule
228
+
229
+ result = start_rule.parse(scanner)
230
+ if result == :unmatched
231
+ # Start rule wasn't matched, which is about the only error condition
232
+ error("--top--", @furthest_failure.to_s,
233
+ pos: @furthest_failure.pos,
234
+ lineno: @furthest_failure.lineno,
235
+ rest: scanner.string[@furthest_failure.pos, 20])
236
+ end
237
+
238
+ # Eat any remaining whitespace
239
+ start_rule.eat_whitespace(scanner)
240
+ if !scanner.eos?
241
+ error("--top--", @furthest_failure.to_s,
242
+ pos: @furthest_failure.pos,
243
+ lineno: @furthest_failure.lineno,
244
+ rest: scanner.string[@furthest_failure.pos, 20])
245
+ end
246
+
247
+ # When all is said and done, raise the error log
248
+ unless @error_log.empty?
249
+ raise Error, @error_log.join("\n")
250
+ end
251
+
252
+ result
253
+ end
254
+
255
+ # Depth of parsing, for log output.
256
+ def depth; (@productions || []).length; end
257
+
258
+ # Current ProdData element
259
+ def prod_data; @prod_data.last || {}; end
260
+
261
+ # Clear out packrat memoizer. This is appropriate when completing a top-level rule when there is no possibility of backtracking.
262
+ def clear_packrat; @packrat.clear; end
263
+
264
+ ##
265
+ # Error information, used as level `3` logger messages.
266
+ # Messages may be logged and are saved for reporting at end of parsing.
267
+ #
268
+ # @param [String] node Relevant location associated with message
269
+ # @param [String] message Error string
270
+ # @param [Hash{Symbol => Object}] options
271
+ # @option options [URI, #to_s] :production
272
+ # @option options [Token] :token
273
+ # @see #debug
274
+ def error(node, message, **options)
275
+ lineno = options[:lineno] || (scanner.lineno if scanner)
276
+ m = "ERROR "
277
+ m += "[line: #{lineno}] " if lineno
278
+ m += message
279
+ m += " (found #{options[:rest].inspect})" if options[:rest]
280
+ m += ", production = #{options[:production].inspect}" if options[:production]
281
+ @error_log << m unless @recovering
282
+ @recovering = true
283
+ debug(node, m, level: 3, **options)
284
+ if options[:raise] || @options[:validate]
285
+ raise Error.new(m, lineno: lineno, rest: options[:rest], production: options[:production])
286
+ end
287
+ end
288
+
289
+ ##
290
+ # Warning information, used as level `2` logger messages.
291
+ # Messages may be logged and are saved for reporting at end of parsing.
292
+ #
293
+ # @param [String] node Relevant location associated with message
294
+ # @param [String] message Error string
295
+ # @param [Hash] options
296
+ # @option options [URI, #to_s] :production
297
+ # @option options [Token] :token
298
+ # @see #debug
299
+ def warn(node, message, **options)
300
+ lineno = options[:lineno] || (scanner.lineno if scanner)
301
+ m = "WARNING "
302
+ m += "[line: #{lineno}] " if lineno
303
+ m += message
304
+ m += " (found #{options[:rest].inspect})" if options[:rest]
305
+ m += ", production = #{options[:production].inspect}" if options[:production]
306
+ debug(node, m, level: 2, **options)
307
+ end
308
+
309
+ ##
310
+ # Progress logged when parsing. Passed as level `1` logger messages.
311
+ #
312
+ # The call is ignored, unless `@options[:logger]` is set.
313
+ #
314
+ # @overload progress(node, message, **options, &block)
315
+ # @param [String] node Relevant location associated with message
316
+ # @param [String] message ("")
317
+ # @param [Hash] options
318
+ # @option options [Integer] :depth
319
+ # Recursion depth for indenting output
320
+ # @see #debug
321
+ def progress(node, *args, &block)
322
+ return unless @options[:logger]
323
+ args << {} unless args.last.is_a?(Hash)
324
+ args.last[:level] ||= 1
325
+ debug(node, *args, &block)
326
+ end
327
+
328
+ ##
329
+ # Debug logging.
330
+ #
331
+ # The call is ignored, unless `@options[:logger]` is set.
332
+ #
333
+ # @overload debug(node, message, **options)
334
+ # @param [Array<String>] args Relevant location associated with message
335
+ # @param [Hash] options
336
+ # @option options [Integer] :depth
337
+ # Recursion depth for indenting output
338
+ # @yieldreturn [String] additional string appended to `message`.
339
+ def debug(*args, &block)
340
+ return unless @options[:logger]
341
+ options = args.last.is_a?(Hash) ? args.pop : {}
342
+ lineno = options[:lineno] || (scanner.lineno if scanner)
343
+ level = options.fetch(:level, 0)
344
+ depth = options[:depth] || self.depth
345
+
346
+ if self.respond_to?(:log_debug)
347
+ level = [:debug, :info, :warn, :error, :fatal][level]
348
+ log_debug(*args, **options.merge(level: level, lineno: lineno, depth: depth), &block)
349
+ elsif @options[:logger].respond_to?(:add)
350
+ args << yield if block_given?
351
+ @options[:logger].add(level, "[#{lineno}]" + (" " * depth) + args.join(" "))
352
+ elsif @options[:logger].respond_to?(:<<)
353
+ args << yield if block_given?
354
+ @options[:logger] << "[#{lineno}]" + (" " * depth) + args.join(" ")
355
+ end
356
+ end
357
+
358
+ # Start for production
359
+ # Adds data avoiable during the processing of the production
360
+ #
361
+ # @return [Hash] composed of production options. Currently only `as_hash` is supported.
362
+ # @see ClassMethods#start_production
363
+ def onStart(prod)
364
+ handler = self.class.start_handlers[prod]
365
+ @productions << prod
366
+ debug("#{prod}(:start)", "",
367
+ lineno: (scanner.lineno if scanner),
368
+ pos: (scanner.pos if scanner),
369
+ depth: (depth + 1)) {"#{prod}, pos: #{scanner ? scanner.pos : '?'}, rest: #{scanner ? scanner.rest[0..20].inspect : '?'}"}
370
+ if handler
371
+ # Create a new production data element, potentially allowing handler
372
+ # to customize before pushing on the @prod_data stack
373
+ data = {}
374
+ begin
375
+ self.class.eval_with_binding(self) {
376
+ handler.call(data, @parse_callback)
377
+ }
378
+ rescue ArgumentError, Error => e
379
+ error("start", "#{e.class}: #{e.message}", production: prod)
380
+ @recovering = false
381
+ end
382
+ @prod_data << data
383
+ elsif self.class.production_handlers[prod]
384
+ # Make sure we push as many was we pop, even if there is no
385
+ # explicit start handler
386
+ @prod_data << {}
387
+ end
388
+ return self.class.start_options.fetch(prod, {}) # any options on this production
389
+ end
390
+
391
+ # Finish of production
392
+ #
393
+ # @param [Object] result parse result
394
+ # @return [Object] parse result, or the value returned from the handler
395
+ def onFinish(result)
396
+ #puts "prod_data(f): " + @prod_data.inspect
397
+ prod = @productions.last
398
+ handler, clear_packrat = self.class.production_handlers[prod]
399
+ data = @prod_data.pop if handler || self.class.start_handlers[prod]
400
+ if handler && !@recovering && result != :unmatched
401
+ # Pop production data element from stack, potentially allowing handler to use it
402
+ result = begin
403
+ self.class.eval_with_binding(self) {
404
+ handler.call(result, data, @parse_callback)
405
+ }
406
+ rescue ArgumentError, Error => e
407
+ error("finish", "#{e.class}: #{e.message}", production: prod)
408
+ @recovering = false
409
+ end
410
+ end
411
+ progress("#{prod}(:finish)", "",
412
+ depth: (depth + 1),
413
+ lineno: (scanner.lineno if scanner),
414
+ level: result == :unmatched ? 0 : 1) do
415
+ "#{result.inspect}@(#{scanner ? scanner.pos : '?'}), rest: #{scanner ? scanner.rest[0..20].inspect : '?'}"
416
+ end
417
+ self.clear_packrat if clear_packrat
418
+ @productions.pop
419
+ result
420
+ end
421
+
422
+ # A terminal with a defined handler
423
+ #
424
+ # @param [Symbol] prod from the symbol of the associated rule
425
+ # @param [String] value the scanned string
426
+ # @return [String, Object] either the result from the handler, or the token
427
+ def onTerminal(prod, value)
428
+ parentProd = @productions.last
429
+ handler = self.class.terminal_handlers[prod]
430
+ if handler && value != :unmatched
431
+ value = begin
432
+ self.class.eval_with_binding(self) {
433
+ handler.call(value, parentProd, @parse_callback)
434
+ }
435
+ rescue ArgumentError, Error => e
436
+ error("terminal", "#{e.class}: #{e.message}", value: value, production: prod)
437
+ @recovering = false
438
+ end
439
+ end
440
+ progress("#{prod}(:terminal)", "",
441
+ depth: (depth + 2),
442
+ lineno: (scanner.lineno if scanner),
443
+ level: value == :unmatched ? 0 : 1) do
444
+ "#{value.inspect}@(#{scanner ? scanner.pos : '?'})"
445
+ end
446
+ value
447
+ end
448
+
449
+ ##
450
+ # Find a rule for a symbol
451
+ #
452
+ # @param [Symbol] sym
453
+ # @return [Rule]
454
+ def find_rule(sym)
455
+ @rules[sym]
456
+ end
457
+
458
+ ##
459
+ # Find a regular expression defined for a terminal
460
+ #
461
+ # @param [Symbol] sym
462
+ # @return [Regexp]
463
+ def find_terminal_regexp(sym)
464
+ self.class.terminal_regexps[sym]
465
+ end
466
+
467
+ ##
468
+ # Record furthest failure.
469
+ #
470
+ # @param [Integer] pos
471
+ # The position in the input stream where the failure occured.
472
+ # @param [Integer] lineno
473
+ # Line where the failure occured.
474
+ # @param [Symbol, String] token
475
+ # The terminal token or string which attempted to match.
476
+ # @see https://arxiv.org/pdf/1405.6646.pdf
477
+ def update_furthest_failure(pos, lineno, token)
478
+ # Skip generated productions
479
+ return if token.is_a?(Symbol) && token.to_s.start_with?('_')
480
+ if @furthest_failure.nil? || pos > @furthest_failure.pos
481
+ @furthest_failure = Unmatched.new(pos, lineno, [token])
482
+ elsif pos == @furthest_failure.pos && !@furthest_failure[:expecting].include?(token)
483
+ @furthest_failure[:expecting] << token
484
+ end
485
+ end
486
+
487
+ public
488
+
489
+ ##
490
+ # @!parse
491
+ # # Record details about an inmatched rule, including the following:
492
+ # #
493
+ # # * Input location and line number at time of failure.
494
+ # # * The rule at which this was found (non-terminal, and nat starting with '_').
495
+ # class Unmatched
496
+ # # @return [Integer] The position within the scanner which did not match.
497
+ # attr_reader :pos
498
+ # # @return [Integer] The line number which did not match.
499
+ # attr_reader :lineno
500
+ # # @return [Array<Symbol,String>]
501
+ # # Strings or production rules that attempted to match at this position.
502
+ # attr_reader :expecting
503
+ # end
504
+ class Unmatched < Struct.new(:pos, :lineno, :expecting)
505
+ def to_s
506
+ "syntax error, expecting #{expecting.map(&:inspect).join(', ')}"
507
+ end
508
+ end
509
+
510
+ ##
511
+ # Raised for errors during parsing.
512
+ #
513
+ # @example Raising a parser error
514
+ # raise Error.new(
515
+ # "invalid token '%' on line 10",
516
+ # rest: '%', lineno: 9, production: :turtleDoc)
517
+ #
518
+ # @see https://ruby-doc.org/core/classes/StandardError.html
519
+ class Error < StandardError
520
+ ##
521
+ # The current production.
522
+ #
523
+ # @return [Symbol]
524
+ attr_reader :production
525
+
526
+ ##
527
+ # The read head when scanning failed
528
+ #
529
+ # @return [String]
530
+ attr_reader :rest
531
+
532
+ ##
533
+ # The line number where the error occurred.
534
+ #
535
+ # @return [Integer]
536
+ attr_reader :lineno
537
+
538
+ ##
539
+ # Initializes a new lexer error instance.
540
+ #
541
+ # @param [String, #to_s] message
542
+ # @param [Hash{Symbol => Object}] options
543
+ # @option options [Symbol] :production (nil)
544
+ # @option options [String] :rest (nil)
545
+ # @option options [Integer] :lineno (nil)
546
+ def initialize(message, **options)
547
+ @production = options[:production]
548
+ @rest = options[:rest]
549
+ @lineno = options[:lineno]
550
+ super(message.to_s)
551
+ end
552
+ end # class Error
553
+ end # class Parser
554
+ end # module EBNF::LL1