Add augmented assignment inference for -= operator#13981
Conversation
| self.mark_symbol_used(symbol); | ||
| let use_id = self.current_ast_ids().record_use(expr); | ||
| self.current_use_def_map_mut().record_use(symbol, use_id); | ||
| } |
There was a problem hiding this comment.
Per Discord: the use shouldn't see the definition, so we had to change the order here. (This hasn't mattered up until augmented assignments.)
| ), | ||
| ); | ||
| Type::Unknown | ||
| }) |
There was a problem hiding this comment.
The logic here is meant to mimic:
if hasattr(a, "__isub__"):
_value = a.__isub__(b)
if _value is not NotImplemented:
a = _value
else:
a = a - b
del _value
else:
a = a - bThere was a problem hiding this comment.
However, I don't really know how to handle the NotImplemented case, if at all -- i.e., how we can determine whether to use __isub__ or fall back to a - b. Can we even detect that from the type system?
There was a problem hiding this comment.
The convention for dunder methods is that if calling the dunder would return NotImplemented, it's annotated "as if it were an illegal call". So int.__sub__ is annotated as "not accepting" float in typeshed, even though actually calling it with a float "works fine" (it just returns NotImplemented, allowing the operation to fallback to float.__rsub__):
>>> (42).__sub__(42.0)
NotImplementedCurrently, though, we don't do any checking of argument types passed into functions, so we can't implement the "fallback to the other method" logic fully right now. There's lots of TODO comments in our tests for binary arithmetic about this currently.
There was a problem hiding this comment.
Some example TODOs for you:
ruff/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
Lines 284 to 291 in 60a2dc5
There was a problem hiding this comment.
The way these dunder methods are typically annotated is that their parameter type should reflect the type of argument for which the method will return a value (not Notimplemented), and any other type will return NotImplemented. So we can detect this from the type system, but currently red-knot can't, because we don't yet have call signature checking (it's something I want to work on this week.)
I'll comment more details above.
There was a problem hiding this comment.
Oops, didn't see Alex's review before submitting mine.
| "Operator `{op}` is unsupported for type `{}`", | ||
| target_type.display(self.db), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Should this... fall back to a - b?
There was a problem hiding this comment.
This relates to your question below about NotImplemented. If there is an __isub__ method, but it doesn't accept the value_type we try to pass it, then we should fall back to __sub__. For any other call error (e.g. __isub__ is set to some non-callable type for some reason) we should just emit a diagnostic and be done here. This will require some more complex matching on the specific error kind we get back from the call -- but this is all a TODO until we have call signature checking.
I would like to add a test for this case (__isub__ exists but doesn't accept the type we pass to it) with a TODO comment in the tests for the right result, to help us remember that this needs fixing.
carljm
left a comment
There was a problem hiding this comment.
Looks good! Mostly just would like to add a few more tests.
| ), | ||
| ); | ||
| Type::Unknown | ||
| }) |
There was a problem hiding this comment.
The way these dunder methods are typically annotated is that their parameter type should reflect the type of argument for which the method will return a value (not Notimplemented), and any other type will return NotImplemented. So we can detect this from the type system, but currently red-knot can't, because we don't yet have call signature checking (it's something I want to work on this week.)
I'll comment more details above.
| "Operator `{op}` is unsupported for type `{}`", | ||
| target_type.display(self.db), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
This relates to your question below about NotImplemented. If there is an __isub__ method, but it doesn't accept the value_type we try to pass it, then we should fall back to __sub__. For any other call error (e.g. __isub__ is set to some non-callable type for some reason) we should just emit a diagnostic and be done here. This will require some more complex matching on the specific error kind we get back from the call -- but this is all a TODO until we have call signature checking.
I would like to add a test for this case (__isub__ exists but doesn't accept the type we pass to it) with a TODO comment in the tests for the right result, to help us remember that this needs fixing.
|
Oh, just noticed the But we don't want to store the assume-Load type on the node. So I think we also need to call |
b989561 to
91006d0
Compare
|
Still getting that same failure after calling |
|
Oh I think it's because we end up visiting the attribute twice, maybe...? Something like that? Because |
|
Ah yeah -- if you look at the error you're getting, I'll bet before it was |
|
One option for how to handle this is to extract that code at the end of |
91006d0 to
7168bc3
Compare
7168bc3 to
7361a48
Compare
|
| // TODO(charlie): Add remaining branches for different types of augmented assignments. | ||
| if let (Operator::Sub, Type::Instance(class)) = (*op, target_type) { | ||
| let class_member = class.class_member(self.db, "__isub__"); | ||
| let call = class_member.call(self.db, &[value_type]); |
There was a problem hiding this comment.
You need to explicitly pass in the self argument here because we've accessed the method on the class rather than the instance, so you're getting the original function object rather than a method object from the class.class_member call
| let call = class_member.call(self.db, &[value_type]); | |
| let call = class_member.call(self.db, &[target_type, value_type]); |
This will matter when we start checking that function calls are being passed arguments of the correct types
| let member_ty = value_ty.member(self.db, &Name::new(&attr.id)); | ||
|
|
||
| member_ty |
There was a problem hiding this comment.
nit
| let member_ty = value_ty.member(self.db, &Name::new(&attr.id)); | |
| member_ty | |
| value_ty.member(self.db, &Name::new(&attr.id)) |
## Summary See: #13981 (comment)
Summary
See: #12699