From 70378db2fff0b1bc39a7d2b011da90486579fb0e Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 15 Aug 2025 11:09:13 -0400 Subject: [PATCH 01/17] Remove impossible case in rb_raw_obj_info_buitin_type for array Since we handle embedded arrays in the if statement above, we don't need to handle it here. --- gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gc.c b/gc.c index 160398f9a6a661..aa3140ed473462 100644 --- a/gc.c +++ b/gc.c @@ -4709,7 +4709,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU C(ARY_EMBED_P(obj), "E"), C(ARY_SHARED_P(obj), "S"), RARRAY_LEN(obj), - ARY_EMBED_P(obj) ? -1L : RARRAY(obj)->as.heap.aux.capa, + RARRAY(obj)->as.heap.aux.capa, (void *)RARRAY_CONST_PTR(obj)); } break; From ef3fdb04d2be6e0337bea2ca84c7158d32b89719 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 15 Aug 2025 11:19:11 -0400 Subject: [PATCH 02/17] Move flags for arrays out of if statements in rb_raw_obj_info_buitin_type --- gc.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gc.c b/gc.c index aa3140ed473462..c78ef4de113ef5 100644 --- a/gc.c +++ b/gc.c @@ -4698,19 +4698,21 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU APPEND_S("shared -> "); rb_raw_obj_info(BUFF_ARGS, ARY_SHARED_ROOT(obj)); } - else if (ARY_EMBED_P(obj)) { - APPEND_F("[%s%s] len: %ld (embed)", - C(ARY_EMBED_P(obj), "E"), - C(ARY_SHARED_P(obj), "S"), - RARRAY_LEN(obj)); - } else { - APPEND_F("[%s%s] len: %ld, capa:%ld ptr:%p", + APPEND_F("[%s%s] ", C(ARY_EMBED_P(obj), "E"), - C(ARY_SHARED_P(obj), "S"), - RARRAY_LEN(obj), - RARRAY(obj)->as.heap.aux.capa, - (void *)RARRAY_CONST_PTR(obj)); + C(ARY_SHARED_P(obj), "S")); + + if (ARY_EMBED_P(obj)) { + APPEND_F("len: %ld (embed)", + RARRAY_LEN(obj)); + } + else { + APPEND_F("len: %ld, capa:%ld ptr:%p", + RARRAY_LEN(obj), + RARRAY(obj)->as.heap.aux.capa, + (void *)RARRAY_CONST_PTR(obj)); + } } break; case T_STRING: { From 094fa3ed0922901b5771fc0e1651ecb161b5cdb6 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 15 Aug 2025 11:21:09 -0400 Subject: [PATCH 03/17] Output array shared root flag in rb_raw_obj_info_buitin_type --- gc.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gc.c b/gc.c index c78ef4de113ef5..c2fc681253aa36 100644 --- a/gc.c +++ b/gc.c @@ -4699,9 +4699,10 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU rb_raw_obj_info(BUFF_ARGS, ARY_SHARED_ROOT(obj)); } else { - APPEND_F("[%s%s] ", - C(ARY_EMBED_P(obj), "E"), - C(ARY_SHARED_P(obj), "S")); + APPEND_F("[%s%s%s] ", + C(ARY_EMBED_P(obj), "E"), + C(ARY_SHARED_P(obj), "S"), + C(ARY_SHARED_ROOT_P(obj), "R")); if (ARY_EMBED_P(obj)) { APPEND_F("len: %ld (embed)", From cb31be5d6a6032da777a8c311469e40c7e26caab Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Mon, 18 Aug 2025 23:59:18 +0900 Subject: [PATCH 04/17] ZJIT: Codegen for NewHash (#14059) This is my first contribution to ZJIT. Co-authored-by: Takashi Kokubun Co-authored-by: Max Bernstein --- test/.excludes-zjit/TestRubyOptimization.rb | 1 + test/ruby/test_zjit.rb | 110 ++++++++++++++++++++ zjit/src/codegen.rs | 61 ++++++++++- 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 test/.excludes-zjit/TestRubyOptimization.rb diff --git a/test/.excludes-zjit/TestRubyOptimization.rb b/test/.excludes-zjit/TestRubyOptimization.rb new file mode 100644 index 00000000000000..5361ab02c7a8af --- /dev/null +++ b/test/.excludes-zjit/TestRubyOptimization.rb @@ -0,0 +1 @@ +exclude(:test_side_effect_in_popped_splat, 'Test fails with ZJIT due to locals invalidation') diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index d30af737c3bde8..485fc403a843d0 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -509,6 +509,116 @@ def test(a, b) = a >= b }, insns: [:opt_ge], call_threshold: 2 end + def test_new_hash_empty + assert_compiles '{}', %q{ + def test = {} + test + }, insns: [:newhash] + end + + def test_new_hash_nonempty + assert_compiles '{"key" => "value", 42 => 100}', %q{ + def test + key = "key" + value = "value" + num = 42 + result = 100 + {key => value, num => result} + end + test + }, insns: [:newhash] + end + + def test_new_hash_single_key_value + assert_compiles '{"key" => "value"}', %q{ + def test = {"key" => "value"} + test + }, insns: [:newhash] + end + + def test_new_hash_with_computation + assert_compiles '{"sum" => 5, "product" => 6}', %q{ + def test(a, b) + {"sum" => a + b, "product" => a * b} + end + test(2, 3) + }, insns: [:newhash] + end + + def test_new_hash_with_user_defined_hash_method + assert_runs 'true', %q{ + class CustomKey + attr_reader :val + + def initialize(val) + @val = val + end + + def hash + @val.hash + end + + def eql?(other) + other.is_a?(CustomKey) && @val == other.val + end + end + + def test + key = CustomKey.new("key") + hash = {key => "value"} + hash[key] == "value" + end + test + } + end + + def test_new_hash_with_user_hash_method_exception + assert_runs 'RuntimeError', %q{ + class BadKey + def hash + raise "Hash method failed!" + end + end + + def test + key = BadKey.new + {key => "value"} + end + + begin + test + rescue => e + e.class + end + } + end + + def test_new_hash_with_user_eql_method_exception + assert_runs 'RuntimeError', %q{ + class BadKey + def hash + 42 + end + + def eql?(other) + raise "Eql method failed!" + end + end + + def test + key1 = BadKey.new + key2 = BadKey.new + {key1 => "value1", key2 => "value2"} + end + + begin + test + rescue => e + e.class + end + } + end + def test_opt_hash_freeze assert_compiles '{}', <<~RUBY, insns: [:opt_hash_freeze] def test = {}.freeze diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a58950ab9acb4b..090f843115bd45 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1,6 +1,6 @@ use std::cell::Cell; use std::rc::Rc; -use std::ffi::{c_int, c_void}; +use std::ffi::{c_int, c_long, c_void}; use crate::asm::Label; use crate::backend::current::{Reg, ALLOC_REGS}; @@ -332,6 +332,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio let out_opnd = match insn { Insn::Const { val: Const::Value(val) } => gen_const(*val), Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)), + Insn::NewHash { elements, state } => gen_new_hash(jit, asm, elements, &function.frame_state(*state))?, Insn::NewRange { low, high, flag, state } => gen_new_range(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), @@ -388,7 +389,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio | Insn::FixnumDiv { .. } | Insn::FixnumMod { .. } | Insn::HashDup { .. } - | Insn::NewHash { .. } | Insn::Send { .. } | Insn::Throw { .. } | Insn::ToArray { .. } @@ -974,7 +974,7 @@ fn gen_new_array( ) -> lir::Opnd { gen_prepare_call_with_gc(asm, state); - let length: ::std::os::raw::c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); + let length: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); let new_array = asm_ccall!(asm, rb_ary_new_capa, length.into()); @@ -985,6 +985,61 @@ fn gen_new_array( new_array } +/// Compile a new hash instruction +fn gen_new_hash( + jit: &mut JITState, + asm: &mut Assembler, + elements: &Vec<(InsnId, InsnId)>, + state: &FrameState, +) -> Option { + gen_prepare_non_leaf_call(jit, asm, state)?; + + let cap: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); + let new_hash = asm_ccall!(asm, rb_hash_new_with_size, lir::Opnd::Imm(cap)); + + if !elements.is_empty() { + let mut pairs = Vec::new(); + for (key_id, val_id) in elements.iter() { + let key = jit.get_opnd(*key_id)?; + let val = jit.get_opnd(*val_id)?; + pairs.push(key); + pairs.push(val); + } + + let n = pairs.len(); + + // Calculate the compile-time NATIVE_STACK_PTR offset from NATIVE_BASE_PTR + // At this point, frame_setup(&[], jit.c_stack_slots) has been called, + // which allocated aligned_stack_bytes(jit.c_stack_slots) on the stack + let frame_size = aligned_stack_bytes(jit.c_stack_slots); + let allocation_size = aligned_stack_bytes(n); + + asm_comment!(asm, "allocate {} bytes on C stack for {} hash elements", allocation_size, n); + asm.sub_into(NATIVE_STACK_PTR, allocation_size.into()); + + // Calculate the total offset from NATIVE_BASE_PTR to our buffer + let total_offset_from_base = (frame_size + allocation_size) as i32; + + for (idx, &pair_opnd) in pairs.iter().enumerate() { + let slot_offset = -total_offset_from_base + (idx as i32 * SIZEOF_VALUE_I32); + asm.mov( + Opnd::mem(VALUE_BITS, NATIVE_BASE_PTR, slot_offset), + pair_opnd + ); + } + + let argv = asm.lea(Opnd::mem(64, NATIVE_BASE_PTR, -total_offset_from_base)); + + let argc = (elements.len() * 2) as ::std::os::raw::c_long; + asm_ccall!(asm, rb_hash_bulk_insert, lir::Opnd::Imm(argc), argv, new_hash); + + asm_comment!(asm, "restore C stack pointer"); + asm.add_into(NATIVE_STACK_PTR, allocation_size.into()); + } + + Some(new_hash) +} + /// Compile a new range instruction fn gen_new_range( asm: &mut Assembler, From d330bcfd49545e586486f773b74be43ee2197192 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 18 Aug 2025 09:01:09 -0700 Subject: [PATCH 05/17] ZJIT: Make sure output operands are not VRegs (#14188) Make LIR SSA. --- zjit/src/backend/lir.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 54bef9d9255184..460e2719dd9a38 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1684,6 +1684,7 @@ impl Assembler { } pub fn cpop_into(&mut self, opnd: Opnd) { + assert!(matches!(opnd, Opnd::Reg(_)), "Destination of cpop_into must be a register, got: {opnd:?}"); self.push_insn(Insn::CPopInto(opnd)); } @@ -1831,6 +1832,7 @@ impl Assembler { } pub fn lea_into(&mut self, out: Opnd, opnd: Opnd) { + assert!(matches!(out, Opnd::Reg(_)), "Destination of lea_into must be a register, got: {out:?}"); self.push_insn(Insn::Lea { opnd, out }); } @@ -1856,7 +1858,7 @@ impl Assembler { } pub fn load_into(&mut self, dest: Opnd, opnd: Opnd) { - assert!(matches!(dest, Opnd::Reg(_) | Opnd::VReg{..}), "Destination of load_into must be a register"); + assert!(matches!(dest, Opnd::Reg(_)), "Destination of load_into must be a register, got: {dest:?}"); match (dest, opnd) { (Opnd::Reg(dest), Opnd::Reg(opnd)) if dest == opnd => {}, // skip if noop _ => self.push_insn(Insn::LoadInto { dest, opnd }), @@ -1882,6 +1884,7 @@ impl Assembler { } pub fn mov(&mut self, dest: Opnd, src: Opnd) { + assert!(!matches!(dest, Opnd::VReg { .. }), "Destination of mov must not be Opnd::VReg, got: {dest:?}"); self.push_insn(Insn::Mov { dest, src }); } @@ -1919,6 +1922,7 @@ impl Assembler { } 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 }); } From 5b3c87a0b006390cf23c7b53c1688a513a6860b3 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 15 Aug 2025 10:23:25 -0400 Subject: [PATCH 06/17] ZJIT: Make jit.get_opnd noisily fail We have a verifier that runs in debug mode that should prevent this. Simplify a bunch of call sites. --- zjit/src/codegen.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 090f843115bd45..2a2b4d1ec730e3 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -46,12 +46,8 @@ impl JITState { } /// Retrieve the output of a given instruction that has been compiled - fn get_opnd(&self, insn_id: InsnId) -> Option { - let opnd = self.opnds[insn_id.0]; - if opnd.is_none() { - debug!("Failed to get_opnd({insn_id})"); - } - opnd + fn get_opnd(&self, insn_id: InsnId) -> lir::Opnd { + self.opnds[insn_id.0].expect(&format!("Failed to get_opnd({insn_id})")) } /// Find or create a label for a given BlockId @@ -313,14 +309,14 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio // Convert InsnId to lir::Opnd macro_rules! opnd { ($insn_id:ident) => { - jit.get_opnd($insn_id.clone())? + jit.get_opnd($insn_id.clone()) }; } macro_rules! opnds { ($insn_ids:ident) => { { - Option::from_iter($insn_ids.iter().map(|insn_id| jit.get_opnd(*insn_id)))? + $insn_ids.iter().map(|insn_id| jit.get_opnd(*insn_id)).collect::>() } }; } @@ -514,12 +510,12 @@ fn gen_setlocal_with_ep(asm: &mut Assembler, jit: &JITState, function: &Function // we can skip the write barrier. if function.type_of(val).is_immediate() { let offset = -(SIZEOF_VALUE_I32 * i32::try_from(local_ep_offset).ok()?); - asm.mov(Opnd::mem(64, ep, offset), jit.get_opnd(val)?); + asm.mov(Opnd::mem(64, ep, offset), jit.get_opnd(val)); } else { // We're potentially writing a reference to an IMEMO/env object, // so take care of the write barrier with a function. let local_index = c_int::try_from(local_ep_offset).ok().and_then(|idx| idx.checked_mul(-1))?; - asm_ccall!(asm, rb_vm_env_write, ep, local_index.into(), jit.get_opnd(val)?); + asm_ccall!(asm, rb_vm_env_write, ep, local_index.into(), jit.get_opnd(val)); } Some(()) } @@ -742,11 +738,11 @@ fn gen_branch_params(jit: &mut JITState, asm: &mut Assembler, branch: &BranchEdg match param_opnd(idx) { Opnd::Reg(reg) => { // If a parameter is a register, we need to parallel-move it - moves.push((reg, jit.get_opnd(arg)?)); + moves.push((reg, jit.get_opnd(arg))); }, param => { // If a parameter is memory, we set it beforehand - asm.mov(param, jit.get_opnd(arg)?); + asm.mov(param, jit.get_opnd(arg)); } } } @@ -1000,8 +996,8 @@ fn gen_new_hash( if !elements.is_empty() { let mut pairs = Vec::new(); for (key_id, val_id) in elements.iter() { - let key = jit.get_opnd(*key_id)?; - let val = jit.get_opnd(*val_id)?; + let key = jit.get_opnd(*key_id); + let val = jit.get_opnd(*val_id); pairs.push(key); pairs.push(val); } @@ -1280,7 +1276,7 @@ fn gen_spill_locals(jit: &JITState, asm: &mut Assembler, state: &FrameState) -> // TODO: Avoid spilling locals that have been spilled before and not changed. asm_comment!(asm, "spill locals"); for (idx, &insn_id) in state.locals().enumerate() { - asm.mov(Opnd::mem(64, SP, (-local_idx_to_ep_offset(jit.iseq, idx) - 1) * SIZEOF_VALUE_I32), jit.get_opnd(insn_id)?); + asm.mov(Opnd::mem(64, SP, (-local_idx_to_ep_offset(jit.iseq, idx) - 1) * SIZEOF_VALUE_I32), jit.get_opnd(insn_id)); } Some(()) } @@ -1291,7 +1287,7 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, state: &FrameState) -> O // gen_send_without_block_direct() spills stack slots above SP for arguments. asm_comment!(asm, "spill stack"); for (idx, &insn_id) in state.stack().enumerate() { - asm.mov(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), jit.get_opnd(insn_id)?); + asm.mov(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), jit.get_opnd(insn_id)); } Some(()) } @@ -1416,12 +1412,12 @@ fn side_exit(jit: &mut JITState, state: &FrameState, reason: SideExitReason) -> fn build_side_exit(jit: &mut JITState, state: &FrameState, reason: SideExitReason, label: Option