Annotate self test

This commit is contained in:
RunasSudo 2017-02-13 20:55:08 +10:30
parent 2982d12588
commit f260c3e4b5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 261 additions and 130 deletions

106
asm.py
View File

@ -25,112 +25,6 @@ parser.add_argument('file', help='.asm file to read')
parser.add_argument('output', help='.bin file to write')
args = parser.parse_args()
line_no = 0
def split_line(line):
tokens = []
token = ''
idx = 0
in_comment = False
in_string = False
in_escape = False
while idx < len(line):
if in_comment:
pass
elif in_string:
if in_escape:
token += line[idx]
else:
if line[idx] == '\\':
in_escape = True
token += line[idx]
elif line[idx] == '"':
in_string = False
token += line[idx]
else:
token += line[idx]
else:
if line[idx] == ' ':
if token != '':
tokens.append(token)
token = ''
elif line[idx] == '"':
in_string = True
token += line[idx]
elif line[idx] == ';':
in_comment = True
else:
token += line[idx]
idx += 1
# Final token
if token != '':
tokens.append(token)
return tokens
def unescape_char(char):
return char.encode('utf-8').decode('unicode_escape')
def assemble_next_instruction(source):
line = source.readline()
global line_no; line_no += 1
if line == '':
return None, []
tokens = split_line(line.strip())
return assemble_instruction(source, tokens)
def assemble_instruction(source, tokens):
if len(tokens) == 0:
return assemble_next_instruction(source)
if tokens[0].endswith(':'):
# Label
label = tokens[0][:-1]
instructions, inst_labels = assemble_instruction(source, tokens[1:])
return instructions, inst_labels + [label]
else:
# Instruction
name = tokens[0]
if name not in instructions_by_name:
raise Exception('Unknown instruction {}'.format(name))
instruction = instructions_by_name[name]()
# Special cases
if isinstance(instruction, InstructionOut) and tokens[1].startswith('"'):
chars = unescape_char(tokens[1][1:-1])
instructions = []
for char in chars:
instruction = InstructionOut()
instruction.args.append(OpLiteral(ord(char)))
instructions.append(instruction)
return instructions, []
elif isinstance(instruction, InstructionData):
if tokens[1].startswith('"'):
chars = unescape_char(tokens[1][1:-1])
instruction.args = [ord(char) for char in chars]
return [instruction], []
else:
instruction.args = [int(x, 16) for x in tokens[1:]]
return [instruction], []
else:
if len(tokens) != instruction.nargs + 1:
raise Exception('Invalid number of arguments: Expected {}, got {}'.format(instruction.nargs, len(tokens) - 1))
for i in range(instruction.nargs):
argstr = tokens[i + 1]
if argstr.startswith('R'):
# Register
arg = OpRegister(int(argstr[1:]))
elif argstr.startswith('$'):
# Label
arg = OpLabel(argstr[1:])
else:
# Hex literal
arg = OpLiteral(int(argstr, 16))
instruction.args.append(arg)
return [instruction], []
# First pass
labels = {}
SYN_MEM = [0] * 32768

View File

