Skip to content

Commit 6feecce

Browse files
[mypyc] feat: specialize isinstance for tuple of primitive types (#19949)
This PR specializes isinstance calls where the type argument is a tuple of primitive types. We can skip tuple creation and the associated refcounting, and daisy-chain the primitive checks with an early exit option at each step.
1 parent bcef9f4 commit 6feecce

File tree

3 files changed

+115
-8
lines changed

3 files changed

+115
-8
lines changed

mypyc/irbuild/specialize.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from __future__ import annotations
1616

17-
from typing import Callable, Final, Optional
17+
from typing import Callable, Final, Optional, cast
1818

1919
from mypy.nodes import (
2020
ARG_NAMED,
@@ -40,6 +40,7 @@
4040
Call,
4141
Extend,
4242
Integer,
43+
PrimitiveDescription,
4344
RaiseStandardError,
4445
Register,
4546
SetAttr,
@@ -589,26 +590,81 @@ def translate_isinstance(builder: IRBuilder, expr: CallExpr, callee: RefExpr) ->
589590
if not (len(expr.args) == 2 and expr.arg_kinds == [ARG_POS, ARG_POS]):
590591
return None
591592

592-
if isinstance(expr.args[1], (RefExpr, TupleExpr)):
593-
builder.types[expr.args[0]] = AnyType(TypeOfAny.from_error)
593+
obj_expr = expr.args[0]
594+
type_expr = expr.args[1]
594595

595-
irs = builder.flatten_classes(expr.args[1])
596+
if isinstance(type_expr, TupleExpr) and not type_expr.items:
597+
# we can compile this case to a noop
598+
return builder.false()
599+
600+
if isinstance(type_expr, (RefExpr, TupleExpr)):
601+
builder.types[obj_expr] = AnyType(TypeOfAny.from_error)
602+
603+
irs = builder.flatten_classes(type_expr)
596604
if irs is not None:
597605
can_borrow = all(
598606
ir.is_ext_class and not ir.inherits_python and not ir.allow_interpreted_subclasses
599607
for ir in irs
600608
)
601-
obj = builder.accept(expr.args[0], can_borrow=can_borrow)
609+
obj = builder.accept(obj_expr, can_borrow=can_borrow)
602610
return builder.builder.isinstance_helper(obj, irs, expr.line)
603611

604-
if isinstance(expr.args[1], RefExpr):
605-
node = expr.args[1].node
612+
if isinstance(type_expr, RefExpr):
613+
node = type_expr.node
606614
if node:
607615
desc = isinstance_primitives.get(node.fullname)
608616
if desc:
609-
obj = builder.accept(expr.args[0])
617+
obj = builder.accept(obj_expr)
610618
return builder.primitive_op(desc, [obj], expr.line)
611619

620+
elif isinstance(type_expr, TupleExpr):
621+
node_names: list[str] = []
622+
for item in type_expr.items:
623+
if not isinstance(item, RefExpr):
624+
return None
625+
if item.node is None:
626+
return None
627+
if item.node.fullname not in node_names:
628+
node_names.append(item.node.fullname)
629+
630+
descs = [isinstance_primitives.get(fullname) for fullname in node_names]
631+
if None in descs:
632+
# not all types are primitive types, abort
633+
return None
634+
635+
obj = builder.accept(obj_expr)
636+
637+
retval = Register(bool_rprimitive)
638+
pass_block = BasicBlock()
639+
fail_block = BasicBlock()
640+
exit_block = BasicBlock()
641+
642+
# Chain the checks: if any succeed, jump to pass_block; else, continue
643+
for i, desc in enumerate(descs):
644+
is_last = i == len(descs) - 1
645+
next_block = fail_block if is_last else BasicBlock()
646+
builder.add_bool_branch(
647+
builder.primitive_op(cast(PrimitiveDescription, desc), [obj], expr.line),
648+
pass_block,
649+
next_block,
650+
)
651+
if not is_last:
652+
builder.activate_block(next_block)
653+
654+
# If any check passed
655+
builder.activate_block(pass_block)
656+
builder.assign(retval, builder.true(), expr.line)
657+
builder.goto(exit_block)
658+
659+
# If all checks failed
660+
builder.activate_block(fail_block)
661+
builder.assign(retval, builder.false(), expr.line)
662+
builder.goto(exit_block)
663+
664+
# Return the result
665+
builder.activate_block(exit_block)
666+
return retval
667+
612668
return None
613669

614670

mypyc/test-data/irbuild-isinstance.test

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,31 @@ def is_tuple(x):
189189
L0:
190190
r0 = PyTuple_Check(x)
191191
return r0
192+
193+
[case testTupleOfPrimitives]
194+
from typing import Any
195+
196+
def is_instance(x: Any) -> bool:
197+
return isinstance(x, (str, int, bytes))
198+
199+
[out]
200+
def is_instance(x):
201+
x :: object
202+
r0, r1, r2 :: bit
203+
r3 :: bool
204+
L0:
205+
r0 = PyUnicode_Check(x)
206+
if r0 goto L3 else goto L1 :: bool
207+
L1:
208+
r1 = PyLong_Check(x)
209+
if r1 goto L3 else goto L2 :: bool
210+
L2:
211+
r2 = PyBytes_Check(x)
212+
if r2 goto L3 else goto L4 :: bool
213+
L3:
214+
r3 = 1
215+
goto L5
216+
L4:
217+
r3 = 0
218+
L5:
219+
return r3

mypyc/test-data/run-misc.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,3 +1173,26 @@ def test_dummy_context() -> None:
11731173
with c:
11741174
assert c.c == 1
11751175
assert c.c == 0
1176+
1177+
[case testIsInstanceTuple]
1178+
from typing import Any
1179+
1180+
def isinstance_empty(x: Any) -> bool:
1181+
return isinstance(x, ())
1182+
def isinstance_single(x: Any) -> bool:
1183+
return isinstance(x, (str,))
1184+
def isinstance_multi(x: Any) -> bool:
1185+
return isinstance(x, (str, int))
1186+
1187+
def test_isinstance_empty() -> None:
1188+
assert isinstance_empty("a") is False
1189+
assert isinstance_empty(1) is False
1190+
assert isinstance_empty(None) is False
1191+
def test_isinstance_single() -> None:
1192+
assert isinstance_single("a") is True
1193+
assert isinstance_single(1) is False
1194+
assert isinstance_single(None) is False
1195+
def test_isinstance_multi() -> None:
1196+
assert isinstance_multi("a") is True
1197+
assert isinstance_multi(1) is True
1198+
assert isinstance_multi(None) is False

0 commit comments

Comments
 (0)