Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions bootstraptest/test_yjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5535,3 +5535,23 @@ def compiled_method
# Resume the fiber — compiled_method's iseq must still be valid
fiber.resume.to_s
}

# regression test for register mapping of methods with over 256 locals
# [Bug #22074]
assert_equal "ok", %q{
source = +"def many_locals\n"
source << " total = 0\n"

128.times do |i|
source << " y#{i} = 1\n"
source << " x#{i} = Object.new\n"
end

source << " total += 1\n"
source << " raise total.inspect unless total == 1\n"
source << "end\n"

eval(source)
many_locals
"ok"
}
116 changes: 73 additions & 43 deletions pathname_builtin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -589,17 +589,17 @@ def each_filename # :yield: filename
# ```ruby
# # Absolute path.
# Pathname('/path/to/some/file.rb').descend {|pn| p pn }
# #<Pathname:/>
# #<Pathname:/path>
# #<Pathname:/path/to>
# #<Pathname:/path/to/some>
# #<Pathname:/path/to/some/file.rb>
# # #<Pathname:/>
# # #<Pathname:/path>
# # #<Pathname:/path/to>
# # #<Pathname:/path/to/some>
# # #<Pathname:/path/to/some/file.rb>
# # Relative path.
# Pathname('path/to/some/file.rb').descend {|pn| p pn }
# #<Pathname:path>
# #<Pathname:path/to>
# #<Pathname:path/to/some>
# #<Pathname:path/to/some/file.rb>
# # #<Pathname:path>
# # #<Pathname:path/to>
# # #<Pathname:path/to/some>
# # #<Pathname:path/to/some/file.rb>
# ```
#
# With no block given, returns a new Enumerator.
Expand Down Expand Up @@ -811,42 +811,39 @@ def children(with_directory=true)
result
end

# Iterates over the children of the directory
# (files and subdirectories, not recursive).
#
# It yields Pathname object for each child.
# :markup: markdown
#
# By default, the yielded pathnames will have enough information to access
# the files.
# call-seq:
# each_child(with_dirnames = true) {|entry| ... } -> array_of_pathnames
# each_child(with_dirnames = true) -> new_enumerator
#
# If you set +with_directory+ to +false+, then the returned pathnames will
# contain the filename only.
# With a block given and `with_dirnames` given as `true` (the default),
# yields a new pathname for each child
# of the entry represented by `self`;
# returns an array of those pathnames:
#
# Pathname("/usr/local").each_child {|f| p f }
# #=> #<Pathname:/usr/local/share>
# # #<Pathname:/usr/local/bin>
# # #<Pathname:/usr/local/games>
# # #<Pathname:/usr/local/lib>
# # #<Pathname:/usr/local/include>
# # #<Pathname:/usr/local/sbin>
# # #<Pathname:/usr/local/src>
# # #<Pathname:/usr/local/man>
# ```ruby
# Pathname('include').each_child {|child| p child }
# # #<Pathname:include/ruby>
# # #<Pathname:include/ruby.h>
# # => [#<Pathname:include/ruby>, #<Pathname:include/ruby.h>]
# ```
#
# Pathname("/usr/local").each_child(false) {|f| p f }
# #=> #<Pathname:share>
# # #<Pathname:bin>
# # #<Pathname:games>
# # #<Pathname:lib>
# # #<Pathname:include>
# # #<Pathname:sbin>
# # #<Pathname:src>
# # #<Pathname:man>
# With a block given and `with_dirnames` given as `false`,
# yields a new pathname for each child
# of the entry represented by `self` with its dirname omitted;
# returns an array of those pathnames:
#
# Note that the results never contain the entries +.+ and +..+ in
# the directory because they are not children.
# ```ruby
# Pathname('include').each_child(false) {|child| p child }
# # #<Pathname:ruby>
# # #<Pathname:ruby.h>
# # => [#<Pathname:ruby>, #<Pathname:ruby.h>]
# ```
#
# See Pathname#children
# Note that entries `'.'` and `'..'` are not children.
#
# With no block given, returns a new Enumerator.
def each_child(with_directory=true, &b)
children(with_directory).each(&b)
end
Expand Down Expand Up @@ -1639,10 +1636,27 @@ class << self
# Pathname object.
def entries() Dir.entries(@path).map {|f| self.class.new(f) } end

