2929 Var ,
3030)
3131from mypy .types import CallableType , Type , UnboundType , get_proper_type
32- from mypyc .common import FAST_PREFIX , LAMBDA_NAME , PROPSET_PREFIX , SELF_NAME
32+ from mypyc .common import FAST_PREFIX , LAMBDA_NAME , PROPCACHE_PREFIX , PROPSET_PREFIX , SELF_NAME
3333from mypyc .ir .class_ir import ClassIR , NonExtClassInfo
3434from mypyc .ir .func_ir import (
3535 FUNC_CLASSMETHOD ,
4242)
4343from mypyc .ir .ops import (
4444 BasicBlock ,
45+ Box ,
46+ Branch ,
47+ Cast ,
4548 ComparisonOp ,
4649 GetAttr ,
4750 Integer ,
4851 LoadLiteral ,
52+ Op ,
4953 Register ,
5054 Return ,
5155 SetAttr ,
7781)
7882from mypyc .irbuild .generator import gen_generator_func , gen_generator_func_body
7983from mypyc .irbuild .targets import AssignmentTarget
84+ from mypyc .irbuild .util import is_cached_property
8085from mypyc .primitives .dict_ops import (
8186 dict_get_method_with_none ,
8287 dict_new_op ,
@@ -500,7 +505,19 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
500505 py_setattr_op , [typ , builder .load_str (name ), decorated_func ], fdef .line
501506 )
502507
508+ cached_property = False
503509 if fdef .is_property :
510+ if is_cached_property_class_member (cdef , fdef .name ):
511+ if class_ir .is_trait :
512+ builder .error (
513+ '"functools.cached_property" is unsupported in traits and protocols' , fdef .line
514+ )
515+ else :
516+ # Add caching to the getter of a functools.cached_property
517+ # (the setter is synthesized below).
518+ cached_property = True
519+ insert_cached_property_ops (func_ir , PROPCACHE_PREFIX + fdef .name , fdef .line )
520+
504521 # If there is a property setter, it will be processed after the getter,
505522 # We populate the optional setter field with none for now.
506523 assert name not in class_ir .properties
@@ -514,6 +531,29 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
514531
515532 class_ir .methods [func_ir .decl .name ] = func_ir
516533
534+ if cached_property :
535+ # Synthesize the setter of a functools.cached_property. Note that this
536+ # must be added to class_ir.methods immediately after the getter, since
537+ # the vtable layout assumes that a property setter directly follows the
538+ # getter.
539+ setter_name = PROPSET_PREFIX + name
540+ setter_ir = gen_cached_property_setter_ir (
541+ builder , class_ir .method_decls [setter_name ], fdef .line
542+ )
543+ builder .functions .append (setter_ir )
544+ class_ir .methods [setter_name ] = setter_ir
545+ class_ir .properties [name ] = (func_ir , setter_ir )
546+
547+ if class_ir .allow_interpreted_subclasses :
548+ # Generate a shadow glue method for the setter so that attribute
549+ # assignment dispatches properly for instances of interpreted
550+ # subclasses.
551+ f = gen_glue_property_setter (
552+ builder , setter_ir .sig , setter_ir , class_ir , class_ir , fdef .line
553+ )
554+ class_ir .glue_methods [(class_ir , setter_name )] = f
555+ builder .functions .append (f )
556+
517557 # If this overrides a parent class method with a different type, we need
518558 # to generate a glue method to mediate between them.
519559 for base in class_ir .mro [1 :]:
@@ -576,7 +616,13 @@ def handle_non_ext_method(
576616
577617 # TODO: Support property setters in non-extension classes
578618 if fdef .is_property :
579- prop = builder .load_module_attr_by_fullname ("builtins.property" , fdef .line )
619+ if is_cached_property_class_member (cdef , fdef .name ):
620+ # Non-extension classes have an instance __dict__, so we can use
621+ # functools.cached_property directly and get the full interpreted
622+ # semantics, including caching.
623+ prop = builder .get_module_attr ("functools" , "cached_property" , fdef .line )
624+ else :
625+ prop = builder .load_module_attr_by_fullname ("builtins.property" , fdef .line )
580626 func_reg = builder .py_call (prop , [func_reg ], fdef .line )
581627
582628 elif builder .mapper .func_to_decl [fdef ].kind == FUNC_CLASSMETHOD :
@@ -1220,3 +1266,87 @@ def gen_property_setter_ir(
12201266 builder .add (Return (builder .none (), line ))
12211267 args , _ , blocks , ret_type , fn_info = builder .leave ()
12221268 return FuncIR (func_decl , args , blocks , line )
1269+
1270+
1271+ def is_cached_property_class_member (cdef : ClassDef , name : str ) -> bool :
1272+ """Is the class member with the given name a functools.cached_property?"""
1273+ sym = cdef .info .names .get (name )
1274+ return sym is not None and isinstance (sym .node , Decorator ) and is_cached_property (sym .node )
1275+
1276+
1277+ def insert_cached_property_ops (func_ir : FuncIR , slot : str , line : int ) -> None :
1278+ """Add caching ops to the getter of a functools.cached_property.
1279+
1280+ The cached value is stored in a hidden attribute (the cache slot named
1281+ 'slot', declared in the prepare phase). The transformed getter is
1282+ equivalent to this:
1283+
1284+ if is_defined(self.<slot>):
1285+ return self.<slot>
1286+ ... original body, with each "return r" replaced by ...
1287+ self.<slot> = r
1288+ return r
1289+
1290+ Boxed property types are stored in the slot as-is, with NULL marking an
1291+ undefined (not yet cached) value. Unboxed types (such as int) are stored
1292+ in a boxed form, since they may not have a dedicated error value.
1293+ """
1294+ self_reg = func_ir .arg_regs [0 ]
1295+ ret_type = func_ir .decl .sig .ret_type
1296+
1297+ # Store the computed value in the cache slot before each return.
1298+ for block in func_ir .blocks :
1299+ new_ops : list [Op ] = []
1300+ for op in block .ops :
1301+ if isinstance (op , Return ):
1302+ value = op .value
1303+ boxed : Value = value
1304+ if value .type .is_unboxed :
1305+ box = Box (value , op .line )
1306+ new_ops .append (box )
1307+ boxed = box
1308+ new_ops .append (SetAttr (self_reg , slot , boxed , op .line ))
1309+ new_ops .append (op )
1310+ block .ops = new_ops
1311+
1312+ # Create a new entry block that returns the value of the cache slot if it
1313+ # is defined, and otherwise runs the original getter body (which now also
1314+ # stores the computed value in the cache slot).
1315+ entry , cache_hit = BasicBlock (), BasicBlock ()
1316+ compute = func_ir .blocks [0 ]
1317+ cached = GetAttr (self_reg , slot , line , allow_error_value = True )
1318+ entry .ops .append (cached )
1319+ entry .ops .append (Branch (cached , compute , cache_hit , Branch .IS_ERROR ))
1320+ result : Value = cached
1321+ if ret_type .is_unboxed :
1322+ result = Unbox (cached , ret_type , line )
1323+ cache_hit .ops .append (result )
1324+ elif not is_same_type (ret_type , cached .type ):
1325+ # The property in a subclass may have a narrower type than the
1326+ # cache slot defined in a base class.
1327+ result = Cast (cached , ret_type , line )
1328+ cache_hit .ops .append (result )
1329+ cache_hit .ops .append (Return (result , line ))
1330+ func_ir .blocks = [entry , cache_hit , * func_ir .blocks ]
1331+
1332+
1333+ def gen_cached_property_setter_ir (builder : IRBuilder , func_decl : FuncDecl , line : int ) -> FuncIR :
1334+ """Generate the setter of a functools.cached_property.
1335+
1336+ Assigning to a cached property stores the value in the cache slot,
1337+ similar to functools.cached_property in CPython (where an assignment
1338+ overrides the cached value through the instance __dict__).
1339+ """
1340+ name = func_decl .name
1341+ builder .enter (name )
1342+ self_type = func_decl .sig .args [0 ].type
1343+ self_reg = builder .add_argument ("self" , self_type )
1344+ value_reg = builder .add_argument ("value" , func_decl .sig .args [1 ].type )
1345+ assert name .startswith (PROPSET_PREFIX )
1346+ slot = PROPCACHE_PREFIX + name [len (PROPSET_PREFIX ) :]
1347+ assert isinstance (self_type , RInstance ), self_type
1348+ slot_type = self_type .class_ir .attr_type (slot )
1349+ builder .add (SetAttr (self_reg , slot , builder .coerce (value_reg , slot_type , line ), line ))
1350+ builder .add (Return (builder .none (), line ))
1351+ args , _ , blocks , ret_type , fn_info = builder .leave ()
1352+ return FuncIR (func_decl , args , blocks , line )
0 commit comments