Fix bugs found running Zork 1 through hybrid interpreter
Spec fixes: implement op_test (bitwise AND branch), add missing branch to op_get_child, handle call-to-address-0 as no-op in op_call/_call. I/O fixes: correct keyboard API (keyboard_input.read_line), non-TTY fallbacks in trivialzui, stdout flush for immediate output. Graceful handling of unmapped ZSCII characters. Add instruction trace buffer for debugging.
This commit is contained in:
parent
8d749fbb7e
commit
311a67e80a
4 changed files with 75 additions and 14 deletions
|
|
@ -115,6 +115,7 @@ class TrivialScreen(zscreen.ZScreen):
|
|||
for char in string:
|
||||
newline_printed = False
|
||||
sys.stdout.write(char)
|
||||
sys.stdout.flush()
|
||||
|
||||
if char == "\n":
|
||||
newline_printed = True
|
||||
|
|
@ -292,6 +293,11 @@ def _unix_read_char():
|
|||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
|
||||
# Check if stdin is a TTY - if not, use simple read
|
||||
if not sys.stdin.isatty():
|
||||
return sys.stdin.read(1)
|
||||
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
|
@ -333,6 +339,14 @@ def _read_line(original_text=None, terminating_characters=None):
|
|||
assert isinstance(original_text, str)
|
||||
assert isinstance(terminating_characters, str)
|
||||
|
||||
# If stdin is not a TTY, use simple line reading
|
||||
if not sys.stdin.isatty():
|
||||
line = sys.stdin.readline()
|
||||
if not line: # EOF
|
||||
raise EOFError("End of input")
|
||||
# Strip newline but keep the content
|
||||
return line.rstrip("\n\r")
|
||||
|
||||
chars_entered = len(original_text)
|
||||
sys.stdout.write(original_text)
|
||||
string = original_text
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import random
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from . import bitfield, zopdecoder, zscreen
|
||||
from .zlogging import log, log_disasm
|
||||
|
|
@ -53,6 +54,7 @@ class ZCpu:
|
|||
self._streammanager = zstreammanager
|
||||
self._ui = zui
|
||||
self._lexer = zlexer
|
||||
self._trace = deque(maxlen=20)
|
||||
|
||||
def _get_handler(self, opcode_class, opcode_number):
|
||||
try:
|
||||
|
|
@ -60,6 +62,8 @@ class ZCpu:
|
|||
except IndexError:
|
||||
opcode_decl = None
|
||||
if not opcode_decl:
|
||||
cls, num = opcode_class, opcode_number
|
||||
print(f"DEBUG: Illegal opcode class={cls} num={num}")
|
||||
raise ZCpuIllegalInstruction
|
||||
|
||||
# If the opcode declaration is a sequence, we have extra
|
||||
|
|
@ -80,6 +84,8 @@ class ZCpu:
|
|||
elif opcode_decl[1] <= self._memory.version:
|
||||
opcode_func = opcode_decl[0]
|
||||
else:
|
||||
cls, num = opcode_class, opcode_number
|
||||
print(f"DEBUG: Illegal opcode class={cls} num={num}")
|
||||
raise ZCpuIllegalInstruction
|
||||
|
||||
# The following is a hack, based on our policy of only
|
||||
|
|
@ -143,6 +149,11 @@ class ZCpu:
|
|||
"""Set up a function call to the given routine address,
|
||||
passing the given arguments. If store_return_value is True,
|
||||
the routine's return value will be stored."""
|
||||
# Calling address 0 is a no-op that returns false (0).
|
||||
if routine_address == 0:
|
||||
if store_return_value:
|
||||
self._write_result(0)
|
||||
return
|
||||
addr = self._memory.packed_address(routine_address)
|
||||
if store_return_value:
|
||||
return_value = self._opdecoder.get_store_address()
|
||||
|
|
@ -168,12 +179,34 @@ class ZCpu:
|
|||
log(f"Jump to offset {branch_offset:+d}")
|
||||
self._opdecoder.program_counter += branch_offset - 2
|
||||
|
||||
def _dump_trace(self):
|
||||
"""Print the last N instructions for debugging."""
|
||||
print("\n=== INSTRUCTION TRACE (last 20) ===")
|
||||
for entry in self._trace:
|
||||
print(entry)
|
||||
print("===================================\n")
|
||||
|
||||
def step(self):
|
||||
"""Execute a single instruction. Returns True if execution should continue."""
|
||||
current_pc = self._opdecoder.program_counter
|
||||
log(f"Reading next opcode at address {current_pc:x}")
|
||||
(opcode_class, opcode_number, operands) = self._opdecoder.get_next_instruction()
|
||||
|
||||
# Record raw byte at this PC for trace
|
||||
cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
|
||||
trace_entry = f" {current_pc:06x} {cls_str}:{opcode_number:02x}"
|
||||
|
||||
try:
|
||||
implemented, func = self._get_handler(opcode_class, opcode_number)
|
||||
except ZCpuIllegalInstruction:
|
||||
trace_entry += f" ILLEGAL (raw byte: {self._memory[current_pc]:02x})"
|
||||
self._trace.append(trace_entry)
|
||||
self._dump_trace()
|
||||
raise
|
||||
|
||||
trace_entry += f" {func.__name__}({', '.join(str(x) for x in operands)})"
|
||||
self._trace.append(trace_entry)
|
||||
|
||||
log_disasm(
|
||||
current_pc,
|
||||
zopdecoder.OPCODE_STRINGS[opcode_class],
|
||||
|
|
@ -256,9 +289,9 @@ class ZCpu:
|
|||
parent = self._objects.get_parent(obj1)
|
||||
self._branch(parent == obj2)
|
||||
|
||||
def op_test(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
def op_test(self, bitmap, flags):
|
||||
"""Test if all bits in flags are set in bitmap, branch if true."""
|
||||
self._branch((bitmap & flags) == flags)
|
||||
|
||||
def op_or(self, a, b):
|
||||
"""Bitwise OR between the two arguments."""
|
||||
|
|
@ -386,8 +419,10 @@ class ZCpu:
|
|||
self._branch(sibling != 0)
|
||||
|
||||
def op_get_child(self, object_num):
|
||||
"""Get and store the first child of the given object."""
|
||||
self._write_result(self._objects.get_child(object_num))
|
||||
"""Get first child of object, store it, branch if nonzero."""
|
||||
child = self._objects.get_child(object_num)
|
||||
self._write_result(child)
|
||||
self._branch(child != 0)
|
||||
|
||||
def op_get_parent(self, object_num):
|
||||
"""Get and store the parent of the given object."""
|
||||
|
|
@ -574,6 +609,10 @@ class ZCpu:
|
|||
# call in v1-3
|
||||
def op_call(self, routine_addr, *args):
|
||||
"""Call the routine r1, passing it any of r2, r3, r4 if defined."""
|
||||
# Calling address 0 is a no-op that returns false (0).
|
||||
if routine_addr == 0:
|
||||
self._write_result(0)
|
||||
return
|
||||
addr = self._memory.packed_address(routine_addr)
|
||||
return_addr = self._opdecoder.get_store_address()
|
||||
current_addr = self._opdecoder.program_counter
|
||||
|
|
@ -614,7 +653,7 @@ class ZCpu:
|
|||
self.op_show_status()
|
||||
|
||||
# Read input from keyboard
|
||||
text = self._ui.keyboard.readline()
|
||||
text = self._ui.keyboard_input.read_line()
|
||||
text = text.lower().strip("\n\r")
|
||||
|
||||
# Store in text buffer
|
||||
|
|
|
|||
|
|
@ -442,7 +442,14 @@ class ZsciiTranslator:
|
|||
try:
|
||||
return self._output_table[index]
|
||||
except KeyError:
|
||||
raise IndexError("No such ZSCII character") from None
|
||||
# Handle undefined ZSCII characters
|
||||
# 0-31 (except 0, 10): control characters, return empty string
|
||||
# 128-154, 252-254: undefined, return placeholder
|
||||
# 155-251: extended characters, should have Unicode table but don't
|
||||
if index < 32:
|
||||
return ""
|
||||
# For undefined or unmapped characters, return a placeholder
|
||||
return "?"
|
||||
|
||||
def utoz(self, char):
|
||||
"""Translate the given Unicode code into the corresponding
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ class MockUI:
|
|||
def __init__(self):
|
||||
self.screen = Mock()
|
||||
self.screen.write = Mock()
|
||||
self.keyboard = Mock()
|
||||
self.keyboard_input = Mock()
|
||||
self.keyboard_input.read_line = Mock()
|
||||
|
||||
|
||||
class ZMachineOpcodeTests(TestCase):
|
||||
|
|
@ -548,7 +549,7 @@ class ZMachineComplexOpcodeTests(TestCase):
|
|||
self.memory[text_buffer_addr] = 20 # max length
|
||||
|
||||
# Mock keyboard input
|
||||
self.ui.keyboard.readline = Mock(return_value="hello world\n")
|
||||
self.ui.keyboard_input.read_line = Mock(return_value="hello world\n")
|
||||
|
||||
# Call sread with text buffer only (no parse buffer)
|
||||
self.cpu.op_sread(text_buffer_addr, 0)
|
||||
|
|
@ -566,7 +567,7 @@ class ZMachineComplexOpcodeTests(TestCase):
|
|||
text_buffer_addr = 0x1000
|
||||
self.memory[text_buffer_addr] = 5 # max length of 5
|
||||
|
||||
self.ui.keyboard.readline = Mock(return_value="hello world\n")
|
||||
self.ui.keyboard_input.read_line = Mock(return_value="hello world\n")
|
||||
|
||||
self.cpu.op_sread(text_buffer_addr, 0)
|
||||
|
||||
|
|
@ -581,7 +582,7 @@ class ZMachineComplexOpcodeTests(TestCase):
|
|||
text_buffer_addr = 0x1000
|
||||
self.memory[text_buffer_addr] = 20
|
||||
|
||||
self.ui.keyboard.readline = Mock(return_value="HELLO World\n")
|
||||
self.ui.keyboard_input.read_line = Mock(return_value="HELLO World\n")
|
||||
|
||||
self.cpu.op_sread(text_buffer_addr, 0)
|
||||
|
||||
|
|
@ -594,7 +595,7 @@ class ZMachineComplexOpcodeTests(TestCase):
|
|||
text_buffer_addr = 0x1000
|
||||
self.memory[text_buffer_addr] = 20
|
||||
|
||||
self.ui.keyboard.readline = Mock(return_value="test\r\n")
|
||||
self.ui.keyboard_input.read_line = Mock(return_value="test\r\n")
|
||||
|
||||
self.cpu.op_sread(text_buffer_addr, 0)
|
||||
|
||||
|
|
@ -607,7 +608,7 @@ class ZMachineComplexOpcodeTests(TestCase):
|
|||
"""Test sread calls op_show_status for V3."""
|
||||
text_buffer_addr = 0x1000
|
||||
self.memory[text_buffer_addr] = 20
|
||||
self.ui.keyboard.readline = Mock(return_value="test\n")
|
||||
self.ui.keyboard_input.read_line = Mock(return_value="test\n")
|
||||
|
||||
# Track if show_status was called
|
||||
call_count = [0]
|
||||
|
|
|
|||
Loading…
Reference in a new issue