diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 6488661c2d..53b056d335 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -657,6 +657,26 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_struct_aset_guards_recv_is_not_frozen + assert_compiles(<<~RUBY, result: :ok, exits: { opt_send_without_block: 1 }) + def foo(obj) + obj.foo = 123 + end + + Foo = Struct.new(:foo) + obj = Foo.new(123) + 100.times do + foo(obj) + end + obj.freeze + begin + foo(obj) + rescue FrozenError + :ok + end + RUBY + end + def test_getblockparam assert_compiles(<<~'RUBY', insns: [:getblockparam]) def foo &blk diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 0fc39322fb..1cf8247a2a 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -8973,6 +8973,17 @@ fn gen_struct_aset( assert!(unsafe { RB_TYPE_P(comptime_recv, RUBY_T_STRUCT) }); assert!((off as i64) < unsafe { RSTRUCT_LEN(comptime_recv) }); + // Even if the comptime recv was not frozen, future recv may be. So we need to emit a guard + // that the recv is not frozen. + // We know all structs are heap objects, so we can check the flag directly. + let recv = asm.stack_opnd(1); + let recv = asm.load(recv); + let flags = asm.load(Opnd::mem(VALUE_BITS, recv, RUBY_OFFSET_RBASIC_FLAGS)); + asm.test(flags, (RUBY_FL_FREEZE as u64).into()); + asm.jnz(Target::side_exit(Counter::opt_aset_frozen)); + + // Not frozen, so we can proceed. + asm_comment!(asm, "struct aset"); let val = asm.stack_pop(1); diff --git a/yjit/src/stats.rs b/yjit/src/stats.rs index 84549fa5d3..105def2fff 100644 --- a/yjit/src/stats.rs +++ b/yjit/src/stats.rs @@ -489,6 +489,7 @@ make_counters! { opt_aset_not_array, opt_aset_not_fixnum, opt_aset_not_hash, + opt_aset_frozen, opt_case_dispatch_megamorphic,