birman 0.2.1 → 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.
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # [file]
4
+ # purpose = "The matcher types and the methods every matcher responds to"
5
+ # responsibility = "Matching at a byte position against a Context; building ASTs for EBNF"
6
+ # dependencies = ["Birman::Context", "StringScanner (via Context#scanner)"]
7
+ # pattern = "Immutable Data objects composed into a tree; packrat memoization at rule refs"
8
+ # public_api = "Birman::Matcher (operators and modifiers below); the Data classes are internal"
3
9
  module Birman
10
+ # [method."Birman.coerce"]
11
+ # params = ["x: String | Regexp | Matcher"]
12
+ # returns = "Matcher: String -> literal (Lit), Regexp -> raw pattern (Pat), matcher unchanged"
13
+ # raises = ["ArgumentError (only(...) outside seq, or anything else)"]
4
14
  def self.coerce(x)
5
15
  case x
6
16
  when Matcher then x
@@ -22,41 +32,128 @@ module Birman
22
32
  #
23
33
  # So `a > b >> c` means `a > (b >> c)`, and a commit inside a choice
24
34
  # needs parentheses: `(keyword("if") > body) | other`.
35
+ # [module.Matcher]
36
+ # purpose = "Operators and modifiers shared by every matcher"
37
+ # notes = "Arguments go through Birman.coerce, so bare strings are literals"
25
38
  module Matcher
26
- def >>(other) = Seq.new([self, Birman.coerce(other)])
27
- def |(other) = Choice.new([self, Birman.coerce(other)])
39
+ # [method.">>"]
40
+ # params = ["other: String | Regexp | Matcher"]
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)
44
+
45
+ # [method."|"]
46
+ # params = ["other: String | Regexp | Matcher"]
47
+ # returns = "Ordered choice: self, else other"
48
+ def |(other) = Choice.new([self, Birman.coerce(other)])
28
49
 
29
50
  # Conjunction: self must match here (without consuming input),
30
51
  # then other is matched from the same spot. Value is other's.
52
+ # [method."&"]
53
+ # params = ["other: String | Regexp | Matcher"]
54
+ # returns = "self as a guard (lookahead), then other from the same position"
55
+ # value = "other's value"
31
56
  def &(other) = Both.new(self, Birman.coerce(other))
32
57
 
33
58
  # Commit: once self matches, everything after it must match too, or
34
59
  # parsing stops right here with an error instead of backtracking.
35
60
  # This is dry-logic's implication operator, applied to parsing.
61
+ # [method.">"]
62
+ # params = ["other: String | Regexp | Matcher"]
63
+ # returns = "Sequence of self then other, committed after self (like seq(self, commit, other))"
36
64
  def >(other)
37
- left = uncommitted_parts(self)
38
- right = uncommitted_parts(Birman.coerce(other))
65
+ left = inline_parts(self)
66
+ right = inline_parts(Birman.coerce(other))
39
67
  Seq.new([*left, *right], left.size)
40
68
  end
41
69
 
70
+ # [method.rep]
71
+ # params = ["min: Integer = 0", "max: Integer | nil = nil (unbounded)"]
72
+ # returns = "Matcher for self repeated min..max times"
73
+ # value = "Array of values; an empty first match counts once, then ends the loop"
42
74
  def rep(min = 0, max = nil) = Rep.new(self, min, max)
43
- def maybe = rep(0, 1).map(&:first)
44
- def absent = Not.new(self)
45
- def present = And.new(self)
46
- def map(&fn) = Action.new(self, fn)
75
+
76
+ # [method.maybe]
77
+ # returns = "Optional self"
78
+ # value = "self's value, or nil when absent"
79
+ def maybe = Maybe.new(self)
80
+
81
+ # [method.absent]
82
+ # returns = "Negative lookahead: succeeds, consuming nothing, when self doesn't match here"
83
+ # value = "nil"
84
+ def absent = Not.new(self)
85
+
86
+ # [method.present]
87
+ # returns = "Positive lookahead: succeeds, consuming nothing, when self matches here"
88
+ # value = "nil"
89
+ def present = And.new(self)
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.
93
+ # [method.map]
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)
131
+
132
+ # [method.label]
133
+ # params = ["name: String"]
134
+ # returns = "self, reported as \"expected <name>\" when it fails without consuming input"
47
135
  def label(name) = Label.new(self, name)
