Skip to content

[wip] [mypyc] feat: unroll certain for loops with known params #19518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions mypyc/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ def __init__(self, label: int = -1) -> None:
self.error_handler: BasicBlock | None = None
self.referenced = False

def __repr__(self) -> str:
return f"{type(self).__name__}(label={self.label}, ops={self.ops})"

@property
def terminated(self) -> bool:
"""Does the block end with a jump, branch or return?
Expand Down
243 changes: 241 additions & 2 deletions mypyc/irbuild/for_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@

from __future__ import annotations

from typing import Callable, ClassVar
from typing import Any, Callable, ClassVar, Union

from mypy.nodes import (
ARG_POS,
BytesExpr,
CallExpr,
DictionaryComprehension,
Expression,
FloatExpr,
GeneratorExpr,
IntExpr,
ListExpr,
Lvalue,
MemberExpr,
NameExpr,
RefExpr,
SetExpr,
StrExpr,
TupleExpr,
TypeAlias,
)
Expand Down Expand Up @@ -90,6 +95,7 @@ def for_loop_helper(
else_insts: GenFunc | None,
is_async: bool,
line: int,
can_unroll: bool = False,
) -> None:
"""Generate IR for a loop.

Expand All @@ -98,6 +104,7 @@ def for_loop_helper(
expr: the expression to iterate over
body_insts: a function that generates the body of the loop
else_insts: a function that generates the else block instructions
can_unroll: whether unrolling is allowed (for semantic safety)
"""
# Body of the loop
body_block = BasicBlock()
Expand All @@ -112,9 +119,28 @@ def for_loop_helper(
normal_loop_exit = else_block if else_insts is not None else exit_block

for_gen = make_for_loop_generator(
builder, index, expr, body_block, normal_loop_exit, line, is_async=is_async
builder,
index,
expr,
body_block,
normal_loop_exit,
line,
is_async=is_async,
body_insts=body_insts,
can_unroll=can_unroll,
)

is_literal_loop: bool = getattr(for_gen, "handles_body_insts", False)

# Only call body_insts if not handled by unrolled generator
if is_literal_loop:
try:
for_gen.begin_body()
return
except AssertionError:
# For whatever reason, we can't unpack the loop in this case.
pass

builder.push_loop_stack(step_block, exit_block)
condition_block = BasicBlock()
builder.goto_and_activate(condition_block)
Expand Down Expand Up @@ -377,6 +403,30 @@ def is_range_ref(expr: RefExpr) -> bool:
)


def is_literal_expr(expr: Expression) -> bool:
# Add other literal types as needed
if isinstance(expr, (IntExpr, StrExpr, FloatExpr, BytesExpr)):
return True
if isinstance(expr, NameExpr) and expr.fullname in {
"builtins.None",
"builtins.True",
"builtins.False",
}:
return True
return False


def is_iterable_expr_with_literal_mambers(expr: Expression) -> bool:
return (
isinstance(expr, (ListExpr, SetExpr, TupleExpr))
and not isinstance(expr, MemberExpr)
and all(
is_literal_expr(item) or is_iterable_expr_with_literal_mambers(item)
for item in expr.items
)
)


def make_for_loop_generator(
builder: IRBuilder,
index: Lvalue,
Expand All @@ -386,10 +436,13 @@ def make_for_loop_generator(
line: int,
is_async: bool = False,
nested: bool = False,
body_insts: GenFunc = None,
can_unroll: bool = True,
) -> ForGenerator:
"""Return helper object for generating a for loop over an iterable.

If "nested" is True, this is a nested iterator such as "e" in "enumerate(e)".
can_unroll: whether unrolling is allowed (for semantic safety)
"""

# Do an async loop if needed. async is always generic
Expand All @@ -402,6 +455,33 @@ def make_for_loop_generator(
return async_obj

rtyp = builder.node_type(expr)

if can_unroll:
# Special case: tuple/list/set literal (unroll the loop)
if is_iterable_expr_with_literal_mambers(expr):
return ForUnrolledSequenceLiteral(
builder, index, body_block, loop_exit, line, expr, body_insts
)

# Special case: RTuple (known-length tuple, struct field iteration)
if isinstance(rtyp, RTuple):
expr_reg = builder.accept(expr)
return ForUnrolledRTuple(
builder, index, body_block, loop_exit, line, rtyp, expr_reg, expr, body_insts
)

# Special case: string literal (unroll the loop)
if isinstance(expr, StrExpr):
return ForUnrolledStringLiteral(
builder, index, body_block, loop_exit, line, expr.value, expr, body_insts
)

# Special case: string literal (unroll the loop)
if isinstance(expr, BytesExpr):
return ForUnrolledBytesLiteral(
builder, index, body_block, loop_exit, line, expr.value, expr, body_insts
)

if is_sequence_rprimitive(rtyp):
# Special case "for x in <list>".
expr_reg = builder.accept(expr)
Expand Down Expand Up @@ -764,6 +844,165 @@ def gen_step(self) -> None:
pass


class _ForUnrolled(ForGenerator):
"""Generate IR for a for loop over a value known at compile time by unrolling the loop.

This class emits the loop body for each element of the value literal directly,
avoiding any runtime iteration logic and generator handling.
"""

def __init__(self, *args: Any, **kwargs: Any):
if type(self) is _ForUnrolled:
raise NotImplementedError(
"This is a base class and should not be initialized directly."
)
super().__init__(*args, **kwargs)

def gen_condition(self) -> None:
# Unrolled: nothing to do here.
pass

def gen_step(self) -> None:
# Unrolled: nothing to do here.
pass


class ForUnrolledSequenceLiteral(_ForUnrolled):
"""Generate IR for a for loop over a tuple literal by unrolling the loop.

This class emits the loop body for each element of the tuple literal directly,
avoiding any runtime iteration logic.
"""

handles_body_insts = True

def __init__(
self,
builder: IRBuilder,
index: Lvalue,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
expr: Union[ListExpr, SetExpr, TupleExpr],
body_insts: GenFunc,
) -> None:
super().__init__(builder, index, body_block, loop_exit, line, nested=False)
self.expr = expr
self.items = expr.items
self.body_insts = body_insts
self.item_types = [builder.node_type(item) for item in self.items]

def begin_body(self) -> None:
builder = self.builder
for item, item_type in zip(self.items, self.item_types):
value = builder.accept(item)
value = builder.coerce(value, item_type, self.line)
builder.assign(builder.get_assignment_target(self.index), value, self.line)
self.body_insts()


class ForUnrolledStringLiteral(_ForUnrolled):
"""Generate IR for a for loop over a string literal by unrolling the loop.

This class emits the loop body for each character of the string literal directly,
avoiding any runtime iteration logic.
"""

handles_body_insts = True

def __init__(
self,
builder: IRBuilder,
index: Lvalue,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
value: str,
expr: Expression,
body_insts: GenFunc,
) -> None:
super().__init__(builder, index, body_block, loop_exit, line, nested=False)
self.value = value
self.expr = expr
self.body_insts = body_insts

def begin_body(self) -> None:
builder = self.builder
for c in self.value:
builder.assign(
builder.get_assignment_target(self.index), builder.accept(StrExpr(c)), self.line
)
self.body_insts()


class ForUnrolledBytesLiteral(_ForUnrolled):
"""Generate IR for a for loop over a string literal by unrolling the loop.

This class emits the loop body for each character of the string literal directly,
avoiding any runtime iteration logic.
"""

handles_body_insts = True

def __init__(
self,
builder: IRBuilder,
index: Lvalue,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
value: bytes,
expr: Expression,
body_insts: GenFunc,
) -> None:
super().__init__(builder, index, body_block, loop_exit, line, nested=False)
self.value = value
self.expr = expr
self.body_insts = body_insts

def begin_body(self) -> None:
builder = self.builder
for c in self.value:
builder.assign(
builder.get_assignment_target(self.index), builder.accept(IntExpr(c)), self.line
)
self.body_insts()


class ForUnrolledRTuple(_ForUnrolled):
"""Generate IR for a for loop over an RTuple by directly accessing struct fields."""

handles_body_insts = True

def __init__(
self,
builder: IRBuilder,
index: Lvalue,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
rtuple_type: RTuple,
expr_reg: Value,
expr: Expression,
body_insts: GenFunc,
) -> None:
super().__init__(builder, index, body_block, loop_exit, line, nested=False)
self.rtuple_type = rtuple_type
self.expr_reg = expr_reg
self.expr = expr
self.body_insts = body_insts

def begin_body(self) -> None:
builder = self.builder
line = self.line
for i, item_type in enumerate(self.rtuple_type.types):
# Directly access the struct field for each RTuple element
value = builder.add(TupleGet(self.expr_reg, i, line))
value = builder.coerce(value, item_type, line)
builder.assign(builder.get_assignment_target(self.index), value, line)
self.body_insts()


def unsafe_index(builder: IRBuilder, target: Value, index: Value, line: int) -> Value:
"""Emit a potentially unsafe index into a target."""
# This doesn't really fit nicely into any of our data-driven frameworks
Expand Down
2 changes: 1 addition & 1 deletion mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def goto(self, target: BasicBlock) -> None:
def activate_block(self, block: BasicBlock) -> None:
"""Add a basic block and make it the active one (target of adds)."""
if self.blocks:
assert self.blocks[-1].terminated
assert self.blocks[-1].terminated, self.blocks[-1]

block.error_handler = self.error_handlers[-1]
self.blocks.append(block)
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy