Skip to content

deps: parser v0.5.0 — the COND half of the do...end-block family - #683

Merged
tannevaled merged 1 commit into
mainfrom
wave35-parser-v0.5.0
Sep 26, 2026
Merged

tannevaled merged 1 commit into
mainfrom
wave35-parser-v0.5.0

Conversation

@tannevaled

@tannevaled tannevaled commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

github.com/go-ruby-parser/parser v0.4.0 → v0.5.0. go.mod/go.sum only.

What v0.5.0 adds

The COND half of the do...end-block family. MRI's lexer does
COND_PUSH(0) at (, [, { and tSTRING_DBEG, and at local_push;
v0.4.0 cleared only the CMDARG flag in those places. So a do...end block was
still refused inside a while/until/for condition as soon as it sat
behind one of those openers, or behind a def/class/module/class << self
in that position.

Witness behaviour, verified against local MRI 4.0.5

Every shape below was run three ways — ruby, rbgo built from main
(parser v0.4.0), rbgo built from this branch (v0.5.0) — as a whole program,
not read off a diff.

shape MRI 4.0.5 main this branch
while (y do end); end runs parse error runs
while [y do end].first; end runs parse error runs
while "#{y do end}".empty?; end runs parse error runs
until (y do end); end runs parse error runs
while def f; y do end; end; break; end runs parse error runs
while ({k: y do end})[:k]; end runs parse error runs
while (class C; y do end; end); break; end runs parse error runs
while (module M; y do end; end); break; end runs parse error runs
while (class << o; y do end; end) && false; end runs parse error runs
while (def o.g; y do end; end) && false; end runs parse error runs
for v in ((y do end) || [1]); break; end runs parse error runs
while (begin; y do end; end); end runs parse error runs
while (case 1 when 1 then y do end end); end runs parse error runs
while (if true then y do end end); end runs parse error runs
while <<~X.empty? + #{y do end} heredoc runs parse error runs
while (y do end; y do end); end runs parse error runs
while (def f; y do end; end) && y do break end runs parse error runs

27 shapes REFUSED → accepted, 0 accepted → REFUSED.

The last row is the hard-scope restoration: after a def body inside the
condition the loop's own do must become a loop do again, and it does.
while (i < 2) do i += 1 end and for v in ([1,2]) do ... end still work
unchanged, so the loop's own do was not eaten.

Binding, not merely parsing

For a shape with two candidate receivers, &b was captured in each receiver
and the block traced to the one that actually received it:

def bar(&b); $got_bar = !b.nil?; nil; end
def foo(x, &b); $got_foo = !b.nil?; nil; end
while (foo bar do end); end
p [$got_foo, $got_bar]

MRI 4.0.5 prints [true, false]; this branch prints [true, false]. The
do...end block binds to the outer command foo, not to the inner bar, as
MRI's precedence requires. main cannot parse the program at all.

The other direction — shapes MRI REFUSES must stay refused

shape MRI main this branch
while y do end; end refuses refuses refuses
while begin; y do end; end; end refuses refuses refuses
until y do end; end refuses refuses refuses
for v in y do end; end refuses refuses refuses
while y y do end; end refuses refuses refuses

Two shapes became reachable over-acceptances, and both reduce to a defect
that already exists on main: rbgo accepts a paren-less command call as a
collection element
, which MRI refuses in any position.

[foo bar]        # MRI: syntax error;  main: [nil]     <- pre-existing
{k: foo bar}     # MRI: syntax error;  main: {k: 1}    <- pre-existing

v0.5.0 does not introduce that; it makes the do...end variant of it
(while [foo bar do end].first; end, while ({k: foo bar do end})[:k]; end)
reachable where the block kept it out before. Filed as a follow-up against the
parser; it is not a regression of this bump.

Measurement

ruby/spec ratchet (pinned corpus 87b1631)

Local, macOS/arm64, TIMEOUT=25, 2-way (a parallel sweep is not
reproducible), the two sweeps back to back under comparable load
(1-minute load average 17→40 for main, 44→21 for the branch — three sibling
agents were working this machine):

main (2aa1c94) this branch
passing examples 22760 22760
files loaded 2196 2197
inputs / output rows 2206 / 2206 2206 / 2206

Zero examples moved. One row differed: core/exception/signo_spec.rb
reported FILEFAIL for main and OK pass=0 for the branch. Run ten more
times (five per binary) it produced a result once out of five for main and
zero times out of five for the branch: Process.kill(:TERM, Process.pid)
kills the rbgo process (rc=143) instead of raising SignalException, so the
shim never gets to print its tally. A pre-existing flake worth 0 examples
either way.

CI agrees independently: the ratchet reads 22772 / 2197 files on main at
2aa1c94 and 22771 / 2196 files on this branch — the same ±1 signal-spec
flake, from the opposite side.

Second corpus: MRI 4.0.5's own stdlib (728 files under rubylibdir)

Parsed with parser.Parse at v0.4.0 and at v0.5.0, 728 inputs and 728 output
rows both times:

v0.4.0 v0.5.0
files that parse 723 / 728 723 / 728

No file moved in either direction. The same five files fail with the same
five messages before and after.

Gates

  • CGO_ENABLED=0 GOWORK=off go test ./... — full suite, 8 packages ok, 0 FAIL.
  • gofmt -l internal/ cmd/ — empty.
  • Coverage, -coverpkg=$(go list ./internal/... | paste -sd, -), same profile
    on a detached worktree at origin/main: the below-100% function list is
    byte-identical
    between main and this branch — 16 functions, 99.9% total
    on this host. No function dropped to 0.0% that was not already there
    (bigdecimal_bind.go:35 Inspect and date_bind.go:44 Inspect read 0.0% on
    both). Run without -race, which needs cgo this host cannot provide.
  • internal/vm/prelude_frozen_gen.go: regenerated with
    go run ./cmd/freeze-prelude from the repo root. It came out byte-identical,
    so the file was deleted and regenerated from scratch to prove the
    generator ran — same md5 bb193eb1e7b30babc8a4f7c0d710742f, 384852 bytes.
    The compiler's lowering did not change.
  • go build ./... and GOOS=wasip1 GOARCH=wasm go build ./... both clean.
    GOOS=js GOARCH=wasm is broken on main already (GOOS=js GOARCH=wasm has not compiled for 3 days, and the test that catches it is behind an env var no workflow sets #682) and is not touched
    here.
  • No replace directive in go.mod, checked before the push and again before
    this description.

Honest summary

This bump is a parser-conformance win with no ruby/spec yield: 26 near-miss
shapes that MRI accepts and main refused now parse and bind correctly, and
neither the pinned ruby/spec corpus nor MRI's own stdlib contains a single
occurrence of any of them. It is worth landing as debt paid down, not as a
score move.

🤖 Generated with Claude Code

MRI's lexer does COND_PUSH(0) at `(`, `[`, `{` and tSTRING_DBEG, and at
local_push. v0.4.0 cleared only the CMDARG flag in those places, so a
`do...end` block was still refused inside a `while`/`until`/`for`
condition once it sat behind one of those openers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled

Copy link
Copy Markdown
Contributor Author

Follow-ups found while measuring this bump. None of them is a regression of
this PR
— each was verified to behave identically on main (parser v0.4.0).

Parser, still refused where MRI 4.0.5 accepts

reproducer MRI rbgo (both) reach
def RB::expand(v); v; end / def self::log_open; 1; end 2 parse error: expected CONST, got "expand" (IDENT) rbconfig.rb, mkmf.rb — 2 of the 5 stdlib files that do not parse
x = if true
then 1
else 2
end (a leading then on its own line)
1 parse error: unexpected token "then" ripper/filter.rb, ripper/lexer.rb
case 1
when
1 then "one"
end (the when value on the next line)
"one" parse error: unexpected token "\n" (NEWLINE) rubygems/request.rb
-> x: 1 { x } (a keyword parameter in a paren-less lambda parameter list) :key parse error: expected {, got "x" (LABEL) core/proc/parameters_spec.rb — 27 examples, the only ruby/spec file that fails to load for a parse error

Parser, accepted where MRI 4.0.5 refuses

reproducer MRI rbgo (both)
[foo bar] — a paren-less command call as an array element syntax error [nil]
{k: foo bar} — same, as a hash value syntax error {k: 1}

This is what makes while [foo bar do end].first; end and
while ({k: foo bar do end})[:k]; end newly reachable over-acceptances: the
root is on main, the do…end block merely used to keep them out.

VM: Process.kill(:TERM, Process.pid) kills the process

core/exception/signo_spec.rb exits rc=143 instead of raising
SignalException, so the harness never sees a tally. It is racy — it produced a
result once in five runs on main and zero in five on this branch, which is the
whole ±1 difference between the two ratchet readings. The same silent exit costs
the eight other files that report nothing:

file examples MRI reaches through our own shim
core/signal/trap_spec.rb 36
core/exception/signal_exception_spec.rb 12
core/kernel/exit_spec.rb 12
core/process/exit_spec.rb 8
core/mutex/lock_spec.rb 4
core/exception/interrupt_spec.rb 3
core/exception/signm_spec.rb 1
core/exception/signo_spec.rb 1

≈77 examples behind one defect. Entry points: internal/vm/process.go:306
(Process.kill) and internal/vm/signal.go (Signal.trap).
core/string/unpack/carret_spec.rb produces nothing for MRI under our shim
either, so that one is a shim gap, not ours.

🤖 Generated with Claude Code

@tannevaled
tannevaled merged commit 7269d0a into main Sep 26, 2026
7 checks passed
@tannevaled
tannevaled deleted the wave35-parser-v0.5.0 branch September 26, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant