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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43738fe8c60d4418ab97a79db6441fbec7ba9a047fc7a6cf2ba7c3e7fb77aa95
|
|
4
|
+
data.tar.gz: 9bbad872875549e7ec95976001ec0a7e01052b2517ee1de3b65fc54b47fa2cc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41a2f87efd811346676b70bef44bcafe2bfb869ebac32d5d3cfb1cff248475375b341fe91745e6978545ea2fda182f1f25bf0e6ac24fcbfe09be3dbf3c921074
|
|
7
|
+
data.tar.gz: 25f083bd0f17b0ea6e7f7bd059d4646cdf06baba1edaff0c8dd7ddcd506da96fcaafbf27d63492deb0ad710dead80e44f3854fac36360cbe6a4bce87793e16ae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,209 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-10-23
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **A chain of `>>` is one seq**, however it's grouped:
|
|
8
|
+
`a.as(:x) >> (b.as(:y) >> c.as(:z))` gives `{x:, y:, z:}`, like
|
|
9
|
+
`(a.as(:x) >> b.as(:y)) >> c.as(:z)`, and `a >> (b >> c)` gives
|
|
10
|
+
`[a, b, c]`, not `[a, [b, c]]`. A seq built any other way
|
|
11
|
+
(`seq(...)`, or with a `commit` or `only`) is one part on either side
|
|
12
|
+
of `>>` or `>`, as in `seq(x, y)`: `seq(a, b) >> c` is `[[a, b], c]`
|
|
13
|
+
rather than `[a, b, c]`, `(seq(a, commit, b) >> c) | d` backtracks
|
|
14
|
+
when `c` fails instead of committing to it,
|
|
15
|
+
`seq("[", only(x), "]") >> y` keeps `y`, and a stored
|
|
16
|
+
`pair = seq(k.as(:k), v.as(:v))` gives `pair >> z` as `[{k:, v:}, z]`.
|
|
17
|
+
Where a value changed, write the seq you mean: `seq(a, b, c)` for the
|
|
18
|
+
flat one, `seq(a, commit, b, c)` to commit to `c`, or `a >> seq(b, c)`
|
|
19
|
+
for `[a, [b, c]]`.
|
|
20
|
+
- **The first parse builds every rule reachable from the root and the
|
|
21
|
+
ignore rule**, so a rule whose matcher can't be built (`.captures` on
|
|
22
|
+
a pattern with no named groups, say) raises `ArgumentError` then, even
|
|
23
|
+
if the input never reaches it. Fix or remove such rules. An undefined
|
|
24
|
+
rule still raises only when reached.
|
|
25
|
+
- **`to_ast` has a `[:lexeme, inner]` node**: inner, then the ignore
|
|
26
|
+
rule. Lexeme rules show as one (`lexeme(:num) { match(/\d+/) }` was
|
|
27
|
+
`[:pat, "/\\d+/"]`), and so does an empty literal
|
|
28
|
+
(`[:lexeme, [:str, ""]]`). Literals with text are still
|
|
29
|
+
`[:str, text]`. Code that walks `to_ast` needs to handle the new node.
|
|
30
|
+
`to_ebnf` is unchanged.
|
|
31
|
+
- **`left_recursion` can't be a rule name**: it's a method of every
|
|
32
|
+
grammar now, and defining the rule raises `ArgumentError`. Rename it.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Direct left recursion. A rule that calls itself before consuming
|
|
37
|
+
input, `rule(:expr) { either(seq(expr, "-", term), term) }`, is grown
|
|
38
|
+
from a seed (Warth et al., 2008), so `3-4-5` parses as `(3-4)-5`, with
|
|
39
|
+
a node per level whose span covers it. Errors say what the last
|
|
40
|
+
attempt needed (`1-` expects a term after the `-`), and `to_ruby`
|
|
41
|
+
generates parsers that do the same. `expr op expr` groups to the
|
|
42
|
+
right (`3-(4-5)`): see the README's Left recursion section. A
|
|
43
|
+
left-recursive rule with no alternative that doesn't start with
|
|
44
|
+
itself (it could never match), or with one before an alternative that
|
|
45
|
+
does (it would never grow), raises `ArgumentError` on the first parse
|
|
46
|
+
and in `to_ruby`, saying how to fix it.
|
|
47
|
+
- Indirect left recursion, through a cycle of rules
|
|
48
|
+
(`sum -> terms -> sum`). Each cycle grows from a seed at its leader,
|
|
49
|
+
as pegen does: the first rule that every way round it passes through.
|
|
50
|
+
The checks above cover the whole cycle. A parse that enters a cycle
|
|
51
|
+
at another rule gets a shorter match than growing from there would:
|
|
52
|
+
see the README's Left recursion section.
|
|
53
|
+
- `examples/leftcalc.rb`: `examples/calc.rb` with its arithmetic written
|
|
54
|
+
as left recursion instead of `infix`, giving the same trees.
|
|
55
|
+
- `Grammar.to_ruby(namespace:)` generates a standalone parser, as Ruby
|
|
56
|
+
source Spinel can compile to a native binary, that agrees with
|
|
57
|
+
`parse` on values, spans and errors. Differential tests check the
|
|
58
|
+
README grammar, `examples/calc.rb` and `examples/json.rb` against the
|
|
59
|
+
interpreter (`rake test`, and `rake spinel` for the compiled
|
|
60
|
+
binaries). Actions and `.map(:name)` run even when nothing reads
|
|
61
|
+
their value, as in the interpreter.
|
|
62
|
+
- Generated parsers support nodes whose `initialize` gives fields
|
|
63
|
+
defaults, `node(:name, :args) { def initialize(name:, args: []) = super }`,
|
|
64
|
+
as the README shows. Each node is built with the default for any
|
|
65
|
+
field its value leaves out (named parts, captures, a single value, or
|
|
66
|
+
a Hash known only at parse time), written where it's built so a `[]`
|
|
67
|
+
default is a new Array for every node. The `initialize` must take
|
|
68
|
+
exactly the fields as keywords, with literal defaults, and only call
|
|
69
|
+
`super`; `to_ruby` refuses anything more, naming the node and what it
|
|
70
|
+
does.
|
|
71
|
+
- Copied actions may read `Float::INFINITY` and `Float::NAN` (and
|
|
72
|
+
`-Float::INFINITY`). Other constant paths are still refused.
|
|
73
|
+
- Matchers without a grammar class. The combinators are available on
|
|
74
|
+
`Birman` (`Birman.seq`, `Birman.str`, `Birman.match`...), and
|
|
75
|
+
`Birman.parse(matcher, input, ignore: nil)` and `Birman.parse!` run
|
|
76
|
+
one as a whole grammar, returning `Success`/`Failure` as
|
|
77
|
+
`Grammar.parse` does. `ignore:` takes a matcher, not a rule name. A
|
|
78
|
+
matcher that refers to a rule, or whose `.action`/`.check` names a
|
|
79
|
+
method only a grammar of your own would have, raises `ArgumentError`
|
|
80
|
+
before parsing.
|
|
81
|
+
- `either` passes on a name all its alternatives share:
|
|
82
|
+
`seq(either(a.as(:x), b.as(:x)), c.as(:y))` gives `{x:, y:}`.
|
|
83
|
+
Generated parsers agree.
|
|
84
|
+
- `match(re).captures`: the value is a Hash of `re`'s named groups,
|
|
85
|
+
`{date: "1979-05-27", time: nil}`, with Symbol keys, in order, and
|
|
86
|
+
`nil` for a group that didn't match, so
|
|
87
|
+
`match(MOMENT).captures.to(Moment)` needs no action re-matching the
|
|
88
|
+
text. A regexp with no named groups, or a name used twice, raises
|
|
89
|
+
`ArgumentError`. Generated parsers support it (reading the groups by
|
|
90
|
+
number, as Spinel's scanner needs), building nodes from it directly.
|
|
91
|
+
- `sep_by(x, sep, trailing: true)` (and `sep_by1`) allows one separator
|
|
92
|
+
after the last item, as in TOML's `[1, 2,]`. A list of none still
|
|
93
|
+
can't be just a separator. After a trailing separator the item is
|
|
94
|
+
still expected, so `[1, x` reports `expected value or "]"`; without
|
|
95
|
+
`trailing`, `[1,]` still reports `expected value`. Generated parsers
|
|
96
|
+
support it.
|
|
97
|
+
- `.check(:name, "label")`, a semantic predicate: the grammar's method
|
|
98
|
+
`name` gets the value and returns truthy to accept it. A falsy result
|
|
99
|
+
is an ordinary failure, "expected label", where the match began, so
|
|
100
|
+
it backtracks (the furthest failure still wins) and commits only
|
|
101
|
+
inside a committed `seq`. Generated parsers support it too.
|
|
102
|
+
- Ways to transform a value without a block, so a grammar can be plain
|
|
103
|
+
data (groundwork for compiling grammars later):
|
|
104
|
+
- `.map(:to_i)` calls that method on the value. The Symbol is kept,
|
|
105
|
+
not turned into a Proc; from `.map(&:to_i)`, just delete the `&`.
|
|
106
|
+
- `.action(:json_number)` passes the value to a grammar method, which
|
|
107
|
+
may be private and which subclasses can override.
|
|
108
|
+
- `.means(v)` replaces the value with `v`: `lit("null").means(nil)`.
|
|
109
|
+
|
|
110
|
+
`.map { }` blocks still work.
|
|
111
|
+
- `Birman::Failure.at(span, reason)`, for checks made in a second pass
|
|
112
|
+
over the AST: a committed `Failure` at the start of a node's span,
|
|
113
|
+
whose message is the reason. The README's Checks section shows one.
|
|
114
|
+
- `large_integer(value, base = 0)`, for actions: `Integer`, but a
|
|
115
|
+
generated parser's gives Bignums in a Spinel binary too, where
|
|
116
|
+
`Integer`, `String#to_i`, `#hex` and `#oct` raise past 63 bits, or
|
|
117
|
+
silently give 2**63 - 1 when Spinel can't tell the receiver's type.
|
|
118
|
+
`to_ruby` refuses actions that call any of those (or `send` them by a
|
|
119
|
+
computed name), and `.map(:hex)`, `.map(:oct)`, and `.map(:to_i)` on
|
|
120
|
+
values that may not be Strings. `examples/json.rb` uses it for
|
|
121
|
+
integers and `\u` escapes.
|
|
122
|
+
- `examples/email.rb`, RFC 5322 email addresses (addr-spec, with UTF-8
|
|
123
|
+
and nested comments): a grammar with no ignore rule. It's checked
|
|
124
|
+
against the generated parsers too.
|
|
125
|
+
- `examples/toml.rb`, TOML 1.0, parsing to Hashes, Times, Dates and the
|
|
126
|
+
rest in two passes: the grammar reads a flat list of statements, and
|
|
127
|
+
a second pass builds the Hash and rejects tables or keys defined
|
|
128
|
+
twice, additions to inline tables and arrays, and dates that don't
|
|
129
|
+
exist, reporting each at its node's span. It's tested against a slice
|
|
130
|
+
of the official toml-test suite (`test/toml-test`), and its generated
|
|
131
|
+
parser against the interpreter, Spinel binary too.
|
|
132
|
+
- `examples/query.rb`, a GitHub-style search query (lookahead, implicit
|
|
133
|
+
AND, a custom error formatter), with an evaluator that filters
|
|
134
|
+
records and a compiler to a SQL condition for ActiveRecord's `where`.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- Left recursion is found from the grammar, not the input. A grammar
|
|
139
|
+
with cycles that have no rule in common raises `ArgumentError` on its
|
|
140
|
+
first parse, whatever the input, and so does `to_ruby`, naming them:
|
|
141
|
+
`left recursion through cycles with no rule in common isn't
|
|
142
|
+
supported: :a -> :a; :b -> :b (each rule can call the next without
|
|
143
|
+
consuming input)`. Before, any left recursion raised, and only when
|
|
144
|
+
some input reached it, naming one rule. Through a custom matcher with
|
|
145
|
+
no `to_ast` it's still found only then, with the old message.
|
|
146
|
+
- Names can't be dropped silently. The same `.as` name twice in one
|
|
147
|
+
`seq`, or a `seq` of named parts or a `.captures` (seen through
|
|
148
|
+
`maybe`, `label`, `quiet`, `check`, `&`, `either`, `between` and
|
|
149
|
+
`only`, but not `rep`, `sep_by` or a transform) inside a `seq` whose
|
|
150
|
+
value would drop it, raises `ArgumentError` when the matcher is
|
|
151
|
+
built. Name the inner seq with `.as`, or list its parts in the outer
|
|
152
|
+
one. A rule reference still keeps its names to itself.
|
|
153
|
+
- `examples/toml.rb` reads `inf` and `nan` as `Float::INFINITY` and
|
|
154
|
+
`Float::NAN`, and builds a local time as a `Moment` with no date (its
|
|
155
|
+
`initialize` defaults `date` and `offset` to nil) rather than a
|
|
156
|
+
separate `Clock` node.
|
|
157
|
+
- The library's own rules (`maybe`, `keyword`, `one_of`, `sep_by`,
|
|
158
|
+
`Rules::Numbers`, `Rules::Strings`) and both examples no longer use
|
|
159
|
+
blocks. EBNF output is unchanged; about 4% fewer allocations per JSON
|
|
160
|
+
parse.
|
|
161
|
+
- Internal: the block matcher `Birman::Action` is now `Birman::Map`;
|
|
162
|
+
`Birman::Action` is the `.action(:name)` matcher.
|
|
163
|
+
|
|
164
|
+
### Fixed
|
|
165
|
+
|
|
166
|
+
- A commit failure inside a rule, caught by `present` or a guard where
|
|
167
|
+
the lookahead began, said only that the rule was expected
|
|
168
|
+
(`expected r1 or "b"`), where the same failure without the commit,
|
|
169
|
+
and the generated parser, said what the rule expected there
|
|
170
|
+
(`expected "a", "c" or "b"`). Now it says that too.
|
|
171
|
+
- A lexeme (and a literal) skips the ignore rule of the grammar running
|
|
172
|
+
it, as a bare string always has. Before, it kept the ignore rule of
|
|
173
|
+
wherever it was built: none when built outside a grammar, another
|
|
174
|
+
grammar's when reused, and a parent's old one in a subclass that had
|
|
175
|
+
already parsed when the parent's `ignore` changed.
|
|
176
|
+
- `parse` raised `ArgumentError` on invalid UTF-8 (from the regexps).
|
|
177
|
+
It's now a `Failure` at the first invalid byte, with `reason` "invalid
|
|
178
|
+
UTF-8" (or "invalid US-ASCII", and so on, for input in other
|
|
179
|
+
encodings), so `parse!` raises `ParseError`. The bytes before it are
|
|
180
|
+
valid, so line and column point at it as usual. Generated parsers do
|
|
181
|
+
the same.
|
|
182
|
+
- A rule reached from several alternatives at one position repeated its
|
|
183
|
+
expectations on every memo hit, growing the list as 3^n (a 16-deep
|
|
184
|
+
grammar took 7 seconds). Merging one in now skips expectations
|
|
185
|
+
already recorded, in the interpreter and in generated parsers;
|
|
186
|
+
`Failure#expected` and error messages are unchanged.
|
|
187
|
+
- A rule named after a private helper of one of Birman's rule modules
|
|
188
|
+
(`Rules::Strings`'s `decode_escape`) silently replaced it; it now
|
|
189
|
+
raises `ArgumentError`, as Grammar's and Combinators' private methods
|
|
190
|
+
already did, and a test lists every private method Birman gives a
|
|
191
|
+
grammar, so a new one can't slip through. The error now says why the
|
|
192
|
+
name is taken: a method Birman relies on, one every Ruby object has,
|
|
193
|
+
or one of the grammar's own.
|
|
194
|
+
- Generated parsers called the wrong method when a rule named `ref` or
|
|
195
|
+
`skip` had a part named like another rule's method (rule `:ref`'s
|
|
196
|
+
choice was `ref_pat`, as was rule `:pat`'s entry), and kept a rule's
|
|
197
|
+
value in the same variable as a part of another rule (`:a_pat`'s and
|
|
198
|
+
`:a`'s `a_pat`). Rules whose names meet in the generated parser (`:a?`
|
|
199
|
+
and `:a_3f`) are refused, and copied action methods can no longer
|
|
200
|
+
replace the runtime's `input` or `invalid_input`.
|
|
201
|
+
- A node with no fields, `Empty = node`, built with `.to` drops the
|
|
202
|
+
value: `seq("(", ")").to(Empty)` gives `(Empty)` with its span.
|
|
203
|
+
Before, `parse` raised `TypeError` when the value wasn't a Hash, and
|
|
204
|
+
`to_ruby` refused it or, for `.means({})`, generated
|
|
205
|
+
`Empty.new(, span(p, q))`, a syntax error.
|
|
206
|
+
|
|
3
207
|
## [0.2.2] - 2026-10-17
|
|
4
208
|
|
|
5
209
|
- **Faster:** about 1.75x on the JSON benchmark (159 KB: 0.37s to 0.21s),
|