Update investigation and journey docs with session 3 findings
This commit is contained in:
parent
bbd70e8577
commit
1b08c36b85
2 changed files with 109 additions and 1 deletions
|
|
@ -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/``).
|
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
|
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
|
- ``step()`` method for async MUD integration — single instruction at a time, no blocking loop
|
||||||
- instruction trace deque (last 20 instructions) for debugging state errors
|
- instruction trace deque (last 20 instructions) for debugging state errors
|
||||||
- smoke test: ``scripts/run_zork1.py`` runs the game headless, exercises core opcode paths
|
- 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:
|
What this enables:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,112 @@ Debugging approach for next session:
|
||||||
5. Look at how the ``and`` result (step 21 in trace) flows through to
|
5. Look at how the ``and`` result (step 21 in trace) flows through to
|
||||||
the verb syntax lookup
|
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
|
Diagnostic Tools
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue