birman 0.2.2 → 0.3.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.
@@ -38,8 +38,9 @@ module Birman
38
38
  module Matcher
39
39
  # [method.">>"]
40
40
  # params = ["other: String | Regexp | Matcher"]
41
- # returns = "Sequence of self then other (flattens into an existing sequence)"
42
- def >>(other) = Seq.new([self, Birman.coerce(other)])
41
+ # returns = "Sequence of self then other: a chain of >> is one seq"
42
+ # notes = "A seq from seq(...), or with a commit or only, is one part, as in seq(self, other)"
43
+ def >>(other) = Seq.new(parts: [*inline_parts(self), *inline_parts(Birman.coerce(other))], open: true)
43
44
 
44
45
  # [method."|"]
45
46
  # params = ["other: String | Regexp | Matcher"]
@@ -61,8 +62,8 @@ module Birman
61
62
  # params = ["other: String | Regexp | Matcher"]
62
63
  # returns = "Sequence of self then other, committed after self (like seq(self, commit, other))"
63
64
  def >(other)
64
- left = uncommitted_parts(self)
65
- right = uncommitted_parts(Birman.coerce(other))
65
+ left = inline_parts(self)
66
+ right = inline_parts(Birman.coerce(other))
66
67
  Seq.new([*left, *right], left.size)
67
68
  end
68
69
 
@@ -75,7 +76,7 @@ module Birman
75
76
  # [method.maybe]
76
77
  # returns = "Optional self"
77
78
  # value = "self's value, or nil when absent"
78
- def maybe = rep(0, 1).map(&:first)
79
+ def maybe = Maybe.new(self)
79
80
 
80
81
  # [method.absent]
81
82
  # returns = "Negative lookahead: succeeds, consuming nothing, when self doesn't match here"
@@ -87,10 +88,46 @@ module Birman
87
88
  # value = "nil"
88
89
  def present = And.new(self)
89
90
 
91
+ # Prefer a Symbol to a block: a Symbol is data a compiler can read,
92
+ # while a block's source is lost and it can capture local variables.
90
93
  # [method.map]
91
- # params = ["fn: block taking self's value"]
92
- # returns = "Matcher whose value is fn's result (self's .as name is kept)"
93
- def map(&fn) = Action.new(self, fn)
94
+ # params = ["method_name: Symbol, called on self's value", "or fn: block taking self's value"]
95
+ # returns = "Matcher whose value is the method's (or block's) result (self's .as name is kept)"
96
+ # raises = ["ArgumentError (both a Symbol and a block, or neither)"]
97
+ def map(method_name = nil, &fn)
98
+ return Send.new(self, method_name) if method_name && !fn
99
+ return Map.new(self, fn) if fn && !method_name
100
+
101
+ raise ArgumentError, "map takes a method name or a block (one, not both)"
102
+ end
103
+
104
+ # Pass the value to a method of the grammar, for real work such as
105
+ # decoding escapes or parsing numbers. The method may be private,
106
+ # and subclasses can override it.
107
+ # [method.action]
108
+ # params = ["method_name: Symbol, a grammar instance method taking one argument"]
109
+ # returns = "Matcher whose value is grammar.method_name(self's value) (self's .as name is kept)"
110
+ def action(method_name) = Action.new(self, method_name)
111
+
112
+ # Reject a match the grammar can't accept for reasons a pattern
113
+ # can't express (1979-02-30 isn't a date). The method may be
114
+ # private, and, like an action, must depend only on the value: it
115
+ # runs inside lookaheads, and its result is memoized with the rule.
116
+ # A rejection is an ordinary failure where self began, so a choice
117
+ # backtracks past it (and a later alternative that gets further
118
+ # owns the error) unless an enclosing seq has committed.
119
+ # [method.check]
120
+ # params = ["method_name: Symbol, a grammar instance method taking one argument", "name: String"]
121
+ # returns = "self, failing as \"expected <name>\" when grammar.method_name(self's value) is falsy"
122
+ # value = "self's value (self's .as name is kept)"
123
+ def check(method_name, name) = Check.new(self, method_name, name)
124
+
125
+ # Replace the value with a constant: lit("null").means(nil). The
126
+ # same object comes back from every match.
127
+ # [method.means]
128
+ # params = ["value: any object"]
129
+ # returns = "Matcher whose value is always value (self's .as name is kept)"
130
+ def means(value) = Const.new(self, value)
94
131
 
