From 4d4d238fcea2cb4e3be1c6b9acffcaffb9375788 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 12 Apr 2023 01:13:16 +0800 Subject: [PATCH 1/3] Tests: Modified dis.py and added primitive tests --- CS4215.md | 11 +- Lib/dis.py | 10 +- tier2_test.py | 322 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 tier2_test.py diff --git a/CS4215.md b/CS4215.md index 5cba2b2c0f459e..e3a70468a7fd78 100644 --- a/CS4215.md +++ b/CS4215.md @@ -16,5 +16,12 @@ language, our interpreter cannot be used as a bootstrap Python. # Where are files located? -The majority of the changes and functionality are in `Python/tier2.c`. Doxygen documentation -is written alongside the code. \ No newline at end of file +The majority of the changes and functionality are in `Python/tier2.c` where Doxygen documentation +is written alongside the code, and in `Tools/cases_generator/` which contains the DSL implementation. + +# Running tests + +We've written simple tests of the main functionalities. +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. + +After building, run `python tier2_test.py` in the repository's root folder. \ No newline at end of file diff --git a/Lib/dis.py b/Lib/dis.py index 65fd696012718f..501035389a1d81 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -474,6 +474,7 @@ def _get_instructions_bytes(code, varname_from_oparg=None, for i in range(start, end): labels.add(target) starts_line = None + ret = [] for offset, op, arg in _unpack_opargs(code): if linestarts is not None: starts_line = linestarts.get(offset, None) @@ -534,9 +535,9 @@ def _get_instructions_bytes(code, varname_from_oparg=None, if arg & (1<locals[0], int->stack[0], int->stack[1]] + "BINARY_CHECK_INT", + "NOP", + "BB_BRANCH_IF_FLAG_UNSET", # Fallthrough! + + # Should propagate the result as int + # TYPE_OVERWRITE + # Locals: [int] + # Stack : [int->locals[0], int] + "BINARY_OP_ADD_INT_REST", + + # There should be no more guards here + # if the type propagator is working + "BINARY_OP_ADD_INT_REST", + "RETURN_VALUE" +] +insts = dis.get_instructions(test_typeprop1, tier2=True) +for x,y in zip(insts, expected): + assert x.opname == y + +bytecode = b"".join([ + # Tests TYPE_SWAP + writeinst("RESUME", 0), + writeinst("LOAD_FAST", 0), # float + writeinst("LOAD_FAST", 1), # int + writeinst("SWAP", 2), # Stack: [int, float] + + writeinst("COPY", 1), + # Should generate the FLOAT specialisation + writeinst("BINARY_OP", 0), + writeinst("CACHE", 0), # For tier1 + + writeinst("SWAP", 2), # [float, int] + writeinst("COPY", 1), + # Should generate the INT specialisation + writeinst("BINARY_OP", 0), + writeinst("CACHE", 0), # For tier1 + + # float + int + writeinst("BINARY_OP", 0), + writeinst("CACHE", 0), # For tier1 + writeinst("RETURN_VALUE", 0) +]) + +def test_typeprop2(a,b): + # Dummy code won't be ran + return a+(a+(a+a)) + +# Switch to bytecode +test_typeprop2.__code__ = test_typeprop2.__code__.replace(co_code=bytecode) +test_typeprop2(0.1,1) + +trigger_tier2(test_typeprop2, (0.1,1)) +expected = [ + "RESUME_QUICK", + "LOAD_FAST", + "LOAD_FAST", + "SWAP", + "COPY", + + # Should gen specialised float + "BINARY_CHECK_FLOAT", + "NOP", + "BB_BRANCH_IF_FLAG_UNSET", + "BINARY_OP_ADD_FLOAT_UNBOXED", + "SWAP", + "COPY", + + # Ladder of types guards + "BINARY_CHECK_FLOAT", + "NOP", + "BB_BRANCH_IF_FLAG_SET", + + # Should gen specialised int + "BINARY_CHECK_INT", + "NOP", + "BB_BRANCH_IF_FLAG_UNSET", + "BINARY_OP_ADD_INT_REST", + # Don't care about the rest of the insts +] +insts = dis.get_instructions(test_typeprop2, tier2=True) +# Assert the value is correct +assert abs(test_typeprop2(0.1,1) - 2.2) < 0.001 +for x,y in zip(insts, expected): + assert x.opname == y + + +####################################### +# Type guard # +# + Float unboxing # +# + Jump rewriting test # +####################################### + +def test_guard_elimination(a,b): + x = b + y = b + # First a+x should inform the type prop that + # `a`, `x`, `b` and `y` are int + # So guard should be eliminated in (a+x) + y + return a + x + y + +trigger_tier2(test_guard_elimination, (0,0)) +expected = [ + # From tier1 bytecode + "RESUME_QUICK", + "LOAD_FAST", + "STORE_FAST", + "LOAD_FAST", + "STORE_FAST", + "LOAD_FAST", + "LOAD_FAST", + + "BINARY_CHECK_FLOAT", # First ladder check + "NOP", + "BB_BRANCH_IF_FLAG_SET", + "BINARY_CHECK_INT", # Second ladder check + "NOP", + "BB_BRANCH_IF_FLAG_UNSET", # Fall through! + + "BINARY_OP_ADD_INT_REST", # a+x + "LOAD_FAST", + "BINARY_OP_ADD_INT_REST", # (a+x) + y (guard eliminated) + "RETURN_VALUE" +] +insts = dis.get_instructions(test_guard_elimination, tier2=True) +for x,y in zip(insts, expected): + assert x.opname == y + +# We only wanna test the stability of the first type guards +# later on +first_guard_test_until = insts[-1].offset + +# Trigger generation of other branch +test_guard_elimination(0.1, 0.1) +insts = dis.get_instructions(test_guard_elimination, tier2=True) +expected = [ + # From tier1 bytecode + "RESUME_QUICK", + "LOAD_FAST", + "STORE_FAST", + "LOAD_FAST", + "STORE_FAST", + "LOAD_FAST", + "LOAD_FAST", + + "BINARY_CHECK_FLOAT", # First ladder check + "NOP", + "BB_JUMP_IF_FLAG_SET", # Rewrite to jump to float case + "POP_TOP", # Pop result + + # The same as above + "BINARY_CHECK_INT", + "NOP", + "BB_BRANCH_IF_FLAG_UNSET", + "BINARY_OP_ADD_INT_REST", + "LOAD_FAST", + "BINARY_OP_ADD_INT_REST", + "RETURN_VALUE", + + # Float case + "BINARY_OP_ADD_FLOAT_UNBOXED", # Unbox + "LOAD_FAST", + "UNBOX_FLOAT", # Unbox local + "STORE_FAST_UNBOXED_BOXED", # Store unboxed float into local + "LOAD_FAST_NO_INCREF", # Load (unboxed) local again + "BINARY_OP_ADD_FLOAT_UNBOXED", # No type guard here + "BOX_FLOAT", # Box to return + "RETURN_VALUE" +] + +test_guard_elimination(1,1) +for x,y in zip(insts, expected): + assert x.opname == y + +# Perform other polymorphism stuff +# We've not implemented type guard elimination +# For these mixed types (e.g., float+int) +# So these will generate more type guards with the same +# mechanisms as above. +# So codegen wise tier2 takes a while to stabilise +assert (test_guard_elimination(1,0.1) - 1.2) < 0.001 +assert (test_guard_elimination(0.1,1) - 2.1) < 0.001 +assert (test_guard_elimination(.4,.5) - 1.4) < 0.001 +assert test_guard_elimination(2,3) == 8 + +# At this point all cases should be generated +# so check if the generated cases are the same +expected = dis.get_instructions(test_guard_elimination, tier2=True) +test_guard_elimination(-192,203) +test_guard_elimination(2.3, 12) +test_guard_elimination(324, 0.12) +test_guard_elimination(0.12,32.1) +insts = dis.get_instructions(test_guard_elimination, tier2=True) + +# Make sure the first type guard is stable +for x,y in zip(insts, expected): + if x.offset >= first_guard_test_until: + break + assert x.opname == y.opname + + +###################### +# Backward jump test # +# + loop peeling # +###################### + +def test_backwards_jump(a): + for i in range(64): + a = i + a + return a + +# Trigger only one JUMP_BACKWARD_QUICK +# i.e., perfect specialisation the first time +trigger_tier2(test_backwards_jump, (0,)) + +# Make sure it looped 64 times +assert test_backwards_jump(7) == 2023 # <-- Hi! ~ Jules + +# Make sure it jumped to the correct spot +insts = dis.get_instructions(test_backwards_jump, tier2=True) +backwards_jump = next(x for x in insts if x.opname == "JUMP_BACKWARD_QUICK") +instidx, jmp_target = next((i,x) for i,x in enumerate(insts) if x.offset == backwards_jump.argval) +assert jmp_target.opname == "NOP" # Space for an EXTENDED_ARG +assert insts[instidx + 1].opname == "BB_TEST_ITER_RANGE" # The loop predicate + + +def test_loop_peeling(a): + for i in range(64): + a = float(i) + a + return a + +# This triggers loop peeling, because +# the first iteration `a` type is int +# and the 2nd iteration `b` type is float +# This should triger a JUMP_FORWARD in place of +# a JUMP_BACKWARD_QUICK +trigger_tier2(test_loop_peeling, (0,)) + +# Make sure it looped 64 times +assert abs(test_loop_peeling(7) - 2023) < 0.001 + +# Make sure the JUMP_FORWARD jumped correctly +insts = dis.get_instructions(test_loop_peeling, tier2=True) +forwards_jump = next(x for x in insts if x.opname == "JUMP_FORWARD") +instidx, jmp_target = next((i,x) for i,x in enumerate(insts) if x.offset == forwards_jump.argval) +assert jmp_target.opname == "NOP" # Space for an EXTENDED_ARG +assert insts[instidx + 1].opname == "BB_TEST_ITER_RANGE" # The loop predicate + +# We also need to make sure JUMP_FORWARD +# jumped into the float-specialised loop body +endidx, endfor = next( + (i,x) for i,x in enumerate(insts) + if (x.opname == "END_FOR" and x.offset > jmp_target.offset)) +# Check for existence of float-specialised instruction in loop body +assert any(1 for _ in + filter(lambda i: i.opname == 'BINARY_OP_ADD_FLOAT_UNBOXED', insts[instidx:endidx])) + From 5e2b22c30547b8bbd0962526d6e8a7f102c9c5ec Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 12 Apr 2023 01:19:29 +0800 Subject: [PATCH 2/3] Docs: Typos and more documentation --- CS4215.md | 15 +++++++++++++-- Python/tier2.c | 4 ++-- tier2_test.py | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CS4215.md b/CS4215.md index e3a70468a7fd78..78a71fdd52125d 100644 --- a/CS4215.md +++ b/CS4215.md @@ -22,6 +22,17 @@ is written alongside the code, and in `Tools/cases_generator/` which contains th # Running tests We've written simple tests of the main functionalities. -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. +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. -After building, run `python tier2_test.py` in the repository's root folder. \ No newline at end of file +After building, run `python tier2_test.py` in the repository's root folder. + +# Debugging output + +In `tier2.c`, two flags can be set to print debug messages: +```c +// Prints codegen debug messages +#define BB_DEBUG 0 + +// Prints typeprop debug messages +#define TYPEPROP_DEBUG 0 +``` \ No newline at end of file diff --git a/Python/tier2.c b/Python/tier2.c index fc3768bbb7039e..847939d19f1e6b 100644 --- a/Python/tier2.c +++ b/Python/tier2.c @@ -9,8 +9,8 @@ #include "opcode.h" -#define BB_DEBUG 1 -#define TYPEPROP_DEBUG 1 +#define BB_DEBUG 0 +#define TYPEPROP_DEBUG 0 // Max typed version basic blocks per basic block #define MAX_BB_VERSIONS 10 diff --git a/tier2_test.py b/tier2_test.py index 46623b1d37d2a8..3a98542eaf461b 100644 --- a/tier2_test.py +++ b/tier2_test.py @@ -296,7 +296,7 @@ def test_loop_peeling(a): # This triggers loop peeling, because # the first iteration `a` type is int -# and the 2nd iteration `b` type is float +# and the 2nd iteration `a` type is float # This should triger a JUMP_FORWARD in place of # a JUMP_BACKWARD_QUICK trigger_tier2(test_loop_peeling, (0,)) From 37b1e5bd5da51bb8d1c4fd04b36a2eb9f3649c57 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 12 Apr 2023 10:33:41 +0800 Subject: [PATCH 3/3] Test: Fixed minor issue --- tier2_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tier2_test.py b/tier2_test.py index 3a98542eaf461b..499de3b47195a0 100644 --- a/tier2_test.py +++ b/tier2_test.py @@ -313,9 +313,9 @@ def test_loop_peeling(a): # We also need to make sure JUMP_FORWARD # jumped into the float-specialised loop body -endidx, endfor = next( +endidx, _ = next( (i,x) for i,x in enumerate(insts) - if (x.opname == "END_FOR" and x.offset > jmp_target.offset)) + if (x.opname == "JUMP_BACKWARD_QUICK" and x.offset > jmp_target.offset)) # Check for existence of float-specialised instruction in loop body assert any(1 for _ in filter(lambda i: i.opname == 'BINARY_OP_ADD_FLOAT_UNBOXED', insts[instidx:endidx]))