48
136
 
49
137
  # Never mention this matcher in error messages (for whitespace,
50
138
  # comments, and other things nobody wants to be told to type).
139
+ # [method.quiet]
140
+ # returns = "self, never mentioned in error messages"
51
141
  def quiet = Quiet.new(self)
52
142
 
53
143
  # Name this part of a sequence. A sequence with any named parts
54
144
  # produces a hash of just those parts instead of an array.
145
+ # [method.as]
146
+ # params = ["key: Symbol"]
147
+ # returns = "self, named: the enclosing seq's value becomes a Hash of named parts"
55
148
  def as(key) = Named.new(self, key)
56
149
 
57
150
  # Build a node from the value: a hash of named parts becomes
58
151
  # keyword arguments; anything else fills the node's first field.
59
152
  # Nodes made with Birman.node get a source span.
153
+ # [method.to]
154
+ # params = ["node: node class (or any Data class)"]
155
+ # returns = "Matcher whose value is node built from self's value"
156
+ # value = "node.new(**hash), or node.new(first_field => value); with a span for node(...) classes"
60
157
  def to(node) = To.new(self, node)
61
158
 
62
159
  # The name given by #as, seen through wrappers like labels.
@@ -64,7 +161,9 @@ module Birman
64
161
 
65
162
  private
66
163
 
67
- 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]
68
167
  end
69
168
 
70
169
  # A bare string: a literal (see Combinators#literal_matcher). What a
@@ -74,19 +173,24 @@ module Birman
74
173
  include Matcher
75
174
 
76
175
  def match(ctx, pos) = ctx.grammar.literal_matcher(text).match(ctx, pos)
77
- 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]
78
178
  end
79
179
 
80
180
  # Exact string, nothing skipped (see Combinators#str).
81
181
  Str = Data.define(:str) do
82
182
  include Matcher
83
183
 
184
+ # The error text is worked out once, not on every failed attempt.
185
+ def initialize(str:)
186
+ @expected = str.inspect.freeze
187
+ super
188
+ end
189
+
84
190
  def match(ctx, pos)
85
- if ctx.input.byteslice(pos, str.bytesize) == str
86
- Match.new(pos + str.bytesize, str)
87
- else
88
- ctx.fail_at(pos, str.inspect)
89
- end
191
+ scanner = ctx.scanner
192
+ scanner.pos = pos
193
+ scanner.match?(str) ? Match.new(pos + str.bytesize, str) : ctx.fail_at(pos, @expected)
90
194
  end
91
195
 
92
196
  def to_ast = [:str, str]
@@ -105,35 +209,66 @@ module Birman
105
209
  Match.new(pos + len, scanner.matched)
106
210
  end
107
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
+
108
218
  def to_ast = [:pat, source]
109
219
  end
110
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
+
111
245
  # Sequence: parts match in order. The value is an array, or a hash
112
246
  # of the named parts if any are named (see Matcher#as). Parts at
113
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.
114
249
  Seq = Data.define(:parts, :commit_at, :only_at) do
115
250
  include Matcher
116
251
 
117
252
  # Arrays are copied and frozen so a matcher is immutable all the way down.
118
- def initialize(parts:, commit_at: nil, only_at: nil)
119
- if only_at && parts.each_with_index.any? { |part, i| i != only_at && part.capture_key }
120
- raise ArgumentError, "a seq with only(...) can't also name parts with .as"
121
- end
122
-
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
123
258
  super(parts: parts.dup.freeze, commit_at:, only_at:)
124
259
  end
