8000 ZJIT: Support invokebuiltin opcodes by composerinteralia · Pull Request #13632 · ruby/ruby · GitHub
[go: up one dir, main page]

Skip to content

ZJIT: Support invokebuiltin opcodes #13632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 18, 2025
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
15 changes: 15 additions & 0 deletions test/ruby/test_zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ def test3 = baz(4, 1)
}
end

def test_invokebuiltin
assert_compiles '["."]', %q{
def test = Dir.glob(".")
test
}
end

def test_invokebuiltin_delegate
assert_compiles '[[], true]', %q{
def test = [].clone(freeze: true)
r = test
[r, r.frozen?]
}
end

def test_opt_plus_const
assert_compiles '3', %q{
def test = 1 + 2
Expand Down
21 changes: 21 additions & 0 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
Insn::IfFalse { val, target } => return gen_if_false(jit, asm, opnd!(val), target),
Insn::SendWithoutBlock { call_info, cd, state, self_val, args, .. } => gen_send_without_block(jit, asm, call_info, *cd, &function.frame_state(*state), self_val, args)?,
Insn::SendWithoutBlockDirect { cme, iseq, self_val, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(self_val), args, &function.frame_state(*state))?,
Insn::InvokeBuiltin { bf, args, state } => gen_invokebuiltin(jit, asm, &function.frame_state(*state), bf, args)?,
Insn::Return { val } => return Some(gen_return(asm, opnd!(val))?),
Insn::FixnumAdd { left, right, state } => gen_fixnum_add(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state))?,
Insn::FixnumSub { left, right, state } => gen_fixnum_sub(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state))?,
Expand Down Expand Up @@ -311,6 +312,26 @@ fn gen_get_constant_path(asm: &mut Assembler, ic: *const iseq_inline_constant_ca
val
}

fn gen_invokebuiltin(jit: &mut JITState, asm: &mut Assembler, state: &FrameState, bf: &rb_builtin_function, args: &Vec<InsnId>) -> Option<lir::Opnd> {
// 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)
if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) {
return None;
}

gen_save_pc(asm, state);

let mut cargs = vec![EC];
for &arg in args.iter() {
let opnd = jit.get_opnd(arg)?;
cargs.push(opnd);
}

let val = asm.ccall(bf.func_ptr as *const u8, cargs);

Some(val)
}

