birman 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +193 -0
- data/lib/birman/ast.rb +80 -0
- data/lib/birman/combinators.rb +75 -0
- data/lib/birman/context.rb +123 -0
- data/lib/birman/error_formatter.rb +38 -0
- data/lib/birman/grammar.rb +122 -0
- data/lib/birman/matchers.rb +396 -0
- data/lib/birman/nodes.rb +90 -0
- data/lib/birman/result.rb +65 -0
- data/lib/birman/rule_set.rb +39 -0
- data/lib/birman/rules.rb +77 -0
- data/lib/birman/version.rb +5 -0
- data/lib/birman.rb +42 -0
- metadata +60 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# Include this to make a class a grammar. The class body also gets
|
|
5
|
+
# `node` (see Nodes), so AST nodes can live beside their rules.
|
|
6
|
+
#
|
|
7
|
+
# Including it in a module makes a module of shared rules: whatever
|
|
8
|
+
# includes that module (a class, or another such module) becomes a
|
|
9
|
+
# grammar too.
|
|
10
|
+
module Grammar
|
|
11
|
+
include Combinators
|
|
12
|
+
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.extend(RuleSet)
|
|
15
|
+
base.extend(Nodes)
|
|
16
|
+
base.extend(base.is_a?(Class) ? ClassMethods : Shared)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Extended onto a module that includes Grammar.
|
|
20
|
+
module Shared
|
|
21
|
+
def included(base)
|
|
22
|
+
super
|
|
23
|
+
base.include(Grammar)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Settings are inherited by subclasses. Passing nil clears an
|
|
28
|
+
# inherited one: `ignore nil`, `keywords nil`.
|
|
29
|
+
module ClassMethods
|
|
30
|
+
UNSET = Object.new.freeze
|
|
31
|
+
|
|
32
|
+
# The rule a parse starts from.
|
|
33
|
+
def root(name = UNSET) = setting(:root, name)
|
|
34
|
+
|
|
35
|
+
# A rule the parser matches and discards after every literal and
|
|
36
|
+
# lexeme, and at the start of input. Usually :spacing (from
|
|
37
|
+
# Birman::Rules::Whitespace or Comments).
|
|
38
|
+
def ignore(name = UNSET) = setting(:ignore, name)
|
|
39
|
+
|
|
40
|
+
# Reserved words, which Birman::Rules::Identifiers won't accept.
|
|
41
|
+
def keywords(*words)
|
|
42
|
+
return setting(:keywords) || [] if words.empty?
|
|
43
|
+
|
|
44
|
+
setting(:keywords, words == [nil] ? [] : words.map(&:to_s))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# These share one instance, so each rule's matcher is built once
|
|
48
|
+
# rather than on every parse. Defining a rule or changing a
|
|
49
|
+
# setting starts a fresh one.
|
|
50
|
+
def parse(input) = instance.parse(input)
|
|
51
|
+
def parse!(input, **) = instance.parse!(input, **)
|
|
52
|
+
def to_ast = instance.to_ast
|
|
53
|
+
def to_ebnf = EBNF.call(to_ast)
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def instance = @instance ||= new
|
|
58
|
+
|
|
59
|
+
def setting(key, value = UNSET)
|
|
60
|
+
ivar = :"@#{key}"
|
|
61
|
+
unless value.equal?(UNSET)
|
|
62
|
+
@instance = nil
|
|
63
|
+
return instance_variable_set(ivar, value)
|
|
64
|
+
end
|
|
65
|
+
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
|
66
|
+
|
|
67
|
+
superclass.public_send(key) if superclass.respond_to?(key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def keywords = self.class.keywords
|
|
72
|
+
|
|
73
|
+
# Returns Success(value) or Failure(...). Never raises on bad input.
|
|
74
|
+
def parse(input)
|
|
75
|
+
ctx = Context.new(input, self)
|
|
76
|
+
start = (ignore_rule && Ref.new(ignore_rule).match(ctx, 0)&.pos) || 0
|
|
77
|
+
result = Ref.new(root_rule).match(ctx, start)
|
|
78
|
+
return Success.new(result.value) if result && result.pos == input.bytesize
|
|
79
|
+
|
|
80
|
+
ctx.fail_at(result.pos, "end of input") if result
|
|
81
|
+
build_failure(input, ctx.furthest, ctx.expected)
|
|
82
|
+
rescue Committed => e
|
|
83
|
+
build_failure(input, e.pos, e.expected, committed: true)
|
|
84
|
+
rescue SystemStackError
|
|
85
|
+
build_failure(input, ctx.position, [], reason: "input is nested too deeply")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the value or raises ParseError, worded by formatter.
|
|
89
|
+
def parse!(input, formatter: ErrorFormatter.new) = parse(input).value!(formatter)
|
|
90
|
+
|
|
91
|
+
def matcher_for(name)
|
|
92
|
+
(@matchers ||= {})[name] ||= begin
|
|
93
|
+
body = :"__rule_#{name}"
|
|
94
|
+
raise ArgumentError, "undefined rule :#{name}" unless respond_to?(body, true)
|
|
95
|
+
|
|
96
|
+
Birman.coerce(send(body))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# { rule_name => ast } for every rule reachable from the root (and
|
|
101
|
+
# the ignore rule), in first-use order.
|
|
102
|
+
def to_ast
|
|
103
|
+
rules = {}
|
|
104
|
+
queue = [root_rule, ignore_rule].compact
|
|
105
|
+
while (name = queue.shift)
|
|
106
|
+
next if rules.key?(name)
|
|
107
|
+
|
|
108
|
+
rules[name] = matcher_for(name).to_ast
|
|
109
|
+
queue.concat(AST.refs(rules[name]))
|
|
110
|
+
end
|
|
111
|
+
rules
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def build_failure(input, byte_pos, expected, committed: false, reason: nil)
|
|
117
|
+
Failure.new(input:, pos: Birman.char_offset(input, byte_pos), expected: expected.uniq, committed:, reason:)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def root_rule = self.class.root || raise(ArgumentError, "#{self.class} has no root rule")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
def self.coerce(x)
|
|
5
|
+
case x
|
|
6
|
+
when Matcher then x
|
|
7
|
+
when String then Str.new(x)
|
|
8
|
+
when Regexp then Pat.new(x)
|
|
9
|
+
else raise ArgumentError, "can't make a matcher from #{x.inspect}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Combinator operators, mixed into every matcher type.
|
|
14
|
+
#
|
|
15
|
+
# Ruby's operator precedence shapes the notation, tightest first:
|
|
16
|
+
#
|
|
17
|
+
# >> sequence
|
|
18
|
+
# & conjunction
|
|
19
|
+
# | ordered choice
|
|
20
|
+
# > commit
|
|
21
|
+
#
|
|
22
|
+
# So `a > b >> c` means `a > (b >> c)`, and a commit inside a choice
|
|
23
|
+
# needs parentheses: `(keyword("if") > body) | other`.
|
|
24
|
+
module Matcher
|
|
25
|
+
def >>(other) = Seq.new([self, Birman.coerce(other)])
|
|
26
|
+
def |(other) = Choice.new([self, Birman.coerce(other)])
|
|
27
|
+
|
|
28
|
+
# Conjunction: self must match here (without consuming input),
|
|
29
|
+
# then other is matched from the same spot. Value is other's.
|
|
30
|
+
def &(other) = Both.new(self, Birman.coerce(other))
|
|
31
|
+
|
|
32
|
+
# Commit: once self matches, everything after it must match too, or
|
|
33
|
+
# parsing stops right here with an error instead of backtracking.
|
|
34
|
+
# This is dry-logic's implication operator, applied to parsing.
|
|
35
|
+
def >(other)
|
|
36
|
+
left = uncommitted_parts(self)
|
|
37
|
+
right = uncommitted_parts(Birman.coerce(other))
|
|
38
|
+
Seq.new([*left, *right], left.size)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rep(min = 0, max = nil) = Rep.new(self, min, max)
|
|
42
|
+
def maybe = rep(0, 1).map(&:first)
|
|
43
|
+
def absent = Not.new(self)
|
|
44
|
+
def present = And.new(self)
|
|
45
|
+
def map(&fn) = Action.new(self, fn)
|
|
46
|
+
def label(name) = Label.new(self, name)
|
|
47
|
+
|
|
48
|
+
# Never mention this matcher in error messages (for whitespace,
|
|
49
|
+
# comments, and other things nobody wants to be told to type).
|
|
50
|
+
def quiet = Quiet.new(self)
|
|
51
|
+
|
|
52
|
+
# Name this part of a sequence. A sequence with any named parts
|
|
53
|
+
# produces a hash of just those parts instead of an array.
|
|
54
|
+
def as(key) = Named.new(self, key)
|
|
55
|
+
|
|
56
|
+
# Build a node from the value: a hash of named parts becomes
|
|
57
|
+
# keyword arguments; anything else fills the node's first field.
|
|
58
|
+
# Nodes made with Birman.node get a source span.
|
|
59
|
+
def to(node) = To.new(self, node)
|
|
60
|
+
|
|
61
|
+
# The name given by #as, seen through wrappers like labels.
|
|
62
|
+
def capture_key = nil
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def uncommitted_parts(m) = m.is_a?(Seq) && m.commit_at.nil? ? m.parts : [m]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Literal string.
|
|
70
|
+
Str = Data.define(:str) do
|
|
71
|
+
include Matcher
|
|
72
|
+
|
|
73
|
+
def match(ctx, pos)
|
|
74
|
+
if ctx.input.byteslice(pos, str.bytesize) == str
|
|
75
|
+
Match.new(pos + str.bytesize, str)
|
|
76
|
+
else
|
|
77
|
+
ctx.fail_at(pos, str.inspect)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_ast = [:str, str]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Regex, anchored at the current position with \G.
|
|
85
|
+
Pat = Data.define(:re, :source) do
|
|
86
|
+
include Matcher
|
|
87
|
+
|
|
88
|
+
def initialize(re:, source: re.inspect) = super
|
|
89
|
+
|
|
90
|
+
def match(ctx, pos)
|
|
91
|
+
scanner = ctx.scanner
|
|
92
|
+
scanner.pos = pos
|
|
93
|
+
len = scanner.match?(re) or return ctx.fail_at(pos, source)
|
|
94
|
+
Match.new(pos + len, scanner.matched)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_ast = [:pat, source]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sequence: parts match in order. The value is an array, or a hash
|
|
101
|
+
# of the named parts if any are named (see Matcher#as). Parts at
|
|
102
|
+
# index `commit_at` and beyond are committed: if one fails, parsing stops.
|
|
103
|
+
Seq = Data.define(:parts, :commit_at) do
|
|
104
|
+
include Matcher
|
|
105
|
+
|
|
106
|
+
# Arrays are copied and frozen so a matcher is immutable all the way down.
|
|
107
|
+
def initialize(parts:, commit_at: nil) = super(parts: parts.dup.freeze, commit_at:)
|
|
108
|
+
|
|
109
|
+
def >>(other) = with(parts: [*parts, Birman.coerce(other)])
|
|
110
|
+
|
|
111
|
+
def match(ctx, pos)
|
|
112
|
+
values = parts.each_with_index.map do |part, i|
|
|
113
|
+
r = part.match(ctx, pos)
|
|
114
|
+
unless r
|
|
115
|
+
fail_committed!(ctx, part, pos) if commit_at && i >= commit_at
|
|
116
|
+
return nil
|
|
117
|
+
end
|
|
118
|
+
pos = r.pos
|
|
119
|
+
r.value
|
|
120
|
+
end
|
|
121
|
+
Match.new(pos, shape(values))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_ast = [:seq, commit_at, parts.map(&:to_ast)]
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
# If the failed part recorded nothing at or past pos (it's quiet,
|
|
129
|
+
# or a negative lookahead), describe the part itself so the error
|
|
130
|
+
# still says what was expected.
|
|
131
|
+
def fail_committed!(ctx, part, pos)
|
|
132
|
+
ctx.fail_at(pos, EBNF.expr(part.to_ast, EBNF::CHOICE)) if ctx.furthest < pos || ctx.expected.empty?
|
|
133
|
+
ctx.commit_failure!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def shape(values)
|
|
137
|
+
keys = parts.map(&:capture_key)
|
|
138
|
+
return values if keys.none?
|
|
139
|
+
|
|
140
|
+
keys.zip(values).select(&:first).to_h
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parts in order, keeping only the value (and the name, see
|
|
145
|
+
# Matcher#as) of the part at index. Unlike Seq it never flattens
|
|
146
|
+
# or reshapes, so the kept part comes through as it was.
|
|
147
|
+
Pick = Data.define(:parts, :index) do
|
|
148
|
+
include Matcher
|
|
149
|
+
|
|
150
|
+
def initialize(parts:, index:) = super(parts: parts.dup.freeze, index:)
|
|
151
|
+
|
|
152
|
+
def match(ctx, pos)
|
|
153
|
+
value = nil
|
|
154
|
+
parts.each_with_index do |part, i|
|
|
155
|
+
r = part.match(ctx, pos) or return nil
|
|
156
|
+
value = r.value if i == index
|
|
157
|
+
pos = r.pos
|
|
158
|
+
end
|
|
159
|
+
Match.new(pos, value)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def to_ast = [:seq, nil, parts.map(&:to_ast)]
|
|
163
|
+
def capture_key = parts[index].capture_key
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# One or more items separated by sep. The value is an array of the
|
|
167
|
+
# items, and the name (see Matcher#as) is the item's.
|
|
168
|
+
SepBy = Data.define(:item, :sep) do
|
|
169
|
+
include Matcher
|
|
170
|
+
|
|
171
|
+
def match(ctx, pos)
|
|
172
|
+
r = item.match(ctx, pos) or return nil
|
|
173
|
+
values = [r.value]
|
|
174
|
+
pos = r.pos
|
|
175
|
+
loop do
|
|
176
|
+
s = sep.match(ctx, pos) or break
|
|
177
|
+
r = item.match(ctx, s.pos) or break
|
|
178
|
+
break if r.pos == pos # an empty separator and item would loop forever
|
|
179
|
+
|
|
180
|
+
values << r.value
|
|
181
|
+
pos = r.pos
|
|
182
|
+
end
|
|
183
|
+
Match.new(pos, values)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def to_ast = [:seq, nil, [item.to_ast, [:rep, 0, nil, [:seq, nil, [sep.to_ast, item.to_ast]]]]]
|
|
187
|
+
def capture_key = item.capture_key
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Ordered choice: the first alternative that matches wins.
|
|
191
|
+
Choice = Data.define(:alts) do
|
|
192
|
+
include Matcher
|
|
193
|
+
|
|
194
|
+
def initialize(alts:) = super(alts: alts.dup.freeze)
|
|
195
|
+
|
|
196
|
+
def |(other) = with(alts: [*alts, Birman.coerce(other)])
|
|
197
|
+
|
|
198
|
+
def match(ctx, pos)
|
|
199
|
+
alts.each do |alt|
|
|
200
|
+
r = alt.match(ctx, pos) and return r
|
|
201
|
+
end
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def to_ast = [:choice, alts.map(&:to_ast)]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Repetition with min/max; value is an array.
|
|
209
|
+
Rep = Data.define(:inner, :min, :max) do
|
|
210
|
+
include Matcher
|
|
211
|
+
|
|
212
|
+
def match(ctx, pos)
|
|
213
|
+
values = []
|
|
214
|
+
while max.nil? || values.size < max
|
|
215
|
+
r = inner.match(ctx, pos) or break
|
|
216
|
+
break if r.pos == pos # an empty match would loop forever
|
|
217
|
+
|
|
218
|
+
values << r.value
|
|
219
|
+
pos = r.pos
|
|
220
|
+
end
|
|
221
|
+
values.size >= min ? Match.new(pos, values) : nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def to_ast = [:rep, min, max, inner.to_ast]
|
|
225
|
+
def capture_key = inner.capture_key
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Negative lookahead. The inner attempt's failures are discarded,
|
|
229
|
+
# since failing is exactly what we hoped for.
|
|
230
|
+
Not = Data.define(:inner) do
|
|
231
|
+
include Matcher
|
|
232
|
+
|
|
233
|
+
def match(ctx, pos)
|
|
234
|
+
r, = ctx.isolate(pos) { ctx.lookahead { inner.match(ctx, pos) } }
|
|
235
|
+
r ? nil : Match.new(pos, nil)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def to_ast = [:not, inner.to_ast]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Positive lookahead: succeeds without consuming if inner matches.
|
|
242
|
+
And = Data.define(:inner) do
|
|
243
|
+
include Matcher
|
|
244
|
+
|
|
245
|
+
def match(ctx, pos) = ctx.peek?(inner, pos) ? Match.new(pos, nil) : nil
|
|
246
|
+
|
|
247
|
+
def to_ast = [:and, inner.to_ast]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Conjunction (see Matcher#&).
|
|
251
|
+
Both = Data.define(:left, :right) do
|
|
252
|
+
include Matcher
|
|
253
|
+
|
|
254
|
+
def match(ctx, pos) = ctx.peek?(left, pos) ? right.match(ctx, pos) : nil
|
|
255
|
+
|
|
256
|
+
def to_ast = [:both, left.to_ast, right.to_ast]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Semantic action: transform the value of a successful match.
|
|
260
|
+
Action = Data.define(:inner, :fn) do
|
|
261
|
+
include Matcher
|
|
262
|
+
|
|
263
|
+
def match(ctx, pos)
|
|
264
|
+
r = inner.match(ctx, pos) or return nil
|
|
265
|
+
r.with(value: fn.call(r.value))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def to_ast = [:action, inner.to_ast]
|
|
269
|
+
def capture_key = inner.capture_key
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Names a matcher for error messages. If inner fails without getting
|
|
273
|
+
# past its starting position, the error says "expected <name>"
|
|
274
|
+
# instead of listing every low-level alternative. If it got further,
|
|
275
|
+
# the deeper (more specific) error is kept.
|
|
276
|
+
Label = Data.define(:inner, :name) do
|
|
277
|
+
include Matcher
|
|
278
|
+
|
|
279
|
+
def match(ctx, pos)
|
|
280
|
+
result, furthest, expected = ctx.isolate(pos) { inner.match(ctx, pos) }
|
|
281
|
+
if result.nil? && furthest == pos
|
|
282
|
+
ctx.fail_at(pos, name)
|
|
283
|
+
else
|
|
284
|
+
ctx.merge(furthest, expected)
|
|
285
|
+
end
|
|
286
|
+
result
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def to_ast = [:label, name, inner.to_ast]
|
|
290
|
+
def capture_key = inner.capture_key
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# See Matcher#as. Passes the value through; the enclosing Seq reads
|
|
294
|
+
# the key.
|
|
295
|
+
Named = Data.define(:inner, :key) do
|
|
296
|
+
include Matcher
|
|
297
|
+
|
|
298
|
+
def match(ctx, pos) = inner.match(ctx, pos)
|
|
299
|
+
def to_ast = inner.to_ast
|
|
300
|
+
def capture_key = key
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# See Matcher#to.
|
|
304
|
+
To = Data.define(:inner, :node) do
|
|
305
|
+
include Matcher
|
|
306
|
+
|
|
307
|
+
def match(ctx, pos)
|
|
308
|
+
r = inner.match(ctx, pos) or return nil
|
|
309
|
+
fields = r.value.is_a?(Hash) ? r.value : { node.members.first => r.value }
|
|
310
|
+
r.with(value: Birman.build(node, fields, ctx, pos, r.pos))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def to_ast = [:action, inner.to_ast]
|
|
314
|
+
def capture_key = inner.capture_key
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A token: inner, then the grammar's ignore rule (usually spacing),
|
|
318
|
+
# keeping inner's value. With no ignore rule it's just inner. `source`
|
|
319
|
+
# is how a literal shows up in EBNF ("let" rather than its internals).
|
|
320
|
+
Lexeme = Data.define(:inner, :ignore, :source) do
|
|
321
|
+
include Matcher
|
|
322
|
+
|
|
323
|
+
def initialize(inner:, ignore:, source: nil) = super
|
|
324
|
+
|
|
325
|
+
def match(ctx, pos)
|
|
326
|
+
r = inner.match(ctx, pos) or return nil
|
|
327
|
+
return r unless ignore
|
|
328
|
+
|
|
329
|
+
s = Ref.new(ignore).match(ctx, r.pos) or return r
|
|
330
|
+
ctx.ignored_after(r.pos, s.pos)
|
|
331
|
+
Match.new(s.pos, r.value)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def to_ast = source ? [:str, source] : inner.to_ast
|
|
335
|
+
def capture_key = inner.capture_key
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Left-associative binary operators: operand (op operand)*, folded
|
|
339
|
+
# into nodes built as node.new(op:, left:, right:), so the node needs
|
|
340
|
+
# exactly those fields, in any order.
|
|
341
|
+
# Each node's span covers its whole subexpression.
|
|
342
|
+
Infix = Data.define(:operand, :op, :node) do
|
|
343
|
+
include Matcher
|
|
344
|
+
|
|
345
|
+
def match(ctx, pos)
|
|
346
|
+
start = pos
|
|
347
|
+
r = operand.match(ctx, pos) or return nil
|
|
348
|
+
left = r.value
|
|
349
|
+
pos = r.pos
|
|
350
|
+
|
|
351
|
+
loop do
|
|
352
|
+
o = op.match(ctx, pos) or break
|
|
353
|
+
right = operand.match(ctx, o.pos) or break
|
|
354
|
+
fields = { op: o.value, left:, right: right.value }
|
|
355
|
+
left = Birman.build(node, fields, ctx, start, right.pos)
|
|
356
|
+
pos = right.pos
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
Match.new(pos, left)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def to_ast
|
|
363
|
+
[:seq, nil, [operand.to_ast, [:rep, 0, nil, [:seq, nil, [op.to_ast, operand.to_ast]]]]]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# See Matcher#quiet.
|
|
368
|
+
Quiet = Data.define(:inner) do
|
|
369
|
+
include Matcher
|
|
370
|
+
|
|
371
|
+
def match(ctx, pos)
|
|
372
|
+
result, = ctx.isolate(pos) { inner.match(ctx, pos) }
|
|
373
|
+
result
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def to_ast = [:quiet, inner.to_ast]
|
|
377
|
+
def capture_key = inner.capture_key
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# A reference to a named rule, resolved through the grammar carried
|
|
381
|
+
# in the context. Each (rule, position) is parsed once and memoized
|
|
382
|
+
# along with its diagnostics: packrat parsing.
|
|
383
|
+
Ref = Data.define(:name) do
|
|
384
|
+
include Matcher
|
|
385
|
+
|
|
386
|
+
def match(ctx, pos)
|
|
387
|
+
result, furthest, expected = ctx.memoize(name, pos) do
|
|
388
|
+
ctx.isolate(pos) { ctx.grammar.matcher_for(name).match(ctx, pos) }
|
|
389
|
+
end
|
|
390
|
+
ctx.merge(furthest, expected)
|
|
391
|
+
result
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def to_ast = [:ref, name]
|
|
395
|
+
end
|
|
396
|
+
end
|
data/lib/birman/nodes.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# Where a node came from in the source. Stored as byte offsets (see
|
|
5
|
+
# Context); from, to, line and column count characters.
|
|
6
|
+
Span = Data.define(:input, :byte_from, :byte_to) do
|
|
7
|
+
def from = Birman.char_offset(input, byte_from)
|
|
8
|
+
def to = Birman.char_offset(input, byte_to)
|
|
9
|
+
def text = input.byteslice(byte_from...byte_to)
|
|
10
|
+
def line = before.count("\n") + 1
|
|
11
|
+
def column = before.length - (before.rindex("\n") || -1)
|
|
12
|
+
def to_s = "line #{line}, column #{column}"
|
|
13
|
+
def inspect = "#<Birman::Span #{line}:#{column} #{text.inspect}>"
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def before = input.byteslice(0, byte_from)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Mixed into every class made with Nodes#node. Adds an optional
|
|
21
|
+
# source span that the parser fills in. The span is deliberately
|
|
22
|
+
# not a Data member, so equality and pattern matching ignore it:
|
|
23
|
+
# `in Num(value)` still works, and two nodes with the same contents
|
|
24
|
+
# are equal wherever they came from.
|
|
25
|
+
module Node
|
|
26
|
+
attr_reader :span
|
|
27
|
+
|
|
28
|
+
# node_class.new(**fields) with a span. The span is set before
|
|
29
|
+
# initialize runs rather than passed to it, so a node can define
|
|
30
|
+
# its own initialize (for field defaults, say) without knowing
|
|
31
|
+
# about spans.
|
|
32
|
+
def self.new_with_span(node_class, span, fields)
|
|
33
|
+
node = node_class.allocate
|
|
34
|
+
node.instance_variable_set(:@span, span)
|
|
35
|
+
node.__send__(:initialize, **fields)
|
|
36
|
+
node
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# span: is still accepted here, for building nodes by hand.
|
|
40
|
+
def initialize(span: nil, **fields)
|
|
41
|
+
@span = span if span
|
|
42
|
+
super(**fields)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with(**changes) = Node.new_with_span(self.class, span, to_h.merge(changes))
|
|
46
|
+
|
|
47
|
+
# S-expression style: (BinOp "+" (Num 1) (Num 2))
|
|
48
|
+
def inspect
|
|
49
|
+
name = self.class.name&.split("::")&.last || "node"
|
|
50
|
+
"#{["(#{name}", *to_h.values.map(&:inspect)].join(" ")})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pretty_print(q) = q.text(inspect)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Extend a module with this to define AST nodes:
|
|
57
|
+
#
|
|
58
|
+
# module MyLang
|
|
59
|
+
# extend Birman::Nodes
|
|
60
|
+
# Num = node(:value)
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# Also available as Birman.node.
|
|
64
|
+
module Nodes
|
|
65
|
+
def node(*members, &body)
|
|
66
|
+
raise ArgumentError, "a node can't have a field named span: every node already has one" if members.include?(:span)
|
|
67
|
+
|
|
68
|
+
Data.define(*members) do
|
|
69
|
+
include Node
|
|
70
|
+
|
|
71
|
+
class_eval(&body) if body
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
extend Nodes
|
|
77
|
+
|
|
78
|
+
# Build a node (or any Data class) from a hash of fields, attaching a
|
|
79
|
+
# span when the class supports one. The span ends where the last
|
|
80
|
+
# token did, leaving out spacing the ignore rule consumed after it;
|
|
81
|
+
# any whitespace consumed some other way (Rules' token helper, say)
|
|
82
|
+
# is trimmed too.
|
|
83
|
+
def self.build(node, fields, ctx, from, to)
|
|
84
|
+
return node.new(**fields) unless node.include?(Node)
|
|
85
|
+
|
|
86
|
+
to = ctx.token_end(to)
|
|
87
|
+
span = Span.new(ctx.input, from, from + ctx.input.byteslice(from, to - from).rstrip.bytesize)
|
|
88
|
+
Node.new_with_span(node, span, fields)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# Internal: one matcher succeeded, ending at `pos` with `value`.
|
|
5
|
+
Match = Data.define(:pos, :value)
|
|
6
|
+
|
|
7
|
+
# Returned by Grammar#parse when the whole input parsed.
|
|
8
|
+
Success = Data.define(:value) do
|
|
9
|
+
def success? = true
|
|
10
|
+
def failure? = false
|
|
11
|
+
def value!(_formatter = nil) = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returned by Grammar#parse when it didn't. Pure data: rendering it
|
|
15
|
+
# as text is the ErrorFormatter's job.
|
|
16
|
+
#
|
|
17
|
+
# committed: true when a match after a commit (>) failed, meaning the
|
|
18
|
+
# parser knew exactly which construct it was in and stopped there.
|
|
19
|
+
#
|
|
20
|
+
# reason: set when the failure isn't about what was expected, such as
|
|
21
|
+
# input nested too deeply for Ruby's stack.
|
|
22
|
+
Failure = Data.define(:input, :pos, :expected, :committed, :reason) do
|
|
23
|
+
def initialize(input:, pos:, expected:, committed:, reason: nil) = super
|
|
24
|
+
|
|
25
|
+
def success? = false
|
|
26
|
+
def failure? = true
|
|
27
|
+
def value!(formatter = ErrorFormatter.new) = raise ParseError.new(self, formatter)
|
|
28
|
+
|
|
29
|
+
def line = input[0, pos].count("\n") + 1
|
|
30
|
+
def column = pos - (input[0, pos].rindex("\n") || -1)
|
|
31
|
+
def found = input[pos]&.inspect || "end of input"
|
|
32
|
+
|
|
33
|
+
def message(formatter = ErrorFormatter.new) = formatter.call(self)
|
|
34
|
+
|
|
35
|
+
# The input can be any size, so only its start is shown.
|
|
36
|
+
def inspect
|
|
37
|
+
shown = input.size > 40 ? "#{input[0, 40]}..." : input
|
|
38
|
+
fields = ["pos=#{pos}", "expected=#{expected.inspect}", "committed=#{committed}"]
|
|
39
|
+
fields << "reason=#{reason.inspect}" if reason
|
|
40
|
+
"#<data Birman::Failure #{fields.join(" ")} input=#{shown.inspect}>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Raised by parse! (and Failure#value!) with the formatted message.
|
|
45
|
+
class ParseError < StandardError
|
|
46
|
+
attr_reader :failure
|
|
47
|
+
|
|
48
|
+
def initialize(failure, formatter = ErrorFormatter.new)
|
|
49
|
+
@failure = failure
|
|
50
|
+
super(formatter.call(failure))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Internal: raised when a match after a commit fails. Carries the diagnostics from
|
|
55
|
+
# the moment of failure. Grammar#parse turns it into a Failure.
|
|
56
|
+
class Committed < StandardError
|
|
57
|
+
attr_reader :pos, :expected
|
|
58
|
+
|
|
59
|
+
def initialize(pos, expected)
|
|
60
|
+
@pos = pos
|
|
61
|
+
@expected = expected
|
|
62
|
+
super("committed failure at #{pos}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# The `rule` and `lexeme` macros. Extend any module or class with it.
|
|
5
|
+
#
|
|
6
|
+
# Each rule becomes two ordinary methods: `name` returns a Ref, and a
|
|
7
|
+
# private `__rule_name` builds the matcher. Because rules are just
|
|
8
|
+
# methods, they travel through `include` and can be overridden like
|
|
9
|
+
# any other Ruby method.
|
|
10
|
+
module RuleSet
|
|
11
|
+
def rule(name, &)
|
|
12
|
+
check_rule_name!(name)
|
|
13
|
+
define_method(:"__rule_#{name}", &)
|
|
14
|
+
private :"__rule_#{name}"
|
|
15
|
+
define_method(name) { Ref.new(name) }
|
|
16
|
+
@instance = nil # a grammar class's cached instance (see Grammar::ClassMethods)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A rule for a token (a number, a string, an identifier): it skips
|
|
20
|
+
# trailing spacing when the grammar declares `ignore`.
|
|
21
|
+
def lexeme(name, &body)
|
|
22
|
+
rule(name) { Lexeme.new(Birman.coerce(instance_exec(&body)), ignore_rule) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# A rule named after a method the grammar relies on (send, class,
|
|
28
|
+
# match, token...) would silently replace it. Redefining a rule is
|
|
29
|
+
# fine, and so are private Kernel names like print or select.
|
|
30
|
+
def check_rule_name!(name)
|
|
31
|
+
return if private_method_defined?(:"__rule_#{name}")
|
|
32
|
+
|
|
33
|
+
owner = [Object, Combinators, Grammar].find { _1.public_method_defined?(name) } ||
|
|
34
|
+
[Combinators, Grammar].find { _1.private_method_defined?(name) } ||
|
|
35
|
+
(instance_method(name).owner if method_defined?(name))
|
|
36
|
+
raise ArgumentError, "rule :#{name} would replace #{owner}##{name}; give the rule another name" if owner
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|