From f717edca05fa42e122b7fb8148aa6a728b3aa582 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:42:20 +0900 Subject: [PATCH] ZJIT: Implement Polymorphic DefinedIvar (#16981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ZJIT: Implement Polymorphic Definedivar Closes: https://github.com/Shopify/ruby/issues/980 Build polymorphic shape/type branches for definedivar in HIR. For each profiled T_OBJECT shape, specialize defined?(`@ivar`) to a constant pushval or nil based on the compile-time ivar index check. DefinedIvar already profiles self on monomorphic shape guard failures for recompilation, so the recompiled HIR can use the collected polymorphic shapes while keeping a generic DefinedIvar fallback for misses and unsupported shapes. ## Benchmark ### lobsters Summary: ```diff - dynamic_definedivar_count: 1,294,854 ( 0.7%) + dynamic_definedivar_count: 503,058 ( 0.3%) ratio_in_zjit: 88.4% ``` ``` zjit-master: ruby 4.1.0dev (2026-05-23T00:08:49Z master 5855d61ee4) +ZJIT stats +PRISM [arm64-darwin25] zjit-polymorphic-definedivar: ruby 4.1.0dev (2026-05-23T07:11:22Z zjit-polymorphic-d.. f75a82e7f1) +ZJIT stats +PRISM [arm64-darwin25] last_commit=ZJIT: Implement Polymorphic Definedivar -------- ---------------- --------------------------------- ------------------------------------ ---------------------------------------- bench zjit-master (ms) zjit-polymorphic-definedivar (ms) zjit-polymorphic-definedivar 1st itr zjit-master/zjit-polymorphic-definedivar lobsters 529.9 ± 3.6% 504.2 ± 7.7% 1.148 1.051 -------- ---------------- --------------------------------- ------------------------------------ ---------------------------------------- ``` Full stats details below:
before patch ``` Top-2 not optimized method types for send (100.0% of total 65,120): null: 44,877 (68.9%) optimized: 20,243 (31.1%) Top-3 not optimized method types for send_without_block (100.0% of total 716,678): optimized_send: 711,905 (99.3%) optimized_block_call: 4,299 ( 0.6%) zsuper: 474 ( 0.1%) Top-1 not optimized method types for super (100.0% of total 3,678): attrset: 3,678 (100.0%) Top-1 instructions with uncategorized fallback reason (100.0% of total 65,868): opt_send_without_block: 65,868 (100.0%) Top-20 send fallback reasons (99.5% of total 22,386,632): invokeblock_not_specialized: 5,946,612 (26.6%) one_or_more_complex_arg_pass: 4,376,419 (19.5%) send_without_block_polymorphic: 3,748,855 (16.7%) send_without_block_no_profiles: 2,830,116 (12.6%) send_without_block_megamorphic: 1,108,467 ( 5.0%) sendforward_not_specialized: 971,957 ( 4.3%) send_polymorphic: 806,862 ( 3.6%) send_without_block_not_optimized_method_type_optimized: 716,204 ( 3.2%) super_polymorphic: 337,181 ( 1.5%) too_many_args_for_lir: 333,633 ( 1.5%) send_not_optimized_need_permission: 238,842 ( 1.1%) send_without_block_not_optimized_need_permission: 180,946 ( 0.8%) send_no_profiles: 177,008 ( 0.8%) argc_param_mismatch: 124,061 ( 0.6%) super_complex_args_pass: 89,280 ( 0.4%) invokesuperforward_not_specialized: 80,729 ( 0.4%) uncategorized: 65,868 ( 0.3%) send_not_optimized_method_type: 65,120 ( 0.3%) super_from_block: 43,185 ( 0.2%) obj_to_string_not_string: 42,731 ( 0.2%) Top-4 setivar fallback reasons (100.0% of total 4,182,333): not_monomorphic: 3,969,254 (94.9%) not_t_object: 119,505 ( 2.9%) complex: 93,131 ( 2.2%) new_shape_needs_extension: 443 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 11,716,846): not_monomorphic: 11,537,525 (98.5%) complex: 179,321 ( 1.5%) Top-3 definedivar fallback reasons (100.0% of total 1,294,854): not_monomorphic: 1,289,423 (99.6%) complex: 5,122 ( 0.4%) not_t_object: 309 ( 0.0%) Top-5 invokeblock handler (100.0% of total 6,968,323): monomorphic_ifunc: 4,143,418 (59.5%) monomorphic_other: 1,444,525 (20.7%) monomorphic_iseq: 865,749 (12.4%) polymorphic: 508,677 ( 7.3%) megamorphic: 5,954 ( 0.1%) Top-6 getblockparamproxy handler (100.0% of total 3,321,873): polymorphic: 2,370,458 (71.4%) nil: 492,185 (14.8%) iseq: 242,627 ( 7.3%) no_profiles: 168,826 ( 5.1%) proc: 40,245 ( 1.2%) megamorphic: 7,532 ( 0.2%) Top-7 popular complex argument-parameter features not optimized (100.0% of total 4,677,994): caller_blockarg: 3,092,361 (66.1%) param_forwardable: 697,044 (14.9%) param_rest: 409,311 ( 8.7%) caller_kwarg: 193,462 ( 4.1%) param_kwrest: 143,181 ( 3.1%) caller_kw_splat: 85,992 ( 1.8%) caller_splat: 56,643 ( 1.2%) Top-3 compile error reasons (100.0% of total 196,397): exception_handler: 192,000 (97.8%) native_stack_too_large: 4,342 ( 2.2%) validation_mismatched_block_arity: 55 ( 0.0%) Top-2 unhandled HIR insns (100.0% of total 229,881): throw: 194,104 (84.4%) invokebuiltin: 35,777 (15.6%) Top-19 side exit reasons (100.0% of total 8,637,525): guard_type_failure: 7,692,359 (89.1%) unhandled_hir_insn: 229,881 ( 2.7%) compile_error: 196,397 ( 2.3%) patchpoint_method_redefined: 118,680 ( 1.4%) block_param_proxy_fallback_miss: 112,551 ( 1.3%) no_profile_send: 93,334 ( 1.1%) block_param_proxy_not_nil: 75,195 ( 0.9%) patchpoint_stable_constant_names: 55,819 ( 0.6%) patchpoint_no_singleton_class: 19,352 ( 0.2%) unhandled_block_arg: 14,541 ( 0.2%) block_param_proxy_not_iseq_or_ifunc: 13,401 ( 0.2%) fixnum_lshift_overflow: 10,165 ( 0.1%) guard_less_failure: 3,120 ( 0.0%) guard_shape_failure: 1,302 ( 0.0%) guard_greater_eq_failure: 941 ( 0.0%) guard_super_method_entry: 407 ( 0.0%) interrupt: 49 ( 0.0%) obj_to_string_fallback: 30 ( 0.0%) guard_not_shared_failure: 1 ( 0.0%) send_count: 182,469,185 dynamic_send_count: 22,386,632 (12.3%) optimized_send_count: 160,082,553 (87.7%) dynamic_setivar_count: 4,182,333 ( 2.3%) dynamic_getivar_count: 11,716,846 ( 6.4%) dynamic_definedivar_count: 1,294,854 ( 0.7%) iseq_optimized_send_count: 54,154,353 (29.7%) inline_cfunc_optimized_send_count: 74,812,159 (41.0%) inline_iseq_optimized_send_count: 6,346,705 ( 3.5%) non_variadic_cfunc_optimized_send_count: 13,789,249 ( 7.6%) variadic_cfunc_optimized_send_count: 10,980,087 ( 6.0%) compiled_iseq_count: 6,166 compiled_side_exit_count: 80,485 failed_iseq_count: 3 compile_time: 2,462ms compile_side_exit_time: 113ms compile_side_exit_time_ratio: 4.6% compile_hir_time: 798ms compile_hir_build_time: 238ms compile_hir_strength_reduce_time: 322ms compile_hir_canonicalize_time: 49ms compile_hir_fold_constants_time: 37ms compile_hir_clean_cfg_time: 26ms compile_hir_eliminate_dead_code_time: 30ms compile_lir_time: 1,525ms profile_time: 39ms gc_time: 33ms invalidation_time: 14ms vm_write_jit_frame_count: 137,433,269 vm_write_sp_count: 137,433,269 vm_write_locals_count: 131,272,119 vm_write_stack_count: 131,272,119 vm_write_to_parent_iseq_local_count: 688,492 guard_type_count: 145,711,992 guard_type_exit_ratio: 5.3% guard_shape_count: 36,890,099 guard_shape_exit_ratio: 0.0% load_field_count: 210,155,590 store_field_count: 15,758,742 side_exit_size: 13,563,708 code_region_bytes: 33,505,280 side_exit_size_ratio: 40.5% zjit_alloc_bytes: 22,783,024 total_mem_bytes: 56,288,304 side_exit_count: 8,637,525 total_insn_count: 995,180,861 vm_insn_count: 115,515,525 zjit_insn_count: 879,665,336 ratio_in_zjit: 88.4% ```
after patch ``` Top-2 not optimized method types for send (100.0% of total 65,119): null: 44,877 (68.9%) optimized: 20,242 (31.1%) Top-3 not optimized method types for send_without_block (100.0% of total 716,636): optimized_send: 711,869 (99.3%) optimized_block_call: 4,293 ( 0.6%) zsuper: 474 ( 0.1%) Top-1 not optimized method types for super (100.0% of total 3,676): attrset: 3,676 (100.0%) Top-1 instructions with uncategorized fallback reason (100.0% of total 65,866): opt_send_without_block: 65,866 (100.0%) Top-20 send fallback reasons (99.5% of total 22,409,237): invokeblock_not_specialized: 5,946,405 (26.5%) one_or_more_complex_arg_pass: 4,376,457 (19.5%) send_without_block_polymorphic: 3,775,975 (16.9%) send_without_block_no_profiles: 2,830,029 (12.6%) send_without_block_megamorphic: 1,104,527 ( 4.9%) sendforward_not_specialized: 971,924 ( 4.3%) send_polymorphic: 806,852 ( 3.6%) send_without_block_not_optimized_method_type_optimized: 716,162 ( 3.2%) super_polymorphic: 337,178 ( 1.5%) too_many_args_for_lir: 333,625 ( 1.5%) send_not_optimized_need_permission: 238,842 ( 1.1%) send_without_block_not_optimized_need_permission: 180,949 ( 0.8%) send_no_profiles: 176,799 ( 0.8%) argc_param_mismatch: 124,064 ( 0.6%) super_complex_args_pass: 89,280 ( 0.4%) invokesuperforward_not_specialized: 80,723 ( 0.4%) uncategorized: 65,866 ( 0.3%) send_not_optimized_method_type: 65,119 ( 0.3%) super_from_block: 43,182 ( 0.2%) obj_to_string_not_string: 42,725 ( 0.2%) Top-4 setivar fallback reasons (100.0% of total 4,182,216): not_monomorphic: 3,969,159 (94.9%) not_t_object: 119,484 ( 2.9%) complex: 93,131 ( 2.2%) new_shape_needs_extension: 442 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 11,716,471): not_monomorphic: 11,537,150 (98.5%) complex: 179,321 ( 1.5%) Top-3 definedivar fallback reasons (100.0% of total 503,058): not_monomorphic: 498,173 (99.0%) complex: 4,576 ( 0.9%) not_t_object: 309 ( 0.1%) Top-5 invokeblock handler (100.0% of total 6,968,099): monomorphic_ifunc: 4,143,324 (59.5%) monomorphic_other: 1,444,457 (20.7%) monomorphic_iseq: 865,705 (12.4%) polymorphic: 508,659 ( 7.3%) megamorphic: 5,954 ( 0.1%) Top-6 getblockparamproxy handler (100.0% of total 3,321,803): polymorphic: 2,370,425 (71.4%) nil: 492,172 (14.8%) iseq: 242,810 ( 7.3%) no_profiles: 168,625 ( 5.1%) proc: 40,239 ( 1.2%) megamorphic: 7,532 ( 0.2%) Top-7 popular complex argument-parameter features not optimized (100.0% of total 4,678,011): caller_blockarg: 3,092,480 (66.1%) param_forwardable: 697,011 (14.9%) param_rest: 409,285 ( 8.7%) caller_kwarg: 193,450 ( 4.1%) param_kwrest: 143,179 ( 3.1%) caller_kw_splat: 85,971 ( 1.8%) caller_splat: 56,635 ( 1.2%) Top-3 compile error reasons (100.0% of total 196,386): exception_handler: 191,989 (97.8%) native_stack_too_large: 4,342 ( 2.2%) validation_mismatched_block_arity: 55 ( 0.0%) Top-2 unhandled HIR insns (100.0% of total 229,876): throw: 194,101 (84.4%) invokebuiltin: 35,775 (15.6%) Top-19 side exit reasons (100.0% of total 8,638,646): guard_type_failure: 7,693,566 (89.1%) unhandled_hir_insn: 229,876 ( 2.7%) compile_error: 196,386 ( 2.3%) patchpoint_method_redefined: 118,676 ( 1.4%) block_param_proxy_fallback_miss: 112,548 ( 1.3%) no_profile_send: 93,330 ( 1.1%) block_param_proxy_not_nil: 75,195 ( 0.9%) patchpoint_stable_constant_names: 55,792 ( 0.6%) patchpoint_no_singleton_class: 19,326 ( 0.2%) unhandled_block_arg: 14,540 ( 0.2%) block_param_proxy_not_iseq_or_ifunc: 13,401 ( 0.2%) fixnum_lshift_overflow: 10,165 ( 0.1%) guard_less_failure: 3,119 ( 0.0%) guard_shape_failure: 1,302 ( 0.0%) guard_greater_eq_failure: 941 ( 0.0%) guard_super_method_entry: 407 ( 0.0%) interrupt: 45 ( 0.0%) obj_to_string_fallback: 30 ( 0.0%) guard_not_shared_failure: 1 ( 0.0%) send_count: 182,477,537 dynamic_send_count: 22,409,237 (12.3%) optimized_send_count: 160,068,300 (87.7%) dynamic_setivar_count: 4,182,216 ( 2.3%) dynamic_getivar_count: 11,716,471 ( 6.4%) dynamic_definedivar_count: 503,058 ( 0.3%) iseq_optimized_send_count: 54,138,292 (29.7%) inline_cfunc_optimized_send_count: 74,815,196 (41.0%) inline_iseq_optimized_send_count: 6,346,484 ( 3.5%) non_variadic_cfunc_optimized_send_count: 13,788,597 ( 7.6%) variadic_cfunc_optimized_send_count: 10,979,731 ( 6.0%) compiled_iseq_count: 6,167 compiled_side_exit_count: 80,471 failed_iseq_count: 3 compile_time: 2,711ms compile_side_exit_time: 125ms compile_side_exit_time_ratio: 4.6% compile_hir_time: 870ms compile_hir_build_time: 266ms compile_hir_strength_reduce_time: 347ms compile_hir_canonicalize_time: 52ms compile_hir_fold_constants_time: 40ms compile_hir_clean_cfg_time: 27ms compile_hir_eliminate_dead_code_time: 32ms compile_lir_time: 1,680ms profile_time: 48ms gc_time: 32ms invalidation_time: 15ms vm_write_jit_frame_count: 137,425,789 vm_write_sp_count: 137,425,789 vm_write_locals_count: 131,264,963 vm_write_stack_count: 131,264,963 vm_write_to_parent_iseq_local_count: 688,457 guard_type_count: 146,412,455 guard_type_exit_ratio: 5.3% guard_shape_count: 36,937,934 guard_shape_exit_ratio: 0.0% load_field_count: 211,394,883 store_field_count: 15,778,119 side_exit_size: 13,563,624 code_region_bytes: 33,505,280 side_exit_size_ratio: 40.5% zjit_alloc_bytes: 22,870,223 total_mem_bytes: 56,375,503 side_exit_count: 8,638,646 total_insn_count: 995,144,390 vm_insn_count: 115,472,694 zjit_insn_count: 879,671,696 ratio_in_zjit: 88.4% ```
* ZJIT: Add TODO for deduplicating DefinedIvar specialization * ZJIT: Add more DefinedIvar HIR tests Cover unsupported receiver profiles and a mixed polymorphic profile with a generic DefinedIvar fallback. * ZJIT: Add more DefinedIvar polymorphic HIR test Cover polymorphic definedivar profiles with: - complex path - non-T_OBJECT path --- zjit/src/hir.rs | 65 +++++++++- zjit/src/hir/opt_tests.rs | 267 +++++++++++++++++++++++++++++++++++++- 2 files changed, 322 insertions(+), 10 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f5363aebe261bb..02e3759cd69d9e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4497,8 +4497,6 @@ impl Function { self.push_insn_id(block, insn_id); continue; } if self.policy.no_side_exits { - // TODO: Support polymorphic DefinedIvar shape-specialized paths. - // https://github.com/Shopify/ruby/issues/980 // On the final version, keep the DefinedIvar fallback instead of another shape guard. self.push_insn_id(block, insn_id); continue; } @@ -7155,7 +7153,68 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // (ID id, IVC ic, VALUE pushval) let id = ID(get_arg(pc, 0).as_u64()); let pushval = get_arg(pc, 2); - state.stack_push(fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id })); + if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) { + self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id }); + let rbasic_flags = fun.load_rbasic_flags(block, self_param); + let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); + let join_param = fun.push_insn(join_block, Insn::Param); + // Dedup by expected shape and type so objects with different classes + // but the same shape can share code. + let mut seen_shape_and_flags = Vec::with_capacity(summary.buckets().len()); + for &profiled_type in summary.buckets() { + // End of the buckets + if profiled_type.is_empty() { break; } + // Runtime immediates cannot pass the HeapBasicObject guard, so don't + // generate unreachable shape branches for profiled immediate buckets. + if profiled_type.flags().is_immediate() { continue; } + // Class/module/T_DATA ivars use different storage rules. + // Let the fallthrough DefinedIvar handle these. + if !profiled_type.flags().is_t_object() { continue; } + let expected_shape = profiled_type.shape(); + let (expected_rbasic_flags, rbasic_flags_mask) = profiled_type.rbasic_flags_and_mask(); + assert!(expected_shape.is_valid()); + // Too-complex shapes use hash tables for ivars; + // rb_shape_get_iv_index doesn't work for them. + // Let the fallthrough DefinedIvar handle these. + if expected_shape.is_complex() { continue; } + if seen_shape_and_flags.contains(&expected_rbasic_flags) { continue; } + seen_shape_and_flags.push(expected_rbasic_flags); + let rbasic_flags_mask = fun.push_insn(block, Insn::Const { val: Const::CUInt64(rbasic_flags_mask) }); + // The expected shape can change over run, so we put it + // as a pointer to keep it stable in snapshot tests. + let expected_rbasic_flags = fun.push_insn(block, Insn::Const { val: Const::CPtr(ptr::without_provenance(expected_rbasic_flags.to_usize())) }); + let expected_rbasic_flags = fun.push_insn(block, Insn::RefineType { val: expected_rbasic_flags, new_type: types::CUInt64 }); + let masked = fun.push_insn(block, Insn::IntAnd { left: rbasic_flags, right: rbasic_flags_mask}); + let has_shape_and_type = fun.push_insn(block, Insn::IsBitEqual { left: masked, right: expected_rbasic_flags }); + let iftrue_block = fun.new_block(insn_idx); + let target = BranchEdge { target: iftrue_block, args: vec![] }; + let fall_through = fun.new_block(insn_idx); + + fun.push_insn(block, Insn::CondBranch { val: has_shape_and_type, + if_true: target, + if_false: BranchEdge { target: fall_through, args: vec![] } + }); + + block = fall_through; + let mut ivar_index: attr_index_t = 0; + let result = if unsafe { rb_shape_get_iv_index(expected_shape.0, id, &mut ivar_index) } { + fun.push_insn(iftrue_block, Insn::Const { val: Const::Value(pushval) }) + } else { + fun.push_insn(iftrue_block, Insn::Const { val: Const::Value(Qnil) }) + }; + fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + } + // In the fallthrough case, do a generic interpreter definedivar and then join. + let result = fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id }); + fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + state.stack_push(join_param); + block = join_block; + } else { + // TODO: Handle monomorphic definedivar specialization here too, including the + // no_side_exits policy, so optimize_getivar doesn't need a separate DefinedIvar + // path. Unlike GetIvar, DefinedIvar isn't emitted by later lowering passes. + state.stack_push(fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id })); + } } YARVINSN_checkkeyword => { // When a keyword is unspecified past index 32, a hash will be used instead. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3212390ecf948f..74b951d2e59410 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5638,7 +5638,40 @@ mod hir_opt_tests { } #[test] - fn test_dont_specialize_definedivar_with_t_data() { + fn test_dont_specialize_definedivar_with_immediate() { + eval(" + module M + def test = defined?(@a) + end + + class Integer + include M + end + + 1.test + 2.test + TEST = M.instance_method(:test) + "); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:StringExact|NilClass = DefinedIvar v6, :@a + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_dont_specialize_definedivar_with_t_struct() { + // Range is T_STRUCT (not T_OBJECT): falls back to DefinedIvar. eval(" class C < Range def test = defined?(@a) @@ -5666,7 +5699,7 @@ mod hir_opt_tests { } #[test] - fn test_dont_specialize_polymorphic_definedivar() { + fn test_optimize_definedivar_polymorphic() { set_call_threshold(3); eval(" class C @@ -5691,9 +5724,206 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - v10:StringExact|NilClass = DefinedIvar v6, :@a + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:NilClass = Const Value(nil) + Jump bb4(v19) + bb6(): + v21:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v22:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v23 = RefineType v22, CUInt64 + v24:CInt64 = IntAnd v11, v21 + v25:CBool = IsBitEqual v24, v23 + CondBranch v25, bb7(), bb8() + bb7(): + v27:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v27) + bb8(): + v29:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v29) + bb4(v12:StringExact|NilClass): CheckInterrupts - Return v10 + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_immediate() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class Integer + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + obj.test + 1.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_t_struct() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class D < Range + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + range = D.new 0, 1 + range.instance_variable_set(:@a, 1) + + obj.test + range.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_complex_shape() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class D + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + complex = D.new + (0..1000).each do |i| + complex.instance_variable_set(:"@v#{i}", i) + end + (0..1000).each do |i| + complex.remove_instance_variable(:"@v#{i}") + end + + obj.test + complex.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 "); } @@ -8010,7 +8240,7 @@ mod hir_opt_tests { fn test_definedivar_shape_guard_recompile() { // Call with one shape to compile, then call with a different shape to // trigger shape guard exits and recompilation. On the recompiled version, - // DefinedIvar stays as a C call fallback to avoid more shape guard exits. + // DefinedIvar uses polymorphic fast paths plus a C call fallback. eval(" class C def initialize(extra = false) @@ -8038,9 +8268,32 @@ mod hir_opt_tests { v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:HeapBasicObject): - v10:StringExact|NilClass = DefinedIvar v6, :@foo + v11:CUInt64 = LoadField v6, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v22:CPtr[CPtr(0x1010)] = Const CPtr(0x1010) + v23 = RefineType v22, CUInt64 + v24:CInt64 = IntAnd v11, v21 + v25:CBool = IsBitEqual v24, v23 + CondBranch v25, bb7(), bb8() + bb7(): + v27:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v27) + bb8(): + v29:StringExact|NilClass = DefinedIvar v6, :@foo + Jump bb4(v29) + bb4(v12:StringExact|NilClass): CheckInterrupts - Return v10 + Return v12 "); }