Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2444,7 +2444,15 @@ def builtin_len(self, val: Value, line: int, use_pyssize_t: bool = False) -> Val
if isinstance(typ, RInstance):
# TODO: Support use_pyssize_t
assert not use_pyssize_t
length = self.gen_method_call(val, "__len__", [], int_rprimitive, line)

if typ.class_ir.has_method("__len__") and typ.class_ir.is_method_final("__len__"):
# Direct C call for final native __len__ methods
decl = typ.class_ir.method_decl("__len__")
length = self.call(decl, [val], [ARG_POS], [None], line)
else:
# Fallback: generic method call for non-native or ambiguous cases
length = self.gen_method_call(val, "__len__", [], int_rprimitive, line)

length = self.coerce(length, int_rprimitive, line)
ok, fail = BasicBlock(), BasicBlock()
cond = self.binary_op(length, Integer(0), ">=", line)
Expand Down
72 changes: 70 additions & 2 deletions mypyc/test-data/irbuild-dunders.test
Original file line number Diff line number Diff line change
@@ -1,24 +1,92 @@
# Test cases for (some) dunder methods

[case testDundersLen]
from typing import final

class C:
def __len__(self) -> int:
return 2

@final
class D:
def __len__(self) -> int:
return 2
class E:
@final
def __len__(self) -> int:
return 2
class F(C):
"""def __len__(self) -> int:
return 3"""
def f(c: C) -> int:
return len(c)
def g(d: D) -> int:
return len(d)
def h(e: E) -> int:
return len(e)
def i(f: F) -> int:
return len(f)
[out]
def C.__len__(self):
self :: __main__.C
L0:
return 4
def D.__len__(self):
self :: __main__.D
L0:
return 4
def E.__len__(self):
self :: __main__.E
L0:
return 4
def f(c):
c :: __main__.C
r0 :: int
r1 :: bit
r2 :: bool
L0:
r0 = c.__len__()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a perfect world, should gen_method_call output the IR on the left or on the right?

I'll try to reimplement the logic from this PR in there and see what happens

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think in this case they should do the same thing, but the original IR is more consistent with the intent of the code, since we aren't calling a static method, so the it would be better.

r0 = C.__len__(c)
r1 = int_ge r0, 0
if r1 goto L2 else goto L1 :: bool
L1:
r2 = raise ValueError('__len__() should return >= 0')
unreachable
L2:
return r0
def g(d):
d :: __main__.D
r0 :: int
r1 :: bit
r2 :: bool
L0:
r0 = D.__len__(d)
r1 = int_ge r0, 0
if r1 goto L2 else goto L1 :: bool
L1:
r2 = raise ValueError('__len__() should return >= 0')
unreachable
L2:
return r0
def h(e):
e :: __main__.E
r0 :: int
r1 :: bit
r2 :: bool
L0:
r0 = E.__len__(e)
r1 = int_ge r0, 0
if r1 goto L2 else goto L1 :: bool
L1:
r2 = raise ValueError('__len__() should return >= 0')
unreachable
L2:
return r0
def i(f):
f :: __main__.F
r0 :: int
r1 :: bit
r2 :: bool
L0:
r0 = C.__len__(f)
Copy link
Contributor Author

@BobTheBuidler BobTheBuidler Sep 28, 2025

Choose a reason for hiding this comment

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

I think this line should be f.__len__() instead of C.__len__(f) because neither C nor C.__len__ nor F is final

But maybe its fine because F isn't decorated with mypyc_attr(allow_interpreted_subclasses=True)? Seeking feedback on this point

Copy link
Collaborator

Choose a reason for hiding this comment

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

Mypyc performs static analysis on class hierarchies, and if you aren't doing separate compilation (most tests don't use it), it can identify all possible subclasses and thus determine that F is effectively final, since there are no (can be no) subclasses.

r1 = int_ge r0, 0
if r1 goto L2 else goto L1 :: bool
L1:
Expand Down
Loading