From 70e7b28bf6a21e2447b4d83c1fd5512a52f0ac52 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 9 Oct 2026 21:16:36 +0200 Subject: [PATCH] deps: parser v0.13.1, so `END { }` runs instead of killing the process `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 --- README.md | 1 - go.mod | 2 +- go.sum | 4 +-- internal/vm/exe_block_test.go | 46 +++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 internal/vm/exe_block_test.go diff --git a/README.md b/README.md index 055a4517..e061ad12 100644 --- a/README.md +++ b/README.md @@ -676,7 +676,6 @@ Otherwise: | `pp` | neither `Kernel#pp` nor `require "pp"` exists (`require "prettyprint"` does work) | | `Process.fork` | does not exist — Go's runtime cannot be forked safely | | `Kernel#fork` | **exists, and does not fork.** `fork { ... }` runs the block **in the same process**, so the “child” mutates the parent's globals, `ENV` and working directory, and returns a synthetic pid (`100001`, counting up) while `Process.pid` is unchanged. Under MRI the block runs in a real child and the parent is untouched. A `SystemExit` raised inside the block unwinds the whole program | -| `END { }` | **crashes the process** with a Go panic when the handler runs at exit — `panic: slice bounds out of range [:1] with capacity 0`, exit 2, where MRI runs the block. For an embedder it takes the host down rather than returning an error from `Run`. Tracked as #805. `BEGIN { }` works. Both used to be refused by the front end, which is how this row came to understate it: a gap turned into a crash and the note still described the gap | | `Thread#backtrace` for another thread | raises `NotImplementedError`; rbgo keeps one frame stack per VM, not per thread | ## Platforms diff --git a/go.mod b/go.mod index 3a6941c0..04f19030 100644 --- a/go.mod +++ b/go.mod @@ -129,7 +129,7 @@ require ( github.com/go-ruby-pagy/pagy v0.0.0-20260717071719-997d15eee011 github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e - github.com/go-ruby-parser/parser v0.13.0 + github.com/go-ruby-parser/parser v0.13.1 github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f github.com/go-ruby-pg/pg v0.0.0-20260916100523-3831ec811a67 github.com/go-ruby-prawn/prawn v0.0.0-20260927145511-370d5985305c diff --git a/go.sum b/go.sum index f43c1add..7d5a1222 100644 --- a/go.sum +++ b/go.sum @@ -533,8 +533,8 @@ github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a h1 github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a/go.mod h1:z370KNJcUH8n39yLdlT0Y1QvHukxjsq1XjGpIzvfqWE= github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e h1:pxhP3nIRxTpj99e74JAVpGj7JQtRSNqLb+B5x2WT5N4= github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e/go.mod h1:wC+J6qfQ8C0aKgi5qsbR03DYrLLuXXezUBHl967am/E= -github.com/go-ruby-parser/parser v0.13.0 h1:JspoqXWvdv1J1LSknUZmCKWwO5x1xT+1CbhFfRaEtNA= -github.com/go-ruby-parser/parser v0.13.0/go.mod h1:Se+ZvnD/4Vm0RRe1U5kZi0GmPSvSczJJXdZLiB0PlJY= +github.com/go-ruby-parser/parser v0.13.1 h1:gq7QVT7KdRp7nZt1UHsa73YDo77qrD8kGdQY/GVEA6g= +github.com/go-ruby-parser/parser v0.13.1/go.mod h1:Se+ZvnD/4Vm0RRe1U5kZi0GmPSvSczJJXdZLiB0PlJY= github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f h1:9pt89wlAp12vbCNGFYmbSqp/PJTHRwUx1yNDXW5hWRI= github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f/go.mod h1:IvW5q5Kyl5l3hrYwiHylOEmk5hpGSo7XqcmUXBHF5H0= github.com/go-ruby-pg/pg v0.0.0-20260916100523-3831ec811a67 h1:X9MKhEAV60oYoL94tqb11lfLw3yW097mVqfjDdy5PK0= diff --git a/internal/vm/exe_block_test.go b/internal/vm/exe_block_test.go new file mode 100644 index 00000000..7f1dc86a --- /dev/null +++ b/internal/vm/exe_block_test.go @@ -0,0 +1,46 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm_test + +import "testing" + +// TestEndBlockRunsInsteadOfCrashing (#805). +// +// `END { }` used to end the process with an uncontained Go panic -- +// `slice bounds out of range [:1] with capacity 0`, exit 2 -- when the handler +// ran at exit. For a program that is a crash instead of an answer; for an +// embedder it is worse, because ruby.Run does not return an error, it takes the +// host process down. +// +// The cause was in the parser and is fixed by v0.13.1: `END { }` desugars to +// `at_exit { }`, and the Block it built left SplatIndex at Go's zero value -- +// which is a valid index, where the field's "no splat" value is -1. So the +// block claimed a *splat parameter at position 0 with no parameters to hold it, +// and this compiler's rest-binding read past an empty environment. +// +// The test lives HERE as well as upstream because the defect was invisible in +// the AST: the parser's own suite was green throughout, and the whole cost +// landed on the consumer that lowers a Block. A downgrade of the dependency +// brings the crash back, and this is what notices. +func TestEndBlockRunsInsteadOfCrashing(t *testing.T) { + checkCases(t, []runCase{ + {`END { puts "E" }`, "E\n"}, + // MRI runs END handlers after the program body, in reverse order of + // registration -- the ordering is the part a "did it crash" test misses. + {`puts "main" +END { puts "E" }`, "main\nE\n"}, + {`END { puts "first registered" } +END { puts "second registered" }`, "second registered\nfirst registered\n"}, + // BEGIN runs before the body and was never broken; it is here so a + // regression in either one is attributable to the right half. + {`BEGIN { puts "B" } +puts "main" +END { puts "E" }`, "B\nmain\nE\n"}, + // at_exit is the shape END desugars to, and it also never broke: if this + // one ever fails with the others, the fault is in at_exit, not in END. + {`at_exit { puts "A" } +puts "main"`, "main\nA\n"}, + }) +}