Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/unary/custom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Custom unary operations

## Class instances

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Sub(Yes): ...
class No: ...

reveal_type(+Yes()) # revealed: bool
reveal_type(-Yes()) # revealed: str
reveal_type(~Yes()) # revealed: int

reveal_type(+Sub()) # revealed: bool
reveal_type(-Sub()) # revealed: str
reveal_type(~Sub()) # revealed: int

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`"
reveal_type(+No()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`"
reveal_type(-No()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`"
reveal_type(~No()) # revealed: Unknown
```

## Classes

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Sub(Yes): ...
class No: ...

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`"
reveal_type(+Yes) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`"
reveal_type(-Yes) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`"
reveal_type(~Yes) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`"
reveal_type(+Sub) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`"
reveal_type(-Sub) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`"
reveal_type(~Sub) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
reveal_type(+No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
reveal_type(-No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
reveal_type(~No) # revealed: Unknown
```

## Function literals

```py
def f():
pass

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[f]`"
reveal_type(+f) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[f]`"
reveal_type(-f) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[f]`"
reveal_type(~f) # revealed: Unknown
```

## Subclass

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Sub(Yes): ...
class No: ...

def yes() -> type[Yes]:
return Yes

def sub() -> type[Sub]:
return Sub

def no() -> type[No]:
return No

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`"
reveal_type(+yes()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`"
reveal_type(-yes()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`"
reveal_type(~yes()) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`"
reveal_type(+sub()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`"
reveal_type(-sub()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`"
reveal_type(~sub()) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`"
reveal_type(+no()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`"
reveal_type(-no()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`"
reveal_type(~no()) # revealed: Unknown
```

## Metaclass

```py
class Meta(type):
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Yes(metaclass=Meta): ...
class Sub(Yes): ...
class No: ...

reveal_type(+Yes) # revealed: bool
reveal_type(-Yes) # revealed: str
reveal_type(~Yes) # revealed: int

reveal_type(+Sub) # revealed: bool
reveal_type(-Sub) # revealed: str
reveal_type(~Sub) # revealed: int

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
reveal_type(+No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
reveal_type(-No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
reveal_type(~No) # revealed: Unknown
```
11 changes: 3 additions & 8 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3193,10 +3193,7 @@ impl<'db> TypeInferenceBuilder<'db> {
(UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()),
(_, Type::Any) => Type::Any,
(_, Type::Unknown) => Type::Unknown,
(
op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert),
Type::Instance(InstanceType { class }),
) => {
(op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert), _) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know there's a lot of variants in the Type enum, but I'd prefer it if we could explicitly list all the remaining ones in this match arm rather than using _ for the second tuple element. The advantage is that if we add another Type variant in the future that we want to special-case in unary expressions (similar to the special-casing we already do for Type::IntLiteral and Type::BooleanLiteral above), there's no chance of us forgetting to add that special-casing. We'll be forced to explicitly consider whether the new variant should be handled in its own match arm or be handled as part of the catch-all fallback case here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure explicitly listing all variants is always the best answer. In this case, the handling in this arm really is a generic implementation of the semantics in terms of other type semantics. It should always be correct for any type (even the ones we special case above here), assuming a correct implementation of calling dunder methods (to_meta_type, member, etc -- all of which do use exhaustive matches and we'll be forced to consider for any new Type variant.) Thus, a special case for a new Type variant here would not be required for correctness, it would just potentially offer more precision. I kind of think it's fine, possibly even good, for that "more precision" to be driven only by actual user needs, and not something we are forced to consider.

But I'm really fine with it either way.

Copy link
Member

@AlexWaygood AlexWaygood Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right that there isn't a correctness issue here. But I would expect more precision here to be both trivial to accomplish for any new variants and simpler for us to compute. The IntLiteral() branches above really involve much less work for us than doing the full lookup for an instance type's dunder method in typeshed or user code (which could involve materialising an MRO!). I don't really see a reason not for us to infer the more precise type whenever possible if it's trivial to do so, and I'd at least like us to be forced to consider whether it would be better to do so

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like there's a -0 vote from Carl and a +1 from Alex, so I went ahead and expanded the clauses out. We can always put it back to _ in the future if we find this more unwieldy than the benefit.

let unary_dunder_method = match op {
UnaryOp::Invert => "__invert__",
UnaryOp::UAdd => "__pos__",
Expand All @@ -3206,9 +3203,8 @@ impl<'db> TypeInferenceBuilder<'db> {
}
};

if let Symbol::Type(class_member, _) =
class.class_member(self.db(), unary_dunder_method)
{
let meta = operand_type.to_meta_type(self.db());
if let Symbol::Type(class_member, _) = meta.member(self.db(), unary_dunder_method) {
let call = class_member.call(self.db(), &[operand_type]);

match call.return_ty_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) {
Expand Down Expand Up @@ -3238,7 +3234,6 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
}
_ => todo_type!(), // TODO other unary op types
}
}

Expand Down
Loading