Skip to content

deps: parser v0.6.0 — refuse paren-less command calls in argument positions - #694

Merged
tannevaled merged 1 commit into
mainfrom
wt39-parser-v060
Sep 26, 2026
Merged

tannevaled merged 1 commit into
mainfrom
wt39-parser-v060

Conversation

@tannevaled

@tannevaled tannevaled commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

What changes

github.com/go-ruby-parser/parser v0.5.0 → v0.6.0. go.mod / go.sum only.
No replace directive (checked before the commit and again before opening this PR).

Why the gate is different this time

The last two bumps made the parser accept more. v0.6.0 makes it REFUSE
more, and correctly so: a paren-less command call was being accepted in
argument positions where MRI rejects it.

So the risk is inverted — the danger is refusing something real Ruby writes,
not accepting something it does not. A corpus of valid Ruby contains none of the
shapes being refused, so the corpus sweep is structurally blind to this
change
and cannot be the primary gate. It is a floor: it proves the ordinary
case still parses.

Primary gate — enumerated both-directions table

135 shapes, each judged against local MRI 4.0.5 (ruby 4.0.5 (2026-05-20) +PRISM arm64-darwin25) with both parsers, ruby --parser=prism -c and
ruby --parser=parse.y -c, and against parser.Parse at v0.5.0 and at v0.6.0.
Integrity: 135 rows, 135 distinct non-empty keys, floor of 80 shapes asserted;
the MRI columns were recomputed on both runs and came out byte-identical.

direction count
accepted → REFUSED, MRI also refuses (both parsers) 40
accepted → REFUSED where MRI accepts — a regression 0
REFUSED → accepted 0
MRI's two parsers disagree → neither direction asserted 2

The 40: [foo bar], [1, foo bar], a multi-line array literal, [*foo bar],
{k: foo bar}, {"a" => foo bar}, {**foo bar}, {a: 1, b: foo bar},
f(1, foo bar), f(1, foo bar, 2), f(k: foo bar), f(*foo bar),
f(&foo bar), 1 + foo bar, x == foo bar, a << foo bar, -foo bar,
~foo bar, def m(a = foo bar), def m(k: foo bar), x = *foo bar,
defined? foo bar, a ? foo bar : baz, a ? b : foo bar, puts 1, foo bar,
foo bar: baz qux, raise Foo, bar baz, assert_equal baz, foo bar,
when foo bar, super(1, foo bar), a[1, foo bar], puts [foo bar],
p({a: foo bar}), puts(*foo bar), a = b.c d, e.f g, x = 1..foo bar,
[foo bar && 1], a = 1, foo bar, def m; return 1, foo bar; end,
def m; yield 1, foo bar; end.

The boundary is head-position, not paren-versus-bracket

Verified directly against both MRI parsers rather than taken on trust:

  • a[foo bar] is legal — an index bracket is
    primary_value '[' opt_call_args rbracket, and call_args admits command.
  • [foo bar] is not — an array literal is tLBRACK aref_args ']', and
    aref_args wants arg_value.

Same token, opposite verdicts. Likewise f(foo bar, 1) is legal and
f(1, foo bar) is not: the rule is head-only, not sole-argument.

17 index-bracket shapes are in the table — a[foo bar], x = a[foo bar],
a[b[foo bar]], [a[foo bar]], {k: a[foo bar]}, 1 + a[foo bar],
-a[foo bar], def m(x = a[foo bar]), defined? a[foo bar],
f(1, a[foo bar]), a[foo bar] = 1, a[foo bar] += 1, a[foo bar] ||= 1,
a.b[foo bar], @h[foo bar], a&.[](foo bar) — and none of them moved. Nor
did any head-position shape: f(foo bar, 1), puts foo bar, foo bar baz,
f(g h i), obj.meth foo bar, obj&.meth foo bar, Foo::bar baz qux,
super foo bar, def m; yield foo bar; end, a.each { break foo bar },
a.each { next foo bar }, return foo bar, x = foo bar, @a = foo bar,
A = foo bar, x += foo bar, x ||= foo bar, a.b = foo bar,
if/unless/while/until/case/for … foo bar, a = b if foo bar,
foo bar rescue nil, foo bar and baz qux, not foo bar,
assert_equal foo bar, baz, lambda { foo bar }, ->(x) { foo bar },
p (foo bar), [(foo bar)], {k: (foo bar)}, foo bar do end.

A further 24 ordinary-Ruby controls (require, attr_accessor :a, :b, a full
parameter list, a squiggly heredoc, %w[], {**a, b: 1}, f(**h),
a.map(&:to_s), a case/in pattern match, def m = 1, {x:} shorthand,
a.map { _1 + 1 }, …) also did not move.

Where MRI's two parsers disagree

Neither direction is asserted for these. rbgo's verdict is unchanged by the
bump — it accepts both, as prism does:

shape prism parse.y
a[foo bar do end] accepts refuses
f(foo bar do end) accepts refuses

The second is new: the known case was only the index form.

Floor — corpus sweeps, no file may stop parsing

Both run before and after, serially, on the same host, 4-way, TIMEOUT=25,
1-minute load average 4.1 → 6.5 across the pair (sibling agents were measuring
this machine concurrently).

MRI 4.0.5 stdlib (RbConfig::CONFIG["rubylibdir"], in-process through
scripts/conformance/heavyweight/frontend): 728 files, 723 accepted before
and after, zero files moved in either direction
. Integrity asserted per run:
rows == inputs (728), no row with an empty key column, distinct filenames ==
input count, and an accepted-count floor above 100 so a zero result cannot pass.

