Repository navigation
fix: an END block records no splat, instead of claiming one at index 0 - #52
Merged
Merged
Conversation
`END { }` desugars to `at_exit { }`, and the Block it built left SplatIndex at
Go's zero value. Zero is a perfectly good index, and this field's "no splat"
value is -1 -- so the block announced a *splat parameter at position 0 with no
parameters to hold it.
Nothing here crashed, because a parser does not run what it builds. The cost
landed entirely in a consumer: go-embedded-ruby's compiler believed the claim
and emitted a rest-binding that read past an empty environment.
$ echo 'END { puts "E" }' > p.rb && rbgo p.rb
panic: runtime error: slice bounds out of range [:1] with capacity 0
$ echo $?
2
An uncontained Go panic from ordinary Ruby -- in the language since 1.0 -- which
for an embedder takes the host process down rather than returning an error.
go-embedded-ruby/ruby#805.
Verified end to end with a replace directive: with this one line, `END { }`,
`main` + `END`, and `BEGIN` + `main` + `END` all match MRI 4.0.7 byte for byte.
Two of the three &ast.Block literals in this file already set SplatIndex -- the
two that build their value from parsed parameters. The one that forgot is the
one with no parameters to parse, which is the shape any future desugaring will
have, so the test that matters here is not the case but the INVARIANT:
TestEveryBlockHasAConsistentSplatIndex parses a corpus and asserts, over every
Block in every tree, that SplatIndex is -1 or an index into Params. It walks by
reflection rather than a type switch, so a node added later is covered without
anyone remembering to extend it -- not remembering is the failure it exists to
catch -- and it fails when the walk finds no Block at all, since a walk that
reads nothing looks exactly like a tree with no defects.
Both tests were checked in the reverse direction: reverting the one-line fix
turns both red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
to go-embedded-ruby/ruby
that referenced
this pull request
Oct 10, 2026
#809) `END { }` -- ordinary Ruby, in the language since 1.0 -- ended the process with an uncontained Go panic when the handler ran at exit: $ echo 'END { puts "E" }' > p.rb && rbgo p.rb panic: runtime error: slice bounds out of range [:1] with capacity 0 $ echo $? 2 For a program that is a crash instead of an answer. For an EMBEDDER it is worse: ruby.Run does not return an error, it takes the host process down, and nothing the host writes around the call prevents it. #805. The cause was upstream and is one line. `END { }` desugars to `at_exit { }`, and the Block the parser built left SplatIndex at Go's ZERO VALUE -- which is a perfectly good index, where that field's "no splat" value is -1. So the block announced a *splat parameter at position 0 with no parameters to hold it, and this compiler's rest-binding believed it and read past an empty environment. go-ruby-parser/parser#52, released as v0.13.1 (a patch: no exported API moved). The regression test lives here as well as upstream, and that is the point rather than belt and braces: the defect was INVISIBLE IN THE AST. The parser's own suite was green throughout, because a parser does not run what it builds -- the entire cost landed on the consumer that lowers a Block. A downgrade of this dependency brings the crash back, and this is what notices. It asserts the ORDERING too, not just the absence of a crash: END and at_exit handlers run LIFO, measured on both engines (MRI 4.0.7 and rbgo agree, "second registered" then "first registered"). BEGIN and at_exit are in the same test as controls, so a regression in any of the three is attributable to the right half. The README row added this morning said END crashes the process. It did, and this is the commit that stops it, so the row goes in the same change -- a document that is wrong on the day it ships is the thing yesterday's doc sweep was about. Closes #805. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One line, and a Go zero value that means something
END { }desugars toat_exit { }, and theBlockit built leftSplatIndexat Go's zero value. Zero is a perfectly good index, and this field's "no
splat" value is
-1:So the block announced a
*splatparameter at position 0 with no parameters tohold it.
Nothing here crashed, which is why it lasted
A parser does not run what it builds, so the cost landed entirely in a consumer.
go-embedded-ruby's compiler believed the claim and emitted a rest-binding that
read past an empty environment:
An uncontained Go panic from ordinary Ruby —
ENDhas been in the languagesince 1.0 — which for an embedder takes the host process down rather than
returning an error.
go-embedded-ruby/ruby#805.Verified end to end with a
replacedirective. With this one line:END { puts "E" }EEputs "main"+ENDmainEmainEBEGIN+main+ENDBmainEBmainEThe test that matters is the invariant, not the case
Two of the three
&ast.Blockliterals inparser.goalready setSplatIndex—the two that build their value from parsed parameters. The one that forgot
is the one with no parameters to parse, and that is the shape any future
desugaring will have.
So
TestEveryBlockHasAConsistentSplatIndexparses a corpus of seventeen sourcesand asserts, over every
Blockin every tree, thatSplatIndexis-1or avalid index into
Params.added later is covered without anyone remembering to extend the walker — not
remembering is precisely the failure this guard exists to catch.
nothing is indistinguishable from a tree with no defects.
Both tests were checked in the reverse direction: reverting the one-line fix
turns both red, with the guard naming the block and its bad index.
🤖 Generated with Claude Code