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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +214 -0
- data/README.md +405 -15
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +105 -13
- 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 +151 -29
- data/lib/birman/error_formatter.rb +6 -0
- data/lib/birman/grammar.rb +139 -17
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +310 -53
- data/lib/birman/nodes.rb +17 -0
- data/lib/birman/result.rb +41 -2
- data/lib/birman/rule_set.rb +38 -6
- data/lib/birman/rules.rb +27 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +30 -0
- metadata +11 -1
data/lib/birman/matchers.rb
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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 =
|
|
38
|
-
right =
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
#
|
|
189
|
-
#
|
|
190
|
-
|
|
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)
|
|
200
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
288
|
-
|
|
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.
|
|
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
|
|
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)
|
|
339
|
-
|
|
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
|
|
347
|
-
# keeping inner's value. With no ignore rule it's
|
|
348
|
-
# is how a literal shows up in EBNF ("let"
|
|
349
|
-
|
|
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:,
|
|
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
|
|
612
|
+
ignore = ctx.ignore_ref or return r
|
|
357
613
|
|
|
358
|
-
s =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|