125
260
 
126
- 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)
127
265
 
128
266
  def match(ctx, pos)
129
- values = parts.each_with_index.map do |part, i|
130
- r = part.match(ctx, pos)
131
- unless r
132
- fail_committed!(ctx, part, pos) if commit_at && i >= commit_at
133
- return nil
134
- end
267
+ values = Array.new(parts.size)
268
+ parts.each_with_index do |part, i|
269
+ r = part.match(ctx, pos) or return failed(ctx, part, pos, i)
270
+ values[i] = r.value
135
271
  pos = r.pos
136
- r.value
137
272
  end
138
273
  Match.new(pos, shape(values))
139
274
  end
@@ -143,8 +278,51 @@ module Birman
143
278
  # With only(...), the kept part's name (if any) passes outward.
144
279
  def capture_key = only_at && parts[only_at].capture_key
145
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
+
146
296
  private
147
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
+
321
+ def failed(ctx, part, pos, index)
322
+ fail_committed!(ctx, part, pos) if commit_at && index >= commit_at
323
+ nil
324
+ end
325
+
148
326
  # If the failed part recorded nothing at or past pos (it's quiet,
149
327
  # or a negative lookahead), describe the part itself so the error
150
328
  # still says what was expected.
@@ -185,19 +363,26 @@ module Birman
185
363
  def capture_key = parts[index].capture_key
186
364
  end
187
365
 
188
- # One or more items separated by sep. The value is an array of the
189
- # items, and the name (see Matcher#as) is the item's.
190
- 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
191
373
  include Matcher
192
374
 
375
+ def initialize(item:, sep:, min: 1, trailing: false) = super
376
+
193
377
  def match(ctx, pos)
194
- r = item.match(ctx, pos) or return nil
378
+ r = item.match(ctx, pos) or return (min.zero? ? Match.new(pos, []) : nil)
195
379
  values = [r.value]
196
380
  pos = r.pos
197
381
  loop do
198
382
  s = sep.match(ctx, pos) or break
199
- r = item.match(ctx, s.pos) or break
200
- 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
201
386
 
202
387
  values << r.value
203
388
  pos = r.pos
@@ -205,7 +390,13 @@ module Birman
205
390
  Match.new(pos, values)
206
391
  end
207
392
 
208
- 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
+
209
400
  def capture_key = item.capture_key
210
401
  end
211
402
 
@@ -225,6 +416,9 @@ module Birman
225
416
  end
226
417
 
227
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 }
228
422
  end
229
423
 
230
424
  # Repetition with min/max; value is an array.
@@ -259,7 +453,7 @@ module Birman
259
453
  include Matcher
260
454
 
261
455
  def match(ctx, pos)
262
- r, = ctx.isolate(pos) { ctx.lookahead { inner.match(ctx, pos) } }
456
+ r = ctx.isolate(pos) { ctx.lookahead { inner.match(ctx, pos) } }
263
457
  r ? nil : Match.new(pos, nil)
264
458
  end
265
459
 
@@ -284,19 +478,57 @@ module Birman
284
478
  def to_ast = [:both, left.to_ast, right.to_ast]
285
479
  end
286
480
 
287
- # Semantic action: transform the value of a successful match.
288
- 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
289
493
  include Matcher
290
494
 
291
495
  def match(ctx, pos)
292
496
  r = inner.match(ctx, pos) or return nil
293
- r.with(value: fn.call(r.value))
497
+ Match.new(r.pos, transform(r.value, ctx))
294
498
  end
295
499
 
296
500
  def to_ast = [:action, inner.to_ast]
297
501
  def capture_key = inner.capture_key
298
502
  end
299
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
+
300
532
  # Names a matcher for error messages. If inner fails (or succeeds
301
533
  # while consuming nothing) without getting past its starting
302
534
  # position, the error says "expected <name>"
@@ -306,7 +538,9 @@ module Birman
306
538
  include Matcher
307
539
 