95
132
  # [method.label]
96
133
  # params = ["name: String"]
@@ -124,7 +161,9 @@ module Birman
124
161
 
125
162
  private
126
163
 
127
- def uncommitted_parts(m) = m.is_a?(Seq) && m.commit_at.nil? && m.only_at.nil? ? m.parts : [m]
164
+ # Only a seq built by >> joins the enclosing one, and only if that
165
+ # can't change what it means: a commit or only(...) would apply to the whole.
166
+ def inline_parts(m) = m.is_a?(Seq) && m.open? && m.commit_at.nil? && m.only_at.nil? ? m.parts : [m]
128
167
  end
129
168
 
130
169
  # A bare string: a literal (see Combinators#literal_matcher). What a
@@ -134,7 +173,8 @@ module Birman
134
173
  include Matcher
135
174
 
136
175
  def match(ctx, pos) = ctx.grammar.literal_matcher(text).match(ctx, pos)
137
- def to_ast = [:str, text]
176
+ # Its spacing comes after the text, so it's a lexeme only when empty.
177
+ def to_ast = text.empty? ? [:lexeme, [:str, text]] : [:str, text]
138
178
  end
139
179
 
140
180
  # Exact string, nothing skipped (see Combinators#str).
@@ -169,25 +209,59 @@ module Birman
169
209
  Match.new(pos + len, scanner.matched)
170
210
  end
171
211
 
212
+ # [method.captures]
213
+ # returns = "Matcher for the regexp whose value is a Hash of its named groups"
214
+ # value = "{name: text} for each named group, in order (Symbol keys); nil for a group that didn't match"
215
+ # raises = ["ArgumentError (no named groups, or a name used twice)"]
216
+ def captures = Captures.new(self)
217
+
172
218
  def to_ast = [:pat, source]
173
219
  end
174
220
 
221
+ # A regexp's named groups as a Hash (see Pat#captures). It does its
222
+ # own scan, so nothing can move the scanner before the groups are read.
223
+ Captures = Data.define(:pat) do
224
+ include Matcher
225
+
226
+ def initialize(pat:)
227
+ twice, = pat.re.named_captures.find { |_, groups| groups.size > 1 }
228
+ problem = pat.re.names.empty? ? "has no named groups" : twice && "names #{twice} more than once"
229
+ raise ArgumentError, "captures needs groups named once each, (?<name>...): #{pat.source} #{problem}" if problem
230
+
231
+ @names = pat.re.names.map(&:to_sym).freeze
232
+ super
233
+ end
234
+
235
+ def match(ctx, pos)
236
+ scanner = ctx.scanner
237
+ scanner.pos = pos
238
+ len = scanner.match?(pat.re) or return ctx.fail_at(pos, pat.source)
239
+ Match.new(pos + len, @names.each_with_index.to_h { |name, i| [name, scanner[i + 1]] })
240
+ end
241
+
242
+ def to_ast = pat.to_ast
243
+ end
244
+
175
245
  # Sequence: parts match in order. The value is an array, or a hash
176
246
  # of the named parts if any are named (see Matcher#as). Parts at
177
247
  # index `commit_at` and beyond are committed: if one fails, parsing stops.
248
+ # A name used twice, or a nested seq's names the value would drop, raise.
178
249
  Seq = Data.define(:parts, :commit_at, :only_at) do
179
250
  include Matcher
180
251
 
181
252
  # Arrays are copied and frozen so a matcher is immutable all the way down.
182
- def initialize(parts:, commit_at: nil, only_at: nil)
183
- if only_at && parts.each_with_index.any? { |part, i| i != only_at && part.capture_key }
184
- raise ArgumentError, "a seq with only(...) can't also name parts with .as"
185
- end
186
-
253
+ # `open` (built by >>, so a later >> extends it) isn't a member: it
254
+ # changes how the seq composes, not what it matches, so == ignores it.
255
+ def initialize(parts:, commit_at: nil, only_at: nil, open: false)
256
+ check_names(parts, only_at)
257
+ @open = open
187
258
  super(parts: parts.dup.freeze, commit_at:, only_at:)
