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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b052c872052f6d727f345e06626e7a11f080717c04474bb6bfdcd3a2ad9464a
4
- data.tar.gz: 0daa868d84a98399a7ed835301ae320962d9867b949aa83e1dc830e1322fab7c
3
+ metadata.gz: 43738fe8c60d4418ab97a79db6441fbec7ba9a047fc7a6cf2ba7c3e7fb77aa95
4
+ data.tar.gz: 9bbad872875549e7ec95976001ec0a7e01052b2517ee1de3b65fc54b47fa2cc6
5
5
  SHA512:
6
- metadata.gz: b3c9f4bf0d79656d051c51fd105634272d61ba0d05cf9d5e770fcd6326463011dcfdd94f3c158be2729729dacfb281d8e4fc0b16446aa3e2999780549604a926
7
- data.tar.gz: 3f62acbd983729b04bb3d632a6b7cb37925f0d3c0d4f24ca116f7c389576e4829ff88bd53cd4a562fe23de26c6af386eeb2df018203c2bb767e81b9eeb21162b
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),