Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8e01768
ZJIT: Break out CFunc send fallback stats (#15193)
tekknolagi Nov 14, 2025
94f701d
ZJIT: Move special Fixnum BOP_EQ into cruby_methods
tekknolagi Nov 14, 2025
47904d8
ZJIT: Move special Fixnum BOP_PLUS into cruby_methods
tekknolagi Nov 14, 2025
4683ce5
ZJIT: Move special Fixnum BOP_MINUS into cruby_methods
tekknolagi Nov 14, 2025
ad6ca3a
ZJIT: Move special Fixnum BOP_GT into cruby_methods
tekknolagi Nov 14, 2025
7a7035e
ZJIT: Move special Fixnum BOP_NEQ into cruby_methods
tekknolagi Nov 14, 2025
f0e5772
ZJIT: Move special Fixnum BOP_MULT into cruby_methods
tekknolagi Nov 14, 2025
0851c2a
ZJIT: Move special Fixnum BOP_DIV into cruby_methods
tekknolagi Nov 14, 2025
1170493
ZJIT: Move special Fixnum BOP_MOD into cruby_methods
tekknolagi Nov 14, 2025
1c4240b
ZJIT: Move special Fixnum BOP_LT into cruby_methods
tekknolagi Nov 14, 2025
236366c
ZJIT: Move special Fixnum BOP_LE into cruby_methods
tekknolagi Nov 14, 2025
35c2c65
ZJIT: Move special Fixnum BOP_GE into cruby_methods
tekknolagi Nov 14, 2025
22d2bb0
ZJIT: Move special Fixnum BOP_AND into cruby_methods
tekknolagi Nov 14, 2025
491be57
ZJIT: Move special Fixnum BOP_OR into cruby_methods
tekknolagi Nov 14, 2025
e417f6f
ZJIT: Remove dead function and set .freeze reason
tekknolagi Nov 14, 2025
a0cce40
ZJIT: Remove done TODO [ci skip]
XrXr Nov 14, 2025
5d35e24
ZJIT: Check argument count matches callee's parameters
XrXr Nov 14, 2025
89849f3
ZJIT: Support JIT-to-JIT calls to callees with optional parameters
XrXr Nov 14, 2025
eb6e36a
Skip tests in TestThreadLockNativeThread when using LSAN
peterzhu2118 Nov 14, 2025
6fabca8
Add rubygems package to fix cygwin CI
fd00 Nov 14, 2025
d7369f0
ZJIT: Add individual tests for complex arg pass counters
rwstauner Nov 12, 2025
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
13 changes: 6 additions & 7 deletions .github/workflows/cygwin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,19 @@ jobs:
- name: Setup Cygwin
uses: cygwin/cygwin-install-action@master
with:
packages: ruby gcc-core make autoconf libtool libssl-devel libyaml-devel libffi-devel zlib-devel
packages: ruby gcc-core make autoconf libtool libssl-devel libyaml-devel libffi-devel zlib-devel rubygems

- name: configure
run: |
./autogen.sh
./configure --disable-install-doc
shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0}

# This fails with: tool/outdate-bundled-gems.rb:3:in 'Kernel#require': cannot load such file -- rubygems (LoadError)
#- name: Extract bundled gems
# run: |
# make ruby -j5
# make extract-gems
# shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0}
- name: Extract bundled gems
run: |
make ruby -j5
make extract-gems
shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0}

- name: make all
timeout-minutes: 30
Expand Down
4 changes: 4 additions & 0 deletions test/-ext-/thread/test_lock_native_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

class TestThreadLockNativeThread < Test::Unit::TestCase
def test_lock_native_thread
omit "LSAN reports memory leak because NT is not freed for MN thread" if Test::Sanitizers.lsan_enabled?

assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY)
require '-test-/thread/lock_native_thread'

Expand All @@ -28,6 +30,8 @@ def test_lock_native_thread
end

def test_lock_native_thread_tls
omit "LSAN reports memory leak because NT is not freed for MN thread" if Test::Sanitizers.lsan_enabled?

assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY)
require '-test-/thread/lock_native_thread'
tn = 10
Expand Down
35 changes: 23 additions & 12 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::SendWithoutBlock { cd, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason),
// Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it.
Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::SendWithoutBlockDirectTooManyArgs),
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir),
Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state)),
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
Expand Down Expand Up @@ -1264,10 +1264,21 @@ fn gen_send_without_block_direct(

// Set up arguments
let mut c_args = vec![recv];
c_args.extend(args);
c_args.extend(&args);

let num_optionals_passed = if unsafe { get_iseq_flags_has_opt(iseq) } {
// See vm_call_iseq_setup_normal_opt_start in vm_inshelper.c
let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) } as u32;
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) } as u32;
assert!(args.len() as u32 <= lead_num + opt_num);
let num_optionals_passed = args.len() as u32 - lead_num;
num_optionals_passed
} else {
0
};

// Make a method call. The target address will be rewritten once compiled.
let iseq_call = IseqCall::new(iseq);
let iseq_call = IseqCall::new(iseq, num_optionals_passed);
let dummy_ptr = cb.get_write_ptr().raw_ptr(cb);
jit.iseq_calls.push(iseq_call.clone());
let ret = asm.ccall_with_iseq_call(dummy_ptr, c_args, &iseq_call);
Expand Down Expand Up @@ -2129,7 +2140,8 @@ c_callable! {
// function_stub_hit_body() may allocate and call gc_validate_pc(), so we always set PC.
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
let iseq = iseq_call.iseq.get();
let pc = unsafe { rb_iseq_pc_at_idx(iseq, 0) }; // TODO: handle opt_pc once supported
let entry_insn_idxs = crate::hir::jit_entry_insns(iseq);
let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idxs[iseq_call.jit_entry_idx.to_usize()]) };
unsafe { rb_set_cfp_pc(cfp, pc) };

// JIT-to-JIT calls don't set SP or fill nils to uninitialized (non-argument) locals.
Expand Down Expand Up @@ -2189,13 +2201,8 @@ fn function_stub_hit_body(cb: &mut CodeBlock, iseq_call: &IseqCallRef) -> Result
debug!("{err:?}: gen_iseq failed: {}", iseq_get_location(iseq_call.iseq.get(), 0));
})?;

// We currently don't support JIT-to-JIT calls for ISEQs with optional arguments.
// So we only need to use jit_entry_ptrs[0] for now. TODO(Shopify/ruby#817): Support optional arguments.
let Some(&jit_entry_ptr) = jit_entry_ptrs.first() else {
return Err(CompileError::JitToJitOptional)
};

