1818 IS_FREE_THREADED ,
1919 NATIVE_PREFIX ,
2020 REG_PREFIX ,
21+ RUNNING_FIELD ,
2122)
2223from mypyc .ir .class_ir import ClassIR
2324from mypyc .ir .func_ir import FUNC_CLASSMETHOD , FUNC_STATICMETHOD , FuncDecl , FuncIR , all_values
@@ -129,12 +130,18 @@ def native_function_header(fn: FuncDecl, emitter: Emitter) -> str:
129130
130131
131132def generate_native_function (
132- fn : FuncIR , emitter : Emitter , source_path : str , module_name : str
133+ fn : FuncIR ,
134+ emitter : Emitter ,
135+ source_path : str ,
136+ module_name : str ,
137+ running_flag_class : ClassIR | None = None ,
133138) -> None :
134139 declarations = Emitter (emitter .context )
135140 names = generate_names_for_ir (fn .arg_regs , fn .blocks )
136141 body = Emitter (emitter .context , names )
137- visitor = FunctionEmitterVisitor (body , declarations , source_path , module_name )
142+ visitor = FunctionEmitterVisitor (
143+ body , declarations , source_path , module_name , running_flag_class
144+ )
138145
139146 declarations .emit_line (f"{ native_function_header (fn .decl , emitter )} {{" )
140147 body .indent ()
@@ -183,6 +190,10 @@ def generate_native_function(
183190 if not is_next_block or is_problematic_op :
184191 fn .blocks [target .label ].referenced = True
185192
193+ if running_flag_class is not None :
194+ # Place this before all labels so it runs on resume, but not on internal jumps.
195+ visitor .emit_claim_running_flag (fn )
196+
186197 common = frequently_executed_blocks (fn .blocks [0 ])
187198
188199 for i in range (len (blocks )):
@@ -209,13 +220,21 @@ def generate_native_function(
209220
210221class FunctionEmitterVisitor (OpVisitor [None ]):
211222 def __init__ (
212- self , emitter : Emitter , declarations : Emitter , source_path : str , module_name : str
223+ self ,
224+ emitter : Emitter ,
225+ declarations : Emitter ,
226+ source_path : str ,
227+ module_name : str ,
228+ running_flag_class : ClassIR | None = None ,
213229 ) -> None :
214230 self .emitter = emitter
215231 self .names = emitter .names
216232 self .declarations = declarations
217233 self .source_path = source_path
218234 self .module_name = module_name
235+ # Set while emitting a generator helper protected by its running flag.
236+ self .running_flag_class = running_flag_class
237+ self .running_flag_ptr : str | None = None
219238 self .literals = emitter .context .literals
220239 self .rare = False
221240 # Next basic block to be processed after the current one (if any), set by caller
@@ -291,8 +310,28 @@ def visit_branch(self, op: Branch) -> None:
291310
292311 self .emit_lines ("} else" , " goto %s;" % self .label (false ))
293312
313+ def emit_claim_running_flag (self , fn : FuncIR ) -> None :
314+ """Claim the generator's running flag or raise ValueError."""
315+ cl = self .running_flag_class
316+ assert cl is not None
317+ struct = cl .struct_name (self .names )
318+ self_str = self .reg (fn .arg_regs [0 ])
319+ self .running_flag_ptr = f"&(({ struct } *){ self_str } )->{ RUNNING_FIELD } "
320+ flag = self .running_flag_ptr
321+ is_coroutine = 1 if cl .has_method ("__await__" ) else 0
322+ self .emit_line (f"if (unlikely(!CPyGen_TryEnter({ flag } ))) {{" )
323+ self .emit_line (f"return CPyGen_AlreadyExecutingError({ is_coroutine } );" )
324+ self .emit_line ("}" )
325+
326+ def emit_release_running_flag (self ) -> None :
327+ """Release the flag; every helper exit is represented by Return."""
328+ assert self .running_flag_ptr is not None
329+ self .emit_line (f"CPyGen_Exit({ self .running_flag_ptr } );" )
330+
294331 def visit_return (self , op : Return ) -> None :
295332 value_str = self .reg (op .value )
333+ if self .running_flag_class is not None :
334+ self .emit_release_running_flag ()
296335 self .emit_line ("return %s;" % value_str )
297336
298337 def visit_tuple_set (self , op : TupleSet ) -> None :
@@ -418,9 +457,15 @@ def emit_load_attr_take_ref(
418457 for attributes safe to borrow on free-threaded builds (Final and vec attrs -- see
419458 transform_member_expr in irbuild), whose values live as long as their container.
420459 The default (GIL) build always takes the plain-load path and increfs separately.
460+
461+ Thread-confined attributes also use plain loads; see
462+ ClassIR.attrs_are_thread_confined.
421463 """
422464 use_get_attr_ref = (
423- IS_FREE_THREADED and is_simple_refcounted_pointer (attr_rtype ) and not op .is_borrowed
465+ IS_FREE_THREADED
466+ and is_simple_refcounted_pointer (attr_rtype )
467+ and not op .is_borrowed
468+ and not cl .attrs_are_thread_confined ()
424469 )
425470 if use_get_attr_ref and cl .is_final_attr (op .attr ):
426471 self .emitter .emit_line (f"{ dest } = CPy_GetAttrRefFinal((PyObject **)&{ attr_expr } );" )
@@ -578,7 +623,11 @@ def visit_set_attr(self, op: SetAttr) -> None:
578623 )
579624 self .emit_line (f"{ dest } = 1;" )
580625 self .emitter .emit_error_check (tmp , ret_type , f"{ dest } = 0;" )
581- elif IS_FREE_THREADED and is_simple_refcounted_pointer (attr_rtype ):
626+ elif (
627+ IS_FREE_THREADED
628+ and is_simple_refcounted_pointer (attr_rtype )
629+ and not cl .attrs_are_thread_confined ()
630+ ):
582631 # In free-threaded builds, publishing a single reference-counted
583632 # 'PyObject *' field must be atomic so a concurrent reader (see
584633 # CPy_GetAttrRef) never observes a torn pointer or a freed value.
0 commit comments