The 0xBE prefix byte triggers extended opcode parsing. Reads the opcode number from the next byte, then parses operand types using the same format as VAR opcodes. Required for all V5+ games.
253 lines
8.5 KiB
Python
253 lines
8.5 KiB
Python
#
|
|
# A class which represents the Program Counter and decodes instructions
|
|
# to be executed by the ZPU. Implements section 4 of Z-code specification.
|
|
#
|
|
# For the license of this file, please consult the LICENSE file in the
|
|
# root directory of this distribution.
|
|
#
|
|
|
|
from .bitfield import BitField
|
|
from .zlogging import log
|
|
|
|
|
|
class ZOperationError(Exception):
|
|
"General exception for ZOperation class"
|
|
|
|
pass
|
|
|
|
|
|
# Constants defining the known instruction types. These types are
|
|
# related to the number of operands the opcode has: for each operand
|
|
# count, there is a separate opcode table, and the actual opcode
|
|
# number is an index into that table.
|
|
OPCODE_0OP = 0
|
|
OPCODE_1OP = 1
|
|
OPCODE_2OP = 2
|
|
OPCODE_VAR = 3
|
|
OPCODE_EXT = 4
|
|
|
|
# Mapping of those constants to strings describing the opcode
|
|
# classes. Used for pretty-printing only.
|
|
OPCODE_STRINGS = {
|
|
OPCODE_0OP: "0OP",
|
|
OPCODE_1OP: "1OP",
|
|
OPCODE_2OP: "2OP",
|
|
OPCODE_VAR: "VAR",
|
|
OPCODE_EXT: "EXT",
|
|
}
|
|
|
|
# Constants defining the possible operand types.
|
|
LARGE_CONSTANT = 0x0
|
|
SMALL_CONSTANT = 0x1
|
|
VARIABLE = 0x2
|
|
ABSENT = 0x3
|
|
|
|
|
|
class ZOpDecoder:
|
|
def __init__(self, zmem, zstack):
|
|
""
|
|
self._memory = zmem
|
|
self._stack = zstack
|
|
self._parse_map = {}
|
|
self.program_counter = self._memory.read_word(0x6)
|
|
|
|
def _get_pc(self):
|
|
byte = self._memory[self.program_counter]
|
|
self.program_counter += 1
|
|
return byte
|
|
|
|
def get_next_instruction(self):
|
|
"""Decode the opcode & operands currently pointed to by the
|
|
program counter, and appropriately increment the program counter
|
|
afterwards. A decoded operation is returned to the caller in the form:
|
|
|
|
[opcode-class, opcode-number, [operand, operand, operand, ...]]
|
|
|
|
If the opcode has no operands, the operand list is present but empty."""
|
|
|
|
opcode = self._get_pc()
|
|
|
|
log(f"Decode opcode {opcode:x}")
|
|
|
|
# Determine the opcode type, and hand off further parsing.
|
|
if self._memory.version >= 5 and opcode == 0xBE:
|
|
# Extended opcode
|
|
return self._parse_opcode_extended()
|
|
|
|
opcode = BitField(opcode)
|
|
if opcode[7] == 0:
|
|
# Long opcode
|
|
return self._parse_opcode_long(opcode)
|
|
elif opcode[6] == 0:
|
|
# Short opcode
|
|
return self._parse_opcode_short(opcode)
|
|
else:
|
|
# Variable opcode
|
|
return self._parse_opcode_variable(opcode)
|
|
|
|
def _parse_opcode_long(self, opcode):
|
|
"""Parse an opcode of the long form."""
|
|
# Long opcodes are always 2OP. The types of the two operands are
|
|
# encoded in bits 5 and 6 of the opcode.
|
|
log("Opcode is long")
|
|
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
|
operands = [
|
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]]),
|
|
]
|
|
return (OPCODE_2OP, opcode[0:5], operands)
|
|
|
|
def _parse_opcode_short(self, opcode):
|
|
"""Parse an opcode of the short form."""
|
|
# Short opcodes can have either 1 operand, or no operand.
|
|
log("Opcode is short")
|
|
operand_type = opcode[4:6]
|
|
operand = self._parse_operand(operand_type)
|
|
if operand is None: # 0OP variant
|
|
log("Opcode is 0OP variant")
|
|
return (OPCODE_0OP, opcode[0:4], [])
|
|
else:
|
|
log("Opcode is 1OP variant")
|
|
return (OPCODE_1OP, opcode[0:4], [operand])
|
|
|
|
def _parse_opcode_variable(self, opcode):
|
|
"""Parse an opcode of the variable form."""
|
|
log("Opcode is variable")
|
|
if opcode[5]:
|
|
log("Variable opcode of VAR kind")
|
|
opcode_type = OPCODE_VAR
|
|
else:
|
|
log("Variable opcode of 2OP kind")
|
|
opcode_type = OPCODE_2OP
|
|
|
|
opcode_num = opcode[0:5]
|
|
|
|
# Parse the types byte to retrieve the operands.
|
|
operands = self._parse_operands_byte()
|
|
|
|
# Special case: opcodes 12 and 26 have a second operands byte.
|
|
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
|
|
log("Opcode has second operand byte")
|
|
operands += self._parse_operands_byte()
|
|
|
|
return (opcode_type, opcode_num, operands)
|
|
|
|
def _parse_opcode_extended(self):
|
|
"""Parse an extended opcode (v5+ feature)."""
|
|
log("Opcode is extended")
|
|
|
|
# Read the extended opcode number
|
|
opcode_num = self._get_pc()
|
|
log(f"Extended opcode number: {opcode_num:x}")
|
|
|
|
# Parse the operand types byte and retrieve operands
|
|
operands = self._parse_operands_byte()
|
|
|
|
return (OPCODE_EXT, opcode_num, operands)
|
|
|
|
def _parse_operand(self, operand_type):
|
|
"""Read and return an operand of the given type.
|
|
|
|
This assumes that the operand is in memory, at the address pointed
|
|
by the Program Counter."""
|
|
assert operand_type <= 0x3
|
|
|
|
if operand_type == LARGE_CONSTANT:
|
|
log("Operand is large constant")
|
|
operand = self._memory.read_word(self.program_counter)
|
|
self.program_counter += 2
|
|
elif operand_type == SMALL_CONSTANT:
|
|
log("Operand is small constant")
|
|
operand = self._get_pc()
|
|
elif operand_type == VARIABLE:
|
|
variable_number = self._get_pc()
|
|
log(f"Operand is variable {variable_number}")
|
|
if variable_number == 0:
|
|
log("Operand value comes from stack")
|
|
operand = self._stack.pop_stack() # TODO: make sure this is right.
|
|
elif variable_number < 16:
|
|
log("Operand value comes from local variable")
|
|
operand = self._stack.get_local_variable(variable_number - 1)
|
|
else:
|
|
log("Operand value comes from global variable")
|
|
operand = self._memory.read_global(variable_number)
|
|
elif operand_type == ABSENT:
|
|
log("Operand is absent")
|
|
operand = None
|
|
if operand is not None:
|
|
log(f"Operand value: {operand}")
|
|
|
|
return operand
|
|
|
|
def _parse_operands_byte(self):
|
|
"""Parse operands given by the operand byte and return a list of
|
|
values.
|
|
"""
|
|
operand_byte = BitField(self._get_pc())
|
|
operands = []
|
|
for operand_type in [
|
|
operand_byte[6:8],
|
|
operand_byte[4:6],
|
|
operand_byte[2:4],
|
|
operand_byte[0:2],
|
|
]:
|
|
operand = self._parse_operand(operand_type)
|
|
if operand is None:
|
|
break
|
|
operands.append(operand)
|
|
|
|
return operands
|
|
|
|
# Public funcs that the ZPU may also need to call, depending on the
|
|
# opcode being executed:
|
|
|
|
def get_zstring(self):
|
|
"""For string opcodes, return the address of the zstring pointed
|
|
to by the PC. Increment PC just past the text."""
|
|
|
|
start_addr = self.program_counter
|
|
bf = BitField(0)
|
|
|
|
while True:
|
|
bf.__init__(self._memory[self.program_counter])
|
|
self.program_counter += 2
|
|
if bf[7] == 1:
|
|
break
|
|
|
|
return start_addr
|
|
|
|
def get_store_address(self):
|
|
"""For store opcodes, read byte pointed to by PC and return the
|
|
variable number in which the operation result should be stored.
|
|
Increment the PC as necessary."""
|
|
return self._get_pc()
|
|
|
|
def get_branch_offset(self):
|
|
"""For branching opcodes, examine address pointed to by PC, and
|
|
return two values: first, either True or False (indicating whether
|
|
to branch if true or branch if false), and second, the address to
|
|
jump to. Increment the PC as necessary."""
|
|
|
|
bf = BitField(self._get_pc())
|
|
branch_if_true = bool(bf[7])
|
|
if bf[6]:
|
|
branch_offset = bf[0:6]
|
|
else:
|
|
# We need to do a little magic here. The branch offset is
|
|
# written as a signed 14-bit number, with signed meaning '-n' is
|
|
# written as '65536-n'. Or in this case, as we have 14 bits,
|
|
# '16384-n'.
|
|
#
|
|
# So, if the MSB (ie. bit 13) is set, we have a negative
|
|
# number. We take the value, and substract 16384 to get the
|
|
# actual offset as a negative integer.
|
|
#
|
|
# If the MSB is not set, we just extract the value and return it.
|
|
#
|
|
# Can you spell "Weird" ?
|
|
branch_offset = self._get_pc() + (bf[0:5] << 8)
|
|
if bf[5]:
|
|
branch_offset -= 8192
|
|
|
|
log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
|
|
return branch_if_true, branch_offset
|