summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakashi Kokubun <takashikkbn@gmail.com>2026-05-11 13:45:19 -0700
committerTakashi Kokubun <takashikkbn@gmail.com>2026-05-11 13:45:19 -0700
commitb00545b9652782b5ab07d16dc7729b0c69242d3c (patch)
tree44719cbf4caa0ae18365a9dddf7974721db4fa45
parentdd78605b2d06600750c331f307083d60df702814 (diff)
merge revision(s) f89b07ef0046257dd796a2e615cc063072114f16: [Backport #21940]
[PATCH] Mark `$_` as box-dynamic to bypass Box gvar_tbl cache `$_` is updated through svar (rb_lastline_set), bypassing rb_gvar_set, so the Box gvar_tbl cache is never invalidated and returns a stale value. Call rb_gvar_box_dynamic so gvar_use_box_tbl() skips the cache. Fixes [Bug #21940](https://bugs.ruby-lang.org/issues/21940)
-rw-r--r--io.c1
-rw-r--r--test/ruby/test_box.rb43
2 files changed, 44 insertions, 0 deletions
diff --git a/io.c b/io.c
index 7088f036c5..3d2e340b03 100644
--- a/io.c
+++ b/io.c
@@ -15738,6 +15738,7 @@ Init_IO(void)
rb_define_virtual_variable("$_", get_LAST_READ_LINE, set_LAST_READ_LINE);
rb_gvar_ractor_local("$_");
+ rb_gvar_box_dynamic("$_");
rb_define_method(rb_cIO, "initialize_copy", rb_io_init_copy, 1);
rb_define_method(rb_cIO, "reopen", rb_io_reopen, -1);
diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb
index 1e162279d4..313a013c07 100644
--- a/test/ruby/test_box.rb
+++ b/test/ruby/test_box.rb
@@ -551,6 +551,49 @@ class TestBox < Test::Unit::TestCase
end;
end
+ def test_lastline_not_cached_in_box
+ assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
+ begin;
+ r, w = IO.pipe
+ w.write("first\nsecond\n")
+ w.close
+ STDIN.reopen(r)
+ via_gets = Ruby::Box.new.eval(<<~'CODE')
+ gets
+ _ = $_
+ gets
+ $_
+ CODE
+ assert_equal "second\n", via_gets
+ end;
+ end
+
+ def test_lastline_not_cached_in_nested_boxes
+ assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
+ begin;
+ r, w = IO.pipe
+ w.write("outer1\ninner1\ninner2\nouter2\n")
+ w.close
+ STDIN.reopen(r)
+ inner_via_gets, outer_via_gets = Ruby::Box.new.eval(<<~'CODE')
+ gets
+ _ = $_
+
+ inner_result = Ruby::Box.new.eval(<<~'INNER')
+ gets
+ _ = $_
+ gets
+ $_
+ INNER
+
+ gets
+ [inner_result, $_]
+ CODE
+ assert_equal "inner2\n", inner_via_gets
+ assert_equal "outer2\n", outer_via_gets
+ end;
+ end
+
def test_load_path_and_loaded_features
setup_box