@ -28,7 +28,7 @@ with open(args.file, 'rb') as data:
SYN_MEM = memory_from_file(data)
# Find things to label
labels, comments_before, comments_inline = {}, {}, {}
labels, comments_before, comments_inline, replacements = {}, {}, {}, {}
SYN_PTR = 0
while SYN_PTR < len(SYN_MEM):
word = SYN_MEM[SYN_PTR]
@ -62,6 +62,9 @@ if args.hints:
elif line.startswith('call '):
loc = int(line.split()[1], 16)
labels['sub_{:04x}'.format(loc)] = loc
elif line.startswith('lbl '):
loc = int(line.split()[1], 16)
labels[line.split()[2]] = loc
elif line.startswith('ren '):
old_label = line.split()[1]
new_label = line.split()[2]
@ -73,18 +76,25 @@ if args.hints:
if loc not in comments_before:
comments_before[loc] = []
comments_before[loc].append(comment)
elif line.startswith('cmi '):
loc = int(line.split()[1], 16)
comment = line[line.index(' ', line.index(' ') + 1) + 1:].strip()
comments_inline[loc] = comment
elif line.startswith('rep '):
loc = int(line.split()[1], 16)
code = line[line.index(' ', line.index(' ') + 1) + 1:].strip()
instruction = assemble_line(None, code)[0][0]
replacements[loc] = instruction
else:
raise Exception('Invalid line in hint file: {}'.format(line))
def escape_char(char):
return char.encode('unicode_escape').decode('utf-8').replace('"', '\\"')
MODE_OUT = False
MODE_DAT = False #False, 1 (data), 2 (text)
SYN_PTR = 0
while SYN_PTR < len(SYN_MEM):
# Handle comments
if SYN_PTR in comments_before:
if MODE_OUT:
print('"')
@ -97,6 +107,12 @@ while SYN_PTR < len(SYN_MEM):
MODE_DAT = False
for comment in comments_before[SYN_PTR]:
print('; {}'.format(comment))
if SYN_PTR in comments_inline:
comment_inline = ' ; {}'.format(comments_inline[SYN_PTR])
else:
comment_inline = ''
# Handle labels
if any(v == SYN_PTR for k, v in labels.items()):
if MODE_OUT:
print('"')
@ -109,6 +125,13 @@ while SYN_PTR < len(SYN_MEM):
MODE_DAT = False
print('${}:'.format(next(k for k, v in labels.items() if v == SYN_PTR)))
# Handle replacements
if SYN_PTR in replacements:
instruction = replacements[SYN_PTR]
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
SYN_PTR += len(instruction.assemble(None))
continue
word = SYN_MEM[SYN_PTR]
if MODE_OUT and word != 19:
@ -162,24 +185,18 @@ while SYN_PTR < len(SYN_MEM):
if MODE_OUT:
print('"')
MODE_OUT = False
print('{:04x}: {}'.format(SYN_PTR, instruction.describe()))
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
elif isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionJt) or isinstance(instruction, InstructionJf) or isinstance(instruction, InstructionCall):
if isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionCall):
op = instruction.args[0]
argidx = 0
else:
op = instruction.args[1]
if isinstance(op, OpLiteral):
loc = op.get(None)
argidx = 1
if isinstance(instruction.args[argidx], OpLiteral):
loc = instruction.args[argidx].get(None)
if any(v == loc for k, v in labels.items()):
label = next(k for k, v in labels.items() if v == loc)
if isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionCall):
print('{:04x}: {: <4} ${}'.format(SYN_PTR, instruction.name, label))
else:
print('{:04x}: {: <4} {} ${}'.format(SYN_PTR, instruction.name, instruction.args[0].describe(), label))
else:
print('{:04x}: {}'.format(SYN_PTR, instruction.describe()))
else:
print('{:04x}: {}'.format(SYN_PTR, instruction.describe()))
instruction.args[argidx] = OpLabel(label)
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
else:
print('{:04x}: {}'.format(SYN_PTR, instruction.describe()))
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
SYN_PTR = next_SYN_PTR

View File

