@@ -13,7 +13,7 @@ use crate::invariants::{
1313 track_single_ractor_assumption, track_stable_constant_names_assumption, track_no_singleton_class_assumption
1414} ;
1515use crate :: gc:: append_gc_offsets;
16- use crate :: payload:: { get_or_create_iseq_payload, get_or_create_iseq_payload_ptr , IseqCodePtrs , IseqPayload , IseqStatus } ;
16+ use crate :: payload:: { get_or_create_iseq_payload, IseqCodePtrs , IseqVersion , IseqVersionRef , IseqStatus } ;
1717use crate :: state:: ZJITState ;
1818use crate :: stats:: { CompileError , exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type} ;
1919use crate :: stats:: { counter_ptr, with_time_stat, Counter , Counter :: { compile_time_ns, exit_compile_error} } ;
@@ -25,6 +25,9 @@ use crate::hir_type::{types, Type};
2525use crate :: options:: get_option;
2626use crate :: cast:: IntoUsize ;
2727
28+ /// At the moment, we support recompiling each ISEQ only once.
29+ pub const MAX_ISEQ_VERSIONS : usize = 2 ;
30+
2831/// Sentinel program counter stored in C frames when runtime checks are enabled.
2932const PC_POISON : Option < * const VALUE > = if cfg ! ( feature = "runtime_checks" ) {
3033 Some ( usize:: MAX as * const VALUE )
@@ -37,6 +40,9 @@ struct JITState {
3740 /// Instruction sequence for the method being compiled
3841 iseq : IseqPtr ,
3942
43+ /// ISEQ version that is being compiled, which will be used by PatchPoint
44+ version : IseqVersionRef ,
45+
4046 /// Low-level IR Operands indexed by High-level IR's Instruction ID
4147 opnds : Vec < Option < Opnd > > ,
4248
@@ -52,9 +58,10 @@ struct JITState {
5258
5359impl JITState {
5460 /// Create a new JITState instance
55- fn new ( iseq : IseqPtr , num_insns : usize , num_blocks : usize ) -> Self {
61+ fn new ( iseq : IseqPtr , version : IseqVersionRef , num_insns : usize , num_blocks : usize ) -> Self {
5662 JITState {
5763 iseq,
64+ version,
5865 opnds : vec ! [ None ; num_insns] ,
5966 labels : vec ! [ None ; num_blocks] ,
6067 jit_entries : Vec :: default ( ) ,
@@ -134,11 +141,10 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
134141}
135142
136143/// Stub a branch for a JIT-to-JIT call
137- fn gen_iseq_call ( cb : & mut CodeBlock , caller_iseq : IseqPtr , iseq_call : & IseqCallRef ) -> Result < ( ) , CompileError > {
144+ pub fn gen_iseq_call ( cb : & mut CodeBlock , iseq_call : & IseqCallRef ) -> Result < ( ) , CompileError > {
138145 // Compile a function stub
139146 let stub_ptr = gen_function_stub ( cb, iseq_call. clone ( ) ) . inspect_err ( |err| {
140- debug ! ( "{err:?}: gen_function_stub failed: {} -> {}" ,
141- iseq_get_location( caller_iseq, 0 ) , iseq_get_location( iseq_call. iseq. get( ) , 0 ) ) ;
147+ debug ! ( "{err:?}: gen_function_stub failed: {}" , iseq_get_location( iseq_call. iseq. get( ) , 0 ) ) ;
142148 } ) ?;
143149
144150 // Update the JIT-to-JIT call to call the stub
@@ -197,29 +203,36 @@ pub fn gen_entry_trampoline(cb: &mut CodeBlock) -> Result<CodePtr, CompileError>
197203fn gen_iseq ( cb : & mut CodeBlock , iseq : IseqPtr , function : Option < & Function > ) -> Result < IseqCodePtrs , CompileError > {
198204 // Return an existing pointer if it's already compiled
199205 let payload = get_or_create_iseq_payload ( iseq) ;
200- match & payload. status {
201- IseqStatus :: Compiled ( code_ptrs) => return Ok ( code_ptrs. clone ( ) ) ,
202- IseqStatus :: CantCompile ( err) => return Err ( err. clone ( ) ) ,
203- IseqStatus :: NotCompiled => { } ,
206+ let last_status = payload. versions . last ( ) . map ( |version| & unsafe { version. as_ref ( ) } . status ) ;
207+ match last_status {
208+ Some ( IseqStatus :: Compiled ( code_ptrs) ) => return Ok ( code_ptrs. clone ( ) ) ,
209+ Some ( IseqStatus :: CantCompile ( err) ) => return Err ( err. clone ( ) ) ,
210+ _ => { } ,
211+ }
212+ // If the ISEQ already hax MAX_ISEQ_VERSIONS, do not compile a new version.
213+ if payload. versions . len ( ) == MAX_ISEQ_VERSIONS {
214+ return Err ( CompileError :: IseqVersionLimitReached ) ;
204215 }
205216
206217 // Compile the ISEQ
207- let code_ptrs = gen_iseq_body ( cb, iseq, function, payload) ;
218+ let mut version = IseqVersion :: new ( iseq) ;
219+ let code_ptrs = gen_iseq_body ( cb, iseq, version, function) ;
208220 match & code_ptrs {
209221 Ok ( code_ptrs) => {
210- payload . status = IseqStatus :: Compiled ( code_ptrs. clone ( ) ) ;
222+ unsafe { version . as_mut ( ) } . status = IseqStatus :: Compiled ( code_ptrs. clone ( ) ) ;
211223 incr_counter ! ( compiled_iseq_count) ;
212224 }
213225 Err ( err) => {
214- payload . status = IseqStatus :: CantCompile ( err. clone ( ) ) ;
226+ unsafe { version . as_mut ( ) } . status = IseqStatus :: CantCompile ( err. clone ( ) ) ;
215227 incr_counter ! ( failed_iseq_count) ;
216228 }
217229 }
230+ payload. versions . push ( version) ;
218231 code_ptrs
219232}
220233
221234/// Compile an ISEQ into machine code
222- fn gen_iseq_body ( cb : & mut CodeBlock , iseq : IseqPtr , function : Option < & Function > , payload : & mut IseqPayload ) -> Result < IseqCodePtrs , CompileError > {
235+ fn gen_iseq_body ( cb : & mut CodeBlock , iseq : IseqPtr , mut version : IseqVersionRef , function : Option < & Function > ) -> Result < IseqCodePtrs , CompileError > {
223236 // If we ran out of code region, we shouldn't attempt to generate new code.
224237 if cb. has_dropped_bytes ( ) {
225238 return Err ( CompileError :: OutOfMemory ) ;
@@ -232,23 +245,23 @@ fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>,
232245 } ;
233246
234247 // Compile the High-level IR
235- let ( iseq_code_ptrs, gc_offsets, iseq_calls) = gen_function ( cb, iseq, function) ?;
248+ let ( iseq_code_ptrs, gc_offsets, iseq_calls) = gen_function ( cb, iseq, version , function) ?;
236249
237250 // Stub callee ISEQs for JIT-to-JIT calls
238251 for iseq_call in iseq_calls. iter ( ) {
239- gen_iseq_call ( cb, iseq , iseq_call) ?;
252+ gen_iseq_call ( cb, iseq_call) ?;
240253 }
241254
242255 // Prepare for GC
243- payload . iseq_calls . extend ( iseq_calls) ;
244- append_gc_offsets ( iseq, & gc_offsets) ;
256+ unsafe { version . as_mut ( ) } . outgoing . extend ( iseq_calls) ;
257+ append_gc_offsets ( iseq, version , & gc_offsets) ;
245258 Ok ( iseq_code_ptrs)
246259}
247260
248261/// Compile a function
249- fn gen_function ( cb : & mut CodeBlock , iseq : IseqPtr , function : & Function ) -> Result < ( IseqCodePtrs , Vec < CodePtr > , Vec < IseqCallRef > ) , CompileError > {
262+ fn gen_function ( cb : & mut CodeBlock , iseq : IseqPtr , version : IseqVersionRef , function : & Function ) -> Result < ( IseqCodePtrs , Vec < CodePtr > , Vec < IseqCallRef > ) , CompileError > {
250263 let num_spilled_params = max_num_params ( function) . saturating_sub ( ALLOC_REGS . len ( ) ) ;
251- let mut jit = JITState :: new ( iseq, function. num_insns ( ) , function. num_blocks ( ) ) ;
264+ let mut jit = JITState :: new ( iseq, version , function. num_insns ( ) , function. num_blocks ( ) ) ;
252265 let mut asm = Assembler :: new_with_stack_slots ( num_spilled_params) ;
253266
254267 // Compile each basic block
@@ -724,17 +737,16 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf
724737
725738/// Record a patch point that should be invalidated on a given invariant
726739fn gen_patch_point ( jit : & mut JITState , asm : & mut Assembler , invariant : & Invariant , state : & FrameState ) {
727- let payload_ptr = get_or_create_iseq_payload_ptr ( jit. iseq ) ;
728740 let invariant = * invariant;
729741 let exit = build_side_exit ( jit, state) ;
730742
731743 // Let compile_exits compile a side exit. Let scratch_split lower it with split_patch_point.
732- asm. patch_point ( Target :: SideExit { exit, reason : PatchPoint ( invariant) } , invariant, payload_ptr ) ;
744+ asm. patch_point ( Target :: SideExit { exit, reason : PatchPoint ( invariant) } , invariant, jit . version ) ;
733745}
734746
735747/// This is used by scratch_split to lower PatchPoint into PadPatchPoint and PosMarker.
736748/// It's called at scratch_split so that we can use the Label after side-exit deduplication in compile_exits.
737- pub fn split_patch_point ( asm : & mut Assembler , target : & Target , invariant : Invariant , payload_ptr : * mut IseqPayload ) {
749+ pub fn split_patch_point ( asm : & mut Assembler , target : & Target , invariant : Invariant , version : IseqVersionRef ) {
738750 let Target :: Label ( exit_label) = * target else {
739751 unreachable ! ( "PatchPoint's target should have been lowered to Target::Label by compile_exits: {target:?}" ) ;
740752 } ;
@@ -747,25 +759,25 @@ pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invari
747759 let side_exit_ptr = cb. resolve_label ( exit_label) ;
748760 match invariant {
749761 Invariant :: BOPRedefined { klass, bop } => {
750- track_bop_assumption ( klass, bop, code_ptr, side_exit_ptr, payload_ptr ) ;
762+ track_bop_assumption ( klass, bop, code_ptr, side_exit_ptr, version ) ;
751763 }
752764 Invariant :: MethodRedefined { klass : _, method : _, cme } => {
753- track_cme_assumption ( cme, code_ptr, side_exit_ptr, payload_ptr ) ;
765+ track_cme_assumption ( cme, code_ptr, side_exit_ptr, version ) ;
754766 }
755767 Invariant :: StableConstantNames { idlist } => {
756- track_stable_constant_names_assumption ( idlist, code_ptr, side_exit_ptr, payload_ptr ) ;
768+ track_stable_constant_names_assumption ( idlist, code_ptr, side_exit_ptr, version ) ;
757769 }
758770 Invariant :: NoTracePoint => {
759- track_no_trace_point_assumption ( code_ptr, side_exit_ptr, payload_ptr ) ;
771+ track_no_trace_point_assumption ( code_ptr, side_exit_ptr, version ) ;
760772 }
761773 Invariant :: NoEPEscape ( iseq) => {
762- track_no_ep_escape_assumption ( iseq, code_ptr, side_exit_ptr, payload_ptr ) ;
774+ track_no_ep_escape_assumption ( iseq, code_ptr, side_exit_ptr, version ) ;
763775 }
764776 Invariant :: SingleRactorMode => {
765- track_single_ractor_assumption ( code_ptr, side_exit_ptr, payload_ptr ) ;
777+ track_single_ractor_assumption ( code_ptr, side_exit_ptr, version ) ;
766778 }
767779 Invariant :: NoSingletonClass { klass } => {
768- track_no_singleton_class_assumption ( klass, code_ptr, side_exit_ptr, payload_ptr ) ;
780+ track_no_singleton_class_assumption ( klass, code_ptr, side_exit_ptr, version ) ;
769781 }
770782 }
771783 } ) ;
@@ -2383,8 +2395,9 @@ c_callable! {
23832395 // code path can be made read-only. But you still need the check as is while holding the VM lock in any case.
23842396 let cb = ZJITState :: get_code_block( ) ;
23852397 let payload = get_or_create_iseq_payload( iseq) ;
2386- let compile_error = match & payload. status {
2387- IseqStatus :: CantCompile ( err) => Some ( err) ,
2398+ let last_status = payload. versions. last( ) . map( |version| & unsafe { version. as_ref( ) } . status) ;
2399+ let compile_error = match last_status {
2400+ Some ( IseqStatus :: CantCompile ( err) ) => Some ( err) ,
23882401 _ if cb. has_dropped_bytes( ) => Some ( & CompileError :: OutOfMemory ) ,
23892402 _ => None ,
23902403 } ;
@@ -2398,7 +2411,15 @@ c_callable! {
23982411
23992412 // Otherwise, attempt to compile the ISEQ. We have to mark_all_executable() beyond this point.
24002413 let code_ptr = with_time_stat( compile_time_ns, || function_stub_hit_body( cb, & iseq_call) ) ;
2414+ if code_ptr. is_ok( ) {
2415+ if let Some ( version) = payload. versions. last_mut( ) {
2416+ unsafe { version. as_mut( ) } . incoming. push( iseq_call) ;
2417+ }
2418+ }
24012419 let code_ptr = code_ptr. unwrap_or_else( |compile_error| {
2420+ // We'll use this Rc again, so increment the ref count decremented by from_raw.
2421+ unsafe { Rc :: increment_strong_count( iseq_call_ptr as * const IseqCall ) ; }
2422+
24022423 prepare_for_exit( iseq, cfp, sp, & compile_error) ;
24032424 ZJITState :: get_exit_trampoline_with_counter( )
24042425 } ) ;
@@ -2715,3 +2736,37 @@ impl IseqCall {
27152736 } ) ;
27162737 }
27172738}
2739+
2740+ #[ cfg( test) ]
2741+ mod tests {
2742+ use crate :: codegen:: MAX_ISEQ_VERSIONS ;
2743+ use crate :: cruby:: test_utils:: * ;
2744+ use crate :: payload:: * ;
2745+
2746+ #[ test]
2747+ fn test_max_iseq_versions ( ) {
2748+ eval ( & format ! ( "
2749+ TEST = -1
2750+ def test = TEST
2751+
2752+ # compile and invalidate MAX+1 times
2753+ i = 0
2754+ while i < {MAX_ISEQ_VERSIONS} + 1
2755+ test; test # compile a version
2756+
2757+ Object.send(:remove_const, :TEST)
2758+ TEST = i
2759+
2760+ i += 1
2761+ end
2762+ " ) ) ;
2763+
2764+ // It should not exceed MAX_ISEQ_VERSIONS
2765+ let iseq = get_method_iseq ( "self" , "test" ) ;
2766+ let payload = get_or_create_iseq_payload ( iseq) ;
2767+ assert_eq ! ( payload. versions. len( ) , MAX_ISEQ_VERSIONS ) ;
2768+
2769+ // The last call should not discard the JIT code
2770+ assert ! ( matches!( unsafe { payload. versions. last( ) . unwrap( ) . as_ref( ) } . status, IseqStatus :: Compiled ( _) ) ) ;
2771+ }
2772+ }
0 commit comments