Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b3f3eab
[mypyc] feat: ForFilter generator helper for builtins.filter
BobTheBuidler Aug 12, 2025
67818c6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2025
74b7a6e
fix: add filter to ir fixtures
BobTheBuidler Aug 12, 2025
eeb09ab
fix: run tests
BobTheBuidler Aug 12, 2025
ddc13b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2025
fc12cea
test with None
BobTheBuidler Aug 12, 2025
5ce8148
Merge branch 'for-filter' of https://github.com/BobTheBuidler/mypy in…
BobTheBuidler Aug 12, 2025
54ad04e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2025
eae9209
IR cases for testing C calls
BobTheBuidler Aug 12, 2025
9941d54
feat: handle native calls and primitive ops
BobTheBuidler Aug 12, 2025
71b27ef
Merge branch 'for-filter' of https://github.com/BobTheBuidler/mypy in…
BobTheBuidler Aug 12, 2025
5237f0b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2025
d68b833
Update run-loops.test
BobTheBuidler Aug 12, 2025
c9680dc
Update for_helpers.py
BobTheBuidler Aug 12, 2025
5bf4b22
test primitive op
BobTheBuidler Aug 12, 2025
c39bb4a
feat: use speciailizers
BobTheBuidler Aug 12, 2025
8e43b2e
Merge branch 'for-filter' of https://github.com/BobTheBuidler/mypy in…
BobTheBuidler Aug 12, 2025
9dceb9a
Revert "Update for_helpers.py"
BobTheBuidler Aug 12, 2025
7c8053f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2025
8aff832
add to docs
BobTheBuidler Aug 12, 2025
0d2c019
Merge branch 'for-filter' of https://github.com/BobTheBuidler/mypy in…
BobTheBuidler Aug 12, 2025
cec1a5d
Update for_helpers.py
BobTheBuidler Aug 13, 2025
5170a10
Merge branch 'master' into for-filter
BobTheBuidler Aug 13, 2025
ba5a978
Merge branch 'master' into for-filter
BobTheBuidler Aug 13, 2025
572793c
Update native_operations.rst
BobTheBuidler Aug 14, 2025
55ed2d6
Merge branch 'master' into for-filter
BobTheBuidler Aug 14, 2025
dbbbb57
Update for_helpers.py
BobTheBuidler Aug 16, 2025
0bc1d26
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 16, 2025
7d56fa9
Update for_helpers.py
BobTheBuidler Aug 16, 2025
4bf480d
Merge branch 'master' into for-filter
BobTheBuidler Sep 7, 2025
11b04c3
Update ir.py
BobTheBuidler Sep 7, 2025
fa54df2
Update ir.py
BobTheBuidler Sep 7, 2025
d2edf7b
Update native_operations.rst
BobTheBuidler Sep 7, 2025
715ce46
refactor run tests
BobTheBuidler Sep 7, 2025
a3b65b3
Update mypyc/test-data/fixtures/ir.py
BobTheBuidler Sep 8, 2025
26db7f5
Merge branch 'master' into for-filter
BobTheBuidler Sep 13, 2025
1197e4e
fix ir generaton
BobTheBuidler Sep 28, 2025
75f61e3
Update for_helpers.py
BobTheBuidler Sep 28, 2025
58798da
Merge branch 'master' into for-filter
BobTheBuidler Sep 30, 2025
f18eca3
Merge branch 'master' into for-filter
BobTheBuidler Oct 2, 2025
26ad0d5
Merge branch 'master' into for-filter
BobTheBuidler Oct 4, 2025
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
1 change: 1 addition & 0 deletions mypyc/doc/native_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ These variants of statements have custom implementations:
* ``for ... in seq:`` (for loop over a sequence)
* ``for ... in enumerate(...):``
* ``for ... in zip(...):``
* ``for ... in filter(...):``
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you considered supporting list(filter(...)) as well -- this seems quite common (in a follow-up PR)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I actually have that drafted already. but it won't be a special case for list(filter(...)) it will be a special case for [list|tuple|set](some_builtin_we_have_a_helper_for_in_for_helpers(...)) which will account for any builtin we have ForGenerator helpers for

Copy link
Contributor Author

@BobTheBuidler BobTheBuidler Aug 15, 2025

Choose a reason for hiding this comment

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

Fwiw this was part of the intent behind the list-built-from-range tests

I wasn't actually testing that we can build a list from a range, I was preparing IR to reflect how this helper would change the C implementation. Will work for map, filter, range, zip, enumerate, and future ops with special-case gen helpers