ruby/spec language + core, corpus pinned at 87b1631: 2206 files.
Integrity asserted per run: 2206 rows, 2206 distinct non-empty keys, and a
plausibility floor on the measured total itself (a total under 1000 fails — 0 is
never plausible). TIMEOUT, a zero pass count and a load failure are three
distinct outcomes and are counted separately: 2196 loaded, 3 timed out, 7
produced no result line, 196 loaded but scored zero — identical on both
sides
.

before (v0.5.0) after (v0.6.0)
passing examples 22759 22760
files whose outcome KIND changed — 0

Zero files moved from parsing to not parsing. One row differs:
core/random/bytes_spec.rb, 8 → 9 passing. That is a flaky spec, not an effect
of the bump: re-run 12 times per binary it oscillates between 8 and 9 under
both (v0.5.0 scored 9 on 6/12 runs, v0.6.0 on 9/12). The FLOOR file is
untouched; the measured total is well above its current 22475.

Other gates

  • CGO_ENABLED=0 GOWORK=off go test -count=1 ./... — green, exit 0, 8 packages,
    zero (cached) results.

  • gofmt -l internal/ cmd/ — empty.

  • CGO_ENABLED=0 go build ./... and GOOS=wasip1 GOARCH=wasm — both clean.

  • Coverage, compared against origin/main rather than read as a total. Same
    -coverpkg=$(go list ./internal/... | paste -sd, -) profile on both sides, each
    go tool cover -func resolved in the tree that produced its profile (a
    baseline profile read through branch sources invents phantom 0.0% entries).
    The rounded totals are equal at 99.9% on this host — no -race available
    locally — which on its own proves nothing, so:

    origin/main this branch
    functions below 100% 16 16
    functions in the measured set 7293 7293

    The below-100 list is byte-identical, and so is the full measured
    function set
    — no function left the set, which is the orphaning signal. The
    two 0.0% entries (internal/vm/bigdecimal_bind.go:35::Inspect,
    internal/vm/date_bind.go:44::Inspect) are present unchanged on main and are
    not introduced here.

    Confirmed independently on the lane that actually gates: the test (ubuntu-latest) coverage step lists 18 functions below 100% on this branch
    and the same 18, byte-identical, on main's last green ci run
    (36254534865) — while both print coverage: 100.0%. The local host lists 16
    of those 18; the two it omits (internal/vm/filestat.go:59::Inspect,
    internal/vm/filestat.go:372::registerFileStat) are a darwin/no--race
    artefact of the host, not of this change, which is why the comparison is a list
    diff against the same lane rather than a total.

  • internal/vm/prelude_frozen_gen.go: the file was deleted and regenerated
    from scratch with go run ./cmd/freeze-prelude from the repo root; its
    SHA-256 came back identical to the committed one
    (8a6a8efcda2edcf44e21f7f9711dab915977fb3b782cb99e62d669f45bf19155). A
    refusing parser change could plausibly have altered how prelude.rb parses;
    it did not.

All seven CI checks are green on this branch: test (ubuntu-latest), test (macos-latest), test (windows-latest), ruby/spec ratchet, arch amd64, arch arm64, wasm (wasip1).

GOOS=js GOARCH=wasm go build ./... still fails on main at
internal/vm/errno.go (syscall.ENOTRECOVERABLE, EOWNERDEAD, ETXTBSY).
That is pre-existing and unrelated — this PR touches no Go source — and #684 is
the fix; it was still open when this was measured.

Pre-existing divergences this table exposed but did not fix

rbgo still accepts five shapes both MRI parsers refuse, unchanged by the bump —
front-end gaps, not regressions:

shape rbgo MRI
!!foo bar accepts refuses
begin; x; rescue foo bar; y; end (exception list) accepts refuses
begin; x; rescue Foo, bar baz; y; end accepts refuses
f(x rescue nil) accepts refuses
f(1, x rescue nil) accepts refuses

The first three are the same arg_value family v0.6.0 addressed, reached
through the unary-! and exc_list productions. The last two are a separate
rule: MRI requires the parentheses, f((x rescue nil)).

Two asymmetries worth recording while the table is fresh, both confirmed against
MRI directly: !foo bar is legal while !!foo bar and ~foo bar are not;
and a = foo bar, 1 is legal while a = 1, foo bar is not — the head-only
rule again, this time in mrhs.

🤖 Generated with Claude Code

…itions

v0.6.0 makes the parser REFUSE more, not accept more. A paren-less command
call was being accepted in argument positions where MRI rejects it:
[foo bar], {k: foo bar}, f(1, foo bar), 1 + foo bar, -foo bar,
def m(a = foo bar), x = *foo bar, defined? foo bar, and more.

Gate for a refusing change is an enumerated both-directions table, not a
corpus sweep: a corpus of valid Ruby contains none of the shapes being
refused, so it is structurally blind here. 135 shapes judged against local
MRI 4.0.5 with both `ruby --parser=prism -c` and `ruby --parser=parse.y -c`:

  40 accepted -> REFUSED, every one refused by both MRI parsers
   0 REFUSED -> accepted
   0 shapes where we now refuse something MRI accepts

The boundary is head-position, not paren-versus-bracket: `a[foo bar]` is an
index bracket (call_args: command) and stays accepted; `[foo bar]` is an
array literal (aref_args: arg_value) and is now refused. Likewise
`f(foo bar, 1)` stays accepted while `f(1, foo bar)` is refused. 17 index
and head-position shapes verified unmoved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 13fa1cf into main Sep 26, 2026
7 checks passed
@tannevaled
tannevaled deleted the wt39-parser-v060 branch September 26, 2026 18:27
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