# Iterates over the entries (files and subdirectories) in the directory. It
# yields a Pathname object for each entry.
# :markup: markdown
#
# This method has existed since 1.8.1.
# call-seq:
# each_entry {|entry| ... } -> nil
# each_entry -> new_enumerator
#
# With a block given,
# yields a new pathname for each entry
# in the entry represented by `self`;
# returns `nil`:
#
# ```ruby
# Pathname('include').each_entry {|entry| p entry }
# # #<Pathname:ruby>
# # #<Pathname:..>
# # #<Pathname:ruby.h>
# # #<Pathname:.>
# # => nil
# ```
#
# With no block given, returns a new Enumerator.
def each_entry(&block) # :yield: pathname
return to_enum(__method__) unless block_given?
Dir.foreach(@path) {|f| yield self.class.new(f) }
Expand All @@ -1661,8 +1675,24 @@ def opendir(&block) # :yield: dir
end

class Pathname # * mixed *
# Removes a file or directory, using <tt>File.unlink</tt> or
# <tt>Dir.unlink</tt> as necessary.
#
# :markup: markdown
#
# call-seq:
# unlink -> 1 or 0
#
# Removes the file or directory represented by `self`, using:
#
# - File.unlink, if `self` represents a file; returns `1`.
# - Dir.unlink, if `self` represents a directory; returns `0`.
#
# Examples:
#
# ```ruby
# Pathname(Tempfile.create).unlink # => 1
# Pathname(Pathname.mktmpdir).unlink # => 0
# ```
#
def unlink()
Dir.unlink @path
rescue Errno::ENOTDIR
Expand Down
6 changes: 4 additions & 2 deletions yjit/src/backend/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::codegen::{gen_counted_exit, gen_outlined_exit};
use crate::cruby::{vm_stack_canary, SIZEOF_VALUE_I32, VALUE, VM_ENV_DATA_SIZE};
use crate::virtualmem::CodePtr;
use crate::asm::{CodeBlock, OutlinedCb};
use crate::core::{Context, RegMapping, RegOpnd, MAX_CTX_TEMPS};
use crate::core::{Context, RegMapping, RegOpnd, MAX_CTX_LOCALS, MAX_CTX_TEMPS};
use crate::options::*;
use crate::stats::*;

Expand Down Expand Up @@ -242,7 +242,9 @@ impl Opnd
let last_idx = stack_size as i32 + VM_ENV_DATA_SIZE as i32 - 1;
assert!(last_idx <= idx, "Local index {} must be >= last local index {}", idx, last_idx);
assert!(idx <= last_idx + num_locals as i32, "Local index {} must be < last local index {} + local size {}", idx, last_idx, num_locals);
RegOpnd::Local((last_idx + num_locals as i32 - idx) as u8)
// Indices that don't fit in u8 are capped to MAX_CTX_LOCALS, which is untrackable.
let local_idx = last_idx + num_locals as i32 - idx;
RegOpnd::Local(local_idx.try_into().unwrap_or(MAX_CTX_LOCALS as u8))
} else {
assert!(idx < stack_size as i32);
RegOpnd::Stack((stack_size as i32 - idx - 1) as u8)
Expand Down
2 changes: 1 addition & 1 deletion yjit/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::invariants::*;
pub const MAX_CTX_TEMPS: usize = 8;

// Maximum number of local variable types or registers we keep track of
const MAX_CTX_LOCALS: usize = 8;
pub const MAX_CTX_LOCALS: usize = 8;

/// An index into `ISEQ_BODY(iseq)->iseq_encoded`. Points
/// to a YARV instruction or an instruction operand.
Expand Down
64 changes: 35 additions & 29 deletions zjit/src/backend/lir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2661,24 +2661,8 @@ impl Assembler
asm.cret(Opnd::UImm(Qundef.as_u64()));
}

