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.
@@ -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
@@ -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