``fantasm.toml`` reference ========================== Per-project configuration for fantasm. ``fantasm.toml`` sits at the project root. fantasm discovers it by walking upwards from the current working directory; the top-level ``--project-root`` option and the ``FANTASM_PROJECT_ROOT`` environment variable both override discovery. Discovery and project context ----------------------------- Three lookup steps, highest priority first: 1. ``--project-root `` on the top-level ``fantasm`` group; 2. The ``FANTASM_PROJECT_ROOT`` environment variable; 3. Walk upwards from the current working directory looking for a ``fantasm.toml``. ``fantasm info`` shows what got resolved. Commands that need a project root fail with ``no project root resolved`` if all three lookups miss; pass ``--project-root`` explicitly or run from inside the project tree to fix that. Top-level structure ------------------- The config is plain TOML. Only sections fantasm understands are listed below; unknown keys are ignored, so your editor or other tooling can scribble its own metadata under any prefix it likes without confusing fantasm. .. code-block:: toml [project] # name, used in messages and as a fallback [binary] # CPU, base address, and per-version binary layout [versions] # directory layout + version-name prefixes [layout] # path templates for projects outside versions/ [[targets]] # explicit per-target paths for any existing layout [verify] # the assembler used by ``fantasm verify`` [memory] # workspace + hardware regions used by every version [[versions.entry]] # one block per version, with the version graph (``[rom]`` is accepted as a back-compatible alias for ``[binary]`` — see below.) ``[project]`` ~~~~~~~~~~~~~ .. code-block:: toml [project] name = "acorn-myrom" default_target = "1.0" # optional * ``name`` *(required)* — used as a fallback for ``[versions] prefixes`` when that option is omitted, and for the project-specific strings the CLI emits in error and help messages. * ``default_target`` — the target per-target commands use when the ``TARGET`` argument is left out. Optional: a project with exactly one target uses that one without being told. See `Targets`_. ``[binary]`` ~~~~~~~~~~~~ The program binary each version disassembles — a sideways ROM image, or any other load-and-run binary. ``cpu`` and ``base_address`` describe how to decode it; the layout keys say where its bytes and metadata live under the version directory. .. code-block:: toml [binary] cpu = "6502" # default; overridden by drivers as needed base_address = 0x8000 # the address the binary is mapped at dir = "rom" # per-version subdirectory (default) extension = "rom" # binary file extension, no dot (default) metadata = "rom.json" # metadata filename (default) filename = "{prefix}-{version_id}" # basename template (default) * ``cpu`` — recognised values, case-insensitive: ``"6502"``, ``"nmos"`` (alias), ``"65c02"``, ``"65sc12"``, ``"65c12"``, ``"cmos"`` (alias). fantasm's audit / cfg / context / compare commands read this default so the right opcode table is used. A driver can pass a different CPU to the disassembler for a specific version (ANFS 4.21 does this — base default of 6502 with the driver overriding to 65c02). * ``base_address`` — defaults to ``0x8000`` (BBC sideways-ROM convention). Override for binaries mapped elsewhere — for example ``0xE000`` for the standalone Acorn Econet Bridge, ``0xF800`` for the BBC Tube Client, or ``0x1900`` for a DFS ``*RUN`` program that loads into main memory. * ``dir`` — the per-version subdirectory holding the binary and its metadata. Default ``"rom"``. * ``extension`` — the binary file's extension, **without** a leading dot. Default ``"rom"``. An empty string (``""``) means the binary has no extension — as for a DFS program named literally ``KEYPAD``. * ``metadata`` — the metadata filename inside ``dir`` (its ``docs`` list is consulted by ``fantasm lint``). Default ``"rom.json"``. * ``filename`` — a template for the binary's **basename** (the ``extension``, if any, is appended afterwards). Default ``"{prefix}-{version_id}"``. Recognised tokens, the same set as ``[versions] driver_filename`` plus case variants: * ``{prefix}`` / ``{prefix_upper}`` / ``{prefix_lower}`` * ``{version_id}`` — the id as written * ``{version_id_no_dots}`` — the id with all ``.`` removed * ``{version_id_upper}`` / ``{version_id_lower}`` Set ``filename = "{version_id_upper}"`` to keep an Acorn DFS-style name — id ``keypad`` renders the on-disk binary ``KEYPAD`` — so an extracted DFS program (and its ``KEYPAD.inf`` sidecar, which the versions/ convention ignores — ``base_address`` applies) stays intact. The ``.asm`` / ``.json`` disassembly outputs always keep the ``{prefix}-{version_id}`` name; only the binary basename follows this template. The binary for version ``V`` (prefix ``P``) resolves to ``versions/P-V//[.]`` and its metadata to ``versions/P-V//``. With the defaults this is the historical ``rom/P-V.rom`` + ``rom/rom.json`` layout. The driver script receives the resolved binary path in the ``FANTASM_BINARY`` environment variable (and, for back-compat, the same value in ``FANTASM_ROM``); see `The driver's environment`_ for the full set. .. note:: ``[rom]`` is still accepted as an alias for ``[binary]`` — a project that predates the neutral vocabulary needs no changes. When both sections are present ``[binary]`` wins (they are not merged), so move all keys together when migrating. A DFS ``*RUN`` program disassembly, for example: .. code-block:: toml [binary] cpu = "6502" base_address = 0x1900 dir = "binary" extension = "" # KEYPAD, not KEYPAD.rom metadata = "binary.json" filename = "{version_id_upper}" # id "keypad" -> binary "KEYPAD" ``[versions]`` ~~~~~~~~~~~~~~ .. code-block:: toml [versions] directory = "versions" # default prefixes = ["anfs", "nfs"] driver_dirname = "disassemble" # default driver_filename = "disasm_{prefix}_{version_id_no_dots}.py" # default * ``directory`` — subdirectory under the project root that holds per-version directories. Default ``"versions"``. * ``prefixes`` — ordered list of acceptable ROM-name prefixes. Version directories are named ``{prefix}-{version_id}/``; resolution tries each prefix in order and returns the first existing directory. NFS uses two; most projects use one. If omitted, ``[project] name`` is used as a single-element fallback. * ``driver_dirname`` — subdirectory under each version directory holding the disassembly driver script. Default ``"disassemble"``. * ``driver_filename`` — filename template for the driver. Tokens: ``{prefix}``, ``{version_id}``, ``{version_id_no_dots}``. The default works when the prefix has no hyphens; projects whose prefix has a hyphen must override (the Econet Bridge's prefix is ``econet-bridge`` but its driver is named ``disasm_econet_bridge_*.py``): .. code-block:: toml [versions] prefixes = ["econet-bridge"] driver_filename = "disasm_econet_bridge_{version_id_no_dots}.py" For NFS version ``3.10`` (prefix ``anfs``), with the defaults, the driver script's path is ``versions/anfs-3.10/disassemble/disasm_anfs_310.py``. Targets ------- A **target** is one thing being disassembled: a program binary loaded at some address, the driver script that disassembles it, the ``.asm`` and ``.json`` the driver writes, and an optional metadata file. Every per-target command — ``disassemble``, ``verify``, ``lint``, ``audit``, ``cfg``, ``comments`` and the rest — takes a ``TARGET`` argument; in a multi-version project each version is a target. The ``TARGET`` argument is optional when the project has exactly one target, or sets ``[project] default_target``:: fantasm verify # the only (or default) target fantasm asm extract &1900 # likewise, with a further argument fantasm verify 1.30 # name one explicitly Leaving it out of a project with several targets and no default fails with a message listing them. Where a target's files live is resolved in three layers. Each overrides the one before it **key by key**, so a project can override just the driver path for one target and keep the convention for everything else: 1. **The versions/ convention** — ``versions/-/`` as described under ``[binary]`` and ``[versions]`` above. A project with neither ``[layout]`` nor ``[[targets]]`` uses only this layer, exactly as it always has. 2. **``[layout]`` templates** — for multi-version projects that keep their files somewhere else. 3. **``[[targets]]`` entries** — explicit paths for one target each, for adopting an existing project as it stands. The set of targets is the ``[[targets]]`` ids, then — when ``[layout]`` is configured — the ``[[versions.entry]]`` ids and any ids found by ``[layout] discover``, then the version directories on disk. ``fantasm project list`` shows them all, with where each one resolved from. ``[[targets]]`` ~~~~~~~~~~~~~~~ One table per target. Paths are relative to the project root: .. code-block:: toml [[targets]] id = "loader" binary = "orig/ELITE" load_address = 0x1900 # optional: see below exec_address = 0x8023 # optional driver = "tools/dis_loader.py" asm = "build/loader.asm" json = "build/loader.json" metadata = "notes/loader.json" # optional driver_cwd = "tools" # optional * ``id`` *(required)* — the name commands use for the target. Letters, digits, ``.``, ``_`` and ``-``. * ``binary``, ``driver``, ``asm``, ``json`` — the files. Any of them may be left out when the target also has a version directory (or ``[layout]`` supplies it); otherwise each is required. * ``metadata`` — optional JSON file whose ``docs`` list ``fantasm lint`` checks for broken ``label:`` / ``glossary:`` links. Relative ``docs`` paths resolve against the project root (or the version directory, for a target that has one). * ``load_address`` / ``exec_address`` — integers (``0x1900``) or hex strings (``"&1900"``, ``"$1900"``, ``"1900"``). When ``load_address`` is absent, fantasm reads the binary's ``.inf`` sidecar (``ELITE.inf`` beside ``ELITE``) if there is one, and otherwise falls back to ``[binary] base_address``. * ``inf`` — ``true`` to require the sidecar (an error if it's missing), ``false`` to ignore it, or a path to a ``.inf`` stored elsewhere. * ``driver_cwd`` — the directory the driver runs in. By default it runs in the directory ``fantasm`` was invoked from; set this when an existing driver uses paths relative to its own location. * ``prefix`` — the ``{prefix}`` used for this target in messages and templates. Defaults to the project's first prefix. * ``verify`` — a table overriding ``[verify]`` keys for this target. * ``images`` — an alternative to ``binary`` / ``load_address`` / ``exec_address`` / ``inf``, as a list of one table with ``path``, ``name`` and those address keys. Only a single image is supported at present; disassemble a multi-file program as one target per file (see :doc:`disc_programs`). Unknown keys are rejected, so a typo such as ``binry`` is reported rather than silently ignored. ``.inf`` files ^^^^^^^^^^^^^^ fantasm reads the many ``.inf`` variants archiving tools have written: the classic DFS form (``$.ELITE FF1900 FF8023 000400 L``), the 8-digit ADFS form with an access byte (``FS3 00001900 00001900 00004C00 33``), quoted names, ``&``/``$``/``0x``-prefixed numbers, ``Locked`` / ``L`` flags and ADFS access strings (``WR/r``), and trailing ``CRC=`` / ``CRC32=`` / ``NEXT=`` extras. High bits of ``&FF``/``&FFFF`` mark an I/O-processor address and are dropped. A ``.inf`` that can't be parsed — or whose address isn't a 6502 address, such as a RISC OS filetype stamp — is reported with a suggestion to give ``load_address`` explicitly. ``[layout]`` ~~~~~~~~~~~~ Path templates rendered for every target, for multi-version projects that don't use ``versions/-/``: .. code-block:: toml [layout] binary = "roms/{id}.rom" driver = "scripts/dis_{id_no_dots}.py" asm = "build/{id}.asm" json = "build/{id}.json" metadata = "docs/{id}.json" # optional discover = "roms/{id}.rom" # optional: find ids on disk [[versions.entry]] id = "1.20" [[versions.entry]] id = "1.30" parents = ["1.20"] Tokens: ``{id}``, ``{id_no_dots}``, ``{id_upper}``, ``{id_lower}``, the ``{version_id}`` family (aliases of the ``{id}`` ones), ``{prefix}``, ``{prefix_upper}`` and ``{prefix_lower}``. * ``discover`` — a path with exactly one ``{id}`` token. Every matching file or directory adds a target. Without it, the targets are the ``[[versions.entry]]`` ids (plus any ``[[targets]]``). * ``load_address``, ``exec_address``, ``inf`` and ``driver_cwd`` may also be given here, applying to every target (``inf`` and ``driver_cwd`` are templates). ``[verify]`` — the round-trip assembler --------------------------------------- ``fantasm verify`` assembles a target's ``.asm`` and compares the result with the original binary. The assembler is beebasm unless this section says otherwise: .. code-block:: toml [verify] command = ["beebasm", "-i", "{asm}", "-o", "{output}"] # default cwd = "{asm_dirpath}" # default output = "{output}" # default * ``command`` — the argument list, or a single string split with shell-style quoting. A bare program name is looked up on ``PATH``; a relative path with a directory part resolves against the project root, so a wrapper script in the repository works (``command = ["tools/assemble.sh", "{asm}", "{output}"]``). * ``cwd`` — the directory the assembler runs in. The default, the listing's own directory, keeps ``INCBIN`` and similar relative directives working. * ``output`` — the file to compare against the binary, relative to ``cwd`` unless absolute. The default ``{output}`` is a fresh temporary file fantasm deletes afterwards. An assembler that always writes a fixed file — for example from a ``SAVE "NAME"`` directive — names that file here instead. Tokens available in all three: ``{asm}`` (absolute path of the listing), ``{asm_dirpath}``, ``{asm_filename}``, ``{output}``, ``{output_dirpath}`` (the temporary directory), ``{binary}`` (absolute path of the original binary) and ``{project_root}``. A ``[[targets]]`` entry's ``verify`` table overrides these keys for that target. The driver's environment ------------------------ ``fantasm disassemble`` runs the target's driver with these environment variables set: .. list-table:: :header-rows: 1 :widths: 30 70 * - Variable - Value * - ``FANTASM_TARGET`` - the target id * - ``FANTASM_BINARY`` - the target's binary (``FANTASM_ROM`` holds the same value, for older drivers) * - ``FANTASM_IMAGES`` - JSON list describing the binary image: ``[{"name", "path", "load_addr", "exec_addr"}]``, with ``load_addr`` resolved as described under ``[[targets]]`` * - ``FANTASM_ASM`` / ``FANTASM_JSON`` - where fantasm expects the generated ``.asm`` / ``.json`` * - ``FANTASM_OUTPUT_DIR`` - the directory holding the ``.json`` A driver is free to ignore them and use its own paths, as long as its outputs land where the configuration says they do — an existing driver needs no changes to be adopted. ``[memory]`` — non-ROM addresses where code and labels can live --------------------------------------------------------------- Two lists at the project level. Both are optional. .. code-block:: toml [memory] regions = [ { start = 0x0016, end = 0x0076 }, # zero-page workspace { start = 0x0D00, end = 0x0FFF }, # NMI workspace ] external_regions = [ { start = 0xFC00, end = 0xFFFF }, # MOS / hardware ] * ``regions`` — RAM ranges where subroutines and labels can live outside the ROM range. The ROM range itself is discovered from each version's JSON metadata, so don't list it here. Reloc-block destinations from a version's ``reloc_blocks`` list are automatically merged into that version's *effective* regions, so don't duplicate them here either. * ``external_regions`` — hardware / OS-mapped addresses that label references can point at, but where no assembly items are emitted. Used by lint and ``comments check`` to allow references to OS routines, hardware registers, etc. ``end`` is **inclusive** in both lists. When ``fantasm`` 0.4.0+ runs ``lint``, the JSON's own ``external_labels`` map is consulted directly, so a workspace address declared via the driver's ``external_label()`` (the same call shape on both dasmos and py8dis) *does not* need a matching ``[memory]`` entry. ``[memory]`` regions still cover addresses that the driver references but doesn't formally declare — typically hardware registers and OS workspace pages where the project doesn't want a name for every byte. A version can override ``regions`` (but not ``external_regions`` — hardware mapping is invariant) by setting ``memory.regions`` in its ``[[versions.entry]]``: .. code-block:: toml [[versions.entry]] id = "page7-variant" parents = ["3.34B"] memory.regions = [ { start = 0x0016, end = 0x0076 }, { start = 0x0700, end = 0x07FF }, # this variant uses page 7 ] Override is **complete replacement**, not merge — explicit and non-magical. Reloc-destination regions still auto-add even when ``memory.regions`` is overridden. ``[[versions.entry]]`` — the version graph ------------------------------------------ Each entry is a node in the version graph. Edges are declared by the ``parents`` list (parent → child, older → newer). A version with no parents is a root or an as-yet-unplaced node; both are valid. For the cookbook on what the graph buys you (path-finding, reloc matching, annotation propagation), see :doc:`version_graph`. Minimal entry ~~~~~~~~~~~~~ .. code-block:: toml [[versions.entry]] id = "1.0" That's enough to make commands like ``audit`` / ``cfg`` / ``comments check`` use the project's ``[memory]`` regions for this version. Without an entry, those commands fall back to the ROM range alone. With reloc blocks ~~~~~~~~~~~~~~~~~ NFS-style firmware copies blocks of code from ROM into RAM at runtime, then jumps there. Each ``move()`` block in the driver gets a matching entry in ``reloc_blocks``: .. code-block:: toml [[versions.entry]] id = "3.34" reloc_blocks = [ { source = 0x9307, dest = 0x0016, length = 0x61 }, { source = 0x934C, dest = 0x0400, length = 0x100 }, { source = 0x944C, dest = 0x0500, length = 0x100 }, { source = 0x954C, dest = 0x0600, length = 0x100 }, ] Per block: * ``source`` — the ROM address where the bytes live in *this* version; * ``dest`` — the runtime address where they execute; * ``length`` — block length in bytes. Across versions, ``dest`` and ``length`` are usually stable (driven by the project's runtime memory map); ``source`` shifts as code is added or removed elsewhere in the ROM. Across an edge, ``compose_chained_map`` matches reloc blocks by ``(dest, length)``; see :doc:`version_graph` for the consequences. ``reloc_blocks`` may be empty or omitted for versions with no ``move()`` blocks — the schema accommodates simple firmware with no relocation as well as the NFS-style architecture. With parents ~~~~~~~~~~~~ Most non-root versions descend from a previous one: .. code-block:: toml [[versions.entry]] id = "3.34B" parents = ["3.34"] reloc_blocks = [ # ... 3.34B's source addresses for the same dest pages ] A version may declare multiple parents — fantasm handles merge nodes: .. code-block:: toml [[versions.entry]] id = "3.40" parents = ["3.35K", "3.34B-japan"] Free-form metadata ~~~~~~~~~~~~~~~~~~ Each ``[[versions.entry]]`` may carry the following fields. None of them affect analysis — they're surfaced for tooling and documentation. .. code-block:: toml [[versions.entry]] id = "3.34" description = "First public release; zero-based workspace layout" release_date = "1984-01-15" # ISO-8601 string, not parsed source = "Acorn Cambridge archive disk #ANFS-001" notes = "Reverse-engineered from binary; no source survives" ``[comments.suggest]`` — label hints for ``fantasm comments suggest`` --------------------------------------------------------------------- Pattern → description map for project-specific comment hints. ``fantasm comments suggest`` mixes these in with the generic 6502 instruction-pattern heuristics so workspace-label references get project-meaningful suggestions. .. code-block:: toml [comments.suggest.label_hints] wksp_drive = "current drive" wksp_filename = "filename buffer" tube_r1 = "Tube FIFO 1 (data)" Patterns are substring-matched against item labels. Per-invocation hints can be added with ``--label-hint PATTERN=description`` on the command. Per-module summary ------------------ Which modules look at which configuration, for a given version ``V``: .. list-table:: :header-rows: 1 :widths: 22 78 * - Module - Reads * - ``audit`` - ``effective_regions(V)`` ∪ ROM-range-of(V) * - ``comment_check`` - ``effective_regions(V)`` ∪ ``effective_external_regions(V)`` ∪ ROM * - ``lint`` - JSON's ``items`` / ``external_labels`` / ``sub_labels`` / ``subroutines`` ∪ ROM ∪ ``effective_regions(V)`` ∪ ``effective_external_regions(V)`` * - ``backfill`` / ``annotations diff`` - ``effective_regions(V_source) ∩ effective_regions(V_target)`` for identity mapping; per-edge reloc tuples derived by matching ``(dest, length)`` across endpoints * - ``cfg`` / ``compare`` / ``fingerprint`` / ``shared`` - only ROM bytes (no version-graph involvement) ``effective_regions(V)`` = (``memory.regions`` from V's override, or the project default) ∪ ``{dest..dest+length-1 for each reloc in V's reloc_blocks}``, with overlapping ranges merged. ``effective_external_regions(V)`` = the project's ``memory.external_regions`` (no per-version override). Example: a complete config -------------------------- A single-version project with a custom driver-filename template, declared workspace, and one ``[[versions.entry]]``: .. code-block:: toml [project] name = "acorn-econet-bridge" [binary] cpu = "6502" base_address = 0xE000 # standalone 6502, ROM at &E000-&FFFF [versions] prefixes = ["econet-bridge"] driver_filename = "disasm_econet_bridge_{version_id_no_dots}.py" [memory] regions = [ { start = 0x0000, end = 0x00FF }, # zero page (workspace) { start = 0x0200, end = 0x04FF }, # workspace tables ] external_regions = [ { start = 0xC000, end = 0xCFFF }, # ADLC A { start = 0xD000, end = 0xDFFF }, # ADLC B ] [[versions.entry]] id = "variant_1" A multi-version project with the version graph populated — see the NFS migration commit on `acorn-nfs`_ for the live example covering eleven versions across two prefixes with full ``reloc_blocks``. .. _acorn-nfs: https://github.com/acornaeology/acorn-nfs