Add a viola embedding audit
This commit is contained in:
parent
64dcd8d6e4
commit
61dd321b86
1 changed files with 156 additions and 0 deletions
156
docs/how/viola-embedding-audit.rst
Normal file
156
docs/how/viola-embedding-audit.rst
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
=============================
|
||||||
|
viola embedding audit — Z-machine interpreter feasibility
|
||||||
|
=============================
|
||||||
|
|
||||||
|
viola is a Python Z-machine interpreter being evaluated for embedding in mudlib
|
||||||
|
to run interactive fiction games over telnet. this audit covers architecture,
|
||||||
|
isolation requirements, and modification paths.
|
||||||
|
|
||||||
|
|
||||||
|
1. global state — fork-level entangled
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
13 of 18 modules in zcode/ have mutable module-level globals.
|
||||||
|
|
||||||
|
critical three:
|
||||||
|
|
||||||
|
- game.py — PC, callstack, currentframe, undolist, interruptstack. modified by
|
||||||
|
20+ functions across multiple modules via global declarations
|
||||||
|
- memory.py — data (entire Z-machine memory as a bytearray). read/written by
|
||||||
|
nearly every module
|
||||||
|
- screen.py — currentWindow, zwindow list, color spectrum dict. all mutated at
|
||||||
|
runtime
|
||||||
|
|
||||||
|
every module does ``import zcode`` then accesses ``zcode.game.PC``,
|
||||||
|
``zcode.memory.data``, etc. hardcoded module-level lookups, not
|
||||||
|
dependency-injected. state mutation touches 1000+ function calls.
|
||||||
|
|
||||||
|
wrapping in a class would require threading a state parameter through the
|
||||||
|
entire codebase. process-level isolation (subprocess per game) is the realistic
|
||||||
|
path for concurrent games.
|
||||||
|
|
||||||
|
|
||||||
|
2. pygame boundary — clean
|
||||||
|
===========================
|
||||||
|
|
||||||
|
zero pygame imports in zcode/. architecture uses adapter pattern:
|
||||||
|
|
||||||
|
- zcode/ imports only vio.zcode as io (aliased)
|
||||||
|
- vio/zcode.py contains ALL pygame calls (1044 lines)
|
||||||
|
- dependency is unidirectional — vio.zcode never imports from zcode/
|
||||||
|
- zcode/ extends io classes via inheritance (window, font, sound channels)
|
||||||
|
|
||||||
|
can swap vio/zcode.py for a telnet IO adapter without touching VM core.
|
||||||
|
|
||||||
|
|
||||||
|
3. error paths — server-hostile
|
||||||
|
================================
|
||||||
|
|
||||||
|
8+ locations call sys.exit() directly:
|
||||||
|
|
||||||
|
- memory out-of-bounds (5 locations in memory.py)
|
||||||
|
- invalid opcode (opcodes.py)
|
||||||
|
- division by zero (numbers.py)
|
||||||
|
- corrupt story file (viola.py)
|
||||||
|
|
||||||
|
fatal path: ``zcode.error.fatal()`` -> ``sys.exit()``. not catchable exceptions.
|
||||||
|
|
||||||
|
fix: patch error.fatal() to raise a custom exception instead.
|
||||||
|
|
||||||
|
|
||||||
|
4. version coverage — V1-V5 solid, V6 partial, V7-V8 theoretical
|
||||||
|
=================================================================
|
||||||
|
|
||||||
|
- V1-V5: all opcodes implemented. most IF games are V3 or V5.
|
||||||
|
- V6: graphics framework exists but 3 opcodes marked "unfinished"
|
||||||
|
- V7-V8: supported at memory/opcode level but likely never tested
|
||||||
|
- no test suite in the repo
|
||||||
|
|
||||||
|
for a MUD, V3 and V5 cover Zork, Curses, Anchorhead, etc.
|
||||||
|
|
||||||
|
|
||||||
|
5. input model — moderate-to-hard adapter needed
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
line input (READ):
|
||||||
|
easy — maps naturally to telnet line mode
|
||||||
|
|
||||||
|
single char (READ_CHAR):
|
||||||
|
hard — needs raw telnet mode or buffering
|
||||||
|
|
||||||
|
timed input:
|
||||||
|
hard — needs async server-side timer
|
||||||
|
|
||||||
|
mouse input:
|
||||||
|
impossible — stub it
|
||||||
|
|
||||||
|
arrow/function keys:
|
||||||
|
moderate — parse ANSI escape sequences
|
||||||
|
|
||||||
|
pygame is hardwired with no abstraction layer. need to create a TelnetInput
|
||||||
|
class implementing the same interface as vio.zcode.input. interface surface is
|
||||||
|
small: getinput, starttimer, stoptimer.
|
||||||
|
|
||||||
|
|
||||||
|
6. step execution mode — moderate refactor, feasible
|
||||||
|
=====================================================
|
||||||
|
|
||||||
|
main loop in routines.py:60 is clean::
|
||||||
|
|
||||||
|
while not quit and not restart:
|
||||||
|
check interrupts -> decode(PC) -> runops(oldpc)
|
||||||
|
|
||||||
|
one instruction = decode + execute. only blocking call is pygame.event.wait()
|
||||||
|
at bottom of input stack. natural yield points are input requests.
|
||||||
|
|
||||||
|
execute_one() API achievable by:
|
||||||
|
|
||||||
|
1. making IO non-blocking
|
||||||
|
2. converting z_read()'s inner while loop to a state machine
|
||||||
|
3. unwrapping recursive interrupt_call() -> execloop() pattern
|
||||||
|
|
||||||
|
|
||||||
|
7. memory/cleanup — will leak, fixable
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
unbounded growth:
|
||||||
|
|
||||||
|
- undo stack (game.py:23) — each undo saves full memory copy (MBs). never
|
||||||
|
cleared.
|
||||||
|
- command history (input.py:22) — every input appended forever
|
||||||
|
- static routine cache (routines.py:30) — never cleared
|
||||||
|
- static word cache (memory.py:108) — never cleared
|
||||||
|
- object caches (objects.py:18-26) — 4 dicts, never purged
|
||||||
|
|
||||||
|
estimate: 10-100+ MB/hour depending on undo usage. fixable by adding cleanup
|
||||||
|
calls to restart/setup path.
|
||||||
|
|
||||||
|
|
||||||
|
8. IFIFF submodule — quetzal is independent
|
||||||
|
============================================
|
||||||
|
|
||||||
|
- quetzal (saves) and blorb (multimedia) are architecturally separate
|
||||||
|
- zcode/game.py imports only quetzal — no blorb references in save/restore
|
||||||
|
- quetzal API: qdata(), save(), restore()
|
||||||
|
- pure stdlib, no external deps
|
||||||
|
- can cherry-pick quetzal.py without pulling in blorb
|
||||||
|
|
||||||
|
|
||||||
|
priority for upstream PRs
|
||||||
|
==========================
|
||||||
|
|
||||||
|
1. patch error.fatal() to raise instead of sys.exit() — small effort, unblocks
|
||||||
|
server use
|
||||||
|
2. add cleanup hooks for caches/undo on restart — small effort, fixes memory
|
||||||
|
leaks
|
||||||
|
3. create IO abstraction interface — medium effort, enables telnet adapter
|
||||||
|
4. step execution mode (execute_one()) — medium effort, enables async embedding
|
||||||
|
5. extract quetzal as standalone — small effort, clean dependency
|
||||||
|
|
||||||
|
|
||||||
|
pragmatic path
|
||||||
|
==============
|
||||||
|
|
||||||
|
the global state issue is bypassed with subprocess isolation (one Python
|
||||||
|
process per game). fine for a MUD with less than 100 concurrent players.
|
||||||
|
process-per-game sidesteps the entire global state refactor.
|
||||||
Loading…
Reference in a new issue