Skip to content

Commit fd338df

Browse files
authored
Tests: Modified dis.py and added primitive tests (#37)
* Tests: Modified dis.py and added primitive tests * Docs: Typos and more documentation * Test: Fixed minor issue
1 parent 69dee64 commit fd338df

4 files changed

Lines changed: 350 additions & 8 deletions

File tree

CS4215.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,23 @@ language, our interpreter cannot be used as a bootstrap Python.
1616

1717
# Where are files located?
1818

19-
The majority of the changes and functionality are in `Python/tier2.c`. Doxygen documentation
20-
is written alongside the code.
19+
The majority of the changes and functionality are in `Python/tier2.c` where Doxygen documentation
20+
is written alongside the code, and in `Tools/cases_generator/` which contains the DSL implementation.
21+
22+
# Running tests
23+
24+
We've written simple tests of the main functionalities.
25+
Unfortunately we did not have time to write comprehensive tests, and it doesn't seem worth it eitherways given the experimental nature of this project.
26+
27+
After building, run `python tier2_test.py` in the repository's root folder.
28+
29+
# Debugging output
30+
31+
In `tier2.c`, two flags can be set to print debug messages:
32+
```c
33+
// Prints codegen debug messages
34+
#define BB_DEBUG 0
35+
36+
// Prints typeprop debug messages
37+
#define TYPEPROP_DEBUG 0
38+
```

Lib/dis.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
474474
for i in range(start, end):
475475
labels.add(target)
476476
starts_line = None
477+
ret = []
477478
for offset, op, arg in _unpack_opargs(code):
478479
if linestarts is not None:
479480
starts_line = linestarts.get(offset, None)
@@ -534,9 +535,9 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
534535
if arg & (1<<i))
535536
elif deop == BINARY_OP:
536537
_, argrepr = _nb_ops[arg]
537-
yield Instruction(_all_opname[op], op,
538+
ret.append(Instruction(_all_opname[op], op,
538539
arg, argval, argrepr,
539-
offset, starts_line, is_jump_target, positions)
540+
offset, starts_line, is_jump_target, positions))
540541
caches = _inline_cache_entries[deop]
541542
if not caches:
542543
continue
@@ -555,10 +556,11 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
555556
argrepr = f"{name}: {int.from_bytes(data, sys.byteorder)}"
556557
else:
557558
argrepr = ""
558-
yield Instruction(
559+
ret.append(Instruction(
559560
"CACHE", CACHE, 0, None, argrepr, offset, None, False,
560561
Positions(*next(co_positions, ()))
561-
)
562+
))
563+
return ret
562564

563565
def disassemble(co, lasti=-1, *, file=None, show_caches=False, adaptive=False, tier2=False):
564566
"""Disassemble a code object."""

Python/tier2.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
#include "opcode.h"
1111

12-
#define BB_DEBUG 1
13-
#define TYPEPROP_DEBUG 1
12+
#define BB_DEBUG 0
13+
#define TYPEPROP_DEBUG 0
1414
// Max typed version basic blocks per basic block
1515
#define MAX_BB_VERSIONS 10
1616

