@@ -962,6 +962,9 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, function: &Function, s
962962 } else {
963963 // Anything can happen inside builtin functions
964964 gen_prepare_non_leaf_call ( jit, asm, function, state) ;
965+ // cexpr!/cstmt! builtins read this frame's locals (its params) directly
966+ // from EP, so make them authoritative in memory.
967+ gen_spill_locals ( jit, asm, state) ;
965968 }
966969
967970 let mut cargs = vec ! [ EC ] ;
@@ -1049,7 +1052,12 @@ fn gen_ccall_with_frame(
10491052 gen_write_jit_frame ( asm, state, 0 ) ;
10501053 gen_save_sp ( asm, caller_stack_size) ;
10511054 gen_spill_stack ( jit, asm, function, state) ;
1052- gen_spill_locals ( jit, asm, state) ;
1055+ // A passed block can read or write this frame's locals through its EP, so
1056+ // make them authoritative in memory. Without a block, the stack map is
1057+ // enough to reconstruct locals lazily on exception/binding.
1058+ if block. is_some ( ) {
1059+ gen_spill_locals ( jit, asm, state) ;
1060+ }
10531061
10541062 let block_handler_specval = if let Some ( BlockHandler :: BlockIseq ( block_iseq) ) = block {
10551063 // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block().
@@ -1144,7 +1152,10 @@ fn gen_ccall_variadic(
11441152 gen_write_jit_frame ( asm, state, 0 ) ;
11451153 gen_save_sp ( asm, caller_stack_size) ;
11461154 gen_spill_stack ( jit, asm, function, state) ;
1147- gen_spill_locals ( jit, asm, state) ;
1155+ // A passed block can read or write this frame's locals through its EP.
1156+ if block. is_some ( ) {
1157+ gen_spill_locals ( jit, asm, state) ;
1158+ }
11481159
11491160 let block_handler_specval = if let Some ( BlockHandler :: BlockIseq ( blockiseq) ) = block {
11501161 gen_block_handler_specval ( asm, blockiseq)
@@ -1505,6 +1516,10 @@ fn gen_send(
15051516 gen_trace_send_fallback ( asm, & reason) ;
15061517
15071518 gen_prepare_fallback_call ( jit, asm, function, state) ;
1519+ // A literal block passed here can read or write this frame's locals through its EP.
1520+ if !blockiseq. is_null ( ) {
1521+ gen_spill_locals ( jit, asm, state) ;
1522+ }
15081523 asm_comment ! ( asm, "call #{} with dynamic dispatch" , ruby_call_method_name( cd) ) ;
15091524 unsafe extern "C" {
15101525 fn rb_vm_send ( ec : EcPtr , cfp : CfpPtr , cd : VALUE , blockiseq : IseqPtr ) -> VALUE ;
@@ -1530,6 +1545,10 @@ fn gen_send_forward(
15301545 gen_trace_send_fallback ( asm, & reason) ;
15311546
15321547 gen_prepare_fallback_call ( jit, asm, function, state) ;
1548+ // A literal block passed here can read or write this frame's locals through its EP.
1549+ if !blockiseq. is_null ( ) {
1550+ gen_spill_locals ( jit, asm, state) ;
1551+ }
15331552
15341553 asm_comment ! ( asm, "call #{} with dynamic dispatch" , ruby_call_method_name( cd) ) ;
15351554 unsafe extern "C" {
@@ -1590,7 +1609,10 @@ fn gen_push_inline_frame(
15901609 gen_write_jit_frame ( asm, state, 0 ) ;
15911610 gen_save_sp ( asm, stack_size) ;
15921611
1593- gen_spill_locals ( jit, asm, state) ;
1612+ // A passed block can read or write this frame's locals through its EP.
1613+ if blockiseq. is_some ( ) {
1614+ gen_spill_locals ( jit, asm, state) ;
1615+ }
15941616
15951617 // This mirrors vm_caller_setup_arg_block() for the `blockiseq != NULL` case.
15961618 // The HIR specialization guards ensure we will only reach here for literal blocks,
@@ -1729,7 +1751,11 @@ fn gen_send_iseq_direct(
17291751 let jit_frame = gen_write_jit_frame ( asm, state, stack_map. len ( ) ) ;
17301752 gen_save_sp ( asm, stack_size) ;
17311753
1732- gen_spill_locals ( jit, asm, state) ;
1754+ // A passed block can read or write this frame's locals through its EP.
1755+ // Without a block, the stack map reconstructs locals lazily on demand.
1756+ if block. is_some ( ) {
1757+ gen_spill_locals ( jit, asm, state) ;
1758+ }
17331759 asm. stack_map ( stack_map, jit_frame, state. depth ) ;
17341760
17351761 // This mirrors vm_caller_setup_arg_block() in for the `blockiseq != NULL` case.
@@ -2018,6 +2044,10 @@ fn gen_invokesuper(
20182044 gen_trace_send_fallback ( asm, & reason) ;
20192045
20202046 gen_prepare_fallback_call ( jit, asm, function, state) ;
2047+ // A literal block passed here can read or write this frame's locals through its EP.
2048+ if !blockiseq. is_null ( ) {
2049+ gen_spill_locals ( jit, asm, state) ;
2050+ }
20212051 asm_comment ! ( asm, "call super with dynamic dispatch" ) ;
20222052 unsafe extern "C" {
20232053 fn rb_vm_invokesuper ( ec : EcPtr , cfp : CfpPtr , cd : VALUE , blockiseq : IseqPtr ) -> VALUE ;
@@ -2043,6 +2073,10 @@ fn gen_invokesuperforward(
20432073 gen_trace_send_fallback ( asm, & reason) ;
20442074
20452075 gen_prepare_fallback_call ( jit, asm, function, state) ;
2076+ // A literal block passed here can read or write this frame's locals through its EP.
2077+ if !blockiseq. is_null ( ) {
2078+ gen_spill_locals ( jit, asm, state) ;
2079+ }
20462080 asm_comment ! ( asm, "call super with dynamic dispatch (forwarding)" ) ;
20472081 unsafe extern "C" {
20482082 fn rb_vm_invokesuperforward ( ec : EcPtr , cfp : CfpPtr , cd : VALUE , blockiseq : IseqPtr ) -> VALUE ;
@@ -3373,7 +3407,6 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, function: &Function, sta
33733407fn gen_prepare_fallback_call ( jit : & JITState , asm : & mut Assembler , function : & Function , state : & FrameState ) {
33743408 gen_write_jit_frame ( asm, state, 0 ) ;
33753409 gen_save_sp ( asm, state. stack_size ( ) ) ;
3376- gen_spill_locals ( jit, asm, state) ;
33773410 gen_spill_stack ( jit, asm, function, state) ;
33783411}
33793412
@@ -3384,31 +3417,46 @@ fn build_stack_map(jit: &JITState, function: &Function, state: &FrameState) -> V
33843417 let mut stack = Vec :: new ( ) ;
33853418 let mut current_state = state. clone ( ) ;
33863419 loop {
3387- stack . extend ( current_state . stack ( ) . rev ( ) . copied ( ) . map ( |insn_id| {
3420+ let to_entry = |insn_id| {
33883421 let opnd = jit. get_opnd ( insn_id) ;
33893422 assert ! (
33903423 matches!( opnd, Opnd :: Value ( _) | Opnd :: VReg { .. } ) ,
33913424 "FrameState should only reference Opnd::Value or Opnd::VReg, but got: {opnd:?}" ,
33923425 ) ;
33933426 StackMapEntry :: Opnd ( opnd)
3394- } ) ) ;
3427+ } ;
3428+
3429+ // Operand stack, top-down.
3430+ stack. extend ( current_state. stack ( ) . rev ( ) . copied ( ) . map ( to_entry) ) ;
3431+ // Frame environment data (me/cref, specval, flags) already lives in memory.
3432+ stack. push ( StackMapEntry :: Skip ( VM_ENV_DATA_SIZE . to_usize ( ) ) ) ;
3433+ // Locals, top-down (local[L-1] .. local[0]). They land at fixed
3434+ // EP-relative slots because we write down from cfp->sp.
3435+ //
3436+ // Locals backed by memory are accessed in mainline code,
3437+ // so we skip over those slots to not interfere and clobber.
3438+ // (The stack map is not the 1st write to the slot.)
3439+ let spilled_locals = current_state. spilled_locals ( ) ;
3440+ for ( idx, & insn_id) in current_state. locals ( ) . enumerate ( ) . rev ( ) {
3441+ if spilled_locals. contains ( & ( idx as u32 ) ) {
3442+ stack. push ( StackMapEntry :: Skip ( 1 ) ) ;
3443+ } else {
3444+ stack. push ( to_entry ( insn_id) ) ;
3445+ }
3446+ }
33953447
33963448 let Some ( caller) = current_state. caller ( ) else {
33973449 break ;
33983450 } ;
3399- stack. push ( StackMapEntry :: Skip ( inline_frame_stack_gap ( current_state. iseq ) ) ) ;
3451+ // Skip the callee's receiver slot below its local table. We currently
3452+ // never map out the stack for `invokeblock`, which doesn't put a
3453+ // receiver on cfp->sp stack.
3454+ stack. push ( StackMapEntry :: Skip ( 1 ) ) ;
34003455 current_state = function. frame_state ( caller) ;
34013456 }
34023457 stack
34033458}
34043459
3405- fn inline_frame_stack_gap ( iseq : IseqPtr ) -> usize {
3406- // The extra slot is for the callee's receiver below its local table.
3407- // We currently never map out the stack for `invokeblock`, which doesn't
3408- // put a receiver on cfp->sp stack.
3409- 1 + unsafe { get_iseq_body_local_table_size ( iseq) } . to_usize ( ) + VM_ENV_DATA_SIZE . to_usize ( )
3410- }
3411-
34123460/// Prepare for calling a C function that may call an arbitrary method.
34133461/// Use gen_prepare_leaf_call_with_gc() if the method is leaf but allocates objects.
34143462fn gen_prepare_non_leaf_call ( jit : & JITState , asm : & mut Assembler , function : & Function , state : & FrameState ) {
@@ -3421,9 +3469,6 @@ fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, function: &Fun
34213469 // Remember the stack map in case it raises an exception
34223470 // and the interpreter uses the stack for handling the exception
34233471 asm. stack_map ( stack_map, jit_frame, state. depth ) ;
3424-
3425- // Spill locals in case the method looks at caller Bindings
3426- gen_spill_locals ( jit, asm, state) ;
34273472}
34283473
34293474/// Frame metadata written by gen_push_frame()
0 commit comments