@ -1,6 +1,94 @@
jmp 0000
ren label_0000 start
jmp 0110
ren label_0110 self_test
cmb 0140 Test jmp
ren label_015b self_test_jmp1
cmi 0142 jmp lands here if fails
cmi 0162 jmp from 0160 lands here if -2
cmi 0164 jmp from 0160 lands here if -1
cmi 0166 jmp from 0160 lands here if correct
cmi 0168 jmp from 0160 lands here if +1
cmi 016a jmp from 0160 lands here if +2
ren label_0166 self_test_jmp2
ren label_0170 self_test_jmp_diagnose-2
ren label_018d self_test_jmp_diagnose-1
ren label_01a8 self_test_jmp_diagnose+1
ren label_01c5 self_test_jmp_diagnose+2
cmi 016e jmp from 0162 lands here
cmi 018c jmp from 0164 lands here
cmi 01a9 jmp from 0168 lands here
cmi 01c7 jmp from 016a lands here
cmb 01e4 Test jt/jf
cmb 01e4 ?? Does not test behaviour for values other than 0 and 1
ren label_01e4 self_test_jtjf
ren label_0432 self_test_jtjf_fail
cmi 01e4 Jump to fail if 0 is true
cmi 01e7 Jump to fail if 1 is false
ren label_01ef self_test_jtjf2
ren label_01f4 self_test_regzero
cmi 01ed Fail if 1 is not true
cmi 01f2 Fail if 0 is not false
cmb 01f4 Test that all registers are zero
cmb 01f4 ?? Because this uses jt/jf, errors may not be detected if involving values other than 0 and 1
ren label_0445 self_test_regzero_fail
cmb 020c Test set
ren label_045e self_test_set_fail
cmb 0218 Test add
cmb 0218 ?? This only tests if 1 + 1 != 0, and will fail to detect almost all other errors
cmi 021c Dodgy!
ren label_0234 self_test_add
cmb 0234 Test add
cmb 0234 ?? This reuses the result from the add test, so will erroneously report an eq failure (instead of an add failure) if 1 + 1 != 2
cmb 0234 It would probably have been a better idea to test eq first, then use that to verify add
cmi 0238 Dodgy!
ren label_024e self_test_pushpop
cmb 024e Test push/pop by exchanging R0 and R1
cmb 024e ?? Because R1 is reused, the test will erroneously report a push/pop failure (instead of an eq failure) if eq returns any other truthy value in the previous test
cmi 025d Dodgy!
ren label_0486 self_test_pushpop_fail
cmb 0264 Test gt
ren label_0473 self_test_gt_fail
cmb 0279 Test and
ren label_0499 self_test_and_fail
cmb 0284 Test or
cmb 02ac Test not
ren label_02ac self_test_not
ren label_04b8 self_test_not_fail
cmb 02c0 Test call
ren sub_0505 self_test_call_subroutine1
lbl 02c2 self_test_call_returnloc1
ren label_0509 self_test_call_fail
ren label_02c4 self_test_call_check_stack1
rep 02c6 eq R1 R0 $self_test_call_check_stack1
cmi 02c6 This test is strange. If R0 == 02c2 as tested below, R0 != 02c4 is guaranteed
rep 02cd eq R1 R0 $self_test_call_returnloc1
cmb 02d4 Test call register value
ren sub_0507 self_test_call_subroutine2
lbl 02d9 self_test_call_returnloc2
rep 02d4 set R0 $self_test_call_subroutine2
ren label_02db self_test_call_check_stack2
rep 02dd eq R1 R0 $self_test_call_check_stack2
rep 02e4 eq R1 R0 $self_test_call_returnloc2
cmb 02eb Test add overflow
ren label_0520 self_test_overflow_fail
cmi 0304 Just to be safe!
cmb 030b Test mult
cmi 030f Test for HHGTTG (non-)compatibility
ren label_0565 self_test_hhgttg_fail
ren label_0586 self_test_mult_fail
cmb 0328 Test mod
ren label_059d self_test_mod_fail
ren label_034d self_test_rwmem
lbl 034b self_test_rwmem_data
cmb 034b Test rmem/wmem
ren label_04d7 self_test_rmem_fail
ren label_04ee self_test_wmem_fail
rep 034d rmem R0 $self_test_rwmem_data
rep 0357 add R2 $self_test_rwmem_data 0001
rep 0365 set R0 $self_test_rwmem_data
ren sub_06bb decrypt_data
cmi 0375 Sneaky!
lbl 17b4 encrypted_data
rep 0377 rmem R0 $encrypted_data
rep 0381 add R2 $encrypted_data 0001
ren label_03ad self_test_wmem_cmd_fail
rep 03ab out $self_test_complete
cmi 03a9 Becomes noop; jt 0013 $self_test_complete
lbl 03d2 self_test_complete