/// Compile the main side-exit code. This function takes only SideExit so
/// that it can be safely deduplicated by using SideExit as a dedup key.
fn compile_exit(asm: &mut Assembler, exit: &SideExit) {
compile_exit_save_state(asm, exit);
// If this side exit should trigger recompilation, call the recompile
// function after saving VM state. The ccall must happen after
// compile_exit_save_state because it clobbers caller-saved registers
// that may hold stack/local operands we need to save.
fn compile_exit_recompile(asm: &mut Assembler, exit: &SideExit) {
if let Some(recompile) = &exit.recompile {
if cfg!(feature = "runtime_checks") {
// Clear jit_return to fully materialize the frame. This must happen
// before any C call in the exit path (e.g. exit_recompile)
// because that C call can trigger GC, which walks the stack and would
// hit the CFP_JIT_RETURN assertion if jit_return still holds the
// runtime_checks poison value (JIT_RETURN_POISON).
asm_comment!(asm, "clear cfp->jit_return");
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), 0.into());
}

use crate::codegen::exit_recompile;
asm_comment!(asm, "profile and maybe recompile");
Expand All @@ -2692,6 +2676,38 @@ impl Assembler
})
);
}
}

/// Compile the main side-exit code. The side exit will optionally record a traced exit
/// stack, optionally trigger recompilation, and then return to the interpreter. Shared
/// exits pass no trace reason so they can still be deduplicated by SideExit.
/// IOW, we should never pass a trace reason if we expect the exit to be
/// deduplicated.
fn compile_exit(asm: &mut Assembler, exit: &SideExit, trace_reason: Option<SideExitReason>) {
// Save VM state before the ccall so that
// rb_profile_frames sees valid cfp->pc and the
// ccall doesn't clobber caller-saved registers
// holding stack/local operands.
compile_exit_save_state(asm, exit);
if trace_reason.is_some() || exit.recompile.is_some() {
if cfg!(feature = "runtime_checks") {
// Clear jit_return to fully materialize the frame. This must happen
// before any C call in the exit path because that C call can trigger
// GC, which walks the stack and would hit the CFP_JIT_RETURN assertion
// if jit_return still holds the runtime_checks poison value
// (JIT_RETURN_POISON).
asm_comment!(asm, "clear cfp->jit_return");
asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), 0.into());
}
}
if let Some(reason) = trace_reason {
// Leak a CString with the reason so it's available at runtime
let reason_cstr = std::ffi::CString::new(reason.to_string())
.unwrap_or_else(|_| std::ffi::CString::new("unknown").unwrap());
let reason_ptr = reason_cstr.into_raw() as *const u8;
asm_ccall!(asm, rb_zjit_record_exit_stack, Opnd::const_ptr(reason_ptr));
}
compile_exit_recompile(asm, exit);
compile_exit_return(asm);
}

Expand Down Expand Up @@ -2768,17 +2784,7 @@ impl Assembler
}

