@@ -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