tier2_test.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import dis
2+
3+
#########
4+
# Utils #
5+
#########
6+
7+
def trigger_tier2(f, args):
8+
for _ in range(64):
9+
f(*args)
10+
11+
def writeinst(opc:str, arg:int=0):
12+
13+
"Makes life easier in writing python bytecode"
14+
15+
nb = max(1,-(-arg.bit_length()//8))
16+
ab = arg.to_bytes(nb, 'big')
17+
ext_arg = dis._all_opmap['EXTENDED_ARG']
18+
inst = bytearray()
19+
for i in range(nb-1):
20+
inst.append(ext_arg)
21+
inst.append(ab[i])
22+
inst.append(dis._all_opmap[opc])
23+
inst.append(ab[-1])
24+
25+
return bytes(inst)
26+
27+
28+
###################
29+
# Type prop tests #
30+
###################
31+
32+
def test_typeprop1(a):
33+
# Dummy code won't be ran
34+
return a+(a+(a+a))
35+
36+
bytecode = b"".join([
37+
# Tests TYPE_SET and TYPE_OVERWRITE
38+
writeinst("RESUME", 0),
39+
writeinst("LOAD_FAST", 0),
40+
writeinst("COPY", 1),
41+
writeinst("COPY", 1),
42+
writeinst("BINARY_OP", 0),
43+
writeinst("CACHE", 0), # For tier1
44+
writeinst("BINARY_OP", 0),
45+
writeinst("CACHE", 0), # For tier1
46+
writeinst("RETURN_VALUE", 0)
47+
])
48+
49+
# Switch to bytecode
50+
test_typeprop1.__code__ = test_typeprop1.__code__.replace(co_code=bytecode)
51+
52+
trigger_tier2(test_typeprop1, (0,))
53+
expected = [
54+
"RESUME_QUICK",
55+
"LOAD_FAST", # Load locals
56+
"COPY",
57+
"COPY", # Copy variable on stack
58+
# All stack variables part of the tree
59+
"BINARY_CHECK_FLOAT",
60+
"NOP", # Space for an EXTENDED_ARG if needed
61+
"BB_BRANCH_IF_FLAG_SET",
62+
63+
# This should let the typeprop know all the locals and stack be int
64+
# TYPE_SET
65+
# Locals: [int]
66+
# Stack : [int->locals[0], int->stack[0], int->stack[1]]
67+
"BINARY_CHECK_INT",
68+
"NOP",
69+
"BB_BRANCH_IF_FLAG_UNSET", # Fallthrough!
70+
71+
# Should propagate the result as int
72+
# TYPE_OVERWRITE
73+
# Locals: [int]
74+
# Stack : [int->locals[0], int]
75+
"BINARY_OP_ADD_INT_REST",
76+
77+
# There should be no more guards here
78+
# if the type propagator is working
79+
"BINARY_OP_ADD_INT_REST",
80+
"RETURN_VALUE"
81+
]
82+
insts = dis.get_instructions(test_typeprop1, tier2=True)
83+
for x,y in zip(insts, expected):
84+
assert x.opname == y
85+
86+
bytecode = b"".join([
87+
# Tests TYPE_SWAP
88+
writeinst("RESUME", 0),
89+
writeinst("LOAD_FAST", 0), # float
90+
writeinst("LOAD_FAST", 1), # int
91+
writeinst("SWAP", 2), # Stack: [int, float]
92+
93+
writeinst("COPY", 1),
94+
# Should generate the FLOAT specialisation
95+
writeinst("BINARY_OP", 0),
96+
writeinst("CACHE", 0), # For tier1
97+
98+
writeinst("SWAP", 2), # [float, int]
99+
writeinst("COPY", 1),
100+
# Should generate the INT specialisation
101+
writeinst("BINARY_OP", 0),
102+
writeinst("CACHE", 0), # For tier1
103+
104+
# float + int
105+
writeinst("BINARY_OP", 0),
106+
writeinst("CACHE", 0), # For tier1
107+
writeinst("RETURN_VALUE", 0)
108+
])
109+
110+
def test_typeprop2(a,b):
111+
# Dummy code won't be ran
112+
return a+(a+(a+a))
113+
114+
# Switch to bytecode
115+
test_typeprop2.__code__ = test_typeprop2.__code__.replace(co_code=bytecode)
116+
test_typeprop2(0.1,1)
117+
118+
trigger_tier2(test_typeprop2, (0.1,1))
119+
expected = [
120+
"RESUME_QUICK",
121+
"LOAD_FAST",
122+
"LOAD_FAST",
123+
"SWAP",
124+
"COPY",
125+
126+
# Should gen specialised float
127+
"BINARY_CHECK_FLOAT",
128+
"NOP",
129+
"BB_BRANCH_IF_FLAG_UNSET",
130+
"BINARY_OP_ADD_FLOAT_UNBOXED",
131+
"SWAP",
132+
"COPY",
133+
134+
# Ladder of types guards
135+
"BINARY_CHECK_FLOAT",
136+
"NOP",
137+
"BB_BRANCH_IF_FLAG_SET",
138+
139+
# Should gen specialised int
140+
"BINARY_CHECK_INT",
141+
"NOP",
142+
"BB_BRANCH_IF_FLAG_UNSET",
143+
"BINARY_OP_ADD_INT_REST",
144+
# Don't care about the rest of the insts
145+
]
146+
insts = dis.get_instructions(test_typeprop2, tier2=True)
147+
# Assert the value is correct
148+
assert abs(test_typeprop2(0.1,1) - 2.2) < 0.001
149+
for x,y in zip(insts, expected):
150+
assert x.opname == y
151+
152+
153+
#######################################
154+
# Type guard #
155+
# + Float unboxing #
156+
# + Jump rewriting test #
157+
#######################################
158+
159+
def test_guard_elimination(a,b):
160+
x = b
161+
y = b
162+
# First a+x should inform the type prop that
163+
# `a`, `x`, `b` and `y` are int
164+
# So guard should be eliminated in (a+x) + y
165+
return a + x + y
166+
167+
trigger_tier2(test_guard_elimination, (0,0))
168+
expected = [
169+
# From tier1 bytecode
170+
"RESUME_QUICK",
171+
"LOAD_FAST",
172+
"STORE_FAST",
173+
"LOAD_FAST",
174+
"STORE_FAST",
175+
"LOAD_FAST",
176+
"LOAD_FAST",
177+
178+
"BINARY_CHECK_FLOAT", # First ladder check
179+
"NOP",
180+
"BB_BRANCH_IF_FLAG_SET",
181+
"BINARY_CHECK_INT", # Second ladder check
182+
"NOP",
183+
"BB_BRANCH_IF_FLAG_UNSET", # Fall through!
184+
185+
"BINARY_OP_ADD_INT_REST", # a+x
186+
"LOAD_FAST",
187+
"BINARY_OP_ADD_INT_REST", # (a+x) + y (guard eliminated)
188+
"RETURN_VALUE"
189+
]
190+
insts = dis.get_instructions(test_guard_elimination, tier2=True)
191+
for x,y in zip(insts, expected):
192+
assert x.opname == y
193+
194+
# We only wanna test the stability of the first type guards
195+
# later on
196+
first_guard_test_until = insts[-1].offset
197+
198+
# Trigger generation of other branch
199+
test_guard_elimination(0.1, 0.1)
200+
insts = dis.get_instructions(test_guard_elimination, tier2=True)
201+
expected = [
202+
# From tier1 bytecode
203+
"RESUME_QUICK",
204+
"LOAD_FAST",
205+
"STORE_FAST",
206+
"LOAD_FAST",
207+
"STORE_FAST",
208+
"LOAD_FAST",
209+
"LOAD_FAST",
210+
211+
"BINARY_CHECK_FLOAT", # First ladder check
212+
"NOP",
213+
"BB_JUMP_IF_FLAG_SET", # Rewrite to jump to float case
214+
"POP_TOP", # Pop result
215+
216+
# The same as above
217+
"BINARY_CHECK_INT",
218+
"NOP",
219+
"BB_BRANCH_IF_FLAG_UNSET",
220+
"BINARY_OP_ADD_INT_REST",
221+
"LOAD_FAST",
222+
"BINARY_OP_ADD_INT_REST",
223+
"RETURN_VALUE",
224+
225+
# Float case
226+
"BINARY_OP_ADD_FLOAT_UNBOXED", # Unbox
227+
"LOAD_FAST",
228+
"UNBOX_FLOAT", # Unbox local
229+
"STORE_FAST_UNBOXED_BOXED", # Store unboxed float into local
230+
"LOAD_FAST_NO_INCREF", # Load (unboxed) local again
231+
"BINARY_OP_ADD_FLOAT_UNBOXED", # No type guard here
232+
"BOX_FLOAT", # Box to return
233+
"RETURN_VALUE"
234+
]
235+
236+
test_guard_elimination(1,1)
237+
for x,y in zip(insts, expected):
238+
assert x.opname == y
239+
240+
# Perform other polymorphism stuff
241+
# We've not implemented type guard elimination
242+
# For these mixed types (e.g., float+int)
243+
# So these will generate more type guards with the same
244+
# mechanisms as above.
245+
# So codegen wise tier2 takes a while to stabilise
246+
assert (test_guard_elimination(1,0.1) - 1.2) < 0.001
247+
assert (test_guard_elimination(0.1,1) - 2.1) < 0.001
248+
assert (test_guard_elimination(.4,.5) - 1.4) < 0.001
249+
assert test_guard_elimination(2,3) == 8
250+
251+
# At this point all cases should be generated
252+
# so check if the generated cases are the same
253+
expected = dis.get_instructions(test_guard_elimination, tier2=True)
254+
test_guard_elimination(-192,203)
255+
test_guard_elimination(2.3, 12)
256+
test_guard_elimination(324, 0.12)
257+
test_guard_elimination(0.12,32.1)
258+
insts = dis.get_instructions(test_guard_elimination, tier2=True)
259+
260+
# Make sure the first type guard is stable
261+
for x,y in zip(insts, expected):
262+
if x.offset >= first_guard_test_until:
263+
break
264+
assert x.opname == y.opname
265+
266+
267+
######################
268+
# Backward jump test #
269+
# + loop peeling #
270+
######################
271+
272+
def test_backwards_jump(a):
273+
for i in range(64):
274+
a = i + a
275+
return a
276+
277+
# Trigger only one JUMP_BACKWARD_QUICK
278+
# i.e., perfect specialisation the first time
279+
trigger_tier2(test_backwards_jump, (0,))
280+
281+
# Make sure it looped 64 times
282+
assert test_backwards_jump(7) == 2023 # <-- Hi! ~ Jules
283+
284+
# Make sure it jumped to the correct spot
285+
insts = dis.get_instructions(test_backwards_jump, tier2=True)
286+
backwards_jump = next(x for x in insts if x.opname == "JUMP_BACKWARD_QUICK")
287+
instidx, jmp_target = next((i,x) for i,x in enumerate(insts) if x.offset == backwards_jump.argval)
288+
assert jmp_target.opname == "NOP" # Space for an EXTENDED_ARG
289+
assert insts[instidx + 1].opname == "BB_TEST_ITER_RANGE" # The loop predicate
290+
291+
292+
def test_loop_peeling(a):
293+
for i in range(64):
294+
a = float(i) + a
295+
return a
296+
297+
# This triggers loop peeling, because
298+
# the first iteration `a` type is int
299+
# and the 2nd iteration `a` type is float
300+
# This should triger a JUMP_FORWARD in place of
301+
# a JUMP_BACKWARD_QUICK
302+
trigger_tier2(test_loop_peeling, (0,))
303+
304+
# Make sure it looped 64 times
305+
assert abs(test_loop_peeling(7) - 2023) < 0.001
306+
307+
# Make sure the JUMP_FORWARD jumped correctly
308+
insts = dis.get_instructions(test_loop_peeling, tier2=True)
309+
forwards_jump = next(x for x in insts if x.opname == "JUMP_FORWARD")
310+
instidx, jmp_target = next((i,x) for i,x in enumerate(insts) if x.offset == forwards_jump.argval)
311+
assert jmp_target.opname == "NOP" # Space for an EXTENDED_ARG
312+
assert insts[instidx + 1].opname == "BB_TEST_ITER_RANGE" # The loop predicate
313+
314+
# We also need to make sure JUMP_FORWARD
315+
# jumped into the float-specialised loop body
316+
endidx, _ = next(
317+
(i,x) for i,x in enumerate(insts)
318+
if (x.opname == "JUMP_BACKWARD_QUICK" and x.offset > jmp_target.offset))
319+
# Check for existence of float-specialised instruction in loop body
320+
assert any(1 for _ in
321+
filter(lambda i: i.opname == 'BINARY_OP_ADD_FLOAT_UNBOXED', insts[instidx:endidx]))
322+

0 commit comments

Comments
 (0)