188
259
  end
189
260
 
190
- def >>(other) = with(parts: [*parts, Birman.coerce(other)])
261
+ def open? = @open
262
+
263
+ # Data#with rebuilds through initialize, which would close the copy.
264
+ def with(**changes) = changes.empty? ? self : Seq.new(open: open?, **to_h, **changes)
191
265
 
192
266
  def match(ctx, pos)
193
267
  values = Array.new(parts.size)
@@ -204,8 +278,46 @@ module Birman
204
278
  # With only(...), the kept part's name (if any) passes outward.
205
279
  def capture_key = only_at && parts[only_at].capture_key
206
280
 
281
+ # The names a part's value is a hash of, when the part has no name of
282
+ # its own to carry that hash: seen through wrappers that keep the
283
+ # value, but not into rep, sep_by or a transform, which reshape it.
284
+ def self.names_inside(part) # rubocop:disable Metrics
285
+ case part
286
+ when Seq then part.only_at ? names_inside(part.parts[part.only_at]) : part.parts.filter_map(&:capture_key)
287
+ when Pick then names_inside(part.parts[part.index])
288
+ when Label, Quiet, Lexeme, Check, Maybe then names_inside(part.inner)
289
+ when Both then names_inside(part.right)
290
+ when Choice then part.alts.flat_map { names_inside(_1) }.uniq
291
+ when Captures then part.pat.re.names.map(&:to_sym)
292
+ else []
293
+ end
294
+ end
295
+
207
296
  private
208
297
 
298
+ def check_names(parts, only_at)
299
+ keys = parts.filter_map(&:capture_key)
300
+ twice = keys.find { keys.count(_1) > 1 }
301
+ raise ArgumentError, "a seq can't name two parts #{twice.inspect}" if twice
302
+
303
+ if only_at
304
+ check_dropped(parts.reject.with_index { |_, i| i == only_at })
305
+ elsif keys.any?
306
+ check_dropped(parts.reject(&:capture_key))
307
+ end
308
+ end
309
+
310
+ # Parts the seq's value leaves out can't have names.
311
+ def check_dropped(dropped)
312
+ raise ArgumentError, "a seq with only(...) can't also name parts with .as" if dropped.any?(&:capture_key)
313
+
314
+ hidden = dropped.flat_map { Seq.names_inside(_1) }
315
+ return if hidden.empty?
316
+
317
+ raise ArgumentError, "a seq inside a seq would lose its names #{hidden.map(&:inspect).join(", ")}: " \
318
+ "list its parts in the outer seq, or name it with .as"
319
+ end
320
+
209
321
  def failed(ctx, part, pos, index)
210
322
  fail_committed!(ctx, part, pos) if commit_at && index >= commit_at
211
323
  nil
@@ -251,19 +363,26 @@ module Birman
251
363
  def capture_key = parts[index].capture_key
252
364
  end
253
365
 
254
- # One or more items separated by sep. The value is an array of the
255
- # items, and the name (see Matcher#as) is the item's.
256
- SepBy = Data.define(:item, :sep) do
366
+ # Items separated by sep: at least one, or with min 0 possibly none.
367
+ # The value is an array of the items (a new one each time, even when
368
+ # empty), and the name (see Matcher#as) is the item's. With trailing,
369
+ # a separator with no item after it ends the list and is kept in the
370
+ # match; the item's failure there is still recorded, so whatever
371
+ # comes next shares the error ("expected value or ]").
372
+ SepBy = Data.define(:item, :sep, :min, :trailing) do
257
373
  include Matcher
258
374
 
375
+ def initialize(item:, sep:, min: 1, trailing: false) = super
376
+
259
377
  def match(ctx, pos)
260
- r = item.match(ctx, pos) or return nil
378
+ r = item.match(ctx, pos) or return (min.zero? ? Match.new(pos, []) : nil)
261
379
  values = [r.value]
