Skip to content

Commit 2d85a55

Browse files
committed
Merge remote-tracking branch 'upstream/master' into default-native-parser
2 parents 432e7cb + ae39cdb commit 2d85a55

19 files changed

Lines changed: 607 additions & 341 deletions

mypy-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ mypy_extensions>=1.0.0
66
pathspec>=1.0.0
77
tomli>=1.1.0; python_version<'3.11'
88
librt>=0.15.0; platform_python_implementation != 'PyPy'
9-
ast-serialize>=0.10.0,<1.0.0
9+
ast-serialize>=0.11.0,<1.0.0

mypyc/codegen/emitmodule.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ def compile_scc_to_ir(
279279
if errors.num_errors > 0:
280280
return modules
281281

282-
env_user_functions = {}
282+
generator_spill_owners = {}
283283
for module in modules.values():
284284
for cls in module.classes:
285285
if cls.env_user_function:
286-
env_user_functions[cls.env_user_function] = cls
286+
generator_spill_owners[cls.env_user_function] = cls
287287

288288
for module in modules.values():
289289
module_path = result.graph[module.fullname].xpath
@@ -296,8 +296,8 @@ def compile_scc_to_ir(
296296
# Insert reference count handling.
297297
insert_ref_count_opcodes(fn)
298298

299-
if fn in env_user_functions:
300-
insert_spills(fn, env_user_functions[fn])
299+
if fn in generator_spill_owners:
300+
insert_spills(fn, generator_spill_owners[fn])
301301

302302
if compiler_options.log_trace:
303303
insert_event_trace_logging(fn, compiler_options)

mypyc/ir/class_ir.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ def __init__(
233233
# value of an attribute is the same as the error value.
234234
self.bitmap_attrs: list[str] = []
235235

236-
# If this is a generator environment class, what is the actual method for it
236+
# If this class owns a generator helper's compiler-generated spill slots, what is the
237+
# actual helper method for it.
237238
self.env_user_function: FuncIR | None = None
238239

239240
# If True, keep one freed, cleared instance available for immediate reuse to
@@ -256,8 +257,10 @@ def __init__(
256257
# Does this generator or coroutine helper serialize execution using an instance flag?
257258
self.has_running_flag = False
258259

259-
# Does this generator object contain its merged environment?
260-
self.has_merged_generator_env = False
260+
# Are this generator object's implementation attributes only accessed while its
261+
# running flag is held (or before publication/during destruction)? This also applies
262+
# when captured source variables live in a separate environment object.
263+
self.has_private_generator_frame = False
261264

262265
def __repr__(self) -> str:
263266
return (
@@ -319,13 +322,13 @@ def needs_getseters_table(self) -> bool:
319322
def attrs_are_thread_confined(self) -> bool:
320323
"""Can these attributes safely use plain access in free-threaded builds?
321324
322-
This requires locals to live directly in the generator object, execution to be
323-
serialized by its running flag, and no Python getseters exposing the attributes.
324-
A separate environment does not qualify because captured locals may be accessed
325-
by nested functions.
325+
This requires the attributes to belong to a private generator frame, execution to
326+
be serialized by its running flag, and no Python getseters exposing the attributes.
327+
Captured locals in a separate environment don't qualify, since nested functions may
328+
access them independently.
326329
"""
327330
return (
328-
self.has_merged_generator_env
331+
self.has_private_generator_frame
329332
and self.has_running_flag
330333
and not self.needs_getseters_table
331334
)
@@ -521,7 +524,7 @@ def serialize(self) -> JsonDict:
521524
"env_user_function": self.env_user_function.id if self.env_user_function else None,
522525
"reuse_freed_instance": self.reuse_freed_instance,
523526
"has_running_flag": self.has_running_flag,
524-
"has_merged_generator_env": self.has_merged_generator_env,
527+
"has_private_generator_frame": self.has_private_generator_frame,
525528
"is_acyclic": self.is_acyclic,
526529
"is_enum": self.is_enum,
527530
"is_coroutine": self.coroutine_name,
@@ -589,7 +592,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
589592
)
590593
ir.reuse_freed_instance = data["reuse_freed_instance"]
591594
ir.has_running_flag = data["has_running_flag"]
592-
ir.has_merged_generator_env = data["has_merged_generator_env"]
595+
ir.has_private_generator_frame = data["has_private_generator_frame"]
593596
ir.is_acyclic = data.get("is_acyclic", False)
594597
ir.is_enum = data["is_enum"]
595598
ir.coroutine_name = data["is_coroutine"]

mypyc/irbuild/builder.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def __init__(
303303
# Whether the current top-level expression contains a suspension point
304304
# (await, yield or yield from). A whole-expression borrow can't span such a
305305
# point, since the borrowed value (and its root) live in registers that are
306-
# not spilled into the generator environment across the suspend.
306+
# not spilled into the generator frame across the suspend.
307307
self.expr_has_suspend = False
308308
# Saved expression state for enclosing functions (see enter()/leave()).
309309
self.expression_depth_stack: list[int] = []
@@ -1046,22 +1046,26 @@ def pop_loop_stack(self) -> None:
10461046
self.nonlocal_control.pop()
10471047

10481048
def make_spill_target(self, type: RType) -> AssignmentTarget:
1049-
"""Moves a given Value instance into the generator class' environment class."""
1050-
name = f"{TEMP_ATTR_NAME}{self.temp_counter}"
1049+
"""Moves a given Value instance into the private generator frame."""
1050+
frame = self.fn_info.generator_class
1051+
# Generator classes for overriding methods can inherit from one another. Include the
1052+
# module-qualified owning class name so unrelated helper spills don't alias an inherited
1053+
# struct field.
1054+
name = f"{TEMP_ATTR_NAME}1_{exported_name(frame.ir.fullname)}_{self.temp_counter}"
10511055
self.temp_counter += 1
1052-
target = self.add_var_to_env_class(Var(name), type, self.fn_info.generator_class)
1056+
target = self.add_var_to_class(Var(name), type, frame.ir, frame.self_reg)
10531057
return target
10541058

10551059
def spill(self, value: Value) -> AssignmentTarget:
1056-
"""Moves a given Value instance into the generator class' environment class."""
1060+
"""Moves a given Value instance into the private generator frame."""
10571061
target = self.make_spill_target(value.type)
10581062
# Shouldn't be able to fail
10591063
self.assign(target, value, NO_TRACEBACK_LINE_NO)
10601064
return target
10611065

10621066
def maybe_spill(self, value: Value) -> Value | AssignmentTarget:
10631067
"""
1064-
Moves a given Value instance into the environment class for generator functions. For
1068+
Moves a given Value instance into the private frame for generator functions. For
10651069
non-generator functions, leaves the Value instance as it is.
10661070
10671071
Returns an AssignmentTarget associated with the Value for generator functions and the
@@ -1073,7 +1077,7 @@ def maybe_spill(self, value: Value) -> Value | AssignmentTarget:
10731077

10741078
def maybe_spill_assignable(self, value: Value) -> Register | AssignmentTarget:
10751079
"""
1076-
Moves a given Value instance into the environment class for generator functions. For
1080+
Moves a given Value instance into the private frame for generator functions. For
10771081
non-generator functions, allocate a temporary Register.
10781082
10791083
Returns an AssignmentTarget associated with the Value for generator functions and an
@@ -1633,24 +1637,45 @@ def add_var_to_env_class(
16331637
keep_alive_on_completion: bool = False,
16341638
prefix: str = "",
16351639
) -> AssignmentTarget:
1636-
# First, define the variable name as an attribute of the environment class, and then
1637-
# construct a target for that attribute.
1640+
return self.add_var_to_class(
1641+
var,
1642+
rtype,
1643+
self.fn_info.env_class,
1644+
base.curr_env_reg,
1645+
reassign=reassign,
1646+
always_defined=always_defined,
1647+
keep_alive_on_completion=keep_alive_on_completion,
1648+
prefix=prefix,
1649+
)
1650+
1651+
def add_var_to_class(
1652+
self,
1653+
var: SymbolNode,
1654+
rtype: RType,
1655+
cls: ClassIR,
1656+
base: Value,
1657+
reassign: bool = False,
1658+
always_defined: bool = False,
1659+
keep_alive_on_completion: bool = False,
1660+
prefix: str = "",
1661+
) -> AssignmentTarget:
1662+
"""Declare an attribute on a class and construct a target using an explicit base."""
16381663
name = prefix + remangle_redefinition_name(var.name)
1639-
self.fn_info.env_class.attributes[name] = rtype
1664+
cls.attributes[name] = rtype
16401665
if keep_alive_on_completion:
1641-
self.fn_info.env_class.attrs_to_keep_alive_on_completion.add(name)
1666+
cls.attrs_to_keep_alive_on_completion.add(name)
16421667
if always_defined:
1643-
self.fn_info.env_class.attrs_with_defaults.add(name)
1644-
attr_target = AssignmentTargetAttr(base.curr_env_reg, name)
1668+
cls.attrs_with_defaults.add(name)
1669+
attr_target = AssignmentTargetAttr(base, name)
16451670

16461671
if reassign:
16471672
# Read the local definition of the variable, and set the corresponding attribute of
1648-
# the environment class' variable to be that value.
1673+
# the class' variable to be that value.
16491674
reg = self.read(self.lookup(var), self.fn_info.fitem.line)
1650-
self.add(SetAttr(base.curr_env_reg, name, reg, self.fn_info.fitem.line))
1675+
self.add(SetAttr(base, name, reg, self.fn_info.fitem.line))
16511676

16521677
# Override the local definition of the variable to instead point at the variable in
1653-
# the environment class.
1678+
# the class.
16541679
return self.add_target(var, attr_target)
16551680

16561681
def is_builtin_ref_expr(self, expr: RefExpr) -> bool:

mypyc/irbuild/callable_class.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class for the nested function.
7878
# this is a toplevel lambda), don't set up an environment.
7979
if builder.fn_infos[-2].contains_nested:
8080
callable_class_ir.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_infos[-2].env_class)
81+
# The link is initialized before the callable is published and is never rebound.
82+
# Treating it as Final permits plain loads on free-threaded builds.
83+
callable_class_ir.final_attributes.add(ENV_ATTR_NAME)
8184
callable_class_ir.mro = [callable_class_ir]
8285
builder.fn_info.callable_class = ImplicitClass(callable_class_ir)
8386
builder.classes.append(callable_class_ir)
@@ -235,7 +238,11 @@ def instantiate_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> Value:
235238
elif builder.fn_info.contains_nested:
236239
curr_env_reg = builder.fn_info.curr_env_reg
237240
if curr_env_reg:
238-
builder.add(SetAttr(func_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
241+
set_env = SetAttr(func_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line)
242+
# A new or freelist-reused callable has had all of its fields cleared, and this store
243+
# happens before the callable can escape.
244+
set_env.mark_as_initializer()
245+
builder.add(set_env)
239246
# Initialize function wrapper for callable classes. As opposed to regular functions,
240247
# each instance of a callable class needs its own wrapper because they might be instantiated
241248
# inside other functions.

mypyc/irbuild/env_class.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ def load_env_registers(builder: IRBuilder, prefix: str = "") -> None:
143143

144144

145145
def load_outer_env(
146-
builder: IRBuilder, base: Value, outer_env: dict[SymbolNode, SymbolTarget]
146+
builder: IRBuilder,
147+
base: Value,
148+
outer_env: dict[SymbolNode, SymbolTarget],
149+
*,
150+
borrow: bool = False,
147151
) -> Value:
148152
"""Load the environment class for a given base into a register.
149153
@@ -156,7 +160,10 @@ def load_outer_env(
156160
157161
Returns the register where the environment class was loaded.
158162
"""
159-
env = builder.add(GetAttr(base, ENV_ATTR_NAME, builder.fn_info.fitem.line))
163+
if borrow:
164+
assert isinstance(base.type, RInstance)
165+
assert base.type.class_ir.is_final_attr(ENV_ATTR_NAME)
166+
env = builder.add(GetAttr(base, ENV_ATTR_NAME, builder.fn_info.fitem.line, borrow=borrow))
160167
assert isinstance(env.type, RInstance), f"{env} must be of type RInstance"
161168

162169
for symbol, target in outer_env.items():
@@ -182,7 +189,9 @@ def load_outer_envs(builder: IRBuilder, base: ImplicitClass) -> None:
182189
if isinstance(base, GeneratorClass):
183190
base.prev_env_reg = load_outer_env(builder, base.curr_env_reg, outer_env)
184191
else:
185-
base.prev_env_reg = load_outer_env(builder, base.self_reg, outer_env)
192+
# The callable stays alive throughout __call__, and its environment link is Final,
193+
# so the environment can be borrowed for the duration of the call.
194+
base.prev_env_reg = load_outer_env(builder, base.self_reg, outer_env, borrow=True)
186195
env_reg = base.prev_env_reg
187196
index -= 1
188197

mypyc/irbuild/for_helpers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ def need_cleanup(self) -> bool:
705705
def init(self, expr_reg: Value, target_type: RType) -> None:
706706
# Define targets to contain the expression, along with the iterator that will be used
707707
# for the for-loop. If we are inside of a generator function, spill these into the
708-
# environment class.
708+
# private generator frame.
709709
builder = self.builder
710710
iter_reg = builder.primitive_op(iter_op, [expr_reg], self.line)
711711
builder.maybe_spill(expr_reg)
@@ -753,7 +753,7 @@ def need_cleanup(self) -> bool:
753753

754754
def init(self, expr_reg: Value, target_type: RType) -> None:
755755
# Define target to contains the generator expression. It's also the iterator.
756-
# If we are inside a generator function, spill these into the environment class.
756+
# If we are inside a generator function, spill these into the private generator frame.
757757
builder = self.builder
758758
self.iter_target = builder.maybe_spill(expr_reg)
759759
self.target_type = target_type
@@ -811,7 +811,7 @@ def init(self, expr_reg: Value, target_type: RType) -> None:
811811
# Define targets to contain the expression, along with the
812812
# iterator that will be used for the for-loop. We are inside
813813
# of a generator function, so we will spill these into
814-
# environment class.
814+
# the private generator frame.
815815
builder = self.builder
816816
iter_reg = builder.call_c(aiter_op, [expr_reg], self.line)
817817
builder.maybe_spill(expr_reg)
@@ -910,7 +910,7 @@ def init(
910910
self.reverse = reverse
911911
# Define target to contain the expression, along with the index that will be used
912912
# for the for-loop. If we are inside of a generator function, spill these into the
913-
# environment class.
913+
# private generator frame.
914914
self.expr_target = builder.maybe_spill(expr_reg)
915915
if is_immutable_rprimitive(expr_reg.type):
916916
# If the expression is an immutable type, we can load the length just once.
@@ -1011,7 +1011,7 @@ def init(self, expr_reg: Value, target_type: RType) -> None:
10111011
builder = self.builder
10121012
self.target_type = target_type
10131013

1014-
# We add some variables to environment class, so they can be read across yield.
1014+
# Spill some values so they can be read across yield.
10151015
self.expr_target = builder.maybe_spill(expr_reg)
10161016
offset = Integer(0)
10171017
self.offset_target = builder.maybe_spill_assignable(offset)

mypyc/irbuild/generator.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
)
5353
from mypyc.irbuild.nonlocalcontrol import ExceptNonlocalControl, gen_generator_func_cleanup
5454
from mypyc.irbuild.prepare import GENERATOR_HELPER_NAME
55+
from mypyc.irbuild.targets import AssignmentTargetAttr
5556
from mypyc.primitives.exc_ops import (
5657
error_catch_op,
5758
exc_matches_op,
@@ -142,11 +143,7 @@ def instantiate_generator_class(builder: IRBuilder) -> Value:
142143
fitem = builder.fn_info.fitem
143144
generator_reg = builder.add(Call(builder.fn_info.generator_class.ir.ctor, [], fitem.line))
144145

145-
if builder.fn_info.can_merge_generator_and_env_classes():
146-
# Set the generator instance to the initial state (zero).
147-
zero = Integer(0)
148-
builder.add(SetAttr(generator_reg, NEXT_LABEL_ATTR_NAME, zero, fitem.line))
149-
else:
146+
if not builder.fn_info.can_merge_generator_and_env_classes():
150147
# Get the current environment register. If the current function is nested, then the
151148
# generator class gets instantiated from the callable class' '__call__' method, and hence
152149
# we use the callable class' environment register. Otherwise, we use the original
@@ -160,9 +157,10 @@ def instantiate_generator_class(builder: IRBuilder) -> Value:
160157
# defined in the current scope.
161158
builder.add(SetAttr(generator_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
162159

163-
# Set the generator instance's environment to the initial state (zero).
164-
zero = Integer(0)
165-
builder.add(SetAttr(curr_env_reg, NEXT_LABEL_ATTR_NAME, zero, fitem.line))
160+
# The continuation label is private generator state even when captured source variables
161+
# require a separate environment.
162+
zero = Integer(0)
163+
builder.add(SetAttr(generator_reg, NEXT_LABEL_ATTR_NAME, zero, fitem.line))
166164
return generator_reg
167165

168166

@@ -171,15 +169,14 @@ def setup_generator_class(builder: IRBuilder) -> ClassIR:
171169
assert isinstance(builder.fn_info.fitem, FuncDef), builder.fn_info.fitem
172170
generator_class_ir = mapper.fdef_to_generator[builder.fn_info.fitem]
173171
generator_class_ir.has_running_flag = True
172+
generator_class_ir.has_private_generator_frame = True
174173
if builder.fn_info.can_merge_generator_and_env_classes():
175174
builder.fn_info.env_class = generator_class_ir
176-
# The merged environment can be thread-confined; see attrs_are_thread_confined.
177-
generator_class_ir.has_merged_generator_env = True
178175
else:
179176
generator_class_ir.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_info.env_class)
180177
if not builder.fn_info.fitem.is_coroutine:
181-
# After completion generators still need generator.__mypyc_env__ for subsequent
182-
# __next__() calls to observe the terminal next-label and raise StopIteration.
178+
# The helper currently loads generator.__mypyc_env__ before terminal dispatch, so
179+
# an exhausted generator still needs the link on subsequent __next__() calls.
183180
# Coroutines can't be resumed after completion, so keeping the environment alive
184181
# there would just extend local lifetimes unnecessarily.
185182
generator_class_ir.attrs_to_keep_alive_on_completion.add(ENV_ATTR_NAME)
@@ -260,7 +257,9 @@ def add_helper_to_generator_class(
260257
)
261258
fn_info.generator_class.ir.methods[GENERATOR_HELPER_NAME] = helper_fn_ir
262259
builder.functions.append(helper_fn_ir)
263-
fn_info.env_class.env_user_function = helper_fn_ir
260+
# Compiler-generated values live on the private generator frame even if source-level
261+
# captured variables require a separate environment.
262+
fn_info.generator_class.ir.env_user_function = helper_fn_ir
264263

265264
return helper_fn_decl
266265

@@ -438,12 +437,13 @@ def setup_env_for_generator_class(builder: IRBuilder) -> None:
438437
else:
439438
cls.curr_env_reg = load_outer_env(builder, cls.self_reg, builder.symtables[-1])
440439

441-
# Define a variable representing the label to go to the next time
442-
# the '__next__' function of the generator is called, and add it
443-
# as an attribute to the environment class.
444-
cls.next_label_target = builder.add_var_to_env_class(
445-
Var(NEXT_LABEL_ATTR_NAME), int32_rprimitive, cls, reassign=False, always_defined=True
446-
)
440+
# The continuation label identifies where execution resumes when the generator is next
441+
# advanced. Only the serialized generator helper accesses it, so keep it on the private
442+
# generator frame instead of a potentially shared closure environment.
443+
cls.ir.attributes[NEXT_LABEL_ATTR_NAME] = int32_rprimitive
444+
cls.ir.attrs_with_defaults.add(NEXT_LABEL_ATTR_NAME)
445+
next_label_target = AssignmentTargetAttr(cls.self_reg, NEXT_LABEL_ATTR_NAME)
446+
cls.next_label_target = builder.add_target(Var(NEXT_LABEL_ATTR_NAME), next_label_target)
447447

448448
# Add arguments from the original generator function to the
449449
# environment of the generator class.

0 commit comments

Comments
 (0)