From 7f4db64f28133c1b14d1714b113a26e82b65209a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 10 Apr 2026 14:01:51 +0200 Subject: [PATCH 01/11] Emit already initialzed constant warnings in a single call When decorating `Warning.warn`, it's much more convenient to get the whole warning in a single string. --- variable.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/variable.c b/variable.c index 9df6e5911cfca1..0eb10e69120230 100644 --- a/variable.c +++ b/variable.c @@ -4002,15 +4002,17 @@ const_tbl_update(struct autoload_const *ac, int autoload_force) else { VALUE name = QUOTE_ID(id); visibility = ce->flag; - if (klass == rb_cObject) - rb_warn("already initialized constant %"PRIsVALUE"", name); - else - rb_warn("already initialized constant %"PRIsVALUE"::%"PRIsVALUE"", - rb_class_name(klass), name); + + VALUE previous = Qnil; if (!NIL_P(ce->file) && ce->line) { - rb_compile_warn(RSTRING_PTR(ce->file), ce->line, - "previous definition of %"PRIsVALUE" was here", name); + previous = rb_sprintf("\n%"PRIsVALUE":%d: warning: previous definition of %"PRIsVALUE" was here", ce->file, ce->line, name); } + + if (klass == rb_cObject) + rb_warn("already initialized constant %"PRIsVALUE"%"PRIsVALUE"", name, previous); + else + rb_warn("already initialized constant %"PRIsVALUE"::%"PRIsVALUE"%"PRIsVALUE"", + rb_class_name(klass), name, previous); } rb_clear_constant_cache_for_id(id); setup_const_entry(ce, klass, val, visibility); From 4245f8e1c8862852a9295c0fdee3aa1b09a567ed Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 10 Apr 2026 08:30:28 -0500 Subject: [PATCH 02/11] [DOC] Harmonize ::atime and #atime methods (#16620) --- file.c | 64 +++++++++++++++++++++++++++++++++------------ pathname_builtin.rb | 28 +++++++++++++------- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/file.c b/file.c index 832e4b1cbbfb6c..748a30868c5b51 100644 --- a/file.c +++ b/file.c @@ -1101,12 +1101,26 @@ static VALUE statx_birthtime(const rb_io_stat_data *st); /* * call-seq: - * stat.atime -> time - * - * Returns the last access time for this file as an object of class - * Time. - * - * File.stat("testfile").atime #=> Wed Dec 31 18:00:00 CST 1969 + * atime -> new_time + * + * Returns a new Time object containing the access time + * of the object represented by +self+ + * at the time +self+ was created; + * see {Snapshot}[rdoc-ref:File::Stat@Snapshot]: + * + * filepath = 't.tmp' + * File.write(filepath, 'foo') + * file = File.new(filepath, 'w') + * stat = File::Stat.new(filepath) + * file.atime # => 2026-03-31 16:26:39.5913207 -0500 + * stat.atime # => 2026-03-31 16:26:39.5913207 -0500 + * File.write(filepath, 'bar') + * file.atime # => 2026-03-31 16:27:01.4981624 -0500 # Changed by access. + * stat.atime # => 2026-03-31 16:26:39.5913207 -0500 # Unchanged by access. + * stat = File::Stat.new(filepath) + * stat.atime # => 2026-03-31 16:27:01.4981624 -0500 # New access time. + * file.close + * File.delete(filepath) * */ @@ -2452,13 +2466,23 @@ rb_file_s_ftype(VALUE klass, VALUE fname) /* * call-seq: - * File.atime(file_name) -> time + * File.atime(object) -> new_time * - * Returns the last access time for the named file as a Time object. + * Returns a new Time object containing the time of the most recent + * access (read or write) to the object, + * which may be a string filepath or dirpath, or a File or Dir object: * - * _file_name_ can be an IO object. + * filepath = 't.tmp' + * File.exist?(filepath) # => false + * File.atime(filepath) # Raises Errno::ENOENT. + * File.write(filepath, 'foo') + * File.atime(filepath) # => 2026-03-31 16:39:37.9290772 -0500 + * File.write(filepath, 'bar') + * File.atime(filepath) # => 2026-03-31 16:39:57.7710876 -0500 * - * File.atime("testfile") #=> Wed Apr 09 08:51:48 CDT 2003 + * File.atime('.') # => 2026-03-31 16:47:49.0970483 -0500 + * File.atime(File.new('README.md')) # => 2026-03-31 11:15:27.8215934 -0500 + * File.atime(Dir.new('.')) # => 2026-03-31 12:39:45.5910591 -0500 * */ @@ -2477,12 +2501,20 @@ rb_file_s_atime(VALUE klass, VALUE fname) /* * call-seq: - * file.atime -> time - * - * Returns the last access time (a Time object) for file, or - * epoch if file has not been accessed. - * - * File.new("testfile").atime #=> Wed Dec 31 18:00:00 CST 1969 + * atime -> new_time + * + * Returns a new Time object containing the time of the most recent + * access (read or write) to the file represented by +self+: + * + * filepath = 't.tmp' + * file = File.new(filepath, 'a+') + * file.atime # => 2026-03-31 17:11:27.7285397 -0500 + * file.write('foo') + * file.atime # => 2026-03-31 17:11:27.7285397 -0500 # Unchanged; not yet written. + * file.flush + * file.atime # => 2026-03-31 17:12:11.3408054 -0500 # Changed; now written. + * file.close + * File.delete(filename) * */ diff --git a/pathname_builtin.rb b/pathname_builtin.rb index ff13d68f8e8407..31d63f4da30d26 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1066,15 +1066,25 @@ def binwrite(...) File.binwrite(@path, ...) end # atime -> new_time # # Returns a new Time object containing the time of the most recent - # access (read or write) to the entry; - # via File.atime: - # - # pn = Pathname.new('t.tmp') - # pn.write('foo') - # pn.atime # => 2026-03-22 13:49:44.5165608 -0500 - # pn.read # => "foo" - # pn.atime # => 2026-03-22 13:49:57.5359349 -0500 - # pn.delete + # access (read or write) to the entry represented by +self+: + # + # filepath = 't.tmp' + # pn = Pathname.new(filepath) + # File.exist?(filepath) # => false + # pn.atime # Raises Errno::ENOENT: No such file or directory + # File.write(filepath, 'foo') + # pn.atime # => 2026-03-22 13:49:44.5165608 -0500 + # File.read(filepath) + # pn.atime # => 2026-03-22 13:49:57.5359349 -0500 + # File.delete(filepath) + # + # dirpath = 'tmp' + # Dir.mkdir(dirpath) + # pn = Pathname.new(dirpath) + # pn.atime # => 2026-03-31 11:46:35.4813492 -0500 + # Dir.empty?(dirname) # => true + # pn.atime # => 2026-03-31 11:51:10.1210092 -0500 + # Dir.delete(dirpath) # def atime() File.atime(@path) end From c30d74b3e2522c081e083cc860646b7a43c5511f Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Fri, 10 Apr 2026 15:44:58 +0100 Subject: [PATCH 03/11] Ensure version from bundled_gems is used in tool/rdoc-srcdir (#16712) Use version from bundled_gems in tool/rdoc-srcdir Previously, `tool/rdoc-srcdir` used `Dir.glob(...).first` to find bundled gems like rdoc and tsort. This picks the first match alphabetically, which can select a stale older version when multiple versions coexist in `.bundle/gems/` (e.g. `rdoc-7.1.0` over `rdoc-7.2.0`). Fix by reading the declared version from `gems/bundled_gems` and constructing the exact path, ensuring the correct version is always loaded regardless of leftover directories. --- tool/rdoc-srcdir | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tool/rdoc-srcdir b/tool/rdoc-srcdir index ecc49b4b2cbb1d..67d024fc0be9b4 100755 --- a/tool/rdoc-srcdir +++ b/tool/rdoc-srcdir @@ -1,7 +1,16 @@ #!ruby -W0 +srcdir = File.dirname(__dir__) +bundled_gems = File.join(srcdir, "gems/bundled_gems") +versions = {} +File.foreach(bundled_gems) do |line| + next if line.start_with?("#") || line.strip.empty? + name, version, = line.split + versions[name] = version +end + %w[tsort rdoc].each do |lib| - path = Dir.glob("#{File.dirname(__dir__)}/.bundle/gems/#{lib}-*").first + path = File.join(srcdir, ".bundle/gems/#{lib}-#{versions[lib]}") $LOAD_PATH.unshift("#{path}/lib") end require 'rdoc/rdoc' From 2183633981fa49728b507940736dad16e7381116 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Fri, 10 Apr 2026 15:05:13 +0200 Subject: [PATCH 04/11] [ruby/rubygems] fix formatting for BUNDLE_PREFER_PATCH variable in man page https://github.com/ruby/rubygems/commit/5bdf29f86c --- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-config.1.ronn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 7a616a4bb37bbc..71d85b5b4eeaf0 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -172,7 +172,7 @@ Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\ \fBplugins\fR (\fBBUNDLE_PLUGINS\fR) Enable Bundler's experimental plugin system\. .TP -\fBprefer_patch\fR (BUNDLE_PREFER_PATCH) +\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR) Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. .TP \fBredirect\fR (\fBBUNDLE_REDIRECT\fR) diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 8a6adb99106e75..c01e836f96b5a6 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -223,7 +223,7 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Whether Bundler will install gems into the default system path (`Gem.dir`). * `plugins` (`BUNDLE_PLUGINS`): Enable Bundler's experimental plugin system. -* `prefer_patch` (BUNDLE_PREFER_PATCH): +* `prefer_patch` (`BUNDLE_PREFER_PATCH`): Prefer updating only to next patch version during updates. Makes `bundle update` calls equivalent to `bundler update --patch`. * `redirect` (`BUNDLE_REDIRECT`): The number of redirects allowed for network requests. Defaults to `5`. From 678b2c1a619a02cf774325bf954c78ec21b32b01 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Fri, 10 Apr 2026 17:06:56 +0200 Subject: [PATCH 05/11] [ruby/rubygems] Update man pages date https://github.com/ruby/rubygems/commit/087625017a --- lib/bundler/man/bundle-add.1 | 2 +- lib/bundler/man/bundle-binstubs.1 | 2 +- lib/bundler/man/bundle-cache.1 | 2 +- lib/bundler/man/bundle-check.1 | 2 +- lib/bundler/man/bundle-clean.1 | 2 +- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-console.1 | 2 +- lib/bundler/man/bundle-doctor.1 | 2 +- lib/bundler/man/bundle-env.1 | 2 +- lib/bundler/man/bundle-exec.1 | 2 +- lib/bundler/man/bundle-fund.1 | 2 +- lib/bundler/man/bundle-gem.1 | 2 +- lib/bundler/man/bundle-help.1 | 2 +- lib/bundler/man/bundle-info.1 | 2 +- lib/bundler/man/bundle-init.1 | 2 +- lib/bundler/man/bundle-install.1 | 2 +- lib/bundler/man/bundle-issue.1 | 2 +- lib/bundler/man/bundle-licenses.1 | 2 +- lib/bundler/man/bundle-list.1 | 2 +- lib/bundler/man/bundle-lock.1 | 2 +- lib/bundler/man/bundle-open.1 | 2 +- lib/bundler/man/bundle-outdated.1 | 2 +- lib/bundler/man/bundle-platform.1 | 2 +- lib/bundler/man/bundle-plugin.1 | 2 +- lib/bundler/man/bundle-pristine.1 | 2 +- lib/bundler/man/bundle-remove.1 | 2 +- lib/bundler/man/bundle-show.1 | 2 +- lib/bundler/man/bundle-update.1 | 2 +- lib/bundler/man/bundle-version.1 | 2 +- lib/bundler/man/bundle.1 | 2 +- lib/bundler/man/gemfile.5 | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 89771d343340b9..f49d8e568c9bed 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ADD" "1" "March 2026" "" +.TH "BUNDLE\-ADD" "1" "April 2026" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index 2a78f530ccefd0..9dbd4f1d3a1f90 100644 --- a/lib/bundler/man/bundle-binstubs.1 +++ b/lib/bundler/man/bundle-binstubs.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-BINSTUBS" "1" "March 2026" "" +.TH "BUNDLE\-BINSTUBS" "1" "April 2026" "" .SH "NAME" \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index a2b0fc6dff194e..e2052ab0ac1606 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CACHE" "1" "March 2026" "" +.TH "BUNDLE\-CACHE" "1" "April 2026" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 index d03b4dc6bdd2ea..825a2889d57a88 100644 --- a/lib/bundler/man/bundle-check.1 +++ b/lib/bundler/man/bundle-check.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CHECK" "1" "March 2026" "" +.TH "BUNDLE\-CHECK" "1" "April 2026" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index 13bd586f486551..0eae33d08d1926 100644 --- a/lib/bundler/man/bundle-clean.1 +++ b/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CLEAN" "1" "March 2026" "" +.TH "BUNDLE\-CLEAN" "1" "April 2026" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 71d85b5b4eeaf0..626f0811d24962 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONFIG" "1" "March 2026" "" +.TH "BUNDLE\-CONFIG" "1" "April 2026" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index 4594cb74be4470..c86b90e3bd23f1 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONSOLE" "1" "March 2026" "" +.TH "BUNDLE\-CONSOLE" "1" "April 2026" "" .SH "NAME" \fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 index e94ebbd8342ba1..fe9a8a35b9afa8 100644 --- a/lib/bundler/man/bundle-doctor.1 +++ b/lib/bundler/man/bundle-doctor.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-DOCTOR" "1" "March 2026" "" +.TH "BUNDLE\-DOCTOR" "1" "April 2026" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-env.1 b/lib/bundler/man/bundle-env.1 index c57bec014eff16..29c4ac2a8e21a5 100644 --- a/lib/bundler/man/bundle-env.1 +++ b/lib/bundler/man/bundle-env.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ENV" "1" "March 2026" "" +.TH "BUNDLE\-ENV" "1" "April 2026" "" .SH "NAME" \fBbundle\-env\fR \- Print information about the environment Bundler is running under .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 index 36fed764aca840..fec7bee39c338c 100644 --- a/lib/bundler/man/bundle-exec.1 +++ b/lib/bundler/man/bundle-exec.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-EXEC" "1" "March 2026" "" +.TH "BUNDLE\-EXEC" "1" "April 2026" "" .SH "NAME" \fBbundle\-exec\fR \- Execute a command in the context of the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1 index 96f182e05f9fbc..2eb07a6c8d8cad 100644 --- a/lib/bundler/man/bundle-fund.1 +++ b/lib/bundler/man/bundle-fund.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-FUND" "1" "March 2026" "" +.TH "BUNDLE\-FUND" "1" "April 2026" "" .SH "NAME" \fBbundle\-fund\fR \- Lists information about gems seeking funding assistance .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index 68c77a03ce187a..bdb84faebdd0a2 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-GEM" "1" "March 2026" "" +.TH "BUNDLE\-GEM" "1" "April 2026" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 index 17e1d4a90449cb..6e6ad14624d739 100644 --- a/lib/bundler/man/bundle-help.1 +++ b/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-HELP" "1" "March 2026" "" +.TH "BUNDLE\-HELP" "1" "April 2026" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index 50f5e36f18c18d..b18b70309c505d 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INFO" "1" "March 2026" "" +.TH "BUNDLE\-INFO" "1" "April 2026" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index 14fd0a73cb4cbf..5ea1c3b4783733 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INIT" "1" "March 2026" "" +.TH "BUNDLE\-INIT" "1" "April 2026" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index 1d52335644f46a..c2f9bfeea1f79b 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INSTALL" "1" "March 2026" "" +.TH "BUNDLE\-INSTALL" "1" "April 2026" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1 index 7e2fcaf0fa5e6e..e99cf67638f392 100644 --- a/lib/bundler/man/bundle-issue.1 +++ b/lib/bundler/man/bundle-issue.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ISSUE" "1" "March 2026" "" +.TH "BUNDLE\-ISSUE" "1" "April 2026" "" .SH "NAME" \fBbundle\-issue\fR \- Get help reporting Bundler issues .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1 index 9170fecd73518f..eb5f7203ec1d76 100644 --- a/lib/bundler/man/bundle-licenses.1 +++ b/lib/bundler/man/bundle-licenses.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LICENSES" "1" "March 2026" "" +.TH "BUNDLE\-LICENSES" "1" "April 2026" "" .SH "NAME" \fBbundle\-licenses\fR \- Print the license of all gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 index 165a99bb58a24a..69276822d2cd00 100644 --- a/lib/bundler/man/bundle-list.1 +++ b/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LIST" "1" "March 2026" "" +.TH "BUNDLE\-LIST" "1" "April 2026" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index 426e80ec77e1bc..ba1915af2e1597 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LOCK" "1" "March 2026" "" +.TH "BUNDLE\-LOCK" "1" "April 2026" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index caeb223844391a..99166e8580f4cf 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OPEN" "1" "March 2026" "" +.TH "BUNDLE\-OPEN" "1" "April 2026" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index 744be279c90128..87725b9029e3da 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OUTDATED" "1" "March 2026" "" +.TH "BUNDLE\-OUTDATED" "1" "April 2026" "" .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 index b859ead72f8c5f..486e2d4406bf6d 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLATFORM" "1" "March 2026" "" +.TH "BUNDLE\-PLATFORM" "1" "April 2026" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index 450d3e0862ea65..1c3feead7630ee 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLUGIN" "1" "March 2026" "" +.TH "BUNDLE\-PLUGIN" "1" "April 2026" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 index f8722bff3eace1..cbfc51399a7029 100644 --- a/lib/bundler/man/bundle-pristine.1 +++ b/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PRISTINE" "1" "March 2026" "" +.TH "BUNDLE\-PRISTINE" "1" "April 2026" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 index df00b8dbdcf0f1..f8981f9fcfcbe1 100644 --- a/lib/bundler/man/bundle-remove.1 +++ b/lib/bundler/man/bundle-remove.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-REMOVE" "1" "March 2026" "" +.TH "BUNDLE\-REMOVE" "1" "April 2026" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index 4f6109a4a6c081..aaf146fa271b8d 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-SHOW" "1" "March 2026" "" +.TH "BUNDLE\-SHOW" "1" "April 2026" "" .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 9e6076a2c2bf06..e5f18f2a1e2674 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-UPDATE" "1" "March 2026" "" +.TH "BUNDLE\-UPDATE" "1" "April 2026" "" .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 index bc0cf692b3da65..24b5dcef45d7a5 100644 --- a/lib/bundler/man/bundle-version.1 +++ b/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VERSION" "1" "March 2026" "" +.TH "BUNDLE\-VERSION" "1" "April 2026" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index c69f0e26bc6f9f..492de63295a132 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE" "1" "March 2026" "" +.TH "BUNDLE" "1" "April 2026" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 2818b122103045..db04250b8b8c7e 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "GEMFILE" "5" "March 2026" "" +.TH "GEMFILE" "5" "April 2026" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" From 573b16aabf31f1bbd2ab23ee3ba01d15f82fabe8 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Fri, 10 Apr 2026 19:39:46 +0100 Subject: [PATCH 06/11] Bump RDoc to latest master (4913d56) (#16713) Update the pinned RDoc revision to pick up the latest changes from ruby/rdoc master. --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index f9075621666abd..eb2ffd37f2c976 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -37,7 +37,7 @@ ostruct 0.6.3 https://github.com/ruby/ostruct pstore 0.2.1 https://github.com/ruby/pstore benchmark 0.5.0 https://github.com/ruby/benchmark logger 1.7.0 https://github.com/ruby/logger -rdoc 7.2.0 https://github.com/ruby/rdoc 911b122a587e24f05434dbeb2c3e39cea607e21f +rdoc 7.2.0 https://github.com/ruby/rdoc 4913d56243f2577c639ba305fd36c40d55c34f1a win32ole 1.9.3 https://github.com/ruby/win32ole irb 1.17.0 https://github.com/ruby/irb cfd0b917d3feb01adb7d413b19faeb0309900599 reline 0.6.3 https://github.com/ruby/reline From 304d37f76223a1cf8e7e002a224a536614639d94 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 10 Apr 2026 15:26:09 -0400 Subject: [PATCH 07/11] ZJIT: Fix hanging loop (#16711) https://github.com/ruby/ruby/pull/16122 (c272297e8a9f2b8034739b915707910b4e568479) worked for maximal SSA but does not work for "normal" SSA. This is because it used information propagating across block args/params as a proxy for tracking changes in dependent blocks. To do this properly we need to move to something like SCCP. See example HIR diff from the repro in codegen test: ```diff diff --git a/before b/after index da00a9e..b1d2a58 100644 --- a/before +++ b/after @@ -64,10 +64,10 @@ bb7(v48:BasicObject): StoreField v46, :_shape_id@0x4, v105 v79:HeapBasicObject = RefineType v46, HeapBasicObject Jump bb5(v79, v41) -bb5(v81:HeapBasicObject, v82:Fixnum[0]): +bb5(v81:HeapBasicObject, v82:Fixnum): PatchPoint NoEPEscape(set_value_loop) v89:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@ADDR, +@0x2b, cme:ADDR) - v114:Fixnum[1] = Const Value(1) + v114:Fixnum = FixnumAdd v82, v89 Jump bb6(v81, v114) ``` (the `Fixnum[0]` is from type inference and the `Fixnum[1]` is from constant folding having folded the `FixnumAdd`) In the meantime, go back to looping over RPO repeatedly. Fix https://github.com/Shopify/ruby/issues/974 --- zjit/src/codegen_tests.rs | 22 +++++++ zjit/src/hir.rs | 118 ++++++++++++++++---------------------- zjit/src/hir/opt_tests.rs | 93 ++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 67 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index e19d365057b87b..55149c22ca0733 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -5610,3 +5610,25 @@ fn test_load_immediates_into_registers_before_masking() { test "#), @"true"); } + +#[test] +fn test_loop_terminates() { + set_call_threshold(3); + // Previous worklist-based type inference only worked for maximal SSA. This is a regression + // test for hanging. + assert_snapshot!(inspect(r#" + class TheClass + def set_value_loop + i = 0 + while i < 10 + @levar ||= i + i += 1 + end + end + end + + 3.times do |i| + TheClass.new.set_value_loop + end + "#), @"3"); +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index dced5ebe4a117c..10c2f461e377c5 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3204,82 +3204,66 @@ impl Function { self.copy_param_types(); let mut reachable = BlockSet::with_capacity(self.blocks.len()); - - // Maintain both a worklist and a fast membership check to avoid linear search - let mut worklist: VecDeque = VecDeque::with_capacity(self.blocks.len()); - let mut in_worklist = BlockSet::with_capacity(self.blocks.len()); - macro_rules! worklist_add { - ($block:expr) => { - if in_worklist.insert($block) { - worklist.push_back($block); - } - }; - } - reachable.insert(self.entries_block); - worklist_add!(self.entries_block); - - // Helper to propagate types along a branch edge and enqueue the target if anything changed - macro_rules! enqueue { - ($self:ident, $target:expr) => { - let newly_reachable = reachable.insert($target.target); - let mut target_changed = newly_reachable; - for (idx, arg) in $target.args.iter().enumerate() { - let arg = self.union_find.borrow().find_const(*arg); - let param = $self.blocks[$target.target.0].params[idx]; - let param = self.union_find.borrow().find_const(param); - let new = self.insn_types[param.0].union(self.insn_types[arg.0]); - if !self.insn_types[param.0].bit_equal(new) { - self.insn_types[param.0] = new; - target_changed = true; - } - } - if target_changed { - worklist_add!($target.target); - } - }; - } - // Walk the graph, computing types until worklist is empty - while let Some(block) = worklist.pop_front() { - in_worklist.remove(block); - if !reachable.get(block) { continue; } - for insn_id in &self.blocks[block.0].insns { - let insn_id = self.union_find.borrow().find_const(*insn_id); - let insn_type = match &self.insns[insn_id.0] { - &Insn::IfTrue { val, ref target } => { - assert!(!self.type_of(val).bit_equal(types::Empty)); - if self.type_of(val).could_be(Type::from_cbool(true)) { - enqueue!(self, target); + // Walk the graph, computing types until fixpoint + let rpo = self.rpo(); + loop { + let mut changed = false; + for &block in &rpo { + if !reachable.get(block) { continue; } + for &insn_id in &self.blocks[block.0].insns { + // Instructions without output, including branch instructions, can't be targets + // of make_equal_to, so we don't need find() here. + let insn_type = match &self.insns[insn_id.0] { + &Insn::IfTrue { val, target: BranchEdge { target, ref args } } => { + assert!(!self.type_of(val).bit_equal(types::Empty)); + if self.type_of(val).could_be(Type::from_cbool(true)) { + reachable.insert(target); + for (idx, arg) in args.iter().enumerate() { + let param = self.blocks[target.0].params[idx]; + self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg)); + } + } + continue; } - continue; - } - &Insn::IfFalse { val, ref target } => { - assert!(!self.type_of(val).bit_equal(types::Empty)); - if self.type_of(val).could_be(Type::from_cbool(false)) { - enqueue!(self, target); + &Insn::IfFalse { val, target: BranchEdge { target, ref args } } => { + assert!(!self.type_of(val).bit_equal(types::Empty)); + if self.type_of(val).could_be(Type::from_cbool(false)) { + reachable.insert(target); + for (idx, arg) in args.iter().enumerate() { + let param = self.blocks[target.0].params[idx]; + self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg)); + } + } + continue; } - continue; - } - &Insn::Jump(ref target) => { - enqueue!(self, target); - continue; - } - &Insn::Entries { ref targets } => { - for target in targets { - if reachable.insert(*target) { - worklist_add!(*target); + &Insn::Jump(BranchEdge { target, ref args }) => { + reachable.insert(target); + for (idx, arg) in args.iter().enumerate() { + let param = self.blocks[target.0].params[idx]; + self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg)); } + continue; } - continue; + Insn::Entries { targets } => { + for &target in targets { + reachable.insert(target); + } + continue; + } + insn if insn.has_output() => self.infer_type(insn_id), + _ => continue, + }; + if !self.type_of(insn_id).bit_equal(insn_type) { + self.insn_types[insn_id.0] = insn_type; + changed = true; } - insn if insn.has_output() => self.infer_type(insn_id), - _ => continue, - }; - if !self.type_of(insn_id).bit_equal(insn_type) { - self.insn_types[insn_id.0] = insn_type; } } + if !changed { + break; + } } } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 96543daf7f9b56..9be09ba21d18f7 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -15544,4 +15544,97 @@ mod hir_opt_tests { Return v43 "); } + + #[test] + fn test_infer_types_across_non_maximal_basic_blocks() { + // Previous worklist-based type inference only worked for maximal SSA. This is a regression + // test for hanging. + eval(" + class TheClass + def set_value_loop + i = 0 + while i < 10 + @levar ||= i + i += 1 + end + end + end + 3.times do |i| + TheClass.new.set_value_loop + end + "); + assert_snapshot!(hir_string_proc("TheClass.instance_method(:set_value_loop)"), @" + fn set_value_loop@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:NilClass = Const Value(nil) + Jump bb3(v1, v2) + bb2(): + EntryPoint JIT(0) + v5:BasicObject = LoadArg :self@0 + v6:NilClass = Const Value(nil) + Jump bb3(v5, v6) + bb3(v8:BasicObject, v9:NilClass): + v13:Fixnum[0] = Const Value(0) + CheckInterrupts + Jump bb6(v8, v13) + bb6(v19:BasicObject, v20:Fixnum): + v24:Fixnum[10] = Const Value(10) + PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) + v110:BoolExact = FixnumLt v20, v24 + CheckInterrupts + v30:CBool = Test v110 + IfTrue v30, bb4(v19, v20) + v35:NilClass = Const Value(nil) + CheckInterrupts + Return v35 + bb4(v40:BasicObject, v41:Fixnum): + PatchPoint SingleRactorMode + v46:HeapBasicObject = GuardType v40, HeapBasicObject + v47:CUInt64 = LoadField v46, :_rbasic_flags@0x1038 + v49:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v50:CPtr[CPtr(0x1039)] = Const CPtr(0x1039) + v51 = RefineType v50, CUInt64 + v52:CInt64 = IntAnd v47, v49 + v53:CBool = IsBitEqual v52, v51 + IfTrue v53, bb8() + v57:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v58:CPtr[CPtr(0x103a)] = Const CPtr(0x103a) + v59 = RefineType v58, CUInt64 + v60:CInt64 = IntAnd v47, v57 + v61:CBool = IsBitEqual v60, v59 + IfTrue v61, bb9() + v97:CShape = LoadField v46, :_shape_id@0x103b + v98:CShape[0x103c] = GuardBitEquals v97, CShape(0x103c) + v99:BasicObject = LoadField v46, :@levar@0x103d + Jump bb7(v99) + bb8(): + v55:BasicObject = LoadField v46, :@levar@0x103d + Jump bb7(v55) + bb9(): + v63:NilClass = Const Value(nil) + Jump bb7(v63) + bb7(v48:BasicObject): + CheckInterrupts + v69:CBool = Test v48 + IfTrue v69, bb5(v46, v41) + PatchPoint NoEPEscape(set_value_loop) + PatchPoint SingleRactorMode + v101:CShape = LoadField v46, :_shape_id@0x103b + v102:CShape[0x103e] = GuardBitEquals v101, CShape(0x103e) + StoreField v46, :@levar@0x103d, v41 + WriteBarrier v46, v41 + v105:CShape[0x103c] = Const CShape(0x103c) + StoreField v46, :_shape_id@0x103b, v105 + v79:HeapBasicObject = RefineType v46, HeapBasicObject + Jump bb5(v79, v41) + bb5(v81:HeapBasicObject, v82:Fixnum): + PatchPoint NoEPEscape(set_value_loop) + v89:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1000, +@0x103f, cme:0x1040) + v114:Fixnum = FixnumAdd v82, v89 + Jump bb6(v81, v114) + "); + } } From 5ffaaf046486bd35710e810584b8f22ff7311a4b Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 9 Apr 2026 19:16:01 -0400 Subject: [PATCH 08/11] ZJIT: Assert no side exits in assert_compiles() Most of the time, we want to assert that we compile and the compiled code runs without exiting. A small number of tests trigger side exits, and those are changed to use assert_compiles_allowing_exits(). ```console $ rg -F 'assert_compiles(' -o | wc 289 289 11862 $ rg -F 'assert_compiles_allowing_exits(' -o | wc 38 38 2196 ``` --- zjit/src/backend/lir.rs | 4 +-- zjit/src/codegen_tests.rs | 74 +++++++++++++++++++-------------------- zjit/src/cruby.rs | 12 +++++++ zjit/src/hir/opt_tests.rs | 10 ++++++ zjit/src/stats.rs | 4 +++ 5 files changed, 65 insertions(+), 39 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index bbe8b5a4ec8293..bb8d1e1e735b03 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2750,12 +2750,12 @@ impl Assembler }).unwrap_or(false); // If enabled, instrument exits first, and then jump to a shared exit. - let counted_exit = if get_option!(stats) || should_record_exit { + let counted_exit = if get_option!(stats) || should_record_exit || cfg!(test) { let counted_exit = self.new_label("counted_exit"); self.write_label(counted_exit.clone()); asm_comment!(self, "Counted Exit: {reason}"); - if get_option!(stats) { + if get_option!(stats) || cfg!(test) { asm_comment!(self, "increment a side exit counter"); self.incr_counter(Opnd::const_ptr(exit_counter_ptr(reason)), 1.into()); diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 55149c22ca0733..474cbee5b5b6ba 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2119,7 +2119,7 @@ fn test_opt_empty_p() { def test(x) = x.empty? "); assert_contains_opcode("test", YARVINSN_opt_empty_p); - assert_snapshot!(assert_compiles("[test([1]), test(\"1\"), test({})]"), @"[false, false, true]"); + assert_snapshot!(assert_compiles_allowing_exits("[test([1]), test(\"1\"), test({})]"), @"[false, false, true]"); } #[test] @@ -2128,7 +2128,7 @@ fn test_opt_succ() { def test(obj) = obj.succ "); assert_contains_opcode("test", YARVINSN_opt_succ); - assert_snapshot!(assert_compiles(r#"[test(-1), test("A")]"#), @r#"[0, "B"]"#); + assert_snapshot!(assert_compiles_allowing_exits(r#"[test(-1), test("A")]"#), @r#"[0, "B"]"#); } #[test] @@ -2137,7 +2137,7 @@ fn test_opt_and() { def test(x, y) = x & y "); assert_contains_opcode("test", YARVINSN_opt_and); - assert_snapshot!(assert_compiles("[test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3])]"), @"[1, [3, 2, 1]]"); + assert_snapshot!(assert_compiles_allowing_exits("[test(0b1101, 3), test([3, 2, 1, 4], [8, 1, 2, 3])]"), @"[1, [3, 2, 1]]"); } #[test] @@ -2146,7 +2146,7 @@ fn test_opt_or() { def test(x, y) = x | y "); assert_contains_opcode("test", YARVINSN_opt_or); - assert_snapshot!(assert_compiles("[test(0b1000, 3), test([3, 2, 1], [1, 2, 3])]"), @"[11, [3, 2, 1]]"); + assert_snapshot!(assert_compiles_allowing_exits("[test(0b1000, 3), test([3, 2, 1], [1, 2, 3])]"), @"[11, [3, 2, 1]]"); } #[test] @@ -2170,7 +2170,7 @@ fn test_fixnum_and_side_exit() { def test(a, b) = a & b "); assert_contains_opcode("test", YARVINSN_opt_and); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" [ test(2, 2), test(0b011, 0b110), @@ -2200,7 +2200,7 @@ fn test_fixnum_or_side_exit() { def test(a, b) = a | b "); assert_contains_opcode("test", YARVINSN_opt_or); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" [ test(1, 2), test(2, 2), @@ -2273,7 +2273,7 @@ fn test_opt_not() { def test(obj) = !obj "); assert_contains_opcode("test", YARVINSN_opt_not); - assert_snapshot!(assert_compiles("[test(nil), test(false), test(0)]"), @"[true, true, false]"); + assert_snapshot!(assert_compiles_allowing_exits("[test(nil), test(false), test(0)]"), @"[true, true, false]"); } #[test] @@ -2369,7 +2369,7 @@ fn test_opt_newarray_send_include_p_redefined() { end "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" def test(x) [:y, 1, Object.new].include?(x) end @@ -2404,7 +2404,7 @@ fn test_opt_duparray_send_include_p_redefined() { end "); assert_contains_opcode("test", YARVINSN_opt_duparray_send); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" def test(x) [:y, 1].include?(x) end @@ -2441,7 +2441,7 @@ fn test_opt_newarray_send_pack_redefined() { end "#); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(assert_compiles(r#" + assert_snapshot!(assert_compiles_allowing_exits(r#" [test(65), test(66), test(67)] "#), @r#"["override:A", "override:B", "override:C"]"#); } @@ -2476,7 +2476,7 @@ fn test_opt_newarray_send_pack_buffer_redefined() { end "#); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(assert_compiles(r#" + assert_snapshot!(assert_compiles_allowing_exits(r#" def test(num, buffer) [num].pack('C', buffer:) end @@ -2509,7 +2509,7 @@ fn test_opt_newarray_send_hash_redefined() { test(20) "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(assert_compiles("test(20)"), @"42"); + assert_snapshot!(assert_compiles_allowing_exits("test(20)"), @"42"); } #[test] @@ -2534,7 +2534,7 @@ fn test_opt_newarray_send_max_redefined() { def test(a,b) = [a,b].max "); assert_contains_opcode("test", YARVINSN_opt_newarray_send); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" def test(a,b) = [a,b].max test(15, 30) [test(15, 30), test(45, 35)] @@ -2694,7 +2694,7 @@ fn test_opt_hash_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_hash_freeze); - assert_snapshot!(assert_compiles("test"), @"5"); + assert_snapshot!(assert_compiles_allowing_exits("test"), @"5"); } #[test] @@ -2799,7 +2799,7 @@ fn test_opt_ary_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_ary_freeze); - assert_snapshot!(assert_compiles("test"), @"5"); + assert_snapshot!(assert_compiles_allowing_exits("test"), @"5"); } #[test] @@ -2828,7 +2828,7 @@ fn test_opt_str_freeze_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_str_freeze); - assert_snapshot!(assert_compiles("test"), @"5"); + assert_snapshot!(assert_compiles_allowing_exits("test"), @"5"); } #[test] @@ -2857,7 +2857,7 @@ fn test_opt_str_uminus_rewritten() { test "); assert_contains_opcode("test", YARVINSN_opt_str_uminus); - assert_snapshot!(assert_compiles("test"), @"5"); + assert_snapshot!(assert_compiles_allowing_exits("test"), @"5"); } #[test] @@ -2928,7 +2928,7 @@ fn test_array_fixnum_aref_out_of_bounds_positive() { test(10) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(assert_compiles("test(10)"), @"nil"); + assert_snapshot!(assert_compiles_allowing_exits("test(10)"), @"nil"); } #[test] @@ -2938,7 +2938,7 @@ fn test_array_fixnum_aref_out_of_bounds_negative() { test(-10) "); assert_contains_opcode("test", YARVINSN_opt_aref); - assert_snapshot!(assert_compiles("test(-10)"), @"nil"); + assert_snapshot!(assert_compiles_allowing_exits("test(-10)"), @"nil"); } #[test] @@ -3666,7 +3666,7 @@ fn test_getivar_t_data_then_string() { end OBJ.test; OBJ.test # profile and compile for Thread (T_DATA) "#); - assert_snapshot!(assert_compiles("[STR.test, STR.test]"), @"[1000, 1000]"); + assert_snapshot!(assert_compiles_allowing_exits("[STR.test, STR.test]"), @"[1000, 1000]"); } #[test] @@ -3694,7 +3694,7 @@ fn test_getivar_t_object_then_string() { end OBJ.test; OBJ.test # profile and compile for MyObject "#); - assert_snapshot!(assert_compiles("[STR.test, STR.test]"), @"[1000, 1000]"); + assert_snapshot!(assert_compiles_allowing_exits("[STR.test, STR.test]"), @"[1000, 1000]"); } #[test] @@ -3725,7 +3725,7 @@ fn test_getivar_t_class_then_string() { p MyClass.test; p MyClass.test # profile and compile for MyClass p STR.test "#); - assert_snapshot!(assert_compiles("[STR.test, STR.test]"), @"[1000, 1000]"); + assert_snapshot!(assert_compiles_allowing_exits("[STR.test, STR.test]"), @"[1000, 1000]"); } @@ -3806,7 +3806,7 @@ fn test_expandarray_splat() { test [3, 4] "); assert_contains_opcode("test", YARVINSN_expandarray); - assert_snapshot!(assert_compiles("test [3, 4]"), @"[3, [4]]"); + assert_snapshot!(assert_compiles_allowing_exits("test [3, 4]"), @"[3, [4]]"); } #[test] @@ -3819,7 +3819,7 @@ fn test_expandarray_splat_post() { test [3, 4, 5] "); assert_contains_opcode("test", YARVINSN_expandarray); - assert_snapshot!(assert_compiles("test [3, 4, 5]"), @"[3, [4], 5]"); + assert_snapshot!(assert_compiles_allowing_exits("test [3, 4, 5]"), @"[3, [4], 5]"); } #[test] @@ -3876,7 +3876,7 @@ fn test_dupn() { test([1, 1]) "); assert_contains_opcode("test", YARVINSN_dupn); - assert_snapshot!(assert_compiles(" + assert_snapshot!(assert_compiles_allowing_exits(" one = [1, 1] start_empty = [] [test(one), one, test(start_empty), start_empty] @@ -4438,7 +4438,7 @@ fn test_nil_value_nil_opt_with_guard_side_exit() { test(nil) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(1)"), @"false"); + assert_snapshot!(assert_compiles_allowing_exits("test(1)"), @"false"); } #[test] @@ -4459,7 +4459,7 @@ fn test_true_nil_opt_with_guard_side_exit() { test(true) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4480,7 +4480,7 @@ fn test_false_nil_opt_with_guard_side_exit() { test(false) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4501,7 +4501,7 @@ fn test_integer_nil_opt_with_guard_side_exit() { test(2) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4522,7 +4522,7 @@ fn test_float_nil_opt_with_guard_side_exit() { test(2.0) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4543,7 +4543,7 @@ fn test_symbol_nil_opt_with_guard_side_exit() { test(:bar) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4553,7 +4553,7 @@ fn test_class_nil_opt_with_guard() { test(String) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(Integer)"), @"false"); + assert_snapshot!(assert_compiles_allowing_exits("test(Integer)"), @"false"); } #[test] @@ -4564,7 +4564,7 @@ fn test_class_nil_opt_with_guard_side_exit() { test(Integer) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4574,7 +4574,7 @@ fn test_module_nil_opt_with_guard() { test(Enumerable) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(Kernel)"), @"false"); + assert_snapshot!(assert_compiles_allowing_exits("test(Kernel)"), @"false"); } #[test] @@ -4585,7 +4585,7 @@ fn test_module_nil_opt_with_guard_side_exit() { test(Kernel) "); assert_contains_opcode("test", YARVINSN_opt_nil_p); - assert_snapshot!(assert_compiles("test(nil)"), @"true"); + assert_snapshot!(assert_compiles_allowing_exits("test(nil)"), @"true"); } #[test] @@ -4923,7 +4923,7 @@ fn test_allocating_in_hir_c_method_is() { second "); assert_contains_opcode("test", YARVINSN_opt_new); - assert_snapshot!(assert_compiles("a(Foo)"), @":k"); + assert_snapshot!(assert_compiles_allowing_exits("a(Foo)"), @":k"); } #[test] @@ -5050,7 +5050,7 @@ fn test_fixnum_div_zero() { test(0) "); assert_contains_opcode("test", YARVINSN_opt_div); - assert_snapshot!(assert_compiles(r#"test(0)"#), @r#""divided by 0""#); + assert_snapshot!(assert_compiles_allowing_exits(r#"test(0)"#), @r#""divided by 0""#); } #[test] diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 6ea041d72f078c..bdea8a240411bd 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1280,11 +1280,23 @@ pub mod test_utils { } /// Like inspect, but also asserts that all compilations triggered by this program succeed. + pub fn assert_compiles_allowing_exits(program: &str) -> String { + use crate::state::ZJITState; + ZJITState::enable_assert_compiles(); + let result = inspect(program); + ZJITState::disable_assert_compiles(); + result + } + + /// Like inspect, but also asserts that all compilations triggered by this program succeed and + /// no side exits occurr during the program. pub fn assert_compiles(program: &str) -> String { use crate::state::ZJITState; + let exits_before = crate::stats::total_exit_count(); ZJITState::enable_assert_compiles(); let result = inspect(program); ZJITState::disable_assert_compiles(); + assert_eq!(exits_before, crate::stats::total_exit_count(), "Program side-exited"); result } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 9be09ba21d18f7..f4755d5faca8de 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7510,6 +7510,15 @@ mod hir_opt_tests { "); } + #[test] + fn test_no_side_exit_assertion() { + eval(" + def side_exit = ::RubyVM::ZJIT.induce_side_exit! + side_exit + "); + std::panic::catch_unwind(|| assert_compiles("side_exit")).expect_err("Should panic because the program should side exit"); + } + #[test] fn test_optimize_getivar_on_class_embedded() { eval(" @@ -7519,6 +7528,7 @@ mod hir_opt_tests { end C.test "); + assert_snapshot!(assert_compiles("C.test"), @"42"); assert_snapshot!(hir_string_proc("C.method(:test)"), @" fn test@:4: bb1(): diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 38d69c75339ea6..2e43706e855fd0 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -983,6 +983,10 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> hash } +pub fn total_exit_count() -> u64 { + EXIT_COUNTERS.iter().fold(0, |sum, counter| sum + unsafe { *counter_ptr(*counter) }) +} + /// Measure the time taken by func() and add that to zjit_compile_time. pub fn with_time_stat(counter: Counter, func: F) -> R where F: FnOnce() -> R { let start = Instant::now(); From 4978bfb2109bdb322e40f50721c8393f1df5e515 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 9 Apr 2026 14:04:53 -0400 Subject: [PATCH 09/11] ZJIT: `fmt::Debug` for VALUE in hex. Shorthand for rb_obj_info() The default `{:?}` still always prints the pointer address and never dereferences it, but now in hex. The "alternate" flag lets you do `println!("{my_ruby_object:#?}")` and get a rich printout like `VALUE(0x000000010232fd00 T_CLASS/Object)`. --- zjit/src/cruby.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index bdea8a240411bd..40df1fab030a6f 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -248,7 +248,7 @@ pub struct rb_iseq_constant_body { /// that this is a handle. Sometimes the C code briefly uses VALUE as /// an unsigned integer type and don't necessarily store valid handles but /// thankfully those cases are rare and don't cross the FFI boundary. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Hash)] #[repr(transparent)] // same size and alignment as simply `usize` pub struct VALUE(pub usize); @@ -757,6 +757,17 @@ impl IseqParameters { } } +impl Debug for VALUE { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // Only use rb_obj_info() when {:#?} since it dereferences the pointer so carries some risk. + if f.alternate() { + write!(f, "VALUE({})", self.obj_info()) + } else { + write!(f, "VALUE(0x{:x})", self.0) + } + } +} + impl From for VALUE { /// For `.into()` convenience fn from(iseq: IseqPtr) -> Self { @@ -1418,6 +1429,13 @@ pub mod test_utils { fn value_from_fixnum_too_small_isize() { assert_eq!(VALUE::fixnum_from_isize(RUBY_FIXNUM_MIN-1), VALUE(1)); } + + #[test] + fn value_fmt_debug() { + assert_eq!("VALUE(0xcafe)", format!("{:?}", VALUE(0xcafe))); + let alternate = format!("{:#?}", eval("::Hash")); + assert!(alternate.contains("Hash"), "'Hash' not substring of '{alternate}'"); + } } #[cfg(test)] pub use test_utils::*; From 044a43f42b6170488d0f3ab509a2a4d564eb8a0c Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 10 Apr 2026 17:09:30 -0400 Subject: [PATCH 10/11] ZJIT: Trace infer_types as a sub-pass of other passes (#16714) This helps us see how much time it takes in compiler tracing. --- zjit/src/hir.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 10c2f461e377c5..786a994d8b0146 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4328,7 +4328,7 @@ impl Function { } } } - self.infer_types(); + crate::stats::trace_compile_phase("infer_types", || self.infer_types()); } fn inline(&mut self) { @@ -4382,7 +4382,7 @@ impl Function { } } } - self.infer_types(); + crate::stats::trace_compile_phase("infer_types", || self.infer_types()); } fn load_shape(&mut self, block: BlockId, recv: InsnId) -> InsnId { @@ -4670,7 +4670,7 @@ impl Function { } } } - self.infer_types(); + crate::stats::trace_compile_phase("infer_types", || self.infer_types()); } fn gen_patch_points_for_optimized_ccall(&mut self, block: BlockId, recv_class: VALUE, method_id: ID, cme: *const rb_callable_method_entry_struct, state: InsnId) { @@ -4989,7 +4989,7 @@ impl Function { self.push_insn_id(block, insn_id); } } - self.infer_types(); + crate::stats::trace_compile_phase("infer_types", || self.infer_types()); } /// Convert `Send` instructions with no profile data into `SideExit` with recompile info. @@ -5488,7 +5488,7 @@ impl Function { changed = true; } if changed { - self.infer_types(); + crate::stats::trace_compile_phase("infer_types", || self.infer_types()); } } From c0d86a0103de7130943d54b4a290b76ec7e0c135 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 29 Mar 2026 08:47:36 +0200 Subject: [PATCH 11/11] class.c: rb_class_duplicate_classext also dup content of cvc_tbl [Bug #21952] Shallow copying the table result in the same memory being shared between multiple box, causing double free when one of the box is garbage collected. --- class.c | 25 ++++++++++++++----------- test/ruby/test_box.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/class.c b/class.c index cd07846173783c..e52a541c6e8968 100644 --- a/class.c +++ b/class.c @@ -226,14 +226,6 @@ struct duplicate_id_tbl_data { VALUE klass; }; -static enum rb_id_table_iterator_result -duplicate_classext_id_table_i(ID key, VALUE value, void *data) -{ - struct rb_id_table *tbl = (struct rb_id_table *)data; - rb_id_table_insert(tbl, key, value); - return ID_TABLE_CONTINUE; -} - static enum rb_id_table_iterator_result duplicate_classext_m_tbl_i(ID key, VALUE value, void *data) { @@ -262,8 +254,19 @@ duplicate_classext_m_tbl(struct rb_id_table *orig, VALUE klass, bool init_missin return tbl; } +static enum rb_id_table_iterator_result +duplicate_classext_cvc_tbl_i(ID key, VALUE value, void *data) +{ + struct rb_id_table *tbl = (struct rb_id_table *)data; + struct rb_cvar_class_tbl_entry *cvc_entry = (struct rb_cvar_class_tbl_entry *)value; + struct rb_cvar_class_tbl_entry *copy = ALLOC(struct rb_cvar_class_tbl_entry); + MEMCPY(copy, cvc_entry, struct rb_cvar_class_tbl_entry, 1); + rb_id_table_insert(tbl, key, (VALUE)copy); + return ID_TABLE_CONTINUE; +} + static struct rb_id_table * -duplicate_classext_id_table(struct rb_id_table *orig, bool init_missing) +duplicate_classext_cvc_tbl(struct rb_id_table *orig, bool init_missing) { struct rb_id_table *tbl; @@ -274,7 +277,7 @@ duplicate_classext_id_table(struct rb_id_table *orig, bool init_missing) return NULL; } tbl = rb_id_table_create(rb_id_table_size(orig)); - rb_id_table_foreach(orig, duplicate_classext_id_table_i, tbl); + rb_id_table_foreach(orig, duplicate_classext_cvc_tbl_i, tbl); return tbl; } @@ -411,7 +414,7 @@ rb_class_duplicate_classext(rb_classext_t *orig, VALUE klass, const rb_box_t *bo * RCLASSEXT_CC_TBL(copy) = NULL */ - RCLASSEXT_CVC_TBL(ext) = duplicate_classext_id_table(RCLASSEXT_CVC_TBL(orig), dup_iclass); + RCLASSEXT_CVC_TBL(ext) = duplicate_classext_cvc_tbl(RCLASSEXT_CVC_TBL(orig), dup_iclass); // Subclasses/back-pointers are only in the prime classext. diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 5da29b497136e7..c6a2c5423ea9d8 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -155,6 +155,37 @@ def test_raising_errors_in_require assert_include Ruby::Box.current.inspect, "main" end + def test_class_variables + # [Bug #21952] + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + Ruby::Box.root.eval(<<~RUBY) + module M + @@x = 1 + end + + class A + include M + end + + class B < A + end + RUBY + + code = <<~REPRO + class ::B + @@x += 1 + end + REPRO + + b1 = Ruby::Box.new + assert_equal 2, b1.eval(code) + + b2 = Ruby::Box.new + assert_equal 2, b2.eval(code) + end; + end + def test_autoload_in_box setup_box