if should_record_exit {
// Save VM state before the ccall so that
// rb_profile_frames sees valid cfp->pc and the
// ccall doesn't clobber caller-saved registers
// holding stack/local operands.
compile_exit_save_state(self, &exit);
// Leak a CString with the reason so it's available at runtime
let reason_cstr = std::ffi::CString::new(reason.to_string())
.unwrap_or_else(|_| std::ffi::CString::new("unknown").unwrap());
let reason_ptr = reason_cstr.into_raw() as *const u8;
asm_ccall!(self, rb_zjit_record_exit_stack, Opnd::const_ptr(reason_ptr));
compile_exit_return(self);
compile_exit(self, &exit, Some(reason));
} else {
// If the side exit has already been compiled, jump to it.
// Otherwise, let it fall through and compile the exit next.
Expand All @@ -2798,7 +2804,7 @@ impl Assembler
let new_exit = self.new_label("side_exit");
self.write_label(new_exit.clone());
asm_comment!(self, "Exit: {pc}");
compile_exit(self, &exit);
compile_exit(self, &exit, None);
compiled_exits.insert(exit, new_exit.unwrap_label());
new_exit
};
Expand Down
6 changes: 3 additions & 3 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::GuardBitEquals { val, expected, reason, state, recompile } => gen_guard_bit_equals(jit, asm, opnd!(val), expected, reason, recompile, &function.frame_state(state)),
&Insn::GuardAnyBitSet { val, mask, reason, state, .. } => gen_guard_any_bit_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)),
&Insn::GuardNoBitsSet { val, mask, reason, state, .. } => gen_guard_no_bits_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)),
&Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)),
&Insn::GuardLess { left, right, reason, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), reason, &function.frame_state(state)),
&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)),
Expand Down Expand Up @@ -885,9 +885,9 @@ fn gen_getblockparam(jit: &mut JITState, asm: &mut Assembler, ep_offset: u32, le
asm.load(Opnd::mem(VALUE_BITS, ep, offset))
}

fn gen_guard_less(jit: &mut JITState, asm: &mut Assembler, left: Opnd, right: Opnd, state: &FrameState) -> Opnd {
fn gen_guard_less(jit: &mut JITState, asm: &mut Assembler, left: Opnd, right: Opnd, reason: SideExitReason, state: &FrameState) -> Opnd {
asm.cmp(left, right);
asm.jge(jit, side_exit(jit, state, SideExitReason::GuardLess));
asm.jge(jit, side_exit(jit, state, reason));
left
}

Expand Down
13 changes: 13 additions & 0 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,19 @@ pub fn get_class_name(class: VALUE) -> String {
name
}

// Return the module name for a given module or class. For anonymous modules, returns None since
// rb_mod_name returns Qnil.
pub fn get_module_name(module: VALUE) -> Option<String> {
// type checks for rb_mod_name()
assert!(unsafe { RB_TYPE_P(module, RUBY_T_MODULE) || RB_TYPE_P(module, RUBY_T_CLASS) }, "Expected class or module");
let name = unsafe { rb_mod_name(module) };
if name == Qnil {
None
} else {
Some(ruby_str_to_rust_string(name))
}
}


#[cfg(test)]
mod class_name_tests {
Expand Down
8 changes: 4 additions & 4 deletions zjit/src/cruby_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ fn inline_array_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In
let index = fun.coerce_to(block, index, types::Fixnum, state);
let index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, state });
let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, reason: SideExitReason::GuardLess, state });
let index = fun.push_insn(block, hir::Insn::AdjustBounds { index, length });
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
use crate::hir::SideExitReason;
Expand All @@ -392,7 +392,7 @@ fn inline_array_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In
// Bounds check: unbox Fixnum index and guard 0 <= idx < length.
let index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index });
let length = fun.push_insn(block, hir::Insn::ArrayLength { array: recv });
let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, state });
let index = fun.push_insn(block, hir::Insn::GuardLess { left: index, right: length, reason: SideExitReason::GuardLess, state });
let index = fun.push_insn(block, hir::Insn::AdjustBounds { index, length });
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
use crate::hir::SideExitReason;
Expand Down Expand Up @@ -492,7 +492,7 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
// the data dependency is gone (say, the StringGetbyte is elided), they can also be elided.
//
// This is unlike most other guards.
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, state });
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: SideExitReason::GuardLess, state });
let unboxed_index = fun.push_insn(block, hir::Insn::AdjustBounds { index: unboxed_index, length: len });
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
use crate::hir::SideExitReason;
Expand All @@ -516,7 +516,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir
offset: RUBY_OFFSET_RSTRING_LEN as i32,
return_type: types::CInt64,
});
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, state });
let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: SideExitReason::GuardLess, state });
let unboxed_index = fun.push_insn(block, hir::Insn::AdjustBounds { index: unboxed_index, length: len });
let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) });
use crate::hir::SideExitReason;
Expand Down
Loading