mud/src/mudlib/zmachine/zstackmanager.py
Jared Miller 3140a4d617
Add return_addr to ZStackBottom for uniform frame access
ZStackBottom already had stack and local_vars for uniform treatment,
but was missing return_addr. Adding it removes 5 type: ignore
suppressions and fixes all ty possibly-missing-attribute warnings.
2026-02-10 17:16:25 -05:00

215 lines
7.3 KiB
Python

#
# A class which manages both (1) the general purpose stack ("data
# stack") used by the story code to store temporary data, and (2) the
# interpreter-private stack of routines ("call stack") and their local
# variables.
#
# For the license of this file, please consult the LICENSE file in the
# root directory of this distribution.
#
from .zlogging import log
class ZStackError(Exception):
"General exception for stack or routine-related errors"
pass
class ZStackUnsupportedVersion(ZStackError):
"Unsupported version of Z-story file."
pass
class ZStackNoRoutine(ZStackError):
"No routine is being executed."
pass
class ZStackNoSuchVariable(ZStackError):
"Trying to access non-existent local variable."
pass
class ZStackPopError(ZStackError):
"Nothing to pop from stack!"
pass
# Helper class used by ZStackManager; a 'routine' object which
# includes its own private stack of data.
class ZRoutine:
def __init__(
self, start_addr, return_addr, zmem, args, local_vars=None, stack=None
):
"""Initialize a routine object beginning at START_ADDR in ZMEM,
with initial argument values in list ARGS. If LOCAL_VARS is None,
then parse them from START_ADDR."""
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 = []
else:
self.stack = stack[:]
if local_vars is not None:
self.local_vars = local_vars[:]
else:
num_local_vars = zmem[self.start_addr]
if not (0 <= num_local_vars <= 15):
log(f"num local vars is {num_local_vars}")
raise ZStackError
self.start_addr += 1
# Initialize the local vars in the ZRoutine's dictionary. This is
# only needed on machines v1 through v4. In v5 and v8 machines, all
# local variables are preinitialized to zero.
self.local_vars = [0 for _ in range(15)]
if 1 <= zmem.version <= 4:
for i in range(num_local_vars):
self.local_vars[i] = zmem.read_word(self.start_addr)
self.start_addr += 2
elif zmem.version not in (5, 8):
raise ZStackUnsupportedVersion
# Place call arguments into local vars, if available
for i in range(0, len(args)):
self.local_vars[i] = args[i]
def pretty_print(self):
"Display a ZRoutine nicely, for debugging purposes."
log(f"ZRoutine: start address: {self.start_addr}")
log(f"ZRoutine: return value address: {self.return_addr}")
log(f"ZRoutine: program counter: {self.program_counter}")
log(f"ZRoutine: local variables: {self.local_vars}")
class ZStackBottom:
"""Sentinel object at the bottom of the call stack.
Includes stack and local_vars attributes so code that walks the call
stack can treat all frames uniformly without special-case checks for
the bottom sentinel.
"""
def __init__(self):
self.program_counter = 0 # used as a cache only
self.return_addr = None
self.stack = []
self.local_vars = [0 for _ in range(15)]
class ZStackManager:
def __init__(self, zmem):
self._memory = zmem
self._stackbottom = ZStackBottom()
self._call_stack = [self._stackbottom]
def get_local_variable(self, varnum):
"""Return value of local variable VARNUM from currently-running
routine. VARNUM must be a value between 0 and 15, and must
exist."""
if self._call_stack[-1] == self._stackbottom:
raise ZStackNoRoutine
if not 0 <= varnum <= 15:
raise ZStackNoSuchVariable
current_routine = self._call_stack[-1]
return current_routine.local_vars[varnum]
def set_local_variable(self, varnum, value):
"""Set value of local variable VARNUM to VALUE in
currently-running routine. VARNUM must be a value between 0 and
15, and must exist."""
if self._call_stack[1] == self._stackbottom:
raise ZStackNoRoutine
if not 0 <= varnum <= 15:
raise ZStackNoSuchVariable
current_routine = self._call_stack[-1]
current_routine.local_vars[varnum] = value
def push_stack(self, value):
"Push VALUE onto the top of the current routine's data stack."
current_routine = self._call_stack[-1]
current_routine.stack.append(value)
def pop_stack(self):
"Remove and return value from the top of the data stack."
current_routine = self._call_stack[-1]
if not current_routine.stack:
frame_idx = len(self._call_stack) - 1
ra = getattr(current_routine, "return_addr", "N/A")
pc = getattr(current_routine, "program_counter", 0)
raise ZStackPopError(f"frame {frame_idx}, return_addr={ra}, pc=0x{pc:06x}")
return current_routine.stack.pop()
def get_stack_frame_index(self):
"Return current stack frame number. For use by 'catch' opcode."
return len(self._call_stack) - 1
# Used by quetzal save-file parser to reconstruct stack-frames.
def push_routine(self, routine):
"""Blindly push a ZRoutine object to the call stack.
WARNING: do not use this unless you know what you're doing; you
probably want the more full-featured start_routine() belowe
instead."""
self._call_stack.append(routine)
# ZPU should call this whenever it decides to call a new routine.
def start_routine(self, routine_addr, return_addr, program_counter, args):
"""Save the state of the currenly running routine (by examining
the current value of the PROGRAM_COUNTER), and prepare for
execution of a new routine at ROUTINE_ADDR with list of initial
arguments ARGS."""
new_routine = ZRoutine(routine_addr, return_addr, self._memory, args)
current_routine = self._call_stack[-1]
current_routine.program_counter = program_counter
self._call_stack.append(new_routine)
return new_routine.start_addr
# ZPU should call this whenever it decides to return from current
# routine.
def finish_routine(self, return_value):
"""Toss the currently running routine from the call stack, and
toss any leftover values pushed to the data stack by said routine.
Return the previous routine's program counter address, so that
execution can resume where from it left off."""
exiting_routine = self._call_stack.pop()
current_routine = self._call_stack[-1]
# Depending on many things, return stuff.
if exiting_routine.return_addr is not None:
if exiting_routine.return_addr == 0:
# Push to stack
self.push_stack(return_value)
elif 0 < exiting_routine.return_addr < 0x10:
# Store in local var
self.set_local_variable(exiting_routine.return_addr - 1, return_value)
else:
# Store in global var
self._memory.write_global(exiting_routine.return_addr, return_value)
return current_routine.program_counter