Add in basic throw, test code generation of mostly the same code as we tested the code graph with.

This commit is contained in:
Jennifer Taylor 2021-05-02 03:49:35 +00:00
parent 829597a871
commit 7493db034f
2 changed files with 278 additions and 4 deletions

View File

@ -217,6 +217,22 @@ class ReturnStatement(Statement):
return [f"{prefix}return {ret};"]
class ThrowStatement(Statement):
# A statement which raises an exception. It appears that there is no
# 'catch' in this version of bytecode so it must be used only as an
# assert.
def __init__(self, exc: Any) -> None:
self.exc = exc
def __repr__(self) -> str:
exc = value_ref(self.exc, "")
return f"throw {exc}"
def render(self, prefix: str) -> List[str]:
exc = value_ref(self.exc, prefix)
return [f"{prefix}throw {exc};"]
class NopStatement(Statement):
# A literal no-op. We will get rid of these in an optimizing pass.
def __repr__(self) -> str:
@ -2257,6 +2273,11 @@ class ByteCodeDecompiler(VerboseOutput):
chunk.actions[i] = ReturnStatement(retval)
continue
if action.opcode == AP2Action.THROW:
retval = get_stack()
chunk.actions[i] = ThrowStatement(retval)
continue
if action.opcode == AP2Action.POP:
# This is a discard. Let's see if its discarding a function or method
# call. If so, that means the return doesn't matter.
@ -2635,6 +2656,7 @@ class ByteCodeDecompiler(VerboseOutput):
# Calculate the statements for this chunk, as well as the leftover stack entries and any borrows.
self.vprint(f"Evaluating graph of ByteCodeChunk {chunk.id}")
new_statements, stack_leftovers, new_borrowed_entries = self.__eval_stack(chunk, stack, offset_map)
borrowed_entries.extend(new_borrowed_entries)
# We need to check and see if the last entry is an IfExpr, and hoist it
# into a statement here.
@ -2671,7 +2693,17 @@ class ByteCodeDecompiler(VerboseOutput):
# The stack for both of these is the leftovers from the previous evaluation as they
# rollover.
stacks[true_start] = [s for s in stack_leftovers]
true_statements, true_borrowed_entries = self.__eval_chunks_impl(true_start, if_body_chunk.true_chunks, next_chunk_id, stacks, insertables, other_stack_locs, offset_map)
true_statements, true_borrowed_entries = self.__eval_chunks_impl(
true_start,
if_body_chunk.true_chunks,
next_chunk_id,
stacks,
insertables,
other_stack_locs,
offset_map,
)
borrowed_entries.extend(true_borrowed_entries)
false_statements: List[Statement] = []
if if_body_chunk.false_chunks:
self.vprint(f"Evaluating graph of IfBody {if_body_chunk.id} false case")
@ -2682,7 +2714,16 @@ class ByteCodeDecompiler(VerboseOutput):
# The stack for both of these is the leftovers from the previous evaluation as they
# rollover.
stacks[false_start] = [s for s in stack_leftovers]
false_statements, false_borrowed_entries = self.__eval_chunks_impl(false_start, if_body_chunk.false_chunks, next_chunk_id, stacks, insertables, other_stack_locs, offset_map)
false_statements, false_borrowed_entries = self.__eval_chunks_impl(
false_start,
if_body_chunk.false_chunks,
next_chunk_id,
stacks,
insertables,
other_stack_locs,
offset_map,
)
borrowed_entries.extend(false_borrowed_entries)
# Convert this IfExpr to a full-blown IfStatement.
new_statements[-1] = IfStatement(
@ -2726,7 +2767,7 @@ class ByteCodeDecompiler(VerboseOutput):
break
start_id = chunk.next_chunks[0]
return statements, stack
return statements, borrowed_entries
def __walk(self, statements: Sequence[Statement], do: Callable[[Statement], Optional[Statement]]) -> List[Statement]:
new_statements: List[Statement] = []

View File

@ -4,7 +4,7 @@ from typing import Dict, List, Sequence, Tuple, Union
from bemani.tests.helpers import ExtendedTestCase
from bemani.format.afp.types.ap2 import AP2Action, IfAction, JumpAction, PushAction, Register
from bemani.format.afp.decompile import BitVector, ByteCode, ByteCodeChunk, ControlFlow, ByteCodeDecompiler
from bemani.format.afp.decompile import BitVector, ByteCode, ByteCodeChunk, ControlFlow, ByteCodeDecompiler, Statement
class TestAFPBitVector(unittest.TestCase):
@ -517,3 +517,236 @@ class TestAFPControlGraph(ExtendedTestCase):
self.assertEqual(self.__equiv(chunks_by_id[1]), ["102: PUSH\n 'b'\nEND_PUSH", "103: END"])
self.assertEqual(self.__equiv(chunks_by_id[2]), ["104: PUSH\n 'a'\nEND_PUSH", "105: END"])
self.assertEqual(self.__equiv(chunks_by_id[3]), [])
class TestAFPDecompile(ExtendedTestCase):
# Note that the offsets made up in these test functions are not realistic. Jump/If instructions
# take up more than one opcode, and the end offset might be more than one byte past the last
# action if that action takes up more than one byte. However, from the perspective of the
# decompiler, it doesn't care about accurate sizes, only that the offsets are correct.
def __make_bytecode(self, actions: Sequence[AP2Action]) -> ByteCode:
return ByteCode(
actions,
actions[-1].offset + 1,
)
def __call_decompile(self, bytecode: ByteCode) -> List[Statement]:
# Just create a dummy compiler so we can access the internal method for testing.
bcd = ByteCodeDecompiler(bytecode)
bcd.decompile()
return bcd.statements
def __equiv(self, statements: List[Statement]) -> List[str]:
return [str(x) for x in statements]
def test_simple_bytecode(self) -> None:
bytecode = self.__make_bytecode([
AP2Action(100, AP2Action.STOP),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ['builtin_StopPlaying()'])
def test_jump_handling(self) -> None:
bytecode = self.__make_bytecode([
JumpAction(100, 102),
JumpAction(101, 104),
JumpAction(102, 101),
JumpAction(103, 106),
JumpAction(104, 103),
JumpAction(105, 107),
JumpAction(106, 105),
AP2Action(107, AP2Action.STOP),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ['builtin_StopPlaying()'])
def test_dead_code_elimination_jump(self) -> None:
# Jump case
bytecode = self.__make_bytecode([
AP2Action(100, AP2Action.STOP),
JumpAction(101, 103),
AP2Action(102, AP2Action.PLAY),
AP2Action(103, AP2Action.STOP),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ['builtin_StopPlaying()', 'builtin_StopPlaying()'])
def test_dead_code_elimination_return(self) -> None:
# Return case
bytecode = self.__make_bytecode([
PushAction(100, ["strval"]),
AP2Action(101, AP2Action.RETURN),
AP2Action(102, AP2Action.STOP),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ["return 'strval'"])
def test_dead_code_elimination_end(self) -> None:
# Return case
bytecode = self.__make_bytecode([
AP2Action(100, AP2Action.STOP),
AP2Action(101, AP2Action.END),
AP2Action(102, AP2Action.END),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ['builtin_StopPlaying()'])
def test_dead_code_elimination_throw(self) -> None:
# Throw case
bytecode = self.__make_bytecode([
PushAction(100, ["exception"]),
AP2Action(101, AP2Action.THROW),
AP2Action(102, AP2Action.STOP),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ["throw 'exception'"])
def test_if_handling_basic(self) -> None:
# If by itself case.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_FALSE, 103),
# False case (fall through from if).
AP2Action(102, AP2Action.PLAY),
# Line after the if statement.
AP2Action(103, AP2Action.END),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ["if (True) {\n builtin_StartPlaying()\n}"])
def test_if_handling_basic_jump_to_end(self) -> None:
# If by itself case.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_FALSE, 103),
# False case (fall through from if).
AP2Action(102, AP2Action.PLAY),
# Some code will jump to the end offset as a way of
# "returning" early from a function.
])
statements = self.__call_decompile(bytecode)
# TODO: The output should be optimized to remove the early return and move the
# start playing section inside the if.
self.assertEqual(self.__equiv(statements), ["if (not True) {\n return\n}", "builtin_StartPlaying()"])
def test_if_handling_diamond(self) -> None:
# If true-false diamond case.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_TRUE, 104),
# False case (fall through from if).
AP2Action(102, AP2Action.STOP),
JumpAction(103, 105),
# True case.
AP2Action(104, AP2Action.PLAY),
# Line after the if statement.
AP2Action(105, AP2Action.END),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ["if (True) {\n builtin_StartPlaying()\n} else {\n builtin_StopPlaying()\n}"])
def test_if_handling_diamond_jump_to_end(self) -> None:
# If true-false diamond case.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_TRUE, 104),
# False case (fall through from if).
AP2Action(102, AP2Action.STOP),
JumpAction(103, 105),
# True case.
AP2Action(104, AP2Action.PLAY),
])
statements = self.__call_decompile(bytecode)
# TODO: The output should be optimized to remove redundant return statements.
self.assertEqual(self.__equiv(statements), ["if (True) {\n builtin_StartPlaying()\n return\n} else {\n builtin_StopPlaying()\n return\n}"])
def test_if_handling_diamond_return_to_end(self) -> None:
# If true-false diamond case but the cases never converge.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_TRUE, 104),
# False case (fall through from if).
PushAction(102, ['b']),
AP2Action(103, AP2Action.RETURN),
# True case.
PushAction(104, ['a']),
AP2Action(105, AP2Action.RETURN),
])
statements = self.__call_decompile(bytecode)
self.assertEqual(self.__equiv(statements), ["if (True) {\n return 'a'\n} else {\n return 'b'\n}"])
def test_if_handling_switch(self) -> None:
# Series of ifs (basically a switch statement).
bytecode = self.__make_bytecode([
# Beginning of the first if statement.
PushAction(100, [Register(0), 1]),
IfAction(101, IfAction.NOT_EQUALS, 104),
# False case (fall through from if).
PushAction(102, ['a']),
JumpAction(103, 113),
# Beginning of the second if statement.
PushAction(104, [Register(0), 2]),
IfAction(105, IfAction.NOT_EQUALS, 108),
# False case (fall through from if).
PushAction(106, ['b']),
JumpAction(107, 113),
# Beginning of the third if statement.
PushAction(108, [Register(0), 3]),
IfAction(109, IfAction.NOT_EQUALS, 112),
# False case (fall through from if).
PushAction(110, ['c']),
JumpAction(111, 113),
# Beginning of default case.
PushAction(112, ['d']),
# Line after the switch statement.
AP2Action(113, AP2Action.RETURN),
])
statements = self.__call_decompile(bytecode)
# TODO: This should be optimized as an if/elseif/else chunk without so much indentation.
self.assertEqual(self.__equiv(statements), [
"if (registers[0] != 1) {\n"
" if (registers[0] != 2) {\n"
" if (registers[0] != 3) {\n"
" tempvar_0 = 'd'\n"
" } else {\n"
" tempvar_0 = 'c'\n"
" }\n"
" } else {\n"
" tempvar_0 = 'b'\n"
" }\n"
"} else {\n"
" tempvar_0 = 'a'\n"
"}",
"return tempvar_0"
])
def test_if_handling_diamond_end_both_sides(self) -> None:
# If true-false diamond case but the cases never converge.
bytecode = self.__make_bytecode([
# Beginning of the if statement.
PushAction(100, [True]),
IfAction(101, IfAction.IS_TRUE, 104),
# False case (fall through from if).
AP2Action(102, AP2Action.STOP),
AP2Action(103, AP2Action.END),
# True case.
AP2Action(104, AP2Action.PLAY),
AP2Action(105, AP2Action.END),
])
statements = self.__call_decompile(bytecode)
# TODO: The output should be optimized to remove redundant return statements.
self.assertEqual(self.__equiv(statements), ["if (True) {\n builtin_StartPlaying()\n return\n} else {\n builtin_StopPlaying()\n return\n}"])