Skip to content

Commit 80dfb36

Browse files
authored
[mypyc] Support undefined locals with native int types (#13616)
There's no reserved value we can use to track if a local variable with a native int type hasn't been initialized yet. Instead, use bitmaps to track whether a local is defined, but only if the local has a native int type and can be undefined. Work on mypyc/mypyc#837.
1 parent 88aed94 commit 80dfb36

File tree

3 files changed

+254
-7
lines changed

3 files changed

+254
-7
lines changed

mypyc/test-data/exceptions.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,44 @@ L0:
598598
r0 = c.x
599599
r1 = c.y
600600
return 1
601+
602+
[case testConditionallyUndefinedI64]
603+
from mypy_extensions import i64
604+
605+
def f(x: i64) -> i64:
606+
if x:
607+
y: i64 = 2
608+
return y
609+
[out]
610+
def f(x):
611+
x, r0, y :: int64
612+
__locals_bitmap0 :: uint32
613+
r1 :: bit
614+
r2, r3 :: uint32
615+
r4 :: bit
616+
r5 :: bool
617+
r6 :: int64
618+
L0:
619+
r0 = <error> :: int64
620+
y = r0
621+
__locals_bitmap0 = 0
622+
r1 = x != 0
623+
if r1 goto L1 else goto L2 :: bool
624+
L1:
625+
y = 2
626+
r2 = __locals_bitmap0 | 1
627+
__locals_bitmap0 = r2
628+
L2:
629+
r3 = __locals_bitmap0 & 1
630+
r4 = r3 == 0
631+
if r4 goto L3 else goto L5 :: bool
632+
L3:
633+
r5 = raise UnboundLocalError('local variable "y" referenced before assignment')
634+
if not r5 goto L6 (error at f:-1) else goto L4 :: bool
635+
L4:
636+
unreachable
637+
L5:
638+
return y
639+
L6:
640+
r6 = <error> :: int64
641+
return r6

mypyc/test-data/run-i64.test

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,3 +920,119 @@ def test_magic_default() -> None:
920920
assert a() == MAGIC
921921
assert a(1) == 1
922922
assert a(MAGIC) == MAGIC
923+
924+
[case testI64UndefinedLocal]
925+
from typing_extensions import Final
926+
927+
MYPY = False
928+
if MYPY:
929+
from mypy_extensions import i64, i32
930+
931+
from testutil import assertRaises
932+
933+
MAGIC: Final = -113
934+
935+
936+
def test_conditionally_defined_local() -> None:
937+
x = not int()
938+
if x:
939+
y: i64 = 5
940+
z: i32 = 6
941+
assert y == 5
942+
assert z == 6
943+
944+
def test_conditionally_undefined_local() -> None:
945+
x = int()
946+
if x:
947+
y: i64 = 5
948+
z: i32 = 6
949+
else:
950+
ok: i64 = 7
951+
assert ok == 7
952+
try:
953+
print(y)
954+
except NameError as e:
955+
assert str(e) == 'local variable "y" referenced before assignment'
956+
else:
957+
assert False
958+
try:
959+
print(z)
960+
except NameError as e:
961+
assert str(e) == 'local variable "z" referenced before assignment'
962+
else:
963+
assert False
964+
965+
def test_assign_error_value_conditionally() -> None:
966+
x = int()
967+
if not x:
968+
y: i64 = MAGIC
969+
z: i32 = MAGIC
970+
assert y == MAGIC
971+
assert z == MAGIC
972+
973+
def test_many_locals() -> None:
974+
x = int()
975+
if x:
976+
a0: i64 = 0
977+
a1: i64 = 1
978+
a2: i64 = 2
979+
a3: i64 = 3
980+
a4: i64 = 4
981+
a5: i64 = 5
982+
a6: i64 = 6
983+
a7: i64 = 7
984+
a8: i64 = 8
985+
a9: i64 = 9
986+
a10: i64 = 10
987+
a11: i64 = 11
988+
a12: i64 = 12
989+
a13: i64 = 13
990+
a14: i64 = 14
991+
a15: i64 = 15
992+
a16: i64 = 16
993+
a17: i64 = 17
994+
a18: i64 = 18
995+
a19: i64 = 19
996+
a20: i64 = 20
997+
a21: i64 = 21
998+
a22: i64 = 22
999+
a23: i64 = 23
1000+
a24: i64 = 24
1001+
a25: i64 = 25
1002+
a26: i64 = 26
1003+
a27: i64 = 27
1004+
a28: i64 = 28
1005+
a29: i64 = 29
1006+
a30: i64 = 30
1007+
a31: i64 = 31
1008+
a32: i64 = 32
1009+
a33: i64 = 33
1010+
with assertRaises(NameError):
1011+
print(a0)
1012+
with assertRaises(NameError):
1013+
print(a31)
1014+
with assertRaises(NameError):
1015+
print(a32)
1016+
with assertRaises(NameError):
1017+
print(a33)
1018+
a0 = 5
1019+
assert a0 == 5
1020+
with assertRaises(NameError):
1021+
print(a31)
1022+
with assertRaises(NameError):
1023+
print(a32)
1024+
with assertRaises(NameError):
1025+
print(a33)
1026+
a32 = 55
1027+
assert a0 == 5
1028+
assert a32 == 55
1029+
with assertRaises(NameError):
1030+
print(a31)
1031+
with assertRaises(NameError):
1032+
print(a33)
1033+
a31 = 10
1034+
a33 = 20
1035+
assert a0 == 5
1036+
assert a31 == 10
1037+
assert a32 == 55
1038+
assert a33 == 20

mypyc/transform/uninit.py

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
from __future__ import annotations
44

55
from mypyc.analysis.dataflow import AnalysisDict, analyze_must_defined_regs, cleanup_cfg, get_cfg
6+
from mypyc.common import BITMAP_BITS
67
from mypyc.ir.func_ir import FuncIR, all_values
78
from mypyc.ir.ops import (
89
Assign,
910
BasicBlock,
1011
Branch,
12+
ComparisonOp,
13+
Integer,
14+
IntOp,
1115
LoadAddress,
1216
LoadErrorValue,
1317
Op,
@@ -16,6 +20,7 @@
1620
Unreachable,
1721
Value,
1822
)
23+
from mypyc.ir.rtypes import bitmap_rprimitive, is_fixed_width_rtype
1924

2025

2126
def insert_uninit_checks(ir: FuncIR) -> None:
@@ -38,6 +43,8 @@ def split_blocks_at_uninits(
3843

3944
init_registers = []
4045
init_registers_set = set()
46+
bitmap_registers: list[Register] = [] # Init status bitmaps
47+
bitmap_backed: list[Register] = [] # These use bitmaps to track init status
4148

4249
# First split blocks on ops that may raise.
4350
for block in blocks:
@@ -70,15 +77,28 @@ def split_blocks_at_uninits(
7077
init_registers.append(src)
7178
init_registers_set.add(src)
7279

73-
cur_block.ops.append(
74-
Branch(
80+
if not is_fixed_width_rtype(src.type):
81+
cur_block.ops.append(
82+
Branch(
83+
src,
84+
true_label=error_block,
85+
false_label=new_block,
86+
op=Branch.IS_ERROR,
87+
line=op.line,
88+
)
89+
)
90+
else:
91+
# We need to use bitmap for this one.
92+
check_for_uninit_using_bitmap(
93+
cur_block.ops,
7594
src,
76-
true_label=error_block,
77-
false_label=new_block,
78-
op=Branch.IS_ERROR,
79-
line=op.line,
95+
bitmap_registers,
96+
bitmap_backed,
97+
error_block,
98+
new_block,
99+
op.line,
80100
)
81-
)
101+
82102
raise_std = RaiseStandardError(
83103
RaiseStandardError.UNBOUND_LOCAL_ERROR,
84104
f'local variable "{src.name}" referenced before assignment',
@@ -89,12 +109,82 @@ def split_blocks_at_uninits(
89109
cur_block = new_block
90110
cur_block.ops.append(op)
91111

112+
if bitmap_backed:
113+
update_register_assignments_to_set_bitmap(new_blocks, bitmap_registers, bitmap_backed)
114+
92115
if init_registers:
93116
new_ops: list[Op] = []
94117
for reg in init_registers:
95118
err = LoadErrorValue(reg.type, undefines=True)
96119
new_ops.append(err)
97120
new_ops.append(Assign(reg, err))
121+
for reg in bitmap_registers:
122+
new_ops.append(Assign(reg, Integer(0, bitmap_rprimitive)))
98123
new_blocks[0].ops[0:0] = new_ops
99124

100125
return new_blocks
126+
127+
128+
def check_for_uninit_using_bitmap(
129+
ops: list[Op],
130+
src: Register,
131+
bitmap_registers: list[Register],
132+
bitmap_backed: list[Register],
133+
error_block: BasicBlock,
134+
ok_block: BasicBlock,
135+
line: int,
136+
) -> None:
137+
"""Check if src is defined using a bitmap.
138+
139+
Modifies ops, bitmap_registers and bitmap_backed.
140+
"""
141+
if src not in bitmap_backed:
142+
# Set up a new bitmap backed register.
143+
bitmap_backed.append(src)
144+
n = (len(bitmap_backed) - 1) // BITMAP_BITS
145+
if len(bitmap_registers) <= n:
146+
bitmap_registers.append(Register(bitmap_rprimitive, f"__locals_bitmap{n}"))
147+
148+
index = bitmap_backed.index(src)
149+
masked = IntOp(
150+
bitmap_rprimitive,
151+
bitmap_registers[index // BITMAP_BITS],
152+
Integer(1 << (index & (BITMAP_BITS - 1)), bitmap_rprimitive),
153+
IntOp.AND,
154+
line,
155+
)
156+
ops.append(masked)
157+
chk = ComparisonOp(masked, Integer(0, bitmap_rprimitive), ComparisonOp.EQ)
158+
ops.append(chk)
159+
ops.append(Branch(chk, error_block, ok_block, Branch.BOOL))
160+
161+
162+
def update_register_assignments_to_set_bitmap(
163+
blocks: list[BasicBlock], bitmap_registers: list[Register], bitmap_backed: list[Register]
164+
) -> None:
165+
"""Update some assignments to registers to also set a bit in a bitmap.
166+
167+
The bitmaps are used to track if a local variable has been assigned to.
168+
169+
Modifies blocks.
170+
"""
171+
for block in blocks:
172+
if any(isinstance(op, Assign) and op.dest in bitmap_backed for op in block.ops):
173+
new_ops: list[Op] = []
174+
for op in block.ops:
175+
if isinstance(op, Assign) and op.dest in bitmap_backed:
176+
index = bitmap_backed.index(op.dest)
177+
new_ops.append(op)
178+
reg = bitmap_registers[index // BITMAP_BITS]
179+
new = IntOp(
180+
bitmap_rprimitive,
181+
reg,
182+
Integer(1 << (index & (BITMAP_BITS - 1)), bitmap_rprimitive),
183+
IntOp.OR,
184+
op.line,
185+
)
186+
new_ops.append(new)
187+
new_ops.append(Assign(reg, new))
188+
else:
189+
new_ops.append(op)
190+
block.ops = new_ops

0 commit comments

Comments
 (0)