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/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/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
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 7a616a4bb37bbc..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"
@@ -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`.
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"
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
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
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'
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);
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 e19d365057b87b..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]
@@ -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/cruby.rs b/zjit/src/cruby.rs
index 6ea041d72f078c..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 {
@@ -1280,11 +1291,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
}
@@ -1406,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::*;
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs
index dced5ebe4a117c..786a994d8b0146 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;
+ }
}
}
@@ -4344,7 +4328,7 @@ impl Function {
}
}
}
- self.infer_types();
+ crate::stats::trace_compile_phase("infer_types", || self.infer_types());
}
fn inline(&mut self) {
@@ -4398,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 {
@@ -4686,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) {
@@ -5005,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.
@@ -5504,7 +5488,7 @@ impl Function {
changed = true;
}
if changed {
- self.infer_types();
+ crate::stats::trace_compile_phase("infer_types", || self.infer_types());
}
}
diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs
index 96543daf7f9b56..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():
@@ -15544,4 +15554,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)
+ ");
+ }
}
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();