Implement V5+ call variants and fix double-byte operand decoding

New opcodes: op_call_vn, op_call_vn2, op_call_vs2, op_catch,
op_check_arg_count. All call variants delegate to existing _call().
ZRoutine now tracks arg_count for check_arg_count.

Fixed zopdecoder double-byte operand parsing for call_vs2/call_vn2:
the old code called _parse_operands_byte() twice, but this method
reads both type byte AND operands together. The second call would
read operand data as a type byte. Refactored into _read_type_byte()
+ _parse_operand_list() so both type bytes are read before any
operand data.

Also fixed the double-byte detection: was checking opcode[0:7] (7
bits = 0x7A for call_vn2) instead of opcode_num (5 bits = 0x1A).
The check never matched, so double-byte opcodes were always
mis-parsed.
This commit is contained in:
Jared Miller 2026-02-10 13:48:08 -05:00
parent 38e60ae40c
commit d71f221277
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 39 additions and 28 deletions

View file

@ -644,8 +644,9 @@ class ZCpu:
self._stackmanager.pop_stack()
def op_catch(self, *args):
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
"""Store the current stack frame index (for throw opcode)."""
frame_index = self._stackmanager.get_stack_frame_index()
self._write_result(frame_index)
def op_quit(self, *args):
"""Quit the game."""
@ -793,9 +794,9 @@ class ZCpu:
"""Set the given window as the active window."""
self._ui.screen.select_window(window_num)
def op_call_vs2(self, *args):
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_call_vs2(self, routine_addr, *args):
"""Call routine with up to 7 arguments and store the result."""
self._call(routine_addr, args, True)
def op_erase_window(self, window_number):
"""Clear the window with the given number. If # is -1, unsplit
@ -884,13 +885,13 @@ class ZCpu:
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_call_vn(self, *args):
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_call_vn(self, routine_addr, *args):
"""Call routine with up to 3 arguments and discard the result."""
self._call(routine_addr, args, False)
def op_call_vn2(self, *args):
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_call_vn2(self, routine_addr, *args):
"""Call routine with up to 7 arguments and discard the result."""
self._call(routine_addr, args, False)
def op_tokenize(self, *args):
"""TODO: Write docstring here."""
@ -908,9 +909,10 @@ class ZCpu:
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_check_arg_count(self, *args):
"""TODO: Write docstring here."""
raise ZCpuNotImplemented
def op_check_arg_count(self, arg_number):
"""Branch if the Nth argument was passed to the current routine."""
current_frame = self._stackmanager._call_stack[-1]
self._branch(arg_number <= current_frame.arg_count)
## EXT opcodes (opcodes 256-284)

View file

@ -122,14 +122,16 @@ class ZOpDecoder:
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:
# Read all type bytes FIRST, before parsing any operands.
# call_vs2 (VAR:12) and call_vn2 (VAR:26) have two type bytes;
# all others have one. Both type bytes must be read before
# operand data starts in the stream.
operand_types = self._read_type_byte()
if opcode_type == OPCODE_VAR and opcode_num in (0xC, 0x1A):
log("Opcode has second operand byte")
operands += self._parse_operands_byte()
operand_types += self._read_type_byte()
operands = self._parse_operand_list(operand_types)
return (opcode_type, opcode_num, operands)
def _parse_opcode_extended(self):
@ -179,25 +181,31 @@ class ZOpDecoder:
return operand
def _parse_operands_byte(self):
"""Parse operands given by the operand byte and return a list of
values.
"""
def _read_type_byte(self):
"""Read one operand type byte and return a list of type codes."""
operand_byte = BitField(self._get_pc())
operands = []
for operand_type in [
return [
operand_byte[6:8],
operand_byte[4:6],
operand_byte[2:4],
operand_byte[0:2],
]:
]
def _parse_operand_list(self, operand_types):
"""Parse operands from a list of type codes, stopping at ABSENT."""
operands = []
for operand_type in operand_types:
operand = self._parse_operand(operand_type)
if operand is None:
break
operands.append(operand)
return operands
def _parse_operands_byte(self):
"""Read one type byte and parse its operands. Used by extended
opcodes and other callers that always have a single type byte."""
return self._parse_operand_list(self._read_type_byte())
# Public funcs that the ZPU may also need to call, depending on the
# opcode being executed:

View file

@ -54,6 +54,7 @@ class ZRoutine:
self.start_addr = start_addr
self.return_addr = return_addr
self.program_counter = 0 # used when execution interrupted
self.arg_count = len(args) # track number of args passed
if stack is None:
self.stack = []