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/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
|
|
@@ -165,8 +409,9 @@ has one.
|
|
|
165
409
|
them; pass `nil` to clear one (`ignore nil`, `keywords nil`)
|
|
166
410
|
- `Grammar.parse` returns `Success`/`Failure`; `parse!` raises `ParseError`
|
|
167
411
|
(`parse!(input, formatter: f)` words it your way).
|
|
168
|
-
Input nested too deeply for Ruby's stack
|
|
169
|
-
|
|
412
|
+
Input nested too deeply for Ruby's stack is a `Failure` too, with
|
|
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,15 +420,153 @@ 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.
|
|
535
|
+
|
|
536
|
+
## Limits
|
|
537
|
+
|
|
538
|
+
- **Deep nesting.** Each level of nesting in the input uses Ruby
|
|
539
|
+
stack. With default settings a simple nested grammar handles about
|
|
540
|
+
750 levels on the main thread, but only about 250 on other threads
|
|
541
|
+
(Puma, Sidekiq, `Thread.new`). Past that, `parse` returns a `Failure`
|
|
542
|
+
whose `reason` is "input is nested too deeply". To allow more, raise
|
|
543
|
+
Ruby's thread stack sizes when starting the process:
|
|
544
|
+
|
|
545
|
+
```sh
|
|
546
|
+
RUBY_THREAD_VM_STACK_SIZE=16777216 RUBY_THREAD_MACHINE_STACK_SIZE=67108864 ruby app.rb
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
That took the same grammar to about 12,000 levels on a thread. The
|
|
550
|
+
main thread gets less (about 2,000), because its machine stack comes
|
|
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)).
|
|
558
|
+
- **Changing rules after parsing.** `Grammar.parse` reuses one
|
|
559
|
+
instance per class, so each rule is built once. Defining a rule or
|
|
560
|
+
changing a setting on a class resets that class's instance, but a
|
|
561
|
+
subclass that has already parsed keeps its own: it won't see a rule
|
|
562
|
+
redefined later on its parent. Define rules before parsing, or call
|
|
563
|
+
`Subclass.new.parse(input)` to get a fresh instance.
|
|
180
564
|
|
|
181
565
|
## Not yet
|
|
182
566
|
|
|
183
|
-
- Left recursion
|
|
184
|
-
|
|
185
|
-
`
|
|
186
|
-
- 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`.
|
|
187
570
|
- Railroad diagrams from `to_ast`
|
|
188
571
|
|
|
189
572
|
## Development
|
|
@@ -191,9 +574,16 @@ has one.
|
|
|
191
574
|
```sh
|
|
192
575
|
bin/setup # install dependencies
|
|
193
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)
|
|
194
579
|
ruby examples/calc.rb
|
|
580
|
+
ruby examples/leftcalc.rb
|
|
195
581
|
ruby examples/json.rb
|
|
196
|
-
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
|
|
197
587
|
```
|
|
198
588
|
|
|
199
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)
|