Update investigation and journey docs with session 3 findings

This commit is contained in:
Jared Miller 2026-02-09 23:06:43 -05:00
parent bbd70e8577
commit 1b08c36b85
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 109 additions and 1 deletions

View file

@ -198,7 +198,7 @@ How many of the ~62 missing zvm opcodes are actually exercised by V3 games? V3 u
UPDATE: Opcode tracing (via ``scripts/trace_zmachine.py``) found Zork 1 uses 69 opcodes. zvm had 36 implemented. 33 were ported from viola. All 69 are now implemented in the hybrid interpreter (``src/mudlib/zmachine/``).
Remaining gaps: save/restore (QuetzalWriter needs completion) and sread tokenization.
All V3 gaps have been resolved. save/restore works, and sread tokenization works correctly.
2. zvm/viola memory layout compatibility
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -260,6 +260,8 @@ What works:
- ``step()`` method for async MUD integration — single instruction at a time, no blocking loop
- instruction trace deque (last 20 instructions) for debugging state errors
- smoke test: ``scripts/run_zork1.py`` runs the game headless, exercises core opcode paths
- parser and lexer: all Zork 1 commands work (look, open mailbox, read leaflet, inventory, take, drop, navigation)
- the interpreter is fully playable for Zork 1
What this enables:

View file

@ -220,6 +220,112 @@ Debugging approach for next session:
5. Look at how the ``and`` result (step 21 in trace) flows through to
the verb syntax lookup
Diagnostic Tools
~~~~~~~~~~~~~~~~
``scripts/debug_zstrings.py`` traces all z-string decoding with caller info.
Run with::
echo -e "look\nquit\ny" | python3 scripts/debug_zstrings.py 2>/tmp/trace.log
Trace goes to stderr, game output to stdout.
To add instruction tracing after sread, add to ``zcpu.py``::
# In step(), after decoding:
print(f"PC={pc:#06x} {handler_name}({', '.join(str(a) for a in args)})",
file=sys.stderr)
Session 3: Parser Fixed - Interpreter Now Playable
---------------------------------------------------
Bug 8: op_jl uses unsigned comparison
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In ``zcpu.py``, ``op_jl`` (jump if less-than) compared raw 16-bit values
without ``_make_signed()``, the same class of bug as Bug 7 (op_dec_chk
and op_inc_chk).
Additionally, ``op_jl`` had a non-standard signature::
def op_jl(self, a, *others):
for b in others:
if a < b:
...
The Z-machine spec says JL takes exactly 2 operands. Compare with
``op_jg`` which correctly uses ``def op_jg(self, a, b)``.
Fix: add ``_make_signed()`` calls to both operands, normalize signature
to ``(self, a, b)`` matching ``op_jg``.
This alone did not fix the parser - needed in combination with Bug 9.
Bug 9: finish_routine return value storage - THE PARSER BUG
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Two sub-bugs in ``finish_routine()`` in ``zstackmanager.py``:
**Range check used decimal instead of hex**::
if result_variable < 10: # WRONG: should be 0x10 (16)
self.set_local_variable(result_variable, result)
Z-machine variable numbering: 0 = stack push, 1-15 = locals, 16+ = globals.
The check ``< 10`` meant local variables 10-15 were written as globals.
**Missing index adjustment for 1-based variable numbering**::
self.set_local_variable(result_variable, result) # WRONG
Z-machine variables are 1-based (variable 1 = first local), but
``set_local_variable()`` uses 0-based indexing (index 0 = first local).
Should be ``set_local_variable(result_variable - 1, result)``.
Both ``_read_variable()`` and ``_write_result()`` in ``zcpu.py``
correctly used ``addr - 1`` and ``< 0x10``, but ``finish_routine()``
in ``zstackmanager.py`` did not.
Effect: when a function returned a value to a local variable, it went
to the wrong slot. The caller read the correct slot and got stale data
(0 or the initialization value from the function call).
This is why every verb was recognized by the dictionary but rejected by
the parser. The word-type checker function returned the correct type,
but the return value landed in the wrong local variable. The parser saw
0 and could not match any syntax pattern.
Room descriptions worked because those code paths used stack returns
(variable 0) or global returns, not local variable returns.
Fix: change ``< 10`` to ``< 0x10``, add ``- 1`` to the
``set_local_variable`` call.
Bug 10: zlexer does not truncate words for dictionary lookup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In ``zlexer.py``, V3 dictionary entries store words truncated to 6
characters (4 bytes of Z-string = 2 Z-words). V4+ uses 9 characters.
``parse_input()`` looked up full-length input words, so "mailbox" (7
characters) did not match "mailbo" (6 characters) in the dictionary.
Fix: truncate lookup key to ``6 if version <= 3 else 9`` before
``dict.get()``. The original word is preserved in the return list for
correct parse buffer positions.
What's Fixed Now
~~~~~~~~~~~~~~~~
After all fixes:
- All Zork 1 commands work: look, open mailbox, read leaflet, go north,
inventory, quit, take, drop
- Navigation, object manipulation, multi-word commands, game logic all
functional
- The interpreter is playable
Diagnostic Tools
----------------