262
380
  pos = r.pos
263
381
  loop do
264
382
  s = sep.match(ctx, pos) or break
265
- r = item.match(ctx, s.pos) or break
266
- break if r.pos == pos # an empty separator and item would loop forever
383
+ r = item.match(ctx, s.pos)
384
+ pos = s.pos if r.nil? && trailing
385
+ break if r.nil? || r.pos == pos # an empty separator and item would loop forever
267
386
 
268
387
  values << r.value
269
388
  pos = r.pos
@@ -271,7 +390,13 @@ module Birman
271
390
  Match.new(pos, values)
272
391
  end
273
392
 
274
- def to_ast = [:seq, nil, [item.to_ast, [:rep, 0, nil, [:seq, nil, [sep.to_ast, item.to_ast]]]]]
393
+ def to_ast
394
+ parts = [item.to_ast, [:rep, 0, nil, [:seq, nil, [sep.to_ast, item.to_ast]]]]
395
+ parts << [:rep, 0, 1, sep.to_ast] if trailing
396
+ list = [:seq, nil, parts]
397
+ min.zero? ? [:rep, 0, 1, list] : list
398
+ end
399
+
275
400
  def capture_key = item.capture_key
276
401
  end
277
402
 
@@ -291,6 +416,9 @@ module Birman
291
416
  end
292
417
 
293
418
  def to_ast = [:choice, alts.map(&:to_ast)]
419
+
420
+ # A name every alternative has passes outward; differing names don't.
421
+ def capture_key = alts.map(&:capture_key).uniq.then { _1.size == 1 ? _1.first : nil }
294
422
  end
295
423
 
296
424
  # Repetition with min/max; value is an array.
@@ -350,19 +478,57 @@ module Birman
350
478
  def to_ast = [:both, left.to_ast, right.to_ast]
351
479
  end
352
480
 
353
- # Semantic action: transform the value of a successful match.
354
- Action = Data.define(:inner, :fn) do
481
+ # Optional (see Matcher#maybe): inner's value, or nil. Never fails.
482
+ Maybe = Data.define(:inner) do
483
+ include Matcher
484
+
485
+ def match(ctx, pos) = inner.match(ctx, pos) || Match.new(pos, nil)
486
+ def to_ast = [:rep, 0, 1, inner.to_ast]
487
+ def capture_key = inner.capture_key
488
+ end
489
+
490
+ # A successful match whose value is replaced: the shared shape of
491
+ # Const, Map, Send and Action, each defining transform(value, ctx).
492
+ module Transform
355
493
  include Matcher
356
494
 
357
495
  def match(ctx, pos)
358
496
  r = inner.match(ctx, pos) or return nil
359
- Match.new(r.pos, fn.call(r.value))
497
+ Match.new(r.pos, transform(r.value, ctx))
360
498
  end
361
499
 
362
500
  def to_ast = [:action, inner.to_ast]
363
501
  def capture_key = inner.capture_key
364
502
  end
365
503
 
504
+ # See Matcher#means.
505
+ Const = Data.define(:inner, :constant) do
506
+ include Transform
507
+
508
+ def transform(_value, _ctx) = constant
509
+ end
510
+
511
+ # Transform the value with a block (see Matcher#map).
512
+ Map = Data.define(:inner, :fn) do
513
+ include Transform
514
+
515
+ def transform(value, _ctx) = fn.call(value)
516
+ end
517
+
518
+ # Transform the value by calling one of its methods (see Matcher#map).
519
+ Send = Data.define(:inner, :method_name) do
520
+ include Transform
521
+
522
+ def transform(value, _ctx) = value.public_send(method_name)
523
+ end
524
+
525
+ # Semantic action: pass the value to a grammar method (see Matcher#action).
526
+ Action = Data.define(:inner, :method_name) do
527
+ include Transform
528
+
529
+ def transform(value, ctx) = ctx.grammar.__send__(method_name, value)
530
+ end
531
+
366
532
  # Names a matcher for error messages. If inner fails (or succeeds
367
533
  # while consuming nothing) without getting past its starting
368
534
  # position, the error says "expected <name>"