80 changes: 80 additions & 0 deletions mypyc/irbuild/for_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,16 @@ def make_for_loop_generator(
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
for_list.init(expr_reg, target_type, reverse=True)
return for_list

elif (
expr.callee.fullname == "builtins.filter"
and len(expr.args) == 2
and all(k == ARG_POS for k in expr.arg_kinds)
):
for_filter = ForFilter(builder, index, body_block, loop_exit, line, nested)
for_filter.init(index, expr.args[0], expr.args[1])
return for_filter

if isinstance(expr, CallExpr) and isinstance(expr.callee, MemberExpr) and not expr.args:
# Special cases for dictionary iterator methods, like dict.items().
rtype = builder.node_type(expr.callee.expr)
Expand Down Expand Up @@ -1203,6 +1213,76 @@ def gen_cleanup(self) -> None:
gen.gen_cleanup()


class ForFilter(ForGenerator):
"""Generate optimized IR for a for loop over filter(f, iterable)."""

def need_cleanup(self) -> bool:
# The wrapped for loops might need cleanup. We might generate a
# redundant cleanup block, but that's okay.
return True

def init(self, index: Lvalue, func: Expression, iterable: Expression) -> None:
self.filter_func_def = func
if (
isinstance(func, NameExpr)
and isinstance(func.node, Var)
and func.node.fullname == "builtins.None"
):
self.filter_func_val = None
else:
self.filter_func_val = self.builder.accept(func)
self.iterable = iterable
self.index = index

self.gen = make_for_loop_generator(
self.builder,
self.index,
self.iterable,
self.body_block,
self.loop_exit,
self.line,
is_async=False,
nested=True,
)

def gen_condition(self) -> None:
self.gen.gen_condition()

def begin_body(self) -> None:
# 1. Assign the next item to the loop variable
self.gen.begin_body()

# 2. Call the filter function
builder = self.builder
line = self.line
item = builder.read(builder.get_assignment_target(self.index), line)

if self.filter_func_val is None:
result = item
else:
fake_call_expr = CallExpr(self.filter_func_def, [self.index], [ARG_POS], [None])

# I put this here to prevent a circular import
from mypyc.irbuild.expression import transform_call_expr

result = transform_call_expr(builder, fake_call_expr)
# result = builder.accept(fake_call_expr)

# Now, filter: only enter the body if func(item) is truthy
cont_block, rest_block = BasicBlock(), BasicBlock()
builder.add_bool_branch(result, rest_block, cont_block)
builder.activate_block(cont_block)
builder.nonlocal_control[-1].gen_continue(builder, line)
builder.goto_and_activate(rest_block)
# At this point, the rest of the loop body (user code) will be emitted

def gen_step(self) -> None:
self.gen.gen_step()

def gen_cleanup(self) -> None:
self.gen.gen_cleanup()


def get_expr_length(expr: Expression) -> int | None:
if isinstance(expr, (StrExpr, BytesExpr)):
return len(expr.value)
Expand Down
8 changes: 8 additions & 0 deletions mypyc/test-data/fixtures/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,11 @@ class classmethod: pass
class staticmethod: pass

NotImplemented: Any = ...

class filter(Generic[_T]):
@overload
def __new__(cls, function: None, iterable: Iterable[_T | None], /) -> Self: ...
@overload
def __new__(cls, function: Callable[[_T], Any], iterable: Iterable[_T], /) -> Self: ...
def __iter__(self) -> Self: ...
def __next__(self) -> _T: ...
272 changes: 272 additions & 0 deletions mypyc/test-data/irbuild-basic.test
Original file line number Diff line number Diff line change
Expand Up @@ -3552,6 +3552,278 @@ L0:
r3 = box(None, 1)
return r3

[case testForFilterBool]
def f(x: int) -> bool:
return bool(x % 2)
def g(a: list[int]) -> int:
s = 0
for x in filter(f, a):
s += x
return s
[out]
def f(x):
x, r0 :: int
r1 :: bit
L0:
r0 = CPyTagged_Remainder(x, 4)
r1 = r0 != 0
return r1
def g(a):
a :: list
s :: int
r0 :: dict
r1 :: str
r2 :: object
r3, r4 :: native_int
r5 :: bit
r6 :: object
r7, x :: int
r8 :: bool
r9 :: int
r10 :: native_int
L0:
s = 0
r0 = __main__.globals :: static
r1 = 'f'
r2 = CPyDict_GetItem(r0, r1)
r3 = 0
L1:
r4 = var_object_size a
r5 = r3 < r4 :: signed
if r5 goto L2 else goto L6 :: bool
L2:
r6 = list_get_item_unsafe a, r3
r7 = unbox(int, r6)
x = r7
r8 = f(x)
if r8 goto L4 else goto L3 :: bool
L3:
goto L5
L4:
r9 = CPyTagged_Add(s, x)
s = r9
L5:
r10 = r3 + 1
r3 = r10
goto L1
L6:
L7:
return s

[case testForFilterInt]
def f(x: int) -> int:
return x % 2
def g(a: list[int]) -> int:
s = 0
for x in filter(f, a):
s += x
return s
[out]
def f(x):
x, r0 :: int
L0:
r0 = CPyTagged_Remainder(x, 4)
return r0
def g(a):
a :: list
s :: int
r0 :: dict
r1 :: str
r2 :: object
r3, r4 :: native_int
r5 :: bit
r6 :: object
r7, x, r8 :: int
r9 :: bit
r10 :: int
r11 :: native_int
L0:
s = 0
r0 = __main__.globals :: static
r1 = 'f'
r2 = CPyDict_GetItem(r0, r1)
r3 = 0
L1:
r4 = var_object_size a
r5 = r3 < r4 :: signed
if r5 goto L2 else goto L6 :: bool
L2:
r6 = list_get_item_unsafe a, r3
r7 = unbox(int, r6)
x = r7
r8 = f(x)
r9 = r8 != 0
if r9 goto L4 else goto L3 :: bool
L3:
goto L5
L4:
r10 = CPyTagged_Add(s, x)
s = r10
L5:
r11 = r3 + 1
r3 = r11
goto L1
L6:
L7:
return s

[case testForFilterStr]
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems specialized enough to move to a run test (see my comments in other PRs about our conventions related to irbuild tests).

def f(x: int) -> str:
return str(x % 2)
def g(a: list[int]) -> int:
s = 0
for x in filter(f, a):
s += x
return s
[out]
def f(x):
x, r0 :: int
r1 :: str
L0:
r0 = CPyTagged_Remainder(x, 4)
r1 = CPyTagged_Str(r0)
return r1
def g(a):
a :: list
s :: int
r0 :: dict
r1 :: str
r2 :: object
r3, r4 :: native_int
r5 :: bit
r6 :: object
r7, x :: int
r8 :: str
r9 :: bit
r10 :: int
r11 :: native_int
L0:
s = 0
r0 = __main__.globals :: static
r1 = 'f'
r2 = CPyDict_GetItem(r0, r1)
r3 = 0
L1:
r4 = var_object_size a
r5 = r3 < r4 :: signed
if r5 goto L2 else goto L6 :: bool
L2:
r6 = list_get_item_unsafe a, r3
r7 = unbox(int, r6)
x = r7
r8 = f(x)
r9 = CPyStr_IsTrue(r8)
if r9 goto L4 else goto L3 :: bool
L3:
goto L5
L4:
r10 = CPyTagged_Add(s, x)
s = r10
L5:
r11 = r3 + 1
r3 = r11
goto L1
L6:
L7:
return s

[case testForFilterPrimitiveOp]
def f(a: list[list[int]]) -> int:
s = 0
for x in filter(len, a):
s += 1
return s
[out]
def f(a):
a :: list
s :: int
r0 :: object
r1 :: str
r2 :: object
r3, r4 :: native_int
r5 :: bit
r6 :: object
r7, x :: list
r8 :: native_int
r9 :: short_int
r10 :: bit
r11 :: int
r12 :: native_int
L0:
s = 0
r0 = builtins :: module
r1 = 'len'
r2 = CPyObject_GetAttr(r0, r1)
r3 = 0
L1:
r4 = var_object_size a
r5 = r3 < r4 :: signed
if r5 goto L2 else goto L6 :: bool
L2:
r6 = list_get_item_unsafe a, r3
r7 = cast(list, r6)
x = r7
r8 = var_object_size x
r9 = r8 << 1
r10 = r9 != 0
if r10 goto L4 else goto L3 :: bool
L3:
goto L5
L4:
r11 = CPyTagged_Add(s, 2)
s = r11
L5:
r12 = r3 + 1
r3 = r12
goto L1
L6:
L7:
return s

[case testForFilterNone]
def f(a: list[int]) -> int:
c = 0
for x in filter(None, a):
c += 1
return 0

[out]
def f(a):
a :: list
c :: int
r0, r1 :: native_int
r2 :: bit
r3 :: object
r4, x :: int
r5 :: bit
r6 :: int
r7 :: native_int
L0:
c = 0
r0 = 0
L1:
r1 = var_object_size a
r2 = r0 < r1 :: signed
if r2 goto L2 else goto L6 :: bool
L2:
r3 = list_get_item_unsafe a, r0
r4 = unbox(int, r3)
x = r4
r5 = x != 0
if r5 goto L4 else goto L3 :: bool
L3:
goto L5
L4:
r6 = CPyTagged_Add(c, 2)
c = r6
L5:
r7 = r0 + 1
r0 = r7
goto L1
L6:
L7:
return 0

[case testStarArgFastPathTuple]
from typing import Any, Callable
def deco(fn: Callable[..., Any]) -> Callable[..., Any]:
Expand Down
Loading