Skip to content

Commit 2437282

Browse files
committed
rewrite: load and store instance attributes.
* Splits abstract.Instance into FrozenInstance and MutableInstance, so that we can easily distinguish between canonical instances created by BaseClass.instantiate() that should ignore any attribute setting done outside of __new__ and __init__, and instances created in the course of bytecode analysis that should respect all modifications. * Implements opcodes for loading and storing attributes. PiperOrigin-RevId: 615684582
1 parent e9a60b7 commit 2437282

9 files changed

Lines changed: 257 additions & 24 deletions

File tree

pytype/rewrite/abstract/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ py_test(
4949
DEPS
5050
.base
5151
.classes
52+
.functions
5253
)
5354

5455
py_library(

pytype/rewrite/abstract/abstract.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
NULL = _base.NULL
1212

1313
BaseClass = _classes.BaseClass
14+
FrozenInstance = _classes.FrozenInstance
1415
InterpreterClass = _classes.InterpreterClass
16+
MutableInstance = _classes.MutableInstance
1517
BUILD_CLASS = _classes.BUILD_CLASS
1618

1719
Args = _functions.Args
20+
BoundFunction = _functions.BoundFunction
1821
InterpreterFunction = _functions.InterpreterFunction
1922

2023
get_atomic_constant = _utils.get_atomic_constant

pytype/rewrite/abstract/base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Base abstract representation of Python values."""
22

3-
from typing import Generic, Set, TypeVar
3+
from typing import Generic, Optional, Set, TypeVar
44

55
from pytype.rewrite.flow import variables
66
from typing_extensions import Self
@@ -13,6 +13,13 @@ class BaseValue:
1313
def to_variable(self: Self) -> variables.Variable[Self]:
1414
return variables.Variable.from_value(self)
1515

16+
def get_attribute(self, name: str) -> Optional['BaseValue']:
17+
del name # unused
18+
return None
19+
20+
def set_attribute(self, name: str, value: 'BaseValue') -> None:
21+
del name, value # unused
22+
1623

1724
class PythonConstant(BaseValue, Generic[_T]):
1825

pytype/rewrite/abstract/classes.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
"""Abstract representations of classes."""
22

3-
from typing import List, Mapping, Optional, Sequence
3+
import dataclasses
4+
5+
from typing import Dict, List, Optional, Protocol, Sequence
46

57
from pytype.rewrite.abstract import base
68
from pytype.rewrite.abstract import functions as functions_lib
79

810

11+
class _HasMembers(Protocol):
12+
13+
members: Dict[str, base.BaseValue]
14+
15+
16+
@dataclasses.dataclass
17+
class ClassCallReturn:
18+
19+
instance: 'MutableInstance'
20+
21+
def get_return_value(self):
22+
return self.instance
23+
24+
925
class BaseClass(base.BaseValue):
1026
"""Base representation of a class."""
1127

12-
def __init__(self, name: str, members: Mapping[str, base.BaseValue]):
28+
def __init__(self, name: str, members: Dict[str, base.BaseValue]):
1329
self.name = name
1430
self.members = members
1531

@@ -30,7 +46,7 @@ def __repr__(self):
3046
def get_attribute(self, name: str) -> Optional[base.BaseValue]:
3147
return self.members.get(name)
3248

33-
def instantiate(self) -> 'Instance':
49+
def instantiate(self) -> 'FrozenInstance':
3450
"""Creates an instance of this class."""
3551
for setup_method_name in self.setup_methods:
3652
setup_method = self.get_attribute(setup_method_name)
@@ -40,19 +56,31 @@ def instantiate(self) -> 'Instance':
4056
if constructor:
4157
raise NotImplementedError('Custom __new__')
4258
else:
43-
instance = Instance(self)
59+
instance = MutableInstance(self)
4460
for initializer_name in self.initializers:
4561
initializer = self.get_attribute(initializer_name)
4662
if isinstance(initializer, functions_lib.InterpreterFunction):
4763
_ = initializer.bind_to(instance).analyze()
48-
return instance
64+
return instance.freeze()
65+
66+
def call(self, args: functions_lib.Args) -> ClassCallReturn:
67+
constructor = self.get_attribute(self.constructor)
68+
if constructor:
69+
raise NotImplementedError('Custom __new__')
70+
else:
71+
instance = MutableInstance(self)
72+
for initializer_name in self.initializers:
73+
initializer = self.get_attribute(initializer_name)
74+
if isinstance(initializer, functions_lib.InterpreterFunction):
75+
_ = initializer.bind_to(instance).call(args)
76+
return ClassCallReturn(instance)
4977

5078

5179
class InterpreterClass(BaseClass):
5280
"""Class defined in the current module."""
5381

5482
def __init__(
55-
self, name: str, members: Mapping[str, base.BaseValue],
83+
self, name: str, members: Dict[str, base.BaseValue],
5684
functions: Sequence[functions_lib.InterpreterFunction],
5785
classes: Sequence['InterpreterClass']):
5886
super().__init__(name, members)
@@ -65,14 +93,49 @@ def __repr__(self):
6593
return f'InterpreterClass({self.name})'
6694

6795

68-
class Instance(base.BaseValue):
96+
class MutableInstance(base.BaseValue):
6997
"""Instance of a class."""
7098

7199
def __init__(self, cls: BaseClass):
72100
self.cls = cls
101+
self.members: Dict[str, base.BaseValue] = {}
73102

74103
def __repr__(self):
75-
return f'Instance({self.cls.name})'
104+
return f'MutableInstance({self.cls.name})'
105+
106+
def get_attribute(self, name: str) -> Optional[base.BaseValue]:
107+
if name in self.members:
108+
return self.members[name]
109+
cls_attribute = self.cls.get_attribute(name)
110+
if isinstance(cls_attribute, functions_lib.SimpleFunction):
111+
return cls_attribute.bind_to(self)
112+
return cls_attribute
113+
114+
def set_attribute(self, name: str, value: base.BaseValue) -> None:
115+
if name in self.members:
116+
raise NotImplementedError(f'Attribute already set: {name}')
117+
self.members[name] = value
118+
119+
def freeze(self) -> 'FrozenInstance':
120+
return FrozenInstance(self)
121+
122+
123+
class FrozenInstance(base.BaseValue):
124+
"""Frozen instance of a class.
125+
126+
This is used by BaseClass.instantiate() to create a snapshot of an instance
127+
whose members map cannot be further modified.
128+
"""
129+
130+
def __init__(self, instance: MutableInstance):
131+
self._underlying = instance
132+
133+
@property
134+
def cls(self):
135+
return self._underlying.cls
136+
137+
def get_attribute(self, name: str) -> Optional[base.BaseValue]:
138+
return self._underlying.get_attribute(name)
76139

77140

78141
BUILD_CLASS = base.Singleton('BUILD_CLASS')

pytype/rewrite/abstract/classes_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pytype.rewrite.abstract import base
22
from pytype.rewrite.abstract import classes
3+
from pytype.rewrite.abstract import functions
34

45
import unittest
56

@@ -20,6 +21,41 @@ def test_instantiate(self):
2021
instance = cls.instantiate()
2122
self.assertEqual(instance.cls, cls)
2223

24+
def test_call(self):
25+
cls = classes.BaseClass('X', {})
26+
instance = cls.call(functions.Args()).get_return_value()
27+
self.assertEqual(instance.cls, cls)
28+
29+
30+
class MutableInstanceTest(unittest.TestCase):
31+
32+
def test_get_instance_attribute(self):
33+
cls = classes.BaseClass('X', {})
34+
instance = classes.MutableInstance(cls)
35+
instance.members['x'] = base.PythonConstant(3)
36+
self.assertEqual(instance.get_attribute('x'), base.PythonConstant(3))
37+
38+
def test_get_class_attribute(self):
39+
cls = classes.BaseClass('X', {'x': base.PythonConstant(3)})
40+
instance = classes.MutableInstance(cls)
41+
self.assertEqual(instance.get_attribute('x'), base.PythonConstant(3))
42+
43+
def test_set_attribute(self):
44+
cls = classes.BaseClass('X', {})
45+
instance = classes.MutableInstance(cls)
46+
instance.set_attribute('x', base.PythonConstant(3))
47+
self.assertEqual(instance.members['x'], base.PythonConstant(3))
48+
49+
50+
class FrozenInstanceTest(unittest.TestCase):
51+
52+
def test_get_attribute(self):
53+
cls = classes.BaseClass('X', {})
54+
mutable_instance = classes.MutableInstance(cls)
55+
mutable_instance.set_attribute('x', base.PythonConstant(3))
56+
instance = mutable_instance.freeze()
57+
self.assertEqual(instance.get_attribute('x'), base.PythonConstant(3))
58+
2359

2460
if __name__ == '__main__':
2561
unittest.main()

pytype/rewrite/abstract/functions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ def analyze(self) -> Sequence[_HasReturnT]:
222222

223223

224224
class _Frame(Protocol):
225+
"""Protocol for a VM frame."""
226+
227+
final_locals: Mapping[str, base.BaseValue]
225228

226229
def make_child_frame(
227230
self,

pytype/rewrite/abstract/functions_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class FakeFrame:
1212

1313
def __init__(self):
1414
self.child_frames = []
15+
self.final_locals = {}
1516

1617
def make_child_frame(self, func, initial_locals):
1718
self.child_frames.append((func, initial_locals))

pytype/rewrite/frame.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def run(self) -> None:
143143
# Set the current state to None so that the load_* and store_* methods
144144
# cannot be used to modify finalized locals.
145145
self._current_state = None
146-
self.final_locals = self._final_locals_as_values()
146+
self._finalize_locals()
147147

148148
def store_local(self, name: str, var: _AbstractVariable) -> None:
149149
self._current_state.store_local(name, var)
@@ -253,7 +253,9 @@ def _call_function(
253253
) -> None:
254254
ret_values = []
255255
for func in func_var.values:
256-
if isinstance(func, abstract.InterpreterFunction):
256+
if isinstance(func, (abstract.InterpreterFunction,
257+
abstract.InterpreterClass,
258+
abstract.BoundFunction)):
257259
frame = func.call(args)
258260
ret_values.append(frame.get_return_value())
259261
elif func is abstract.BUILD_CLASS:
@@ -262,7 +264,7 @@ def _call_function(
262264
frame = builder.call(abstract.Args())
263265
cls = abstract.InterpreterClass(
264266
name=abstract.get_atomic_constant(name, str),
265-
members=frame.final_locals,
267+
members=dict(frame.final_locals),
266268
functions=frame.functions,
267269
classes=frame.classes,
268270
)
@@ -273,7 +275,7 @@ def _call_function(
273275
self._stack.push(
274276
variables.Variable(tuple(variables.Binding(v) for v in ret_values)))
275277

276-
def _final_locals_as_values(self) -> Mapping[str, abstract.BaseValue]:
278+
def _finalize_locals(self) -> None:
277279
final_values = {}
278280
for name, var in self._final_locals.items():
279281
values = var.values
@@ -283,7 +285,39 @@ def _final_locals_as_values(self) -> Mapping[str, abstract.BaseValue]:
283285
final_values[name] = values[0]
284286
else:
285287
raise NotImplementedError('Empty variable not yet supported')
286-
return immutabledict.immutabledict(final_values)
288+
# We've stored SET_ATTR results as local values. Now actually perform the
289+
# attribute setting.
290+
# TODO(b/241479600): If we're deep in a stack of method calls, we should
291+
# instead merge the attribute values into the parent frame so that any
292+
# conditions on the bindings are preserved.
293+
for name, value in final_values.items():
294+
target_name, dot, attr_name = name.rpartition('.')
295+
if not dot or target_name not in self._final_locals:
296+
continue
297+
for target in self._final_locals[target_name].values:
298+
target.set_attribute(attr_name, value)
299+
self.final_locals = immutabledict.immutabledict(final_values)
300+
301+
def _load_attr(
302+
self, target_var: _AbstractVariable, attr_name: str) -> _AbstractVariable:
303+
if target_var.name:
304+
name = f'{target_var.name}.{attr_name}'
305+
else:
306+
name = None
307+
try:
308+
# Check if we've stored the attribute in the current frame.
309+
return self.load_local(name)
310+
except KeyError as e:
311+
# We're loading an attribute without a locally stored value.
312+
attr_bindings = []
313+
for target in target_var.values:
314+
attr = target.get_attribute(attr_name)
315+
if not attr:
316+
raise NotImplementedError('Attribute error') from e
317+
# TODO(b/241479600): If there's a condition on the target binding, we
318+
# should copy it.
319+
attr_bindings.append(variables.Binding(attr))
320+
return variables.Variable(tuple(attr_bindings), name)
287321

288322
def byte_RESUME(self, opcode):
289323
del opcode # unused
@@ -307,6 +341,14 @@ def byte_STORE_GLOBAL(self, opcode):
307341
def byte_STORE_DEREF(self, opcode):
308342
self.store_deref(opcode.argval, self._stack.pop())
309343

344+
def byte_STORE_ATTR(self, opcode):
345+
attr_name = opcode.argval
346+
attr, target = self._stack.popn(2)
347+
if not target.name:
348+
raise NotImplementedError('Missing target name')
349+
full_name = f'{target.name}.{attr_name}'
350+
self.store_local(full_name, attr)
351+
310352
def byte_MAKE_FUNCTION(self, opcode):
311353
if opcode.arg not in (0, pyc_marshal.Flags.MAKE_FUNCTION_HAS_FREE_VARS):
312354
raise NotImplementedError('MAKE_FUNCTION not fully implemented')
@@ -358,6 +400,21 @@ def byte_LOAD_GLOBAL(self, opcode):
358400
name = opcode.argval
359401
self._stack.push(self.load_global(name))
360402

403+
def byte_LOAD_ATTR(self, opcode):
404+
attr_name = opcode.argval
405+
target_var = self._stack.pop()
406+
self._stack.push(self._load_attr(target_var, attr_name))
407+
408+
def byte_LOAD_METHOD(self, opcode):
409+
method_name = opcode.argval
410+
instance_var = self._stack.pop()
411+
# https://docs.python.org/3/library/dis.html#opcode-LOAD_METHOD says that
412+
# this opcode should push two values onto the stack: either the unbound
413+
# method and its `self` or NULL and the bound method. Since we always
414+
# retrieve a bound method, we push the NULL
415+
self._stack.push(abstract.NULL.to_variable())
416+
self._stack.push(self._load_attr(instance_var, method_name))
417+
361418
def byte_PRECALL(self, opcode):
362419
del opcode # unused
363420

0 commit comments

Comments
 (0)