@@ -387,6 +553,24 @@ module Birman
387
553
  def capture_key = inner.capture_key
388
554
  end
389
555
 
556
+ # Semantic predicate (see Matcher#check). Like Label, inner runs
557
+ # isolated, so a rejection can drop what inner recorded (a rep's
558
+ # stopping point, say), as Not does, and report only its own name.
559
+ Check = Data.define(:inner, :method_name, :name) do
560
+ include Matcher
561
+
562
+ def match(ctx, pos)
563
+ result = ctx.isolate(pos) { inner.match(ctx, pos) }
564
+ return ctx.fail_at(pos, name) if result && !ctx.grammar.__send__(method_name, result.value)
565
+
566
+ ctx.merge(ctx.isolated_furthest, ctx.isolated_expected)
567
+ result
568
+ end
569
+
570
+ def to_ast = [:check, name, inner.to_ast]
571
+ def capture_key = inner.capture_key
572
+ end
573
+
390
574
  # See Matcher#as. Passes the value through; the enclosing Seq reads
391
575
  # the key.
392
576
  Named = Data.define(:inner, :key) do
@@ -403,7 +587,10 @@ module Birman
403
587
 
404
588
  def match(ctx, pos)
405
589
  r = inner.match(ctx, pos) or return nil
406
- fields = r.value.is_a?(Hash) ? r.value : { node.members.first => r.value }
590
+ fields = if r.value.is_a?(Hash) then r.value
591
+ elsif node.members.empty? then {} # a node with no fields drops the value
592
+ else { node.members.first => r.value }
593
+ end
407
594
  Match.new(r.pos, Birman.build(node, fields, ctx, pos, r.pos))
408
595
  end
409
596
 
@@ -411,27 +598,26 @@ module Birman
411
598
  def capture_key = inner.capture_key
412
599
  end
413
600
 
414
- # A token: inner, then the grammar's ignore rule (usually spacing),
415
- # keeping inner's value. With no ignore rule it's just inner. `source`
416
- # is how a literal shows up in EBNF ("let" rather than its internals).
417
- Lexeme = Data.define(:inner, :ignore, :source) do
601
+ # A token: inner, then the ignore rule (usually spacing) of the
602
+ # grammar running it, keeping inner's value. With no ignore rule it's
603
+ # just inner. `source` is how a literal shows up in EBNF ("let"
604
+ # rather than its internals).
605
+ Lexeme = Data.define(:inner, :source) do
418
606
  include Matcher
419
607
 
420
- def initialize(inner:, ignore:, source: nil)
421
- @ignore_ref = ignore && Ref.new(ignore) # built once, not per token
422
- super
423
- end
608
+ def initialize(inner:, source: nil) = super
424
609
 
425
610
  def match(ctx, pos)
426
611
  r = inner.match(ctx, pos) or return nil
427
- return r unless ignore
612
+ ignore = ctx.ignore_ref or return r
428
613
 
429
- s = @ignore_ref.match(ctx, r.pos) or return r
614
+ s = ignore.match(ctx, r.pos) or return r
430
615
  ctx.ignored_after(r.pos, s.pos)
431
616
  Match.new(s.pos, r.value)
432
617
  end
433
618
 
434
- def to_ast = source ? [:str, source] : inner.to_ast
619
+ # Shown as a lexeme unless it's a literal with text (see Lit#to_ast).
620
+ def to_ast = source.to_s.empty? ? [:lexeme, inner.to_ast] : [:str, source]
435
621
  def capture_key = inner.capture_key
436
622
  end
437
623
 
data/lib/birman/result.rb CHANGED
@@ -36,6 +36,10 @@ module Birman
36
36
  #
37
37
  # reason: set when the failure isn't about what was expected, such as
38
38
  # input nested too deeply for Ruby's stack.
39
+ #
40
+ # Failure.at(span, reason) is one for a check made after parsing, on
41
+ # the AST: it points at a node's span, committed, with no expected
42
+ # list, so the message reads "line L, column C: reason".
39
43
  # [class.Failure]
40
44
  # purpose = "The input didn't parse"
41
45
  # fields = [
