# ZIL Importer Reference The `moo.zil_import` package converts Infocom ZIL source files into a DjangoMOO bootstrap package. It produced the `zork1` dataset (covered in django-moo's bootstrapping reference) from the upstream `dungeon.zil` and `actions.zil` files released by Microsoft / Activision under the MIT License in 2025. The importer is a build tool, not a runtime component. The output it produces — `moo/bootstrap/zork1/` — is committed to the repository. Day-to-day use of django-moo never invokes the importer; it runs only when the importer itself is being changed or when a new upstream ZIL source is being absorbed. Every generated file carries a ``# Generated by moo/zil_import — do not edit by hand`` header. For background — what ZIL is, why translation is structured this way, and how the pipeline is laid out — see {doc}`../explanation/zil-importer`. ## Pipeline The package is a four-stage pipeline; each stage is independently testable and exposes a small public surface: ```text *.zil ──► parser ──► converter ──► translator ──► generator ──► moo/bootstrap// (tokens (IR dataclasses) (per-routine (per-bootstrap + AST) Python text) file emission) ``` | Stage | Module | Public entry points | | -------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Parser | `moo.zil_import.parser` | `tokenize`, `parse`, `parse_file`, `Str`, `Token`, `ParseError` | | Converter | `moo.zil_import.converter` | `extract_all`, `extract_syntax_rules` | | Translator | `moo.zil_import.translator` (package) | `ZilTranslator`, `translate_routine`, `translate_m_clause`, `translate_f_clause`, `has_m_dispatch`, `has_f_dispatch`, `sanitize_ident` | | Generator | `moo.zil_import.generator` (package) | `generate_all`, `GeneratorIR`, `GeneratorOptions` | | Migration gate | `moo.zil_import.migration` | `MIGRATED_VERBS` | | Coverage audit | `moo.zil_import.audit` | `RegenAudit` | | Snapshot | `moo.zil_import.snapshot` | `capture_snapshot`, `restore_snapshot`, `SnapshotSiteMismatch` | | Verb metadata | `moo.zil_import.verb_metadata` | `parse_verb_file`, `iter_verb_files`, `VerbShebang` | | Game config | `moo.zil_import.game_config` | `GameConfig`, `ZORK1_CONFIG`, `HHG_CONFIG`, `GAME_CONFIGS`, `resolve_game_config` | | CLI | `moo.zil_import.cli` | `main` | ## Running the importer ```bash uv run python -m moo.zil_import dungeon.zil actions.zil --output moo/bootstrap/zork1 ``` `moo.zil_import.__main__` delegates to `cli.main`, which parses each input file, runs `converter.extract_all`, then `generator.generate_all`. The bootstrap output is committed to the repository; subsequent regenerations are an importer-development activity, not a deployment step. CLI flags: | Flag | Default | Effect | | ------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------- | | `FILE.zil` | required, repeatable | ZIL source files, parsed in order. Manifests (``) are expanded recursively. | | `--game-config` | `zork1` | Per-game configuration slug; choices are `zork1` and `hhg` (extend via `game_config.GAME_CONFIGS`). | | `--output` | `moo/bootstrap/` | Bootstrap output directory. Defaults to the selected game-config's `dataset_name`. | | `--verbose` / `-v` | off | Drop log level to `DEBUG`. | | `--lint` | off | Run pylint on every generated file; fail when the score drops below `--lint-threshold`. Adds 30–60 s. | | `--lint-threshold` | `9.0` | Minimum acceptable pylint score (only used with `--lint`). | ## IR dataclasses The intermediate representation is a small set of dataclasses defined in `moo.zil_import.ir`. Every stage downstream of the converter operates on these — never on raw AST nodes. ```{eval-rst} .. py:currentmodule:: moo.zil_import.ir .. autoclass:: ZilRoom :members: .. autoclass:: ZilObject :members: .. autoclass:: ZilExit :members: .. autoclass:: ZilRoutine :members: .. autoclass:: ZilTable :members: ``` `ZilRoutine.initial_values` keys parameter and aux-var names (in UPPER-KEBAB form) to their declared default expressions. A missing key means the variable has no declared default and the translator emits `None` (parameters) or `0` (aux locals — Z-machine semantics). ## Parser ```{eval-rst} .. py:currentmodule:: moo.zil_import.parser .. autofunction:: tokenize .. autofunction:: parse .. autofunction:: parse_file .. autoclass:: Str .. autoclass:: Token :members: .. autoclass:: ParseError ``` `Str` is a `str` subclass used to distinguish ZIL string literals (`"hello"`) from bare atoms (`HELLO`). They look identical at the character level after the parser strips quotes, but the translator needs to emit one as a Python string literal and the other as either an object reference or a state-variable read. `isinstance(node, Str)` discriminates. The number tokenizer uses a negative lookahead so ZIL predicate atoms like `0?` and `1?` are recognised as single atoms rather than `number, atom` pairs. ## Converter ```{eval-rst} .. py:currentmodule:: moo.zil_import.converter .. autofunction:: extract_all ``` `extract_all` walks the parsed AST and returns a 9-tuple of dicts — rooms, objects, routines, tables, globals, syntax rules, synonyms, compound verbs, and bare-syntax rules — keyed by ZIL atom name (for example, `"WHITE-HOUSE"`, `"TROLL"`, `"ROBBER-FUNCTION"`). Forms it does not recognise (free-standing strings, ``, etc.) are silently dropped. Any form that matches a known head but fails extraction is logged at WARNING level with the atom name and the cause, and the form is skipped. ### Converter notes ZIL `` and `` literals are extracted by `_extract_table_values`. The helper resolves bare atoms (`@`-prefixed references retained as references for late lookup), preserves `<>` nil slots in villain records, and skips any `(PURE)`-style flag groups — the IR carries only the data slot values. ## Translator ```{eval-rst} .. py:currentmodule:: moo.zil_import.translator .. autoclass:: ZilTranslator :members: .. autofunction:: translate_routine .. autofunction:: translate_m_clause .. autofunction:: translate_f_clause .. autofunction:: has_m_dispatch .. autofunction:: has_f_dispatch .. autofunction:: sanitize_ident ``` `ZilTranslator` accepts two optional sets, `object_atoms` and `routine_atoms`, that disambiguate bare atoms in expression context: - An atom in `object_atoms` translates to `lookup("name")` — a real DjangoMOO object reference suitable for attribute access. - An atom in `routine_atoms` (a known ZIL routine) translates to a zero-arg verb dispatch on the routine's owner (or `_.thing` for free helpers). - An atom appearing as a key in `GLOBAL_MAP` (in `translator/constants.py`) translates to the canonical Python expression for that ZIL global — `WINNER` and `PLAYER` map to `context.player`, `HERE` to `context.player.here()`, `SCORE` to `context.player.zstate_get('SCORE')`, and so on. - Anything else falls back to `context.player.zstate_get('NAME')`. The generator passes the union of room and object atoms as `object_atoms`, the set of routine names as `routine_atoms`, and a `GameConfig` whose `npc_atom_map` extends `GLOBAL_MAP` per-game (e.g. `,THIEF` → `lookup("thief")`). ### Translator package layout `moo.zil_import.translator` is a package with five files: | File | Role | | ----------------- | ---------------------------------------------------------------------------------------------------------- | | `__init__.py` | The `ZilTranslator` class and the public free functions; orchestrates expression and statement dispatch. | | `constants.py` | Atom maps, dispatch-mode tables (`PROP_MAP`, `GLOBAL_MAP`, `M_CLAUSES`, `F_CLAUSES`, `M_TO_VERB`, `SDK_HEADS`, …), Python keyword/builtin shadow lists, and pylint-disable formatters. | | `identifiers.py` | Pure-function helpers for naming: `sanitize_ident`, `verb_attr_safe`, `predicate_python_name`, `routine_dot_name`, `as_object`, `ends_in_unconditional_return`, `pylint_disable_line`. | | `stmt_handlers.py`| Statement-level form-head handlers — one function per ZIL form (`MOVE`, `REMOVE`, `SETG`, `PUTP`, `COND`, `JIGS-UP`, …). `HANDLERS` is the dispatch table. | | `expr_handlers.py`| Expression-level form-head handlers — arithmetic, comparison, flag predicates, table operators, `VERB?` dispatch, etc. `HANDLERS` is the dispatch table. | Adding a new ZIL idiom is a one-line dispatch-table entry plus a small handler function. ### Translation idioms A representative sample of the translator's recurring patterns: | ZIL form | Python emission | | --------------------------------- | -------------------------------------------------------- | | `` | `print("msg")` | | `` | `print()` | | `` | `print(lookup("obj").desc(), end='')` | | `` | `lookup("lamp").moveto(context.player.here())` | | `` | `_.remove(lookup("obj"))` | | `` / `` | `lookup("x").set_flag("flag", True)` / `… False` | | `` | `lookup("obj").set_property("prop", val)` | | `` | `context.player.zstate_set('KEY', val)` | | `` | `if cond: body` / `else: body` (true if/elif/else chain) | | `` (statement context) | `if a: b` (nested if-chain to avoid expression-as-stmt) | | `` (expression) | `(x := expr)` (walrus) | | `<0? .X>` / `<1? .X>` | `x == 0` / `x == 1` | | `<==? a b>` / `` | `a == b` / `a in (b, c)` | | Bare `,FOO` in expression | `GLOBAL_MAP['FOO']` if known, else `context.player.zstate_get('FOO')` | | `` (predicate routine)| `is_lit(context.player.here())` | | `>` | `_.queue("r", 5)` | | `` | `_.jigs_up("msg")` | | `` | `_.score_update(n)` | | `` | `_.pick("table")` | For predicate routines (whose ZIL name ends in `?`) the translator wraps the trailing expression of the body in `return` so the implicit ZIL return value becomes an explicit Python one. ### Identifier sanitization ZIL allows `-`, `?`, and a leading `.` (local-var deref) in atoms; Python does not. `sanitize_ident` (in `translator/identifiers.py`): - strips leading `.`, - replaces `-` with `_` and trailing `?` with `_p`, - collapses any other non-alphanumeric character to `_`, - prefixes leading digits with `v_`, - suffixes Python keywords and shadowed builtins (`set`, `list`, `dict`, `print`, …) with `_v`. The result is always a valid, non-shadowing Python identifier. ### ZilTranslator state `ZilTranslator.__init__` accepts a routine plus several optional context dictionaries supplied by the generator. The most commonly relevant: - `object_atoms`, `routine_atoms` — atom-disambiguation sets described above. - `action_owner` — `(atom, is_room)` of the room/object whose `ACTION` property points at this routine. Drives the `--on` shebang on the generated verb file. - `display_names`, `substrate_display_names` — atom → display-name maps so `--on ""` shebangs resolve at verb-load time. - `routine_to_verbs` — V-routine → list of player verbs that dispatch to it via SYNTAX rules. - `pre_handler_routines` — V-routines whose `PRE-X` handler should be inlined at the top of the substrate body. - `lint_active` — when set, the verb file emits only format-intrinsic pylint disables (no file-level escape hatches). - `game_config` — `GameConfig` instance (defaults to `ZORK1_CONFIG`) whose `npc_atom_map` extends `GLOBAL_MAP` per-game. ## Game configuration ```{eval-rst} .. py:currentmodule:: moo.zil_import.game_config .. autoclass:: GameConfig :members: ``` `ZORK1_CONFIG` is the default instance and supplies the Zork 1 banner, manifest list, license blurb, and NPC atom map. `HHG_CONFIG` is a second registered config (Infocom's *Hitchhiker's Guide to the Galaxy*) that extends `GameConfig` with three HHG-specific knobs: - `player_avatar_atoms` — the multi-POV atoms `ARTHUR`, `FORD`, `TRILLIAN`, `ZAPHOD` that name protagonist placeholders rather than real NPCs. - `synonym_expansions` — full-word aliases for ZIL's 6-char-truncated dictionary entries (e.g. `ASPIRI` → `aspirin`). - `reset_body_filename` — the script under `moo/zil_import/scripts/` whose contents become `099_reset_state.py` for this dataset. Both configs are registered in `GAME_CONFIGS` and the CLI's `--game-config` flag selects between them. To target a third title, construct a new `GameConfig`, register it in `GAME_CONFIGS`, and either run `python -m moo.zil_import … --game-config ` or pass it directly to `generate_all` / `ZilTranslator`. ## Generator ```{eval-rst} .. py:currentmodule:: moo.zil_import.generator .. autofunction:: generate_all .. autoclass:: GeneratorIR .. autoclass:: GeneratorOptions ``` `generate_all` accepts the parsed rooms/objects/routines plus a `GeneratorIR` (tables, globals, syntax tables) and `GeneratorOptions` (linter, game config). The output layout: | Output file | Source | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | `/__init__.py` | empty (so test discovery doesn't run the bootstrap) | | `/bootstrap.py` | orchestrator that loads the numbered scripts | | `/010_classes.py` | rendered template — root-class hierarchy + Wizard parent | | `/013_globals.py` | ZIL `` / `` / `` scalar initial values | | `/020_rooms.py` | one stanza per `ZilRoom` | | `/030_objects.py` | one stanza per `ZilObject`, parented by its CONTBIT/etc. | | `/035_tables.py` | ZIL `
` / `` data attached to the System Object | | `/040_exits.py` | per-room exit lists | | `/050_daemons.py` | seed/reset records for realtime-scheduled daemons | | `/098_adventurer.py` | non-wizard player avatar plus Player record migration | | `/099_reset_state.py` | canonical world-state reset (per-game body via `GameConfig.reset_body_filename`) | | `/coverage.json` | `RegenAudit` decision-point report; ratcheted by `test_translator_coverage.py` | | `/verbs///*.py` | one file per translated routine; owner is the action_owner | | `/verbs//.py` | combined M-/F-clause emission; aliases every clause-role name in its shebang | | `/verbs/syntax_rows/[_][_].py` | one parser-entry runner per `ZilSyntaxRule` cell (gated by `MIGRATED_VERBS`) | | `/verbs/thing/v_routines/v_.py` | passive V-routine helper invoked programmatically from the syntax-row runner | | `/tests/conftest.py` | `zork_world` fixture wrapping `t_init` | | `/tests/test_rooms.py` | room sanity assertions | | `/tests/test_objects.py` | object sanity assertions, ambiguous-name aware | | `/tests/test_exits.py` | structural exit assertions | | `/tests/test_translated_verbs.py` | meta-test that every translated verb file loads | Empty M-*/F-* clauses (whose ZIL body is `<>`) are skipped — the generator does not register a no-op verb for them. The combined M-/F-emission is implemented by `ZilTranslator.translate_combined_clauses` and produces a single verb file per action-owning routine with an `if rarg == "M-X": …` ladder; the per-event filename layout the legacy generator emitted (`__m_.py`) is no longer used. ## Migration gate ```{eval-rst} .. py:currentmodule:: moo.zil_import.migration .. autodata:: MIGRATED_VERBS :no-value: ``` `MIGRATED_VERBS` is the per-verb switch for the syntax-row dispatcher refactor. Verbs listed in this set emit through `templates/syntax_row.py.j2` — one runner per `(verb, particle, iobj_prep, arity)` cell — and their substrate `--dspec` is rewritten to `none` so the runner is the only parser entry point. Verbs not in the set continue through the legacy per-verb-atom dispatcher under `verbs/actor/dispatchers/`. The module will be removed once the legacy path retires. ## Coverage audit ```{eval-rst} .. py:currentmodule:: moo.zil_import.audit .. autoclass:: RegenAudit :members: ``` `RegenAudit` accumulates per-routine drop records during emission and writes `coverage.json` next to the bootstrap output. Tracked drop kinds are `m_clause_dropped`, `f_clause_dropped`, `verb_clause_dropped`, `syntax_rule_dropped`, and `unhandled_form`. The ratchet test at `tests/test_translator_coverage.py` compares `coverage.json` against a checked-in baseline: a new drop fails as a regression; a healed drop fails so the baseline can be re-collected via `tests/_collect_coverage_baseline.py`. ## Snapshot and restore ```{eval-rst} .. py:currentmodule:: moo.zil_import.snapshot .. autofunction:: capture_snapshot .. autofunction:: restore_snapshot .. autoexception:: SnapshotSiteMismatch ``` Use the snapshot helpers when a `--reset` alone doesn't unwind enough state — long sessions accumulate mutated object locations, daemon counters, and NPC flags that the avatar-only reset can't fix. `capture_snapshot` freezes every Object on the target site (`location_pk`, properties, flags) into a JSON file; `restore_snapshot` re-asserts `site_pk` and `site_domain` against the recorded values before any write, raising `SnapshotSiteMismatch` if they disagree. ## Verb-file metadata ```{eval-rst} .. py:currentmodule:: moo.zil_import.verb_metadata .. autofunction:: parse_verb_file .. autofunction:: iter_verb_files .. autoclass:: VerbShebang :members: ``` Used by `tests/test_bootstrap_consistency.py` to enforce shebang/body alignment across every generated verb file — every verb-name literal in the body must appear in the shebang's `names` list, and every `.perform("X", …)` call must name a verb registered somewhere in the bootstrap. ## Verb tree layout The static templates under `moo/zil_import/verbs/` are copied verbatim into the bootstrap output. Each top-level directory is a verb-`--on` owner: | Directory | Owner | Contents | | --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `verbs/system/` | `System Object` | Parser shims, dispatch helpers (`do_command`, `dispatch_multi`, `resolve_dobj_late`), movement, queue daemons, take/peek helpers. | | `verbs/root/` | `Root` | Predicates and primitives every object inherits — `flag`/`getp`, `output`, `moveto`. | | `verbs/thing/` | `Thing` | Substrate verbs plus subdirs: `predicates/` (LIT?, ACCESSIBLE?, …), `object_handlers/`, `daemons/` (queue routines), `v_routines/` (passive V-* helpers). | | `verbs/actor/` | `Actor` | Player-side verbs and state — `echo`, `state` getters/setters, dispatchers under `dispatchers/`. | | `verbs/exit/` | `Exit` | The `move` verb that traverses exit Objects. | | `verbs/zork1/` | game-specific (Zork 1 only) | Per-game overrides — e.g. `pot_of_gold_pre_take.py`. Files outside this directory must stay game-agnostic. | | `verbs/hhg/` | game-specific (HHG only) | HHG-specific overrides (`brick_death.py`, identity-flag handlers, …). Same neutrality contract as `verbs/zork1/`. | When the translator emits a routine whose `ACTION` property points at a specific room or object, the per-action-owner verb file lands under `verbs//`. After regen the directory tree therefore mixes the static template owners above with action-owner directories like `verbs/sailor/`, `verbs/granite_wall/`, `verbs/troll/`, etc. If you add a new routine head to the translator's recognition table (`SDK_HEADS` in `translator/constants.py`), make sure the corresponding SDK verb exists under `verbs/root/` or `verbs/thing/` before regenerating. ## Regenerating the bootstrap After changing the parser, converter, translator, generator, or any verb template — or after pulling a new upstream `dungeon.zil` / `actions.zil` — regenerate with: ```bash uv run python -m moo.zil_import \ ~/path/to/zork1/dungeon.zil \ ~/path/to/zork1/actions.zil \ --output moo/bootstrap/zork1 ``` Then sync the running database: ```bash docker compose run webapp manage.py moo_init --bootstrap zork1 --sync ``` `--sync` overwrites verb source in place via `replace=True`. Object creation is idempotent because the bootstrap uses `bootstrap.get_or_create_object`.