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/README.md
CHANGED
|
@@ -20,7 +20,7 @@ bundle add birman
|
|
|
20
20
|
or in your Gemfile:
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
|
-
gem "birman", "~> 0.
|
|
23
|
+
gem "birman", "~> 0.3.0"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
@@ -77,8 +77,41 @@ end
|
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
See `examples/calc.rb` for a complete small language with an
|
|
80
|
-
interpreter, `examples/
|
|
81
|
-
|
|
80
|
+
interpreter, `examples/leftcalc.rb` for its arithmetic written as left
|
|
81
|
+
recursion instead of `infix`, `examples/json.rb` for a full JSON parser (tested against
|
|
82
|
+
`JSON.parse`), `examples/toml.rb` for TOML 1.0 (tested against the
|
|
83
|
+
official toml-test suite), `examples/query.rb` for a GitHub-style
|
|
84
|
+
search query (lookahead, implicit AND, a custom error formatter) with
|
|
85
|
+
an evaluator that filters records and a compiler to SQL for
|
|
86
|
+
ActiveRecord's `where`, and `Grammar.to_ebnf` to print any grammar.
|
|
87
|
+
|
|
88
|
+
## Why not a regex?
|
|
89
|
+
|
|
90
|
+
A Ruby regex can match nested input with `\g<name>`, but it can't
|
|
91
|
+
build the structure (a capture keeps only its last match), and it
|
|
92
|
+
can't say what's wrong when the match fails.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
re = /\A(?<l>\[\s*(?:(?:\d+|\g<l>)(?:\s*,\s*(?:\d+|\g<l>))*)?\s*\])\z/
|
|
96
|
+
re.match?("[1, [2, 3], []]") # => true
|
|
97
|
+
re.match("[1, [2, 3], []]")[:l] # => "[1, [2, 3], []]" (just the string back)
|
|
98
|
+
re.match?("[1, [2, 3]") # => false (no hint why)
|
|
99
|
+
|
|
100
|
+
class Lists
|
|
101
|
+
include Birman::Grammar
|
|
102
|
+
include Birman::Rules::Whitespace
|
|
103
|
+
ignore :spacing
|
|
104
|
+
root :list
|
|
105
|
+
rule(:list) { seq("[", commit, only(sep_by(item, ",")), "]") }
|
|
106
|
+
rule(:item) { either(match(/\d+/).map(&:to_i), list) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Lists.parse("[1, [2, 3], []]") # => Success([1, [2, 3], []])
|
|
110
|
+
puts Lists.parse("[1, [2, x], []]").message
|
|
111
|
+
# line 1, column 9: expected /\d+/ or "[", found "x"
|
|
112
|
+
# [1, [2, x], []]
|
|
113
|
+
# ^
|
|
114
|
+
```
|
|
82
115
|
|
|
83
116
|
## Building blocks
|
|
84
117
|
|
|
@@ -94,15 +127,43 @@ interpreter, `examples/json.rb` for a full JSON parser (tested against
|
|
|
94
127
|
| `infix(operand, "+", "-", node: BinOp)` | left-associative binary operators; `BinOp` needs `op`, `left`, `right` fields |
|
|
95
128
|
| `parens(x)`, `between(o, c, x)` | wrapped |
|
|
96
129
|
| `sep_by(x, ",")`, `sep_by1` | separated lists |
|
|
130
|
+
| `sep_by(x, ",", trailing: true)` | and one more `","` may end the list (not an empty one) |
|
|
131
|
+
| `match(re).captures` | a hash of `re`'s named groups (`{date: "1979-05-27", time: nil}`; `nil` for a group that didn't match) |
|
|
97
132
|
| `.rep(min, max)`, `.maybe` | repetition, optional |
|
|
98
133
|
| `.absent`, `.present` | negative / positive lookahead |
|
|
99
|
-
| `.map
|
|
134
|
+
| `.map(:to_i)` | call that method on the value |
|
|
135
|
+
| `.action(:name)` | pass the value to the grammar's method `name` |
|
|
136
|
+
| `.check(:name, "label")` | fail as "expected label" unless the grammar's method `name` accepts the value |
|
|
137
|
+
| `.means(v)` | the value is `v` (`lit("null").means(nil)`) |
|
|
138
|
+
| `.map { }` | transform the value with a block (prefer the above) |
|
|
100
139
|
| `.label("name")` | "expected name" in errors |
|
|
101
140
|
| `.quiet` | never mention in errors |
|
|
102
141
|
|
|
142
|
+
Rules can be left-recursive, written the way a textbook grammar writes
|
|
143
|
+
left-associative operators (see [Left recursion](#left-recursion)):
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
rule(:expr) { either(seq(expr.as(:left), either("+", "-").as(:op), term.as(:right)).to(BinOp), term) }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`1 - 2 - 3` parses as `(1 - 2) - 3`, the same tree
|
|
150
|
+
`infix(term, "+", "-", node: BinOp)` builds.
|
|
151
|
+
|
|
152
|
+
A name used twice in one `seq` raises `ArgumentError`, and so does a
|
|
153
|
+
`seq` of named parts (or `match(re).captures`) inside another whose
|
|
154
|
+
value would drop it, even through `maybe`, `label`, `either` and the
|
|
155
|
+
like: list the parts in the outer `seq`, or name the inner one,
|
|
156
|
+
`seq(...).as(:pair)`. A name every alternative of an `either` shares
|
|
157
|
+
passes outward; alternatives named differently pass on none. A rule
|
|
158
|
+
keeps its names to itself, so using one inside a `seq` is always fine.
|
|
159
|
+
|
|
103
160
|
The operator forms still work for short rules: `a >> b` (sequence),
|
|
104
161
|
`a | b` (choice), `a & b` (guard), `a > b` (commit). Precedence, tightest
|
|
105
|
-
first: `>>`, `&`, `|`, `>`.
|
|
162
|
+
first: `>>`, `&`, `|`, `>`. A chain of `>>` is one sequence:
|
|
163
|
+
`a.as(:x) >> b.as(:y) >> c.as(:z)` is `seq(a.as(:x), b.as(:y), c.as(:z))`,
|
|
164
|
+
however it's grouped, and `a > b >> c` is `seq(a, commit, b, c)`. A seq
|
|
165
|
+
built any other way (by `seq(...)`, or with a commit or `only`) stays
|
|
166
|
+
one part, as it would in `seq`: `pair >> c` is `seq(pair, c)`.
|
|
106
167
|
|
|
107
168
|
## Literals, spacing, and lexemes
|
|
108
169
|
|
|
@@ -115,7 +176,11 @@ Declare `ignore :spacing` and whitespace (and comments, with
|
|
|
115
176
|
word-like ones (`"let"`) are keywords, so `"let"` won't match the
|
|
116
177
|
start of `letter`.
|
|
117
178
|
- `str("...")` and `match(/.../)` are **raw**: exactly those characters,
|
|
118
|
-
no skipping.
|
|
179
|
+
no skipping. `match(/.../).captures` is raw too, its value a hash of
|
|
180
|
+
the named groups with Symbol keys, every name always present. Each
|
|
181
|
+
name must be used once (`(?<n>a)|(?<n>b)` raises `ArgumentError`), and
|
|
182
|
+
unnamed groups aren't included: Ruby stops capturing them once a group
|
|
183
|
+
is named. `.to(Node)` builds a node from it like any hash.
|
|
119
184
|
- Rules for tokens (numbers, names, strings) should be declared with
|
|
120
185
|
**`lexeme`** instead of `rule`, so they skip trailing spacing too. The
|
|
121
186
|
built-in `Birman::Rules` already do this.
|
|
@@ -132,6 +197,9 @@ of at whatever a looser alternative accepted.
|
|
|
132
197
|
- `rep` and `maybe` don't stop a commit. `seq("else", commit, expr).maybe`
|
|
133
198
|
rejects `else` followed by garbage rather than quietly treating it as
|
|
134
199
|
"no else clause".
|
|
200
|
+
- Neither does a trailing separator. `sep_by(x, ",", trailing: true)`
|
|
201
|
+
ends the list at a last `","` only when the `x` after it fails
|
|
202
|
+
without committing.
|
|
135
203
|
- Lookaheads do. `x.absent`, `x.present` and the guard in `a & b` only
|
|
136
204
|
ask whether `x` would match, so a commit failure inside them is just
|
|
137
205
|
"no".
|
|
@@ -141,6 +209,182 @@ of at whatever a looser alternative accepted.
|
|
|
141
209
|
This is Parsec-style commitment, not the choice-local "cut" from the
|
|
142
210
|
PEG literature.
|
|
143
211
|
|
|
212
|
+
## Left recursion
|
|
213
|
+
|
|
214
|
+
A rule can call itself before consuming anything, as grammars usually
|
|
215
|
+
write left-associative operators:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
rule(:expr) { either(seq(expr, "-", term).map { |l, _, r| l - r }, term) }
|
|
219
|
+
rule(:term) { either(seq(term, "*", integer).map { |l, _, r| l * r }, integer) }
|
|
220
|
+
|
|
221
|
+
parse!("5*4*2-5*9*2") # => -50, as ((5*4)*2) - ((5*9)*2)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`expr op term` is the left-associative form, the same shape `infix`
|
|
225
|
+
builds. Birman grows the match from a seed (Warth et al., 2008): it
|
|
226
|
+
tries the rule with its own call failing, then again with that call
|
|
227
|
+
returning the last match, for as long as the match gets further. So
|
|
228
|
+
each level is a value or node of its own, `((3-4)-5)`, and a node's
|
|
229
|
+
span covers its whole subexpression. A long chain uses no more stack
|
|
230
|
+
than a short one, and errors say what the last attempt needed
|
|
231
|
+
(`1-` expects an integer after the `-`). `to_ruby` generates parsers
|
|
232
|
+
that do the same.
|
|
233
|
+
|
|
234
|
+
The recursion can go through other rules too (indirect left
|
|
235
|
+
recursion), as when each level of precedence is split in two:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
rule(:sum) { terms }
|
|
239
|
+
rule(:terms) { either(seq(sum, "-", term).map { |l, _, r| l - r }, term) }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Each cycle grows at one rule, its leader, as pegen does: the first rule
|
|
243
|
+
(in the order the grammar uses them) that every way round the cycle
|
|
244
|
+
passes through, here `sum`. While it grows, the cycle's other rules
|
|
245
|
+
are matched afresh each round, since what they match depends on how
|
|
246
|
+
far it has got.
|
|
247
|
+
|
|
248
|
+
- The rule needs an alternative that doesn't start with itself, its
|
|
249
|
+
base case (`term` above), and the alternatives that do start with it
|
|
250
|
+
come first. Tried first, `term` would win every round, so
|
|
251
|
+
`either(term, seq(expr, "-", term))` could never grow past `3`. Either
|
|
252
|
+
mistake in the rule's own `either` (not one nested inside a `seq`)
|
|
253
|
+
raises `ArgumentError` on the first parse, or `to_ruby`, saying which: ``in left-recursive rule :expr, the alternative `term`
|
|
254
|
+
comes before `expr "-" term`, so the rule can never grow past it``.
|
|
255
|
+
In a cycle, any of its rules counts as the rule itself: one of them
|
|
256
|
+
needs a base case, and in each, the alternatives that start with a
|
|
257
|
+
rule of the cycle come first.
|
|
258
|
+
- `expr op expr`, with the rule on both sides, comes out
|
|
259
|
+
right-associative: `3-4-5` is `3-(4-5)`. The call on the right grows
|
|
260
|
+
to the end of the input before the left one can. Write `expr op term`.
|
|
261
|
+
- A cycle is grown only at its leader, so a parse that enters it at
|
|
262
|
+
another rule gets that rule's match with the leader's grown one,
|
|
263
|
+
which may already have taken what that rule needed. In
|
|
264
|
+
`l ← p ".x" / "x"; p ← p "(n)" / l` (Medeiros et al., 2014), every
|
|
265
|
+
way round passes through `p`, so `p` leads even when the parse
|
|
266
|
+
starts at `l`; then `x.x` matches only `x`, since `p` has grown over
|
|
267
|
+
the `.x`. Starting at `p` parses it. Usually this doesn't come up: of
|
|
268
|
+
the rules that could lead, the one the grammar uses first does.
|
|
269
|
+
- Cycles with no rule in common (`a` and `b` each call themselves and
|
|
270
|
+
each other) have nowhere to grow both from, so they raise
|
|
271
|
+
`ArgumentError` on the first parse, or `to_ruby`, naming them:
|
|
272
|
+
`left recursion through cycles with no rule in common isn't
|
|
273
|
+
supported: :a -> :a; :b -> :b`.
|
|
274
|
+
- Left recursion through a custom matcher, which Birman can't see into,
|
|
275
|
+
is caught only when the parse gets there.
|
|
276
|
+
|
|
277
|
+
## Checks
|
|
278
|
+
|
|
279
|
+
An action can only return a value, and raising escapes `parse`, so an
|
|
280
|
+
action can't turn a match down. `.check` can:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
rule(:date) { match(/\d{4}-\d\d-\d\d/).check(:real_date?, "a real date") }
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def real_date?(text) = Date.valid_date?(*text.split("-").map(&:to_i))
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The method (it may be private) gets the value and returns truthy to
|
|
291
|
+
accept it; the value passes through unchanged. A falsy result is an
|
|
292
|
+
ordinary failure, "expected a real date", where the match began, and
|
|
293
|
+
whatever the inner match recorded (a `rep`'s stopping point, say) is
|
|
294
|
+
dropped. (This example uses `Date` and `to_i`, so it won't go into a
|
|
295
|
+
generated parser; see below.)
|
|
296
|
+
|
|
297
|
+
- It backtracks like any failure. The furthest failure still wins, so
|
|
298
|
+
if a later alternative gets further (`1979` matching a number before
|
|
299
|
+
`-02-30` trips it up), that alternative owns the error.
|
|
300
|
+
- Inside `rep` it ends the loop, and inside `maybe` it gives `nil`.
|
|
301
|
+
- It commits only when an enclosing `seq` already has:
|
|
302
|
+
`seq("on", commit, date)` stops at the rejected date. To commit to the
|
|
303
|
+
check itself, look first: `pattern.present > pattern.check(...)`.
|
|
304
|
+
- An outer `.label` at the same position replaces its label.
|
|
305
|
+
- Like actions, the method must depend only on the value: it also runs
|
|
306
|
+
inside lookaheads, and its result is memoized with the rule.
|
|
307
|
+
|
|
308
|
+
Some checks need more than one value: a name defined twice, a `break`
|
|
309
|
+
outside a loop. Make those in a second pass over the AST, and report
|
|
310
|
+
one at the node it's about with `Birman::Failure.at(node.span, reason)`.
|
|
311
|
+
It's a `Failure` like `parse` returns, committed, with `reason` as
|
|
312
|
+
its message:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
def self.check(program)
|
|
316
|
+
seen = {}
|
|
317
|
+
program.each do |let|
|
|
318
|
+
return Birman::Failure.at(let.span, "#{let.name} is already defined") if seen[let.name]
|
|
319
|
+
|
|
320
|
+
seen[let.name] = true
|
|
321
|
+
end
|
|
322
|
+
Birman::Success.new(program)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
case Grammar.parse(source)
|
|
326
|
+
in Birman::Success(program) then check(program)
|
|
327
|
+
in failure then failure
|
|
328
|
+
end
|
|
329
|
+
# line 2, column 1: x is already defined
|
|
330
|
+
# let x = 2
|
|
331
|
+
# ^
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Without a grammar
|
|
335
|
+
|
|
336
|
+
For a one-off pattern, skip the class: the combinators are on `Birman`
|
|
337
|
+
too, and `Birman.parse` runs a matcher as a whole grammar.
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
B = Birman
|
|
341
|
+
date = B.seq(B.match(/\d{4}/), "-", B.match(/\d\d/)).map { _1.join }
|
|
342
|
+
|
|
343
|
+
B.parse(date, "2026-10") # => Success("2026-10")
|
|
344
|
+
B.parse!(date, "26-10") # raises Birman::ParseError
|
|
345
|
+
|
|
346
|
+
B.parse(B.seq("let", B.match(/\w+/)), "let x", ignore: /\s*/)
|
|
347
|
+
# => Success(["let", "x"])
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`parse` returns `Success`/`Failure` and never raises on bad input, as
|
|
351
|
+
`Grammar.parse` does; `parse!` takes `formatter:` too. `ignore:` is a
|
|
352
|
+
matcher (a regexp, a string or any matcher) rather than a rule name,
|
|
353
|
+
since there's no grammar for a name to refer to; literals skip it, as
|
|
354
|
+
in a grammar with `ignore`, but `str` and `match` stay raw, so skip
|
|
355
|
+
spacing after them yourself if you need to. Matchers are values, so the grammar built
|
|
356
|
+
for one is cached and reused for an equal one.
|
|
357
|
+
|
|
358
|
+
To drop the `Birman.` prefix, mix `Birman::Combinators` into a class
|
|
359
|
+
of your own: `extend` it for matchers held in constants, `include` it
|
|
360
|
+
for matchers built by instance methods.
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
class Dates
|
|
364
|
+
extend Birman::Combinators
|
|
365
|
+
DATE = seq(match(/\d{4}/), "-", match(/\d\d/))
|
|
366
|
+
def self.parse(s) = Birman.parse(DATE, s)
|
|
367
|
+
end
|
|
368
|
+
Dates.parse("2026-10") # => Success(["2026", "-", "10"])
|
|
369
|
+
|
|
370
|
+
class Inst
|
|
371
|
+
include Birman::Combinators
|
|
372
|
+
def date = seq(match(/\d{4}/).as(:y), "-", match(/\d\d/).as(:m))
|
|
373
|
+
end
|
|
374
|
+
Birman.parse(Inst.new.date, "2026-10") # => Success({y: "2026", m: "10"})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The class is just a namespace, not a grammar: matchers have no `parse`
|
|
378
|
+
of their own, so run them with `Birman.parse`, and bare strings skip
|
|
379
|
+
spacing only when you pass `ignore:`.
|
|
380
|
+
|
|
381
|
+
`rule` and `lexeme` aren't in `Combinators`, so anything named or
|
|
382
|
+
recursive, and `Birman::Rules` (`number`, `identifier`, ...), still
|
|
383
|
+
needs `include Birman::Grammar`; so do `keywords` and `.action`/`.check`
|
|
384
|
+
with methods of your own (`large_integer` works). A rule reference,
|
|
385
|
+
or an action or check naming a method of your own, raises
|
|
386
|
+
`ArgumentError` before parsing starts.
|
|
387
|
+
|
|
144
388
|
## Nodes
|
|
145
389
|
|
|
146
390
|
`node(:a, :b)` (in any grammar class, from `extend Birman::Nodes`, or
|
|
@@ -166,7 +410,8 @@ has one.
|
|
|
166
410
|
- `Grammar.parse` returns `Success`/`Failure`; `parse!` raises `ParseError`
|
|
167
411
|
(`parse!(input, formatter: f)` words it your way).
|
|
168
412
|
Input nested too deeply for Ruby's stack is a `Failure` too, with
|
|
169
|
-
`reason` set (see Limits)
|
|
413
|
+
`reason` set (see Limits), and so is invalid UTF-8: `reason` is
|
|
414
|
+
"invalid UTF-8", at the first bad byte.
|
|
170
415
|
- `Failure#message(formatter)` takes anything with `#call(failure)`
|
|
171
416
|
- `Grammar.to_ast` and `Grammar.to_ebnf` for introspection
|
|
172
417
|
- Rules are methods, so they compose and override through `include`.
|
|
@@ -175,8 +420,118 @@ has one.
|
|
|
175
420
|
combinators; any class that includes it is a grammar. Settings
|
|
176
421
|
(`root`, `ignore`, `keywords`) still go on the class.
|
|
177
422
|
- A rule can't be named after a method the grammar relies on (`send`,
|
|
178
|
-
`class`, `match`, `token
|
|
179
|
-
|
|
423
|
+
`class`, `match`, `token`, or a private helper in one of Birman's
|
|
424
|
+
modules, like `Rules::Strings`'s `decode_escape`), nor after a public
|
|
425
|
+
method of your own. Defining one raises `ArgumentError` saying whose
|
|
426
|
+
method it would replace, instead of silently breaking the grammar.
|
|
427
|
+
Redefining a rule is fine, and so is replacing your grammar's own
|
|
428
|
+
private helpers.
|
|
429
|
+
|
|
430
|
+
## Generating a parser
|
|
431
|
+
|
|
432
|
+
`Grammar.to_ruby` writes a standalone parser: Ruby source with no
|
|
433
|
+
dependency on Birman that gives exactly the results `parse` gives
|
|
434
|
+
(values, spans, error positions, expected lists and messages).
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
File.write("calc_parser.rb", Calc::Grammar.to_ruby(namespace: "CalcParser"))
|
|
438
|
+
require_relative "calc_parser"
|
|
439
|
+
CalcParser::Parser.new("print 1 + 2").parse # => Success or Failure
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The output is written in the Ruby subset that
|
|
443
|
+
[Spinel](https://github.com/matz/spinel) compiles to a native binary.
|
|
444
|
+
Add an entry point and compile it:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
source = Calc::Grammar.to_ruby(namespace: "CalcParser") + <<~RUBY
|
|
448
|
+
result = CalcParser::Parser.new($stdin.read).parse
|
|
449
|
+
puts result.success? ? result.value.inspect : result.message
|
|
450
|
+
RUBY
|
|
451
|
+
File.write("calc_parser.rb", source) # then: spinel calc_parser.rb -o calc
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Nodes come out as plain classes under `<namespace>::BirmanRT::Node`,
|
|
455
|
+
with the same fields, `span`, `==` and `inspect`. Values transformed
|
|
456
|
+
with `.map(:sym)`, `.action(:name)` and `.means(v)` compile, as does
|
|
457
|
+
`.check(:name, "label")`: an action's (or check's) method is copied from
|
|
458
|
+
its source, with the grammar methods it calls
|
|
459
|
+
and the constants it reads (`Float::INFINITY` and `Float::NAN` too), so
|
|
460
|
+
copying needs the `prism` gem (bundled from Ruby 3.3). So is a node's
|
|
461
|
+
`initialize` that only gives fields defaults (see Nodes): each node the
|
|
462
|
+
generated parser builds gets the default for a field its value leaves
|
|
463
|
+
out, a new one each time, as in Ruby. The generator raises `Birman::Compiler::Error`, naming
|
|
464
|
+
the rule, for what it can't translate faithfully:
|
|
465
|
+
|
|
466
|
+
- `.map { }` blocks (their source is lost) and custom matcher classes
|
|
467
|
+
- regexps using `\A`, `^`, lookbehind, or `\b`/`\B` at their start:
|
|
468
|
+
the generated parser scans from the current position, where those
|
|
469
|
+
would see less of the input than the interpreter's do
|
|
470
|
+
- regexps with a wide `{m,n}` (more than 16 apart) alongside a
|
|
471
|
+
lookaround, atomic group, backreference or lazy interval: Spinel's
|
|
472
|
+
regexp engine tries every length on those, exponentially. Making the
|
|
473
|
+
repeat atomic on its own, `(?>[a-z]{1,63})`, is allowed
|
|
474
|
+
- `.to(Node)` that may leave out a field with no default: named parts
|
|
475
|
+
that miss one, or one value (or a value that may or may not be a Hash
|
|
476
|
+
of fields) for a node with a field past its first that has none
|
|
477
|
+
- node classes with methods of their own (a `node(...) { }` body),
|
|
478
|
+
except an `initialize` that takes exactly the fields as keywords,
|
|
479
|
+
gives some literal defaults (nil, true, false, numbers, strings,
|
|
480
|
+
symbols, and arrays and hashes of them) and only calls `super`, as
|
|
481
|
+
`def initialize(name:, args: []) = super` does; and
|
|
482
|
+
names that would clash in the generated code (a node called `Success`,
|
|
483
|
+
a copied constant called `FAIL`, two copied constants of one name,
|
|
484
|
+
rules `:a?` and `:a_3f`, which are both `a_3f` there)
|
|
485
|
+
- actions that call `super`, Birman's own API (`keywords`, rules), or
|
|
486
|
+
classes the generated parser won't have, or that read a constant path
|
|
487
|
+
other than `Float::INFINITY` and `Float::NAN` (`Math::PI`; copy it
|
|
488
|
+
into a constant of the grammar)
|
|
489
|
+
- actions that call `Integer(...)`, `.hex`, `.oct` or `.to_i` (on
|
|
490
|
+
anything but an Integer literal, since a receiver's type can't be
|
|
491
|
+
told), or name them (`map(&:to_i)`): past 63 bits, in a Spinel
|
|
492
|
+
binary they raise or, where Spinel can't tell the receiver is a
|
|
493
|
+
String, silently give 2**63 - 1. Use `large_integer(text)` or
|
|
494
|
+
`large_integer(text, base)` (`16` for `hex`, `8` for `oct`), which
|
|
495
|
+
Birman gives every grammar: it is `Integer(text, base)` in Ruby, and
|
|
496
|
+
in the generated parser it returns Bignums under Spinel too (from
|
|
497
|
+
Floats as well: exactly, as Ruby's `Integer` does). `.map(:to_i)` is
|
|
498
|
+
fine on a String, an optional String (`.maybe`; nil is 0) or an
|
|
499
|
+
Integer; `.map(:hex)`, `.map(:oct)`, and `.map(:to_i)` on anything
|
|
500
|
+
else are refused, as are `send` and `method` with a name that isn't
|
|
501
|
+
a literal symbol. This is a net for the common forms, not a proof:
|
|
502
|
+
`Float(text).round`, `text.to_f.to_int` and whatever else slips past
|
|
503
|
+
it may raise, or give a wrong number without a word. `large_integer`
|
|
504
|
+
takes Integer bases and Floats (truncated) as Ruby does; Rational and
|
|
505
|
+
NaN bases differ (`TypeError`, `FloatDomainError`), a base that isn't
|
|
506
|
+
a number raises `TypeError` as in Ruby 3.3+ (3.2's `Integer` ignores
|
|
507
|
+
it), and in a binary negative bases (Ruby's "prefix or this base")
|
|
508
|
+
raise
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
def json_number(text) = text.match?(/[.eE]/) ? Float(text) : large_integer(text)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Actions and `.map(:name)` always run, as in the interpreter, even when
|
|
515
|
+
nothing reads their value. The generated parser reads UTF-8 or binary input (other
|
|
516
|
+
encodings raise `ArgumentError`). It also nests deeper before reporting
|
|
517
|
+
"input is nested too deeply": its methods use less stack than the
|
|
518
|
+
interpreter's matchers, and a Spinel binary has a 64 MB stack (JSON
|
|
519
|
+
arrays nest about 55,000 deep), so input that fails with the interpreter
|
|
520
|
+
can parse when compiled. One limit runs the other way: Spinel allows 64
|
|
521
|
+
`begin` blocks active at once (and exits the process past that), and
|
|
522
|
+
each lookahead (`absent`, `present`, `&`) holds one while its inner
|
|
523
|
+
matcher runs. So lookaheads nested more than 56 deep are a Failure,
|
|
524
|
+
"input is nested too deeply". To allow more, build with a bigger stack
|
|
525
|
+
and say so when generating:
|
|
526
|
+
|
|
527
|
+
```ruby
|
|
528
|
+
File.write("p.rb", G.to_ruby(namespace: "P", lookahead_depth: 4000))
|
|
529
|
+
# spinel p.rb -o p --cc="cc -DSP_EXC_STACK_MAX=4096"
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Copied actions are exactly what you wrote. Under Spinel, a string that
|
|
533
|
+
isn't valid UTF-8 (JSON's lone `\ud83d`) has the same bytes but
|
|
534
|
+
`inspect`s differently.
|
|
180
535
|
|
|
181
536
|
## Limits
|
|
182
537
|
|
|
@@ -194,6 +549,12 @@ has one.
|
|
|
194
549
|
That took the same grammar to about 12,000 levels on a thread. The
|
|
195
550
|
main thread gets less (about 2,000), because its machine stack comes
|
|
196
551
|
from the operating system (`ulimit -s`).
|
|
552
|
+
- **Left recursion on both sides.** `expr op expr` groups to the right
|
|
553
|
+
(see [Left recursion](#left-recursion)); write `expr op term` for
|
|
554
|
+
left-associative operators.
|
|
555
|
+
- **Entering a left-recursive cycle away from its leader** gets a
|
|
556
|
+
shorter match than growing from there would (see
|
|
557
|
+
[Left recursion](#left-recursion)).
|
|
197
558
|
- **Changing rules after parsing.** `Grammar.parse` reuses one
|
|
198
559
|
instance per class, so each rule is built once. Defining a rule or
|
|
199
560
|
changing a setting on a class resets that class's instance, but a
|
|
@@ -203,10 +564,9 @@ has one.
|
|
|
203
564
|
|
|
204
565
|
## Not yet
|
|
205
566
|
|
|
206
|
-
- Left recursion
|
|
207
|
-
|
|
208
|
-
`
|
|
209
|
-
- Compiling grammars to Ruby methods for speed
|
|
567
|
+
- Left recursion through cycles with no rule in common (see
|
|
568
|
+
[Left recursion](#left-recursion)); give them a rule in common, or
|
|
569
|
+
use `infix`.
|
|
210
570
|
- Railroad diagrams from `to_ast`
|
|
211
571
|
|
|
212
572
|
## Development
|
|
@@ -214,9 +574,16 @@ has one.
|
|
|
214
574
|
```sh
|
|
215
575
|
bin/setup # install dependencies
|
|
216
576
|
bundle exec rake # tests + rubocop
|
|
577
|
+
bundle exec rake spinel # generated parsers compiled by Spinel vs the interpreter
|
|
578
|
+
# (SPINEL=/path/to/spinel; skipped if not found)
|
|
217
579
|
ruby examples/calc.rb
|
|
580
|
+
ruby examples/leftcalc.rb
|
|
218
581
|
ruby examples/json.rb
|
|
219
|
-
ruby
|
|
582
|
+
ruby examples/email.rb
|
|
583
|
+
ruby examples/toml.rb
|
|
584
|
+
ruby examples/query.rb
|
|
585
|
+
ruby bench/json.rb # vs JSON.parse, Parslet, Treetop, Citrus (installs them);
|
|
586
|
+
# also to_ruby, and its Spinel binary if Spinel is found
|
|
220
587
|
```
|
|
221
588
|
|
|
222
589
|
## License
|
data/lib/birman/ast.rb
CHANGED
|
@@ -7,7 +7,11 @@ module Birman
|
|
|
7
7
|
# [:seq, commit_at, [parts...]] [:choice, [alts...]]
|
|
8
8
|
# [:rep, min, max, inner] [:not, inner] [:and, inner]
|
|
9
9
|
# [:both, left, right] [:action, inner] [:label, name, inner]
|
|
10
|
-
# [:quiet, inner]
|
|
10
|
+
# [:check, name, inner] [:quiet, inner] [:lexeme, inner]
|
|
11
|
+
#
|
|
12
|
+
# A lexeme is inner, then the grammar's ignore rule. Literals are
|
|
13
|
+
# lexemes too, but show as [:str, text]: the ignore rule runs only
|
|
14
|
+
# after the text, so only an empty literal shows as [:lexeme, ...].
|
|
11
15
|
module AST
|
|
12
16
|
module_function
|
|
13
17
|
|
|
@@ -27,6 +31,9 @@ module Birman
|
|
|
27
31
|
# a* a+ a? a{2,3} !a not &a lookahead / guard
|
|
28
32
|
#
|
|
29
33
|
# Actions and labels don't change what's accepted, so they're omitted.
|
|
34
|
+
# Checks do, but by running Ruby on the value, which EBNF can't show
|
|
35
|
+
# (`&{name}` would only repeat the error message), so they're omitted
|
|
36
|
+
# too: the EBNF accepts a superset of what the grammar does.
|
|
30
37
|
module EBNF
|
|
31
38
|
module_function
|
|
32
39
|
|
|
@@ -49,7 +56,9 @@ module Birman
|
|
|
49
56
|
in [:ref, name] then name.to_s
|
|
50
57
|
in [:action, inner] then expr(inner, prec)
|
|
51
58
|
in [:label, _, inner] then expr(inner, prec)
|
|
59
|
+
in [:check, _, inner] then expr(inner, prec)
|
|
52
60
|
in [:quiet, inner] then expr(inner, prec)
|
|
61
|
+
in [:lexeme, inner] then expr(inner, prec)
|
|
53
62
|
in [:both, [:not, guard], right]
|
|
54
63
|
wrap("!#{expr(guard, ATOM)} #{expr(right, PREFIX)}", prec > SEQ)
|
|
55
64
|
in [:choice, alts] then wrap(alts.map { expr(_1, SEQ) }.join(" / "), prec > CHOICE)
|
data/lib/birman/combinators.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Birman
|
|
|
57
57
|
# params = ["word: String"]
|
|
58
58
|
# returns = "Matcher for word when not followed by a word character"
|
|
59
59
|
# value = "word"
|
|
60
|
-
def keyword(word) = (str(word)
|
|
60
|
+
def keyword(word) = Pick.new([str(word), match(/\w/).absent], 0).label(word.inspect)
|
|
61
61
|
|
|
62
62
|
# --- Literals and the readable forms ---
|
|
63
63
|
|
|
@@ -70,8 +70,8 @@ module Birman
|
|
|
70
70
|
def lit(x) = Birman.coerce(x)
|
|
71
71
|
|
|
72
72
|
# The matcher a literal stands for: "let" is a keyword, "=" an exact
|
|
73
|
-
# string, either way followed by the
|
|
74
|
-
# once per grammar instance.
|
|
73
|
+
# string, either way followed by the ignore rule of the grammar
|
|
74
|
+
# running it. Built once per grammar instance.
|
|
75
75
|
# [method.literal_matcher]
|
|
76
76
|
# params = ["text: String"]
|
|
77
77
|
# returns = "The matcher a literal stands for: keyword or exact string, then the ignore rule"
|
|
@@ -80,7 +80,7 @@ module Birman
|
|
|
80
80
|
def literal_matcher(text)
|
|
81
81
|
(@literal_matchers ||= {})[text] ||= begin
|
|
82
82
|
base = text.match?(/\A\w+\z/) ? keyword(text) : str(text)
|
|
83
|
-
Lexeme.new(base,
|
|
83
|
+
Lexeme.new(base, text)
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -90,7 +90,8 @@ module Birman
|
|
|
90
90
|
# params = ["*items: String | Regexp | Matcher | commit | only(...)"]
|
|
91
91
|
# returns = "Matcher for the items in order"
|
|
92
92
|
# value = "Array of item values; a Hash of the .as-named parts if any; the only(...) part's value if used"
|
|
93
|
-
# raises = ["ArgumentError (commit first; more than one only; only mixed with .as names)"
|
|
93
|
+
# raises = ["ArgumentError (commit first; more than one only; only mixed with .as names)",
|
|
94
|
+
# "ArgumentError (a name used twice; a nested seq's names the value would drop)"]
|
|
94
95
|
def seq(*items)
|
|
95
96
|
commit_at = items.index(COMMIT)
|
|
96
97
|
raise ArgumentError, "commit can't come first in a seq: nothing has matched yet" if commit_at&.zero?
|
|
@@ -118,7 +119,7 @@ module Birman
|
|
|
118
119
|
# [method.either]
|
|
119
120
|
# params = ["*alts: String | Regexp | Matcher"]
|
|
120
121
|
# returns = "Ordered choice: the first alternative that matches wins"
|
|
121
|
-
# value = "The winning alternative's value"
|
|
122
|
+
# value = "The winning alternative's value (a name all the alternatives share passes outward)"
|
|
122
123
|
def either(*alts) = alts.size == 1 ? lit(alts.first) : Choice.new(alts.map { lit(_1) })
|
|
123
124
|
|
|
124
125
|
# One of several literals. Given a hash, each literal produces its
|
|
@@ -131,7 +132,7 @@ module Birman
|
|
|
131
132
|
def one_of(*choices)
|
|
132
133
|
return either(*choices) unless choices in [Hash => table]
|
|
133
134
|
|
|
134
|
-
either(*table.map { |text, value| lit(text).
|
|
135
|
+
either(*table.map { |text, value| lit(text).means(value) })
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
# Left-associative binary operators, folded into nodes:
|
|
@@ -144,18 +145,21 @@ module Birman
|
|
|
144
145
|
def infix(operand, *ops, node:) = Infix.new(Birman.coerce(operand), either(*ops).label("operator"), node)
|
|
145
146
|
|
|
146
147
|
# One or more items separated by sep; value is an array of items.
|
|
148
|
+
# With trailing: true, one separator may follow the last item:
|
|
149
|
+
# seq("[", only(sep_by(value, ",", trailing: true)), "]") # [1, 2,]
|
|
147
150
|
# [method.sep_by1]
|
|
148
|
-
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher"]
|
|
149
|
-
# returns = "Matcher for one or more items separated by sep"
|
|
151
|
+
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher", "trailing: Boolean = false"]
|
|
152
|
+
# returns = "Matcher for one or more items separated by sep (and, if trailing, maybe one more sep)"
|
|
150
153
|
# value = "Array of item values (item's .as name passes outward)"
|
|
151
|
-
def sep_by1(item, sep) = SepBy.new(lit(item), lit(sep))
|
|
154
|
+
def sep_by1(item, sep, trailing: false) = SepBy.new(lit(item), lit(sep), 1, trailing)
|
|
152
155
|
|
|
153
|
-
# Zero or more items separated by sep.
|
|
156
|
+
# Zero or more items separated by sep. A trailing separator needs an
|
|
157
|
+
# item before it: "," alone isn't an empty list.
|
|
154
158
|
# [method.sep_by]
|
|
155
|
-
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher"]
|
|
156
|
-
# returns = "Matcher for zero or more items separated by sep"
|
|
159
|
+
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher", "trailing: Boolean = false"]
|
|
160
|
+
# returns = "Matcher for zero or more items separated by sep (and, if trailing, maybe one more sep)"
|
|
157
161
|
# value = "Array of item values, [] when there are none"
|
|
158
|
-
def sep_by(item, sep) =
|
|
162
|
+
def sep_by(item, sep, trailing: false) = SepBy.new(lit(item), lit(sep), 0, trailing)
|
|
159
163
|
|
|
160
164
|
# inner wrapped in open/close; value is inner's.
|
|
161
165
|
# [method.between]
|
|
@@ -180,4 +184,11 @@ module Birman
|
|
|
180
184
|
|
|
181
185
|
def ignore_rule = self.class.respond_to?(:ignore) ? self.class.ignore : nil
|
|
182
186
|
end
|
|
187
|
+
|
|
188
|
+
# Birman.seq, Birman.str and the rest, for building matchers outside a
|
|
189
|
+
# grammar (see Birman.parse). literal_matcher stays private: it's what
|
|
190
|
+
# a grammar runs for a literal, and Birman isn't one.
|
|
191
|
+
extend Combinators
|
|
192
|
+
|
|
193
|
+
singleton_class.send(:private, :literal_matcher)
|
|
183
194
|
end
|