@@ -45,8 +49,13 @@ module Birman
45
49
  # "committed: Boolean (failed after a commit)",
46
50
  # "reason: String | nil (set when it isn't about expectations, e.g. nesting too deep)"
47
51
  # ]
48
- # methods = ["line", "column", "found", "message(formatter)", "value!(formatter) -> raises ParseError"]
52
+ # methods = [
53
+ # "Failure.at(span, reason) -> Failure at span's start (for checks after parsing)",
54
+ # "line", "column", "found", "message(formatter)", "value!(formatter) -> raises ParseError"
55
+ # ]
49
56
  Failure = Data.define(:input, :pos, :expected, :committed, :reason) do
57
+ def self.at(span, reason) = new(input: span.input, pos: span.from, expected: [], committed: true, reason:)
58
+
50
59
  def initialize(input:, pos:, expected:, committed:, reason: nil) = super
51
60
 
52
61
  def success? = false
@@ -38,21 +38,34 @@ module Birman
38
38
  # returns = "nil"
39
39
  # side_effects = ["Defines a rule that skips trailing spacing via the grammar's ignore rule"]
40
40
  def lexeme(name, &body)
41
- rule(name) { Lexeme.new(Birman.coerce(instance_exec(&body)), ignore_rule) }
41
+ rule(name) { Lexeme.new(Birman.coerce(instance_exec(&body))) }
42
42
  end
43
43
 
44
44
  private
45
45
 
46
46
  # A rule named after a method the grammar relies on (send, class,
47
- # match, token...) would silently replace it. Redefining a rule is
48
- # fine, and so are private Kernel names like print or select.
47
+ # match, token, or a private helper in any of Birman's modules) would
48
+ # silently replace it. Redefining a rule is fine, and so are private
49
+ # Kernel names like print or select, and the grammar's own private
50
+ # helpers: those are the grammar's to replace.
49
51
  def check_rule_name!(name)
50
52
  return if private_method_defined?(:"__rule_#{name}")
51
53
 
52
- owner = [Object, Combinators, Grammar].find { _1.public_method_defined?(name) } ||
53
- [Combinators, Grammar].find { _1.private_method_defined?(name) } ||
54
+ birman = [Object, Combinators, Grammar] | ancestors.select { _1.name&.start_with?("Birman::") }
55
+ owner = birman.find { _1.public_method_defined?(name) } ||
56
+ (birman - [Object]).find { _1.private_method_defined?(name, false) } ||
54
57
  (instance_method(name).owner if method_defined?(name))
55
- raise ArgumentError, "rule :#{name} would replace #{owner}##{name}; give the rule another name" if owner
58
+ return unless owner
59
+
60
+ raise ArgumentError, "rule :#{name} would replace #{owner}##{name}, #{why_taken(owner, birman)}, " \
61
+ "since rules are methods too; give the rule another name"
62
+ end
63
+
64
+ def why_taken(owner, birman)
65
+ if owner == Object then "a method every Ruby object has"
66
+ elsif birman.include?(owner) then "which Birman relies on"
67
+ else "a method of this grammar"
68
+ end
56
69
  end
57
70
  end
58
71
  end
data/lib/birman/rules.rb CHANGED
@@ -48,8 +48,8 @@ module Birman
48
48
  module Numbers
49
49
  extend RuleSet
50
50
 
51
- lexeme(:integer) { match(/\d+/).map(&:to_i).label("integer") }
52
- lexeme(:float) { match(/\d+\.\d+/).map(&:to_f).label("float") }
51
+ lexeme(:integer) { match(/\d+/).map(:to_i).label("integer") }
52
+ lexeme(:float) { match(/\d+\.\d+/).map(:to_f).label("float") }
53
53
  lexeme(:number) { (float | integer).label("number") }
54
54
  end
55
55
 
@@ -57,6 +57,7 @@ module Birman
57
57
  # commits, so an unterminated string is reported where it breaks.
58
58
  # [module.Rules.Strings]
59
59
  # rules = ["string: double-quoted, escapes \\n \\t \\r \\0, value is the String"]
