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"}, + }) +}