summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/zjit-macos.yml2
-rw-r--r--.github/workflows/zjit-ubuntu.yml2
-rw-r--r--doc/file/timestamps.md14
-rw-r--r--doc/string/split.rdoc2
-rw-r--r--file.c78
-rw-r--r--lib/bundled_gems.rb79
-rw-r--r--lib/bundler/cli/outdated.rb8
-rw-r--r--lib/bundler/lazy_specification.rb21
-rw-r--r--lib/bundler/materialization.rb10
-rw-r--r--lib/bundler/resolver.rb6
-rw-r--r--lib/prism/translation/parser/lexer.rb22
-rw-r--r--pathname_builtin.rb74
-rw-r--r--prism/prism.c223
-rw-r--r--proc.c10
-rw-r--r--spec/bundler/commands/lock_spec.rb4
-rw-r--r--spec/bundler/install/gemfile/platform_spec.rb4
-rw-r--r--spec/bundler/install/gemfile/specific_platform_spec.rb151
-rw-r--r--spec/bundler/lock/lockfile_spec.rb2
-rw-r--r--spec/bundler/resolver/platform_spec.rb16
-rw-r--r--test/digest/test_digest.rb2
-rw-r--r--test/prism/errors/rescue_modifier_argument_value_alias.txt3
-rw-r--r--test/prism/errors/rescue_modifier_argument_value_not.txt4
-rw-r--r--test/prism/errors/rescue_modifier_endless_method_multiple_assignment.txt4
-rw-r--r--test/prism/errors/rescue_modifier_multiple_assignment_logical_operator.txt3
-rw-r--r--test/prism/errors/rescue_modifier_multiple_assignment_trailing_comma.txt3
-rw-r--r--test/prism/errors/rescue_modifier_single_assignment_multiple_assignment.txt4
-rw-r--r--test/prism/fixtures/rescue_modifier_assignment.txt3
-rw-r--r--test/prism/fixtures/rescue_modifier_block_command_value.txt5
-rw-r--r--test/prism/fixtures/rescue_modifier_command_value.txt6
-rw-r--r--test/prism/fixtures/rescue_modifier_multiple_assignment.txt9
-rw-r--r--test/prism/fixtures/rescue_modifier_multiple_value.txt5
-rw-r--r--test/prism/fixtures/rescue_modifier_statement_value.txt8
-rw-r--r--test/prism/locals_test.rb18
-rw-r--r--test/prism/ruby/parser_test.rb1
-rw-r--r--test/ruby/test_proc.rb19
-rw-r--r--test/ruby/test_yjit.rb49
-rw-r--r--test/ruby/test_zjit.rb17
-rw-r--r--test/test_bundled_gems.rb33
-rw-r--r--vm.c78
-rw-r--r--yjit/src/codegen.rs23
-rw-r--r--zjit.h27
-rw-r--r--zjit/bindgen/src/main.rs23
-rw-r--r--zjit/src/backend/arm64/mod.rs24
-rw-r--r--zjit/src/backend/lir.rs129
-rw-r--r--zjit/src/backend/tests.rs4
-rw-r--r--zjit/src/backend/x86_64/mod.rs2
-rw-r--r--zjit/src/codegen.rs142
-rw-r--r--zjit/src/codegen_tests.rs63
-rw-r--r--zjit/src/cruby_bindings.inc.rs7
-rw-r--r--zjit/src/cruby_methods.rs12
-rw-r--r--zjit/src/hir.rs83
-rw-r--r--zjit/src/hir/opt_tests.rs292
-rw-r--r--zjit/src/jit_frame.rs66
-rw-r--r--zjit/src/stats.rs2
54 files changed, 1580 insertions, 321 deletions
diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml
index 3340ba08a7..4921d9ef70 100644
--- a/.github/workflows/zjit-macos.yml
+++ b/.github/workflows/zjit-macos.yml
@@ -93,7 +93,7 @@ jobs:
rustup install ${{ matrix.rust_version }} --profile minimal
rustup default ${{ matrix.rust_version }}
- - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
+ - uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml
index 8fd1ef9b7a..30c6138466 100644
--- a/.github/workflows/zjit-ubuntu.yml
+++ b/.github/workflows/zjit-ubuntu.yml
@@ -125,7 +125,7 @@ jobs:
ruby-version: '3.1'
bundler: none
- - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
+ - uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
diff --git a/doc/file/timestamps.md b/doc/file/timestamps.md
index c8ad616567..60934edb05 100644
--- a/doc/file/timestamps.md
+++ b/doc/file/timestamps.md
@@ -51,6 +51,13 @@ Each of these methods returns the modification time for an entry as a Time objec
- File::Stat#mtime.
- Pathname#mtime.
+The modification time (along with the access time) may also be updated explicitly:
+
+- File::lutime.
+- File::utime.
+- Pathname#lutime.
+- Pathname#utime.
+
## Access \Time
The access time for an entry is the time the entry last read.
@@ -64,6 +71,13 @@ Each of these methods returns the access time for an entry as a Time object:
- File::Stat#atime.
- Pathname#atime.
+The access time (along with the modification time) may also be updated explicitly:
+
+- File::lutime.
+- File::utime.
+- Pathname#lutime.
+- Pathname#utime.
+
## Metadata-Change \Time
The metadata-change time for an entry is the time the entry last read.
diff --git a/doc/string/split.rdoc b/doc/string/split.rdoc
index 8679149003..dc6292d182 100644
--- a/doc/string/split.rdoc
+++ b/doc/string/split.rdoc
@@ -68,7 +68,7 @@ and trailing empty strings are included:
When +limit+ is negative,
there is no limit on the size of the array,
-and trailing empty strings are omitted:
+and trailing empty strings are included:
'abracadabra'.split('', -1) # => ["a", "b", "r", "a", "c", "a", "d", "a", "b", "r", "a", ""]
'abracadabra'.split('a', -1) # => ["", "br", "c", "d", "br", ""]
diff --git a/file.c b/file.c
index d41ca1bb7a..dfd05452fd 100644
--- a/file.c
+++ b/file.c
@@ -3374,14 +3374,55 @@ rb_file_s_utime(int argc, VALUE *argv, VALUE _)
#if defined(HAVE_UTIMES) && (defined(HAVE_LUTIMES) || (defined(HAVE_UTIMENSAT) && defined(AT_SYMLINK_NOFOLLOW)))
/*
+ * :markup: markdown
+ *
* call-seq:
- * File.lutime(atime, mtime, file_name, ...) -> integer
+ * File.lutime(atime, mtime, *paths) -> path_count
+ *
+ * Like File#utime, but does not follow symbolic links,
+ * and therefore changes the times of the entries given by `paths`,
+ * regardless of whether they are symbolic links;
+ * returns the number of `paths` given:
+ *
+ * ```ruby
+ * # Create a file and a link to it.
+ * file_path = 't.tmp'
+ * File.write(file_path, '')
+ * link_path = 'link'
+ * File.symlink(file_path, link_path)
+ * # Take snapshots of both.
+ * file_stat = File.stat(file_path)
+ * link_stat = File.lstat(link_path)
+ * # Fetch access times and modification times of both.
+ * file_stat.atime # => 2026-06-15 10:45:11.376753268 -0500
+ * file_stat.mtime # => 2026-06-15 10:44:47.335854904 -0500
+ * link_stat.atime # => 2026-06-15 10:44:59.788801128 -0500
+ * link_stat.mtime # => 2026-06-15 10:44:49.367845961 -0500
+ * # Update access time and modification time of the link.
+ * time = Time.now # => 2026-06-15 10:48:57.847422496 -0500
+ * File.lutime(time, time, link_path)
+ * # Take fresh snapshots of both.
+ * file_stat = File.stat(file_path)
+ * link_stat = File.lstat(link_path)
+ * # Fetch access time and modification time of file (not changed).
+ * file_stat.atime # => 2026-06-15 10:45:11.376753268 -0500
+ * file_stat.mtime # => 2026-06-15 10:44:47.335854904 -0500
+ * # Fetch access time and modification time of link (changed).
+ * link_stat.atime # => 2026-06-15 10:49:27.119146136 -0500
+ * link_stat.mtime # => 2026-06-15 10:48:57.847422496 -0500
+ * # Clean up.
+ * File.delete(file_path)
+ * File.delete(link_path)
+ * ```
+ *
+ * Arguments `atime` and `mtime` may be Time objects (as above).
+ *
+ * Either or both may be integers;
+ * when an integer `i` is passed, `Time.new(i)` is used.
+ *
+ * Either or both may be `nil`, in which case `Time.now` is used.
*
- * Sets the access and modification times of each named file to the
- * first two arguments. If a file is a symlink, this method acts upon
- * the link itself as opposed to its referent; for the inverse
- * behavior, see File.utime. Returns the number of file
- * names in the argument list.
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
@@ -3427,15 +3468,28 @@ syserr_fail2_in(const char *func, int e, VALUE s1, VALUE s2)
#ifdef HAVE_LINK
/*
+ * :markup: markdown
+
* call-seq:
- * File.link(old_name, new_name) -> 0
+ * File.link(path, new_path) -> 0
+ *
+ * Not available on some systems.
+ *
+ * Creates a new entry at `new_path` for the existing entry at `path`
+ * using a [hard link](https://en.wikipedia.org/wiki/Hard_link):
*
- * Creates a new name for an existing file using a hard link. Will not
- * overwrite <i>new_name</i> if it already exists (raising a subclass
- * of SystemCallError). Not available on all platforms.
+ * ```ruby
+ * File.write('doc/t.tmp', 'foo')
+ * File.link('doc/t.tmp', 'lib/u.tmp')
+ * File.read('lib/u.tmp') # => "foo"
+ * File.write('lib/u.tmp', 'bar')
+ * File.read('doc/t.tmp') # => "bar"
+ * File.delete('doc/t.tmp')
+ * File.read('lib/u.tmp') # => "bar"
+ * File.delete('lib/u.tmp')
+ * ```
*
- * File.link("testfile", ".testfile") #=> 0
- * IO.readlines(".testfile")[0] #=> "This is line one\n"
+ * Raises an exception if the entry at `new_path` exists.
*/
static VALUE
diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb
index e6b643678b..fa13f9aa09 100644
--- a/lib/bundled_gems.rb
+++ b/lib/bundled_gems.rb
@@ -46,6 +46,14 @@ module Gem::BUNDLED_GEMS # :nodoc:
DLEXT = /\.#{Regexp.union(dlext)}\z/
LIBEXT = /\.#{Regexp.union("rb", *dlext)}\z/
+ # Decorate Kernel#require so that requiring a gem that is no longer a default
+ # gem emits a warning. Called by Bundler during setup
+ # (lib/bundler/rubygems_integration.rb), guarded by respond_to?(:replace_require)
+ # because older Ruby versions ship without this file.
+ #
+ # The decorating method is defined via define_method, so its base_label is
+ # "replace_require" rather than "require"; this is why uplevel has to recognize
+ # both labels (build_message instead skips our own frames by file path).
def self.replace_require(specs)
return if [::Kernel.singleton_class, ::Kernel].any? {|klass| klass.respond_to?(:no_warning_require) }
@@ -67,20 +75,28 @@ module Gem::BUNDLED_GEMS # :nodoc:
end
end
+ # base_labels that belong to the require machinery: "replace_require" is the
+ # define_method block installed by replace_require, "require" covers both
+ # Kernel#require and the wrappers that Bootsnap and Zeitwerk install on top.
+ REQUIRE_LABELS = ["replace_require", "require"].freeze
+
+ # Compute the uplevel: argument for Kernel#warn so the warning points at the
+ # user's require call site rather than at our machinery. We walk down the run
+ # of require frames (there can be several when Bootsnap/Zeitwerk are present)
+ # and stop at the first frame past them.
def self.uplevel
frame_count = 0
- require_labels = ["replace_require", "require"]
uplevel = 0
require_found = false
Thread.each_caller_location do |cl|
frame_count += 1
if require_found
- unless require_labels.include?(cl.base_label)
+ unless REQUIRE_LABELS.include?(cl.base_label)
return uplevel
end
else
- if require_labels.include?(cl.base_label)
+ if REQUIRE_LABELS.include?(cl.base_label)
require_found = true
end
end
@@ -93,35 +109,53 @@ module Gem::BUNDLED_GEMS # :nodoc:
require_found ? 1 : (frame_count - 1).nonzero?
end
- def self.warning?(name, specs: nil)
- # name can be a feature name or a file path with String or Pathname
+ # Resolve a require target to the bundled gem it belongs to.
+ #
+ # +name+ is a feature name or a file path (String or Pathname). Returns
+ # +[feature, gem_name, subfeature]+ where +feature+ is the normalized feature
+ # string (extension and bootsnap-expanded LIBDIR/ARCHDIR prefix removed),
+ # +gem_name+ is the matching SINCE key, and +subfeature+ is true when the
+ # require targets a nested path (e.g. "benchmark/ips") rather than the gem's
+ # top-level feature. Returns nil when the target is not a bundled gem.
+ #
+ # This is a deliberately cheap check: the SINCE membership test here excludes
+ # the vast majority of candidates before the costly checks in warning?
+ # (see [Bug #20641]).
+ def self.find_gem(name)
feature = File.path(name).sub(LIBEXT, "")
- # The actual checks needed to properly identify the gem being required
- # are costly (see [Bug #20641]), so we first do a much cheaper check
- # to exclude the vast majority of candidates.
- subfeature = if feature.include?("/")
+ if feature.include?("/")
# bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`,
- # and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`.
+ # and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`, so strip
+ # those prefixes back off before matching. [Bug #20450]
feature.delete_prefix!(ARCHDIR)
feature.delete_prefix!(LIBDIR)
# 1. A segment for the EXACT mapping and SINCE check
# 2. A segment for the SINCE check for dashed names
# 3. A segment to check if there's a subfeature
segments = feature.split("/", 3)
- name = segments.shift
- name = EXACT[name] || name
- if !SINCE[name]
- name = "#{name}-#{segments.shift}"
- return unless SINCE[name]
+ gem = segments.shift
+ gem = EXACT[gem] || gem
+ if !SINCE[gem]
+ gem = "#{gem}-#{segments.shift}"
+ return unless SINCE[gem]
end
- segments.any?
+ [feature, gem, segments.any?]
else
- name = EXACT[feature] || feature
- return unless SINCE[name]
- false
+ gem = EXACT[feature] || feature
+ return unless SINCE[gem]
+ [feature, gem, false]
end
+ end
+
+ def self.warning?(name, specs: nil)
+ # name can be a feature name or a file path with String or Pathname
+ feature, name, subfeature = find_gem(name)
+ return unless feature
+ # Callers can suppress warnings for specific gems/features for the duration
+ # of a block via this thread-local. prelude.rb uses it so that `binding.irb`
+ # can load irb (which pulls in reline and rdoc) without warning. [Bug #21723]
if suppress_list = Thread.current[:__bundled_gems_warning_suppression]
return if suppress_list.include?(name) || suppress_list.include?(feature)
end
@@ -202,6 +236,13 @@ module Gem::BUNDLED_GEMS # :nodoc:
msg
end
+ # Activate +gem+ even when it is not declared in the current Gemfile, by
+ # building and setting up a temporary Bundler definition.
+ #
+ # Unlike the rest of this file, this neither detects nor warns about bundled
+ # gems. It exists so that +binding.irb+ (see prelude.rb) and
+ # <tt>bundle console</tt> (lib/bundler/cli/console.rb) can load irb even when
+ # it is not declared. Only usable when Bundler is available.
def self.force_activate(gem)
require "bundler"
Bundler.reset!
diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb
index 465e56ada2..e5c9273383 100644
--- a/lib/bundler/cli/outdated.rb
+++ b/lib/bundler/cli/outdated.rb
@@ -77,7 +77,7 @@ module Bundler
gemfile_specs + dependency_specs
end
- specs.sort_by(&:name).uniq(&:name).each do |current_spec|
+ specs_for_outdated_check(specs).each do |current_spec|
next unless gems.empty? || gems.include?(current_spec.name)
active_spec = retrieve_active_spec(definition, current_spec)
@@ -152,6 +152,12 @@ module Bundler
end
end
+ def specs_for_outdated_check(specs)
+ specs.group_by(&:name).values.filter_map do |matching_specs|
+ MatchPlatform.select_best_platform_match(matching_specs, Bundler.local_platform).first || matching_specs.first
+ end.sort_by(&:name)
+ end
+
def retrieve_active_spec(definition, current_spec)
active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform)
return unless active_spec
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb
index 0da621d21f..86b2da4516 100644
--- a/lib/bundler/lazy_specification.rb
+++ b/lib/bundler/lazy_specification.rb
@@ -9,7 +9,8 @@ module Bundler
include ForcePlatform
attr_reader :name, :version, :platform, :materialization
- attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides
+ attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version
+ attr_accessor :overrides, :locked_platforms
#
# For backwards compatibility with existing lockfiles, if the most specific
@@ -49,6 +50,7 @@ module Bundler
@force_ruby_platform = default_force_ruby_platform
@most_specific_locked_platform = nil
@materialization = nil
+ @locked_platforms = nil
end
def missing?
@@ -145,7 +147,7 @@ module Bundler
# Exact spec is incompatible; in frozen mode, try to find a compatible platform variant
# In non-frozen mode, return nil to trigger re-resolution and lockfile update
if Bundler.frozen_bundle?
- materialize([name, version]) {|specs| resolve_best_platform(specs) }
+ materialize([name, version]) {|specs| resolve_best_platform(specs, locked_platforms_only: true) }
end
else
materialize([name, version]) {|specs| resolve_best_platform(specs) }
@@ -186,12 +188,12 @@ module Bundler
# Try platforms in order of preference until finding a compatible spec.
# Used for legacy lockfiles and as a fallback when the exact locked spec
# is incompatible. Falls back to frozen bundle behavior if none match.
- def resolve_best_platform(specs)
- find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs)
+ def resolve_best_platform(specs, locked_platforms_only: false)
+ find_compatible_platform_spec(specs, locked_platforms_only: locked_platforms_only) || frozen_bundle_fallback(specs)
end
- def find_compatible_platform_spec(specs)
- candidate_platforms.each do |plat|
+ def find_compatible_platform_spec(specs, locked_platforms_only: false)
+ candidate_platforms(locked_platforms_only: locked_platforms_only).each do |plat|
candidates = MatchPlatform.select_best_platform_match(specs, plat)
spec = choose_compatible(candidates, fallback_to_non_installable: false)
return spec if spec
@@ -201,9 +203,12 @@ module Bundler
# Platforms to try in order of preference. Ruby platform is last since it
# requires compilation, but works when precompiled gems are incompatible.
- def candidate_platforms
+ def candidate_platforms(locked_platforms_only: false)
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
- [target, platform, Gem::Platform::RUBY].uniq
+ platforms = [target, platform, Gem::Platform::RUBY].uniq
+ return platforms unless locked_platforms_only && locked_platforms
+
+ platforms & locked_platforms
end
# In frozen mode, accept any candidate. Will error at install time.
diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb
index 82e48464a7..d73e9124a8 100644
--- a/lib/bundler/materialization.rb
+++ b/lib/bundler/materialization.rb
@@ -12,6 +12,7 @@ module Bundler
@dep = dep
@platform = platform
@candidates = candidates
+ set_locked_platforms
end
def complete?
@@ -55,5 +56,14 @@ module Bundler
private
attr_reader :dep, :platform
+
+ def set_locked_platforms
+ return unless @candidates
+
+ platforms = @candidates.map(&:platform)
+ @candidates.each do |candidate|
+ candidate.locked_platforms = platforms if candidate.respond_to?(:locked_platforms=)
+ end
+ end
end
end
diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb
index 7dac202042..6c5331495b 100644
--- a/lib/bundler/resolver.rb
+++ b/lib/bundler/resolver.rb
@@ -305,11 +305,15 @@ module Bundler
next groups if package.force_ruby_platform?
end
- platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq)
+ platform_specs = platform_specs.flatten.uniq
+ platform_group = Resolver::SpecGroup.new((platform_specs + ruby_specs).uniq)
next groups if platform_group == ruby_group
groups << Resolver::Candidate.new(version, group: platform_group, priority: 1)
+ platform_only_group = Resolver::SpecGroup.new(platform_specs)
+ groups << Resolver::Candidate.new(version, group: platform_only_group, priority: 0) unless platform_only_group == platform_group
+
groups
end
end
diff --git a/lib/prism/translation/parser/lexer.rb b/lib/prism/translation/parser/lexer.rb
index e82042867f..dadc53d38e 100644
--- a/lib/prism/translation/parser/lexer.rb
+++ b/lib/prism/translation/parser/lexer.rb
@@ -192,17 +192,25 @@ module Prism
EXPR_BEG = 0x1
EXPR_LABEL = 0x400
- # It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`.
+ # It is used to determine whether `do` is of the token type `kDO` or
+ # `kDO_LAMBDA`.
#
- # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned
- # instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046
+ # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note
+ # that `kDO` is still returned instead of `kDO_LAMBDA`, which is
+ # expected: https://github.com/ruby/prism/pull/3046
LAMBDA_TOKEN_TYPES = Set.new([:kDO_LAMBDA, :tLAMBDA, :tLAMBEG])
- # The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem.
- # The following token types are listed as those classified as `tLPAREN`.
+ # The `PARENTHESIS_LEFT` token in Prism is classified as either
+ # `tLPAREN` or `tLPAREN2` in the Parser gem. The following token types
+ # are listed as those classified as `tLPAREN`.
LPAREN_CONVERSION_TOKEN_TYPES = Set.new([
- :kBREAK, :tCARET, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3,
- :tEQL, :tLPAREN, :tLPAREN2, :tLPAREN_ARG, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS, :tLCURLY
+ :kAND, :kBEGIN, :kBREAK, :kCASE, :kDO_COND, :kDO_LAMBDA, :kDO, :kELSE,
+ :kELSIF, :kENSURE, :kFOR, :kIF_MOD, :kIF, :kIN, :kNEXT, :kOR,
+ :kRESCUE_MOD, :kRESCUE, :kRETURN, :kTHEN, :kUNLESS_MOD, :kUNLESS,
+ :kUNTIL_MOD, :kUNTIL, :kWHEN, :kWHILE_MOD, :kWHILE,
+ :tAMPER, :tANDOP, :tBANG, :tCARET, :tCOMMA, :tDIVIDE, :tDOT2, :tDOT3,
+ :tEQL, :tLCURLY, :tLPAREN_ARG, :tLPAREN, :tLPAREN2, :tLSHFT, :tNL,
+ :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS
])
# Types of tokens that are allowed to continue a method call with comments in-between.
diff --git a/pathname_builtin.rb b/pathname_builtin.rb
index a9e5ead00c..60ae6d3412 100644
--- a/pathname_builtin.rb
+++ b/pathname_builtin.rb
@@ -1436,7 +1436,28 @@ class Pathname # * File *
# Returns <tt>'unknown'</tt> if the type cannot be determined.
def ftype() File.ftype(@path) end
- # See <tt>File.link</tt>. Creates a hard link.
+ # :markup: markdown
+ #
+ # call-seq:
+ # make_link(path) -> 0
+ #
+ # Not available on some systems.
+ #
+ # Creates a new entry at the path in `self` for the existing entry at `path`
+ # using a [hard link](https://en.wikipedia.org/wiki/Hard_link):
+ #
+ # ```ruby
+ # File.write('doc/t.tmp', 'foo')
+ # Pathname('lib/u.tmp').make_link('doc/t.tmp')
+ # File.read('lib/u.tmp') # => "foo"
+ # File.write('lib/u.tmp', 'bar')
+ # File.read('doc/t.tmp') # => "bar"
+ # File.delete('doc/t.tmp')
+ # File.read('lib/u.tmp') # => "bar"
+ # File.delete('lib/u.tmp')
+ # ```
+ #
+ # Raises an exception if the entry at the path in `self` exists.
def make_link(old) File.link(old, @path) end
# See <tt>File.open</tt>. Opens the file for reading or writing.
@@ -1489,11 +1510,56 @@ class Pathname # * File *
# See <tt>File.utime</tt>. Update the access and modification times.
def utime(atime, mtime) File.utime(atime, mtime, @path) end
- # Update the access and modification times of the file.
+ # :markup: markdown
+ #
+ # call-seq:
+ # lutime(atime, mtime) -> 1
+ #
+ # Like Pathname#utime, but does not follow symbolic links,
+ # and therefore changes the times of the entry in `self`,
+ # regardless of whether it is a symbolic link:
+ #
+ # ```ruby
+ # # Create a file and a link to it.
+ # file_path = 't.tmp'
+ # link_path = 'link'
+ # File.write(file_path, '')
+ # File.symlink(file_path, link_path)
+ # # Take snapshots of both.
+ # file_stat = File.stat(file_path)
+ # link_stat = File.lstat(link_path)
+ # # Fetch access times and modification times of both.
+ # file_stat.atime # => 2026-06-15 11:03:29.600373255 -0500
+ # file_stat.mtime # => 2026-06-15 11:03:22.247352211 -0500
+ # link_stat.atime # => 2026-06-15 11:03:29.251372254 -0500
+ # link_stat.mtime # => 2026-06-15 11:03:26.66436484 -0500
+ # # Update access time and modification time of the link.
+ # pn = Pathname(link_path)
+ # time = Time.now # => 2026-06-15 11:08:07.384287523 -0500
+ # pn.lutime(time, time)
+ # # Take fresh snapshots of both.
+ # file_stat = File.stat(file_path)
+ # link_stat = File.lstat(link_path)
+ # # Fetch access time and modification time of file (not changed).
+ # file_stat.atime # => 2026-06-15 11:03:29.600373255 -0500
+ # file_stat.mtime # => 2026-06-15 11:03:22.247352211 -0500
+ # # Fetch access time and modification time of link (changed).
+ # link_stat.atime # => 2026-06-15 11:08:29.847301399 -0500
+ # link_stat.mtime # => 2026-06-15 11:08:07.384287523 -0500
+ # # Clean up.
+ # File.delete(file_path)
+ # File.delete(link_path)
+ # ```
+ #
+ # Arguments `atime` and `mtime` may be Time objects (as above).
+ #
+ # Either or both may be integers;
+ # when an integer `i` is passed, `Time.new(i)` is used.
+ #
+ # Either or both may be `nil`, in which case `Time.now` is used.
#
- # Same as Pathname#utime, but does not follow symbolic links.
+ # See {File System Timestamps}[rdoc-ref:file/timestamps.md].
#
- # See File.lutime.
def lutime(atime, mtime) File.lutime(atime, mtime, @path) end
# call-seq:
diff --git a/prism/prism.c b/prism/prism.c
index 36a6028fe5..1a2cc64596 100644
--- a/prism/prism.c
+++ b/prism/prism.c
@@ -12736,6 +12736,17 @@ expect1_opening(pm_parser_t *parser, pm_token_type_t type, pm_diagnostic_id_t di
#define PM_PARSE_ACCEPTS_DO_BLOCK ((uint8_t) 0x4)
#define PM_PARSE_IN_ENDLESS_DEF ((uint8_t) 0x8)
+/**
+ * Indicates that we are parsing a statement even though the binding power is
+ * below PM_BINDING_POWER_STATEMENT. Only used for the value of a `rescue`
+ * modifier, whose CRuby grammar is a `stmt` in statement, multiple-assignment,
+ * and command-call contexts. Permits statement-only constructs that the
+ * binding power would otherwise reject: a multiple-value right-hand side
+ * (`b = c, d`), a leading splat value (`b = *c`), a parenthesized target list
+ * (`(b, c), d = 1`), and `alias`/`undef`/`END {}`.
+ */
+#define PM_PARSE_ACCEPTS_STATEMENT ((uint8_t) 0x10)
+
static pm_node_t *
parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t flags, pm_diagnostic_id_t diag_id, uint16_t depth);
@@ -18857,7 +18868,7 @@ parse_symbol_array(pm_parser_t *parser, uint16_t depth) {
* assignment, or a set of statements.
*/
static pm_node_t *
-parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint16_t depth) {
+parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t flags, uint16_t depth) {
pm_token_t opening = parser->current;
pm_node_flags_t paren_flags = 0;
@@ -18952,6 +18963,13 @@ parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint16_
} else if (context_p(parser, PM_CONTEXT_FOR_INDEX) && match1(parser, PM_TOKEN_KEYWORD_IN)) {
/* All set, we're inside a for loop and we're parsing multiple
* targets. */
+ } else if (flags & PM_PARSE_ACCEPTS_STATEMENT) {
+ /* The rescue-modifier value parser promotes this target on a
+ * following `=` or comma. Reject any other binary operator that
+ * would otherwise consume the target list (e.g. `(a, b) + c`). */
+ if (pm_binding_powers[parser->current.type].binary && !match1(parser, PM_TOKEN_EQUAL)) {
+ pm_parser_err_node(parser, result, PM_ERR_WRITE_TARGET_UNEXPECTED);
+ }
} else if (binding_power != PM_BINDING_POWER_STATEMENT) {
/* Multi targets are not allowed when it's not a statement
* level. */
@@ -19058,6 +19076,23 @@ parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint16_
}
/**
+ * Parse a splat (`*expression`) whose `*` operator has just been lexed and is
+ * sitting in parser->previous. The expression is optional, since an anonymous
+ * splat is just `*` with no following name.
+ */
+static pm_node_t *
+parse_splat(pm_parser_t *parser, uint8_t flags, uint16_t depth) {
+ pm_token_t operator = parser->previous;
+ pm_node_t *name = NULL;
+
+ if (token_begins_expression_p(parser->current.type)) {
+ name = parse_expression(parser, PM_BINDING_POWER_INDEX, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_STAR, (uint16_t) (depth + 1));
+ }
+
+ return UP(pm_splat_node_create(parser, &operator, name));
+}
+
+/**
* Parse an expression that begins with the previous node that we just lexed.
*/
static PRISM_INLINE pm_node_t *
@@ -19178,7 +19213,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
}
case PM_TOKEN_PARENTHESIS_LEFT:
case PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES:
- return parse_parentheses(parser, binding_power, depth);
+ return parse_parentheses(parser, binding_power, flags, depth);
case PM_TOKEN_BRACE_LEFT: {
// If we were passed a current_hash_keys via the parser, then that
// means we're already parsing a hash and we want to share the set
@@ -19580,7 +19615,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
parser_lex(parser);
return UP(pm_source_line_node_create(parser, &parser->previous));
case PM_TOKEN_KEYWORD_ALIAS: {
- if (binding_power != PM_BINDING_POWER_STATEMENT) {
+ if (binding_power != PM_BINDING_POWER_STATEMENT && !(flags & PM_PARSE_ACCEPTS_STATEMENT)) {
pm_parser_err_current(parser, PM_ERR_STATEMENT_ALIAS);
}
@@ -19808,7 +19843,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
));
}
case PM_TOKEN_KEYWORD_END_UPCASE: {
- if (binding_power != PM_BINDING_POWER_STATEMENT) {
+ if (binding_power != PM_BINDING_POWER_STATEMENT && !(flags & PM_PARSE_ACCEPTS_STATEMENT)) {
pm_parser_err_current(parser, PM_ERR_STATEMENT_POSTEXE_END);
}
@@ -19840,14 +19875,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
// First, parse out the first index expression.
if (accept1(parser, PM_TOKEN_USTAR)) {
- pm_token_t star_operator = parser->previous;
- pm_node_t *name = NULL;
-
- if (token_begins_expression_p(parser->current.type)) {
- name = parse_expression(parser, PM_BINDING_POWER_INDEX, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_STAR, (uint16_t) (depth + 1));
- }
-
- index = UP(pm_splat_node_create(parser, &star_operator, name));
+ index = parse_splat(parser, flags, depth);
} else if (token_begins_expression_p(parser->current.type)) {
index = parse_expression(parser, PM_BINDING_POWER_INDEX, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_COMMA, (uint16_t) (depth + 1));
} else {
@@ -19901,7 +19929,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
return parse_conditional(parser, PM_CONTEXT_IF, opening_newline_index, if_after_else, (uint16_t) (depth + 1));
case PM_TOKEN_KEYWORD_UNDEF: {
- if (binding_power != PM_BINDING_POWER_STATEMENT) {
+ if (binding_power != PM_BINDING_POWER_STATEMENT && !(flags & PM_PARSE_ACCEPTS_STATEMENT)) {
pm_parser_err_current(parser, PM_ERR_STATEMENT_UNDEF);
}
@@ -20361,14 +20389,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
return UP(pm_error_recovery_node_create(parser, PM_TOKEN_START(parser, &parser->previous), PM_TOKEN_LENGTH(&parser->previous)));
}
- pm_token_t operator = parser->previous;
- pm_node_t *name = NULL;
-
- if (token_begins_expression_p(parser->current.type)) {
- name = parse_expression(parser, PM_BINDING_POWER_INDEX, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_STAR, (uint16_t) (depth + 1));
- }
-
- pm_node_t *splat = UP(pm_splat_node_create(parser, &operator, name));
+ pm_node_t *splat = parse_splat(parser, flags, depth);
if (match1(parser, PM_TOKEN_COMMA)) {
return parse_targets_validate(parser, splat, PM_BINDING_POWER_INDEX, (uint16_t) (depth + 1));
@@ -20574,6 +20595,25 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u
}
}
+static pm_node_t *
+parse_rescue_modifier_value(pm_parser_t *parser, uint8_t flags, bool statement, uint16_t depth);
+
+/**
+ * When a `rescue` modifier's handler is itself a statement (a multiple or
+ * implicit-array assignment, a pattern match, or a command call before
+ * `=>`/`in`), its parse stops before any trailing operator above the modifier
+ * level. Such an operator cannot follow a statement — only statement modifiers
+ * (`if`/`unless`/...) can — so report it and skip past it.
+ */
+static void
+parse_rescue_modifier_terminator(pm_parser_t *parser, uint8_t flags, uint16_t depth) {
+ if (pm_binding_powers[parser->current.type].left > PM_BINDING_POWER_MODIFIER) {
+ PM_PARSER_ERR_TOKEN_FORMAT(parser, &parser->current, PM_ERR_EXPECT_EOL_AFTER_STATEMENT, pm_token_str(parser->current.type));
+ parser_lex(parser);
+ parse_expression(parser, pm_binding_powers[parser->previous.type].right, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1));
+ }
+}
+
/**
* Parse a value that is going to be written to some kind of variable or method
* call. We need to handle this separately because the rescue modifier is
@@ -20605,9 +20645,22 @@ parse_assignment_value(pm_parser_t *parser, pm_binding_power_t previous_binding_
pm_token_t rescue = parser->current;
parser_lex(parser);
- pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, flags & PM_PARSE_ACCEPTS_DO_BLOCK, PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1));
+ // As in parse_assignment_values, the resbody is a `stmt` (permitting a
+ // multiple assignment / command call) when the rescued value is itself a
+ // command call, and a plain `arg` otherwise.
+ bool statement_value = pm_command_call_value_p(value) || pm_block_call_p(value);
+ uint8_t rescue_flags = (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (statement_value ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0));
+
+ pm_node_t *right = parse_rescue_modifier_value(parser, rescue_flags, statement_value, (uint16_t) (depth + 1));
context_pop(parser);
+ // A pattern-match resbody is a statement, but here the rescue is nested
+ // in an assignment value where parse_expression_terminator cannot see
+ // it, so reject a trailing operator above the modifier level directly.
+ if (PM_NODE_TYPE_P(right, PM_MATCH_REQUIRED_NODE) || PM_NODE_TYPE_P(right, PM_MATCH_PREDICATE_NODE)) {
+ parse_rescue_modifier_terminator(parser, flags, depth);
+ }
+
return UP(pm_rescue_modifier_node_create(parser, value, &rescue, right));
}
@@ -20664,10 +20717,19 @@ parse_assignment_value_local(pm_parser_t *parser, const pm_node_t *node) {
*/
static pm_node_t *
parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power, uint8_t flags, pm_diagnostic_id_t diag_id, uint16_t depth) {
+ bool statement_level = (previous_binding_power == PM_BINDING_POWER_STATEMENT) || (flags & PM_PARSE_ACCEPTS_STATEMENT);
+
bool permitted = true;
- if (previous_binding_power != PM_BINDING_POWER_STATEMENT && match1(parser, PM_TOKEN_USTAR)) permitted = false;
+ if (!statement_level && match1(parser, PM_TOKEN_USTAR)) permitted = false;
- pm_node_t *value = parse_starred_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MODIFIER ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0))), diag_id, (uint16_t) (depth + 1));
+ // A command call (e.g. `x = y z`) is permitted as the value when assigning
+ // directly (carrying the caller's flag), or in any statement-level context
+ // — which includes a rescue modifier value via the flag.
+ uint8_t command_call_flag = (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT)
+ ? (uint8_t) (flags & PM_PARSE_ACCEPTS_COMMAND_CALL)
+ : ((previous_binding_power < PM_BINDING_POWER_MODIFIER || statement_level) ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0);
+
+ pm_node_t *value = parse_starred_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | command_call_flag), diag_id, (uint16_t) (depth + 1));
if (!permitted) pm_parser_err_node(parser, value, PM_ERR_UNEXPECTED_MULTI_WRITE);
parse_assignment_value_local(parser, value);
@@ -20676,7 +20738,7 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding
// Block calls (command call + do block, e.g., `foo bar do end`) cannot
// be followed by a comma to form a multi-value RHS because each element
// of a multi-value assignment must be an `arg`, not a `block_call`.
- if (previous_binding_power == PM_BINDING_POWER_STATEMENT && !pm_block_call_p(value) && (PM_NODE_TYPE_P(value, PM_SPLAT_NODE) || match1(parser, PM_TOKEN_COMMA))) {
+ if (statement_level && !pm_block_call_p(value) && (PM_NODE_TYPE_P(value, PM_SPLAT_NODE) || match1(parser, PM_TOKEN_COMMA))) {
single_value = false;
pm_array_node_t *array = pm_array_node_create(parser, NULL);
@@ -20705,26 +20767,41 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding
// Contradicting binding powers, the right-hand-side value of the assignment
// allows the `rescue` modifier.
- if ((single_value || (binding_power == (PM_BINDING_POWER_MULTI_ASSIGNMENT + 1))) && match1(parser, PM_TOKEN_KEYWORD_RESCUE_MODIFIER)) {
+ bool multiple_assignment = (binding_power == (PM_BINDING_POWER_MULTI_ASSIGNMENT + 1));
+ if ((single_value || multiple_assignment) && match1(parser, PM_TOKEN_KEYWORD_RESCUE_MODIFIER)) {
+ bool command_value = pm_command_call_value_p(value) || pm_block_call_p(value);
+
+ // A multiple assignment whose value is a command call (`x, y = foo
+ // bar`) is a complete statement (parse.y: `mlhs '='
+ // command_call_value`, which has no rescue), so a trailing `rescue`
+ // modifies the whole assignment rather than the value. Leave it for the
+ // statement-level rescue instead of binding it to the value here. For a
+ // non-command value the rescue does bind to the value (parse.y:
+ // `mlhs '=' mrhs_arg modifier_rescue stmt`).
+ if (multiple_assignment && command_value) return value;
+
context_push(parser, PM_CONTEXT_RESCUE_MODIFIER);
pm_token_t rescue = parser->current;
parser_lex(parser);
- bool accepts_command_call_inner = false;
+ // The resbody is a `stmt` (parse.y: `command_rhs`/`mlhs '=' mrhs_arg`),
+ // which permits a multiple assignment and a command call, when this is a
+ // multiple assignment or the rescued value is itself a command call.
+ // Otherwise it is a plain `arg` (parse.y: `arg_rhs`).
+ bool statement_value = multiple_assignment || command_value;
+ uint8_t rescue_flags = (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (statement_value ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0));
- // RHS can accept command call iff the value is a call with arguments
- // but without parenthesis.
- if (PM_NODE_TYPE_P(value, PM_CALL_NODE)) {
- pm_call_node_t *call_node = (pm_call_node_t *) value;
- if ((call_node->arguments != NULL) && (call_node->opening_loc.length == 0)) {
- accepts_command_call_inner = true;
- }
- }
-
- pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (accepts_command_call_inner ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1));
+ pm_node_t *right = parse_rescue_modifier_value(parser, rescue_flags, statement_value, (uint16_t) (depth + 1));
context_pop(parser);
+ // A pattern-match resbody is a statement, but here the rescue is nested
+ // in an assignment value where parse_expression_terminator cannot see
+ // it, so reject a trailing operator above the modifier level directly.
+ if (PM_NODE_TYPE_P(right, PM_MATCH_REQUIRED_NODE) || PM_NODE_TYPE_P(right, PM_MATCH_PREDICATE_NODE)) {
+ parse_rescue_modifier_terminator(parser, flags, depth);
+ }
+
return UP(pm_rescue_modifier_node_create(parser, value, &rescue, right));
}
@@ -20732,6 +20809,74 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding
}
/**
+ * Parse the value of a `rescue` modifier. When `statement` is true the value
+ * is a `stmt` in CRuby's grammar (parse.y: `stmt modifier_rescue stmt`,
+ * `mlhs '=' mrhs_arg modifier_rescue stmt`, and `command_rhs`), which means it
+ * may be a multiple assignment, e.g. `a rescue b, c = 1`. We parse it at the
+ * rescue modifier's right binding power so that trailing statement modifiers
+ * (if/unless/while/until) stay outside the value, and promote to a multiple
+ * assignment exactly as the statement-level parser does when a comma (or
+ * leading splat) appears. Otherwise the value is a plain `arg` (parse.y:
+ * `arg modifier_rescue arg`), parsed above the `and`/`or`/`not` level so that
+ * those stay outside it.
+ */
+static pm_node_t *
+parse_rescue_modifier_value(pm_parser_t *parser, uint8_t flags, bool statement, uint16_t depth) {
+ if (statement) {
+ pm_node_t *value;
+ bool multiple;
+
+ if (match1(parser, PM_TOKEN_USTAR)) {
+ // A leading splat can only begin a multiple assignment target list.
+ parser_lex(parser);
+ value = parse_splat(parser, flags, depth);
+ multiple = true;
+ } else {
+ // The flag lets a single-target assignment take a multiple-value or
+ // splat right-hand side (`b = c, d` / `b = *c`); a comma _before_ an
+ // `=`, or a parenthesized target list (`(b, c), d = 1`), instead
+ // promotes to a multiple assignment target list below.
+ value = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, flags | PM_PARSE_ACCEPTS_STATEMENT, PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1));
+ multiple = match1(parser, PM_TOKEN_COMMA) || PM_NODE_TYPE_P(value, PM_MULTI_TARGET_NODE);
+ }
+
+ if (multiple) {
+ pm_node_t *target = parse_targets_validate(parser, value, PM_BINDING_POWER_INDEX, (uint16_t) (depth + 1));
+
+ // A promoted target list is only a valid rescue value as part of a
+ // complete `targets = values`. parse_targets_validate already
+ // reports a missing `=` for every terminator except `)` (which it
+ // permits for an enclosing mlhs paren that does not apply here), so
+ // reject that case.
+ if (!match1(parser, PM_TOKEN_EQUAL)) {
+ if (match1(parser, PM_TOKEN_PARENTHESIS_RIGHT)) pm_parser_err_node(parser, target, PM_ERR_WRITE_TARGET_UNEXPECTED);
+ return target;
+ }
+
+ pm_token_t operator = parser->current;
+ parser_lex(parser);
+
+ pm_node_t *values = parse_assignment_values(parser, PM_BINDING_POWER_STATEMENT, PM_BINDING_POWER_MULTI_ASSIGNMENT + 1, flags, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL, (uint16_t) (depth + 1));
+ value = parse_write(parser, target, &operator, values);
+ }
+
+ // Reject a trailing operator that cannot follow a statement resbody.
+ // Pattern-match handlers are statements too, but for the bare statement
+ // form they are reported by parse_expression_terminator instead (which
+ // keeps its existing error-recovery), so they are excluded here.
+ if (!PM_NODE_TYPE_P(value, PM_MATCH_REQUIRED_NODE) && !PM_NODE_TYPE_P(value, PM_MATCH_PREDICATE_NODE)) {
+ parse_rescue_modifier_terminator(parser, flags, depth);
+ }
+
+ return value;
+ }
+
+ // Otherwise the resbody is a plain `arg` (parse.y: `arg modifier_rescue
+ // arg`), parsed above the `and`/`or`/`not` level so those stay outside it.
+ return parse_expression(parser, PM_BINDING_POWER_DEFINED, flags, PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1));
+}
+
+/**
* Ensure a call node that is about to become a call operator node does not
* have arguments or a block attached. If it does, then we'll need to add an
* error message and destroy the arguments/block. Ideally we would keep the node
@@ -21014,7 +21159,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
parser_lex(parser);
pm_node_t *value = parse_assignment_values(parser, previous_binding_power, PM_NODE_TYPE_P(node, PM_MULTI_TARGET_NODE) ? PM_BINDING_POWER_MULTI_ASSIGNMENT + 1 : binding_power, flags, PM_ERR_EXPECT_EXPRESSION_AFTER_EQUAL, (uint16_t) (depth + 1));
- if (PM_NODE_TYPE_P(node, PM_MULTI_TARGET_NODE) && previous_binding_power != PM_BINDING_POWER_STATEMENT) {
+ if (PM_NODE_TYPE_P(node, PM_MULTI_TARGET_NODE) && previous_binding_power != PM_BINDING_POWER_STATEMENT && !(flags & PM_PARSE_ACCEPTS_STATEMENT)) {
pm_parser_err_node(parser, node, PM_ERR_UNEXPECTED_MULTI_WRITE);
}
@@ -21806,7 +21951,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
parser_lex(parser);
accept1(parser, PM_TOKEN_NEWLINE);
- pm_node_t *value = parse_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1));
+ pm_node_t *value = parse_rescue_modifier_value(parser, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), previous_binding_power == PM_BINDING_POWER_STATEMENT, (uint16_t) (depth + 1));
context_pop(parser);
return UP(pm_rescue_modifier_node_create(parser, node, &token, value));
diff --git a/proc.c b/proc.c
index bfdc2cc25b..cb6d214cb5 100644
--- a/proc.c
+++ b/proc.c
@@ -3875,7 +3875,15 @@ curry(RB_BLOCK_CALL_FUNC_ARGLIST(_, args))
return arity;
}
else {
- return rb_proc_call_with_block(proc, check_argc(RARRAY_LEN(passed)), RARRAY_CONST_PTR(passed), blockarg);
+ // `passed` is the only reference keeping this array (and thus the
+ // buffer that RARRAY_CONST_PTR points into) alive, but it is otherwise
+ // unused after this point. Without RB_GC_GUARD the compiler may drop it
+ // before the call returns, so conservative stack marking misses it and
+ // GC can reclaim the array while it is still being read as argv,
+ // crashing with "try to mark T_NONE object".
+ VALUE result = rb_proc_call_with_block(proc, check_argc(RARRAY_LEN(passed)), RARRAY_CONST_PTR(passed), blockarg);
+ RB_GC_GUARD(passed);
+ return result;
}
}
diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb
index 8ab3cc7e8d..1a43400923 100644
--- a/spec/bundler/commands/lock_spec.rb
+++ b/spec/bundler/commands/lock_spec.rb
@@ -1083,8 +1083,10 @@ RSpec.describe "bundle lock" do
simulate_platform("x86-mingw32") { bundle :lock }
checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "ffi", "1.9.14"
c.checksum gem_repo4, "ffi", "1.9.14", "x86-mingw32"
c.checksum gem_repo4, "gssapi", "1.2.0"
+ c.checksum gem_repo4, "mixlib-shellout", "2.2.6"
c.checksum gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"
c.checksum gem_repo4, "win32-process", "0.8.3"
end
@@ -1093,9 +1095,11 @@ RSpec.describe "bundle lock" do
GEM
remote: https://gem.repo4/
specs:
+ ffi (1.9.14)
ffi (1.9.14-x86-mingw32)
gssapi (1.2.0)
ffi (>= 1.0.1)
+ mixlib-shellout (2.2.6)
mixlib-shellout (2.2.6-universal-mingw32)
win32-process (~> 0.8.2)
win32-process (0.8.3)
diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb
index e12933ebcf..c28af3d4ab 100644
--- a/spec/bundler/install/gemfile/platform_spec.rb
+++ b/spec/bundler/install/gemfile/platform_spec.rb
@@ -208,6 +208,7 @@ RSpec.describe "bundle install across platforms" do
c.checksum gem_repo4, "empyrean", "0.1.0"
c.checksum gem_repo4, "ffi", "1.9.23", "java"
c.checksum gem_repo4, "method_source", "0.9.0"
+ c.checksum gem_repo4, "pry", "0.11.3"
c.checksum gem_repo4, "pry", "0.11.3", "java"
c.checksum gem_repo4, "spoon", "0.0.6"
end
@@ -220,6 +221,9 @@ RSpec.describe "bundle install across platforms" do
empyrean (0.1.0)
ffi (1.9.23-java)
method_source (0.9.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
pry (0.11.3-java)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb
index 97b1d233bf..eeda764c39 100644
--- a/spec/bundler/install/gemfile/specific_platform_spec.rb
+++ b/spec/bundler/install/gemfile/specific_platform_spec.rb
@@ -261,6 +261,99 @@ RSpec.describe "bundle install with specific platforms" do
expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
end
end
+
+ it "adds and installs the ruby variant when only an incompatible platform-specific variant was locked" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.10"
+ build_gem "nokogiri", "1.18.10" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.10-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+ expect(out).to include("Installing nokogiri 1.18.10")
+ expect(the_bundle).to include_gem("nokogiri 1.18.10")
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.10)
+ nokogiri (1.18.10-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "fails at install time when only an incompatible platform-specific variant is locked" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.10"
+ build_gem "nokogiri", "1.18.10" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.10-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ expect(exitstatus).not_to eq(0)
+ expect(err).to include("nokogiri-1.18.10-x86_64-linux requires ruby version < #{Gem.ruby_version}")
+ end
+ end
end
it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do
@@ -766,10 +859,13 @@ RSpec.describe "bundle install with specific platforms" do
bundle "update --conservative nokogiri"
end
+ checksums.checksum gem_repo4, "nokogiri", "1.13.0"
+
expect(lockfile).to eq <<~L
GEM
remote: https://gem.repo4/
specs:
+ nokogiri (1.13.0)
nokogiri (1.13.0-x86_64-darwin)
sorbet-static (0.5.10601-x86_64-darwin)
@@ -785,6 +881,61 @@ RSpec.describe "bundle install with specific platforms" do
L
end
+ it "locks ruby fallback variant dependencies without adding the ruby platform" do
+ build_repo4 do
+ build_gem "native_tool", "1.0" do |s|
+ s.add_dependency "rake"
+ end
+
+ build_gem "native_tool", "1.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "rake"
+
+ build_gem "sorbet-static", "0.5.10601" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "native_tool"
+ gem "sorbet-static"
+ G
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "native_tool", "1.0"
+ c.checksum gem_repo4, "native_tool", "1.0", "x86_64-linux"
+ c.checksum gem_repo4, "rake", "1.0"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ native_tool (1.0)
+ rake
+ native_tool (1.0-x86_64-linux)
+ rake (1.0)
+ sorbet-static (0.5.10601-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ native_tool
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do
build_repo4 do
build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|
diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb
index 654ac02aa7..0a2aa8aca8 100644
--- a/spec/bundler/lock/lockfile_spec.rb
+++ b/spec/bundler/lock/lockfile_spec.rb
@@ -1324,6 +1324,7 @@ RSpec.describe "the lockfile format" do
G
checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "platform_specific", "1.0"
c.checksum gem_repo2, "platform_specific", "1.0", "universal-java-16"
end
@@ -1331,6 +1332,7 @@ RSpec.describe "the lockfile format" do
GEM
remote: https://gem.repo2/
specs:
+ platform_specific (1.0)
platform_specific (1.0-universal-java-16)
PLATFORMS
diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb
index a1d095d024..83bec610a6 100644
--- a/spec/bundler/resolver/platform_spec.rb
+++ b/spec/bundler/resolver/platform_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe "Resolving platform craziness" do
should_resolve_as %w[foo-1.0.0]
end
- it "prefers the platform specific gem to the ruby version" do
+ it "prefers the platform specific gem to the ruby version, but keeps the ruby fallback" do
@index = build_index do
gem "foo", "1.0.0"
gem "foo", "1.0.0", "x64-mingw-ucrt"
@@ -79,7 +79,7 @@ RSpec.describe "Resolving platform craziness" do
dep "foo"
platforms "x64-mingw-ucrt"
- should_resolve_as %w[foo-1.0.0-x64-mingw-ucrt]
+ should_resolve_as %w[foo-1.0.0 foo-1.0.0-x64-mingw-ucrt]
end
describe "on a linux platform" do
@@ -88,7 +88,7 @@ RSpec.describe "Resolving platform craziness" do
# Gem's platform is *-linux => gem is glibc + maybe musl compatible
# Gem's platform is *-linux-musl => gem is musl compatible but not glibc
- it "favors the platform version-specific gem on a version-specifying linux platform" do
+ it "favors the platform version-specific gem on a version-specifying linux platform, but keeps the ruby fallback" do
@index = build_index do
gem "foo", "1.0.0"
gem "foo", "1.0.0", "x86_64-linux"
@@ -97,10 +97,10 @@ RSpec.describe "Resolving platform craziness" do
dep "foo"
platforms "x86_64-linux-musl"
- should_resolve_as %w[foo-1.0.0-x86_64-linux-musl]
+ should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux-musl]
end
- it "favors the version-less gem over the version-specific gem on a gnu linux platform" do
+ it "favors the version-less gem over the version-specific gem on a gnu linux platform, but keeps the ruby fallback" do
@index = build_index do
gem "foo", "1.0.0"
gem "foo", "1.0.0", "x86_64-linux"
@@ -109,7 +109,7 @@ RSpec.describe "Resolving platform craziness" do
dep "foo"
platforms "x86_64-linux"
- should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux]
end
it "ignores the platform version-specific gem on a gnu linux platform" do
@@ -122,7 +122,7 @@ RSpec.describe "Resolving platform craziness" do
should_not_resolve
end
- it "falls back to the platform version-less gem on a linux platform with a version" do
+ it "falls back to the platform version-less gem on a linux platform with a version, but keeps the ruby fallback" do
@index = build_index do
gem "foo", "1.0.0"
gem "foo", "1.0.0", "x86_64-linux"
@@ -130,7 +130,7 @@ RSpec.describe "Resolving platform craziness" do
dep "foo"
platforms "x86_64-linux-musl"
- should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux]
end
it "falls back to the ruby platform gem on a gnu linux platform when only a version-specifying gem is available" do
diff --git a/test/digest/test_digest.rb b/test/digest/test_digest.rb
index 084eda8b1b..95cce93940 100644
--- a/test/digest/test_digest.rb
+++ b/test/digest/test_digest.rb
@@ -204,7 +204,7 @@ module TestDigest
Data1 => "352441c2",
Data2 => "171a3f5f",
}
- end
+ end if defined?(Digest::CRC32)
class TestBase < Test::Unit::TestCase
def test_base
diff --git a/test/prism/errors/rescue_modifier_argument_value_alias.txt b/test/prism/errors/rescue_modifier_argument_value_alias.txt
new file mode 100644
index 0000000000..e4d0b9dc90
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_argument_value_alias.txt
@@ -0,0 +1,3 @@
+x = 1 rescue alias a b
+ ^~~~~ unexpected an `alias` at a non-statement position
+
diff --git a/test/prism/errors/rescue_modifier_argument_value_not.txt b/test/prism/errors/rescue_modifier_argument_value_not.txt
new file mode 100644
index 0000000000..5d066eb43a
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_argument_value_not.txt
@@ -0,0 +1,4 @@
+x = 1 rescue not b
+ ^ expected a `(` after `not`
+ ^ unexpected local variable or method, expecting end-of-input
+
diff --git a/test/prism/errors/rescue_modifier_endless_method_multiple_assignment.txt b/test/prism/errors/rescue_modifier_endless_method_multiple_assignment.txt
new file mode 100644
index 0000000000..892f78cc9b
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_endless_method_multiple_assignment.txt
@@ -0,0 +1,4 @@
+def m = foo bar rescue a, b = 1
+ ^ unexpected ',', expecting end-of-input
+ ^ unexpected ',', ignoring it
+
diff --git a/test/prism/errors/rescue_modifier_multiple_assignment_logical_operator.txt b/test/prism/errors/rescue_modifier_multiple_assignment_logical_operator.txt
new file mode 100644
index 0000000000..faca9bf49b
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_multiple_assignment_logical_operator.txt
@@ -0,0 +1,3 @@
+a rescue b, c = d and e
+ ^~~ unexpected 'and', expecting end-of-input
+
diff --git a/test/prism/errors/rescue_modifier_multiple_assignment_trailing_comma.txt b/test/prism/errors/rescue_modifier_multiple_assignment_trailing_comma.txt
new file mode 100644
index 0000000000..bffd9e7280
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_multiple_assignment_trailing_comma.txt
@@ -0,0 +1,3 @@
+(a rescue b,)
+ ^~ unexpected write target
+
diff --git a/test/prism/errors/rescue_modifier_single_assignment_multiple_assignment.txt b/test/prism/errors/rescue_modifier_single_assignment_multiple_assignment.txt
new file mode 100644
index 0000000000..be666ef886
--- /dev/null
+++ b/test/prism/errors/rescue_modifier_single_assignment_multiple_assignment.txt
@@ -0,0 +1,4 @@
+z = 1 rescue a, b = 1
+ ^ unexpected ',', expecting end-of-input
+ ^ unexpected ',', ignoring it
+
diff --git a/test/prism/fixtures/rescue_modifier_assignment.txt b/test/prism/fixtures/rescue_modifier_assignment.txt
new file mode 100644
index 0000000000..4882835d77
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_assignment.txt
@@ -0,0 +1,3 @@
+x, y = 1 rescue a, b = 1
+x, y = a rescue b, c = 1
+x, y = a rescue b = c d
diff --git a/test/prism/fixtures/rescue_modifier_block_command_value.txt b/test/prism/fixtures/rescue_modifier_block_command_value.txt
new file mode 100644
index 0000000000..94091f072a
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_block_command_value.txt
@@ -0,0 +1,5 @@
+x = foo a do end rescue b
+x = foo a do end rescue b, c = 1
+x = a.foo b do end rescue c, d = 1
+x, y = foo a do end rescue b
+x += foo a do end rescue b, c = 1
diff --git a/test/prism/fixtures/rescue_modifier_command_value.txt b/test/prism/fixtures/rescue_modifier_command_value.txt
new file mode 100644
index 0000000000..7b31adcd2d
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_command_value.txt
@@ -0,0 +1,6 @@
+a rescue b = c d
+x = foo bar rescue b, c = 1
+x = foo bar rescue b = c d
+x += foo bar rescue b, c = 1
+x, y = foo bar rescue b
+x, y = foo bar rescue b, c = 1
diff --git a/test/prism/fixtures/rescue_modifier_multiple_assignment.txt b/test/prism/fixtures/rescue_modifier_multiple_assignment.txt
new file mode 100644
index 0000000000..79ca21f577
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_multiple_assignment.txt
@@ -0,0 +1,9 @@
+a rescue b, c = 1
+a rescue b, c, d = 1
+a rescue *b, c = 1
+a rescue b, *c = 1
+a rescue b, *c, d = 1
+a rescue *b = 1
+a rescue (b, c), d = 1
+a rescue b.c, d = 1
+a rescue b[0], c = 1
diff --git a/test/prism/fixtures/rescue_modifier_multiple_value.txt b/test/prism/fixtures/rescue_modifier_multiple_value.txt
new file mode 100644
index 0000000000..bac7b039a0
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_multiple_value.txt
@@ -0,0 +1,5 @@
+a rescue b = c, d
+a rescue b = c, d, e
+a rescue b = *c
+a rescue b = c, *d
+a rescue b = *c, d
diff --git a/test/prism/fixtures/rescue_modifier_statement_value.txt b/test/prism/fixtures/rescue_modifier_statement_value.txt
new file mode 100644
index 0000000000..d3951a9608
--- /dev/null
+++ b/test/prism/fixtures/rescue_modifier_statement_value.txt
@@ -0,0 +1,8 @@
+a rescue alias x y
+a rescue undef m
+a rescue END { b }
+a rescue retry
+alias x y rescue b
+undef m rescue b
+END { b } rescue b
+x = foo bar rescue retry
diff --git a/test/prism/locals_test.rb b/test/prism/locals_test.rb
index 417730a8a7..c4514edd51 100644
--- a/test/prism/locals_test.rb
+++ b/test/prism/locals_test.rb
@@ -77,6 +77,10 @@ module Prism
parts[10]
end
+ def catch_table
+ parts[12]
+ end
+
def instructions
parts[13]
end
@@ -97,6 +101,20 @@ module Prism
yield ISeq.new(opnd)
end
end
+
+ # The handler for a rescue clause is compiled into its own iseq that is
+ # reachable only through the catch table. Its local table only ever
+ # holds the implicit `$!` error variable (rescue clauses do not
+ # introduce user locals of their own -- `rescue => e` puts `e` in the
+ # enclosing scope), and prism does not model it as a scope, so we treat
+ # it as transparent and descend straight into any iseqs nested within
+ # it (e.g. a block or an `END {}` inside a rescue clause). Only
+ # `:rescue` entries are followed: `:break`/`:next`/`:redo`/`:retry`
+ # entries either have no iseq or reference one already reachable through
+ # the instructions, and `:ensure` bodies are compiled inline.
+ catch_table.each do |entry|
+ ISeq.new(entry[1]).each_child { |child| yield child } if entry[0] == :rescue && entry[1].is_a?(Array)
+ end
end
end
diff --git a/test/prism/ruby/parser_test.rb b/test/prism/ruby/parser_test.rb
index ad9fa0c92c..856ecedc1d 100644
--- a/test/prism/ruby/parser_test.rb
+++ b/test/prism/ruby/parser_test.rb
@@ -132,7 +132,6 @@ module Prism
"whitequark/lbrace_arg_after_command_args.txt",
"whitequark/multiple_pattern_matches.txt",
"whitequark/newline_in_hash_argument.txt",
- "whitequark/pattern_matching_expr_in_paren.txt",
"whitequark/pattern_matching_hash.txt",
"whitequark/ruby_bug_14690.txt",
"whitequark/ruby_bug_9669.txt",
diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb
index f74342322f..eefd5b189b 100644
--- a/test/ruby/test_proc.rb
+++ b/test/ruby/test_proc.rb
@@ -342,6 +342,25 @@ class TestProc < Test::Unit::TestCase
assert_equal(9, b)
end
+ # Not named test_curry_* so that test_curry_with_trace does not re-run it
+ # under set_trace_func (which would be needlessly slow with GC.stress).
+ def test_proc_curry_keeps_args_alive
+ # The argument array passed down by `curry` must stay alive across the
+ # inner call; otherwise GC may reclaim it while it is still read as argv
+ # and crash with "try to mark T_NONE object". See the RB_GC_GUARD in `curry`.
+ assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60)
+ begin;
+ GC.stress = true
+ l = ->(a, b, c) { a + b + c }
+ 30.times do
+ l1 = l.curry.call(1)
+ l2 = l1.curry.call(2)
+ assert_equal(6, l2.curry.call(3))
+ assert_equal(6, l1.curry.call(2, 3))
+ end
+ end;
+ end
+
def test_lambda?
l = proc {}
assert_equal(false, l.lambda?)
diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb
index df7054c101..33f13511cb 100644
--- a/test/ruby/test_yjit.rb
+++ b/test/ruby/test_yjit.rb
@@ -1041,6 +1041,55 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_bmethod_super_block_forwarding
+ # super() in a block method forwards the block handler of environment
+ # it is in. In this test, the block is in a class, which never has
+ # a block handler, so Parent#foo gets no block.
+ assert_compiles(<<~RUBY, result: nil)
+ class Parent
+ def foo = defined?(yield)
+ end
+
+ class BmethodSuper < Parent
+ define_method(:foo) { super() }
+ end
+
+ BmethodSuper.new.foo {}
+ RUBY
+
+ # For contrast, the super() here forwards the block passed to add_then_override
+ # because block with super() is rooted in add_then_override, a "def" method.
+ assert_compiles(<<~RUBY, result: [1, 2])
+ def add_then_override(object)
+ object.define_singleton_method(:then) { super() }
+ end
+
+ add_then_override(one = Object.new) { 1 }
+ add_then_override(two = Object.new) { 2 }
+ [one.then {}, two.then {}]
+ RUBY
+ end
+
+ def test_bug_22116
+ # Regression test from report which used to crash during environment escape
+ # to pass block to Hash.new
+ assert_compiles(<<~RUBY, call_threshold: 1)
+ class C
+ def foo
+ Hash.new {|h, k| h[k] = [] }
+ end
+ end
+
+ class D < C
+ define_method(:foo) do
+ super()
+ end
+ end
+
+ D.new.foo
+ RUBY
+ end
+
# Tests calling a variadic cfunc with many args
def test_build_large_struct
assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], call_threshold: 2)
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb
index a56fea6d51..198301d933 100644
--- a/test/ruby/test_zjit.rb
+++ b/test/ruby/test_zjit.rb
@@ -382,6 +382,23 @@ class TestZJIT < Test::Unit::TestCase
}, call_threshold: 14, num_profiles: 5
end
+ def test_regression_gc_stress_with_lazy_block_code
+ assert_compiles ':ok', %q{
+ def allocate_array
+ [1, 2, 3]
+ end
+
+ begin
+ GC.stress = true
+ allocate_array
+ allocate_array
+ :ok
+ ensure
+ GC.stress = false
+ end
+ }
+ end
+
def test_exit_tracing
# Smoke test: --zjit-trace-exits writes a Fuchsia trace (.fxt) file to /tmp
assert_compiles('true', <<~RUBY, extra_args: ['--zjit-trace-exits'])
diff --git a/test/test_bundled_gems.rb b/test/test_bundled_gems.rb
index 0889584185..884412b416 100644
--- a/test/test_bundled_gems.rb
+++ b/test/test_bundled_gems.rb
@@ -16,6 +16,39 @@ class TestBundlerGem < Gem::TestCase
assert_nil Gem::BUNDLED_GEMS.warning?("csv", specs: {})
end
+ def test_find_gem_plain_name
+ assert_equal ["csv", "csv", false], Gem::BUNDLED_GEMS.find_gem("csv")
+ end
+
+ def test_find_gem_returns_nil_for_non_bundled_gem
+ assert_nil Gem::BUNDLED_GEMS.find_gem("some_gem")
+ assert_nil Gem::BUNDLED_GEMS.find_gem("some/nested/gem")
+ end
+
+ def test_find_gem_exact_mapping
+ # kconv is provided by the nkf gem; the feature keeps its original name
+ assert_equal ["kconv", "nkf", false], Gem::BUNDLED_GEMS.find_gem("kconv")
+ end
+
+ def test_find_gem_dashed_name
+ # resolv/replace is provided by the resolv-replace gem
+ assert_equal ["resolv/replace", "resolv-replace", false], Gem::BUNDLED_GEMS.find_gem("resolv/replace")
+ end
+
+ def test_find_gem_subfeature
+ assert_equal ["benchmark/ips", "benchmark", true], Gem::BUNDLED_GEMS.find_gem("benchmark/ips")
+ end
+
+ def test_find_gem_strips_libdir_prefix
+ path = File.join(::RbConfig::CONFIG.fetch("rubylibdir"), "csv.rb")
+ assert_equal ["csv", "csv", false], Gem::BUNDLED_GEMS.find_gem(path)
+ end
+
+ def test_find_gem_strips_archdir_prefix
+ path = File.join(::RbConfig::CONFIG.fetch("rubyarchdir"), "syslog.so")
+ assert_equal ["syslog", "syslog", false], Gem::BUNDLED_GEMS.find_gem(path)
+ end
+
def test_no_warning_warning
assert_nil Gem::BUNDLED_GEMS.warning?("some_gem", specs: {})
assert_nil Gem::BUNDLED_GEMS.warning?("/path/to/some_gem.rb", specs: {})
diff --git a/vm.c b/vm.c
index 1d2a3eb66a..9da23c68b6 100644
--- a/vm.c
+++ b/vm.c
@@ -2849,6 +2849,27 @@ rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t
if (jit_frame->materialize_block_code) {
cfp->block_code = NULL;
}
+
+ // Materialize Ruby stack slots kept off the VM stack. On side exit,
+ // the exiting frame is already written by compile_exit_save_state()
+ // and skipped here after materialize_exit_trampoline clears its
+ // jit_return, so this restores older ZJIT frames from stack maps.
+ int32_t stack_size = (int32_t)jit_frame->stack_size;
+ if (stack_size > 0) {
+ VALUE *stack = cfp->sp - stack_size;
+ for (int32_t i = 0; i < stack_size; i++) {
+ VALUE entry = jit_frame->stack[i];
+ if (ZJIT_STACK_MAP_VREG_P(entry)) {
+ // Decode a native stack slot offset generated by ZJIT's backend.
+ // It's an offset from NATIVE_BASE_PTR, which is copied into
+ // cfp->jit_return, to the encoded stack slot.
+ stack[i] = ((VALUE *)cfp->jit_return)[-(ssize_t)ZJIT_STACK_MAP_VREG_INDEX(entry)];
+ }
+ else {
+ stack[i] = entry;
+ }
+ }
+ }
cfp->jit_return = 0;
}
if (end_cfp == cfp) break;
@@ -3641,11 +3662,15 @@ rb_execution_context_update(rb_execution_context_t *ec)
rb_control_frame_t *cfp = ec->cfp;
rb_control_frame_t *limit_cfp = (void *)(ec->vm_stack + ec->vm_stack_size);
- for (i = 0; i < (long)(sp - p); i++) {
- VALUE ref = p[i];
- VALUE update = rb_gc_location(ref);
- if (ref != update) {
- p[i] = update;
+ // ZJIT leaves uninitialized slots on the VM stack, so we cannot
+ // safely use rb_gc_location on such slots.
+ if (!rb_zjit_enabled_p) {
+ for (i = 0; i < (long)(sp - p); i++) {
+ VALUE ref = p[i];
+ VALUE update = rb_gc_location(ref);
+ if (ref != update) {
+ p[i] = update;
+ }
}
}
@@ -3653,12 +3678,15 @@ rb_execution_context_update(rb_execution_context_t *ec)
const VALUE *ep = cfp->ep;
cfp->self = rb_gc_location(cfp->self);
if (CFP_ZJIT_FRAME_P(cfp)) {
- rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)CFP_ZJIT_FRAME(cfp));
- // block_code must always be relocated. For ISEQ frames, the JIT caller
- // may have written it (gen_block_handler_specval) for passing blocks.
- // For C frames, rb_iterate0 may have written an ifunc to block_code
- // after the JIT pushed the frame. NULL is safe to pass to rb_gc_location.
- cfp->block_code = (void *)rb_gc_location((VALUE)cfp->block_code);
+ const zjit_jit_frame_t *jit_frame = CFP_ZJIT_FRAME(cfp);
+ rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)jit_frame);
+ // materialize_block_code means cfp->block_code is lazy and may
+ // still contain stale data from a previous frame. Otherwise it
+ // was initialized by ZJIT and may have been written later by
+ // vm_caller_setup_arg_block (ISEQ frames) or rb_iterate0 (C frames).
+ if (!jit_frame->materialize_block_code) {
+ cfp->block_code = (void *)rb_gc_location((VALUE)cfp->block_code);
+ }
}
else {
cfp->_iseq = (rb_iseq_t *)rb_gc_location((VALUE)cfp->_iseq);
@@ -3706,7 +3734,14 @@ rb_execution_context_mark(const rb_execution_context_t *ec)
rb_control_frame_t *limit_cfp = (void *)(ec->vm_stack + ec->vm_stack_size);
for (long i = 0; i < (long)(sp - p); i++) {
- rb_gc_mark_movable(p[i]);
+ if (rb_zjit_enabled_p) {
+ // ZJIT leaves uninitialized slots on the VM stack, so we need
+ // to mark such slots conservatively.
+ rb_gc_mark_maybe(p[i]);
+ }
+ else {
+ rb_gc_mark_movable(p[i]);
+ }
}
while (cfp != limit_cfp) {
@@ -3714,10 +3749,21 @@ rb_execution_context_mark(const rb_execution_context_t *ec)
VM_ASSERT(!!VM_ENV_FLAGS(ep, VM_ENV_FLAG_ESCAPED) == vm_ep_in_heap_p_(ec, ep));
rb_gc_mark_movable(cfp->self);
- rb_gc_mark_movable((VALUE)CFP_ISEQ(cfp));
- // Mark block_code directly (not through rb_zjit_cfp_block_code)
- // because rb_iterate0 may write a valid ifunc after JIT frame push.
- rb_gc_mark_movable((VALUE)cfp->block_code);
+ if (CFP_ZJIT_FRAME_P(cfp)) {
+ const zjit_jit_frame_t *jit_frame = CFP_ZJIT_FRAME(cfp);
+ rb_gc_mark_movable((VALUE)jit_frame->iseq);
+ // materialize_block_code means cfp->block_code is lazy and may
+ // still contain stale data from a previous frame. Otherwise it
+ // was initialized by ZJIT and may have been written later by
+ // vm_caller_setup_arg_block (ISEQ frames) or rb_iterate0 (C frames).
+ if (!jit_frame->materialize_block_code) {
+ rb_gc_mark_movable((VALUE)cfp->block_code);
+ }
+ }
+ else {
+ rb_gc_mark_movable((VALUE)cfp->_iseq);
+ rb_gc_mark_movable((VALUE)cfp->block_code);
+ }
if (VM_ENV_LOCAL_P(ep) && VM_ENV_BOXED_P(ep)) {
const rb_box_t *box = VM_ENV_BOX(ep);
diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs
index 743a10e15a..fcddb18ac7 100644
--- a/yjit/src/codegen.rs
+++ b/yjit/src/codegen.rs
@@ -6805,8 +6805,8 @@ enum SpecVal {
pub enum BlockHandler {
// send, invokesuper: blockiseq operand
BlockISeq(IseqPtr),
- // invokesuper: GET_BLOCK_HANDLER() (GET_LEP()[VM_ENV_DATA_INDEX_SPECVAL])
- LEPSpecVal,
+ // For invokesuper; equivalent to GET_BLOCK_HANDLER()
+ FindFromCurrentFrame,
// part of the allocate-free block forwarding scheme
BlockParamProxy,
// To avoid holding the block arg (e.g. proc and symbol) across C calls,
@@ -6870,9 +6870,20 @@ fn gen_push_frame(
let cfp_self = asm.lea(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF));
asm.or(cfp_self, Opnd::Imm(1))
}
- BlockHandler::LEPSpecVal => {
- let lep_opnd = gen_get_lep(jit, asm);
- asm.load(Opnd::mem(64, lep_opnd, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL))
+ BlockHandler::FindFromCurrentFrame => {
+ // Find the calling frame's block handler. Similar shape to implementation for
+ // defined?(yield). See also: vm_caller_setup_arg_block() with is_super, which
+ // ends up in VM_CF_BLOCK_HANDLER().
+ //
+ // When the calling ISEQ is rooted in a ISEQ_TYPE_METHOD, the block handler is
+ // at local_ep[VM_ENV_DATA_INDEX_SPECVAL]. Otherwise, the frame has no block
+ // handler.
+ if ISEQ_TYPE_METHOD == unsafe { rb_get_iseq_body_type(rb_get_iseq_body_local_iseq(jit.iseq)) } {
+ let lep_opnd = gen_get_lep(jit, asm);
+ asm.load(Opnd::mem(64, lep_opnd, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL))
+ } else {
+ VM_BLOCK_HANDLER_NONE.into()
+ }
}
BlockHandler::BlockParamProxy => {
let ep_opnd = gen_get_lep(jit, asm);
@@ -9880,7 +9891,7 @@ fn gen_invokesuper_specialized(
let block = if let Some(iseq) = jit.get_arg(1).as_optional_ptr() {
BlockHandler::BlockISeq(iseq)
} else {
- BlockHandler::LEPSpecVal
+ BlockHandler::FindFromCurrentFrame
};
// Fallback to dynamic dispatch if this callsite is megamorphic
diff --git a/zjit.h b/zjit.h
index ac0721d053..b4e399ba2a 100644
--- a/zjit.h
+++ b/zjit.h
@@ -9,6 +9,25 @@
# define ZJIT_STATS (USE_ZJIT && RUBY_DEBUG)
#endif
+// Stack map entries are either immediate Ruby VALUEs or tagged native-stack
+// locations. Stack maps never contain heap VALUEs, so 0x08 is available: it is
+// not Qfalse (0), and its low 3 bits are zero, so RB_SPECIAL_CONST_P is false.
+#define ZJIT_STACK_MAP_VREG_TAG 0x08
+#define ZJIT_STACK_MAP_TAG_MASK 0xff
+#define ZJIT_STACK_MAP_SHIFT 8
+
+static inline bool
+ZJIT_STACK_MAP_VREG_P(VALUE entry)
+{
+ return (entry & ZJIT_STACK_MAP_TAG_MASK) == ZJIT_STACK_MAP_VREG_TAG;
+}
+
+static inline size_t
+ZJIT_STACK_MAP_VREG_INDEX(VALUE entry)
+{
+ return entry >> ZJIT_STACK_MAP_SHIFT;
+}
+
// JITFrame is defined here as the single source of truth and imported into
// Rust via bindgen. C code reads fields directly; Rust uses an impl block.
typedef struct zjit_jit_frame {
@@ -23,6 +42,14 @@ typedef struct zjit_jit_frame {
// (which write block_code themselves), so we must restore it.
// Always false for C frames.
bool materialize_block_code;
+
+ // Number of Ruby stack slots described by stack[].
+ // rb_zjit_materialize_frames() copies them to cfp->sp - stack_size.
+ uint32_t stack_size;
+ // Flexible array of stack map entries. Each entry is either an immediate
+ // VALUE or a tagged native-stack index from cfp->jit_return for a value
+ // kept by the JIT.
+ VALUE stack[];
} zjit_jit_frame_t;
#if USE_ZJIT
diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs
index 7c2925641c..041949be81 100644
--- a/zjit/bindgen/src/main.rs
+++ b/zjit/bindgen/src/main.rs
@@ -316,6 +316,8 @@ fn main() {
.allowlist_function("rb_zjit_insn_leaf")
.allowlist_type("jit_bindgen_constants")
.allowlist_type("zjit_struct_offsets")
+ .allowlist_var("ZJIT_STACK_MAP_SHIFT")
+ .allowlist_var("ZJIT_STACK_MAP_VREG_TAG")
.allowlist_var("ZJIT_JIT_RETURN_C_FRAME")
.allowlist_function("rb_assert_holding_vm_lock")
.allowlist_function("rb_jit_shape_complex_p")
@@ -458,12 +460,25 @@ fn main() {
// Unwrap the Result and panic on failure.
.expect("Unable to generate bindings");
+ // Write to a Vec for post-processing
+ let mut bindings_string = Vec::new();
+ bindings.write(Box::new(&mut bindings_string)).expect("Couldn't write bindings!");
+
+ // Use i32 for this type since that's what the assembler APIs expect
+ const JIT_CONSTANTS_NEEDLE: &[u8] = b"pub type jit_bindgen_constants = u32;";
+ const JIT_CONSTANTS_REPLACEMENT: &[u8] = b"pub type jit_bindgen_constants = i32;";
+ // Yes, this search-and-replace could be faster, but it's a small file.
+ for line in bindings_string.as_mut_slice().split_mut(|&byte| byte == b'\n') {
+ if line == JIT_CONSTANTS_NEEDLE {
+ line.copy_from_slice(JIT_CONSTANTS_REPLACEMENT);
+ break;
+ }
+ }
+
+ // Write out to file
let mut out_path: PathBuf = src_root;
out_path.push(jit_name);
out_path.push("src");
out_path.push("cruby_bindings.inc.rs");
-
- bindings
- .write_to_file(out_path)
- .expect("Couldn't write bindings!");
+ std::fs::write(out_path, bindings_string).expect("file output failed");
}
diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs
index ff1661f6fc..87b388eefa 100644
--- a/zjit/src/backend/arm64/mod.rs
+++ b/zjit/src/backend/arm64/mod.rs
@@ -263,6 +263,9 @@ impl Assembler {
Opnd::Mem(mem) => {
if mem_disp_fits_bits(mem.disp) {
opnd
+ } else if asm.accept_scratch_reg {
+ asm.lea_into(SCRATCH1_OPND, Opnd::Mem(Mem { num_bits: 64, ..mem }));
+ Opnd::mem(mem.num_bits, SCRATCH1_OPND, 0)
} else {
let base = asm.lea(Opnd::Mem(Mem { num_bits: 64, ..mem }));
Opnd::mem(mem.num_bits, base, 0)
@@ -627,6 +630,12 @@ impl Assembler {
*opnd = split_load_operand(asm, *opnd);
asm.push_insn(insn);
},
+ Insn::Store { dest, .. } => {
+ if asm.accept_scratch_reg && matches!(dest, Opnd::Mem(_)) {
+ *dest = split_memory_address(asm, *dest);
+ }
+ asm.push_insn(insn);
+ },
Insn::Mul { left, right, .. } => {
*left = split_load_operand(asm, *left);
*right = split_load_operand(asm, *right);
@@ -1673,7 +1682,7 @@ impl Assembler {
});
trace_compile_phase("resolve_ssa", || {
- asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS);
+ asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS, total_stack_slots);
asm.resolve_ssa(&intervals, &assignments);
});
@@ -2193,6 +2202,19 @@ mod tests {
}
#[test]
+ fn test_store_with_scratch_reg_and_large_displacement() {
+ let (mut asm, mut cb, _) = setup_asm_with_scratch_reg();
+ asm.store(Opnd::mem(64, SP, -0x140), C_RET_OPND);
+
+ asm.compile_with_num_regs(&mut cb, 0);
+ assert_disasm_snapshot!(cb.disasm(), @"
+ 0x0: sub x17, x21, #0x140
+ 0x4: stur x0, [x17]
+ ");
+ assert_snapshot!(cb.hexdump(), @"b10205d1200200f8");
+ }
+
+ #[test]
#[should_panic]
fn test_store_with_invalid_scratch_reg() {
let (_, scratch_reg) = Assembler::new_with_scratch_reg();
diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs
index f394062d51..84c8c8ba0f 100644
--- a/zjit/src/backend/lir.rs
+++ b/zjit/src/backend/lir.rs
@@ -4,11 +4,10 @@ use std::mem::take;
use std::rc::Rc;
use crate::bitset::BitSet;
use crate::codegen::{local_size_and_idx_to_ep_offset, perf_symbol_range_start, perf_symbol_range_end};
-use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, vm_stack_canary, YarvInsnIdx };
+use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, VALUE, ZJIT_STACK_MAP_SHIFT, ZJIT_STACK_MAP_VREG_TAG, vm_stack_canary, YarvInsnIdx, zjit_jit_frame};
use crate::hir::{Invariant, SideExitReason};
use crate::hir;
use crate::options::{TraceExits, PerfMap, get_option};
-use crate::cruby::VALUE;
use crate::payload::{IseqVersionRef, get_or_create_iseq_payload};
use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, side_exit_counter, CompileError};
use crate::virtualmem::CodePtr;
@@ -698,6 +697,7 @@ pub enum Insn {
/// that are split from this CCall during register assignment.
end_marker: Option<PosMarkerFn>,
out: Opnd,
+ stack_map: Option<StackMap>,
},
// C function return
@@ -940,8 +940,11 @@ macro_rules! for_each_operand_impl {
visit_one!(opnd0);
visit_one!(opnd1);
}
- Insn::CCall { opnds, .. } => {
+ Insn::CCall { opnds, stack_map, .. } => {
visit_many!(opnds);
+ if let Some(StackMap { stack, .. }) = stack_map {
+ visit_many!(stack);
+ }
}
// only iterate over preserved in the const iterator
#[allow(unused_variables)]
@@ -1387,6 +1390,18 @@ impl StackState {
}
}
+/// Stack map to materialize Ruby stack slots from JIT-kept values.
+#[derive(Clone, Debug)]
+pub struct StackMap {
+ /// Ruby stack slots to reconstruct if this frame is materialized.
+ /// Each operand must be either an immediate Ruby VALUE or a VReg whose
+ /// final register/spill location will be encoded after register allocation.
+ stack: Vec<Opnd>,
+ /// Heap-allocated JITFrame whose trailing stack map storage receives the
+ /// encoded entries once this CCall's register allocation is known.
+ jit_frame: *const zjit_jit_frame,
+}
+
/// Initial capacity for asm.insns vector
const ASSEMBLER_INSNS_CAPACITY: usize = 256;
@@ -1420,6 +1435,11 @@ pub struct Assembler {
/// Current instruction index, incremented for each instruction pushed
idx: usize,
+
+ /// Pending stack map to attach to the next CCall. The register allocator
+ /// consumes this through Insn::CCall, after it knows whether each live VReg
+ /// is in a saved register or an allocator spill slot.
+ stack_map: Option<StackMap>,
}
impl Assembler
@@ -1435,6 +1455,7 @@ impl Assembler
current_block_id: BlockId(0),
num_vregs: 0,
idx: 0,
+ stack_map: None,
}
}
@@ -2103,6 +2124,7 @@ impl Assembler
intervals: &[Interval],
assignments: &[Option<Allocation>],
regs: &[Reg],
+ total_stack_slots: usize,
) {
use crate::backend::parcopy;
use crate::backend::current::{C_RET_OPND, SCRATCH_REG, ALLOC_REGS};
@@ -2116,7 +2138,7 @@ impl Assembler
let mut new_ids = Vec::with_capacity(old_ids.len());
for (insn, insn_id) in old_insns.into_iter().zip(old_ids.into_iter()) {
- if let Insn::CCall { opnds, out, start_marker, end_marker, fptr } = insn {
+ if let Insn::CCall { opnds, out, start_marker, end_marker, fptr, stack_map } = insn {
let insn_number = insn_id.map(|id| id.0).unwrap_or(0);
// Do we have a case where a ccall is emitted, but nobody
// uses the result?
@@ -2126,15 +2148,29 @@ impl Assembler
.end
.is_some_and(|end| end > insn_number);
+ // Build a set of VRegIds that can be referenced by JITFrame for materializing the VM stack
+ let stack_vreg_ids: HashSet<usize> = if let Some(StackMap { stack, .. }) = &stack_map {
+ stack.iter().filter_map(|opnd| match opnd {
+ Opnd::VReg { idx: VRegId(vreg_id), .. } => Some(*vreg_id),
+ _ => None,
+ }).collect()
+ } else {
+ HashSet::default()
+ };
+
// Find survivors: intervals that survive this Call instruction
// We need to preserve the "surviving" registers past the ccall,
// so we're going to push them all on the stack, then pop
// after we make the ccall
let survivors: Vec<usize> = intervals.iter()
.filter(|interval| {
- interval.has_bounds()
- && interval.survives(insn_number)
- && assignments[interval.id].and_then(|alloc| alloc.alloc_pool_index(ALLOC_REGS.len())).is_some()
+ // We need to spill register intervals on this CCall in two cases:
+ // 1) The VReg is referenced in an instruction after the CCall
+ let survives_call = interval.has_bounds() && interval.survives(insn_number);
+ // 2) The VReg is referenced by the stack map for the CCall
+ let stack_map_reg = stack_vreg_ids.contains(&interval.id);
+ let is_register = assignments[interval.id].and_then(|alloc| alloc.alloc_pool_index(ALLOC_REGS.len())).is_some();
+ is_register && (survives_call || stack_map_reg)
})
.map(|interval| interval.id)
.collect();
@@ -2156,6 +2192,59 @@ impl Assembler
}
new_ids.push(None);
}
+
+ if let Some(StackMap { stack, jit_frame }) = stack_map {
+ assert_eq!(unsafe { (*jit_frame).stack_size } as usize, stack.len());
+ for (idx, stack_opnd) in stack.iter().enumerate() {
+ let entry = match stack_opnd {
+ Opnd::UImm(value) => {
+ let value = VALUE(*value as usize);
+ // TODO: Investigate using a constant pool to track any value reference in the stack map
+ assert!(value.special_const_p(), "StackMap should only materialize immediate VALUEs, but got: {value:?}");
+ value
+ }
+ Opnd::VReg { idx: VRegId(vreg_id), .. } => {
+ // Calculate the offset from NATIVE_BASE_PTR to the stack slot for this VReg.
+ let vreg_stack_index = match assignments[*vreg_id].expect("StackMap VReg should have an allocation") {
+ Allocation::Reg(_) | Allocation::Fixed(_) => {
+ let position = survivors.iter().position(|&survivor_id| survivor_id == *vreg_id).unwrap();
+ // See Assembler::alloc_stack's native stack diagram. rb_zjit_materialize_frames()
+ // reads vreg_stack_index as cfp->jit_return[-vreg_stack_index].
+ // For both arches, FrameSetup may add one native stack slot for
+ // alignment before these CPushPairs.
+ // TODO: Centralize stack slot offset calculation in StackState.
+ let frame_alignment_slots = if total_stack_slots % 2 == 1 {
+ 1
+ } else {
+ 0
+ };
+ (total_stack_slots + frame_alignment_slots)
+ .checked_add(1)
+ .expect("StackMap requires a JITFrame slot")
+ + position
+ }
+ Allocation::Stack(stack_idx) => {
+ // StackState places allocator spills at:
+ // cfp->jit_return[-(self.stack_base_idx + stack_idx + 1)]
+ // so encode the matching materializer index.
+ self.stack_base_idx
+ .checked_add(1)
+ .expect("StackMap requires a JITFrame slot")
+ + stack_idx
+ }
+ };
+
+ // Encode the offset as a shifted-and-tagged integer.
+ let encoded = (vreg_stack_index << ZJIT_STACK_MAP_SHIFT) | ZJIT_STACK_MAP_VREG_TAG as usize;
+ debug_assert!(!VALUE(encoded).special_const_p(), "encoded StackMap VReg should not look like an immediate VALUE");
+ VALUE(encoded)
+ }
+ _ => unreachable!("unexpected operand in StackMap: {stack_opnd:?}"),
+ };
+ unsafe { (*jit_frame.cast_mut()).stack.as_mut_ptr().add(idx).write(entry); }
+ }
+ }
+
// Extract arguments from CCall, clear opnds
assert!(opnds.len() <= regs.len());
@@ -2198,7 +2287,8 @@ impl Assembler
// be empty now
start_marker: None,
end_marker: None,
- fptr
+ fptr,
+ stack_map: None,
});
new_ids.push(insn_id);
@@ -3228,7 +3318,8 @@ impl Assembler {
let canary_opnd = self.set_stack_canary();
let out = self.new_vreg(Opnd::match_num_bits(&opnds));
let fptr = Opnd::const_ptr(fptr);
- self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out });
+ let stack_map = self.stack_map.take();
+ self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out, stack_map });
self.clear_stack_canary(canary_opnd);
out
}
@@ -3237,14 +3328,16 @@ impl Assembler {
/// new vreg for the result.
pub fn ccall_into(&mut self, out: Opnd, fptr: *const u8, opnds: Vec<Opnd>) {
let fptr = Opnd::const_ptr(fptr);
- self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out });
+ let stack_map = self.stack_map.take();
+ self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out, stack_map });
}
/// Call a C function stored in a register
pub fn ccall_reg(&mut self, fptr: Opnd, num_bits: u8) -> Opnd {
assert!(matches!(fptr, Opnd::Reg(_)), "ccall_reg must be called with Opnd::Reg: {fptr:?}");
let out = self.new_vreg(num_bits);
- self.push_insn(Insn::CCall { fptr, opnds: vec![], start_marker: None, end_marker: None, out });
+ let stack_map = self.stack_map.take();
+ self.push_insn(Insn::CCall { fptr, opnds: vec![], start_marker: None, end_marker: None, out, stack_map });
out
}
@@ -3258,12 +3351,14 @@ impl Assembler {
end_marker: impl Fn(CodePtr, &CodeBlock) + 'static,
) -> Opnd {
let out = self.new_vreg(Opnd::match_num_bits(&opnds));
+ let stack_map = self.stack_map.take();
self.push_insn(Insn::CCall {
fptr: Opnd::const_ptr(fptr),
opnds,
start_marker: Some(Rc::new(start_marker)),
end_marker: Some(Rc::new(end_marker)),
out,
+ stack_map,
});
out
}
@@ -3492,6 +3587,15 @@ impl Assembler {
out
}
+ /// Attach a stack map to the next CCall emitted by this Assembler.
+ /// The map is queued here because gen_stack_map() runs before the CCall is
+ /// emitted, but the map is filled only after register allocation assigns
+ /// locations to the VRegs it references.
+ pub fn stack_map(&mut self, stack: Vec<Opnd>, jit_frame: *const zjit_jit_frame) {
+ assert!(self.stack_map.is_none());
+ self.stack_map = Some(StackMap { stack, jit_frame });
+ }
+
pub fn store(&mut self, dest: Opnd, src: Opnd) {
assert!(!matches!(dest, Opnd::VReg { .. }), "Destination of store must not be Opnd::VReg, got: {dest:?}");
self.push_insn(Insn::Store { dest, src });
@@ -4405,6 +4509,7 @@ mod tests {
start_marker: None,
end_marker: None,
out: v3,
+ stack_map: None,
});
// v4 = Add(v3, v1)
@@ -4436,7 +4541,7 @@ mod tests {
"v1 should be in a register");
// Run the pipeline: handle_caller_saved_regs then resolve_ssa
- asm.handle_caller_saved_regs(&intervals, &assignments, regs);
+ asm.handle_caller_saved_regs(&intervals, &assignments, regs, 0);
asm.resolve_ssa(&intervals, &assignments);
let insns = &asm.basic_blocks[b1.0].insns;
diff --git a/zjit/src/backend/tests.rs b/zjit/src/backend/tests.rs
index 7174ac4c80..e38dc4707f 100644
--- a/zjit/src/backend/tests.rs
+++ b/zjit/src/backend/tests.rs
@@ -182,9 +182,9 @@ fn test_jcc_ptr()
let (mut asm, mut cb) = setup_asm();
let side_exit = Target::CodePtr(cb.get_write_ptr().add_bytes(4));
- let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK as i32));
+ let not_mask = asm.not(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_MASK));
asm.test(
- Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32),
+ Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG),
not_mask,
);
asm.push_insn(Insn::Jnz(side_exit));
diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs
index d3bf847ab2..0f4acf1cef 100644
--- a/zjit/src/backend/x86_64/mod.rs
+++ b/zjit/src/backend/x86_64/mod.rs
@@ -1195,7 +1195,7 @@ impl Assembler {
});
trace_compile_phase("resolve_ssa", || {
- asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS);
+ asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS, total_stack_slots);
asm.resolve_ssa(&intervals, &assignments);
});
diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs
index c53e0a0f9b..bb21c3dda2 100644
--- a/zjit/src/codegen.rs
+++ b/zjit/src/codegen.rs
@@ -626,9 +626,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
Insn::ObjectAlloc { val, state } => gen_object_alloc(jit, asm, opnd!(val), &function.frame_state(*state)),
&Insn::ObjectAllocClass { class, state } => gen_object_alloc_class(asm, class, &function.frame_state(state)),
Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)),
- // concatstrings shouldn't have 0 strings
- // If it happens we abort the compilation for now
- Insn::StringConcat { strings, state, .. } if strings.is_empty() => return Err(*state),
Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, opnds!(strings), &function.frame_state(*state)),
&Insn::StringGetbyte { string, index } => gen_string_getbyte(asm, opnd!(string), opnd!(index)),
Insn::StringSetbyteFixnum { string, index, value } => gen_string_setbyte_fixnum(asm, opnd!(string), opnd!(index), opnd!(value)),
@@ -659,9 +656,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
Insn::InvokeBlockIfunc { cd, block_handler, args, state, .. } => gen_invokeblock_ifunc(jit, asm, *cd, opnd!(block_handler), opnds!(args), &function.frame_state(*state)),
Insn::InvokeProc { recv, args, state, kw_splat } => gen_invokeproc(jit, asm, opnd!(recv), opnds!(args), *kw_splat, &function.frame_state(*state)),
- // Ensure we have enough room fit ec, self, and arguments
- // TODO remove this check when we have stack args (we can use Time.new to test it)
- Insn::InvokeBuiltin { bf, state, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return Err(*state),
Insn::InvokeBuiltin { bf, leaf, args, state, .. } => gen_invokebuiltin(jit, asm, &function.frame_state(*state), bf, *leaf, opnds!(args)),
&Insn::EntryPoint { jit_entry_idx } => no_output!(gen_entry_point(jit, asm, jit_entry_idx)),
Insn::Return { val } => no_output!(gen_return(asm, opnd!(val))),
@@ -722,12 +716,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::GuardGreaterEq { left, right, state, .. } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)),
Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))),
Insn::CCall { cfunc, recv, args, name, owner: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)),
- // Give up CCallWithFrame for 7+ args since asm.ccall() supports at most 6 args (recv + args).
- // We're currently emitting a CCallWithFrame for `super` in to a cfunction.
- // We can't lower to `gen_send_without_block` because the
- // source opcode isn't necessarily `opt_send_without_block`
- // and so the interpreter stack layout may be incompatible.
- Insn::CCallWithFrame { cd, state, args, block, .. } if args.len() + 1 > C_ARG_OPNDS.len() => return Err(*state),
Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, block, .. } =>
gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)),
Insn::CCallVariadic { cfunc, recv, name, args, cme, state, block, return_type: _, elidable: _ } => {
@@ -770,7 +758,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
Insn::LoadEC => gen_load_ec(),
Insn::LoadSP => gen_load_sp(),
&Insn::GetEP { level } => gen_get_ep(asm, level),
- Insn::LoadSelf => gen_load_self(),
+ Insn::LoadSelf => gen_load_self(asm),
&Insn::LoadField { recv, id, offset, return_type } => gen_load_field(asm, opnd!(recv), id, offset, return_type),
&Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), function.type_of(val))),
&Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))),
@@ -967,6 +955,7 @@ fn gen_fixnum_bit_check(asm: &mut Assembler, val: Opnd, index: u8) -> Opnd {
}
fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf: &rb_builtin_function, leaf: bool, args: Vec<Opnd>) -> lir::Opnd {
+ // +2 for ec, self
assert!(bf.argc + 2 <= C_ARG_OPNDS.len() as i32,
"gen_invokebuiltin should not be called for builtin function {} with too many arguments: {}",
unsafe { std::ffi::CStr::from_ptr(bf.name).to_str().unwrap() },
@@ -1052,11 +1041,14 @@ fn gen_ccall_with_frame(
gen_stack_overflow_check(jit, asm, state, state.stack_size());
let args_with_recv_len = args.len() + 1;
+ if args_with_recv_len > C_ARG_OPNDS.len() {
+ unimplemented!("Passing C call arguments on the stack");
+ }
let caller_stack_size = state.stack().len() - args_with_recv_len;
// Can't use gen_prepare_non_leaf_call() because we need to adjust the SP
// to account for the receiver and arguments (and block arguments if any)
- gen_save_pc_for_gc(asm, state);
+ gen_save_pc_for_gc(asm, state, 0);
gen_save_sp(asm, caller_stack_size);
gen_spill_stack(jit, asm, state);
gen_spill_locals(jit, asm, state);
@@ -1089,7 +1081,7 @@ fn gen_ccall_with_frame(
asm_comment!(asm, "switch to new CFP");
let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, new_cfp);
- asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
let mut cfunc_args = vec![recv];
cfunc_args.extend(args);
@@ -1099,7 +1091,7 @@ fn gen_ccall_with_frame(
asm_comment!(asm, "pop C frame");
let new_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, new_cfp);
- asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
asm_comment!(asm, "restore SP register for the caller");
let new_sp = asm.sub(SP, sp_offset.into());
@@ -1150,7 +1142,7 @@ fn gen_ccall_variadic(
// Can't use gen_prepare_non_leaf_call() because we need to adjust the SP
// to account for the receiver and arguments (like gen_ccall_with_frame does)
- gen_save_pc_for_gc(asm, state);
+ gen_save_pc_for_gc(asm, state, 0);
gen_save_sp(asm, caller_stack_size);
gen_spill_stack(jit, asm, state);
gen_spill_locals(jit, asm, state);
@@ -1178,7 +1170,7 @@ fn gen_ccall_variadic(
asm_comment!(asm, "switch to new CFP");
let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, new_cfp);
- asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
let argv_ptr = gen_push_opnds(jit, asm, &args);
asm.count_call_to(&name.contents_lossy());
@@ -1187,7 +1179,7 @@ fn gen_ccall_variadic(
asm_comment!(asm, "pop C frame");
let new_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, new_cfp);
- asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
asm_comment!(asm, "restore SP register for the caller");
let new_sp = asm.sub(SP, sp_offset.into());
@@ -1312,7 +1304,7 @@ fn gen_check_interrupts(jit: &mut JITState, asm: &mut Assembler, state: &FrameSt
asm_comment!(asm, "RUBY_VM_CHECK_INTS(ec)");
// Not checking interrupt_mask since it's zero outside finalize_deferred_heap_pages,
// signal_exec, or rb_postponed_job_flush.
- let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG as i32));
+ let interrupt_flag = asm.load(Opnd::mem(32, EC, RUBY_OFFSET_EC_INTERRUPT_FLAG));
asm.test(interrupt_flag, interrupt_flag);
asm.jnz(jit, side_exit(jit, state, SideExitReason::Interrupt));
}
@@ -1382,8 +1374,8 @@ fn gen_load_sp() -> Opnd {
SP
}
-fn gen_load_self() -> Opnd {
- Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF)
+fn gen_load_self(asm: &mut Assembler) -> Opnd {
+ asm.load(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF))
}
fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, return_type: Type) -> Opnd {
@@ -1491,7 +1483,7 @@ fn gen_send(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
unsafe extern "C" {
fn rb_vm_send(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -1514,7 +1506,7 @@ fn gen_send_forward(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
unsafe extern "C" {
@@ -1537,7 +1529,7 @@ fn gen_send_without_block(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
unsafe extern "C" {
fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE;
@@ -1568,8 +1560,9 @@ fn gen_push_inline_frame(
// Save cfp->pc and cfp->sp for the caller frame.
// Cannot use gen_prepare_non_leaf_call because we need special SP math.
- gen_save_pc_for_gc(asm, state);
- gen_save_sp(asm, state.stack().len() - args.len() - 1); // -1 for receiver
+ let stack_size = state.stack().len() - args.len() - 1; // -1 for receiver
+ gen_save_pc_for_gc(asm, state, 0);
+ gen_save_sp(asm, stack_size);
gen_spill_locals(jit, asm, state);
gen_spill_stack(jit, asm, state);
@@ -1636,7 +1629,7 @@ fn gen_push_inline_frame(
}
let callee_depth = state.depth + 1;
let callee_entry_pc = unsafe { rb_iseq_pc_at_idx(iseq, 0) };
- let callee_entry_frame = JITFrame::new_iseq(callee_entry_pc, iseq);
+ let callee_entry_frame = JITFrame::new_iseq(callee_entry_pc, iseq, 0);
asm_comment!(asm, "install entry JITFrame for inlined callee");
asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, jit_frame_slot_offset(callee_depth)), Opnd::const_ptr(callee_entry_frame));
let callee_jit_return = cfp_jit_return_for_depth(asm, callee_depth);
@@ -1704,11 +1697,12 @@ fn gen_send_iseq_direct(
// Save cfp->pc and cfp->sp for the caller frame
// Can't use gen_prepare_non_leaf_call because we need special SP math.
- gen_save_pc_for_gc(asm, state);
- gen_save_sp(asm, state.stack().len() - args.len() - 1); // -1 for receiver
+ let stack_size = state.stack().len() - args.len() - 1; // -1 for receiver
+ let jit_frame = gen_save_pc_for_gc(asm, state, stack_size);
+ gen_save_sp(asm, stack_size);
gen_spill_locals(jit, asm, state);
- gen_spill_stack(jit, asm, state);
+ gen_stack_map(jit, asm, state, stack_size, jit_frame);
// This mirrors vm_caller_setup_arg_block() in for the `blockiseq != NULL` case.
// The HIR specialization guards ensure we will only reach here for literal blocks,
@@ -1767,7 +1761,7 @@ fn gen_send_iseq_direct(
asm_comment!(asm, "switch to new CFP");
let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, new_cfp);
- asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
let params = unsafe { iseq.params() };
@@ -1843,7 +1837,7 @@ fn gen_invokeblock(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call invokeblock");
unsafe extern "C" {
@@ -1868,7 +1862,7 @@ fn gen_invokeblock_ifunc(
) -> lir::Opnd {
let _ = cd; // cd is not needed for the direct call
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
// Push args to memory so we can pass argv pointer
let argv_ptr = gen_push_opnds(jit, asm, &args);
@@ -1898,7 +1892,7 @@ fn gen_invokeproc(
kw_splat: bool,
state: &FrameState,
) -> lir::Opnd {
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call invokeproc");
@@ -1927,7 +1921,7 @@ fn gen_invokesuper(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call super with dynamic dispatch");
unsafe extern "C" {
fn rb_vm_invokesuper(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -1950,7 +1944,7 @@ fn gen_invokesuperforward(
) -> lir::Opnd {
gen_incr_send_fallback_counter(asm, reason);
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
asm_comment!(asm, "call super with dynamic dispatch (forwarding)");
unsafe extern "C" {
fn rb_vm_invokesuperforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -2072,7 +2066,7 @@ fn gen_opt_newarray_hash(
state: &FrameState,
) -> lir::Opnd {
// `Array#hash` will hash the elements of the array.
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
let array_len: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long");
@@ -2098,7 +2092,7 @@ fn gen_array_max(
elements: Vec<Opnd>,
state: &FrameState,
) -> lir::Opnd {
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
let array_len: u32 = elements.len().try_into().expect("Unable to fit length of elements into u32");
@@ -2124,7 +2118,7 @@ fn gen_array_min(
elements: Vec<Opnd>,
state: &FrameState,
) -> lir::Opnd {
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
let array_len: u32 = elements.len().try_into().expect("Unable to fit length of elements into u32");
@@ -2150,7 +2144,7 @@ fn gen_array_include(
target: Opnd,
state: &FrameState,
) -> lir::Opnd {
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
let array_len: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long");
@@ -2178,7 +2172,7 @@ fn gen_array_pack_buffer(
buffer: Option<Opnd>,
state: &FrameState,
) -> lir::Opnd {
- gen_prepare_non_leaf_call(jit, asm, state);
+ gen_prepare_fallback_call(jit, asm, state);
let array_len: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long");
@@ -2365,7 +2359,7 @@ fn gen_entry_point(jit: &mut JITState, asm: &mut Assembler, jit_entry_idx: Optio
// Publish a valid entry JITFrame before setting cfp->jit_return. The entry point is
// always the top-level frame (depth 0). Inlined frames get their own deeper
// slots in gen_push_lightweight_frame().
- let jit_frame = JITFrame::new_iseq(entry_pc(jit.iseq(), jit_entry_idx), jit.iseq());
+ let jit_frame = JITFrame::new_iseq(entry_pc(jit.iseq(), jit_entry_idx), jit.iseq(), 0);
asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), Opnd::const_ptr(jit_frame));
asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), NATIVE_BASE_PTR);
}
@@ -2377,7 +2371,7 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) {
asm_comment!(asm, "pop stack frame");
let incr_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
asm.mov(CFP, incr_cfp);
- asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
+ asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
// Order here is important. Because we're about to tear down the frame,
// we need to load the return value, which might be part of the frame.
@@ -2891,13 +2885,13 @@ fn cfp_jit_return_for_depth(asm: &mut Assembler, depth: InlineDepth) -> Opnd {
/// Save only the PC to CFP. Use this when you need to call gen_save_sp()
/// immediately after with a custom stack size (e.g., gen_ccall_with_frame
/// adjusts SP to exclude receiver and arguments).
-fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState) {
+fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState, stack_map_size: usize) -> *const zjit_jit_frame {
let opcode: usize = state.get_opcode().try_into().unwrap();
let next_pc: *const VALUE = unsafe { state.pc.offset(insn_len(opcode) as isize) };
gen_incr_counter(asm, Counter::vm_write_jit_frame_count);
asm_comment!(asm, "save JITFrame to CFP");
- let jit_frame = JITFrame::new_iseq(next_pc, state.iseq);
+ let jit_frame = JITFrame::new_iseq(next_pc, state.iseq, stack_map_size);
asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, jit_frame_slot_offset(state.depth)), Opnd::const_ptr(jit_frame));
// CFP_PC for a live JIT frame routes through the JITFrame on the native
@@ -2907,6 +2901,7 @@ fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState) {
// jit_frame->pc into cfp->pc and cleared cfp->jit_return: the JIT keeps
// running, lands on this routine again, and the poison would replace
// the valid materialized pc behind the GC's back.
+ jit_frame
}
/// Save the current PC on the CFP as a preparation for calling a C function
@@ -2917,12 +2912,13 @@ fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState) {
/// because the backend spills all live registers onto the C stack on CCall.
/// However, to avoid marking uninitialized stack slots, this also updates SP,
/// which may have cfp->sp for a past frame or a past non-leaf call.
-fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool) {
- gen_save_pc_for_gc(asm, state);
+fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool, stack_map_size: usize) -> *const zjit_jit_frame {
+ let jit_frame = gen_save_pc_for_gc(asm, state, stack_map_size);
gen_save_sp(asm, state.stack_size());
if leaf {
asm.expect_leaf_ccall(state.stack_size());
}
+ jit_frame
}
fn gen_prepare_leaf_call_with_gc(asm: &mut Assembler, state: &FrameState) {
@@ -2939,7 +2935,7 @@ fn gen_prepare_leaf_call_with_gc(asm: &mut Assembler, state: &FrameState) {
// We use state.without_stack() to pass stack_size=0 to gen_save_sp() because we don't write
// VM stack slots on leaf calls, which leaves those stack slots uninitialized. ZJIT keeps
// live objects on the C stack, so they are protected from GC properly.
- gen_prepare_call_with_gc(asm, &state.without_stack(), true);
+ gen_prepare_call_with_gc(asm, &state.without_stack(), true, 0);
}
/// Save the current SP on the CFP
@@ -2976,17 +2972,43 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, state: &FrameState) {
}
}
+/// Prepare for VM fallback helpers that read arguments from the VM stack.
+///
+/// Direct JIT-to-JIT calls keep cfp->sp lazy, so this must publish SP before
+/// writing stack slots. Otherwise spilling the stack can overwrite frame
+/// metadata below the real VM-stack base.
+fn gen_prepare_fallback_call(jit: &JITState, asm: &mut Assembler, state: &FrameState) {
+ gen_save_pc_for_gc(asm, state, 0);
+ gen_save_sp(asm, state.stack_size());
+ gen_spill_locals(jit, asm, state);
+ gen_spill_stack(jit, asm, state);
+}
+
+/// Record the Ruby stack values needed to materialize this frame after the next
+/// non-leaf C call. The actual JITFrame entries are encoded by the register
+/// allocator, where VReg locations on the native stack are known.
+fn gen_stack_map(jit: &JITState, asm: &mut Assembler, state: &FrameState, stack_size: usize, jit_frame: *const zjit_jit_frame) {
+ let mut stack = Vec::new();
+ for &insn_id in state.stack().take(stack_size) {
+ let opnd = jit.get_opnd(insn_id);
+ // JITFrame only supports materializing Opnd::Value or Opnd::VReg out of the frame
+ assert!(matches!(opnd, Opnd::Value(_) | Opnd::VReg { .. }), "FrameState should only reference Opnd::Value or Opnd::VReg, but got: {opnd:?}");
+ stack.push(opnd);
+ }
+ asm.stack_map(stack, jit_frame);
+}
+
/// Prepare for calling a C function that may call an arbitrary method.
/// Use gen_prepare_leaf_call_with_gc() if the method is leaf but allocates objects.
fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, state: &FrameState) {
// TODO: Lazily materialize caller frames when needed
// Save PC for backtraces and allocation tracing
// and SP to avoid marking uninitialized stack slots
- gen_prepare_call_with_gc(asm, state, false);
+ let jit_frame = gen_prepare_call_with_gc(asm, state, false, state.stack_size());
// Spill the virtual stack in case it raises an exception
// and the interpreter uses the stack for handling the exception
- gen_spill_stack(jit, asm, state);
+ gen_stack_map(jit, asm, state, state.stack_size(), jit_frame);
// Spill locals in case the method looks at caller Bindings
gen_spill_locals(jit, asm, state);
@@ -3436,6 +3458,20 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result<CodeP
asm.new_block_without_id("gen_function_stub");
asm_comment!(asm, "Stub: {}", iseq_get_location(iseq_call.iseq.get(), 0));
+ // If the stubbed ISEQ fails to compile, function_stub_hit exits to the
+ // interpreter with this callee frame. Direct JIT-to-JIT calls pass arguments
+ // in C argument registers, so spill the packed argument locals first. The
+ // fallback path will reshape these around any optional positional gaps.
+ let argc = iseq_call.argc.to_usize();
+ assert!(argc < C_ARG_OPNDS.len(), "SendDirect must fit receiver plus arguments in C argument registers");
+ let local_size = unsafe { get_iseq_body_local_table_size(iseq_call.iseq.get()) }.to_usize();
+ for arg_idx in 0..argc {
+ asm.store(
+ Opnd::mem(64, SP, -local_size_and_idx_to_bp_offset(local_size, arg_idx) * SIZEOF_VALUE_I32),
+ C_ARG_OPNDS[arg_idx + 1],
+ );
+ }
+
// Call function_stub_hit using the shared trampoline. See `gen_function_stub_hit_trampoline`.
// Use load_into instead of mov, which is split on arm64, to avoid clobbering ALLOC_REGS.
asm.load_into(scratch_reg, Opnd::const_ptr(Rc::into_raw(iseq_call)));
@@ -3541,8 +3577,10 @@ pub fn gen_materialize_exit_trampoline(cb: &mut CodeBlock, exit_trampoline: Code
let mut asm = Assembler::new();
asm.new_block_without_id("materialize_exit_trampoline");
- asm_comment!(asm, "materialize ZJIT frames");
+ asm_comment!(asm, "clear JITFrame materialized by exit code");
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), 0.into());
+
+ asm_comment!(asm, "materialize ZJIT frames");
asm_ccall!(asm, rb_zjit_materialize_frames, EC, CFP);
asm.jmp(Target::CodePtr(exit_trampoline));
diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs
index b052147add..99d5f90516 100644
--- a/zjit/src/codegen_tests.rs
+++ b/zjit/src/codegen_tests.rs
@@ -1294,6 +1294,69 @@ fn test_invokesuper_to_cfunc_with_too_many_args_exits() {
"#), @"[1, 2, 3, 4, 5, 6]");
}
+// Repro for the production "Failed to get_opnd(vN)" panic
+// (PriceRs::PricingService#build_rust_adjustment_from_row, introduced by #17186).
+//
+// A regular send to a C method with 7 fixed args is reduced to a CCallWithFrame
+// with recv + 7 = 8 operands, which exceeds C_ARG_OPNDS.len() (6). gen_insn bails
+// with `return Err(*state)`; the caller emits a side exit and `break`s out of the
+// block. But the call's *result* is stored in a local and used in a *later* basic
+// block (the `if` arm here). Because codegen bailed before assigning a LIR operand
+// to the result, compiling that later block calls get_opnd(result) on a None entry
+// and panics. The existing `test_invokesuper_to_cfunc_with_too_many_args_exits` does
+// not catch this because there the call result is the method's tail value and is not
+// referenced past the bailed block.
+//
+// NOTE: This currently ABORTS with `Failed to get_opnd(vN)` (the bug). The snapshot
+// below is the expected behavior once the backend exits cleanly: `flag` is true so
+// `test` returns the cfunc's result, the array [1, 2, 3, 4, 5, 6, 7].
+#[test]
+fn test_ccall_with_frame_too_many_args_result_used_in_later_block() {
+ unsafe extern "C" fn test_seven_args(
+ _self: VALUE,
+ a: VALUE,
+ b: VALUE,
+ c: VALUE,
+ d: VALUE,
+ e: VALUE,
+ f: VALUE,
+ g: VALUE,
+ ) -> VALUE {
+ unsafe { rb_ary_new_from_args(7, a, b, c, d, e, f, g) }
+ }
+
+ with_rubyvm(|| {
+ let klass = define_class("ZJITSevenArgs", unsafe { rb_cObject });
+ unsafe {
+ rb_define_method(
+ klass,
+ c"seven".as_ptr(),
+ Some(std::mem::transmute::<
+ unsafe extern "C" fn(VALUE, VALUE, VALUE, VALUE, VALUE, VALUE, VALUE, VALUE) -> VALUE,
+ unsafe extern "C" fn(VALUE) -> VALUE,
+ >(test_seven_args)),
+ 7,
+ );
+ }
+ });
+
+ assert_snapshot!(assert_compiles_allowing_exits(r#"
+ def test(obj, flag)
+ priceable = obj.seven(1, 2, 3, 4, 5, 6, 7)
+ if flag
+ priceable
+ else
+ nil
+ end
+ end
+
+ obj = ZJITSevenArgs.new
+ test(obj, true) # profile receiver class
+ test(obj, true) # compile -> currently panics: Failed to get_opnd(vN)
+ test(obj, true)
+ "#), @"[1, 2, 3, 4, 5, 6, 7]");
+}
+
#[test]
fn test_string_new_preserves_string_arg() {
assert_snapshot!(inspect(r#"
diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs
index 75535272af..261cdf2c51 100644
--- a/zjit/src/cruby_bindings.inc.rs
+++ b/zjit/src/cruby_bindings.inc.rs
@@ -234,6 +234,8 @@ pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1;
pub const VM_ENV_DATA_INDEX_FLAGS: u32 = 0;
pub const VM_BLOCK_HANDLER_NONE: u32 = 0;
pub const SHAPE_ID_NUM_BITS: u32 = 32;
+pub const ZJIT_STACK_MAP_VREG_TAG: u32 = 8;
+pub const ZJIT_STACK_MAP_SHIFT: u32 = 8;
pub const ZJIT_JIT_RETURN_C_FRAME: u32 = 1;
pub type rb_alloc_func_t = ::std::option::Option<unsafe extern "C" fn(klass: VALUE) -> VALUE>;
pub const RUBY_Qfalse: ruby_special_consts = 0;
@@ -1921,11 +1923,12 @@ pub const DEFINED_FUNC: defined_type = 16;
pub const DEFINED_CONST_FROM: defined_type = 17;
pub type defined_type = u32;
#[repr(C)]
-#[derive(Debug, Copy, Clone)]
pub struct zjit_jit_frame {
pub pc: *const VALUE,
pub iseq: *const rb_iseq_t,
pub materialize_block_code: bool,
+ pub stack_size: u32,
+ pub stack: __IncompleteArrayField<VALUE>,
}
pub const ISEQ_BODY_OFFSET_PARAM: zjit_struct_offsets = 16;
pub type zjit_struct_offsets = u32;
@@ -1940,7 +1943,7 @@ pub const RUBY_OFFSET_EC_INTERRUPT_FLAG: jit_bindgen_constants = 32;
pub const RUBY_OFFSET_EC_INTERRUPT_MASK: jit_bindgen_constants = 36;
pub const RUBY_OFFSET_EC_THREAD_PTR: jit_bindgen_constants = 48;
pub const RUBY_OFFSET_EC_RACTOR_ID: jit_bindgen_constants = 64;
-pub type jit_bindgen_constants = u32;
+pub type jit_bindgen_constants = i32;
pub const rb_invalid_shape_id: shape_id_t = 524287;
pub type rb_iseq_param_keyword_struct =
rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword;
diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs
index b8d314b858..68c0ea4d90 100644
--- a/zjit/src/cruby_methods.rs
+++ b/zjit/src/cruby_methods.rs
@@ -321,13 +321,13 @@ fn inline_thread_current(fun: &mut hir::Function, block: hir::BlockId, _recv: hi
let thread_ptr = fun.push_insn(block, hir::Insn::LoadField {
recv: ec,
id: FieldName::thread_ptr,
- offset: RUBY_OFFSET_EC_THREAD_PTR as i32,
+ offset: RUBY_OFFSET_EC_THREAD_PTR,
return_type: types::CPtr,
});
let thread_self = fun.push_insn(block, hir::Insn::LoadField {
recv: thread_ptr,
id: FieldName::SelfParam,
- offset: RUBY_OFFSET_THREAD_SELF as i32,
+ offset: RUBY_OFFSET_THREAD_SELF,
// TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread.
// You can make subclasses...
return_type: types::BasicObject,
@@ -467,7 +467,7 @@ fn inline_string_bytesize(fun: &mut hir::Function, block: hir::BlockId, recv: hi
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
- offset: RUBY_OFFSET_RSTRING_LEN as i32,
+ offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
@@ -491,7 +491,7 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
- offset: RUBY_OFFSET_RSTRING_LEN as i32,
+ offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
// TODO(max): Find a way to mark these guards as not needed for correctness... as in, once
@@ -519,7 +519,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
- offset: RUBY_OFFSET_RSTRING_LEN as i32,
+ offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: SideExitReason::GuardLess, state });
@@ -542,7 +542,7 @@ fn inline_string_empty_p(fun: &mut hir::Function, block: hir::BlockId, recv: hir
let len = fun.push_insn(block, hir::Insn::LoadField {
recv,
id: FieldName::len,
- offset: RUBY_OFFSET_RSTRING_LEN as i32,
+ offset: RUBY_OFFSET_RSTRING_LEN,
return_type: types::CInt64,
});
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs
index bd2b4e29d6..013996ba9f 100644
--- a/zjit/src/hir.rs
+++ b/zjit/src/hir.rs
@@ -531,6 +531,7 @@ pub enum SideExitReason {
UnhandledCallType(CallType),
UnhandledBlockArg,
TooManyKeywordParameters,
+ TooManyArgsForLir,
FixnumAddOverflow,
FixnumSubOverflow,
FixnumMultOverflow,
@@ -2578,6 +2579,7 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
// See: https://github.com/ruby/ruby/pull/15911#discussion_r2710544982
let block_arg = if 0 != params.flags.has_block() { 1 } else { 0 };
let final_argc = caller_positional + kw_total_num as usize + block_arg;
+ // TODO: Support passing arguments on the stack in C calls
if final_argc + 1 > C_ARG_OPNDS.len() { // +1 for self
function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
return false;
@@ -4257,6 +4259,13 @@ impl Function {
self.set_dynamic_send_reason(insn_id, ArgcParamMismatch);
continue;
}
+ // TODO: Support passing arguments on the stack in C calls
+ // +1 for self
+ if args.len()+1 > C_ARG_OPNDS.len() {
+ self.push_insn_id(block, insn_id);
+ self.set_dynamic_send_reason(insn_id, TooManyArgsForLir);
+ continue;
+ }
emit_super_call_guards(self, block, super_cme, current_cme, mid, state, frame_state.iseq);
@@ -4841,7 +4850,7 @@ impl Function {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
let ptr = self.push_insn(block, Insn::LoadField {
recv, id: FieldName::as_heap,
- offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32,
+ offset: ROBJECT_OFFSET_AS_HEAP_FIELDS,
return_type: types::CPtr,
});
let offset = SIZEOF_VALUE_I32 * ivar_index as i32;
@@ -4853,7 +4862,7 @@ impl Function {
fn load_ivar_embedded(&mut self, block: BlockId, recv: InsnId, id: ID, ivar_index: attr_index_t) -> InsnId {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
- let offset = ROBJECT_OFFSET_AS_ARY as i32
+ let offset = ROBJECT_OFFSET_AS_ARY
+ (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
self.push_insn(block, Insn::LoadField {
recv, id: id.into(), offset,
@@ -4883,9 +4892,9 @@ impl Function {
match layout {
ShapeLayout::RClass | ShapeLayout::RData => {
let offset = if layout == ShapeLayout::RClass {
- RCLASS_OFFSET_PRIME_FIELDS_OBJ as i32
+ RCLASS_OFFSET_PRIME_FIELDS_OBJ
} else {
- TDATA_OFFSET_FIELDS_OBJ as i32
+ TDATA_OFFSET_FIELDS_OBJ
};
let fields_obj = self.push_insn(block, Insn::LoadField {
@@ -5061,10 +5070,10 @@ impl Function {
// Current shape contains this ivar
let (ivar_storage, offset) = if recv_type.flags().is_embedded() {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
- let offset = ROBJECT_OFFSET_AS_ARY as i32 + (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
+ let offset = ROBJECT_OFFSET_AS_ARY + (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
(self_val, offset)
} else {
- let as_heap = self.push_insn(block, Insn::LoadField { recv: self_val, id: FieldName::as_heap, offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32, return_type: types::CPtr });
+ let as_heap = self.push_insn(block, Insn::LoadField { recv: self_val, id: FieldName::as_heap, offset: ROBJECT_OFFSET_AS_HEAP_FIELDS, return_type: types::CPtr });
let offset = SIZEOF_VALUE_I32 * ivar_index as i32;
(as_heap, offset)
};
@@ -5236,6 +5245,13 @@ impl Function {
return Err(());
}
+ // TODO: Support passing arguments on the stack in C calls
+ // +1 for self
+ if (argc as usize)+1 > C_ARG_OPNDS.len() {
+ fun.set_dynamic_send_reason(send_insn_id, TooManyArgsForLir);
+ return Err(());
+ }
+
// Check singleton class assumption first, before emitting other patchpoints
if !fun.assume_no_singleton_classes(block, recv_class, state) {
fun.set_dynamic_send_reason(send_insn_id, SingletonClassSeen);
@@ -7626,6 +7642,7 @@ fn add_iseq_to_hir(
}
YARVINSN_concatstrings => {
let count = get_arg(pc, 0).as_u32();
+ debug_assert!(count > 0, "concatstrings should have arguments");
let strings = state.stack_pop_n(count as usize)?;
let insn_id = fun.push_insn(block, Insn::StringConcat { strings, state: exit_id });
state.stack_push(insn_id);
@@ -9136,6 +9153,12 @@ fn add_iseq_to_hir(
}
YARVINSN_invokebuiltin => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };
+ // TODO: Support passing arguments on the stack in C calls
+ // +2 for ec, self
+ if (bf.argc + 2) as usize > C_ARG_OPNDS.len() {
+ fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::TooManyArgsForLir, recompile: None });
+ break; // End the block
+ }
let mut args = vec![];
for _ in 0..bf.argc {
@@ -9165,6 +9188,13 @@ fn add_iseq_to_hir(
YARVINSN_opt_invokebuiltin_delegate |
YARVINSN_opt_invokebuiltin_delegate_leave => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };
+ // TODO: Support passing arguments on the stack in C calls
+ // +2 for ec, self
+ if (bf.argc + 2) as usize > C_ARG_OPNDS.len() {
+ fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::TooManyArgsForLir, recompile: None });
+ break; // End the block
+ }
+
let index = get_arg(pc, 1).as_usize();
let argc = bf.argc as usize;
@@ -9396,6 +9426,10 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
let opt_num: usize = params.opt_num.try_into().expect("iseq param opt_num >= 0");
let lead_num: usize = params.lead_num.try_into().expect("iseq param lead_num >= 0");
let passed_opt_num = jit_entry_idx;
+ // We don't need to check crate::cruby::iseq_escapes_ep because we
+ // don't enter ISEQ_TYPE_MAIN/ISEQ_TYPE_EVAL using JIT-to-JIT calls.
+ // TODO: Stop compiling such JIT entries (Shopify/ruby#992)
+ let iseq_escapes_ep = crate::invariants::iseq_escapes_ep(iseq);
// If the iseq has keyword parameters, the keyword bits local will be appended to the local table.
let kw_bits_idx: Option<usize> = if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
@@ -9418,9 +9452,9 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
let mut entry_state = FrameState::new(iseq);
let mut ep: Option<InsnId> = None;
for local_idx in 0..num_locals(iseq) {
- if (lead_num + passed_opt_num..lead_num + opt_num).contains(&local_idx) {
+ let local = if (lead_num + passed_opt_num..lead_num + opt_num).contains(&local_idx) {
// Omitted optionals are locals, so they start as nils before their code run
- entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(Qnil) }));
+ fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(Qnil) })
} else if Some(local_idx) == kw_bits_idx {
// Read the kw_bits value written by the caller to the callee frame.
// This tells us which optional keywords were NOT provided and need their defaults evaluated.
@@ -9430,20 +9464,37 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
let ep_offset_u32 = u32::try_from(ep_offset)
.unwrap_or_else(|_| panic!("Could not convert ep_offset {ep_offset} to u32"));
let ep = *ep.get_or_insert_with(|| fun.push_insn(jit_entry_block, Insn::GetEP { level: 0 }));
- entry_state.locals.push(fun.get_local_from_ep(
+ fun.get_local_from_ep(
jit_entry_block,
iseq,
ep,
ep_offset_u32,
0,
types::BasicObject,
- ));
+ )
} else if local_idx < param_size {
let id = unsafe { rb_zjit_local_id(iseq, local_idx.try_into().unwrap()) };
- entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: id.into(), val_type: types::BasicObject }));
+ let local = fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: id.into(), val_type: types::BasicObject });
arg_idx += 1;
+ local
} else {
- entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(Qnil) }));
+ fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(Qnil) })
+ };
+ entry_state.locals.push(local);
+
+ // Once an ISEQ has escaped EP, HIR getlocal may need to read from the
+ // VM frame instead of FrameState. Direct JIT-to-JIT entry passes locals
+ // as C arguments, so initialize the frame slots here before such reads.
+ if iseq_escapes_ep {
+ let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
+ let local_id = unsafe { rb_zjit_local_id(iseq, local_idx.try_into().unwrap()) };
+ let ep = *ep.get_or_insert_with(|| fun.push_insn(jit_entry_block, Insn::GetEP { level: 0 }));
+ fun.push_insn(jit_entry_block, Insn::StoreField {
+ recv: ep,
+ id: local_id.into(),
+ offset: -(SIZEOF_VALUE_I32 * ep_offset),
+ val: local,
+ });
}
}
(self_param, entry_state)
@@ -10040,13 +10091,13 @@ mod validation_tests {
function.push_insn(entry, Insn::LoadField {
recv,
id: FieldName::as_heap,
- offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32,
+ offset: ROBJECT_OFFSET_AS_HEAP_FIELDS,
return_type: types::CPtr,
});
let ivar = function.push_insn(entry, Insn::LoadField {
recv,
id: FieldName::Id(ID(1)),
- offset: ROBJECT_OFFSET_AS_ARY as i32,
+ offset: ROBJECT_OFFSET_AS_ARY,
return_type: types::BasicObject,
});
function.push_insn(entry, Insn::Return { val: ivar });
@@ -10073,13 +10124,13 @@ mod validation_tests {
function.push_insn(entry, Insn::LoadField {
recv,
id: FieldName::as_heap,
- offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32,
+ offset: ROBJECT_OFFSET_AS_HEAP_FIELDS,
return_type: types::BasicObject,
});
let ivar = function.push_insn(entry, Insn::LoadField {
recv,
id: FieldName::Id(ID(1)),
- offset: ROBJECT_OFFSET_AS_ARY as i32,
+ offset: ROBJECT_OFFSET_AS_ARY,
return_type: types::Array,
});
function.push_insn(entry, Insn::Return { val: ivar });
diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs
index 5b94aed90e..0953905c19 100644
--- a/zjit/src/hir/opt_tests.rs
+++ b/zjit/src/hir/opt_tests.rs
@@ -3849,20 +3849,22 @@ mod hir_opt_tests {
EntryPoint JIT(0)
v5:BasicObject = LoadArg :self@0
v6:NilClass = Const Value(nil)
+ v7:CPtr = GetEP 0
+ StoreField v7, :a@0x1000, v6
Jump bb3(v5, v6)
- bb3(v8:BasicObject, v9:NilClass):
- v13:Fixnum[1] = Const Value(1)
- SetLocal :a, l0, EP@3, v13
- PatchPoint MethodRedefined(Object@0x1000, lambda@0x1008, cme:0x1010)
- v45:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile
- v46:BasicObject = CCallWithFrame v45, :Kernel#lambda@0x1038, block=0x1040
- v20:CPtr = GetEP 0
- v21:BasicObject = LoadField v20, :a@0x1048
- PatchPoint MethodRedefined(Object@0x1000, foo@0x1049, cme:0x1050)
- v32:CPtr = GetEP 0
- v33:BasicObject = LoadField v32, :a@0x1048
+ bb3(v10:BasicObject, v11:NilClass):
+ v15:Fixnum[1] = Const Value(1)
+ SetLocal :a, l0, EP@3, v15
+ PatchPoint MethodRedefined(Object@0x1008, lambda@0x1010, cme:0x1018)
+ v47:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v10, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile
+ v48:BasicObject = CCallWithFrame v47, :Kernel#lambda@0x1040, block=0x1048
+ v22:CPtr = GetEP 0
+ v23:BasicObject = LoadField v22, :a@0x1000
+ PatchPoint MethodRedefined(Object@0x1008, foo@0x1050, cme:0x1058)
+ v34:CPtr = GetEP 0
+ v35:BasicObject = LoadField v34, :a@0x1000
CheckInterrupts
- Return v33
+ Return v35
");
}
@@ -14486,25 +14488,30 @@ mod hir_opt_tests {
EntryPoint JIT(0)
v9:BasicObject = LoadArg :self@0
v10:BasicObject = LoadArg :a@1
- v11:BasicObject = LoadArg :_b@2
- v12:BasicObject = LoadArg :_c@3
- v13:NilClass = Const Value(nil)
- Jump bb3(v9, v10, v11, v12, v13)
- bb3(v15:BasicObject, v16:BasicObject, v17:BasicObject, v18:BasicObject, v19:NilClass):
+ v11:CPtr = GetEP 0
+ StoreField v11, :a@0x1001, v10
+ v13:BasicObject = LoadArg :_b@2
+ StoreField v11, :_b@0x1002, v13
+ v15:BasicObject = LoadArg :_c@3
+ StoreField v11, :_c@0x1003, v15
+ v17:NilClass = Const Value(nil)
+ StoreField v11, :formatted@0x1004, v17
+ Jump bb3(v9, v10, v13, v15, v17)
+ bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject, v24:NilClass):
CheckInterrupts
- SetLocal :formatted, l0, EP@3, v16
+ SetLocal :formatted, l0, EP@3, v21
PatchPoint SingleRactorMode
- SetIvar v15, :@formatted, v16
- v47:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1008))
+ SetIvar v20, :@formatted, v21
+ v52:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1008))
PatchPoint MethodRedefined(Class@0x1010, lambda@0x1018, cme:0x1020)
- v63:BasicObject = CCallWithFrame v47, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050
- v50:CPtr = GetEP 0
- v51:BasicObject = LoadField v50, :a@0x1001
- v52:BasicObject = LoadField v50, :_b@0x1002
- v53:BasicObject = LoadField v50, :_c@0x1058
- v54:BasicObject = LoadField v50, :formatted@0x1059
+ v68:BasicObject = CCallWithFrame v52, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050
+ v55:CPtr = GetEP 0
+ v56:BasicObject = LoadField v55, :a@0x1001
+ v57:BasicObject = LoadField v55, :_b@0x1002
+ v58:BasicObject = LoadField v55, :_c@0x1003
+ v59:BasicObject = LoadField v55, :formatted@0x1004
CheckInterrupts
- Return v63
+ Return v68
");
}
@@ -15729,21 +15736,24 @@ mod hir_opt_tests {
EntryPoint JIT(0)
v7:HeapBasicObject = LoadArg :self@0
v8:BasicObject = LoadArg :blk@1
- v9:NilClass = Const Value(nil)
- Jump bb3(v7, v8, v9)
- bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass):
+ v9:CPtr = GetEP 0
+ StoreField v9, :blk@0x1001, v8
+ v11:NilClass = Const Value(nil)
+ StoreField v9, :other_block@0x1002, v11
+ Jump bb3(v7, v8, v11)
+ bb3(v14:HeapBasicObject, v15:BasicObject, v16:NilClass):
PatchPoint NoSingletonClass(B@0x1008)
PatchPoint MethodRedefined(B@0x1008, proc@0x1010, cme:0x1018)
- v39:ObjectSubclass[class_exact:B] = GuardType v11, ObjectSubclass[class_exact:B] recompile
- v40:BasicObject = CCallWithFrame v39, :Kernel#proc@0x1040, block=0x1048
- v19:CPtr = GetEP 0
- v20:BasicObject = LoadField v19, :blk@0x1050
- SetLocal :other_block, l0, EP@3, v40
- v27:CPtr = GetEP 0
- v28:BasicObject = LoadField v27, :other_block@0x1051
- v30:BasicObject = InvokeSuper v39, 0x1058, v28 # SendFallbackReason: super: complex argument passing to `super` call
+ v42:ObjectSubclass[class_exact:B] = GuardType v14, ObjectSubclass[class_exact:B] recompile
+ v43:BasicObject = CCallWithFrame v42, :Kernel#proc@0x1040, block=0x1048
+ v22:CPtr = GetEP 0
+ v23:BasicObject = LoadField v22, :blk@0x1001
+ SetLocal :other_block, l0, EP@3, v43
+ v30:CPtr = GetEP 0
+ v31:BasicObject = LoadField v30, :other_block@0x1002
+ v33:BasicObject = InvokeSuper v42, 0x1050, v31 # SendFallbackReason: super: complex argument passing to `super` call
CheckInterrupts
- Return v30
+ Return v33
");
}
@@ -16328,67 +16338,82 @@ mod hir_opt_tests {
EntryPoint JIT(0)
v16:BasicObject = LoadArg :self@0
v17:BasicObject = LoadArg :list@1
- v18:NilClass = Const Value(nil)
- v19:NilClass = Const Value(nil)
+ v18:CPtr = GetEP 0
+ StoreField v18, :list@0x1001, v17
v20:NilClass = Const Value(nil)
- Jump bb3(v16, v17, v18, v19, v20)
- bb3(v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:BasicObject, v40:NilClass):
- v43:NilClass = Const Value(nil)
- SetLocal :sep, l0, EP@5, v43
- Jump bb5(v36, v37, v43, v39, v40)
+ StoreField v18, :sep@0x1002, v20
+ v22:NilClass = Const Value(nil)
+ StoreField v18, :iter_method@0x1005, v22
+ v24:NilClass = Const Value(nil)
+ StoreField v18, :kwsplat@0x1006, v24
+ Jump bb3(v16, v17, v20, v22, v24)
+ bb3(v51:BasicObject, v52:BasicObject, v53:BasicObject, v54:BasicObject, v55:NilClass):
+ v58:NilClass = Const Value(nil)
+ SetLocal :sep, l0, EP@5, v58
+ Jump bb5(v51, v52, v58, v54, v55)
bb4():
EntryPoint JIT(1)
- v23:BasicObject = LoadArg :self@0
- v24:BasicObject = LoadArg :list@1
- v25:BasicObject = LoadArg :sep@2
- v26:NilClass = Const Value(nil)
- v27:NilClass = Const Value(nil)
- Jump bb5(v23, v24, v25, v26, v27)
- bb5(v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:BasicObject, v51:NilClass):
- v54:StaticSymbol[:each] = Const Value(VALUE(0x1008))
- SetLocal :iter_method, l0, EP@4, v54
- Jump bb7(v47, v48, v49, v54, v51)
- bb6():
- EntryPoint JIT(2)
- v30:BasicObject = LoadArg :self@0
- v31:BasicObject = LoadArg :list@1
+ v28:BasicObject = LoadArg :self@0
+ v29:BasicObject = LoadArg :list@1
+ v30:CPtr = GetEP 0
+ StoreField v30, :list@0x1001, v29
v32:BasicObject = LoadArg :sep@2
- v33:BasicObject = LoadArg :iter_method@3
+ StoreField v30, :sep@0x1002, v32
v34:NilClass = Const Value(nil)
- Jump bb7(v30, v31, v32, v33, v34)
- bb7(v58:BasicObject, v59:BasicObject, v60:BasicObject, v61:BasicObject, v62:NilClass):
- CheckInterrupts
- v68:CBool = Test v60
- v69:Truthy = RefineType v60, Truthy
- CondBranch v68, bb8(v58, v59, v69, v61, v62), bb11()
+ StoreField v30, :iter_method@0x1005, v34
+ v36:NilClass = Const Value(nil)
+ StoreField v30, :kwsplat@0x1006, v36
+ Jump bb5(v28, v29, v32, v34, v36)
+ bb5(v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:NilClass):
+ v69:StaticSymbol[:each] = Const Value(VALUE(0x1008))
+ SetLocal :iter_method, l0, EP@4, v69
+ Jump bb7(v62, v63, v64, v69, v66)
+ bb6():
+ EntryPoint JIT(2)
+ v40:BasicObject = LoadArg :self@0
+ v41:BasicObject = LoadArg :list@1
+ v42:CPtr = GetEP 0
+ StoreField v42, :list@0x1001, v41
+ v44:BasicObject = LoadArg :sep@2
+ StoreField v42, :sep@0x1002, v44
+ v46:BasicObject = LoadArg :iter_method@3
+ StoreField v42, :iter_method@0x1005, v46
+ v48:NilClass = Const Value(nil)
+ StoreField v42, :kwsplat@0x1006, v48
+ Jump bb7(v40, v41, v44, v46, v48)
+ bb7(v73:BasicObject, v74:BasicObject, v75:BasicObject, v76:BasicObject, v77:NilClass):
+ CheckInterrupts
+ v83:CBool = Test v75
+ v84:Truthy = RefineType v75, Truthy
+ CondBranch v83, bb8(v73, v74, v84, v76, v77), bb11()
bb11():
- v71:Falsy = RefineType v60, Falsy
+ v86:Falsy = RefineType v75, Falsy
PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020)
- v118:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v58, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile
- v119:BasicObject = CCallWithFrame v118, :Kernel#lambda@0x1048, block=0x1050
- v75:CPtr = GetEP 0
- v76:BasicObject = LoadField v75, :list@0x1001
- v78:BasicObject = LoadField v75, :iter_method@0x1058
- v79:BasicObject = LoadField v75, :kwsplat@0x1059
- SetLocal :sep, l0, EP@5, v119
- Jump bb8(v118, v76, v119, v78, v79)
- bb8(v83:BasicObject, v84:BasicObject, v85:BasicObject, v86:BasicObject, v87:BasicObject):
+ v133:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile
+ v134:BasicObject = CCallWithFrame v133, :Kernel#lambda@0x1048, block=0x1050
+ v90:CPtr = GetEP 0
+ v91:BasicObject = LoadField v90, :list@0x1001
+ v93:BasicObject = LoadField v90, :iter_method@0x1005
+ v94:BasicObject = LoadField v90, :kwsplat@0x1006
+ SetLocal :sep, l0, EP@5, v134
+ Jump bb8(v133, v91, v134, v93, v94)
+ bb8(v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject, v102:BasicObject):
PatchPoint SingleRactorMode
- PatchPoint StableConstantNames(0x1060, CONST)
- v115:HashExact[VALUE(0x1068)] = Const Value(VALUE(0x1068))
- SetLocal :kwsplat, l0, EP@3, v115
- v96:CPtr = GetEP 0
- v97:BasicObject = LoadField v96, :list@0x1001
- v99:CPtr = GetEP 0
- v100:BasicObject = LoadField v99, :iter_method@0x1058
- v102:BasicObject = Send v97, 0x1070, :__send__, v100 # SendFallbackReason: Send: unsupported method type Optimized
- v103:CPtr = GetEP 0
- v104:BasicObject = LoadField v103, :list@0x1001
- v105:BasicObject = LoadField v103, :sep@0x1002
- v106:BasicObject = LoadField v103, :iter_method@0x1058
- v107:BasicObject = LoadField v103, :kwsplat@0x1059
+ PatchPoint StableConstantNames(0x1058, CONST)
+ v130:HashExact[VALUE(0x1060)] = Const Value(VALUE(0x1060))
+ SetLocal :kwsplat, l0, EP@3, v130
+ v111:CPtr = GetEP 0
+ v112:BasicObject = LoadField v111, :list@0x1001
+ v114:CPtr = GetEP 0
+ v115:BasicObject = LoadField v114, :iter_method@0x1005
+ v117:BasicObject = Send v112, 0x1068, :__send__, v115 # SendFallbackReason: Send: unsupported method type Optimized
+ v118:CPtr = GetEP 0
+ v119:BasicObject = LoadField v118, :list@0x1001
+ v120:BasicObject = LoadField v118, :sep@0x1002
+ v121:BasicObject = LoadField v118, :iter_method@0x1005
+ v122:BasicObject = LoadField v118, :kwsplat@0x1006
CheckInterrupts
- Return v102
+ Return v117
");
}
@@ -18779,4 +18804,89 @@ mod hir_opt_tests {
Return v200
");
}
+
+ #[test]
+ fn test_ccall_with_frame_too_many_args_result_used_in_later_block() {
+ unsafe extern "C" fn test_seven_args(
+ _self: VALUE,
+ a: VALUE,
+ b: VALUE,
+ c: VALUE,
+ d: VALUE,
+ e: VALUE,
+ f: VALUE,
+ g: VALUE,
+ ) -> VALUE {
+ unsafe { rb_ary_new_from_args(7, a, b, c, d, e, f, g) }
+ }
+
+ with_rubyvm(|| {
+ let klass = define_class("ZJITSevenArgs", unsafe { rb_cObject });
+ unsafe {
+ rb_define_method(
+ klass,
+ c"seven".as_ptr(),
+ Some(std::mem::transmute::<
+ unsafe extern "C" fn(VALUE, VALUE, VALUE, VALUE, VALUE, VALUE, VALUE, VALUE) -> VALUE,
+ unsafe extern "C" fn(VALUE) -> VALUE,
+ >(test_seven_args)),
+ 7,
+ );
+ }
+ });
+
+ eval(r#"
+ def test(obj, flag)
+ priceable = obj.seven(1, 2, 3, 4, 5, 6, 7)
+ if flag
+ priceable
+ else
+ nil
+ end
+ end
+
+ obj = ZJITSevenArgs.new
+ test(obj, true) # profile receiver class
+ "#);
+ assert_snapshot!(hir_string("test"), @"
+ fn test@<compiled>:3:
+ bb1():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ v2:CPtr = LoadSP
+ v3:BasicObject = LoadField v2, :obj@0x1000
+ v4:BasicObject = LoadField v2, :flag@0x1001
+ v5:NilClass = Const Value(nil)
+ Jump bb3(v1, v3, v4, v5)
+ bb2():
+ EntryPoint JIT(0)
+ v8:BasicObject = LoadArg :self@0
+ v9:BasicObject = LoadArg :obj@1
+ v10:BasicObject = LoadArg :flag@2
+ v11:NilClass = Const Value(nil)
+ Jump bb3(v8, v9, v10, v11)
+ bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:NilClass):
+ v21:Fixnum[1] = Const Value(1)
+ v23:Fixnum[2] = Const Value(2)
+ v25:Fixnum[3] = Const Value(3)
+ v27:Fixnum[4] = Const Value(4)
+ v29:Fixnum[5] = Const Value(5)
+ v31:Fixnum[6] = Const Value(6)
+ v33:Fixnum[7] = Const Value(7)
+ v35:BasicObject = Send v14, :seven, v21, v23, v25, v27, v29, v31, v33 # SendFallbackReason: Too many arguments for LIR
+ PatchPoint NoEPEscape(test)
+ CheckInterrupts
+ v43:CBool = Test v15
+ v44:Falsy = RefineType v15, Falsy
+ CondBranch v43, bb5(), bb4(v13, v14, v44, v35)
+ bb5():
+ v46:Truthy = RefineType v15, Truthy
+ CheckInterrupts
+ Return v35
+ bb4(v53:BasicObject, v54:BasicObject, v55:Falsy, v56:BasicObject):
+ v60:NilClass = Const Value(nil)
+ CheckInterrupts
+ Return v60
+ ");
+ }
}
diff --git a/zjit/src/jit_frame.rs b/zjit/src/jit_frame.rs
index 3dafd385be..eaedcd0a60 100644
--- a/zjit/src/jit_frame.rs
+++ b/zjit/src/jit_frame.rs
@@ -1,4 +1,8 @@
-use crate::cruby::{IseqPtr, VALUE, rb_gc_mark_movable, rb_gc_location};
+use std::alloc::{alloc, handle_alloc_error, Layout};
+use std::mem::{align_of, size_of};
+use std::ptr;
+
+use crate::cruby::{__IncompleteArrayField, IseqPtr, VALUE, rb_gc_mark_movable, rb_gc_location};
use crate::cruby::zjit_jit_frame;
use crate::codegen::iseq_may_write_block_code;
use crate::state::ZJITState;
@@ -8,18 +12,43 @@ use crate::state::ZJITState;
pub type JITFrame = zjit_jit_frame;
impl JITFrame {
- /// Allocate a JITFrame on the heap, register it with ZJITState, and return
- /// a raw pointer that remains valid for the lifetime of the process.
- fn alloc(jit_frame: JITFrame) -> *const Self {
- let raw_ptr = Box::into_raw(Box::new(jit_frame));
+ /// Allocate a JITFrame and its trailing stack map on the heap, register it
+ /// with ZJITState, and return a raw pointer that remains valid for the
+ /// lifetime of the process.
+ fn alloc(
+ pc: *const VALUE,
+ iseq: IseqPtr,
+ materialize_block_code: bool,
+ stack_size: usize,
+ ) -> *const Self {
+ // JITFrame ends with a flexible stack[] array, so allocate enough
+ // space for the fixed fields plus the requested stack map entries.
+ let frame_size = size_of::<JITFrame>()
+ .checked_add(stack_size.checked_mul(size_of::<VALUE>()).unwrap())
+ .unwrap();
+ let layout = Layout::from_size_align(frame_size, align_of::<JITFrame>()).unwrap();
+ let raw_ptr = unsafe { alloc(layout) as *mut JITFrame };
+ if raw_ptr.is_null() {
+ handle_alloc_error(layout);
+ }
+
+ unsafe {
+ ptr::write(raw_ptr, JITFrame {
+ pc,
+ iseq,
+ materialize_block_code,
+ stack_size: stack_size.try_into().unwrap(),
+ stack: __IncompleteArrayField::new(),
+ });
+ }
ZJITState::get_jit_frames().push(raw_ptr);
raw_ptr as *const _
}
/// Create a JITFrame for an ISEQ frame.
- pub fn new_iseq(pc: *const VALUE, iseq: IseqPtr) -> *const Self {
+ pub fn new_iseq(pc: *const VALUE, iseq: IseqPtr, stack_size: usize) -> *const Self {
let materialize_block_code = !iseq_may_write_block_code(iseq);
- Self::alloc(JITFrame { pc, iseq, materialize_block_code })
+ Self::alloc(pc, iseq, materialize_block_code, stack_size)
}
/// Mark the iseq pointer for GC. Called from rb_zjit_root_mark.
@@ -94,6 +123,29 @@ mod tests {
"), @"1");
}
+ // Direct JIT-to-JIT entry passes callee locals as native arguments. If the
+ // callee ISEQ has already escaped EP, later getlocal reads use EP memory,
+ // so JIT entry must materialize those locals into the callee frame.
+ #[test]
+ fn test_jit_entry_materializes_ep_escaped_locals() {
+ assert_snapshot!(inspect("
+ def poison(*) = nil
+
+ def victim(a, b, c)
+ lambda { a }
+ a
+ end
+
+ def jit_entry
+ poison([], [], [], [])
+ victim(:expected, 1, 2)
+ end
+
+ jit_entry
+ Array.new(100) { jit_entry }.uniq
+ "), @"[:expected]");
+ }
+
// Materialize frames on side exit: a type guard triggers a side exit with
// multiple JIT frames on the stack. All frames must be materialized before
// the interpreter resumes.
diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs
index b019ba0b87..a769005fb9 100644
--- a/zjit/src/stats.rs
+++ b/zjit/src/stats.rs
@@ -234,6 +234,7 @@ make_counters! {
exit_block_param_proxy_profile_not_covered,
exit_block_param_wb_required,
exit_too_many_keyword_parameters,
+ exit_too_many_args_for_lir,
exit_no_profile_send,
exit_splatkw_not_nil_or_hash,
exit_splatkw_polymorphic,
@@ -630,6 +631,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter {
BlockParamProxyProfileNotCovered => exit_block_param_proxy_profile_not_covered,
BlockParamWbRequired => exit_block_param_wb_required,
TooManyKeywordParameters => exit_too_many_keyword_parameters,
+ TooManyArgsForLir => exit_too_many_args_for_lir,
SplatKwNotNilOrHash => exit_splatkw_not_nil_or_hash,
SplatKwPolymorphic => exit_splatkw_polymorphic,
SplatKwNotProfiled => exit_splatkw_not_profiled,