60
+ # helpers = ["decode_escape(text): the character a backslash escape stands for"]
60
61
  # notes = "The opening quote commits, so an unterminated string is reported where it breaks"
61
62
  module Strings
62
63
  extend RuleSet
@@ -64,15 +65,19 @@ module Birman
64
65
  ESCAPES = { "n" => "\n", "t" => "\t", "r" => "\r", "0" => "\0" }.freeze
65
66
 
66
67
  lexeme(:string) do
67
- (str('"') > (string_part.rep >> str('"').label("closing quote")))
68
- .map { |(_, parts, _)| parts.join }
68
+ seq(str('"'), commit, only(string_part.rep), str('"').label("closing quote"))
69
+ .map(:join)
69
70
  .label("string")
70
71
  end
71
72
 
72
73
  rule(:string_part) do
73
- (match(/\\./).map { ESCAPES.fetch(_1[1], _1[1]) } |
74
+ (match(/\\./).action(:decode_escape) |
74
75
  match(/[^"\\\n]+/)).label("string character")
75
76
  end
77
+
78
+ private
79
+
80
+ def decode_escape(text) = ESCAPES.fetch(text[1], text[1])
76
81
  end
77
82
 
78
83
  # Identifiers that can't be reserved words. Declare those in the
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # [file]
4
+ # purpose = "Running a matcher without writing a grammar class"
5
+ # responsibility = "Birman.parse and Birman.parse!, via a cached anonymous grammar per matcher"
6
+ # dependencies = ["Birman::Grammar", "Birman::AST"]
7
+ module Birman
8
+ # Parses input with matcher as the whole grammar: Success or Failure,
9
+ # exactly as Grammar.parse, and bad input never raises.
10
+ #
11
+ # Birman.parse(Birman.seq("let", Birman.match(/\w+/)), "let x", ignore: /\s*/)
12
+ # # => #<data Birman::Success value=["let", "x"]>
13
+ #
14
+ # ignore is a matcher (a String, Regexp or Matcher), not a rule name:
15
+ # there is no grammar here for a name to refer to. Literals skip it
16
+ # after themselves, and it's skipped at the start of input.
17
+ # [method."Birman.parse"]
18
+ # params = ["matcher: String | Regexp | Matcher", "input: String", "ignore: String | Regexp | Matcher | nil"]
19
+ # returns = "Birman::Success(value) when all of input parses, else Birman::Failure"
20
+ # raises = ["ArgumentError if matcher or ignore refers to a rule, or an .action/.check method Birman lacks"]
21
+ def self.parse(matcher, input, ignore: nil) = Standalone.grammar(matcher, ignore).parse(input)
22
+
23
+ # [method."Birman.parse!"]
24
+ # params = ["matcher, input, ignore: as Birman.parse", "formatter: #call(failure) = ErrorFormatter.new"]
25
+ # returns = "The parsed value"
26
+ # raises = ["Birman::ParseError", "ArgumentError, as Birman.parse"]
27
+ def self.parse!(matcher, input, ignore: nil, **) = Standalone.grammar(matcher, ignore).parse!(input, **)
28
+
29
+ # The anonymous grammar classes behind Birman.parse: the matcher is
30
+ # the root rule and ignore the ignore rule. Matchers are immutable
31
+ # values, so equal ones share a class, and with it the per-grammar
32
+ # caches (literal matchers, rule matchers). The cache is bounded, so
33
+ # matchers built afresh for each parse (with a new .map block, say)
34
+ # don't pile up.
35
+ # [module.Standalone]
36
+ # purpose = "Builds and caches the anonymous grammar Birman.parse runs"
37
+ module Standalone
38
+ ROOT = :__standalone_root
39
+ IGNORE = :__standalone_ignore
40
+ LIMIT = 256
41
+
42
+ @grammars = {}
43
+ @lock = Mutex.new
44
+
45
+ # [method."Standalone.grammar"]
46
+ # params = ["matcher: String | Regexp | Matcher", "ignore: String | Regexp | Matcher | nil"]
47
+ # returns = "A grammar class whose root rule is matcher (to_ebnf, to_ruby and parse work on it)"
48
+ # raises = ["ArgumentError if matcher or ignore refers to a rule, or an .action/.check method Birman lacks"]
49
+ def self.grammar(matcher, ignore)
50
+ key = [matcher, ignore].freeze
51
+ @lock.synchronize do
52
+ @grammars[key] ||= begin
53
+ @grammars.shift if @grammars.size >= LIMIT
54
+ build(matcher, ignore)
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.build(matcher, ignore)
60
+ grammar = Class.new do
61
+ include Grammar
62
+
63
+ root ROOT
64
+ rule(ROOT) { matcher }
65
+ next unless ignore
66
+
67
+ rule(IGNORE) { ignore }
68
+ self.ignore IGNORE
69
+ end
70
+ check!(grammar, [matcher, ignore].compact.map { Birman.coerce(_1) })
71
+ grammar
72
+ end
73
+
74
+ # A rule reference, or an action or check calling a method only a
75
+ # grammar of your own would have, raises only when the parse reaches
76
+ # it (and the method as a NoMethodError on an anonymous class), so a
77
+ # mistake could hide in an untried alternative. Checked up front.
78
+ def self.check!(grammar, matchers)
79
+ matchers.each { each_matcher(_1) { |m| check_matcher!(grammar, m) } }
80
+ end
81
+
82
+ def self.check_matcher!(grammar, matcher)
83
+ case matcher
84
+ when Ref
85
+ raise ArgumentError, "undefined rule #{matcher.name.inspect}: " \
86
+ "a matcher run by Birman.parse can't refer to rules; define them in a grammar class"
87
+ when Action, Check
88
+ name = matcher.method_name
89
+ return if grammar.method_defined?(name) || grammar.private_method_defined?(name)
90
+
91
+ raise ArgumentError, "undefined method #{name.inspect} for .#{matcher.is_a?(Action) ? "action" : "check"}: " \
92
+ "a matcher run by Birman.parse can only use Birman's own methods there; " \
93
+ "define yours in a grammar class"
94
+ end
95
+ end
96
+
97
+ # Every matcher inside root, root included, once each.
98
+ def self.each_matcher(root)
99
+ queue = [root]
100
+ seen = {}.compare_by_identity
101
+ while (m = queue.shift)
102
+ next if seen[m]
103
+
104
+ seen[m] = true
105
+ yield m
106
+ queue.concat(m.to_h.values.flat_map { Array(_1) }.grep(Matcher)) if m.is_a?(Data)
107
+ end
108
+ end
109
+ private_class_method :build, :check!, :check_matcher!, :each_matcher
110
+ end
111
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Birman
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/birman.rb CHANGED
@@ -33,6 +33,25 @@ module Birman
33
33
  def self.char_offset(input, byte_pos)
34
34
  input.ascii_only? ? byte_pos : input.byteslice(0, byte_pos).length
35
35
  end
36
+
37
+ # The byte offset of the first byte in input that doesn't start a
38
+ # valid character, or nil if input is valid. Grammar#parse fails
39
+ # there before a regexp can raise on it. Ruby splits invalid bytes
40
+ # into characters of their own, so everything before it is valid,
41
+ # and a Failure's line and column count as usual.
42
+ # [method."Birman.invalid_byte"]
43
+ # params = ["input: String"]
44
+ # returns = "Integer | nil: the byte offset of the first invalid byte"
45
+ def self.invalid_byte(input)
46
+ return if input.valid_encoding?
47
+
48
+ pos = 0
49
+ input.each_char do |char|
50
+ return pos unless char.valid_encoding?
51
+
52
+ pos += char.bytesize
53
+ end
54
+ end
36
55
  end
37
56
 
38
57
  require "strscan"
@@ -45,6 +64,9 @@ require_relative "birman/matchers"
45
64
  require_relative "birman/rule_set"
46
65
  require_relative "birman/combinators"
47
66
  require_relative "birman/grammar"
67
+ require_relative "birman/standalone"
48
68
  require_relative "birman/error_formatter"
49
69
  require_relative "birman/ast"
70
+ require_relative "birman/left_recursion"
50
71
  require_relative "birman/rules"
72
+ require_relative "birman/compiler"