summaryrefslogtreecommitdiff
path: root/zjit
diff options
context:
space:
mode:
authorTakashi Kokubun <takashikkbn@gmail.com>2026-03-24 09:47:50 -0700
committerGitHub <noreply@github.com>2026-03-24 09:47:50 -0700
commite74823a08098ef87c7a2fc3a35647c4c4467ca40 (patch)
treee5942ce7d282e925eadb8f2c4e2999be60a63576 /zjit
parent05277c6f4332b964858fdade944549c575069255 (diff)
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.
Diffstat (limited to 'zjit')
-rw-r--r--zjit/src/codegen_tests.rs32
-rw-r--r--zjit/src/hir.rs7
2 files changed, 38 insertions, 1 deletions
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<Function, ParseError> {
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) });