Getting started
This page walks through bringing a brand-new ROM project under
fantasm. To adopt an existing project as it stands — or to
disassemble a disc-loaded program rather than a ROM — see
Disassembling a disc-loaded program.
By the end you will have a working fantasm.toml, a single ROM
version registered, a disassembly driver script that produces
.asm and .json artefacts, and a CI-ready
disassemble → lint → verify pipeline.
What fantasm does — and doesn’t
fantasm operates on the output of a per-version
disassembly driver — a Python script that turns ROM bytes plus an
annotation script into a .asm listing and a .json index.
The driver typically uses one of:
dasmos — the current recommendation. A pluggable tracing disassembler with a stable 1.0 API, byte-faithful round-trip guarantees, and bundled BBC-Micro / 6502 environments.
py8dis — the predecessor library. Existing py8dis drivers continue to work end-to-end with fantasm; new projects should reach for dasmos.
fantasm itself is disassembler-agnostic — it runs the driver as a subprocess and reads the artefacts. The same fantasm install handles dasmos and py8dis projects side by side, and a project can migrate from one to the other without touching its fantasm configuration.
fantasm does not perform the disassembly itself; instead it provides:
a CLI for orchestrating per-version disassembly runs (
fantasm disassemble VID);round-trip verification against beebasm (
fantasm verify VID);annotation lint, basic-block / call-graph analysis, address mapping across versions, and annotation propagation along a multi-version DAG.
Both the chosen disassembler library and beebasm (the cross-assembler) are workflow prerequisites: they need to be available in your environment but fantasm doesn’t ship them. Both are documented under installation prerequisites below.
Installing fantasm
fantasm is on PyPI and intended to be added as a normal dependency to
your project’s pyproject.toml:
# in your project's pyproject.toml
[project]
dependencies = [
"fantasm>=0.4.0",
# Pick whichever disassembler your driver uses. dasmos is the
# current recommendation; py8dis still works for projects that
# haven't migrated yet.
"dasmos>=2.0",
# ...or, for a py8dis-based driver:
# "py8dis @ git+https://github.com/acornaeology/py8dis.git",
]
After uv sync the fantasm console script is on PATH and
runs as uv run fantasm <command>. Standalone installs work too:
pip install fantasm
fantasm --help
You also need beebasm on PATH. Build it from the upstream
sources, or install via your
distribution if available.
Initialising a project
In a new repo, run:
uv run fantasm project init --name acorn-myrom --prefix myrom
This writes a minimal fantasm.toml at the project root and
ensures a versions/ directory exists. The --prefix option
declares the leading component of each version directory’s name —
e.g. versions/myrom-1.0/. Multi-prefix projects (NFS uses two:
anfs and nfs) repeat the option:
uv run fantasm project init --name acorn-nfs --prefix anfs --prefix nfs
The generated fantasm.toml covers the bare minimum. Edit it to
add [binary] base_address, [memory] regions, and per-version
[[versions.entry]] entries — see fantasm.toml reference.
Adding the first version
Each ROM image lives in its own directory under versions/:
uv run fantasm project add 1.0
That creates versions/myrom-1.0/ with three subdirectories:
versions/myrom-1.0/
rom/ # drop the original ROM bytes here as myrom-1.0.rom
disassemble/ # the disassembly driver script will live here
output/ # generated .asm and .json land here
Drop the ROM file into rom/ (the filename must match
{prefix}-{version_id}.rom).
Writing a disassembly driver
The driver is a Python script that calls into the disassembler
library of your choice to declare the ROM’s load address, entry
points, labels, comments, subroutine boundaries, and any relocated
code blocks. The conventional path is
versions/{prefix}-{version_id}/disassemble/disasm_{prefix}_{version_id_no_dots}.py;
fantasm looks there automatically.
A minimal dasmos driver (the current recommendation):
"""Disassembly driver for myrom 1.0."""
import os
from pathlib import Path
import dasmos
# `fantasm disassemble VID` sets these env vars; direct python
# invocations fall back to the conventional layout.
_script_dirpath = Path(__file__).resolve().parent
_version_dirpath = _script_dirpath.parent
_rom_filepath = os.environ.get(
"FANTASM_ROM",
str(_version_dirpath / "rom" / "myrom-1.0.rom"),
)
_output_dirpath = Path(os.environ.get(
"FANTASM_OUTPUT_DIR",
str(_version_dirpath / "output"),
))
d = dasmos.Disassembler.create(cpu="6502")
d.load(_rom_filepath, 0x8000)
d.entry(0x8000, name="reset")
# ... declare entry points, labels, comments, subroutine() calls ...
ir = d.disassemble()
_output_dirpath.mkdir(parents=True, exist_ok=True)
(_output_dirpath / "myrom-1.0.asm").write_text(
str(ir.render("beebasm")), encoding="utf-8",
)
(_output_dirpath / "myrom-1.0.json").write_text(
str(ir.render("json")), encoding="utf-8",
)
The exact dasmos DSL is beyond the scope of fantasm’s docs (see the dasmos project for the full driver-API reference), but the sibling repos linked from the front page have working drivers you can copy.
py8dis drivers also work — fantasm runs whichever driver script
the version directory contains. The
py8dis project has
its own DSL reference; py8dis’s scripts/py8dis2dasmos.py
porter (shipped with dasmos) translates a py8dis driver to the
dasmos shape automatically when you’re ready to migrate.
Running the pipeline
With the driver in place:
# 1. Run the disassembly driver to generate .asm and .json.
uv run fantasm disassemble 1.0
# 2. Validate every comment / label / subroutine address in the
# driver matches an address fantasm knows about. Zero output
# means every annotation lined up.
uv run fantasm lint 1.0
# 3. Reassemble the .asm with beebasm and byte-compare to the
# original ROM. The single load-bearing correctness check.
uv run fantasm verify 1.0
While the project has a single version, the 1.0 can be left out
of every command (uv run fantasm verify); once there are several,
name one or set [project] default_target.
That’s the loop. fantasm verify failing means the disassembly is
not faithful — usually because a label, an instruction, or a data
declaration in the driver doesn’t match what’s actually in the ROM
bytes. Iterate on the driver until verify passes; then start
annotating.
Once you have a project running, see Workflows for the next
layer of analysis commands (audit, cfg, comments,
backfill, addresses, …) and fantasm.toml reference for the
fantasm.toml knobs that govern them.
CI integration
The four sibling repos all use a tiny GitHub Actions workflow built around the three commands above. The shape:
- name: Disassemble
run: uv run fantasm disassemble ${{ matrix.version }}
- name: Lint
run: uv run fantasm lint ${{ matrix.version }}
- name: Verify
run: uv run fantasm verify ${{ matrix.version }}
Each step exits non-zero on failure, so the workflow halts and
surfaces the diagnostic. The matrix.version indirection lets one
workflow cover every registered version.