Fix Quetzal Stks field mapping: return_pc to caller, varnum to frame
return_pc for each frame belongs on the caller's program_counter (the resume address when this routine exits). varnum is the store variable that receives the return value, stored as return_addr on the frame itself. Also handle flags bit 4 (discard result → return_addr=None).
This commit is contained in:
parent
776cfba021
commit
8526e48247
4 changed files with 239 additions and 172 deletions
|
|
@ -207,13 +207,17 @@ class QuetzalParser:
|
||||||
ptr += 3
|
ptr += 3
|
||||||
flags_bitfield = bitfield.BitField(bytes[ptr])
|
flags_bitfield = bitfield.BitField(bytes[ptr])
|
||||||
ptr += 1
|
ptr += 1
|
||||||
_varnum = bytes[ptr] ### TODO: tells us which variable gets the result
|
varnum = bytes[ptr]
|
||||||
ptr += 1
|
ptr += 1
|
||||||
_argflag = bytes[ptr]
|
_argflag = bytes[ptr]
|
||||||
ptr += 1
|
ptr += 1
|
||||||
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
ptr += 2
|
ptr += 2
|
||||||
|
|
||||||
|
# Quetzal flags bit 4: if set, routine discards its return value
|
||||||
|
discard_result = flags_bitfield[4]
|
||||||
|
store_var = None if discard_result else varnum
|
||||||
|
|
||||||
# read anywhere from 0 to 15 local vars, pad to 15
|
# read anywhere from 0 to 15 local vars, pad to 15
|
||||||
num_locals = flags_bitfield[0:4]
|
num_locals = flags_bitfield[0:4]
|
||||||
local_vars = []
|
local_vars = []
|
||||||
|
|
@ -233,16 +237,16 @@ class QuetzalParser:
|
||||||
stack_values.append(val)
|
stack_values.append(val)
|
||||||
log(f" Found {len(stack_values)} local stack values")
|
log(f" Found {len(stack_values)} local stack values")
|
||||||
|
|
||||||
### Interesting... the reconstructed stack frames have no 'start
|
# return_pc belongs on the CALLER frame (previous in the stack).
|
||||||
### address'. I guess it doesn't matter, since we only need to
|
# When this routine finishes, finish_routine() returns
|
||||||
### pop back to particular return addresses to resume each
|
# caller.program_counter as the resume address.
|
||||||
### routine.
|
prev_frame = stackmanager._call_stack[-1]
|
||||||
|
prev_frame.program_counter = return_pc
|
||||||
### TODO: I can exactly which of the 7 args is "supplied", but I
|
|
||||||
### don't understand where the args *are*??
|
|
||||||
|
|
||||||
|
# store_var (varnum) is the variable that receives the return
|
||||||
|
# value when this routine finishes — NOT the return PC.
|
||||||
routine = zstackmanager.ZRoutine(
|
routine = zstackmanager.ZRoutine(
|
||||||
0, return_pc, self._zmachine._mem, [], local_vars, stack_values
|
0, store_var, self._zmachine._mem, [], local_vars, stack_values
|
||||||
)
|
)
|
||||||
stackmanager.push_routine(routine)
|
stackmanager.push_routine(routine)
|
||||||
log(" Added new frame to stack.")
|
log(" Added new frame to stack.")
|
||||||
|
|
@ -543,24 +547,28 @@ class QuetzalWriter:
|
||||||
call_stack = stackmanager._call_stack
|
call_stack = stackmanager._call_stack
|
||||||
|
|
||||||
# Skip the ZStackBottom sentinel (first element)
|
# Skip the ZStackBottom sentinel (first element)
|
||||||
for frame in call_stack[1:]:
|
for i, frame in enumerate(call_stack[1:], start=1):
|
||||||
# Get the actual number of local vars that have non-zero values
|
|
||||||
# or are explicitly set (we store all 15 always, but only serialize
|
|
||||||
# the ones that are actually used)
|
|
||||||
num_local_vars = len(frame.local_vars)
|
num_local_vars = len(frame.local_vars)
|
||||||
|
|
||||||
# Write return_pc as 24-bit big-endian (3 bytes)
|
# Quetzal return_pc = caller's saved program counter.
|
||||||
return_pc = frame.return_addr if frame.return_addr else 0
|
# The previous frame (caller) stores the resume PC that
|
||||||
|
# finish_routine() returns when this frame exits.
|
||||||
|
prev_frame = call_stack[i - 1]
|
||||||
|
return_pc = prev_frame.program_counter or 0
|
||||||
result.append((return_pc >> 16) & 0xFF)
|
result.append((return_pc >> 16) & 0xFF)
|
||||||
result.append((return_pc >> 8) & 0xFF)
|
result.append((return_pc >> 8) & 0xFF)
|
||||||
result.append(return_pc & 0xFF)
|
result.append(return_pc & 0xFF)
|
||||||
|
|
||||||
# Write flags byte (bits 0-3 = num local vars)
|
# Write flags byte (bits 0-3 = num local vars,
|
||||||
result.append(num_local_vars & 0x0F)
|
# bit 4 = discard return value)
|
||||||
|
flags = num_local_vars & 0x0F
|
||||||
|
if frame.return_addr is None:
|
||||||
|
flags |= 0x10
|
||||||
|
result.append(flags)
|
||||||
|
|
||||||
# Write varnum (which variable gets return value)
|
# Write varnum (which variable gets the return value)
|
||||||
# TODO: track this properly, for now use 0
|
varnum = frame.return_addr if frame.return_addr is not None else 0
|
||||||
result.append(0)
|
result.append(varnum & 0xFF)
|
||||||
|
|
||||||
# Write argflag (bitmask of supplied arguments)
|
# Write argflag (bitmask of supplied arguments)
|
||||||
# TODO: track this properly, for now use 0
|
# TODO: track this properly, for now use 0
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
Tests that verify the complete save/restore pipeline works end-to-end by
|
Tests that verify the complete save/restore pipeline works end-to-end by
|
||||||
generating save data with QuetzalWriter and restoring it with QuetzalParser.
|
generating save data with QuetzalWriter and restoring it with QuetzalParser.
|
||||||
|
|
||||||
|
Field mapping reminder:
|
||||||
|
- Quetzal return_pc for frame N → caller (frame N-1) program_counter
|
||||||
|
- Quetzal varnum → frame.return_addr (store variable for return value)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,7 +21,6 @@ class TestQuetzalRoundTrip:
|
||||||
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
||||||
|
|
||||||
# Create a minimal z-machine story file (V3)
|
# Create a minimal z-machine story file (V3)
|
||||||
# Based on the Z-machine spec, this is the minimum viable header
|
|
||||||
story_data = bytearray(8192)
|
story_data = bytearray(8192)
|
||||||
story_data[0] = 3 # Version 3
|
story_data[0] = 3 # Version 3
|
||||||
story_data[0x02:0x04] = [0x12, 0x34] # Release number
|
story_data[0x02:0x04] = [0x12, 0x34] # Release number
|
||||||
|
|
@ -28,7 +31,6 @@ class TestQuetzalRoundTrip:
|
||||||
story_data[0x12:0x18] = b"860101" # Serial number
|
story_data[0x12:0x18] = b"860101" # Serial number
|
||||||
story_data[0x1C:0x1E] = [0xAB, 0xCD] # Checksum
|
story_data[0x1C:0x1E] = [0xAB, 0xCD] # Checksum
|
||||||
|
|
||||||
# Create pristine and current memory
|
|
||||||
pristine_mem = ZMemory(bytes(story_data))
|
pristine_mem = ZMemory(bytes(story_data))
|
||||||
current_mem = ZMemory(bytes(story_data))
|
current_mem = ZMemory(bytes(story_data))
|
||||||
|
|
||||||
|
|
@ -38,10 +40,11 @@ class TestQuetzalRoundTrip:
|
||||||
current_mem[0x200] = 0xFF
|
current_mem[0x200] = 0xFF
|
||||||
|
|
||||||
# Create a stack with one frame
|
# Create a stack with one frame
|
||||||
|
# return_addr=5 means "store return value in local var 4"
|
||||||
stack_manager = ZStackManager(current_mem)
|
stack_manager = ZStackManager(current_mem)
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1234,
|
return_addr=5,
|
||||||
zmem=current_mem,
|
zmem=current_mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x0001, 0x0002, 0x0003],
|
local_vars=[0x0001, 0x0002, 0x0003],
|
||||||
|
|
@ -55,18 +58,17 @@ class TestQuetzalRoundTrip:
|
||||||
zmachine._mem = current_mem
|
zmachine._mem = current_mem
|
||||||
zmachine._stackmanager = stack_manager
|
zmachine._stackmanager = stack_manager
|
||||||
zmachine._cpu = Mock()
|
zmachine._cpu = Mock()
|
||||||
zmachine._cpu._program_counter = 0x0850 # Current PC
|
zmachine._cpu._program_counter = 0x0850
|
||||||
zmachine._opdecoder = Mock()
|
zmachine._opdecoder = Mock()
|
||||||
|
|
||||||
# SAVE: Generate save data
|
# SAVE
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
save_data = writer.generate_save_data()
|
save_data = writer.generate_save_data()
|
||||||
|
|
||||||
# Verify we got valid IFF data
|
|
||||||
assert save_data[:4] == b"FORM"
|
assert save_data[:4] == b"FORM"
|
||||||
assert save_data[8:12] == b"IFZS"
|
assert save_data[8:12] == b"IFZS"
|
||||||
|
|
||||||
# MODIFY: Change the zmachine state to different values
|
# CORRUPT: Change the zmachine state
|
||||||
current_mem[0x100] = 0x99
|
current_mem[0x100] = 0x99
|
||||||
current_mem[0x101] = 0x99
|
current_mem[0x101] = 0x99
|
||||||
current_mem[0x200] = 0x00
|
current_mem[0x200] = 0x00
|
||||||
|
|
@ -76,12 +78,11 @@ class TestQuetzalRoundTrip:
|
||||||
stack_manager._call_stack.append(ZStackBottom())
|
stack_manager._call_stack.append(ZStackBottom())
|
||||||
zmachine._cpu._program_counter = 0x9999
|
zmachine._cpu._program_counter = 0x9999
|
||||||
|
|
||||||
# Verify state was actually modified
|
|
||||||
assert current_mem[0x100] == 0x99
|
assert current_mem[0x100] == 0x99
|
||||||
assert len(stack_manager._call_stack) == 1 # Only bottom sentinel
|
assert len(stack_manager._call_stack) == 1
|
||||||
assert zmachine._cpu._program_counter == 0x9999
|
assert zmachine._cpu._program_counter == 0x9999
|
||||||
|
|
||||||
# RESTORE: Load save data
|
# RESTORE
|
||||||
parser = QuetzalParser(zmachine)
|
parser = QuetzalParser(zmachine)
|
||||||
parser.load_from_bytes(save_data)
|
parser.load_from_bytes(save_data)
|
||||||
|
|
||||||
|
|
@ -90,11 +91,12 @@ class TestQuetzalRoundTrip:
|
||||||
assert current_mem[0x101] == 0x43
|
assert current_mem[0x101] == 0x43
|
||||||
assert current_mem[0x200] == 0xFF
|
assert current_mem[0x200] == 0xFF
|
||||||
|
|
||||||
# VERIFY: Stack was restored (get fresh reference since parser replaces it)
|
# VERIFY: Stack was restored
|
||||||
restored_stack = zmachine._stackmanager
|
restored_stack = zmachine._stackmanager
|
||||||
assert len(restored_stack._call_stack) == 2 # Bottom + one frame
|
assert len(restored_stack._call_stack) == 2 # Bottom + one frame
|
||||||
restored_frame = restored_stack._call_stack[1]
|
restored_frame = restored_stack._call_stack[1]
|
||||||
assert restored_frame.return_addr == 0x1234
|
# return_addr is the store variable (varnum), not a PC
|
||||||
|
assert restored_frame.return_addr == 5
|
||||||
assert restored_frame.local_vars[:3] == [0x0001, 0x0002, 0x0003]
|
assert restored_frame.local_vars[:3] == [0x0001, 0x0002, 0x0003]
|
||||||
assert restored_frame.stack == [0x1111, 0x2222]
|
assert restored_frame.stack == [0x1111, 0x2222]
|
||||||
|
|
||||||
|
|
@ -109,35 +111,39 @@ class TestQuetzalRoundTrip:
|
||||||
from mudlib.zmachine.zmemory import ZMemory
|
from mudlib.zmachine.zmemory import ZMemory
|
||||||
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
||||||
|
|
||||||
# Create minimal story
|
|
||||||
story_data = bytearray(8192)
|
story_data = bytearray(8192)
|
||||||
story_data[0] = 3 # Version 3
|
story_data[0] = 3
|
||||||
story_data[0x02:0x04] = [0x10, 0x00] # Release
|
story_data[0x02:0x04] = [0x10, 0x00]
|
||||||
story_data[0x04:0x06] = [0x10, 0x00] # High memory
|
story_data[0x04:0x06] = [0x10, 0x00]
|
||||||
story_data[0x06:0x08] = [0x08, 0x00] # Initial PC
|
story_data[0x06:0x08] = [0x08, 0x00]
|
||||||
story_data[0x0E:0x10] = [0x04, 0x00] # Static memory
|
story_data[0x0E:0x10] = [0x04, 0x00]
|
||||||
story_data[0x0C:0x0E] = [0x02, 0x00] # Globals
|
story_data[0x0C:0x0E] = [0x02, 0x00]
|
||||||
story_data[0x12:0x18] = b"860101" # Serial
|
story_data[0x12:0x18] = b"860101"
|
||||||
story_data[0x1C:0x1E] = [0x00, 0x00] # Checksum
|
story_data[0x1C:0x1E] = [0x00, 0x00]
|
||||||
|
|
||||||
pristine_mem = ZMemory(bytes(story_data))
|
pristine_mem = ZMemory(bytes(story_data))
|
||||||
current_mem = ZMemory(bytes(story_data))
|
current_mem = ZMemory(bytes(story_data))
|
||||||
|
|
||||||
# Create nested call frames
|
# Create nested call frames with proper semantics:
|
||||||
|
# return_addr = store variable (varnum)
|
||||||
|
# program_counter = resume PC for when the next frame returns
|
||||||
stack_manager = ZStackManager(current_mem)
|
stack_manager = ZStackManager(current_mem)
|
||||||
|
|
||||||
routine1 = ZRoutine(
|
routine1 = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1000,
|
return_addr=0, # varnum 0 = push to stack
|
||||||
zmem=current_mem,
|
zmem=current_mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0xAAAA, 0xBBBB],
|
local_vars=[0xAAAA, 0xBBBB],
|
||||||
stack=[0x1111],
|
stack=[0x1111],
|
||||||
)
|
)
|
||||||
stack_manager.push_routine(routine1)
|
stack_manager.push_routine(routine1)
|
||||||
|
# resume PC in routine1 after routine2 returns
|
||||||
|
routine1.program_counter = 0x5123
|
||||||
|
|
||||||
routine2 = ZRoutine(
|
routine2 = ZRoutine(
|
||||||
start_addr=0x6000,
|
start_addr=0x6000,
|
||||||
return_addr=0x2000,
|
return_addr=3, # varnum 3 = local var 2
|
||||||
zmem=current_mem,
|
zmem=current_mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0xCCCC],
|
local_vars=[0xCCCC],
|
||||||
|
|
@ -145,7 +151,6 @@ class TestQuetzalRoundTrip:
|
||||||
)
|
)
|
||||||
stack_manager.push_routine(routine2)
|
stack_manager.push_routine(routine2)
|
||||||
|
|
||||||
# Set up zmachine
|
|
||||||
zmachine = Mock()
|
zmachine = Mock()
|
||||||
zmachine._pristine_mem = pristine_mem
|
zmachine._pristine_mem = pristine_mem
|
||||||
zmachine._mem = current_mem
|
zmachine._mem = current_mem
|
||||||
|
|
@ -168,17 +173,19 @@ class TestQuetzalRoundTrip:
|
||||||
parser = QuetzalParser(zmachine)
|
parser = QuetzalParser(zmachine)
|
||||||
parser.load_from_bytes(save_data)
|
parser.load_from_bytes(save_data)
|
||||||
|
|
||||||
# Verify both frames restored (get fresh reference)
|
# Verify both frames restored
|
||||||
restored_stack = zmachine._stackmanager
|
restored_stack = zmachine._stackmanager
|
||||||
assert len(restored_stack._call_stack) == 3 # Bottom + two frames
|
assert len(restored_stack._call_stack) == 3 # Bottom + two frames
|
||||||
|
|
||||||
frame1 = restored_stack._call_stack[1]
|
frame1 = restored_stack._call_stack[1]
|
||||||
assert frame1.return_addr == 0x1000
|
assert frame1.return_addr == 0 # varnum
|
||||||
assert frame1.local_vars[:2] == [0xAAAA, 0xBBBB]
|
assert frame1.local_vars[:2] == [0xAAAA, 0xBBBB]
|
||||||
assert frame1.stack == [0x1111]
|
assert frame1.stack == [0x1111]
|
||||||
|
# frame1 should have the resume PC for after frame2 returns
|
||||||
|
assert frame1.program_counter == 0x5123
|
||||||
|
|
||||||
frame2 = restored_stack._call_stack[2]
|
frame2 = restored_stack._call_stack[2]
|
||||||
assert frame2.return_addr == 0x2000
|
assert frame2.return_addr == 3 # varnum
|
||||||
assert frame2.local_vars[:1] == [0xCCCC]
|
assert frame2.local_vars[:1] == [0xCCCC]
|
||||||
assert frame2.stack == [0x2222, 0x3333]
|
assert frame2.stack == [0x2222, 0x3333]
|
||||||
|
|
||||||
|
|
@ -190,7 +197,6 @@ class TestQuetzalRoundTrip:
|
||||||
from mudlib.zmachine.zmemory import ZMemory
|
from mudlib.zmachine.zmemory import ZMemory
|
||||||
from mudlib.zmachine.zstackmanager import ZStackManager
|
from mudlib.zmachine.zstackmanager import ZStackManager
|
||||||
|
|
||||||
# Create story with specific pattern in dynamic memory
|
|
||||||
story_data = bytearray(8192)
|
story_data = bytearray(8192)
|
||||||
story_data[0] = 3
|
story_data[0] = 3
|
||||||
story_data[0x02:0x04] = [0x10, 0x00]
|
story_data[0x02:0x04] = [0x10, 0x00]
|
||||||
|
|
@ -201,14 +207,12 @@ class TestQuetzalRoundTrip:
|
||||||
story_data[0x12:0x18] = b"860101"
|
story_data[0x12:0x18] = b"860101"
|
||||||
story_data[0x1C:0x1E] = [0x00, 0x00]
|
story_data[0x1C:0x1E] = [0x00, 0x00]
|
||||||
|
|
||||||
# Set a pattern in dynamic memory
|
|
||||||
for i in range(0x100, 0x200):
|
for i in range(0x100, 0x200):
|
||||||
story_data[i] = i & 0xFF
|
story_data[i] = i & 0xFF
|
||||||
|
|
||||||
pristine_mem = ZMemory(bytes(story_data))
|
pristine_mem = ZMemory(bytes(story_data))
|
||||||
current_mem = ZMemory(bytes(story_data))
|
current_mem = ZMemory(bytes(story_data))
|
||||||
|
|
||||||
# Only modify a few bytes
|
|
||||||
current_mem[0x150] = 0xFF
|
current_mem[0x150] = 0xFF
|
||||||
current_mem[0x180] = 0xAA
|
current_mem[0x180] = 0xAA
|
||||||
|
|
||||||
|
|
@ -222,18 +226,15 @@ class TestQuetzalRoundTrip:
|
||||||
zmachine._cpu._program_counter = 0x0800
|
zmachine._cpu._program_counter = 0x0800
|
||||||
zmachine._opdecoder = Mock()
|
zmachine._opdecoder = Mock()
|
||||||
|
|
||||||
# Save and restore
|
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
save_data = writer.generate_save_data()
|
save_data = writer.generate_save_data()
|
||||||
|
|
||||||
# Corrupt all dynamic memory
|
|
||||||
for i in range(0x100, 0x200):
|
for i in range(0x100, 0x200):
|
||||||
current_mem[i] = 0x00
|
current_mem[i] = 0x00
|
||||||
|
|
||||||
parser = QuetzalParser(zmachine)
|
parser = QuetzalParser(zmachine)
|
||||||
parser.load_from_bytes(save_data)
|
parser.load_from_bytes(save_data)
|
||||||
|
|
||||||
# Verify all bytes restored correctly
|
|
||||||
for i in range(0x100, 0x200):
|
for i in range(0x100, 0x200):
|
||||||
if i == 0x150:
|
if i == 0x150:
|
||||||
assert current_mem[i] == 0xFF
|
assert current_mem[i] == 0xFF
|
||||||
|
|
@ -248,7 +249,7 @@ class TestQuetzalRoundTrip:
|
||||||
|
|
||||||
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
from mudlib.zmachine.quetzal import QuetzalParser, QuetzalWriter
|
||||||
from mudlib.zmachine.zmemory import ZMemory
|
from mudlib.zmachine.zmemory import ZMemory
|
||||||
from mudlib.zmachine.zstackmanager import ZStackManager
|
from mudlib.zmachine.zstackmanager import ZRoutine, ZStackManager
|
||||||
|
|
||||||
story_data = bytearray(8192)
|
story_data = bytearray(8192)
|
||||||
story_data[0] = 3
|
story_data[0] = 3
|
||||||
|
|
@ -274,16 +275,13 @@ class TestQuetzalRoundTrip:
|
||||||
zmachine._cpu._program_counter = 0x0800
|
zmachine._cpu._program_counter = 0x0800
|
||||||
zmachine._opdecoder = Mock()
|
zmachine._opdecoder = Mock()
|
||||||
|
|
||||||
# Save with empty stack
|
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
save_data = writer.generate_save_data()
|
save_data = writer.generate_save_data()
|
||||||
|
|
||||||
# Add a dummy frame
|
# Add a dummy frame to verify it gets cleared
|
||||||
from mudlib.zmachine.zstackmanager import ZRoutine
|
|
||||||
|
|
||||||
dummy = ZRoutine(
|
dummy = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1000,
|
return_addr=1,
|
||||||
zmem=current_mem,
|
zmem=current_mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x9999],
|
local_vars=[0x9999],
|
||||||
|
|
@ -291,11 +289,9 @@ class TestQuetzalRoundTrip:
|
||||||
)
|
)
|
||||||
stack_manager.push_routine(dummy)
|
stack_manager.push_routine(dummy)
|
||||||
|
|
||||||
# Restore
|
|
||||||
parser = QuetzalParser(zmachine)
|
parser = QuetzalParser(zmachine)
|
||||||
parser.load_from_bytes(save_data)
|
parser.load_from_bytes(save_data)
|
||||||
|
|
||||||
# Should have only bottom sentinel (get fresh reference)
|
|
||||||
restored_stack = zmachine._stackmanager
|
restored_stack = zmachine._stackmanager
|
||||||
assert len(restored_stack._call_stack) == 1
|
assert len(restored_stack._call_stack) == 1
|
||||||
assert current_mem[0x100] == 0x42
|
assert current_mem[0x100] == 0x42
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
Tests for QuetzalWriter._generate_stks_chunk() serialization.
|
Tests for QuetzalWriter._generate_stks_chunk() serialization.
|
||||||
|
|
||||||
The Stks chunk serializes the Z-machine call stack. Each frame has:
|
The Stks chunk serializes the Z-machine call stack. Each frame has:
|
||||||
- Bytes 0-2: return_pc (24-bit big-endian)
|
- Bytes 0-2: return_pc (24-bit big-endian) — caller's resume PC
|
||||||
- Byte 3: flags (bits 0-3 = num local vars)
|
- Byte 3: flags (bits 0-3 = num local vars, bit 4 = discard result)
|
||||||
- Byte 4: varnum (which variable gets return value)
|
- Byte 4: varnum (which variable gets return value)
|
||||||
- Byte 5: argflag (bitmask of supplied arguments)
|
- Byte 5: argflag (bitmask of supplied arguments)
|
||||||
- Bytes 6-7: eval_stack_size (16-bit big-endian)
|
- Bytes 6-7: eval_stack_size (16-bit big-endian)
|
||||||
|
|
@ -11,6 +11,10 @@ The Stks chunk serializes the Z-machine call stack. Each frame has:
|
||||||
- Next (eval_stack_size * 2) bytes: evaluation stack values
|
- Next (eval_stack_size * 2) bytes: evaluation stack values
|
||||||
|
|
||||||
All multi-byte values are big-endian. Bottom frame has return_pc=0.
|
All multi-byte values are big-endian. Bottom frame has return_pc=0.
|
||||||
|
|
||||||
|
Field mapping to runtime:
|
||||||
|
- return_pc for frame N → stored as program_counter on frame N-1 (the caller)
|
||||||
|
- varnum → stored as return_addr on the frame (the store variable)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
@ -49,10 +53,14 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
def test_single_frame_serialization(self):
|
def test_single_frame_serialization(self):
|
||||||
"""Single routine frame should serialize correctly."""
|
"""Single routine frame should serialize correctly."""
|
||||||
# Create a routine with known values
|
# Set up caller resume PC on ZStackBottom
|
||||||
|
sentinel = self.zmachine._stackmanager._call_stack[0]
|
||||||
|
sentinel.program_counter = 0x4200
|
||||||
|
|
||||||
|
# Create a routine with return_addr = store variable (varnum 5)
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x4200,
|
return_addr=5,
|
||||||
zmem=self._mem,
|
zmem=self._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x1234, 0x5678, 0xABCD],
|
local_vars=[0x1234, 0x5678, 0xABCD],
|
||||||
|
|
@ -62,21 +70,15 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
chunk = self.writer._generate_stks_chunk()
|
chunk = self.writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected bytes for this frame:
|
# return_pc comes from ZStackBottom.program_counter (0x4200)
|
||||||
# return_pc (0x4200): 0x00, 0x42, 0x00
|
# varnum is frame.return_addr (5)
|
||||||
# flags (3 local vars): 0x03
|
|
||||||
# varnum (0): 0x00
|
|
||||||
# argflag (0): 0x00
|
|
||||||
# eval_stack_size (2): 0x00, 0x02
|
|
||||||
# local_vars: 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD
|
|
||||||
# stack: 0x11, 0x11, 0x22, 0x22
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x42,
|
0x42,
|
||||||
0x00, # return_pc
|
0x00, # return_pc (from caller's program_counter)
|
||||||
0x03, # flags (3 local vars)
|
0x03, # flags (3 local vars)
|
||||||
0x00, # varnum
|
0x05, # varnum (store variable)
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x02, # eval_stack_size = 2
|
0x02, # eval_stack_size = 2
|
||||||
|
|
@ -96,21 +98,26 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
def test_multiple_frames_serialization(self):
|
def test_multiple_frames_serialization(self):
|
||||||
"""Multiple nested frames should serialize in order."""
|
"""Multiple nested frames should serialize in order."""
|
||||||
# Frame 1: outer routine
|
sentinel = self.zmachine._stackmanager._call_stack[0]
|
||||||
|
sentinel.program_counter = 0 # main routine has no caller
|
||||||
|
|
||||||
|
# Frame 1: outer routine (varnum=0 means push result to stack)
|
||||||
routine1 = ZRoutine(
|
routine1 = ZRoutine(
|
||||||
start_addr=0x1000,
|
start_addr=0x1000,
|
||||||
return_addr=0, # bottom frame
|
return_addr=0,
|
||||||
zmem=self._mem,
|
zmem=self._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x0001],
|
local_vars=[0x0001],
|
||||||
stack=[],
|
stack=[],
|
||||||
)
|
)
|
||||||
self.zmachine._stackmanager.push_routine(routine1)
|
self.zmachine._stackmanager.push_routine(routine1)
|
||||||
|
# Set routine1's resume PC (where to go after frame2 returns)
|
||||||
|
routine1.program_counter = 0x1050
|
||||||
|
|
||||||
# Frame 2: inner routine
|
# Frame 2: inner routine (varnum=3)
|
||||||
routine2 = ZRoutine(
|
routine2 = ZRoutine(
|
||||||
start_addr=0x2000,
|
start_addr=0x2000,
|
||||||
return_addr=0x1050,
|
return_addr=3,
|
||||||
zmem=self._mem,
|
zmem=self._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x0002, 0x0003],
|
local_vars=[0x0002, 0x0003],
|
||||||
|
|
@ -120,15 +127,14 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
chunk = self.writer._generate_stks_chunk()
|
chunk = self.writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected: frame1 bytes + frame2 bytes
|
# Frame 1: return_pc from sentinel.pc (0), varnum=0
|
||||||
# Frame 1: return_pc=0, 1 local var, 0 stack
|
|
||||||
frame1 = bytes(
|
frame1 = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # return_pc = 0
|
0x00, # return_pc = 0 (from sentinel)
|
||||||
0x01, # flags (1 local var)
|
0x01, # flags (1 local var)
|
||||||
0x00, # varnum
|
0x00, # varnum = 0 (push to stack)
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # eval_stack_size = 0
|
0x00, # eval_stack_size = 0
|
||||||
|
|
@ -136,14 +142,14 @@ class QuetzalStksTests(TestCase):
|
||||||
0x01, # local_vars[0]
|
0x01, # local_vars[0]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# Frame 2: return_pc=0x1050, 2 local vars, 1 stack
|
# Frame 2: return_pc from routine1.pc (0x1050), varnum=3
|
||||||
frame2 = bytes(
|
frame2 = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x10,
|
0x10,
|
||||||
0x50, # return_pc
|
0x50, # return_pc (from routine1.program_counter)
|
||||||
0x02, # flags (2 local vars)
|
0x02, # flags (2 local vars)
|
||||||
0x00, # varnum
|
0x03, # varnum = 3
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x01, # eval_stack_size = 1
|
0x01, # eval_stack_size = 1
|
||||||
|
|
@ -160,9 +166,12 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
def test_frame_with_no_locals_or_stack(self):
|
def test_frame_with_no_locals_or_stack(self):
|
||||||
"""Frame with no local vars or stack values should serialize correctly."""
|
"""Frame with no local vars or stack values should serialize correctly."""
|
||||||
|
sentinel = self.zmachine._stackmanager._call_stack[0]
|
||||||
|
sentinel.program_counter = 0x2500
|
||||||
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x3000,
|
start_addr=0x3000,
|
||||||
return_addr=0x2500,
|
return_addr=1,
|
||||||
zmem=self._mem,
|
zmem=self._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[],
|
local_vars=[],
|
||||||
|
|
@ -176,9 +185,9 @@ class QuetzalStksTests(TestCase):
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x25,
|
0x25,
|
||||||
0x00, # return_pc
|
0x00, # return_pc (from sentinel.program_counter)
|
||||||
0x00, # flags (0 local vars)
|
0x00, # flags (0 local vars)
|
||||||
0x00, # varnum
|
0x01, # varnum = 1
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # eval_stack_size = 0
|
0x00, # eval_stack_size = 0
|
||||||
|
|
@ -186,10 +195,49 @@ class QuetzalStksTests(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(chunk, expected)
|
self.assertEqual(chunk, expected)
|
||||||
|
|
||||||
|
def test_discard_result_sets_flags_bit4(self):
|
||||||
|
"""Frame with return_addr=None should set bit 4 in flags byte."""
|
||||||
|
sentinel = self.zmachine._stackmanager._call_stack[0]
|
||||||
|
sentinel.program_counter = 0
|
||||||
|
|
||||||
|
routine = ZRoutine(
|
||||||
|
start_addr=0x1000,
|
||||||
|
return_addr=None,
|
||||||
|
zmem=self._mem,
|
||||||
|
args=[],
|
||||||
|
local_vars=[0x0001, 0x0002],
|
||||||
|
stack=[],
|
||||||
|
)
|
||||||
|
self.zmachine._stackmanager.push_routine(routine)
|
||||||
|
|
||||||
|
chunk = self.writer._generate_stks_chunk()
|
||||||
|
|
||||||
|
# flags = 0x02 (2 locals) | 0x10 (discard) = 0x12
|
||||||
|
expected = bytes(
|
||||||
|
[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00, # return_pc
|
||||||
|
0x12, # flags (2 locals + discard bit)
|
||||||
|
0x00, # varnum (0 when discarding)
|
||||||
|
0x00, # argflag
|
||||||
|
0x00,
|
||||||
|
0x00, # eval_stack_size = 0
|
||||||
|
0x00,
|
||||||
|
0x01, # local_vars[0]
|
||||||
|
0x00,
|
||||||
|
0x02, # local_vars[1]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(chunk, expected)
|
||||||
|
|
||||||
def test_round_trip_with_parser(self):
|
def test_round_trip_with_parser(self):
|
||||||
"""Generated stks bytes should parse back identically."""
|
"""Generated stks bytes should parse back identically."""
|
||||||
from mudlib.zmachine.quetzal import QuetzalParser
|
from mudlib.zmachine.quetzal import QuetzalParser
|
||||||
|
|
||||||
|
sentinel = self.zmachine._stackmanager._call_stack[0]
|
||||||
|
sentinel.program_counter = 0 # main routine caller
|
||||||
|
|
||||||
# Create a complex stack state
|
# Create a complex stack state
|
||||||
routine1 = ZRoutine(
|
routine1 = ZRoutine(
|
||||||
start_addr=0x1000,
|
start_addr=0x1000,
|
||||||
|
|
@ -200,10 +248,11 @@ class QuetzalStksTests(TestCase):
|
||||||
stack=[0xAAAA, 0xBBBB],
|
stack=[0xAAAA, 0xBBBB],
|
||||||
)
|
)
|
||||||
self.zmachine._stackmanager.push_routine(routine1)
|
self.zmachine._stackmanager.push_routine(routine1)
|
||||||
|
routine1.program_counter = 0x1234 # resume PC for after routine2
|
||||||
|
|
||||||
routine2 = ZRoutine(
|
routine2 = ZRoutine(
|
||||||
start_addr=0x2000,
|
start_addr=0x2000,
|
||||||
return_addr=0x1234,
|
return_addr=5,
|
||||||
zmem=self._mem,
|
zmem=self._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x4444],
|
local_vars=[0x4444],
|
||||||
|
|
@ -220,41 +269,38 @@ class QuetzalStksTests(TestCase):
|
||||||
|
|
||||||
# Verify the stack was reconstructed correctly
|
# Verify the stack was reconstructed correctly
|
||||||
# Parser creates a new stack manager, skip bottom sentinel
|
# Parser creates a new stack manager, skip bottom sentinel
|
||||||
frames = self.zmachine._stackmanager._call_stack[1:]
|
call_stack = self.zmachine._stackmanager._call_stack
|
||||||
|
frames = call_stack[1:]
|
||||||
self.assertEqual(len(frames), 2)
|
self.assertEqual(len(frames), 2)
|
||||||
|
|
||||||
# Check frame 1
|
# Check frame 1: return_addr = varnum, caller PC on sentinel
|
||||||
assert isinstance(frames[0], ZRoutine)
|
assert isinstance(frames[0], ZRoutine)
|
||||||
self.assertEqual(frames[0].return_addr, 0)
|
self.assertEqual(frames[0].return_addr, 0)
|
||||||
self.assertEqual(frames[0].local_vars[:3], [0x1111, 0x2222, 0x3333])
|
self.assertEqual(frames[0].local_vars[:3], [0x1111, 0x2222, 0x3333])
|
||||||
self.assertEqual(frames[0].stack, [0xAAAA, 0xBBBB])
|
self.assertEqual(frames[0].stack, [0xAAAA, 0xBBBB])
|
||||||
|
# Sentinel should have frame1's return_pc (0)
|
||||||
|
self.assertEqual(call_stack[0].program_counter, 0)
|
||||||
|
|
||||||
# Check frame 2
|
# Check frame 2: return_addr = varnum, caller PC on frame1
|
||||||
assert isinstance(frames[1], ZRoutine)
|
assert isinstance(frames[1], ZRoutine)
|
||||||
self.assertEqual(frames[1].return_addr, 0x1234)
|
self.assertEqual(frames[1].return_addr, 5)
|
||||||
self.assertEqual(frames[1].local_vars[:1], [0x4444])
|
self.assertEqual(frames[1].local_vars[:1], [0x4444])
|
||||||
self.assertEqual(frames[1].stack, [0xCCCC, 0xDDDD, 0xEEEE])
|
self.assertEqual(frames[1].stack, [0xCCCC, 0xDDDD, 0xEEEE])
|
||||||
|
# Frame1 should have frame2's return_pc (0x1234)
|
||||||
|
self.assertEqual(frames[0].program_counter, 0x1234)
|
||||||
|
|
||||||
def test_parse_return_pc_third_byte(self):
|
def test_parse_return_pc_goes_to_caller(self):
|
||||||
"""Parser should correctly read all 3 bytes of return_pc (regression test)."""
|
"""Parser should put return_pc on the caller frame's program_counter."""
|
||||||
from mudlib.zmachine.quetzal import QuetzalParser
|
from mudlib.zmachine.quetzal import QuetzalParser
|
||||||
|
|
||||||
# Construct a minimal stack frame with return_pc=0x123456
|
# Construct a minimal stack frame with return_pc=0x123456
|
||||||
# where the third byte (0x56) is non-zero and would be wrong if skipped.
|
|
||||||
#
|
|
||||||
# Stack frame format:
|
|
||||||
# - Bytes 0-2: return_pc (0x12, 0x34, 0x56)
|
|
||||||
# - Byte 3: flags (0 local vars)
|
|
||||||
# - Byte 4: varnum (0)
|
|
||||||
# - Byte 5: argflag (0)
|
|
||||||
# - Bytes 6-7: eval_stack_size (0)
|
|
||||||
stks_bytes = bytes(
|
stks_bytes = bytes(
|
||||||
[
|
[
|
||||||
0x12,
|
0x12,
|
||||||
0x34,
|
0x34,
|
||||||
0x56, # return_pc = 0x123456
|
0x56, # return_pc = 0x123456
|
||||||
0x00, # flags (0 local vars)
|
0x00, # flags (0 local vars)
|
||||||
0x00, # varnum
|
0x07, # varnum = 7
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # eval_stack_size = 0
|
0x00, # eval_stack_size = 0
|
||||||
|
|
@ -264,18 +310,46 @@ class QuetzalStksTests(TestCase):
|
||||||
parser = QuetzalParser(self.zmachine)
|
parser = QuetzalParser(self.zmachine)
|
||||||
parser._parse_stks(stks_bytes)
|
parser._parse_stks(stks_bytes)
|
||||||
|
|
||||||
# The parser should have reconstructed the stack with correct return_pc
|
call_stack = self.zmachine._stackmanager._call_stack
|
||||||
frames = self.zmachine._stackmanager._call_stack[1:] # skip sentinel
|
frames = call_stack[1:] # skip sentinel
|
||||||
self.assertEqual(len(frames), 1)
|
self.assertEqual(len(frames), 1)
|
||||||
# If bug exists, it would read bytes[0], bytes[1], bytes[3] = 0x12, 0x34, 0x00
|
|
||||||
# giving return_pc = 0x123400 instead of 0x123456
|
# return_pc goes to caller (sentinel) program_counter
|
||||||
|
self.assertEqual(call_stack[0].program_counter, 0x123456)
|
||||||
|
# varnum goes to frame's return_addr
|
||||||
assert isinstance(frames[0], ZRoutine)
|
assert isinstance(frames[0], ZRoutine)
|
||||||
self.assertEqual(
|
self.assertEqual(frames[0].return_addr, 7)
|
||||||
frames[0].return_addr,
|
|
||||||
0x123456,
|
def test_parse_discard_bit_restores_none(self):
|
||||||
"Parser should read bytes[ptr+2], not bytes[ptr+3]",
|
"""Parser should set return_addr=None when flags bit 4 is set."""
|
||||||
|
from mudlib.zmachine.quetzal import QuetzalParser
|
||||||
|
|
||||||
|
# flags = 0x12 = 2 locals + discard bit
|
||||||
|
stks_bytes = bytes(
|
||||||
|
[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00, # return_pc = 0
|
||||||
|
0x12, # flags (2 locals + discard)
|
||||||
|
0x00, # varnum (ignored when discarding)
|
||||||
|
0x00, # argflag
|
||||||
|
0x00,
|
||||||
|
0x00, # eval_stack_size = 0
|
||||||
|
0x00,
|
||||||
|
0x01, # local_vars[0]
|
||||||
|
0x00,
|
||||||
|
0x02, # local_vars[1]
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser = QuetzalParser(self.zmachine)
|
||||||
|
parser._parse_stks(stks_bytes)
|
||||||
|
|
||||||
|
frames = self.zmachine._stackmanager._call_stack[1:]
|
||||||
|
self.assertEqual(len(frames), 1)
|
||||||
|
self.assertIsNone(frames[0].return_addr)
|
||||||
|
self.assertEqual(frames[0].local_vars[:2], [1, 2])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _mem(self):
|
def _mem(self):
|
||||||
"""Helper to get mock memory."""
|
"""Helper to get mock memory."""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
"""Tests for QuetzalWriter Stks chunk generation."""
|
"""Tests for QuetzalWriter Stks chunk generation.
|
||||||
|
|
||||||
|
Field mapping:
|
||||||
|
- Quetzal return_pc → previous frame's program_counter (caller resume PC)
|
||||||
|
- Quetzal varnum → frame.return_addr (store variable for return value)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TestStksChunkGeneration:
|
class TestStksChunkGeneration:
|
||||||
|
|
@ -11,7 +16,6 @@ class TestStksChunkGeneration:
|
||||||
from mudlib.zmachine.quetzal import QuetzalWriter
|
from mudlib.zmachine.quetzal import QuetzalWriter
|
||||||
from mudlib.zmachine.zstackmanager import ZStackManager
|
from mudlib.zmachine.zstackmanager import ZStackManager
|
||||||
|
|
||||||
# Setup: create a mock zmachine with only the bottom sentinel
|
|
||||||
zmachine = Mock()
|
zmachine = Mock()
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
|
|
@ -21,7 +25,6 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Empty stack should serialize to empty bytes (no frames)
|
|
||||||
assert result == b""
|
assert result == b""
|
||||||
|
|
||||||
def test_single_frame_no_locals_no_stack(self):
|
def test_single_frame_no_locals_no_stack(self):
|
||||||
|
|
@ -36,10 +39,12 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
|
||||||
# Create a routine with no locals and no stack values
|
# Set caller resume PC on sentinel
|
||||||
|
stack_manager._call_stack[0].program_counter = 0x1234
|
||||||
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1234,
|
return_addr=7, # varnum: store to local var 6
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[],
|
local_vars=[],
|
||||||
|
|
@ -51,19 +56,13 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected format:
|
|
||||||
# Bytes 0-2: return_pc = 0x1234 (24-bit big-endian)
|
|
||||||
# Byte 3: flags = 0 (0 local vars)
|
|
||||||
# Byte 4: varnum = 0 (which variable gets return value)
|
|
||||||
# Byte 5: argflag = 0
|
|
||||||
# Bytes 6-7: eval_stack_size = 0
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x12,
|
0x12,
|
||||||
0x34, # return_pc (24-bit)
|
0x34, # return_pc (from sentinel.program_counter)
|
||||||
0x00, # flags (0 locals)
|
0x00, # flags (0 locals)
|
||||||
0x00, # varnum
|
0x07, # varnum (frame.return_addr)
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # eval_stack_size = 0
|
0x00, # eval_stack_size = 0
|
||||||
|
|
@ -83,11 +82,11 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
stack_manager._call_stack[0].program_counter = 0x2000
|
||||||
|
|
||||||
# Create a routine with 3 local variables
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x2000,
|
return_addr=0x10, # varnum: store to global var 0x10
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x1111, 0x2222, 0x3333],
|
local_vars=[0x1111, 0x2222, 0x3333],
|
||||||
|
|
@ -99,20 +98,13 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected format:
|
|
||||||
# Bytes 0-2: return_pc = 0x2000
|
|
||||||
# Byte 3: flags = 3 (3 local vars in bits 0-3)
|
|
||||||
# Byte 4: varnum = 0x00
|
|
||||||
# Byte 5: argflag = 0
|
|
||||||
# Bytes 6-7: eval_stack_size = 0
|
|
||||||
# Bytes 8-13: three local vars (0x1111, 0x2222, 0x3333)
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x20,
|
0x20,
|
||||||
0x00, # return_pc
|
0x00, # return_pc (from sentinel.program_counter)
|
||||||
0x03, # flags (3 locals)
|
0x03, # flags (3 locals)
|
||||||
0x00, # varnum
|
0x10, # varnum (frame.return_addr)
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # eval_stack_size = 0
|
0x00, # eval_stack_size = 0
|
||||||
|
|
@ -138,11 +130,11 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
stack_manager._call_stack[0].program_counter = 0x3000
|
||||||
|
|
||||||
# Create a routine with stack values but no locals
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x3000,
|
return_addr=0, # varnum 0: push result to eval stack
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[],
|
local_vars=[],
|
||||||
|
|
@ -154,20 +146,13 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected format:
|
|
||||||
# Bytes 0-2: return_pc = 0x3000
|
|
||||||
# Byte 3: flags = 0 (0 locals)
|
|
||||||
# Byte 4: varnum = 0x00
|
|
||||||
# Byte 5: argflag = 0
|
|
||||||
# Bytes 6-7: eval_stack_size = 2
|
|
||||||
# Bytes 8-11: two stack values
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x30,
|
0x30,
|
||||||
0x00, # return_pc
|
0x00, # return_pc
|
||||||
0x00, # flags (0 locals)
|
0x00, # flags (0 locals)
|
||||||
0x00, # varnum
|
0x00, # varnum (push to stack)
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x02, # eval_stack_size = 2
|
0x02, # eval_stack_size = 2
|
||||||
|
|
@ -191,11 +176,11 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
stack_manager._call_stack[0].program_counter = 0x4567
|
||||||
|
|
||||||
# Create a routine with 2 locals and 3 stack values
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x4567,
|
return_addr=2, # varnum: store to local var 1
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x0001, 0x0002],
|
local_vars=[0x0001, 0x0002],
|
||||||
|
|
@ -211,9 +196,9 @@ class TestStksChunkGeneration:
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x45,
|
0x45,
|
||||||
0x67, # return_pc
|
0x67, # return_pc (from sentinel.program_counter)
|
||||||
0x02, # flags (2 locals)
|
0x02, # flags (2 locals)
|
||||||
0x00, # varnum
|
0x02, # varnum
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x03, # eval_stack_size = 3
|
0x03, # eval_stack_size = 3
|
||||||
|
|
@ -244,21 +229,24 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
|
||||||
# Create first routine
|
# Sentinel has return PC for frame 1
|
||||||
|
stack_manager._call_stack[0].program_counter = 0x1000
|
||||||
|
|
||||||
routine1 = ZRoutine(
|
routine1 = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1000,
|
return_addr=0, # varnum: push to stack
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0xAAAA],
|
local_vars=[0xAAAA],
|
||||||
stack=[0xBBBB],
|
stack=[0xBBBB],
|
||||||
)
|
)
|
||||||
stack_manager.push_routine(routine1)
|
stack_manager.push_routine(routine1)
|
||||||
|
# Frame1 has return PC for frame 2
|
||||||
|
routine1.program_counter = 0x2000
|
||||||
|
|
||||||
# Create second routine (nested)
|
|
||||||
routine2 = ZRoutine(
|
routine2 = ZRoutine(
|
||||||
start_addr=0x6000,
|
start_addr=0x6000,
|
||||||
return_addr=0x2000,
|
return_addr=5, # varnum: store to local var 4
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0xCCCC],
|
local_vars=[0xCCCC],
|
||||||
|
|
@ -271,10 +259,9 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected: two frames concatenated
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
# Frame 1
|
# Frame 1: return_pc from sentinel (0x1000), varnum=0
|
||||||
0x00,
|
0x00,
|
||||||
0x10,
|
0x10,
|
||||||
0x00, # return_pc
|
0x00, # return_pc
|
||||||
|
|
@ -287,12 +274,12 @@ class TestStksChunkGeneration:
|
||||||
0xAA, # local_var[0]
|
0xAA, # local_var[0]
|
||||||
0xBB,
|
0xBB,
|
||||||
0xBB, # stack[0]
|
0xBB, # stack[0]
|
||||||
# Frame 2
|
# Frame 2: return_pc from routine1 (0x2000), varnum=5
|
||||||
0x00,
|
0x00,
|
||||||
0x20,
|
0x20,
|
||||||
0x00, # return_pc
|
0x00, # return_pc
|
||||||
0x01, # flags (1 local)
|
0x01, # flags (1 local)
|
||||||
0x00, # varnum
|
0x05, # varnum
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
0x00,
|
0x00,
|
||||||
0x01, # eval_stack_size = 1
|
0x01, # eval_stack_size = 1
|
||||||
|
|
@ -316,11 +303,12 @@ class TestStksChunkGeneration:
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
stack_manager = ZStackManager(zmachine._mem)
|
stack_manager = ZStackManager(zmachine._mem)
|
||||||
|
# Sentinel PC = 0 (main routine has no caller)
|
||||||
|
stack_manager._call_stack[0].program_counter = 0
|
||||||
|
|
||||||
# Create a routine with return_addr=0 (main routine)
|
|
||||||
routine = ZRoutine(
|
routine = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x0000,
|
return_addr=0, # varnum: push to stack
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x1234],
|
local_vars=[0x1234],
|
||||||
|
|
@ -332,12 +320,11 @@ class TestStksChunkGeneration:
|
||||||
writer = QuetzalWriter(zmachine)
|
writer = QuetzalWriter(zmachine)
|
||||||
result = writer._generate_stks_chunk()
|
result = writer._generate_stks_chunk()
|
||||||
|
|
||||||
# Expected format with return_pc = 0
|
|
||||||
expected = bytes(
|
expected = bytes(
|
||||||
[
|
[
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x00, # return_pc = 0 (main routine)
|
0x00, # return_pc = 0
|
||||||
0x01, # flags (1 local)
|
0x01, # flags (1 local)
|
||||||
0x00, # varnum
|
0x00, # varnum
|
||||||
0x00, # argflag
|
0x00, # argflag
|
||||||
|
|
@ -365,22 +352,24 @@ class TestStksRoundTrip:
|
||||||
zmachine._mem = Mock()
|
zmachine._mem = Mock()
|
||||||
zmachine._mem.version = 5
|
zmachine._mem.version = 5
|
||||||
|
|
||||||
# Create original stack with multiple frames
|
|
||||||
original_stack = ZStackManager(zmachine._mem)
|
original_stack = ZStackManager(zmachine._mem)
|
||||||
|
# Sentinel has return PC for frame 1
|
||||||
|
original_stack._call_stack[0].program_counter = 0
|
||||||
|
|
||||||
routine1 = ZRoutine(
|
routine1 = ZRoutine(
|
||||||
start_addr=0x5000,
|
start_addr=0x5000,
|
||||||
return_addr=0x1234,
|
return_addr=5, # varnum: store to local var 4
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0x0001, 0x0002, 0x0003],
|
local_vars=[0x0001, 0x0002, 0x0003],
|
||||||
stack=[0x1111, 0x2222],
|
stack=[0x1111, 0x2222],
|
||||||
)
|
)
|
||||||
original_stack.push_routine(routine1)
|
original_stack.push_routine(routine1)
|
||||||
|
routine1.program_counter = 0x5678 # resume PC for after routine2
|
||||||
|
|
||||||
routine2 = ZRoutine(
|
routine2 = ZRoutine(
|
||||||
start_addr=0x6000,
|
start_addr=0x6000,
|
||||||
return_addr=0x5678,
|
return_addr=3, # varnum: store to local var 2
|
||||||
zmem=zmachine._mem,
|
zmem=zmachine._mem,
|
||||||
args=[],
|
args=[],
|
||||||
local_vars=[0xAAAA],
|
local_vars=[0xAAAA],
|
||||||
|
|
@ -398,20 +387,20 @@ class TestStksRoundTrip:
|
||||||
parser = QuetzalParser(zmachine)
|
parser = QuetzalParser(zmachine)
|
||||||
parser._parse_stks(stks_data)
|
parser._parse_stks(stks_data)
|
||||||
|
|
||||||
# Verify the deserialized stack matches
|
|
||||||
restored_stack = zmachine._stackmanager
|
restored_stack = zmachine._stackmanager
|
||||||
|
|
||||||
# Should have 2 frames plus the bottom sentinel
|
|
||||||
assert len(restored_stack._call_stack) == 3
|
assert len(restored_stack._call_stack) == 3
|
||||||
|
|
||||||
# Check frame 1
|
# Check frame 1: return_addr is varnum
|
||||||
frame1 = restored_stack._call_stack[1]
|
frame1 = restored_stack._call_stack[1]
|
||||||
assert frame1.return_addr == 0x1234
|
assert frame1.return_addr == 5
|
||||||
assert frame1.local_vars[:3] == [0x0001, 0x0002, 0x0003]
|
assert frame1.local_vars[:3] == [0x0001, 0x0002, 0x0003]
|
||||||
assert frame1.stack == [0x1111, 0x2222]
|
assert frame1.stack == [0x1111, 0x2222]
|
||||||
|
# Frame1's program_counter was set from frame2's return_pc
|
||||||
|
assert frame1.program_counter == 0x5678
|
||||||
|
|
||||||
# Check frame 2
|
# Check frame 2
|
||||||
frame2 = restored_stack._call_stack[2]
|
frame2 = restored_stack._call_stack[2]
|
||||||
assert frame2.return_addr == 0x5678
|
assert frame2.return_addr == 3
|
||||||
assert frame2.local_vars[:1] == [0xAAAA]
|
assert frame2.local_vars[:1] == [0xAAAA]
|
||||||
assert frame2.stack == [0xBBBB, 0xCCCC, 0xDDDD]
|
assert frame2.stack == [0xBBBB, 0xCCCC, 0xDDDD]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue