ebnf 1.1.1 → 2.1.0

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