/// Lowering for [`Insn::CCall`]. This is a low-level raw call that doesn't know
/// anything about the callee, so handling for e.g. GC safety is dealt with elsewhere.
fn gen_ccall(jit: &mut JITState, asm: &mut Assembler, cfun: *const u8, args: &[InsnId]) -> Option<lir::Opnd> {
Expand Down
4 changes: 2 additions & 2 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,8 @@ pub mod test_utils {
}

/// Get the ISeq of a specified method
pub fn get_method_iseq(name: &str) -> *const rb_iseq_t {
let wrapped_iseq = eval(&format!("RubyVM::InstructionSequence.of(method(:{}))", name));
pub fn get_method_iseq(recv: &str, name: &str) -> *const rb_iseq_t {
let wrapped_iseq = eval(&format!("RubyVM::InstructionSequence.of({}.method(:{}))", recv, name));
unsafe { rb_iseqw_to_iseq(wrapped_iseq) }
}

Expand Down
93 changes: 88 additions & 5 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
use std::{
cell::RefCell,
collections::{HashMap, HashSet, VecDeque},
ffi::{c_int, c_void},
ffi::{c_int, c_void, CStr},
mem::{align_of, size_of},
ptr,
slice::Iter
Expand Down Expand Up @@ -477,6 +477,9 @@ pub enum Insn {
state: InsnId,
},

// Invoke a builtin function
InvokeBuiltin { bf: rb_builtin_function, args: Vec<InsnId>, state: InsnId },

/// Control flow instructions
Return { val: InsnId },

Expand Down Expand Up @@ -636,6 +639,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
}
Ok(())
}
Insn::InvokeBuiltin { bf, args, .. } => {
write!(f, "InvokeBuiltin {}", unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap())?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
Insn::Return { val } => { write!(f, "Return {val}") }
Insn::FixnumAdd { left, right, .. } => { write!(f, "FixnumAdd {left}, {right}") },
Insn::FixnumSub { left, right, .. } => { write!(f, "FixnumSub {left}, {right}") },
Expand Down Expand Up @@ -1027,6 +1037,7 @@ impl Function {
args: args.iter().map(|arg| find!(*arg)).collect(),
state: *state,
},
InvokeBuiltin { bf, args, state } => InvokeBuiltin { bf: *bf, args: find_vec!(*args), state: *state },
ArraySet { array, idx, val } => ArraySet { array: find!(*array), idx: *idx, val: find!(*val) },
ArrayDup { val , state } => ArrayDup { val: find!(*val), state: *state },
&HashDup { val , state } => HashDup { val: find!(val), state },
Expand Down Expand Up @@ -1123,6 +1134,7 @@ impl Function {
Insn::SendWithoutBlock { .. } => types::BasicObject,
Insn::SendWithoutBlockDirect { .. } => types::BasicObject,
Insn::Send { .. } => types::BasicObject,
Insn::InvokeBuiltin { .. } => types::BasicObject,
Insn::Defined { .. } => types::BasicObject,
Insn::DefinedI 8000 var { .. } => types::BasicObject,
Insn::GetConstantPath { .. } => types::BasicObject,
Expand Down Expand Up @@ -1727,6 +1739,10 @@ impl Function {
worklist.extend(args);
worklist.push_back(state);
}
Insn::InvokeBuiltin { args, state, .. } => {
worklist.extend(args);
worklist.push_back(state)
}
Insn::CCall { args, .. } => worklist.extend(args),
Insn::GetIvar { self_val, state, .. } | Insn::DefinedIvar { self_val, state, .. } => {
worklist.push_back(self_val);
Expand Down Expand Up @@ -2614,6 +2630,35 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let insn_id = fun.push_insn(block, Insn::NewRange { low, high, flag, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_invokebuiltin => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };

let mut args = vec![];
for _ in 0..bf.argc {
args.push(state.stack_pop()?);
}
args.push(self_param);
args.reverse();

let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_opt_invokebuiltin_delegate |
YARVINSN_opt_invokebuiltin_delegate_leave => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };
let index = get_arg(pc, 1).as_usize();
let argc = bf.argc as usize;

let mut args = vec![self_param];
for &local in state.locals().skip(index).take(argc) {
args.push(local);
}

let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id });
state.stack_push(insn_id);
}
_ => {
// Unknown opcode; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
Expand Down Expand Up @@ -2907,7 +2952,7 @@ mod tests {

#[track_caller]
fn assert_method_hir(method: &str, hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method));
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let function = iseq_to_hir(iseq).unwrap();
assert_function_hir(function, hir);
Expand All @@ -2934,7 +2979,7 @@ mod tests {

#[track_caller]
fn assert_method_hir_with_opcodes(method: &str, opcodes: &[u32], hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method));
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
for &opcode in opcodes {
assert!(iseq_contains_opcode(iseq, opcode), "iseq {method} does not contain {}", insn_name(opcode as usize));
}
Expand All @@ -2956,7 +3001,7 @@ mod tests {

#[track_caller]
fn assert_compile_fails(method: &str, reason: ParseError) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method));
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let result = iseq_to_hir(iseq);
assert!(result.is_err(), "Expected an error but succesfully compiled to HIR: {}", FunctionPrinter::without_snapshot(&result.unwrap()));
Expand Down Expand Up @@ -4180,6 +4225,44 @@ mod tests {
Return v10
"#]]);
}

#[test]
fn test_invokebuiltin_delegate_with_args() {
assert_method_hir_with_opcode("Float", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#"
fn Float:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject):
v6:BasicObject = InvokeBuiltin rb_f_float, v0, v1, v2
Jump bb1(v0, v1, v2, v3, v6)
bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject, v12:BasicObject):
Return v12
"#]]);
}

#[test]
fn test_invokebuiltin_delegate_without_args() {
assert_method_hir_with_opcode("class", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#"
fn class:
bb0(v0:BasicObject):
v3:BasicObject = InvokeBuiltin _bi20, v0
Jump bb1(v0, v3)
bb1(v5:BasicObject, v6:BasicObject):
Return v6
"#]]);
}

#[test]
fn test_invokebuiltin_with_args() {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("GC", "start"));
assert!(iseq_contains_opcode(iseq, YARVINSN_invokebuiltin), "iseq GC.start does not contain invokebuiltin");
let function = iseq_to_hir(iseq).unwrap();
assert_function_hir(function, expect![[r#"
fn start:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject):
v6:FalseClassExact = Const Value(false)
v8:BasicObject = InvokeBuiltin gc_start_internal, v0, v1, v2, v3, v6
Return v8
"#]]);
}
}

#[cfg(test)]
Expand All @@ -4190,7 +4273,7 @@ mod opt_tests {

#[track_caller]
fn assert_optimized_method_hir(method: &str, hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq(method));
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let mut function = iseq_to_hir(iseq).unwrap();
function.optimize();
Expand Down
Loading
0