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 Why the ZIL Importer Exists.

Pipeline

The package is a four-stage pipeline; each stage is independently testable and exposes a small public surface:

*.zil  ──► parser ──► converter ──► translator ──► generator ──► moo/bootstrap/<name>/
         (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

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 (<INSERT-FILE …>) are expanded recursively.

--game-config

zork1

Per-game configuration slug; choices are zork1 and hhg (extend via game_config.GAME_CONFIGS).

--output

moo/bootstrap/<slug>

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.

class moo.zil_import.ir.ZilRoom(atom, desc, ldesc, fdesc, exits=<factory>, flags=<factory>, globals=<factory>, action=None, value=0, pseudo=<factory>)

One ZIL <ROOM …> form: title, descriptions, exits, flags, ACTION.

class moo.zil_import.ir.ZilObject(atom, location, synonyms=<factory>, adjectives=<factory>, desc=None, ldesc=None, fdesc=None, text=None, flags=<factory>, action=None, capacity=0, size=5, value=0, tvalue=0, vtype=None)

One ZIL <OBJECT …> form: location, synonyms, descriptions, flags, ACTION.

class moo.zil_import.ir.ZilExit(direction, dest, message, condition, else_message, per_routine)

One direction’s exit record from a ZIL ROOM property.

class moo.zil_import.ir.ZilRoutine(name, params, aux_vars, body, raw_zil, initial_values=<factory>)

One ZIL <ROUTINE …> form: name, params, aux locals, body AST.

class moo.zil_import.ir.ZilTable(name, values)

One ZIL <GLOBAL name <TABLE …>> or <LTABLE …> value list.

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

moo.zil_import.parser.tokenize(source)

Tokenize ZIL source text.

Parameters:

source (str) – Raw ZIL source.

Return type:

list[Token]

Returns:

List of Token instances (whitespace dropped).

moo.zil_import.parser.parse(tokens)

Parse a flat token list into a list of top-level nodes.

Parameters:

tokens (list[Token]) – Token stream from tokenize().

Return type:

list[Union[str, int, None, list, tuple]]

Returns:

Top-level AST nodes (lists, tuples, atoms, ints, None).

Raises:

ParseError – On unmatched <> / () brackets.

moo.zil_import.parser.parse_file(path)

Parse a ZIL source file.

Parameters:

path (str) – Filesystem path to the ZIL source.

Return type:

tuple[list[Union[str, int, None, list, tuple]], str]

Returns:

(nodes, source_text) where nodes is the AST and source_text is the raw file contents.

class moo.zil_import.parser.Str

A ZIL string literal — distinct from bare atoms at the type level.

See ZIL Importer Reference for the rationale.

class moo.zil_import.parser.Token(kind, value, line, offset=0)

One lexed ZIL token: kind, raw text, source line, and byte offset.

class moo.zil_import.parser.ParseError

Raised on a malformed ZIL source — unmatched <> / () brackets or an unknown token.

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

moo.zil_import.converter.extract_all(nodes)

Walk parsed ZIL AST and extract every IR collection.

compound_verb_dict maps (verb, particle) → V-routine for forms like <SYNTAX TURN OFF OBJECT = V-LAMP-OFF> so the generator can emit particle-aware dispatchers.

Parameters:

nodes (list) – Top-level AST nodes from moo.zil_import.parser.parse().

Return type:

tuple[dict[str, ZilRoom], dict[str, ZilObject], dict[str, ZilRoutine], dict[str, ZilTable], dict[str, object], dict[str, list[tuple[int, str]]], dict[str, list[str]], dict[tuple[str, str], str], dict[str, list[tuple[int, str]]]]

Returns:

9-tuple of (rooms, objects, routines, tables, globals_dict, syntax_dict, synonyms_dict, compound_verb_dict, bare_syntax_dict).

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, <COMPILATION-FLAG …>, 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 <TABLE> and <LTABLE> 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

class moo.zil_import.translator.ZilTranslator(routine, object_atoms=None, routine_atoms=None, ambiguous_object_atoms=None, action_owner=None, owner_overrides=None, pre_handler_routines=None, display_names=None, substrate_display_names=None, routine_to_verbs=None, strictly_zero_object=None, allows_bare_invocation=None, lint_active=False, game_config=None)

Translate a single ZilRoutine body into Python verb source.

See ZIL Importer Reference (ZilTranslator state) for the full meaning of each constructor argument.

Parameters:
  • routine (ZilRoutine) – ZIL routine IR to translate.

  • object_atoms (set[str] | None) – Atoms naming a Room/Object — drive lookup("name") emission for atom refs.

  • routine_atoms (set[str] | None) – Atoms naming another ZIL routine — drive zero-arg dispatch on bare atoms in expression position.

  • action_owner (tuple[str, bool] | None) – (atom, is_room) of the owning room/object, or None for global helpers.

  • owner_overrides (dict[str, str] | None) – Per-routine --on $<owner> shebang overrides (uppercase routine name → owner property).

  • pre_handler_routines (set[str] | None) – V-routine names whose PRE-X handler should be inlined at the top of the substrate body.

  • display_names (dict[str, str] | None) – Atom → globally-unique display name.

  • substrate_display_names (dict[str, str] | None) – Substrate snake-name → display name.

  • routine_to_verbs (dict[str, list[str]] | None) – V-routine name → list of player verbs that dispatch to it via SYNTAX rules.

  • strictly_zero_object (set[str] | None) – V-routine names whose only SYNTAX form is 0-OBJECT (emit no --dspec).

  • lint_active (bool) – When set, emit only format-intrinsic pylint disables in the verb-file header.

  • game_config (GameConfig | None) – Per-game configuration; defaults to ZORK1_CONFIG.

f_constants_found()

Return the list of F-* constants handled by this routine.

Return type:

list[str]

Returns:

F-* constants in clause order, or [].

has_f_dispatch()

Return True if this routine dispatches on F-* constants via COND/MODE.

Return type:

bool

Returns:

True when an F-* dispatch COND is present.

has_m_dispatch()

Return True if this routine dispatches on M-* constants via COND/RARG.

Return type:

bool

Returns:

True when an M-* dispatch COND is present.

has_verb_dispatch()

True if the routine has a per-clause-splittable VERB? COND.

Only action-owner routines get split; global helpers don’t.

Return type:

bool

Returns:

True when the routine is splittable.

m_constants_found()

Return the list of M-* constants handled by this routine.

Return type:

list[str]

Returns:

M-* constants in clause order, or [].

translate()

Return the full verb-file body, or empty when the residual is a no-op.

M-/F-/VERB? clauses that get split into per-clause files are pruned here so the residual god-verb carries only the catch-all body.

Return type:

str

Returns:

The complete verb-file source, or "" when the generator should skip emission.

translate_combined_clauses(skip_constants=None)

Emit one verb file that handles every M-/F-clause this routine dispatches on.

The body is a single if rarg == "M-X": ... elif rarg == "M-Y": ... ladder mirroring ZIL’s <COND (<EQUAL? .RARG ,M-X> …) (<EQUAL? .RARG ,M-Y> …)>. All M-clause role names (preturnfunc for M-BEG, turnfunc for M-END, …) plus the F-clause roles plus the routine atom itself end up in the shebang as aliases so the parser / do_command invocations that name a specific role still resolve.

Returns "" when every clause translates to a no-op (the substrate dispatch path handles the routine without per-clause overrides).

Return type:

str

Returns:

Complete verb-file source, or "".

translate_f_clause(f_constant)

Translate one F-* combat branch into a verb-file body.

Mirrors translate_m_clause but keyed on .MODE / F-...; the emitted file’s verb is taken from M_TO_VERB (F-DEADf_dead) and dispatches on the routine’s action_owner (the villain).

Parameters:

f_constant (str) – The F-* constant whose clause to extract (e.g. "F-DEAD").

Return type:

str

Returns:

The clause body as a complete verb-file source, or "" when the body is a no-op.

translate_m_clause(m_constant)

Translate one M-* branch into a verb-file body.

Parameters:

m_constant (str) – The M-* constant whose clause to extract (e.g. "M-BEG").

Return type:

str

Returns:

The clause body as a complete verb-file source, or "" when the clause is a no-op (<> / RFALSE / RTRUE) so the substrate verb runs via normal parser dispatch.

translate_object_function_combined()

Emit one combined-callback file for an OBJECT-FUNCTION’s VERB? dispatch.

Phase 7 collapse: instead of emitting one --on "<obj_name>" file per VERB? clause (rusty_knife/attack.py, rusty_knife/take.py, …), emit a single <routine_atom>.py whose body is an if verb_name == "attack": ... elif verb_name == "take": ... ladder. Invoked exclusively via dispatch_object_function(obj, verb_name, prep, iobj) which looks up obj.action and calls obj.invoke_verb(action_atom, …); the file is parser-inert (--dspec none).

Replaces the per-VERB? split files for OBJECT action_owners. Pure ROOM-FUNCTION routines continue through translate_combined_clauses().

Returns "" for action_owner=None / room owners / verb-less routines / all-noop bodies (the caller should fall back to the legacy emission path or skip emission entirely).

Return type:

str

translate_verb_clause(verb_atoms, extra_test, body_forms)

Translate one VERB? clause as a complete verb file body.

Parameters:
  • verb_atoms (list[str]) – ZIL verb atoms the clause dispatches on.

  • extra_test – Optional extra test from an enclosing AND, or None.

  • body_forms (list) – Body forms of the clause.

Return type:

str

Returns:

Verb-file source, or "" for a no-op body.

verb_clauses_for_split()

Yield (verb_atoms, extra_test, body_forms) for each splittable clause.

Return type:

list[tuple[list[str], list, list]]

Returns:

A list of clause tuples.

moo.zil_import.translator.translate_routine(routine, *, game_config=None)

Translate a ZilRoutine to a complete verb-file string.

Parameters:
  • routine (ZilRoutine) – The ZIL routine IR to translate.

  • game_config (GameConfig | None) – Optional per-game configuration; defaults to ZORK1_CONFIG.

Return type:

str

Returns:

Complete verb-file source.

moo.zil_import.translator.translate_m_clause(routine, m_constant, *, game_config=None)

Translate one M-* clause from an ACTION routine.

Parameters:
  • routine (ZilRoutine) – The ZIL routine IR to translate.

  • m_constant (str) – The M-* constant whose clause to extract.

  • game_config (GameConfig | None) – Optional per-game configuration; defaults to ZORK1_CONFIG.

Return type:

str

Returns:

Verb-file source for the clause, or "" for a no-op.

moo.zil_import.translator.translate_f_clause(routine, f_constant, *, game_config=None)

Translate one F-* combat clause from a per-villain ACTION routine.

Parameters:
  • routine (ZilRoutine) – The ZIL routine IR to translate.

  • f_constant (str) – The F-* constant whose clause to extract.

  • game_config (GameConfig | None) – Optional per-game configuration; defaults to ZORK1_CONFIG.

Return type:

str

Returns:

Verb-file source for the clause, or "" for a no-op.

moo.zil_import.translator.has_m_dispatch(routine)

Return True if routine dispatches on M-* constants.

Parameters:

routine (ZilRoutine) – The ZIL routine IR.

Return type:

bool

Returns:

True when an M-* dispatch COND is present.

moo.zil_import.translator.has_f_dispatch(routine)

Return True if routine dispatches on F-* combat constants.

Parameters:

routine (ZilRoutine) – The ZIL routine IR.

Return type:

bool

Returns:

True when an F-* dispatch COND is present.

moo.zil_import.translator.sanitize_ident(name)

Convert a ZIL atom into a valid Python identifier.

See ZIL Importer Reference (Identifier sanitization) for the full rule set.

Parameters:

name (str) – The ZIL atom to sanitize.

Return type:

str

Returns:

A valid, non-shadowing Python identifier.

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. ,THIEFlookup("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

<TELL "msg" CR>

print("msg")

<CRLF>

print()

<PRINTD ,OBJ>

print(lookup("obj").desc(), end='')

<MOVE ,LAMP ,HERE>

lookup("lamp").moveto(context.player.here())

<REMOVE ,OBJ>

_.remove(lookup("obj"))

<FSET ,X ,FLAG> / <FCLEAR …>

lookup("x").set_flag("flag", True) / False

<PUTP ,OBJ ,PROP val>

lookup("obj").set_property("prop", val)

<SETG KEY val>

context.player.zstate_set('KEY', val)

<COND (cond body) (T body)>

if cond: body / else: body (true if/elif/else chain)

<AND a b> (statement context)

if a: b (nested if-chain to avoid expression-as-stmt)

<SET X expr> (expression)

(x := expr) (walrus)

<0? .X> / <1? .X>

x == 0 / x == 1

<==? a b> / <EQUAL? a b c>

a == b / a in (b, c)

Bare ,FOO in expression

GLOBAL_MAP['FOO'] if known, else context.player.zstate_get('FOO')

<LIT? ,HERE> (predicate routine)

is_lit(context.player.here())

<ENABLE <QUEUE r 5>>

_.queue("r", 5)

<JIGS-UP "msg">

_.jigs_up("msg")

<SCORE n>

_.score_update(n)

<PICK-ONE ,TABLE>

_.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 "<display>" 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_configGameConfig instance (defaults to ZORK1_CONFIG) whose npc_atom_map extends GLOBAL_MAP per-game.

Game configuration

class moo.zil_import.game_config.GameConfig(name, dataset_name, banner, manifest_files, license_blurb, npc_atom_map=<factory>, zork_number=1, exit_condition_overrides=<factory>, player_avatar_atoms=frozenset({'ADVENTURER', 'ME', 'PLAYER', 'WINNER'}), reset_body_filename='_reset_state_body.py', synonym_expansions=<factory>, adjective_expansions=<factory>, verb_atom_expansions=<factory>, avatar_aliases=(), skip_routines=frozenset({}))

Per-game knobs the translator and generator read at runtime.

Variables:
  • name – Human-readable game name (used in docstrings and banners).

  • dataset_name – Bootstrap dataset key — also the directory name under moo/bootstrap/ and the value passed to bootstrap.initialize_dataset(...) and pytest.mark.parametrize("t_init", [...]).

  • banner – Multi-line banner emitted by the bootstrap loader. {rooms} and {objects} placeholders are filled in by the generator.

  • manifest_files – Names of the canonical ZIL source files (used in the bootstrap docstring).

  • license_blurb – Licensing/credit paragraph for the bootstrap module docstring.

  • npc_atom_map – ZIL NPC atoms → DjangoMOO object names. The translator merges this into its global atom→Python-expr map so ,THIEF / ,TROLL etc. compile to the right lookup("...") call.

  • zork_number – Numeric ZORK-NUMBER constant used by Infocom titles to gate text variants in shared source files. Only meaningful when translating an Infocom game with the %<COND ... ZORK-NUMBER ...> macro; defaults to 1 for Zork 1. Other titles set their own value (Zork II → 2, Zork III → 3); games without the macro can ignore it.

  • exit_condition_overrides – Game-specific exit guards keyed on (room_atom, direction). The canonical ZIL emits room ACTION routines that block movement on flags (TROLL-MELEE blocks all four exits while TROLL-FLAG is set); the auto-translator only catches per-direction CEXIT/FEXIT guards. This map lets a game force a condition_flag + nogo_msg on the generated exit when the ACTION-based guard would have caught it.

  • player_avatar_atoms – ZIL ACTORBIT atoms that name the player-character placeholder rather than a real NPC (ME, ADVENTURER for Zork; ARTHUR, FORD, TRILLIAN, ZAPHOD for HHG). These keep Actor as their parent so they don’t get an anonymous Player row.

  • reset_body_filename – Filename (under moo/zil_import/scripts/) whose contents become the generated 099_reset_state.py. Defaults to Zork’s _reset_state_body.py for back-compat; HHG overrides to its own _hhg_reset_state_body.py.

  • synonym_expansions – Per-game alias-expansion map. ZIL truncates dictionary entries to 6 chars (ASPIRI stands for aspirin, ANALGE for analgesic). The generator adds the value as an extra alias on any object whose synonym list contains the key, so player-typed full words still parse. Keys are uppercase truncated ZIL atoms; values are full-word lowercase aliases.

  • adjective_expansions – Same shape as synonym_expansions but applied to ADJECTIVE atoms. ZIL truncates adjective atoms the same way (DEMOLI for demolition, OFFICI for official). Used by the generator when emitting cross-product <adj> <syn> multi-word aliases so take junk mail resolves.

  • verb_atom_expansions – Same shape applied to verb atoms in syntax_dict. ZIL truncates verb atoms too (INVENT for inventory); the generator pulls the expanded form into the emitted dispatcher’s alias list so players can type the full word.

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. ASPIRIaspirin).

  • 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 <slug> or pass it directly to generate_all / ZilTranslator.

Generator

moo.zil_import.generator.generate_all(rooms, objects, routines, output_dir, *, ir=None, options=None)

Generate the full moo/bootstrap/<dataset>/ tree.

The ZIL IR dicts (tables, globals, syntax rules, synonyms, compound verbs, bare-form syntax) are bundled into GeneratorIR; the optional linter and per-game knobs into GeneratorOptions.

Parameters:
  • rooms (dict[str, ZilRoom]) – All ZIL rooms keyed by atom.

  • objects (dict[str, ZilObject]) – All ZIL objects keyed by atom.

  • routines (dict[str, ZilRoutine]) – All ZIL routines keyed by name.

  • output_dir (Path) – Target directory (created if missing).

  • ir (GeneratorIR | None) – ZIL IR bundle (tables / globals / syntax / synonyms / compound-verb / bare-syntax dicts). Defaults to an empty GeneratorIR.

  • options (GeneratorOptions | None) – Linter and per-game knobs. Defaults to a fresh GeneratorOptions with ZORK1_CONFIG.

Return type:

None

class moo.zil_import.generator.GeneratorIR(tables=<factory>, globals_dict=<factory>, syntax_dict=<factory>, synonyms_dict=<factory>, compound_verb_dict=<factory>, bare_syntax_dict=None, rules=<factory>)

Optional intermediate-representation dicts produced by the converter.

Each is keyed by a ZIL atom; values describe tables, scalar globals, SYNTAX rules, SYNONYM aliases, compound-verb particles, and bare-form syntax rules. Absent keys default to empty dicts so the generator can run on partial IR.

Variables:
  • tables – Atom → ZIL table values.

  • globals_dict – Atom → scalar GLOBAL initial value.

  • syntax_dict – Verb → list of (arity, V-routine) tuples.

  • synonyms_dict – Verb → synonym list.

  • compound_verb_dict(verb, particle) → V-routine.

  • bare_syntax_dict – Bare-form syntax rules; None means “use syntax_dict”.

  • rules – Verb → typed ZilSyntaxRule list. The per-syntax-row emitter consumes this; legacy dispatcher emission reads the dict views above.

class moo.zil_import.generator.GeneratorOptions(linter=None, game_config=None)

Per-run options for generate_all.

Variables:
  • linter – Optional moo.zil_import.lint.Linter. When set, the generator runs per-file pylint and raises on threshold breach.

  • game_config – Per-game knobs (banner, dataset name, NPC atom map). Defaults to ZORK1_CONFIG when None.

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

<output>/__init__.py

empty (so test discovery doesn’t run the bootstrap)

<output>/bootstrap.py

orchestrator that loads the numbered scripts

<output>/010_classes.py

rendered template — root-class hierarchy + Wizard parent

<output>/013_globals.py

ZIL <GLOBAL> / <SETG> / <CONSTANT> scalar initial values

<output>/020_rooms.py

one stanza per ZilRoom

<output>/030_objects.py

one stanza per ZilObject, parented by its CONTBIT/etc.

<output>/035_tables.py

ZIL <TABLE> / <LTABLE> data attached to the System Object

<output>/040_exits.py

per-room exit lists

<output>/050_daemons.py

seed/reset records for realtime-scheduled daemons

<output>/098_adventurer.py

non-wizard player avatar plus Player record migration

<output>/099_reset_state.py

canonical world-state reset (per-game body via GameConfig.reset_body_filename)

<output>/coverage.json

RegenAudit decision-point report; ratcheted by test_translator_coverage.py

<output>/verbs/<owner>/<topic>/*.py

one file per translated routine; owner is the action_owner

<output>/verbs/<owner>/<routine_atom>.py

combined M-/F-clause emission; aliases every clause-role name in its shebang

<output>/verbs/syntax_rows/<verb>[_<particle>][_<iobj_prep>].py

one parser-entry runner per ZilSyntaxRule cell (gated by MIGRATED_VERBS)

<output>/verbs/thing/v_routines/v_<routine>.py

passive V-routine helper invoked programmatically from the syntax-row runner

<output>/tests/conftest.py

zork_world fixture wrapping t_init

<output>/tests/test_rooms.py

room sanity assertions

<output>/tests/test_objects.py

object sanity assertions, ambiguous-name aware

<output>/tests/test_exits.py

structural exit assertions

<output>/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 (<verb>__m_<event>.py) is no longer used.

Migration gate

moo.zil_import.migration.MIGRATED_VERBS: set[str]

Verb atoms whose dispatch lands on the new syntax-row emitter. Phase 4 starts with single-rule verbs (no compound particles, no prep-iobj variants). Multi-rule verbs from the originally-planned Batch A (take, drop, look, examine, read, open) require either per-particle parser disambiguation or in-body dispatch logic and land in later phases.

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

class moo.zil_import.audit.RegenAudit(game, routines=<factory>)

Accumulator for one regen. The generator instantiates one per game, calls the add_* / record_* methods at decision points, and serialises it to coverage.json after the routines loop.

add_drop(name, kind, **details)

Record one drop on the named routine.

details becomes the JSON entry alongside kind. Convention: include enough identifying info that the drop is human-actionable (constant name, verb atoms, source-form snippet).

Parameters:
  • name (str) – ZIL routine name.

  • kind (str) – Drop category ("m_clause", "verb_clause", "syntax_rule", "unhandled_form", …).

  • details (Any) – Free-form identifying fields.

Return type:

None

record_emitted(name, *, action_owner=None, files=None)

Mark a routine as successfully emitted.

Parameters:
  • name (str) – ZIL routine name.

  • action_owner (tuple[str, bool] | None) – (atom, is_room) of the owning room or object, or None for substrate routines.

  • files (list[str] | None) – Verb-file paths the emission produced.

Return type:

None

record_f_clause(name, constant, mode)

Record how an F-* combat clause was emitted.

Parameters:
  • name (str) – ZIL routine name.

  • constant (str) – F-* constant (e.g. "F-DEAD").

  • mode (str) – "combined" or "per_clause".

Return type:

None

record_file(name, file_path)

Attach an emitted verb-file path to a routine record.

Parameters:
  • name (str) – ZIL routine name.

  • file_path (str) – Relative path under the bootstrap output dir.

Return type:

None

record_m_clause(name, constant, mode)

Record how an M-* clause was emitted.

Parameters:
  • name (str) – ZIL routine name.

  • constant (str) – M-* constant (e.g. "M-BEG").

  • mode (str) – "combined" (combined emission) or "per_clause" (legacy per-clause file).

Return type:

None

record_skipped(name, reason)

Mark a routine as deliberately skipped (e.g. in _SKIP_ROUTINES).

Parameters:
  • name (str) – ZIL routine name.

  • reason (str) – Short tag explaining why (e.g. "_SKIP_ROUTINES").

Return type:

None

to_dict()

Render the accumulated audit as the coverage.json payload.

Return type:

dict[str, Any]

Returns:

A dict with game, generated_at, a summary block, and the per-routine routines map.

write(output_dir)

Write coverage.json into the bootstrap output dir.

Parameters:

output_dir (Path) – Bootstrap output directory.

Return type:

Path

Returns:

Path to the written file.

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

moo.zil_import.snapshot.capture_snapshot(site, repo, snapshot_path)

Capture every Object on site to snapshot_path as JSON.

Parameters:
  • site – Active django.contrib.sites.models.Site. Used to scope the Object query and recorded in the file header.

  • repo (str) – Dataset name ("zork1", "hhg") — recorded so a restore can warn on dataset mismatch.

  • snapshot_path (Path) – Destination JSON file. Parent dir is created on demand.

Return type:

Path

Returns:

Path to the written snapshot file.

moo.zil_import.snapshot.restore_snapshot(snapshot_path, site)

Restore an Object snapshot onto site.

Re-asserts site_pk and site_domain before any write. The file’s recorded site MUST match the site argument’s actual values — otherwise raises SnapshotSiteMismatch and aborts.

Parameters:
  • snapshot_path (Path) – JSON file written by capture_snapshot().

  • site – Active django.contrib.sites.models.Site.

Return type:

None

exception moo.zil_import.snapshot.SnapshotSiteMismatch

Snapshot’s recorded site doesn’t match the site argument.

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

moo.zil_import.verb_metadata.parse_verb_file(path)

Parse a verb file’s shebang line.

Parameters:

path (Path) – Verb file path. Read once; first line scanned for #!moo verb and the rest returned as the body.

Return type:

tuple[VerbShebang, str] | None

Returns:

(shebang, body) tuple, or None if the file has no shebang.

moo.zil_import.verb_metadata.iter_verb_files(bootstrap_root)

Walk every .py under <bootstrap_root>/verbs/ and yield the ones with a valid shebang.

Parameters:

bootstrap_root (Path) – Directory containing verbs/ (e.g. moo/bootstrap/zork1).

Return type:

Iterator[tuple[Path, VerbShebang, str]]

class moo.zil_import.verb_metadata.VerbShebang(names, on, dspec, ispec)

Parsed #!moo verb line.

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/<owner-slug>/. 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:

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:

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.