// Update the stub to call the code pointer
let jit_entry_ptr = jit_entry_ptrs[iseq_call.jit_entry_idx.to_usize()];
let code_addr = jit_entry_ptr.raw_ptr(cb);
let iseq = iseq_call.iseq.get();
iseq_call.regenerate(cb, |asm| {
Expand Down Expand Up @@ -2407,7 +2414,7 @@ impl Assembler {

/// Store info about a JIT entry point
pub struct JITEntry {
/// Index that corresponds to jit_entry_insns()
/// Index that corresponds to [crate::hir::jit_entry_insns]
jit_entry_idx: usize,
/// Position where the entry point starts
start_addr: Cell<Option<CodePtr>>,
Expand All @@ -2430,6 +2437,9 @@ pub struct IseqCall {
/// Callee ISEQ that start_addr jumps to
pub iseq: Cell<IseqPtr>,

/// Index that corresponds to [crate::hir::jit_entry_insns]
jit_entry_idx: u32,

/// Position where the call instruction starts
start_addr: Cell<Option<CodePtr>>,

Expand All @@ -2441,11 +2451,12 @@ pub type IseqCallRef = Rc<IseqCall>;

impl IseqCall {
/// Allocate a new IseqCall
fn new(iseq: IseqPtr) -> IseqCallRef {
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
let iseq_call = IseqCall {
iseq: Cell::new(iseq),
start_addr: Cell::new(None),
end_addr: Cell::new(None),
jit_entry_idx,
};
Rc::new(iseq_call)
}
Expand Down
94 changes: 94 additions & 0 deletions zjit/src/cruby_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@ pub fn init() -> Annotations {
annotate!(rb_cBasicObject, "initialize", inline_basic_object_initialize);
annotate!(rb_cInteger, "succ", inline_integer_succ);
annotate!(rb_cInteger, "^", inline_integer_xor);
annotate!(rb_cInteger, "==", inline_integer_eq);
annotate!(rb_cInteger, "+", inline_integer_plus);
annotate!(rb_cInteger, "-", inline_integer_minus);
annotate!(rb_cInteger, "*", inline_integer_mult);
annotate!(rb_cInteger, "/", inline_integer_div);
annotate!(rb_cInteger, "%", inline_integer_mod);
annotate!(rb_cInteger, "&", inline_integer_and);
annotate!(rb_cInteger, "|", inline_integer_or);
annotate!(rb_cInteger, ">", inline_integer_gt);
annotate!(rb_cInteger, ">=", inline_integer_ge);
annotate!(rb_cInteger, "<", inline_integer_lt);
annotate!(rb_cInteger, "<=", inline_integer_le);
annotate!(rb_cString, "to_s", inline_string_to_s, types::StringExact);
let thread_singleton = unsafe { rb_singleton_class(rb_cThread) };
annotate!(thread_singleton, "current", types::BasicObject, no_gc, leaf);
Expand Down Expand Up @@ -420,6 +432,84 @@ fn inline_integer_xor(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I
None
}

fn try_inline_fixnum_op(fun: &mut hir::Function, block: hir::BlockId, f: &dyn Fn(hir::InsnId, hir::InsnId) -> hir::Insn, bop: u32, left: hir::InsnId, right: hir::InsnId, state: hir::InsnId) -> Option<hir::InsnId> {
if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, INTEGER_REDEFINED_OP_FLAG) } {
// If the basic operation is already redefined, we cannot optimize it.
return None;
}
if fun.likely_a(left, types::Fixnum, state) && fun.likely_a(right, types::Fixnum, state) {
if bop == BOP_NEQ {
// For opt_neq, the interpreter checks that both neq and eq are unchanged.
fun.push_insn(block, hir::Insn::PatchPoint { invariant: hir::Invariant::BOPRedefined { klass: INTEGER_REDEFINED_OP_FLAG, bop: BOP_EQ }, state });
}
// Rely on the MethodRedefined PatchPoint for other bops.
let left = fun.coerce_to(block, left, types::Fixnum, state);
let right = fun.coerce_to(block, right, types::Fixnum, state);
return Some(fun.push_insn(block, f(left, right)));
}
None
}

fn inline_integer_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumEq { left, right }, BOP_EQ, recv, other, state)
}

fn inline_integer_plus(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumAdd { left, right, state }, BOP_PLUS, recv, other, state)
}

fn inline_integer_minus(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumSub { left, right, state }, BOP_MINUS, recv, other, state)
}

fn inline_integer_mult(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumMult { left, right, state }, BOP_MULT, recv, other, state)
}

fn inline_integer_div(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumDiv { left, right, state }, BOP_DIV, recv, other, state)
}

fn inline_integer_mod(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumMod { left, right, state }, BOP_MOD, recv, other, state)
}

fn inline_integer_and(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumAnd { left, right, }, BOP_AND, recv, other, state)
}

fn inline_integer_or(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumOr { left, right, }, BOP_OR, recv, other, state)
}

fn inline_integer_gt(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumGt { left, right }, BOP_GT, recv, other, state)
}

fn inline_integer_ge(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumGe { left, right }, BOP_GE, recv, other, state)
}

fn inline_integer_lt(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumLt { left, right }, BOP_LT, recv, other, state)
}

fn inline_integer_le(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumLe { left, right }, BOP_LE, recv, other, state)
}

fn inline_basic_object_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
let c_result = fun.push_insn(block, hir::Insn::IsBitEqual { left: recv, right: other });
Expand All @@ -429,6 +519,10 @@ fn inline_basic_object_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hi

fn inline_basic_object_neq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> {
let &[other] = args else { return None; };
let result = try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumNeq { left, right }, BOP_NEQ, recv, other, state);
if result.is_some() {
return result;
}
let recv_class = fun.type_of(recv).runtime_exact_ruby_class()?;
if !fun.assume_expected_cfunc(block, recv_class, ID!(eq), rb_obj_equal as _, state) {
return None;
Expand Down
Loading