308
540
  def match(ctx, pos)
309
- result, furthest, expected = ctx.isolate(pos) { inner.match(ctx, pos) }
541
+ result = ctx.isolate(pos) { inner.match(ctx, pos) }
542
+ furthest = ctx.isolated_furthest
543
+ expected = ctx.isolated_expected
310
544
  if furthest == pos && (result.nil? || (result.pos == pos && expected.any?))
311
545
  ctx.fail_at(pos, name)
312
546
  else
@@ -319,6 +553,24 @@ module Birman
319
553
  def capture_key = inner.capture_key
320
554
  end
321
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
+
322
574
  # See Matcher#as. Passes the value through; the enclosing Seq reads
323
575
  # the key.
324
576
  Named = Data.define(:inner, :key) do
@@ -335,32 +587,37 @@ module Birman
335
587
 
336
588
  def match(ctx, pos)
337
589
  r = inner.match(ctx, pos) or return nil
338
- fields = r.value.is_a?(Hash) ? r.value : { node.members.first => r.value }
339
- r.with(value: Birman.build(node, fields, ctx, pos, r.pos))
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
594
+ Match.new(r.pos, Birman.build(node, fields, ctx, pos, r.pos))
340
595
  end
341
596
 
342
597
  def to_ast = [:action, inner.to_ast]
343
598
  def capture_key = inner.capture_key
344
599
  end
345
600
 
346
- # A token: inner, then the grammar's ignore rule (usually spacing),
347
- # keeping inner's value. With no ignore rule it's just inner. `source`
348
- # is how a literal shows up in EBNF ("let" rather than its internals).
349
- 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
350
606
  include Matcher
351
607
 
352
- def initialize(inner:, ignore:, source: nil) = super
608
+ def initialize(inner:, source: nil) = super
353
609
 
354
610
  def match(ctx, pos)
355
611
  r = inner.match(ctx, pos) or return nil
356
- return r unless ignore
612
+ ignore = ctx.ignore_ref or return r
357
613
 
358
- s = Ref.new(ignore).match(ctx, r.pos) or return r
614
+ s = ignore.match(ctx, r.pos) or return r
359
615
  ctx.ignored_after(r.pos, s.pos)
360
616
  Match.new(s.pos, r.value)
361
617
  end
362
618
 
363
- 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]
364
621
  def capture_key = inner.capture_key
365
622
  end
366
623
 
@@ -398,8 +655,7 @@ module Birman
398
655
  include Matcher
399
656
 
400
657
  def match(ctx, pos)
401
- result, = ctx.isolate(pos) { inner.match(ctx, pos) }
402
- result
658
+ ctx.isolate(pos) { inner.match(ctx, pos) }
403
659
  end
404
660
 
405
661
  def to_ast = [:quiet, inner.to_ast]
@@ -414,7 +670,8 @@ module Birman
414
670
 
415
671
  def match(ctx, pos)
416
672
  result, furthest, expected = ctx.memoize(name, pos) do
417
- ctx.isolate(pos) { ctx.grammar.matcher_for(name).match(ctx, pos) }
673
+ r = ctx.isolate(pos) { ctx.grammar.matcher_for(name).match(ctx, pos) }
674
+ [r, ctx.isolated_furthest, ctx.isolated_expected]
418
675
  end
419
676
  ctx.merge(furthest, expected)
420
677
  result
data/lib/birman/nodes.rb CHANGED
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # [file]
4
+ # purpose = "AST nodes with source spans"
5
+ # responsibility = "node(...) classes, Span, and building nodes during a parse"
6
+ # pattern = "Data classes plus a mixin"
3
7
  module Birman
4
8
  # Where a node came from in the source. Stored as byte offsets (see
5
9
  # Context); from, to, line and column count characters.
10
+ # [class.Span]
11
+ # purpose = "Where a node came from in the input"
12
+ # fields = ["input: String", "byte_from, byte_to: Integer (byte offsets)"]
13
+ # methods = ["from, to: character offsets", "text", "line", "column", "to_s => \"line L, column C\""]
6
14
  Span = Data.define(:input, :byte_from, :byte_to) do
7
15
  def from = Birman.char_offset(input, byte_from)
8
16
  def to = Birman.char_offset(input, byte_to)
@@ -22,6 +30,9 @@ module Birman
22
30
  # not a Data member, so equality and pattern matching ignore it:
23
31
  # `in Num(value)` still works, and two nodes with the same contents
24
32
  # are equal wherever they came from.
33
+ # [module.Node]
34
+ # purpose = "Mixed into every node(...) class"
35
+ # adds = ["span (ignored by == and pattern matching)", "with(**changes) keeping the span", "s-expression inspect"]
25
36
  module Node
26
37
  attr_reader :span
27
38
 
@@ -61,7 +72,13 @@ module Birman
61
72
  # end
62
73
  #
63
74
  # Also available as Birman.node.
75
+ # [module.Nodes]
76
+ # purpose = "Provides node(...); grammars get it automatically, other modules can extend it"
64
77
  module Nodes
78
+ # [method.node]
79
+ # params = ["*members: Symbol (field names; not :span)", "body: optional block of methods (may define initialize)"]
80
+ # returns = "A Data class including Birman::Node"
81
+ # raises = ["ArgumentError if a field is named span"]
65
82
  def node(*members, &body)
66
83
  raise ArgumentError, "a node can't have a field named span: every node already has one" if members.include?(:span)
67
84
 
data/lib/birman/result.rb CHANGED
@@ -1,10 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # [file]
4
+ # purpose = "What a parse returns"
5
+ # responsibility = "Success and Failure values, and the ParseError that parse! raises"
6
+ # pattern = "Result objects (Data), pattern-matchable"
3
7
  module Birman
4
- # Internal: one matcher succeeded, ending at `pos` with `value`.
5
- Match = Data.define(:pos, :value)
8
+ # Internal: one matcher succeeded, ending at `pos` with `value`. A
9
+ # plain class rather than Data: one is made for every successful
10
+ # match, and Data.new allocates a keyword Hash each time.
11
+ class Match
12
+ attr_reader :pos, :value
13
+
14
+ def initialize(pos, value)
15
+ @pos = pos
16
+ @value = value
17
+ end
18
+ end
6
19
 
7
20
  # Returned by Grammar#parse when the whole input parsed.
21
+ # [class.Success]
22
+ # purpose = "The whole input parsed"
23
+ # fields = ["value: whatever the root rule produced"]
24
+ # usage = "case grammar.parse(input) in Birman::Success(value) then ..."
8
25
  Success = Data.define(:value) do
9
26
  def success? = true
10
27
  def failure? = false
@@ -19,7 +36,26 @@ module Birman
19
36
  #
20
37
  # reason: set when the failure isn't about what was expected, such as
21
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".
43
+ # [class.Failure]
44
+ # purpose = "The input didn't parse"
45
+ # fields = [
46
+ # "input: String",
47
+ # "pos: Integer (character offset of the furthest failure)",
48
+ # "expected: Array of String (what would have matched there)",
49
+ # "committed: Boolean (failed after a commit)",
50
+ # "reason: String | nil (set when it isn't about expectations, e.g. nesting too deep)"
51
+ # ]
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
+ # ]
22
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
+
23
59
  def initialize(input:, pos:, expected:, committed:, reason: nil) = super
24
60
 
25
61
  def success? = false
@@ -42,6 +78,9 @@ module Birman
42
78
  end
43
79
 
44
80
  # Raised by parse! (and Failure#value!) with the formatted message.
81
+ # [class.ParseError]
82
+ # purpose = "Raised by parse! and Failure#value!"
83
+ # fields = ["failure: Birman::Failure", "message: formatted by the formatter given"]
45
84
  class ParseError < StandardError
46
85
  attr_reader :failure
47
86