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 <DIR> 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.

[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]

[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.

[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/<dir>/<filename>[.<extension>] and its metadata to versions/P-V/<dir>/<metadata>. 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:

[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]

[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):

    [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/<prefix>-<id>/ 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:

[[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 Disassembling a disc-loaded program).

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/<prefix>-<id>/:

[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:

[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:

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.

[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]]:

[[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 The version graph.

Minimal entry

[[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:

[[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 The 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:

[[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:

[[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.

[[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.

[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:

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]]:

[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.