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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +204 -0
- data/README.md +381 -14
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +25 -14
- data/lib/birman/compiler/actions.rb +309 -0
- data/lib/birman/compiler/emitter.rb +940 -0
- data/lib/birman/compiler/literals.rb +29 -0
- data/lib/birman/compiler/nodes.rb +158 -0
- data/lib/birman/compiler/regexps.rb +182 -0
- data/lib/birman/compiler/runtime.rb +758 -0
- data/lib/birman/compiler/types.rb +128 -0
- data/lib/birman/compiler.rb +155 -0
- data/lib/birman/context.rb +124 -15
- data/lib/birman/grammar.rb +93 -16
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +224 -38
- data/lib/birman/result.rb +10 -1
- data/lib/birman/rule_set.rb +19 -6
- data/lib/birman/rules.rb +10 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +22 -0
- metadata +11 -1
data/lib/birman/matchers.rb
CHANGED
|
@@ -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
|
|
42
|
-
|
|
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 =
|
|
65
|
-
right =
|
|
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 =
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
#
|
|
255
|
-
#
|
|
256
|
-
|
|
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)
|
|
266
|
-
|
|
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
|
|
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
|
-
#
|
|
354
|
-
|
|
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,
|
|
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)
|
|
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
|
|
415
|
-
# keeping inner's value. With no ignore rule it's
|
|
416
|
-
# is how a literal shows up in EBNF ("let"
|
|
417
|
-
|
|
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:,
|
|
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
|
|
612
|
+
ignore = ctx.ignore_ref or return r
|
|
428
613
|
|
|
429
|
-
s =
|
|
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
|
-
|
|
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 = [
|
|
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
|
data/lib/birman/rule_set.rb
CHANGED
|
@@ -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))
|
|
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
|
|
48
|
-
# fine, and so are private
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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(
|
|
52
|
-
lexeme(:float) { match(/\d+\.\d+/).map(
|
|
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('"')
|
|
68
|
-
.map
|
|
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(/\\./).
|
|
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
|
data/lib/birman/version.rb
CHANGED
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"
|