diff --git a/docs/how/viola-embedding-audit.rst b/docs/how/viola-embedding-audit.rst new file mode 100644 index 0000000..e44d870 --- /dev/null +++ b/docs/how/viola-embedding-audit.rst @@ -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.