View File

@ -16,4 +16,5 @@
from .binfile import *
from .bytecode import *
from .assembly import *
from .cpu import *

129
libsynacor/assembly.py Normal file
View File

@ -0,0 +1,129 @@
# synacor.py - An implementation of the Synacor Challenge
# Copyright © 2017 RunasSudo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from libsynacor import *
def escape_char(char):
return char.encode('unicode_escape').decode('utf-8').replace('"', '\\"')
def unescape_char(char):
return char.encode('utf-8').decode('unicode_escape')
def split_line(line):
tokens = []
token = ''
idx = 0
in_comment = False
in_string = False
in_escape = False
while idx < len(line):
if in_comment:
pass
elif in_string:
if in_escape:
token += line[idx]
else:
if line[idx] == '\\':
in_escape = True
token += line[idx]
elif line[idx] == '"':
in_string = False
token += line[idx]
else:
token += line[idx]
else:
if line[idx] == ' ':
if token != '':
tokens.append(token)
token = ''
elif line[idx] == '"':
in_string = True
token += line[idx]
elif line[idx] == ';':
in_comment = True
else:
token += line[idx]
idx += 1
# Final token
if token != '':
tokens.append(token)
return tokens
line_no = 0
def assemble_next_instruction(source):
line = source.readline()
global line_no; line_no += 1
if line == '':
return None, []
return assemble_line(source, line)
def assemble_line(source, line):
tokens = split_line(line.strip())
return assemble_instruction(source, tokens)
def assemble_instruction(source, tokens):
if len(tokens) == 0:
return assemble_next_instruction(source)
if tokens[0].endswith(':'):
# Label
label = tokens[0][:-1]
instructions, inst_labels = assemble_instruction(source, tokens[1:])
return instructions, inst_labels + [label]
else:
# Instruction
name = tokens[0]
if name not in instructions_by_name:
raise Exception('Unknown instruction {}'.format(name))
instruction = instructions_by_name[name]()
# Special cases
if isinstance(instruction, InstructionOut) and tokens[1].startswith('"'):
chars = unescape_char(tokens[1][1:-1])
instructions = []
for char in chars:
instruction = InstructionOut()
instruction.args.append(OpLiteral(ord(char)))
instructions.append(instruction)
return instructions, []
elif isinstance(instruction, InstructionData):
if tokens[1].startswith('"'):
chars = unescape_char(tokens[1][1:-1])
instruction.args = [ord(char) for char in chars]
return [instruction], []
else:
instruction.args = [int(x, 16) for x in tokens[1:]]
return [instruction], []
else:
if len(tokens) != instruction.nargs + 1:
raise Exception('Invalid number of arguments: Expected {}, got {}'.format(instruction.nargs, len(tokens) - 1))
for i in range(instruction.nargs):
argstr = tokens[i + 1]
if argstr.startswith('R'):
# Register
arg = OpRegister(int(argstr[1:]))
elif argstr.startswith('$'):
# Label
arg = OpLabel(argstr[1:])
else:
# Hex literal
arg = OpLiteral(int(argstr, 16))
instruction.args.append(arg)
return [instruction], []

View File

@ -49,11 +49,13 @@ class OpRegister(Operand):
def assemble(self, labels):
return self.register + 32768
# Used only in assembling process
# Used only in dis/assembling process
class OpLabel(Operand):
def __init__(self, label):
self.label = label
def describe(self):
return '${}'.format(self.label)
def assemble(self, labels):
if labels is None:
# First pass
@ -85,7 +87,7 @@ class Instruction:
description = '{: <4}'.format(self.name)
for i in range(self.nargs):
description += ' {}'.format(self.args[i].describe())
return description
return description.strip()
def assemble(self, labels):
return [self.opcode] + [self.args[i].assemble(labels) for i in range(self.nargs)]