From e74823a08098ef87c7a2fc3a35647c4c4467ca40 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 24 Mar 2026 09:47:50 -0700 Subject: ZJIT: Skip too-complex shapes in polymorphic getivar (#16526) Too-complex shapes use hash tables for ivar storage, and rb_shape_get_iv_index() doesn't work for them (it asserts in debug builds). Without this check, the polymorphic getinstancevariable optimization incorrectly returns nil for ivars on too-complex objects. Let the fallthrough GetIvar handle these shapes instead. --- zjit/src/codegen_tests.rs | 32 +++++++++++++++++++++++++++++++- zjit/src/hir.rs | 7 +++++++ 2 files changed, 38 insertions(+), 1 deletion(-) (limited to 'zjit') diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 4f479aa072..660119fb15 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -6,7 +6,7 @@ use crate::backend::lir::Assembler; use crate::codegen::MAX_ISEQ_VERSIONS; use crate::cruby::*; use crate::hir::{Insn, iseq_to_hir}; -use crate::options::rb_zjit_prepare_options; +use crate::options::{rb_zjit_prepare_options, set_call_threshold}; use crate::payload::IseqVersion; use crate::hir::tests::hir_build_tests::assert_contains_opcode; use crate::payload::*; @@ -5258,3 +5258,33 @@ fn test_tracepoint_return_value_with_rescue() { ary "), @"[[:return, :f_raise, :f_raise_return]]"); } + +// Regression test: polymorphic getivar must not return nil for too-complex shapes. +// Too-complex shapes use hash tables for ivar storage, and rb_shape_get_iv_index() +// doesn't work for them. The polymorphic path must fall through to GetIvar instead. +#[test] +fn test_polymorphic_getivar_too_complex_shape() { + // Need threshold >= 3 so both shapes get profiled before compilation + set_call_threshold(3); + assert_snapshot!(inspect(r#" + class C + def initialize(foo) + @foo = foo + end + def foo = @foo + end + + # Create a normal object and a too-complex object of the same class + normal = C.new(:normal) + complex = C.new(:complex) + 1001.times { |i| complex.instance_variable_set(:"@v#{i}", i) } + 1001.times { |i| complex.remove_instance_variable(:"@v#{i}") } + + # Profile with both shapes before compilation triggers at call 3 + normal.foo # call 1: profile normal shape + complex.foo # call 2: profile too-complex shape + + # The too-complex object should still return :complex, not nil + [normal.foo, complex.foo] + "#), @"[:normal, :complex]"); +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index db7a328771..b8e37059eb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4287,6 +4287,9 @@ impl Function { } fn load_ivar(&mut self, block: BlockId, self_val: InsnId, recv_type: ProfiledType, id: ID, state: InsnId) -> InsnId { + // Too-complex shapes use hash tables; rb_shape_get_iv_index doesn't support them. + // Callers must filter these out before calling load_ivar. + assert!(!recv_type.shape().is_too_complex(), "load_ivar called with too-complex shape"); let mut ivar_index: u16 = 0; if ! unsafe { rb_shape_get_iv_index(recv_type.shape().0, id, &mut ivar_index) } { // If there is no IVAR index, then the ivar was undefined when we @@ -7950,6 +7953,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { if profiled_type.flags().is_immediate() { continue; } let expected_shape = profiled_type.shape(); 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 GetIvar handle these. + if expected_shape.is_too_complex() { continue; } if seen_shapes.contains(&expected_shape) { continue; } seen_shapes.push(expected_shape); let expected_shape_const = fun.push_insn(block, Insn::Const { val: Const::CShape(expected_shape) }); -- cgit v1.2.3