API reference
Every public symbol fantasm exposes is re-exported from the top-level
fantasm.api package, so from fantasm.api import
verify_round_trip, build_call_graph, … is the intended import
path. The per-module documentation below is for readers who want the
full picture.
The CLI surface is documented separately in CLI reference and lives
under fantasm.cli (one module per command group); it is not
covered here.
Top-level package
Programmatic API for fantasm.
Sub-modules will hold per-topic surfaces (labels, comments, cfg, audit, …). This package re-exports the stable, flat surface so callers can do either:
from fantasm.api import rename_labels from fantasm.api.labels import rename_labels
The flat re-exports are populated as topics are ported in from the sibling
disasm_tools packages.
- exception fantasm.api.InfParseError[source]
Bases:
ValueErrorA
.inffile can’t be parsed, or holds no usable address.
- class fantasm.api.InfRecord(filename, load_address, exec_address=None, length=None, access=None, locked=False, extras=<factory>)[source]
Bases:
objectThe fields of one
.infline, as written.load_addressandexec_addressare the raw values (which may carry high-order bits); useload_address_16()/exec_address_16()for the 6502 address.- Parameters:
- load_address_16()[source]
The 16-bit load address. Raises
InfParseError.- Return type:
- fantasm.api.inf_address(value, field_name='load')[source]
Reduce a
.infaddress field to a 16-bit 6502 address.Values up to
&FFFFare returned unchanged. Values whose high bits are all set (&FFxxxx/&FFFFxxxx) mark an I/O processor address and are reduced to their low 16 bits. Anything else — a second-processor address, or a RISC OS filetype-and-timestamp stamp (&FFFtttdd) — raisesInfParseError.
- fantasm.api.parse_inf(text, *, source=None)[source]
Parse the text of a
.inffile.sourcenames the file in error messages. RaisesInfParseErrorwhen the text holds no line, or no load address can be found.
- fantasm.api.read_inf(inf_filepath)[source]
Read and parse a
.inffile. RaisesInfParseError.
- fantasm.api.sidecar_inf_filepath(binary_filepath)[source]
Return the existing
.infsidecar for a binary, elseNone.Tries
NAME.inf/NAME.INFbesideNAME, then — for a binary with an extension, such asFS3.bin—FS3.inf/FS3.INF.
- class fantasm.api.TargetSpec(id, binary, driver, asm, json, load_address=None, exec_address=None, metadata=None, inf_source=None)[source]
Bases:
objectA
[[targets]]entry to write intofantasm.toml.Paths are as they should appear in the file: relative to the project root (or absolute).
inf_sourcenames the.inffile the addresses were read from, recorded as a comment.- Parameters:
- fantasm.api.append_target(fantasm_toml_filepath, spec)[source]
Append a
[[targets]]entry to an existingfantasm.toml.Raises
ValueErrorif a target with the same id is already declared, or if the result would not parse (for example because the file declarestargetsas an inline array); the file is left unchanged in that case.- Parameters:
fantasm_toml_filepath (Path)
spec (TargetSpec)
- Return type:
None
- exception fantasm.api.TargetConfigError[source]
Bases:
ValueError[layout]or[[targets]]configuration is invalid, or a target’s files can’t be resolved from it.
- class fantasm.api.TargetFiles(version_id, prefix, version_dirpath, binary_filepath, metadata_filepath, asm_filepath, json_filepath, driver_filepath, load_address=None, exec_address=None, source='convention', image_name=None, inf_filepath=None, driver_cwd_dirpath=None)[source]
Bases:
objectThe resolved files and load address of one target.
version_idis the target id (the name is kept from when every target was a version).version_dirpathis the target’s version directory under the convention, or the project root for a target configured wholly by[layout]/[[targets]]; relativedocspaths in the metadata file resolve against it.metadata_filepathisNonewhen no metadata file is configured.load_addressisNoneonly on instances built by hand.- Parameters:
version_id (str)
prefix (str)
version_dirpath (Path)
binary_filepath (Path)
metadata_filepath (Path | None)
asm_filepath (Path)
json_filepath (Path)
driver_filepath (Path)
load_address (int | None)
exec_address (int | None)
source (str)
image_name (str | None)
inf_filepath (Path | None)
driver_cwd_dirpath (Path | None)
- exception fantasm.api.TargetNotFoundError(target_id, available)[source]
Bases:
LookupErrorNo configured target has the requested id.
- fantasm.api.default_target_id(project)[source]
The target commands use when none is named, or
None.[project] default_targetwhen set (it must name a known target), else the only target when there is exactly one.- Parameters:
project (ProjectContext)
- Return type:
str | None
- fantasm.api.list_target_ids(project)[source]
Return every known target id, without duplicates.
Order:
[[targets]]ids in config order; then, when[layout]is configured, the[[versions.entry]]ids and the ids found by[layout] discover; then the version directories on disk.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.resolve_target(project, target_id)[source]
Resolve a target’s files and load address.
Raises:
VersionNotFoundErrorfor an unknown id in a project that uses only theversions/convention (exactly as before targets existed);TargetNotFoundErrorfor an unknown id in a project with[layout]or[[targets]];TargetConfigErrorfor invalid configuration, or a path no layer can supply;InfParseErrorfor an unreadable.infsidecar.
- Parameters:
project (ProjectContext)
target_id (str)
- Return type:
- class fantasm.api.AssemblerConfig(command=('beebasm', '-i', '{asm}', '-o', '{output}'), cwd='{asm_dirpath}', output='{output}')[source]
Bases:
objectHow to assemble a listing for round-trip verification.
commandis the argument list;cwdthe directory to run it in;outputthe file to compare against the original binary, relative tocwdunless absolute. Each may use these tokens:{asm}— absolute path of the listing;{asm_dirpath}/{asm_filename}— its directory and name;{output}— a fresh temporary file path fantasm reads back (and deletes) afterwards;{output_dirpath}— the temporary directory holding it;{binary}— absolute path of the original binary;{project_root}— the project root, when known.
The defaults reproduce the built-in beebasm invocation:
beebasm -i {asm} -o {output}run in the listing’s directory, comparing{output}. An assembler that always writes a fixed file — for example via aSAVE "NAME"directive — setsoutputto that file instead.
- exception fantasm.api.AssemblerConfigError[source]
Bases:
ValueErrorA
[verify]assembler configuration is invalid.
- exception fantasm.api.AssemblerNotFoundError[source]
Bases:
RuntimeErrorRaised when the configured assembler can’t be found.
- exception fantasm.api.AssemblerOutputMissingError[source]
Bases:
RuntimeErrorThe assembler succeeded but the expected output file is absent.
- class fantasm.api.AsmSection(start_line, end_line, lines)[source]
Bases:
objectA slice of assembly lines extracted by
extract_section().start_lineandend_lineare 0-based;end_lineis exclusive.linesis the captured text without line numbers.
- exception fantasm.api.DotBinaryNotFoundError[source]
Bases:
FileNotFoundErrorThe graphviz
dot(or other engine) binary is not on PATH.
- exception fantasm.api.VersionNotFoundError(version_id, versions_dirpath, available)[source]
Bases:
LookupErrorRaised when no version directory matches the requested ID.
- fantasm.api.basic_blocks_to_dot(blocks, *, sub_name=None, sub_addr=None)[source]
Render a per-subroutine basic-block CFG as graphviz dot source.
- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.basic_blocks_to_graph(blocks, *, sub_name=None, sub_addr=None)[source]
Build a
DiGraphfrom a list of basic blocks.Nodes are keyed by
blk_NNNN(4-digit uppercase hex) and carryaddr,label(HTML),is_entry,is_return. Edges carrykind(one of theBasicBlockExitkinds) andtarget_addr. External targets (branch / jump destinations outside the supplied blocks) are emitted as placeholder nodes withexternal=True.- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.basic_blocks_to_graphml(blocks, *, sub_name=None, sub_addr=None)[source]
Render a per-subroutine basic-block CFG as GraphML text.
- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.build_index(asm_lines)[source]
Build
(addr_to_line, label_to_line)indices.For each line, looks for a
;NNNN:comment marker (the disassembler’s address tag) and a:NNNN[...]runtime-address annotation; both are recorded inaddr_to_line. Lines starting with.labelare recorded inlabel_to_line. The first occurrence wins for any given key.
- fantasm.api.build_memory_regions(meta, base_regions=())[source]
Build the full memory-region list for a single version.
Appends the ROM region (
meta["load_addr"]tometa["end_addr"] - 1) ontobase_regions.base_regionstypically comes fromfantasm.api.version_graph.VersionGraph.effective_regions(), converted fromRegiondataclasses to(start, end)tuples. Defaults to empty — ROM-only.Raises
KeyErrorifload_addrorend_addrare missing frommeta.
- fantasm.api.build_target_refs(items)[source]
Return an
addr -> referencing itemsindex.Indexes by each item’s
targetfield (the address of an instruction’s operand target, when the operand is a code/data reference).
- fantasm.api.call_graph_to_dot(graph, *, title=None)[source]
Render a call graph (
DiGraph) as graphviz dot source.
- fantasm.api.classify_labels(auto_labels, items, target_refs, audit_subs, memory_regions)[source]
Classify each auto-generated label into a category.
Returns a list of dicts with keys:
name,addr,category,parent_sub_name,parent_sub_addr,inbound_refs(each{addr, mnemonic, sub_name, sub_addr, cross_sub}),cross_sub_count.
- fantasm.api.collect_auto_labels(items)[source]
Return
[(label_name, addr), ...]for every auto-generated label.Walks
items’"labels"and"sub_labels"fields and keeps anything matchingAUTO_LABEL_RE.
- fantasm.api.collect_labels(data)[source]
Return every label declared in a disassembly, with source tag.
Walks
data["items"]’s"labels"and"sub_labels"fields (taggedsource="driver") anddata["external_labels"](taggedsource="env"). Each entry is a dict with keysname,addr,source.Driver-defined labels are emitted in item / declaration order; environment labels follow, in declaration order from the JSON. Duplicates are not collapsed — a label that appears as both an item label and a sub-label will yield two records. Callers that need uniqueness should dedupe on
(name, addr).
- fantasm.api.extract_section(asm_lines, start_target, end_target=None, default_window=40)[source]
Extract a section of assembly lines spanning two targets.
The section starts at the line resolved from
start_targetand ends at the line resolved fromend_target(inclusive). When noend_targetis given,default_windowlines are taken.The start is backed up across preceding blank, comment-only, and label-only lines so the captured section includes the natural “header” of the routine or block.
Raises
LookupErrorif either target cannot be resolved.
- fantasm.api.filter_call_graph(graph, *, focus=None, up_depth=None, down_depth=None, exclude_external=False, include_pattern=None, exclude_pattern=None, min_degree=0)[source]
Return a filtered copy of a call graph.
The filters compose. When
focusis supplied, the result is restricted to a neighbourhood: ancestors up toup_depthhops and descendants up todown_depthhops. Either may beNone(unlimited). Whenfocusis omitted, the depth options are ignored and the whole graph is considered.include_pattern/exclude_patternare regex strings matched against each node’snameattribute (or its id if no name).min_degreedrops low-connectivity nodes; focus nodes are immune.
- fantasm.api.find_containing_sub(addr, rom_subs)[source]
Return the last subroutine in
rom_subswhoseaddr <= addr.rom_subsmust be sorted byaddrascending.
- fantasm.api.find_containing_sub_for_addr(addr, sorted_subs, memory_regions)[source]
Find the subroutine in the same memory region whose start ≤ addr.
Unlike
fantasm.api.audit.find_containing_sub(), this variant is region-aware: a sub in a different memory region is never treated as containingaddr.
- fantasm.api.find_line_for_target(target, addr_to_line, label_to_line)[source]
Resolve a textual target to a 0-based line number.
Resolution order:
If
targetparses as a hex address, return the line that maps to that address — or, failing an exact match, the line for the nearest indexed address less than or equal totarget.If
targetmatches a label exactly, return that label’s line.If
targetis a substring of exactly one label, return that label’s line. If it matches multiple labels, emits aUserWarninglisting the matches and returns the lexically first match’s line so callers continue to make progress.
Returns
Nonewhen nothing matches.
- fantasm.api.find_sub(subs, target)[source]
Find a subroutine by hex address or name.
Tries (1) hex address
addr_str(with $/&/0x prefixes stripped), (2) exact name match, (3) substring match on name. When the substring match is ambiguous, emits aUserWarninglisting the matches and returnsNone.
- fantasm.api.find_undeclared_subs(json_filepath)[source]
Find JSR targets that lack
subroutine()declarations.Returns a list of dicts with:
addr,name,range_str,code_count,data_count,caller_count,container.
- fantasm.api.inbound_refs_to(addr, items_by_addr, target_refs)[source]
Return ref-site records pointing at
addr.Combines two sources:
code-flow references — items whose
target == addr(taken fromtarget_refs).data references — addresses listed in the
referencesfield of the item ataddr(table / pointer references the disassembler couldn’t attribute to a specific instruction).
Each record carries
{"addr", "mnemonic", "kind"}. Code-flow refs use the referencing item’s mnemonic; data refs use the mnemonic of the item at the referencing address when available, else"data_ref".kindis the dasmos >= 2.0ReferenceKind(direct/indexed/pointer/ …) of the access, joined from thereferencesfield of the item ataddrby caller address; it isNonefor a caller the schema-v2referencesfield does not classify (including any JSON produced by pre-2.0 dasmos, which did not carry kinds). Sorted byaddr; duplicates (same source addr) are collapsed, with the code-flow record taking precedence.
- fantasm.api.instruction_length(opcode, cpu='6502')[source]
Length in bytes of the instruction starting with
opcode.Returns 0 if
opcodeis invalid on the named CPU.
- fantasm.api.label_inventory(data)[source]
Build the full label inventory for one disassembly.
Each entry:
{"name", "addr", "source", "length", "ref_count", "refs", "direct_count", "indexed_count", "other_count", "is_code", "index_base_only"}.lengthislen(name).refsis the list returned byinbound_refs_to()for the label’s address;ref_countis its length, broken out so sorts / filters don’t pay the indexing cost.The three per-kind counts split
refsby the dasmos >= 2.0ReferenceKindof each site:direct_count(plainlda addr),indexed_count(indexing-baselda addr,X), andother_count(every other or unclassified site — pointer references, and any site leftkind == Noneby pre-2.0 JSON).is_codeis true when the item at the label’s address is atype == "code"item — the caveat flag for thed.index_base()workflow, where a genuine code entry point a loop happens to read aslda entry,Xmust not be moved off the memory map.index_base_onlyis true when the label has at least one reference and every one isindexed— the exactd.index_base()conversion candidate.Driver labels are deduped on
(name, addr)(a label can show up as both an item label and a sub-label of the parent item); env labels are kept as-is.
- fantasm.api.load_subroutines(json_filepath, *, memory_regions=None)[source]
Load JSON output and compute per-subroutine extents and flags.
Returns a list of subroutine dicts augmented with:
items,code_count,data_count,last_mnemonic,terminates,next_sub,entry_refs,branch_entry_refs,escaping_branches,flags. Theflagsentry is a set of strings drawn fromALL_FLAGS.memory_regionsconstrains where subroutine extents may run. The list should already include both the ROM range and any workspace regions where small subs may live. If omitted, it defaults to the ROM range alone (derived from the JSON’smeta) — sub extents are then bounded by ROM only.
- fantasm.api.mnemonic(opcode, cpu='6502')[source]
Mnemonic for
opcodeon the named CPU.Returns
"???"for opcodes that are invalid on the named CPU.
- fantasm.api.opcode_tables(cpu='6502')[source]
Return
(lengths, mnemonics)for the named CPU.Recognised values (case-insensitive):
"6502"(default, NMOS),"65c02"(CMOS, BBC Master 128)."nmos","cmos","65sc12"and"65c12"are accepted as aliases.Raises
ValueErrorfor unknown CPUs.The returned lists are independent for each CPU family but the same list objects are returned on each call — callers must not mutate them.
- fantasm.api.parse_address(text)[source]
Parse a hex address in any of the conventional notations.
Recognises
$8000,&8000,0x8000and bare8000. ReturnsNoneif the text is not a valid hex integer.
- fantasm.api.project_rom_prefixes(project)[source]
Return the configured ROM-name prefixes for the project.
Reads
[versions] prefixesfromfantasm.toml. If unset, falls back to[project] name. If neither is set, returns an empty tuple — callers should treat that as a configuration error.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.project_versions_dirpath(project)[source]
Return the project’s versions directory.
Reads
[versions] directoryfromfantasm.toml(defaulting to"versions"), resolved against the project root.Raises
RuntimeErrorif the project root has not been resolved.- Parameters:
project (ProjectContext)
- Return type:
Path
- fantasm.api.reference_addr(ref)[source]
Return the caller runtime address of a single
referencesentry.Accepts the dasmos >= 2.0
{"addr", "kind", "move_id"?}object or the pre-2.0 bare-int address.
- fantasm.api.reference_addrs(refs)[source]
Return the caller addresses of a whole
referenceslist.None(the field absent) yields an empty list.
- fantasm.api.reference_kind(ref)[source]
Return the
ReferenceKindof areferencesentry, if known.dasmos >= 2.0 objects carry a
kind; the pre-2.0 bare-int schema did not classify references, so this isNonefor those.
- fantasm.api.region_for_addr(addr, memory_regions)[source]
Return the (start, end) region containing
addr, orNone.
- fantasm.api.render_dot(dot_text, output_filepath, *, format='png', engine='dot')[source]
Pipe
dot_textintoengineand write the rendered output.Returns the output path. Raises
DotBinaryNotFoundErrorif the engine binary is missing, and propagatessubprocess.CalledProcessErrorfrom the engine itself.
- fantasm.api.resolve_version_dirpath(versions_dirpath, version_id, prefixes)[source]
Return the directory containing the named version.
Looks for
{prefix}-{version_id}underversions_dirpathfor each prefix in order, returning the first one that exists. RaisesVersionNotFoundErrorwhen no candidate matches; the exception carries the list of available version directory names so a caller can render a helpful error.
- fantasm.api.resolve_version_dirpath_for_project(project, version_id)[source]
Resolve a version directory using project-configured prefixes.
- Parameters:
project (ProjectContext)
version_id (str)
- Return type:
Path
- fantasm.api.rom_prefix(version_dirpath, prefixes)[source]
Return the ROM prefix matched by
version_dirpath’s name.For example, with prefixes
("anfs", "nfs")and a directory namedanfs-3.10, returns"anfs". Multi-hyphen prefixes liketube-6502-clientare matched eagerly, sotube-6502-client-1.10returns"tube-6502-client".Falls back to the substring before the last hyphen if no configured prefix matches — this preserves the old behaviour for callers operating on arbitrary directory names.
- fantasm.api.rom_prefix_for_project(project, version_dirpath)[source]
Extract a ROM prefix using project-configured prefixes.
- Parameters:
project (ProjectContext)
version_dirpath (Path)
- Return type:
- fantasm.api.scan_routine_range(addr, items_by_addr, sorted_addrs)[source]
Walk the items starting at
addruntil a terminating mnemonic.Returns
(range_end, code_count, data_count, falls_through).range_endis the address of the terminating instruction, orNoneif the routine falls through without terminating.
6502 / 65C02 opcodes and mnemonics
MOS 6502 / WDC 65C02 instruction tables and helpers.
This module is intentionally just CPU facts — opcode lengths, mnemonics, and a small dispatcher. Project-specific concerns like ROM base address and bank size belong in fantasm.toml, not here.
Tables for both the NMOS 6502 (the ubiquitous variant) and the CMOS
65C02 / 65SC12 (used in the BBC Master 128) are exposed. Use
opcode_tables() for the common case, or read the OPCODE_*
constants directly for the NMOS tables and the OPCODE_*_65C02
constants for the CMOS tables.
Source for the 65C02 overrides: py8dis/cpu65C02.py in the
acornaeology py8dis fork (also reflected in dasmos’s
ext/cpus/cpu65c02).
- fantasm.api.mos6502.opcode_tables(cpu='6502')[source]
Return
(lengths, mnemonics)for the named CPU.Recognised values (case-insensitive):
"6502"(default, NMOS),"65c02"(CMOS, BBC Master 128)."nmos","cmos","65sc12"and"65c12"are accepted as aliases.Raises
ValueErrorfor unknown CPUs.The returned lists are independent for each CPU family but the same list objects are returned on each call — callers must not mutate them.
Project paths and version directory layout
Project-layout path resolution.
The four sibling projects each had a tiny paths.py that hard-coded a
single ROM-name prefix (adfs, nfs, econet-bridge, …) — and
NFS uniquely supported two (anfs / nfs). fantasm generalises that
by reading the prefixes from fantasm.toml.
Schema (under [versions] in fantasm.toml):
[versions]
directory = "versions" # default; relative to project root
prefixes = ["anfs", "nfs"] # ordered: first match wins
If prefixes is omitted the project’s [project] name is used as a
single-element fallback.
The pure helpers (resolve_version_dirpath(), rom_prefix())
take prefix lists explicitly; the *_for_project wrappers obtain them
from a fantasm.config.ProjectContext.
Library code in fantasm raises VersionNotFoundError rather than
calling sys.exit; the CLI layer translates the exception into a
clean exit code with a helpful message.
- exception fantasm.api.paths.VersionNotFoundError(version_id, versions_dirpath, available)[source]
Bases:
LookupErrorRaised when no version directory matches the requested ID.
- fantasm.api.paths.project_binary_base(project)[source]
Return the project’s default load address (
base_addresskey).Reads
[binary] base_address(or[rom] base_address), defaulting to0x8000(the BBC sideways-ROM convention).- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_binary_dirname(project)[source]
Return the per-version binary subdirectory name (
dirkey).Reads
[binary] dir(or[rom] dir), defaulting to"rom".- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_binary_extension(project)[source]
Return the binary file extension, without a leading dot.
Reads
[binary] extension(or[rom] extension), defaulting to"rom". An empty string means the binary has no extension (e.g. a DFS*RUNprogram namedKEYPAD).- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_binary_filename_template(project)[source]
Return the binary-basename template (
filenamekey).Reads
[binary] filename(or[rom] filename), defaulting to"{prefix}-{version_id}". Rendered byrender_binary_filename(); the extension (if any) is appended afterwards. Set e.g."{version_id_upper}"to keep an Acorn DFS-style name likeKEYPADfrom the idkeypad.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_binary_metadata_filename(project)[source]
Return the per-version metadata filename (
metadatakey).Reads
[binary] metadata(or[rom] metadata), defaulting to"rom.json". The file lives in the binary subdirectory.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_binary_section(project)[source]
Return the project’s binary-layout config section.
Reads
[binary]fromfantasm.tomlwhen present, else falls back to the historical[rom]section, else an empty mapping. The two are not merged — whichever section is present wins — so a project migrating to[binary]should move all of its keys.- Parameters:
project (ProjectContext)
- Return type:
Mapping[str, Any]
- fantasm.api.paths.project_cpu(project)[source]
Return the project’s default CPU (
cpukey).Reads
[binary] cpu(or[rom] cpu), defaulting to"6502".- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_driver_dirname(project)[source]
Return the project’s driver-script subdirectory name.
Reads
[versions] driver_dirnamefromfantasm.toml, defaulting to"disassemble".- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_driver_filename_template(project)[source]
Return the project’s driver-filename template.
Reads
[versions] driver_filenamefromfantasm.toml. Default:"disasm_{prefix}_{version_id_no_dots}.py".- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_rom_prefixes(project)[source]
Return the configured ROM-name prefixes for the project.
Reads
[versions] prefixesfromfantasm.toml. If unset, falls back to[project] name. If neither is set, returns an empty tuple — callers should treat that as a configuration error.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.paths.project_versions_dirpath(project)[source]
Return the project’s versions directory.
Reads
[versions] directoryfromfantasm.toml(defaulting to"versions"), resolved against the project root.Raises
RuntimeErrorif the project root has not been resolved.- Parameters:
project (ProjectContext)
- Return type:
Path
- fantasm.api.paths.render_binary_filename(template, prefix, version_id)[source]
Render a binary-basename template with project / version tokens.
Same token set as
render_driver_filename()(see_filename_tokens()). The binary’s extension, if any, is appended by the caller after the rendered basename.
- fantasm.api.paths.render_driver_filename(template, prefix, version_id)[source]
Render a driver-filename template with project / version tokens.
See
_filename_tokens()for the recognised tokens.
- fantasm.api.paths.resolve_version_dirpath(versions_dirpath, version_id, prefixes)[source]
Return the directory containing the named version.
Looks for
{prefix}-{version_id}underversions_dirpathfor each prefix in order, returning the first one that exists. RaisesVersionNotFoundErrorwhen no candidate matches; the exception carries the list of available version directory names so a caller can render a helpful error.
- fantasm.api.paths.resolve_version_dirpath_for_project(project, version_id)[source]
Resolve a version directory using project-configured prefixes.
- Parameters:
project (ProjectContext)
version_id (str)
- Return type:
Path
- fantasm.api.paths.rom_prefix(version_dirpath, prefixes)[source]
Return the ROM prefix matched by
version_dirpath’s name.For example, with prefixes
("anfs", "nfs")and a directory namedanfs-3.10, returns"anfs". Multi-hyphen prefixes liketube-6502-clientare matched eagerly, sotube-6502-client-1.10returns"tube-6502-client".Falls back to the substring before the last hyphen if no configured prefix matches — this preserves the old behaviour for callers operating on arbitrary directory names.
- fantasm.api.paths.rom_prefix_for_project(project, version_dirpath)[source]
Extract a ROM prefix using project-configured prefixes.
- Parameters:
project (ProjectContext)
version_dirpath (Path)
- Return type:
Project-root discovery and fantasm.toml loading.
- class fantasm.config.ProjectContext(root_dirpath, config_filepath=None, config=<factory>)[source]
Bases:
objectThe resolved project context for a single fantasm invocation.
- fantasm.config.resolve_project_context(explicit_root_dirpath)[source]
Resolve the project root and load
fantasm.tomlif present.Resolution order:
explicit_root_dirpath— populated by Click from--project-rootor, failing that, theFANTASM_PROJECT_ROOTenvironment variable.Walk upwards from the current working directory looking for
fantasm.toml.Unresolved (returns a context with
root_dirpath = None).
- Parameters:
explicit_root_dirpath (Path | None)
- Return type:
Targets: resolving a target’s files
Disassembly targets and where their files live.
A target is one thing being disassembled: a program binary loaded
at some address, the driver script that disassembles it, and the
.asm / .json artefacts the driver writes. Every per-version
command operates on a target; in a multi-version project each version
is a target.
Where a target’s files live is resolved in three layers, each one overriding the one before it key by key:
Convention — the
versions/<prefix>-<id>/tree, configured by[versions]and[binary]. This is the default, and the only layer a project without[layout]or[[targets]]uses.``[layout]`` templates — path templates rendered per target id, for multi-version projects whose files don’t follow the
versions/convention:[layout] binary = "roms/{id}.rom" driver = "scripts/dis_{id_no_dots}.py" asm = "build/{id}.asm" json = "build/{id}.json"
``[[targets]]`` entries — explicit per-target paths, for adopting a project with its own layout as it stands:
[[targets]] id = "loader" binary = "orig/ELITE" # load/exec from orig/ELITE.inf driver = "tools/dis_loader.py" asm = "build/loader.asm" json = "build/loader.json"
All configured paths are relative to the project root. A target’s
load address comes from, in order: its load_address, its .inf
sidecar (for [[targets]] entries, read automatically when present
or named by inf), the [layout] load_address, and finally
[binary] base_address.
The set of known targets is the [[targets]] ids, then (when
[layout] is configured) the [[versions.entry]] ids and any ids
discovered by [layout] discover, then the version directories on
disk. With exactly one target — or a [project] default_target —
commands can be run without naming it.
- exception fantasm.api.targets.TargetConfigError[source]
Bases:
ValueError[layout]or[[targets]]configuration is invalid, or a target’s files can’t be resolved from it.
- class fantasm.api.targets.TargetFiles(version_id, prefix, version_dirpath, binary_filepath, metadata_filepath, asm_filepath, json_filepath, driver_filepath, load_address=None, exec_address=None, source='convention', image_name=None, inf_filepath=None, driver_cwd_dirpath=None)[source]
Bases:
objectThe resolved files and load address of one target.
version_idis the target id (the name is kept from when every target was a version).version_dirpathis the target’s version directory under the convention, or the project root for a target configured wholly by[layout]/[[targets]]; relativedocspaths in the metadata file resolve against it.metadata_filepathisNonewhen no metadata file is configured.load_addressisNoneonly on instances built by hand.- Parameters:
version_id (str)
prefix (str)
version_dirpath (Path)
binary_filepath (Path)
metadata_filepath (Path | None)
asm_filepath (Path)
json_filepath (Path)
driver_filepath (Path)
load_address (int | None)
exec_address (int | None)
source (str)
image_name (str | None)
inf_filepath (Path | None)
driver_cwd_dirpath (Path | None)
- exception fantasm.api.targets.TargetNotFoundError(target_id, available)[source]
Bases:
LookupErrorNo configured target has the requested id.
- fantasm.api.targets.default_target_id(project)[source]
The target commands use when none is named, or
None.[project] default_targetwhen set (it must name a known target), else the only target when there is exactly one.- Parameters:
project (ProjectContext)
- Return type:
str | None
- fantasm.api.targets.discover_target_ids(project)[source]
Ids found on disk by the
[layout] discovertemplate.The template is a path relative to the project root containing exactly one
{id}token, for example"roms/{id}.rom"; every matching file or directory contributes its{id}part. Returns[]whendiscoverisn’t set.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.targets.list_target_ids(project)[source]
Return every known target id, without duplicates.
Order:
[[targets]]ids in config order; then, when[layout]is configured, the[[versions.entry]]ids and the ids found by[layout] discover; then the version directories on disk.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.targets.project_layout(project)[source]
Return the validated
[layout]section (empty when absent).- Parameters:
project (ProjectContext)
- Return type:
Mapping[str, Any]
- fantasm.api.targets.project_target_entries(project)[source]
Return the validated
[[targets]]entries, in config order.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.targets.render_layout_template(template, target_id, prefix, *, key='path')[source]
Render a
[layout]template. RaisesTargetConfigError.
- fantasm.api.targets.resolve_target(project, target_id)[source]
Resolve a target’s files and load address.
Raises:
VersionNotFoundErrorfor an unknown id in a project that uses only theversions/convention (exactly as before targets existed);TargetNotFoundErrorfor an unknown id in a project with[layout]or[[targets]];TargetConfigErrorfor invalid configuration, or a path no layer can supply;InfParseErrorfor an unreadable.infsidecar.
- Parameters:
project (ProjectContext)
target_id (str)
- Return type:
- fantasm.api.targets.target_tokens(target_id, prefix)[source]
Token substitutions for
[layout]templates.{id},{id_no_dots},{id_upper},{id_lower}, the{prefix}family and the{version_id}family (aliases of the{id}ones, matching[versions] driver_filename).
- fantasm.api.targets.target_verify_settings(project, target_id)[source]
Return
[verify]merged with the target’s ownverifytable.- Parameters:
project (ProjectContext)
target_id (str)
- Return type:
Mapping[str, Any]
- fantasm.api.targets.uses_target_config(project)[source]
Truewhen[layout]or[[targets]]is configured.- Parameters:
project (ProjectContext)
- Return type:
Acorn .inf sidecar files
Parse Acorn .inf sidecar files.
Files extracted from Acorn DFS, ADFS and Econet discs usually travel
with a .inf sidecar recording the attributes the host filing
system can’t hold: load address, execution address, length and
access. There is no single format — every archiving tool wrote its
own variant — but they share a shape: one line holding the file’s
name followed by whitespace-separated fields. Typical examples:
$.ELITE FF1900 FF8023 000400 L
$.ELITE 001900 008023 Locked CRC=1A2B NEXT $.ELITE2
FS3 00001900 00001900 00004C00 33
"My File" &1900 &8023 &400 WR/r CRC32=0123ABCD
$.LOADER FFFF0E00 FFFF0E00 00000300 19 CRC=E4AB
The parser is deliberately tolerant:
the filename may be quoted; a leading
&/0xnumber in place of a filename is taken as “no filename”;numbers are hexadecimal, with or without a
&,$or0xprefix, and of any width (6-digit DFS and 8-digit ADFS fields are both common);the numeric fields fill load, exec, length and access in that order; trailing ones may be absent;
L/Lockedmarks a locked file; ADFS-style access strings such asWR/rare recorded as the access field;KEY=VALUEextras (CRC=,CRC32=,NEXT=,DATETIME=…) and the olderCRC 1A2B/NEXT $.FILEkeyword-then-value forms are collected and otherwise ignored.
Addresses are recorded as written; inf_address() reduces one to
the 16-bit 6502 address it denotes, accepting the FF/FFFF
“I/O processor” high bits and rejecting values that aren’t addresses
at all (such as a RISC OS filetype-and-timestamp stamp).
- exception fantasm.api.inf.InfParseError[source]
Bases:
ValueErrorA
.inffile can’t be parsed, or holds no usable address.
- class fantasm.api.inf.InfRecord(filename, load_address, exec_address=None, length=None, access=None, locked=False, extras=<factory>)[source]
Bases:
objectThe fields of one
.infline, as written.load_addressandexec_addressare the raw values (which may carry high-order bits); useload_address_16()/exec_address_16()for the 6502 address.- Parameters:
- load_address_16()[source]
The 16-bit load address. Raises
InfParseError.- Return type:
- fantasm.api.inf.inf_address(value, field_name='load')[source]
Reduce a
.infaddress field to a 16-bit 6502 address.Values up to
&FFFFare returned unchanged. Values whose high bits are all set (&FFxxxx/&FFFFxxxx) mark an I/O processor address and are reduced to their low 16 bits. Anything else — a second-processor address, or a RISC OS filetype-and-timestamp stamp (&FFFtttdd) — raisesInfParseError.
- fantasm.api.inf.parse_inf(text, *, source=None)[source]
Parse the text of a
.inffile.sourcenames the file in error messages. RaisesInfParseErrorwhen the text holds no line, or no load address can be found.
- fantasm.api.inf.read_inf(inf_filepath)[source]
Read and parse a
.inffile. RaisesInfParseError.
Round-trip verification
Round-trip assembly verification.
Re-assembles a generated .asm file and compares the resulting
bytes against the original binary. Used to catch regressions where a
refactor changes the disassembler’s output in a way that no longer
matches the source binary.
The assembler is beebasm by default. A project using another
assembler (or a wrapper script) describes how to run it with an
AssemblerConfig — typically read from fantasm.toml’s
[verify] section by AssemblerConfig.from_mapping().
Sibling verify.py mixed the comparison with print/exit. The
fantasm port returns a VerifyResult so callers (CLI,
tests, scripts) can format the outcome themselves.
- class fantasm.api.verify.AssemblerConfig(command=('beebasm', '-i', '{asm}', '-o', '{output}'), cwd='{asm_dirpath}', output='{output}')[source]
Bases:
objectHow to assemble a listing for round-trip verification.
commandis the argument list;cwdthe directory to run it in;outputthe file to compare against the original binary, relative tocwdunless absolute. Each may use these tokens:{asm}— absolute path of the listing;{asm_dirpath}/{asm_filename}— its directory and name;{output}— a fresh temporary file path fantasm reads back (and deletes) afterwards;{output_dirpath}— the temporary directory holding it;{binary}— absolute path of the original binary;{project_root}— the project root, when known.
The defaults reproduce the built-in beebasm invocation:
beebasm -i {asm} -o {output}run in the listing’s directory, comparing{output}. An assembler that always writes a fixed file — for example via aSAVE "NAME"directive — setsoutputto that file instead.
- exception fantasm.api.verify.AssemblerConfigError[source]
Bases:
ValueErrorA
[verify]assembler configuration is invalid.
- exception fantasm.api.verify.AssemblerNotFoundError[source]
Bases:
RuntimeErrorRaised when the configured assembler can’t be found.
- exception fantasm.api.verify.AssemblerOutputMissingError[source]
Bases:
RuntimeErrorThe assembler succeeded but the expected output file is absent.
- exception fantasm.api.verify.BeebasmNotFoundError[source]
Bases:
AssemblerNotFoundErrorRaised when
beebasmis required but not onPATH.
- class fantasm.api.verify.VerifyResult(matched, rom_size, assembled_size, compared_size, first_diff_offset, beebasm_returncode, beebasm_stderr)[source]
Bases:
objectOutcome of a round-trip verification.
matchedisTruewhen the assembled bytes equal the ROM bytes used for comparison. On a miss,first_diff_offsetis the byte offset of the first differing byte (or, if one stream is shorter, the length of the shorter stream).For sub-banked ROM images (where the file is larger than what is mapped at runtime — e.g. the Tube Client’s 4 KB image with only the upper 2 KB live at &F800-&FFFF), the comparison runs against the trailing portion of the ROM file matching the assembled output’s length.
rom_sizeis then the file size whilecompared_sizeis the trailing-slice length actually compared. When no slice is needed the two are equal.beebasm_returncodeandbeebasm_stderrcapture the assembler’s exit info; both are populated even on success.- Parameters:
- fantasm.api.verify.verify_round_trip(rom_filepath, asm_filepath, beebasm_filepath=None, *, assembler=None, project_root_dirpath=None)[source]
Assemble
asm_filepathand compare it torom_filepath.assemblerdescribes how to assemble (default: beebasm, seeAssemblerConfig).beebasm_filepathmay be passed explicitly (e.g. from thebeebasm_filepathtest fixture) and replaces abeebasmcommand name; otherwise a bare command name is looked up onPATH, and a relative path with a directory part resolves againstproject_root_dirpath(when given).Raises
BeebasmNotFoundError(or, for another assembler,AssemblerNotFoundError) if the assembler isn’t found,FileNotFoundErrorfor missing inputs, andAssemblerOutputMissingErrorif the assembler succeeded but didn’t write the expected output file.When the binary is larger than the assembled output (sub-banked ROM images, like the Tube Client’s 4 KB file of which only the upper 2 KB is mapped at &F800-&FFFF), the comparison runs against the trailing portion of the file matching the assembled length. The unsliced file size is reported on the result as
rom_sizeand the slice length ascompared_size.- Parameters:
- Return type:
Subroutine audit and memory regions
Audit subroutine annotations in a ROM disassembly.
Pure-logic helpers and data-loading routines, factored out of the
sibling disasm_tools.audit. Loads the JSON output from a
disassembly run and computes per-subroutine extents, termination
analysis, entry references, escaping branches, and a flag set
describing potential issues.
The presentational format_* and top-level audit() entry from
the sibling code are intentionally not yet ported — they print
directly to stdout, which is a CLI concern. They will land alongside
the fantasm audit Click sub-command.
Region awareness is project-aware: callers pass a version’s
memory_regions (typically derived from
fantasm.api.version_graph.VersionGraph.effective_regions() plus
the ROM range from JSON metadata). When omitted, the default is the
ROM range alone — sub extents are bounded by ROM only. The earlier
hardcoded BBC ranges that the sibling code carried have been removed.
TERMINATING_MNEMONICS and BRANCH_MNEMONICS are 6502 facts;
this module imports them from fantasm.api.mos6502 (the
canonical home) and uses them internally.
- class fantasm.api.audit.PlaceholderLabel(name, addr, kind, line_number)[source]
Bases:
objectA tracer-auto-discovered routine carrying a hex-tail placeholder.
These names are visible in
output/<ver>.asmbut never reach the JSON’s declaredsubroutineslist, so the regular audit can’t flag them.addris parsed from the hex tail ofname.
- fantasm.api.audit.build_memory_regions(meta, base_regions=())[source]
Build the full memory-region list for a single version.
Appends the ROM region (
meta["load_addr"]tometa["end_addr"] - 1) ontobase_regions.base_regionstypically comes fromfantasm.api.version_graph.VersionGraph.effective_regions(), converted fromRegiondataclasses to(start, end)tuples. Defaults to empty — ROM-only.Raises
KeyErrorifload_addrorend_addrare missing frommeta.
- fantasm.api.audit.find_containing_sub(addr, rom_subs)[source]
Return the last subroutine in
rom_subswhoseaddr <= addr.rom_subsmust be sorted byaddrascending.
- fantasm.api.audit.find_placeholder_labels(asm_lines)[source]
Scan asm output for hex-tail placeholder labels.
Returns the list in line-number order. Each match is one of:
.lXXXX/.cXXXX— pure auto-label (no prefix).sub_cXXXX— auto-discovered subroutine.loop_cXXXX— auto-discovered loop entry
where
XXXXis exactly four lowercase hex digits. Any other prefix is reported as"auto-label"(the structural shape is the same — only the convention differs).- Parameters:
- Return type:
- fantasm.api.audit.find_sub(subs, target)[source]
Find a subroutine by hex address or name.
Tries (1) hex address
addr_str(with $/&/0x prefixes stripped), (2) exact name match, (3) substring match on name. When the substring match is ambiguous, emits aUserWarninglisting the matches and returnsNone.
- fantasm.api.audit.find_undeclared_subs(json_filepath)[source]
Find JSR targets that lack
subroutine()declarations.Returns a list of dicts with:
addr,name,range_str,code_count,data_count,caller_count,container.
- fantasm.api.audit.load_subroutines(json_filepath, *, memory_regions=None)[source]
Load JSON output and compute per-subroutine extents and flags.
Returns a list of subroutine dicts augmented with:
items,code_count,data_count,last_mnemonic,terminates,next_sub,entry_refs,branch_entry_refs,escaping_branches,flags. Theflagsentry is a set of strings drawn fromALL_FLAGS.memory_regionsconstrains where subroutine extents may run. The list should already include both the ROM range and any workspace regions where small subs may live. If omitted, it defaults to the ROM range alone (derived from the JSON’smeta) — sub extents are then bounded by ROM only.
- fantasm.api.audit.region_for_addr(addr, memory_regions)[source]
Return the (start, end) region containing
addr, orNone.
- fantasm.api.audit.scan_routine_range(addr, items_by_addr, sorted_addrs)[source]
Walk the items starting at
addruntil a terminating mnemonic.Returns
(range_end, code_count, data_count, falls_through).range_endis the address of the terminating instruction, orNoneif the routine falls through without terminating.
Comment-vs-code consistency checks
Mechanical comment-vs-code consistency checks.
Runs checks on a disassembly’s JSON output to find inline and block
comments that contradict the instruction or surroundings. Each check
returns None (no finding) or a list of finding dicts, each tagged
with one of two confidence levels:
HIGH: the comment almost certainly disagrees with the code.MEDIUM: flagged for human review.
Sibling disasm_tools.comment_check was byte-identical across all
four forks at 677 lines. This port lifts the pure-logic surface
(constants, individual check functions, run_checks()) into
fantasm. The presentational format_findings() and the top-level
comment_check() entry are deferred until CLI integration.
- class fantasm.api.comment_check.ParentVersion(version_id, data, address_map=None)[source]
Bases:
objectA parent version’s disassembly, for
check_copied_comments().address_mapis an optional parent→child confidence map ({parent_addr: (child_addr, confidence)}, as produced bycompose_chained_map()); when given, findings say where the parent’s annotated instruction lives in the child.
- fantasm.api.comment_check.build_known_addrs(data, *, regions=())[source]
Build the set of “known” addresses for stale-addr checks.
Combines item addresses, subroutine addresses, external labels, and constants from
data, plus every address insideregions.regionsis(start, end)inclusive tuples, typically the union ofeffective_regions()andeffective_external_regions()for the version being checked. Defaults to empty — only addresses that appear in the JSON output count as known.The hardcoded BBC zero-page / OS / hardware ranges that the sibling code carried have been removed; pass them via
regionsinstead.
- fantasm.api.comment_check.check_block_md_link_in_code_span(item)[source]
comments_beforecarries a Markdown link inside a code span.
- fantasm.api.comment_check.check_block_stale_addr(item, known_addrs, seen)[source]
comments_beforecontains stale&XXXXaddresses.Excludes disassembler-generated reference lines and
*****separators. Skips(item_addr, stale)pairs already inseento avoid duplicating findings already produced for descriptions.
- fantasm.api.comment_check.check_branch_phrasing(item, _context)[source]
Comment phrasing that belongs to a different kind of instruction.
“Branch …” or “Loop until …” on an instruction that neither branches nor sets up a loop test, or “Load/Push/Pull …” on a branch, usually means the comment sits one instruction away from where it belongs. MEDIUM confidence.
- fantasm.api.comment_check.check_branch_target(item, _context)[source]
Comment explicitly claims a different branch/jump target.
Only fires when phrasing indicates the hex address IS the target (“branch to &XXXX”, “via &XXXX”, etc.). HIGH confidence.
- fantasm.api.comment_check.check_byte_of_address(item, _context)[source]
Comment calls an immediate the lo/hi byte of an address it isn’t.
Fires on the first
lo/low/hi/highword in the comment that is followed by either a four-digit&XXXX(whose matching half must equal the operand) or a parenthesised two-digit(&XX)(which must equal the operand). Typical cause: a comment copied from another version, where the address differs. HIGH confidence for an explicit&XXXX, MEDIUM for(&XX).
- fantasm.api.comment_check.check_chain_comments(sorted_items, sub_range=None)[source]
Increment/decrement chains use consistent comment style.
For chains of 3+ instructions, each segment-first instruction (entry point) should describe the cumulative effect, and non-first instructions should say
(continued). Enumeration comments likeAdd 1 (of 5)are flagged. Chains of exactly 2 are skipped. MEDIUM confidence.
- fantasm.api.comment_check.check_char_literal_operand(item, _context)[source]
Immediate rendered as a character but described as an address part.
LDA #')'with a comment about an offset, low byte or address usually means the value is really something like<(close_spool_cmd)and the character rendering is wrong. MEDIUM confidence.
- fantasm.api.comment_check.check_code_read_as_data(sorted_items, *, image_range=None)[source]
Code items that are only ever read as a table, never executed.
A code item whose every cross-reference is an indexed read (
LDA table,Y,CMP table,X) is usually a data table the tracer decoded as code, typically because it follows a branch that is in fact always taken. Excluded, because reading instruction bytes there is deliberate:subroutine entry points;
items whose operand bytes carry their own labels (e.g. a ROM header
JMPwhose address bytes are copied elsewhere);items outside
image_range((start, end)exclusive), such as code relocated into workspace;reads that are immediately followed by an indexed store (a copy loop), or that use a biased base such as
table - &81,Y, whose raw address deliberately falls before the table;items also used as the base of an indexed store, which must be addressing RAM beyond the item (e.g.
hazel_minus_1,Yon the ROM’s last byte), not a table in it.
MEDIUM confidence.
- fantasm.api.comment_check.check_copied_comments(sorted_items, parent)[source]
Inline comments copied by numeric address onto different code.
When a version is bootstrapped from its parent, an annotation can keep the parent’s address after the code beneath it has moved. The address is still a valid item start, so lint is satisfied, but the comment describes a different instruction. Flags inline comments whose text matches the parent’s comment at the same address while the instruction (or item type) there differs. Skipped when the comment fits both instructions: they differ only in register (
DEX/DEY), or the child instruction is a store described as “Write …”/”Store …”.MEDIUM confidence: either side may be the wrong one.
- Parameters:
parent (ParentVersion)
- Return type:
- fantasm.api.comment_check.check_cr_value(item, context)[source]
Comment says CR1=&XX / CR2=&XX but preceding LDA loaded a different value.
HIGH confidence.
- fantasm.api.comment_check.check_desc_md_link_in_code_span(sub)[source]
Subroutine description / title / on_entry / on_exit carry one.
- fantasm.api.comment_check.check_desc_stale_addr(sub, known_addrs)[source]
Subroutine description / title / on_entry / on_exit contains stale
&XXXX.
- fantasm.api.comment_check.check_md_link_in_code_span(item, _context)[source]
Inline comment carries a Markdown link inside a code span.
HIGH confidence.
- fantasm.api.comment_check.check_mid_item_comments(sorted_items, annotations)[source]
Inline comments keyed to an address inside an item.
The disassembler attaches such a comment to the item that covers the address, so it renders against the wrong line, usually one or two instructions away from what it describes. Typical cause: annotations carried over from a version whose instructions start a byte or two earlier. Needs the driver’s annotations, since the JSON records only the item the comment ended up on.
HIGH confidence inside an instruction, MEDIUM inside data.
- fantasm.api.comment_check.check_quoted_string_address(item, context)[source]
Comment places a quoted string at an address that holds something else.
Matches
"SP." string at &8529and similar, then compares the quoted text against the bytes at that address (top bit ignored, as Acorn strings often flag their last character). Addresses outside the disassembled image are not checked. MEDIUM confidence.
- fantasm.api.comment_check.check_reg_value(item, _context)[source]
Comment claims A=/X=/Y= with a value not matching the instruction.
HIGH confidence.
- fantasm.api.comment_check.check_stale_addr(item, context)[source]
Comment contains an
&XXXXnot in the version’s address space.MEDIUM confidence.
- fantasm.api.comment_check.check_tube_register(item, _context)[source]
Comment claims a Tube register access that doesn’t match the operand.
MEDIUM confidence.
- fantasm.api.comment_check.find_chains(sorted_items)[source]
Find chains of 2+ consecutive same-mnemonic increment/decrement ops.
- fantasm.api.comment_check.find_md_links_in_code_spans(text)[source]
Find Markdown address-links sitting inside backtick code spans.
Each Markdown
[label](address:HEX)link rendered inside a backtick code span (\`...\`) appears verbatim in the asm listing and the rendered HTML — the link syntax isn’t interpreted inside<code>. Authors typically slip into this when wrappingMNEMONIC + operandtogether inside one code span.Returns the list of offending span substrings (with their enclosing backticks) in source order. Empty list when there’s nothing to report.
- fantasm.api.comment_check.find_stale_addrs(text, known_addrs)[source]
Find
&XXXXhex addresses intextnot inknown_addrs.
- fantasm.api.comment_check.parse_imm_value(operand)[source]
Parse an immediate operand like
#&1Cor#42. Returns int or None.
- fantasm.api.comment_check.run_checks(data, sub_target=None, *, regions=(), driver_annotations=None, parents=())[source]
Run every check against
dataand return a flat list of findings.sub_target, if given, restricts checks to the subroutine starting at that hex address. RaisesValueErrorfor an invalid address.regionsextends the “known address” set used by stale-addr checks; seebuild_known_addrs(). Pass the version’s effective + external regions for project-aware behaviour.driver_annotations(fromparse_driver_annotations()) enablescheck_mid_item_comments();parentsenablescheck_copied_comments()against each parent version.
Comment suggestions
Pattern-based comment suggestions for uncommented instructions.
Two layers of heuristics:
Instruction patterns — generic 6502 facts.
PHAalways means “Save A on stack”;CLCmeans “Clear carry”; etc. These don’t vary per project and ship asDEFAULT_INSTRUCTION_HINTS.Workspace label hints — project-specific. Maps from a label substring (e.g.
"wksp_ch_flags") to a human description (e.g."channel flags"). When an instruction’s operand contains a matching label, the suggestion combines the mnemonic and the hint ("Get channel flags","Store in channel flags", etc.). Defaults to empty — users supply their project’s hints viafantasm.tomlor CLI arguments.
Sibling ADFS tools/suggest_comments.py was the model. The
fantasm port lifts the heuristics into pure helpers and parameterises
the workspace hints so any project can adopt the workflow.
- class fantasm.api.suggest.CommentSuggestion(addr, text)[source]
Bases:
objectOne suggested comment for an instruction.
- fantasm.api.suggest.suggest_comments(items, *, label_hints={}, declared_subs=frozenset({}), instruction_hints=None, address_range=None)[source]
Generate suggestions for every uncommented code item.
Skips non-code items, items already carrying
comment_inline, and items wheresuggest_for_instruction()returnsNone. Withaddress_range = (start, end), restricts to items in[start, end).
- fantasm.api.suggest.suggest_for_instruction(item, *, label_hints={}, declared_subs=frozenset({}), instruction_hints=None)[source]
Generate a comment suggestion for a single code item.
Logic order:
JSR / JMP to a target in
declared_subsreturnsNone— the disassembler already emits the subroutine’s title.If the item’s operand contains any key from
label_hints, and the mnemonic has a load/store template, returns the templated suggestion ("Get channel flags", etc.).Falls back to
instruction_hints(defaultDEFAULT_INSTRUCTION_HINTS).Returns
Noneif nothing matches.
Lint
Lint disassembly driver scripts and project documentation.
Pure-logic helpers for validating that:
Annotation addresses (
comment/subroutine/label) in a driver script correspond to addresses the disassembler actually emitted.Doc addresses cited from
rom.jsonaddress_linksmap to a covered range.Glossary links resolve.
Generated assembly contains no double-comment lines.
NFS was the most capable fork (555 lines vs. 522 / 522 / 492). The fantasm port takes NFS as the base but adopts ADFS’s annotation regex, which captures the subroutine name as well as its address — strictly more information for downstream callers. NFS’s richer docstrings are preserved.
Defaults that were per-project hardcodes in the sibling code (e.g.
+0x2000 ROM size) become parameters so a single fantasm install
serves projects with different bank sizes.
The presentational lint_* and top-level lint() entry are
deferred to CLI integration.
- fantasm.api.lint.address_in_ranges(address, ranges)[source]
Trueiffaddressfalls within any inclusive(start, end)range.
- fantasm.api.lint.address_ranges_from_data(data, *, base_regions=(), rom_base_default=32768, rom_size_default=8192, block_padding=32, last_block_padding=16, block_gap_threshold=256)[source]
Build address ranges covered by the disassembly.
Returns
(start, end)inclusive tuples. Components, in order:base_regions(workspace + hardware/OS regions, typically derived fromeffective_regions()∪effective_external_regions());the full ROM range from JSON metadata;
relocated-code clusters: addresses outside the ROM range are grouped into contiguous blocks (gaps larger than
block_gap_thresholdstart a new block) and padded out byblock_paddingto cover data tails within relocated blocks.
rom_base_default/rom_size_defaultare fallbacks for when the JSON metadata lacksload_addr/end_addr(BBC 8K sideways-ROM defaults; pass different values for 16K ADFS, 32K-bank carts, etc.).
- fantasm.api.lint.extract_annotations(driver_text)[source]
Extract all annotation addresses from driver-script text.
Recognises
comment,subroutineandlabelcalls in both the py8dis (bare function) and dasmos (d.<method>) dialects, including calls that span several lines, viaparse_driver_annotations(). Calls whose address isn’t a literal are skipped. Returns dicts with keyskind,address,line_number(1-based, of the opening line),detail, andname(Nonefor comments, or when the name isn’t a string literal).For
subroutinecalls,detailis"entry_point"unless the call passesis_entry_point=False, in which case it is"metadata_only"(the call is documentation-only and the address shouldn’t be expected to appear in the disassembler output).Raises
DriverParseErrorwhen the script is not valid Python.
- fantasm.api.lint.find_code_block_ranges(md_text)[source]
Find character offsets of fenced code blocks in Markdown.
Returns inclusive
(start, end)character offsets covering each fenced block from its opening fence to its closing one. Recognises both\`\`\and~~~fences.
- fantasm.api.lint.find_inline_scheme_links(text)[source]
Extract every inline
label:/glossary:link intext.Each result is
{"scheme", "target", "name", "line_number"}wherenameis the bare label name (@version/?flagstripped) for alabel:link, or the lower-cased slug for aglossary:link.
- fantasm.api.lint.find_nth_occurrence(text, pattern, n)[source]
Find the character offset of the
n-th (0-based) occurrence ofpattern.Returns
-1ifpatterndoes not occurn + 1times.
- fantasm.api.lint.glossary_slugs_from_markdown(md_text)[source]
Parse
GLOSSARY.mdand return the set of term slugs, for validatingglossary:SLUGreferences.
- fantasm.api.lint.load_address_ranges(json_filepath, *, base_regions=(), rom_base_default=32768, rom_size_default=8192)[source]
File-IO wrapper around
address_ranges_from_data().
- fantasm.api.lint.load_valid_addresses(json_filepath)[source]
File-IO wrapper around
valid_addresses_from_data().
- fantasm.api.lint.offset_in_code_block(offset, code_block_ranges)[source]
Trueiffoffsetfalls inside any fenced code block.
- fantasm.api.lint.valid_addresses_from_data(data)[source]
Return the set of valid addresses derived from disassembly JSON.
Combines item addresses, sub-label addresses, external-label addresses, subroutine addresses, and the full ROM range (so labels at
move()source addresses are accepted).
- fantasm.api.lint.valid_label_names_from_data(data)[source]
Return every label name the disassembly defines, for validating
label:NAMEreferences.Mirrors the site generator’s
build_label_addrsname set so alabel:the linter accepts is exactly one the generator can resolve: item labels, item sub-labels, subroutine names, memory-map and index-base names, and external-label names.
Call graph and basic blocks
Inter-procedural call-graph construction.
Builds a networkx.DiGraph whose nodes are subroutines and
whose edges represent JSR (call) and JMP (tail-call) relationships.
The sibling disasm_tools.cfg carried a hardcoded
MEMORY_REGIONS constant including the NFS-specific ROM range
0x8000-0x9FFF. The fantasm port reuses
fantasm.api.audit.build_memory_regions(), which derives the ROM
range from the disassembly’s JSON metadata — so projects with
different ROM bank sizes get correct extents without special-casing.
The presentational format_* and top-level cfg() entry from
the sibling code are deferred to CLI integration.
- class fantasm.api.cfg.BasicBlock(addr, items, entries, exits, commented, total)[source]
Bases:
objectOne straight-line basic block.
itemsis the list of code items (from the JSON disassembly) in this block.commentedis the number of items with a non-emptycomment_inline;totalislen(items).- Parameters:
addr (int)
entries (tuple[BasicBlockEntry, ...])
exits (tuple[BasicBlockExit, ...])
commented (int)
total (int)
- entries: tuple[BasicBlockEntry, ...]
- exits: tuple[BasicBlockExit, ...]
- class fantasm.api.cfg.BasicBlockEntry(source, kind)[source]
Bases:
objectA predecessor of a basic block.
kindmirrorsBasicBlockExit.kind.
- class fantasm.api.cfg.BasicBlockExit(target, kind)[source]
Bases:
objectWhere a basic block transfers control next.
kindis one of"branch","fall","jump","return","call".targetis the destination address orNonefor"return"/ dynamic jumps.
- fantasm.api.cfg.build_call_graph(json_filepath, *, memory_regions=None)[source]
Build a call-graph DiGraph from a disassembler JSON output.
Nodes (keyed by
"0xNNNN") represent ROM subroutines and external OS entry points. Edges carrycall_sites(sorted list of caller addresses) andtype("jsr"or"jmp").memory_regionsconstrains where subroutine extents may run. Defaults to the ROM range alone (derived from the JSON’smeta); pass effective regions from the version graph for project-aware behaviour.
- fantasm.api.cfg.find_basic_blocks(items)[source]
Identify basic blocks within a sequence of code items.
A basic block starts at:
The first item;
The target of any branch (within the items list);
The item after any branch / jump / call;
Any item that carries a label.
Each block ends at a branch / jump / return, or fall-through into the next block. Returns blocks sorted by start address, with
entriespopulated by reverse-walking the exits.- Parameters:
- Return type:
Graphviz and GraphML rendering
Graphviz / GraphML rendering for call graphs and basic-block CFGs.
Pure-Python emission, no pydot dependency. The call graph is
already a networkx.DiGraph (see fantasm.api.cfg); a
basic-block CFG is built on demand from the find_basic_blocks()
output. GraphML uses networkx.generate_graphml().
Output is designed for the dot layout engine — left-to-right for
the call graph, top-to-bottom for per-routine CFGs. The
render_dot() helper shells out to the dot binary when it is
on PATH.
Styling vocabulary (cribbed from OWL BASIC’s CFG visualiser and the Solarized palette):
Hexagon: subroutine entry / root (call graph) and entry block (CFG).
Doubleoctagon: external / OS entry point (call graph only).
Rounded box: ordinary internal subroutine / basic block.
Double border: blocks ending in
rts/rti/brk.
Edge styling:
Call graph: solid for
jsr; dashed orange forjmptail-calls.CFG: blue for the taken side of a conditional branch; green for the fall-through; red for unconditional
jmp; dashed grey for the rarecallexit kind (currently unused byfantasm.api.cfgbut reserved).
- exception fantasm.api.dot.DotBinaryNotFoundError[source]
Bases:
FileNotFoundErrorThe graphviz
dot(or other engine) binary is not on PATH.
- fantasm.api.dot.basic_blocks_to_dot(blocks, *, sub_name=None, sub_addr=None)[source]
Render a per-subroutine basic-block CFG as graphviz dot source.
- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.dot.basic_blocks_to_graph(blocks, *, sub_name=None, sub_addr=None)[source]
Build a
DiGraphfrom a list of basic blocks.Nodes are keyed by
blk_NNNN(4-digit uppercase hex) and carryaddr,label(HTML),is_entry,is_return. Edges carrykind(one of theBasicBlockExitkinds) andtarget_addr. External targets (branch / jump destinations outside the supplied blocks) are emitted as placeholder nodes withexternal=True.- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.dot.basic_blocks_to_graphml(blocks, *, sub_name=None, sub_addr=None)[source]
Render a per-subroutine basic-block CFG as GraphML text.
- Parameters:
blocks (Sequence[BasicBlock])
sub_name (str | None)
sub_addr (int | None)
- Return type:
- fantasm.api.dot.call_graph_to_dot(graph, *, title=None)[source]
Render a call graph (
DiGraph) as graphviz dot source.
- fantasm.api.dot.filter_call_graph(graph, *, focus=None, up_depth=None, down_depth=None, exclude_external=False, include_pattern=None, exclude_pattern=None, min_degree=0)[source]
Return a filtered copy of a call graph.
The filters compose. When
focusis supplied, the result is restricted to a neighbourhood: ancestors up toup_depthhops and descendants up todown_depthhops. Either may beNone(unlimited). Whenfocusis omitted, the depth options are ignored and the whole graph is considered.include_pattern/exclude_patternare regex strings matched against each node’snameattribute (or its id if no name).min_degreedrops low-connectivity nodes; focus nodes are immune.
- fantasm.api.dot.render_dot(dot_text, output_filepath, *, format='png', engine='dot')[source]
Pipe
dot_textintoengineand write the rendered output.Returns the output path. Raises
DotBinaryNotFoundErrorif the engine binary is missing, and propagatessubprocess.CalledProcessErrorfrom the engine itself.
Code context (depth, sub-context, uncommented gaps)
Per-subroutine context computations.
Used by the inline-comment and rename workflows: depth ordering, calling-convention extraction (entry / call sites / exits / post-call context), and uncommented-region analysis.
Sibling disasm_tools.context was the inline-comment workflow’s
input generator; ADFS tools/sub_context.py was a sister script
focused on calling-convention extraction. The fantasm port lifts the
pure-logic surface from both into one module.
- class fantasm.api.context.CallSiteContext(addr, mnemonic, in_sub_name, in_sub_addr, asm_lines, call_line_index)[source]
Bases:
objectOne call site for a target subroutine.
in_sub_nameandin_sub_addridentify the calling subroutine (when the caller could be located in the call graph).asm_linesis the assembly window around the call, with thecall_line_index(relative toasm_lines) marking the call instruction itself.- Parameters:
- class fantasm.api.context.CoverageGroup(label, start_addr, end_addr, code_count, commented_count)[source]
Bases:
objectOne row of a coverage breakdown.
Carries the (commented, total) counts for a contiguous slice of the disassembly — a 0x100-byte ROM page or a single subroutine, according to the breakdown the report was built with.
- class fantasm.api.context.CoverageReport(code_count, commented_count, subroutine_count, groups, group_by)[source]
Bases:
objectAggregate inline-comment coverage for a disassembly.
code_count/commented_countcover every code item in the JSON;subroutine_countis the number of declared subroutines.groupsis empty for the top-level summary; populated when agroup_bywas supplied tocompute_coverage().- Parameters:
- groups: tuple[CoverageGroup, ...]
- class fantasm.api.context.ExitPointContext(addr, mnemonic, asm_lines, exit_line_index)[source]
Bases:
objectOne terminating instruction within a subroutine.
- class fantasm.api.context.SubContext(addr, name, title, body_lines, body_start_line, call_sites, exit_points)[source]
Bases:
objectCalling-convention context for a single subroutine.
- Parameters:
addr (int)
name (str)
title (str)
body_start_line (int)
call_sites (tuple[CallSiteContext, ...])
exit_points (tuple[ExitPointContext, ...])
- call_sites: tuple[CallSiteContext, ...]
- exit_points: tuple[ExitPointContext, ...]
- class fantasm.api.context.UncommentedSubReport(addr, name, title, commented, total, density_pct, callees, workspace_refs)[source]
Bases:
objectA subroutine that’s a candidate for annotation work.
Surfaces the subroutine’s named callees and any workspace label references found within its extent, so the user can infer the sub’s purpose from its relationships with already-understood code.
- Parameters:
- fantasm.api.context.analyse_uncommented_subs(audit_subs, *, label_to_name=None, workspace_label_patterns=(), density_threshold_pct=30.0, min_items=20)[source]
Find subroutines below a comment-density threshold and report context.
For each subroutine in
audit_subs(typically fromfantasm.api.audit.load_subroutines()) with at leastmin_itemscode items and inline-comment density belowdensity_threshold_pct, returns anUncommentedSubReportcarrying:the named callees (JSR / JMP targets that resolve via
label_to_name);the workspace label references whose name contains any of the
workspace_label_patternssubstrings (typically("wksp_", "fsm_", "zp_", ...)for BBC-style projects).
With no
workspace_label_patterns, no label filtering is applied andworkspace_refsis empty.label_to_namemaps addresses to their canonical names; pass the union of subroutine names and external-label names for best resolution.
- fantasm.api.context.compute_call_depths(graph)[source]
Return
{node_id -> depth}over the internal subgraph.Depth 0 is a leaf (no internal callees). Depth
Nis one more than the maximum depth of its callees. Cycles are collapsed via strongly-connected-component condensation so a cycle’s nodes all share the same depth.Only internal nodes (
external=False) are included.
- fantasm.api.context.compute_coverage(data, *, audit_subs=None, group_by=None)[source]
Compute aggregate inline-comment coverage for
data.datais a parsed disassembler-emitted JSON (the same shapefantasm.cli_helpers.AnalysisContext.dataproduces — dasmos and py8dis both emit it). A code item counts as “commented” when itscomment_inlinefield is non-empty; block-levelcomment_above/comment_beloware deliberately ignored — the metric of interest is per-instruction inline density.group_byis one of:None— only the global counts;groupsis empty."page"— group by 0x100-byte ROM page."sub"— group by enclosing subroutine.audit_subsis then required (typically passed fromfantasm.api.audit.load_subroutines()).
Other values raise
ValueError.- Parameters:
- Return type:
- fantasm.api.context.extract_sub_context(sub, asm_lines, call_graph, *, body_window=20, caller_context=3, after_context=2, exit_context=3)[source]
Build a
SubContextforsub.subis a single dict fromfantasm.api.audit.load_subroutines().asm_linesis the full assembly file split withsplitlines(keepends=True).call_graphis fromfantasm.api.cfg.build_call_graph().The body window starts at the subroutine’s address and extends up to
body_windowlines (or to the next subroutine, whichever comes first). Call sites come from the call graph’s predecessors, withcaller_contextlines before andafter_contextlines after each. Exit points are every terminating mnemonic within the sub’s extent, withexit_contextlines before each.
Auto-label classification and rename application
Auto-generated-label classification.
Pure-logic helpers for classifying labels emitted by the disassembler
(c####, l####, loop_c####, sub_c####) into five
categories: subroutine, shared_tail, data,
internal_loop, internal_conditional. These categories drive
the ordered rename workflow.
Sibling disasm_tools.labels had a single generate_labels()
entry that mixed file IO, printing, and template rendering. The
fantasm port lifts the pure helpers into a stable surface; the
file-writing wrapper will land with the fantasm labels Click
sub-command.
Names imported elsewhere in the sibling tree (and now re-exported publicly here):
AUTO_LABEL_REcollect_auto_labels()(was_collect_auto_labels)CATEGORY_ORDER
- fantasm.api.labels.DIRECT_KIND = 'direct'
The
ReferenceKindvalue for a plainlda addr/sta addr.
- fantasm.api.labels.INDEXED_KIND = 'indexed'
The
ReferenceKindvalue dasmos assigns to an indexing-base access (lda addr,X/sta addr,Ywhere the byte touched isaddr+Xand the base byte itself is never read or written). A label whose references are exclusively this kind is ad.index_base()conversion candidate.
- fantasm.api.labels.build_target_refs(items)[source]
Return an
addr -> referencing itemsindex.Indexes by each item’s
targetfield (the address of an instruction’s operand target, when the operand is a code/data reference).
- fantasm.api.labels.classify_labels(auto_labels, items, target_refs, audit_subs, memory_regions)[source]
Classify each auto-generated label into a category.
Returns a list of dicts with keys:
name,addr,category,parent_sub_name,parent_sub_addr,inbound_refs(each{addr, mnemonic, sub_name, sub_addr, cross_sub}),cross_sub_count.
- fantasm.api.labels.collect_auto_labels(items)[source]
Return
[(label_name, addr), ...]for every auto-generated label.Walks
items’"labels"and"sub_labels"fields and keeps anything matchingAUTO_LABEL_RE.
- fantasm.api.labels.collect_labels(data)[source]
Return every label declared in a disassembly, with source tag.
Walks
data["items"]’s"labels"and"sub_labels"fields (taggedsource="driver") anddata["external_labels"](taggedsource="env"). Each entry is a dict with keysname,addr,source.Driver-defined labels are emitted in item / declaration order; environment labels follow, in declaration order from the JSON. Duplicates are not collapsed — a label that appears as both an item label and a sub-label will yield two records. Callers that need uniqueness should dedupe on
(name, addr).
- fantasm.api.labels.find_containing_sub_for_addr(addr, sorted_subs, memory_regions)[source]
Find the subroutine in the same memory region whose start ≤ addr.
Unlike
fantasm.api.audit.find_containing_sub(), this variant is region-aware: a sub in a different memory region is never treated as containingaddr.
- fantasm.api.labels.inbound_refs_to(addr, items_by_addr, target_refs)[source]
Return ref-site records pointing at
addr.Combines two sources:
code-flow references — items whose
target == addr(taken fromtarget_refs).data references — addresses listed in the
referencesfield of the item ataddr(table / pointer references the disassembler couldn’t attribute to a specific instruction).
Each record carries
{"addr", "mnemonic", "kind"}. Code-flow refs use the referencing item’s mnemonic; data refs use the mnemonic of the item at the referencing address when available, else"data_ref".kindis the dasmos >= 2.0ReferenceKind(direct/indexed/pointer/ …) of the access, joined from thereferencesfield of the item ataddrby caller address; it isNonefor a caller the schema-v2referencesfield does not classify (including any JSON produced by pre-2.0 dasmos, which did not carry kinds). Sorted byaddr; duplicates (same source addr) are collapsed, with the code-flow record taking precedence.
- fantasm.api.labels.label_inventory(data)[source]
Build the full label inventory for one disassembly.
Each entry:
{"name", "addr", "source", "length", "ref_count", "refs", "direct_count", "indexed_count", "other_count", "is_code", "index_base_only"}.lengthislen(name).refsis the list returned byinbound_refs_to()for the label’s address;ref_countis its length, broken out so sorts / filters don’t pay the indexing cost.The three per-kind counts split
refsby the dasmos >= 2.0ReferenceKindof each site:direct_count(plainlda addr),indexed_count(indexing-baselda addr,X), andother_count(every other or unclassified site — pointer references, and any site leftkind == Noneby pre-2.0 JSON).is_codeis true when the item at the label’s address is atype == "code"item — the caveat flag for thed.index_base()workflow, where a genuine code entry point a loop happens to read aslda entry,Xmust not be moved off the memory map.index_base_onlyis true when the label has at least one reference and every one isindexed— the exactd.index_base()conversion candidate.Driver labels are deduped on
(name, addr)(a label can show up as both an item label and a sub-label of the parent item); env labels are kept as-is.
- fantasm.api.labels.sort_labels(classified)[source]
Sort by category order, then parent-sub address, then label address.
Apply label-rename edits to a disassembly driver script.
Two rewrite strategies live here:
Inline (
apply_renames_inline()) — find every existinglabel(0xXXXX, …)/d.label(0xXXXX, …)declaration and rewrite its name at that line, wherever it sits in the driver. This matches the natural shape of hand-written drivers where label calls are interleaved with the code they annotate.Section (
apply_renames_to_lines()) — find a dedicated# Code label renamescluster, then either update an existing entry inside it or insert a new one in address-sorted order. Useful for auto-generated drivers that group reversible renames in one override section.
Sibling disasm_tools.rename_labels mixed parsing with file IO;
the fantasm port lifts the parsers and the text transformers and
leaves the IO to the fantasm labels apply CLI command.
- fantasm.api.rename_labels.apply_renames_inline(lines, renames)[source]
Rewrite
label(…)/d.label(…)declarations in place.For each
addr -> new_nameinrenames: locate the matching declaration line and rewrite its name. Indentation and thelabelvsd.labelcallable form are preserved.Returns
(new_lines, name_map)wherename_mapis theaddr -> (old_name, new_name)mapping actually applied — callers use this to drive optional textual reference rewrites on top of the declaration edits.Raises
LookupError(with every missing address listed) if any rename has no matching declaration. The driver is not partially rewritten — either every requested rename has a home or the function refuses to act.
- fantasm.api.rename_labels.apply_renames_to_lines(lines, renames)[source]
Return a new list of lines with the requested renames applied.
For each
addr -> new_nameinrenames: if the address already has a label declaration in the rename section, its name is updated in place; otherwise a newlabel(0x{addr:04X}, "name")line is inserted in address-sorted order.The input
linessequence is not mutated. Returns a list of lines suitable for writing back to the driver script.
- fantasm.api.rename_labels.find_insert_position(lines, section_start, section_end, target_addr)[source]
Find the line at which to insert a new
label()fortarget_addr.Maintains address-sorted order within the rename section; falls back to
section_end + 1if the section is empty or has no label declarations within bounds.
- fantasm.api.rename_labels.find_rename_section(lines)[source]
Find the
# Code label renamessection’s body bounds.Returns
(section_start, section_end)0-indexed.section_startis the line index of the header itself;section_endis the index of the line BEFORE the next# =+separator (or the last line of the file). Both areNoneif the section is absent.
- fantasm.api.rename_labels.parse_label_decl_lines(lines)[source]
Parse every
label(…)/d.label(…)declaration inlines.Like
parse_label_declarations()but recognises thed.labelform (dasmos’s actual call style) as well as the barelabelform. Each record carries the sourcelineindex, the matchedaddrandname, theprefix(the indentation captured at the start of the line), and thecallabletext (labelord.label) so callers can preserve it when rewriting.
- fantasm.api.rename_labels.parse_label_declarations(lines)[source]
Parse all
label()declarations from driver-script lines.Returns dicts with
addr,name,line(0-indexed).
- fantasm.api.rename_labels.update_ref_strings(lines, name_map)[source]
Rewrite textual references to renamed labels.
Walks each line and replaces occurrences of every
old_namewith the correspondingnew_namein three contexts:d.comment(...)string args, triple-quoteddescription=blocks, and Markdown[`name`](address:...)anchors. Other occurrences (raw identifiers, unrelated string text) are left alone.Returns
(new_lines, replacement_count). The count totals individual textual replacements, not lines touched.
Cross-reference field accessors
Accessors for the references field of a dasmos JSON item.
dasmos 2.0 (meta.schema_version 2) changed each incoming
cross-reference from a bare int caller address to a structured
object:
{"addr": int, "kind": str, "move_id": int?}
where kind is the ReferenceKind value (direct / pointer
/ indexed / indexed_pointer) and move_id is present only
when non-zero. The pre-2.0 schema (through dasmos 1.14.0) emitted the
bare address instead.
These accessors read either shape so fantasm can consume JSON produced
by an old or new disassembler run without the caller having to branch
on meta.schema_version.
- fantasm.api.references.reference_addr(ref)[source]
Return the caller runtime address of a single
referencesentry.Accepts the dasmos >= 2.0
{"addr", "kind", "move_id"?}object or the pre-2.0 bare-int address.
Subroutine-declaration insertion
Compute insertion points for new subroutine() declarations.
Parses a disassembly driver script (whether built on dasmos or
py8dis — both expose a subroutine() API with the same call
shape) to locate existing subroutine() declarations, identify
the main address-ordered block, and compute where a new declaration
for a given target address should be inserted.
Sibling disasm_tools.insert_point was byte-identical across all
four forks. The fantasm port lifts the pure-logic helpers
(parse_subroutine_declarations(), find_main_block(),
compute_insert_point()); the file-IO + printing wrapper that
formed the sibling find_insert_point is deferred to CLI
integration.
- exception fantasm.api.insert_point.AlreadyDeclared(declaration)[source]
Bases:
LookupErrorRaised when the target address already has a declaration.
- Parameters:
declaration (dict)
- Return type:
None
- class fantasm.api.insert_point.InsertPoint(insert_line, predecessor, successor, block_start_line, block_end_line)[source]
Bases:
objectResolved insertion point for a new
subroutine()declaration.insert_lineis 0-based.predecessorandsuccessorare the surrounding declaration dicts (orNone).- Parameters:
- fantasm.api.insert_point.compute_insert_point(lines, target_addr)[source]
Compute where in
linesa newsubroutine()fortarget_addrbelongs.Raises
AlreadyDeclaredif the address already has a declaration. RaisesLookupErrorif no declarations are present at all (so there’s no main block to anchor to).- Parameters:
- Return type:
Promotion candidates
Find labels worth promoting to entry points or subroutines.
Scores every labelled code item by combining four signals:
whether the predecessor item is a terminating mnemonic or data-separated (no fall-through path);
in-degree (how many JSR / branch references reach the label);
whether those references are calls (JSR/JMP) or branches;
come-from distance (how far the call sites are).
Higher scores are stronger candidates for promoting to entry()
or subroutine() declarations.
ADFS and EBR shipped this module identically; NFS and TUBE didn’t have it. The fantasm port lifts the pure analysis function out of the file-IO + print wrapper so it’s testable directly on a JSON data dict.
- fantasm.api.promote.analyze_labels(data)[source]
Score every labelled code item in
datafor promotion.Returns a list of dicts (sorted by descending score, then by address) with keys:
addr,name,score,total_refs,jsr_refs,branch_refs,max_distance,mean_distance,after_terminal,is_entry,is_subroutine.
ROM comparison
ROM binary comparison via difflib.SequenceMatcher.
Compares two ROM images at three granularities — byte-level, opcode-only (structural), and full instruction (opcode + operands) — and produces a human-readable report.
Sibling disasm_tools.compare was the most diverged module
(NFS at 470 lines vs. 319 in the others). NFS is taken as the base.
Refactors relative to the sibling:
ROM_BASEis no longer imported frommos6502— it’s now arom_baseparameter ondisassemble_linear(),format_address(),format_instruction(), andcompare_roms(). The default of0x8000matches the BBC sideways-ROM convention.The hardcoded
"NFS"literal in the report header is gone — the caller provides labels that already identify the project.Instructioncarriesrom_basesorom_addressworks without external plumbing.File-reading + printing wrapper deferred to
fantasm compareCLI sub-command.
- class fantasm.api.compare.Instruction(offset, opcode, operand_bytes, length, is_valid, rom_base=32768, mnemonic_str='???')[source]
Bases:
objectOne instruction from a linear sweep.
- Parameters:
- class fantasm.api.compare.RomHeader(language_entry, service_entry, rom_type, copyright_offset, binary_version, title, version_string, copyright)[source]
Bases:
objectParsed BBC Micro sideways-ROM header.
- Parameters:
- fantasm.api.compare.compare_headers(header_a, header_b, label_a, label_b)[source]
Return report lines comparing two ROM headers.
- fantasm.api.compare.compare_roms(data_a, data_b, label_a, label_b, cpu_a='6502', cpu_b='6502', rom_base=32768)[source]
Generate the full comparison report as a single string.
cpu_a/cpu_bselect the CPU variant for each ROM (the two ROMs may run different CPUs, e.g. NMOS vs. CMOS). Both ROMs share the samerom_basefor address formatting.
- fantasm.api.compare.disassemble_linear(data, cpu='6502', rom_base=32768)[source]
Decompose
datainto instructions via linear sweep.Invalid opcodes (length 0 in the table) are emitted as single one-byte
???records. This is intentional: both ROMs in a comparison are swept identically, so misinterpreted data tables still align in the SequenceMatcher.- Parameters:
- Return type:
- fantasm.api.compare.format_address(offset, rom_base=32768)[source]
Format a ROM offset as
$NNNNatrom_base + offset.
- fantasm.api.compare.format_instruction(inst)[source]
Format a single instruction for display.
Examples:
"LDA #$42 (A9 42)",".byte $FF (FF)".- Parameters:
inst (Instruction)
- Return type:
- fantasm.api.compare.parse_rom_header(data)[source]
Parse the BBC Micro sideways-ROM header out of
data.Header layout (offsets from start of ROM):
+0:JMP language_entry(3 bytes:$4C lo hi)+3:JMP service_entry(3 bytes:$4C lo hi)+6: ROM type byte+7: copyright offset (NUL preceding"(C)")+8: binary version+9: title (NUL-terminated), then version string up to the copyright offset, then"(C)..."NUL-terminated
Cross-version address mapping
Opcode-level address mapping between two ROM versions.
Provides a primary LCS-based mapping (difflib.SequenceMatcher on
opcode sequences — order-preserving, so it cannot follow code that
has been reordered or relocated between versions) and a supplementary
seed-and-extend pass (k-gram seeds + alignment, borrowed from
bioinformatics) that catches moved blocks.
Sibling disasm_tools.blockmatch was NFS-only (the cross-version
NFS work needed it). The fantasm port is essentially verbatim except
that ROM_BASE was lifted out of mos6502 — the helpers now
take rom_base as a parameter (default 0x8000).
Returns address mappings as dict[int, int] keyed by ROM address
(e.g. 0xA000 -> 0xA1B4).
Anchoring contract. Every emitted mapping is anchored in a
contiguous run of ≥ min_block_length matching opcodes (the LCS
“equal” run for the primary map; a single get_matching_blocks()
sub-block for the supplementary map). Source addresses that don’t
lie inside such a run are absent from the dict, so consumers must
explicitly handle “no mapping available” (KeyError /
dict.get(...) is None). The default threshold is 5 — short runs
are coincidental opcode coincidences that misleadingly suggest a
per-instruction correspondence where the surrounding code has
actually diverged. Lower the threshold deliberately when working on
tiny test fixtures.
- class fantasm.api.blockmatch.RelocatedBlock(a_start_addr, a_end_addr, b_start_addr, b_end_addr, ratio, matched_pairs, a_opcodes, b_opcodes)[source]
Bases:
objectA region of ROM A matched to a moved region in ROM B.
- Parameters:
- fantasm.api.blockmatch.build_full_address_map(data_a, data_b, cpu_a='6502', cpu_b='6502', *, rom_base=32768, min_block_length=5, **kwargs)[source]
Primary + supplementary opcode-level address map in one call.
Every emitted mapping is anchored in a contiguous run of ≥
min_block_lengthmatching opcodes (LCS run for primary, matching sub-block for supplementary). Source addresses outside such a run are absent from the dict — consumers must explicitly handle “no mapping available”. This is the contract thatfantasm sharedandfantasm backfillalready follow; see issue #10 for the incident that motivated the threshold.Returns
(full_map, primary_map, supplementary_map, blocks). Primary mappings take precedence on conflict.Tiny test fixtures (a handful of instructions) need
min_block_length=1to produce any mappings — the default of 5 is calibrated for real-ROM-sized inputs.
- fantasm.api.blockmatch.build_primary_map(insts_a, insts_b, rom_base=32768, *, min_block_length=5)[source]
LCS-based opcode mapping. Order-preserving; misses reorders.
Only
equalruns of length ≥min_block_lengthcontribute mappings. Shorter runs are dropped because they tend to be coincidental opcode-value matches that produce misleading identity-like mappings in regions where the surrounding code has actually diverged (see issue #10). Passmin_block_length=1deliberately when working on tiny test fixtures.
- fantasm.api.blockmatch.disassemble_to_opcodes(data, cpu='6502')[source]
Linear sweep returning
(offset, opcode, length)tuples.Invalid opcodes (length 0 in the table) advance one byte.
cpuselects between"6502"and"65c02".
- fantasm.api.blockmatch.find_relocated_blocks(insts_a, insts_b, primary_addr_map, *, rom_base=32768, k=6, min_seeds=2, max_seed_gap=8, min_block_opcodes=8, min_match_length=5, min_ratio=0.85)[source]
Find supplementary opcode-level mappings via k-mer seed-and-extend.
Conflicts inside the supplementary pass (one A or B instruction claimed by two clusters) are resolved greedily by ratio. Conflicts with the primary map are not allowed: the primary map is authoritative.
min_block_opcodesfilters out candidate windows shorter than that.min_match_lengththen filters individual matching sub-blocks within an aligned window — so even when a long candidate window is accepted, mappings only come from contiguous runs of ≥min_match_lengthmatching opcodes inside it (mirroring the LCS-run gate the primary map applies). This prevents stray single-opcode coincidences inside an aligned window from generating per-instruction mappings (see issue #10).Returns
(supplementary_map, blocks).supplementary_maphas no overlap withprimary_addr_map.- Parameters:
- Return type:
Fingerprinting (block-level deduplication)
Build short structural “fingerprints” of ROM regions.
Used to spot near-duplicate code blocks across ROM versions, often
as a cross-check for the fantasm.api.blockmatch results.
NFS-only sibling module. Ported with the same ROM_BASE lift-out
as the rest of the suite (rom_base is now a parameter; default
0x8000).
- fantasm.api.fingerprint.find_duplicate_blocks(fingerprints)[source]
Group addresses by fingerprint; keep groups with 2+ addresses.
- fantasm.api.fingerprint.fingerprint_blocks(data, block_size=64, cpu='6502', rom_base=32768)[source]
Fingerprint each
block_size-byte block ofdata.Returns
{rom_address -> fingerprint}keyed by the block’s starting ROM address.
- fantasm.api.fingerprint.fingerprint_rom_file(rom_filepath, block_size=64, cpu='6502', rom_base=32768)[source]
Convenience: load a ROM and fingerprint its blocks.
Byte-signature search (literal + wildcard patterns)
Byte-signature search across a ROM image.
Locate exact byte sequences (with optional ?? wildcards) inside
the raw ROM bytes. Distinct from
fantasm.api.fingerprint (block-level opcode fingerprints
within one ROM) and fantasm.api.find_shared (opcode-sequence
matching across two or more ROMs) — this module operates on raw
bytes only and is the right choice when you have a known sequence
(“4C B9 FF” — JMP somewhere ending FF) and want to know whether
and where it appears.
Pattern syntax (mirrors the convention in IDA / Ghidra / radare2):
"4C B9 FF"— three literal bytes."4c??ff"— same three positions, middle byte is a wildcard."$4C $?? $FF"/"0x4C 0x?? 0xFF"— per-token$and0xprefixes are tolerated."??"for “any single byte”; nibble-level wildcards (e.g."4?") are intentionally not supported — 6502 opcode encoding doesn’t reward nibble masking.
Use parse_byte_pattern() to compile a pattern and
find_byte_pattern() to scan a ROM. The CLI front-end is at
fantasm bytes find.
- class fantasm.api.bytes_search.BytePattern(bytes_, wildcards)[source]
Bases:
objectA compiled byte-search pattern.
bytes_[i]is the literal byte expected at positionifor every position not inwildcards. Positions inwildcardsmatch any byte; the values stored at those indices inbytes_are placeholder zeros and should not be inspected directly.
- class fantasm.api.bytes_search.ByteMatch(address, offset, captures)[source]
Bases:
objectOne hit of a
BytePatternagainst a ROM image.addressis the runtime address (rom_base + offset);offsetis the raw file offset.capturescarries the bytes the pattern’s wildcard positions matched, in pattern order — empty when the pattern has no wildcards.
- fantasm.api.bytes_search.find_byte_pattern(rom_bytes, pattern, *, rom_base=32768)[source]
Scan
rom_bytesfor every occurrence ofpattern.Returns matches in ascending offset order. Each match’s
capturescarries the bytes at the pattern’s wildcard positions, in pattern order; for a pure-literal patterncapturesis empty.- Parameters:
rom_bytes (bytes)
pattern (BytePattern)
rom_base (int)
- Return type:
- fantasm.api.bytes_search.parse_byte_pattern(text)[source]
Parse a hex-with-wildcards string into a
BytePattern.Recognises:
hex pairs (
4C,4c);??wildcards (one wildcard per pair);per-token
$and0x/0Xprefixes;any amount of whitespace between tokens.
Raises
ValueErrorfor empty input, lone?(must be??), nibble-level wildcards (4?), odd-length hex, non-hex characters, or patterns made entirely of wildcards (which would match every position).- Parameters:
text (str)
- Return type:
Data-declaration review and heuristic classification
Review and reclassification of data declarations.
Two related capabilities, both serving the “Phase D” data-review workflow on annotated 6502 disassemblies:
find_data_runs()ranks contiguous runs of same-type data items (byte/word/string) so the longest ones — typical candidates for a closer look — surface first.find_classification_candidates()applies three heuristic classifiers (padding, string, code) to runs of bytes the disassembler flagged as rawbytedata, surfacing spans that might actually be strings, valid code, or ROM padding — candidates for reclassification with stricter declarations in the driver.
Both functions read the disassembler JSON items directly (dasmos
and py8dis emit the same shape). The classifiers
are designed to be used both as a suite (via the orchestrator) and
individually (the looks_like_* functions accept a raw
bytes span).
The code-likelihood classifier walks every starting alignment with
the per-CPU fantasm.api.mos6502.OPCODE_LENGTHS /
fantasm.api.mos6502.OPCODE_LENGTHS_65C02 table; conceptually
this is equivalent to a regex of the form (L1|L2.|L3..)+ over
the byte stream, but the explicit loop is faster, supports both
CPU families cleanly via the existing tables, and produces
per-alignment runs (the longest from any starting alignment within
a span) rather than the regex’s left-to-right alternation, which
biases toward shorter matches at each position.
- class fantasm.api.data_review.Classification(addr, length, kind, confidence, preview)[source]
Bases:
objectOne reclassification candidate produced by the orchestrator.
- class fantasm.api.data_review.CodeClassification(start_offset, length, instruction_count, first_mnemonic, confidence)[source]
Bases:
objectLongest valid 6502 instruction sweep found within a byte span.
- Parameters:
- class fantasm.api.data_review.DataRun(start_addr, end_addr, item_type, item_count, byte_length, label, commented_count)[source]
Bases:
objectOne contiguous run of same-type data items.
end_addris inclusive;item_countis the number of JSON items in the run,byte_lengththe total bytes spanned (often larger thanitem_countbecause a singlebyte/worditem can carry multiple bytes).- Parameters:
- class fantasm.api.data_review.HiBytesTableClassification(start_offset, length, rom_page_range, confidence)[source]
Bases:
objectA run of bytes all in the project’s ROM-page range.
The high-byte halves of PHA/PHA/RTS-style dispatch tables look like this: every entry’s high byte is a ROM page number, so the contiguous run sits entirely inside the band
rom_base >> 8..(rom_end - 1) >> 8. Such runs happen to decode as long sequences of “valid” 6502 instructions (because most opcodes in the 0x80-0xBF range exist), which the code classifier would otherwise falsely flag as code.
- class fantasm.api.data_review.PaddingClassification(start_offset, length, fill_byte, pattern_length, confidence)[source]
Bases:
objectRepeating-pattern run starting at offset 0 of a span.
fill_byteis the leading byte of the pattern;pattern_lengthis 1 forFF FF FF/ 2 forAB CD AB CD/ etc.
- class fantasm.api.data_review.StringClassification(start_offset, length, text, confidence)[source]
Bases:
objectBest printable-ASCII run found within a byte span.
- fantasm.api.data_review.classify_run_bytes(span, *, run_addr=0, cpu='6502', rom_page_range=None, min_string=4, min_code=8, min_padding=4, min_hi_bytes=8)[source]
Apply padding → string → hi_bytes_table → code classifiers across
span.Walks
spanleft-to-right, calling each classifier in priority order at the current cursor position. The first classifier that returns a match consumes those bytes; the cursor advances past the match before the next round. The hi_bytes_table classifier is skipped whenrom_page_rangeisNone.Returned
Classificationaddrvalues arerun_addr + match_offsetso callers can plot the findings back onto the ROM directly.
- fantasm.api.data_review.find_classification_candidates(items, *, cpu='6502', target_types=('byte',), rom_page_range=None, min_string=4, min_code=8, min_padding=4, min_hi_bytes=8)[source]
Walk runs of target-type items and surface reclassification hints.
For each contiguous run of items whose type is in
target_types(default("byte",)— the catch-all bucket the disassembler emits for unclassified data), concatenate the run’s bytes and apply the classifiers in priority order: padding → string → hi_bytes_table → code. The first classifier to claim bytes at a given offset wins; the cursor advances past the match before the next classifier runs.The
hi_bytes_tablestep runs only whenrom_page_rangeis provided (typically derived by the caller from the JSON’smeta.load_addr/meta.end_addr). It catches runs of bytes all in the project’s ROM-page band — the high-byte halves of dispatch tables, which would otherwise be misread as long valid-code sweeps by the code classifier.Returns one
Classificationper claimed span, sorted by length descending so the most-interesting candidates surface first. Spans that no classifier claims do not appear in the output (the user is interested in candidates for reclassification, not in noting that the disassembler already got the rest right).
- fantasm.api.data_review.find_data_runs(items, *, min_bytes=8, item_types=('byte', 'word', 'string'))[source]
Return contiguous runs of same-type items, longest first.
A run is a maximal stretch of consecutive items whose
typematches one initem_types. Items whose type is not initem_types(most importantlycode) break runs. Within a run, items keep their individual labels and comments; only the leading label is exposed on the returnedDataRun(the “what is this region?” hint).Runs whose total byte length is below
min_bytesare dropped — by default 8 bytes, which lets a four-entry vector table (4 × 2 = 8) through while filtering out noise from the many short EQUB-pair runs in any real ROM.Returns runs sorted by
byte_lengthdescending so the most-interesting candidates surface first.
- fantasm.api.data_review.looks_like_code(span, *, cpu='6502', min_length=8)[source]
Find the longest valid-opcode sweep within
span.Walks every starting alignment in
spanand consumes instructions according to the per-CPU opcode-length table. A sweep ends at the first byte that doesn’t decode to a declared opcode (length 0) or that would extend past the end ofspan. The longest sweep across all alignments wins.cpuselects the opcode-length table —"6502"(NMOS, default) or"65c02"(CMOS).Confidence is currently a coarse length heuristic (
min(length / 32, 1.0)); fields are reserved for future operand-sanity scoring.- Parameters:
- Return type:
CodeClassification | None
- fantasm.api.data_review.looks_like_hi_bytes_table(span, *, rom_page_range, min_length=8)[source]
Find the longest run of bytes all in
rom_page_range.rom_page_rangeis an inclusive(low, high)tuple of byte values — typically derived from the project’s ROM extents (e.g.(0x80, 0xBF)for a 16 KB BBC sideways ROM at &8000,(0xE0, 0xFF)for the Acorn Econet Bridge at &E000). A contiguous run of bytes all within this band is a strong signal of a high-byte address table — the “lo bytes / hi bytes” half of a PHA/PHA/RTS dispatch.Such runs happen to decode as long stretches of “valid” 6502 instructions (most opcodes in 0x80-0xBF exist), which is what makes
looks_like_code()mis-fire on them. Running this classifier first in the priority chain claims the bytes before the code classifier sees them.Confidence is 1.0 for an exact in-band run.
- fantasm.api.data_review.looks_like_padding(span, *, min_length=4, max_pattern_length=4)[source]
Detect a repeating-byte-pattern run starting at offset 0.
Tries every pattern length from 1 up to
max_pattern_length(default 4 — ROM fill is overwhelmingly 1-byte fills likeFF FF FF…/00 00 00…/EA EA EA…, sometimes 2-byte alternating fills likeAB CD AB CD, very rarely longer). The match requires at least two full repetitions of the pattern, which avoids classifying coincidentally-repeated short instruction sequences as padding.Confidence is 1.0 for an exact pattern match.
- Parameters:
- Return type:
PaddingClassification | None
- fantasm.api.data_review.looks_like_string(span, *, min_length=4)[source]
Find the longest printable-ASCII run within
span.A character is “printable” if it is in 0x20-0x7E or is one of the common whitespace controls (
\t,\n,\r). A trailing\0is allowed as a terminator — it ends the run and is not included in the returned text — but the run must still meetmin_length.Confidence is the printable density (printables / total run bytes) over the matched range; for a clean string this is 1.0 (or ~0.95 with one terminator).
- Parameters:
- Return type:
StringClassification | None
Print-inline hook discovery
Detect missing print-inline hook_subroutine() declarations.
A common cause of long unannotated byte runs in Acorn-style
6502 ROMs is a missing hook_subroutine() for a print-inline
helper (stringz / stringcr / stringhi style). The
hook_subroutine API and the bundled hook names are shared
between the supported disassembler libraries (dasmos and py8dis),
so the same detection logic applies regardless of which one the
driver targets. The shape of the missing hook in driver JSON
output is:
JSR <target> ← code item, mnemonic="jsr"
EQUS "..." ← string item with the inline text.
For stringz / stringcr the terminator
(0x00 / 0x0D) is the last byte of the
string item; for stringhi it is *not* —
see `_classify_terminator`.
EQUB ... ← resume code, mis-emitted as raw byte
items because the hook wasn't wired up
Once hook_subroutine(<target>, name, <kind>_hook) is added to
the driver, the disassembler treats the post-JSR bytes correctly
and emits the resume code as code items. Until then, every
call site contributes a mysterious unannotated byte run downstream.
This module surfaces the missing hook by looking at the pattern at every call site, not at the byte runs in isolation. A JSR target whose call sites consistently match the “string-then-byte-run-decoding-as-code” signature is almost certainly a print-inline helper that needs hooking.
- class fantasm.api.hooks.HookCandidate(target, target_label, matching_call_sites, total_call_sites, hook_kind, sample_strings)[source]
Bases:
objectA JSR target that looks like an unhooked print-inline helper.
matching_call_siteslists JSR addresses whose post-call pattern matches the missing-hook signature;total_call_sitesis every JSR (and JMP, treated as tail-call) that targetstarget.confidenceis the ratio — when 1.0, every call site matches the pattern, which is the strongest signal a hook is missing.- Parameters:
- fantasm.api.hooks.find_hook_candidates(items, *, cpu='6502', min_call_sites=2, min_resume_code_bytes=4, min_confidence=0.5)[source]
Find JSR targets with the missing-print-inline-hook signature.
Walks
itemslinearly. For eachcodeitem withmnemonic == "jsr"(orjmp, which the disassembler treats similarly when used as a tail-call), the next two items are inspected:The first must be a
stringitem.The second must be a
byteitem whosebytesdecode as valid 6502 instructions starting at offset 0 for at leastmin_resume_code_bytesbytes.
Matching call sites are grouped by target. A target is reported as a candidate when:
it has at least
min_call_sitesmatching call sites;the matching ratio (matching / total) is at least
min_confidence.
The returned list is sorted by
confidencedescending then matching call-site count descending.
- fantasm.api.hooks.render_hook_subroutine_line(candidate, *, placeholder_name='FILL_ME')[source]
Render a paste-ready
hook_subroutine(addr, name, hook)line.Falls back to
placeholder_namewhen the candidate has no target label, and to a comment marker when the kind isunknown(no hook-function name to emit).- Parameters:
candidate (HookCandidate)
placeholder_name (str)
- Return type:
Annotation backfill across versions
Back-propagate annotations between ROM versions.
Builds an opcode-level confidence map between two ROM images and
parses disassembly driver scripts to extract their annotations
(comment/label/subroutine calls — the same call shape
in dasmos and py8dis). Together these let annotations from a
richly-annotated version flow back to less annotated ones via
the high-confidence portion of the address map.
Sibling disasm_tools.backfill (621 lines, byte-identical across
forks) carried two heavy NFS-specific pieces:
VERSION_CHAIN— adjacent-version pairs and their relocation blocks, hardcoded for NFS.build_chained_mapand the top-levelbackfill()— file-IO, printing, and version-chain walking that depends on the above.
The fantasm port lifts the pure helpers out:
build_confidence_map(), build_confidence_map_for_block(),
group_logical_statements(), parse_annotations(),
translate_address_in_text(), translate_subroutine().
Project-specific concerns are parameters now: rom_base (was a
module-level constant) and workspace_ranges (was hardcoded as
range(0x0000, 0x0100) plus NFS’s range(0x0D00, 0x1000)).
A future fantasm.toml schema will let projects declare their own
version chains; build_chained_map will land then alongside the
fantasm backfill Click sub-command.
- class fantasm.api.backfill.AnnotationDiff(source_addr, target_addr, confidence, kind, source_value, target_value, status)[source]
Bases:
objectA single annotation difference between two driver scripts.
statusis one of:"differs"— both source and target carry an annotation of this kind at the mapped addresses, but the values differ (different comment text, different label name, different sub title);"missing_in_target"— source has an annotation; target’s mapped address has no annotation of this kind;"no_mapping"— source’s address has no entry in the confidence map (or below threshold), so the annotation can’t be located in the target at all.
For
"no_mapping",target_addrisNoneandconfidenceis0.- Parameters:
- class fantasm.api.backfill.PropagationCandidate(source_addr, target_addr, confidence, kind, name, text)[source]
Bases:
objectOne annotation that backfill proposes to copy from source to target.
confidenceis the composed block-length from the chain map.textcarries the comment string (forkind == "comment") or the fullsubroutine()source text (forkind == "subroutine"); for labels it’s the label name.- Parameters:
- class fantasm.api.backfill.PropagationReport(candidates, skipped_no_mapping, skipped_below_threshold, skipped_target_has_annotation, skipped_label_name_conflict)[source]
Bases:
objectResult of
propose_propagations()— accepted + skipped tallies.- Parameters:
- candidates: tuple[PropagationCandidate, ...]
- fantasm.api.backfill.build_confidence_map(data_a, data_b, reloc_blocks=(), *, rom_base=32768, workspace_ranges=((0, 256),), high_confidence=1000, cpu='6502')[source]
Full confidence map between two ROM versions.
reloc_blocksis an iterable of(src_a, src_b, runtime_dest, length)tuples describing relocated code — each block is matched separately at its runtime destination and merged in.workspace_rangesget identity mappings athigh_confidenceso labels for workspace variables propagate even when no opcodes anchor them.Returns
{addr_a: (addr_b, block_length)}.
- fantasm.api.backfill.build_confidence_map_for_block(insts_a, insts_b, base_a, base_b)[source]
Build
{addr_a: (addr_b, block_length)}from two instruction lists.base_a/base_bare added to raw offsets to produce final addresses.block_lengthis the number of consecutive matching opcodes in the enclosing equal block — the confidence signal.
- fantasm.api.backfill.diff_annotations(source_text, target_text, confidence_map, *, threshold=5)[source]
Compare annotations across two driver scripts.
For each annotation in
source_text, looks up its mapped target address inconfidence_map(filtered bythreshold) and compares against the target driver’s annotations at that address. Returns a list ofAnnotationDiffrecords covering every discrepancy.“Discrepancy” includes addresses with no mapping, addresses where the target lacks the same kind of annotation, and addresses where both sides have annotations of the same kind but with different content.
- fantasm.api.backfill.group_logical_statements(lines)[source]
Group lines into balanced-paren statements.
Returns
(start_line_idx, end_line_idx_exclusive, lines_list)tuples. Quoted strings and#comments are skipped when counting parens.
- fantasm.api.backfill.make_project_rom_loader(project)[source]
Return a memoised program-binary loader keyed by version id.
The loader resolves each version’s directory against the project config (
[versions] prefixes/directory), then reads the binary from the configured[binary]layout (dir/extension), defaulting toversions/<prefix>-<id>/rom/<prefix>-<id>.rom. Bytes are cached for the lifetime of the returned closure.Raises
FileNotFoundErrorwhen a requested version’s binary file is missing — the API-layer counterpart to the CLI’sclick.UsageError.- Parameters:
project (ProjectContext)
- Return type:
- fantasm.api.backfill.parse_annotations(script_text)[source]
Parse driver-script text and return its annotations.
Accepts both the py8dis (
comment(0x..., inline=True)) and dasmos (d.comment(0x..., align=Align.INLINE)) call shapes viaparse_driver_annotations(). Only inline comments are collected; block comments are not propagated.Returns
(inline_comments, labels, label_names, subroutines):inline_comments:{addr -> [(text, full_line), ...]}labels:{addr -> (name, full_line)}— last label winslabel_names: set of every label name encounteredsubroutines:{addr -> full_statement_text}(may be multi-line)
Raises
DriverParseErrorwhen the script is not valid Python.
- fantasm.api.backfill.propose_propagations(source_text, target_text, confidence_map, *, threshold=5)[source]
Build the list of annotations worth propagating from source to target.
For each annotation in
source_text:drop it if its address has no mapping in
confidence_map(or maps belowthreshold);drop it if the target address already carries an annotation of the same kind (deduplication is conservative — exact text match for comments, any-label-at-addr for labels, any-sub-at-addr for subroutines);
drop a label if its name is already used elsewhere in the target driver script.
The remaining annotations are returned as
PropagationCandidaterecords, alongside a tally of skip reasons for the user-facing report.
- fantasm.api.backfill.propose_translations(project, *, source_version, target_version, source_driver, target_driver=None, threshold=5, cpu=None, rom_base=None, workspace_ranges=((0, 256),), high_confidence=1000)[source]
High-level translation proposal pipeline for generator scripts.
Glues together the version graph, opcode-level confidence-map composition, and annotation parsing — the same pipeline
fantasm backfillruns on the CLI side, exposed as a public API so cross-version baseline generators (e.g. anacorn-nfs/scripts/generate_421_variant_1.py) can use the same anchored translation engine instead of reaching forfantasm.api.blockmatch.build_full_address_map()and inheriting its weaker contract (see issue #10).Anchoring contract: every emitted candidate is anchored in a composed shared block of length ≥
thresholdopcodes across the full source-to-target path through the version graph (the weakest hop is the binding constraint). Source addresses outside such a block produce no candidate — the consumer should route those to# UNMAPPED:.- Parameters:
project (ProjectContext) – The project context. Use
fantasm.config.resolve_project_context()to obtain one (resolve_project_context(None)walks up from the current working directory).source_version (str) – Version id whose annotations to translate.
target_version (str) – Version id to translate them to.
source_driver (Path) – Path to the source driver script.
target_driver (Path | None) – Optional path to an existing target driver. When given, candidates whose target address already carries an annotation of the same kind are skipped (the same dedup rule the CLI applies). When omitted (typical for “fresh baseline” use), all anchored translations are proposed.
threshold (int) – Minimum composed block length for a candidate. Default 5 matches
fantasm backfill’s CLI default.cpu (str | None) – Override for the project’s
[binary]/[rom] cpu.rom_base (int | None) – Override for the project’s
[binary]/[rom] base_address.workspace_ranges (Iterable[tuple[int, int]]) – Identity-mapped workspace regions.
high_confidence (int) – Confidence stamped on workspace mappings.
- Raises:
FileNotFoundError –
source_driver(or, when given,target_driver) does not exist; or any version’s ROM bytes are missing.VersionGraphError – The version graph could not be loaded.
VersionNotInGraphError – A version isn’t in the graph.
NoPathError – No path between the two versions.
- Return type:
- fantasm.api.backfill.subroutine_name(full_text)[source]
Return the name argument of a
subroutine()call’s source text.
- fantasm.api.backfill.translate_address_in_text(text, old_addr, new_addr)[source]
Replace
0xOLDADDRwith0xNEWADDReverywhere intext.
Driver-script annotation parsing
Parse annotation calls out of disassembly driver scripts.
Driver scripts annotate a binary with comment, label and
subroutine calls. Two dialects are in use:
py8dis calls them as bare functions, flagging inline comments with
inline=True:comment(0x8430, "Transfer to X", inline=True) subroutine(0x8430, "name", hook=None)
dasmos calls them as methods on a
Disassembler(any receiver name is accepted), with comment placement given by anAlignenum. Long calls are typically split one argument per line:d.comment(0x8430, "Transfer to X", align=Align.INLINE) d.subroutine( 0x8430, "name", title="...", )
Rather than matching either surface syntax with regular expressions,
the script is parsed with ast and every call to one of the
three annotation functions is inspected. An annotation is reported
only when its address argument is a literal integer (or a constant
+/- expression of literals, such as 0x8000 + 3); calls
whose address is computed at run time — typically inside a loop
over a table — are skipped, as no static tool can place them.
- class fantasm.api.driver_annotations.DriverAnnotation(kind, address, line_number, end_line_number, name, text, inline, is_entry_point, source)[source]
Bases:
objectOne annotation call found in a driver script.
nameis the label or subroutine name,textthe comment text; each isNonewhen absent or not a string literal.inlineis only meaningful for comments.is_entry_pointis only meaningful for subroutines.sourceholds the exact source lines spanned by the call (linesline_numbertoend_line_numberinclusive, 1-based).- Parameters:
- exception fantasm.api.driver_annotations.DriverParseError[source]
Bases:
ValueErrorThe driver script is not valid Python and can’t be parsed.
- fantasm.api.driver_annotations.parse_driver_annotations(driver_text)[source]
Return every literal-addressed annotation call in
driver_text.Results are in source order. Raises
DriverParseErrorwhen the script has a syntax error.- Parameters:
driver_text (str)
- Return type:
Driver-script address sorting
Sort top-level annotation calls in dasmos / py8dis driver scripts by address.
A driver script is a Python file that drives a tracing
disassembler library through annotation calls — d.label(0x9000,
"foo"), d.comment(0xA000, "..."), etc. As these files grow
they tend to drift out of address order, so reading the file
top-to-bottom doesn’t follow the address space. This module
rewrites the file so the output is the unique address-ordered
permutation that satisfies the script’s real ordering constraints.
The model is a partial-order topological sort. Each top-level
statement is a node; ordering constraints (Python execution
prerequisites, add_move translation requirements, the
disassemble → render edge) are explicit edges. Where topology
leaves multiple statements free, the tie is broken by primary
address. The result is a single canonical ordering with no
per-anchor heuristics.
## Hard edges
import/from/ module-level assignments preceded = … Disassembler.create(…).d = … Disassembler.create(…)precedes everyd.X(…)call.d.load(…)precedes every otherd.X(…)call.d.add_move(dest, src, length)precedes every annotation whose primary address falls inside[dest, dest+length)or[src, src+length).Every annotation, setup, env, hook, hint, constant, and indented block precedes
ir = d.disassemble().ir = d.disassemble()precedes every subsequent statement (renders, file IO, prints).Module-level name dependencies. For every module-scope name, a statement that writes it precedes later statements that read or write it, and a statement that reads it precedes a later statement that writes it (RAW / WAR / WAW). Read-after-read is free, so the
dreceiver — written once, read by everyd.X(…)call — never orders annotations relative to each other. This edge is what keeps an accumulator setup (addr = 0x8579) ahead of thefor/whileloop that consumes and reassignsaddr, and keeps adefahead of the statement that calls it.
## Computed setup-assigns
A module-level assignment is normally setup (keyed -inf,
pinned to the top). But an assignment whose value reads a name
produced only by a non-setup statement — e.g. ENTRIES =
_parse(…) calling a module-level def — is demoted to a
soft anchor so the name edge from the def can order it after
its definition. Pure-literal / constant assigns (addr = 0x8579,
_table = [...], TOP = BASE + 0x100) read no such name and
stay at the top.
## Unsortable inputs
Name edges can, in pathological inputs, close a cycle with the
setup / render edges. When the DAG is cyclic there is no valid
canonical order, so the sort is a no-op: sort_driver_text
returns the input unchanged and is_sorted reports True.
This guarantees the sort never changes whether the driver runs to
completion.
## Tiebreak (sort key for free choices)
Setup (
import, module assigns,Disassembler.create,d.load):-inf— topology pins them to the top.d.use_environment(…):-1— clusters ahead of the address-keyed run.d.add_move(dest, src, length):min(dest, src).d.hook_subroutine(addr, …)/d.format_hint(addr, …):addr.d.constant(value, name)with literal int value:value.d.label/comment/subroutine/banner/entry/byte/word/string/expr/expr_label/rts_code_ptrwith literal first arg: the literal value.Indented block (
for/if/while/with/def) with a derivable first-iteration address: that address.Indented block or unrecognised top-level call with no derivable address: a soft anchor — sort key inherited from the nearest preceding addressable statement.
Render block (
ir = d.disassemble()and after):+inf, again topology-pinned.Final tiebreak: original line number.
Indented blocks move as one atomic unit (header + body together).
The block’s address is derived (in order of preference) from the
first literal int in the loop iterable, then from the first
literal-int positional arg of any sortable d.X(…) call in the
body. If neither yields a literal, the block becomes a soft
anchor — pinned to its source neighbourhood by edges to the
immediately surrounding statements — and the rest of the file
sorts around it.
## Byte-identity contract
Statement text is moved verbatim. We never re-emit numeric
literals, never reformat keyword args, never normalise quoting.
ast.parse is used to understand structure; the original
source-text bytes are sliced by line range and concatenated in
the new order. Hex literals stay hex.
- class fantasm.api.driver_sort.Unit(leading_lines, statement_lines, trailing_lines, kind, address, original_index)[source]
Bases:
objectOne source-level unit: leading filler + one statement + trailing filler.
leading_linesandtrailing_linesare blank or comment-only lines that travel with the statement when it moves.statement_linesare the lines of one top-level Python statement (possibly multi-line or a whole indented block).kinddescribes the statement’s role for the sort.addressis the unit’s primary sort address (Nonefor setup / render / soft-anchor units).original_indexis the unit’s position in the original file (stable-sort tiebreaker).- Parameters:
- fantasm.api.driver_sort.build_units(text)[source]
Parse
textinto a list ofUnitrecords, in source order.
- fantasm.api.driver_sort.is_sorted(text)[source]
Return True if
textis already in canonical sorted order.Equivalent to
sort_driver_text(text) == textbut slightly cheaper (skips the emit pass when the topology already matches).
Version graph (DAG) and chain composition
Version DAG and per-version memory-region resolution.
Loads [memory] and [[versions.entry]] from a project’s
fantasm.toml and exposes a graph object that path-finds between
versions and computes effective memory regions per version (project
defaults plus the per-version override, plus reloc destinations
auto-merged).
See docs/configuration.md for the full schema. Quick recap of the
relevant TOML:
[memory]
regions = [{ start = 0x0016, end = 0x0076 }, ...]
external_regions = [{ start = 0xFC00, end = 0xFFFF }, ...]
[[versions.entry]]
id = "3.34"
parents = []
reloc_blocks = [{ source = 0x9307, dest = 0x0016, length = 0x61 }, ...]
[[versions.entry]]
id = "page7-variant"
parents = ["3.34B"]
memory.regions = [...] # complete replacement of project regions
reloc_blocks = [...]
- class fantasm.api.version_graph.Edge(parent_id, child_id, walked_forward)[source]
Bases:
objectAn edge in the version DAG.
parent_idis always the older endpoint,child_idalways the newer.walked_forwardisTruewhen a path traverses the edge in the canonical direction (parent → child) andFalsewhen traversing it backwards (child → parent), which callers likebackfilluse to decide whether to invert the per-hop confidence map.
- exception fantasm.api.version_graph.NoPathError(source_id, target_id)[source]
Bases:
VersionGraphErrorRaised when no path exists between two versions.
- class fantasm.api.version_graph.Region(start, end)[source]
Bases:
objectAn inclusive
[start, end]address range.
- class fantasm.api.version_graph.RelocBlock(source, dest, length)[source]
Bases:
objectA
move()-style relocation: source bytes → runtime destination.
- class fantasm.api.version_graph.Version(id, parents, reloc_blocks, explicit_regions, notes=None, description=None, release_date=None, source=None)[source]
Bases:
objectA node in the version graph.
explicit_regionsisNoneif the entry didn’t overridememory.regions(in which case the graph’s project defaults apply).Free-form metadata fields (
notes,description,release_date,source) are surfaced for tooling and documentation but don’t affect any analysis.release_dateis expected to be an ISO-8601 date string but isn’t parsed.- Parameters:
- reloc_blocks: tuple[RelocBlock, ...]
- class fantasm.api.version_graph.VersionGraph(versions, project_regions=(), project_external_regions=())[source]
Bases:
objectA version DAG with per-version effective-region resolution.
- Parameters:
- find_path(source_id, target_id)[source]
Return the shortest path of edges from
source_idtotarget_id.Walks the undirected projection of the DAG (each edge can be traversed in either direction). Same-source same-target returns an empty list. Raises
VersionNotInGraphErrorfor unknown endpoints andNoPathErrorwhen the endpoints are in disconnected components.
- effective_regions(version_id)[source]
Effective non-ROM regions for
version_id.The version’s explicit
memory.regionsoverride (if set) otherwise the project default, plus reloc-block destinations from this version’sreloc_blocks. Overlapping or adjacent regions are merged; the result is sorted by start address.
- effective_external_regions(version_id)[source]
Effective hardware-mapped regions for
version_id.Project-level only — there’s no per-version override (the BBC’s hardware memory map is invariant within a project). Validates that
version_idexists in the graph.
- reloc_pairs_for_edge(edge)[source]
Derive
(src_parent, src_child, dest, length)tuples foredge.Reloc blocks are matched across the edge’s two endpoints by
(dest, length). When either side has multiple blocks with the same key (rare), they are paired off positionally in the order they appear in each version’sreloc_blockslist. Blocks that exist on only one side are silently dropped — a block whosedest/lengthdiffers across versions can’t contribute to opcode matching anyway.Tuples are returned sorted by parent source address for determinism. The order
(src_parent, src_child, dest, length)is independent of the edge’s traversal direction; callers walkingchild -> parentswap or invert the resulting hop map themselves.
- exception fantasm.api.version_graph.VersionGraphCycleError[source]
Bases:
VersionGraphErrorRaised when the parents declarations imply a cycle.
- exception fantasm.api.version_graph.VersionGraphError[source]
Bases:
LookupErrorBase class for version-graph errors.
- exception fantasm.api.version_graph.VersionNotInGraphError[source]
Bases:
VersionGraphErrorRaised when a referenced version isn’t declared in the graph.
- fantasm.api.version_graph.compose_chained_map(graph, source_id, target_id, rom_loader, *, rom_base=32768, cpu='6502', workspace_ranges=((0, 256),), high_confidence=1000)[source]
Compose a confidence map from
source_idtotarget_id.Walks the shortest path through
graphbetween the two versions, builds a per-hop confidence map for each edge using ROM bytes obtained fromrom_loader, and composes them end-to-end with min-confidence (weakest link).Per-hop maps are always built canonically (parent → child) and inverted on the fly when the path traverses an edge backward, so the chain composition is direction-aware.
Returns
{source_addr: (target_addr, confidence)}covering addresses where a path exists; addresses that don’t compose through every hop are simply absent. Same-version returns{}.
- fantasm.api.version_graph.load_version_graph(project_context)[source]
Build a
VersionGraphfromproject_context.config.Reads
[memory]and[[versions.entry]]. Returns an empty graph (no versions, empty regions) when the config lacks either section — this is the right behaviour for a project that doesn’t use the version-DAG features.Raises
RuntimeErrorif the project root isn’t resolved.- Parameters:
project_context (ProjectContext)
- Return type:
Assembly-source extraction
Extract sections of a disassembly .asm file by address or label.
Pure helpers for indexing assembly lines and resolving address/label targets. CLI-facing wrappers (file open + print) live separately.
Refactors relative to the sibling copies:
find_line_for_target()no longer takes the unusedlinesparameter and emits ambiguous-match warnings viawarningsinstead of printing to stderr.extract_section()returns the captured slice (start line and text) as a pure function; the file-basedextractentry was a CLI-style mixed-IO helper and has been split out so the rest of the module is easier to test and re-use.
- class fantasm.api.asm_extract.AsmSection(start_line, end_line, lines)[source]
Bases:
objectA slice of assembly lines extracted by
extract_section().start_lineandend_lineare 0-based;end_lineis exclusive.linesis the captured text without line numbers.
- fantasm.api.asm_extract.build_index(asm_lines)[source]
Build
(addr_to_line, label_to_line)indices.For each line, looks for a
;NNNN:comment marker (the disassembler’s address tag) and a:NNNN[...]runtime-address annotation; both are recorded inaddr_to_line. Lines starting with.labelare recorded inlabel_to_line. The first occurrence wins for any given key.
- fantasm.api.asm_extract.extract_section(asm_lines, start_target, end_target=None, default_window=40)[source]
Extract a section of assembly lines spanning two targets.
The section starts at the line resolved from
start_targetand ends at the line resolved fromend_target(inclusive). When noend_targetis given,default_windowlines are taken.The start is backed up across preceding blank, comment-only, and label-only lines so the captured section includes the natural “header” of the routine or block.
Raises
LookupErrorif either target cannot be resolved.
- fantasm.api.asm_extract.find_line_for_target(target, addr_to_line, label_to_line)[source]
Resolve a textual target to a 0-based line number.
Resolution order:
If
targetparses as a hex address, return the line that maps to that address — or, failing an exact match, the line for the nearest indexed address less than or equal totarget.If
targetmatches a label exactly, return that label’s line.If
targetis a substring of exactly one label, return that label’s line. If it matches multiple labels, emits aUserWarninglisting the matches and returns the lexically first match’s line so callers continue to make progress.
Returns
Nonewhen nothing matches.
Project bootstrap (fantasm project init / add / list)
Project scaffolding.
Helpers to initialise a new fantasm project and to add or list its targets. Two layouts are supported:
versions (the default) — write
fantasm.tomland create the standardversions/directory, one{prefix}-{id}/directory per version;explicit — adopt an existing project as it stands: write
fantasm.tomlwith a[[targets]]entry naming the files where they already are, and create nothing else.
Pure helpers; the fantasm project Click sub-commands wrap these.
- class fantasm.api.project.ProjectInitConfig(name, prefixes, versions_dirname='versions', cpu='6502', layout='versions', targets=())[source]
Bases:
objectConfiguration for initialising a fantasm project.
prefixesmust contain at least one entry. Each prefix matches the regex[A-Za-z0-9_-]+(no spaces, no slashes).- Parameters:
- targets: tuple[TargetSpec, ...] = ()
- class fantasm.api.project.TargetSpec(id, binary, driver, asm, json, load_address=None, exec_address=None, metadata=None, inf_source=None)[source]
Bases:
objectA
[[targets]]entry to write intofantasm.toml.Paths are as they should appear in the file: relative to the project root (or absolute).
inf_sourcenames the.inffile the addresses were read from, recorded as a comment.- Parameters:
- class fantasm.api.project.VersionInfo(prefix, version_id, dirpath)[source]
Bases:
objectA discovered version directory.
- fantasm.api.project.add_version(versions_dirpath, version_id, prefix, binary_dirname='rom')[source]
Create a new version directory
{prefix}-{version_id}/.Creates the binary subdirectory (
binary_dirname, defaultrom/per the historical ROM convention) andoutput/(each with a.gitkeep). Returns the new version directory path. RaisesFileExistsErrorif it already exists,ValueErrorfor an invalid prefix.
- fantasm.api.project.append_target(fantasm_toml_filepath, spec)[source]
Append a
[[targets]]entry to an existingfantasm.toml.Raises
ValueErrorif a target with the same id is already declared, or if the result would not parse (for example because the file declarestargetsas an inline array); the file is left unchanged in that case.- Parameters:
fantasm_toml_filepath (Path)
spec (TargetSpec)
- Return type:
None
- fantasm.api.project.binary_addresses(binary_filepath, load_address=None)[source]
Return
(load, exec, inf_filepath)for a binary.With
load_addressgiven, returns it (and no exec address or.inf). Otherwise reads the binary’s.infsidecar. RaisesInfParseError, with a hint to give the address explicitly, when there’s no sidecar or it can’t be parsed.
- fantasm.api.project.default_target_paths(target_id)[source]
Default driver / asm / json paths for a new explicit target.
- fantasm.api.project.init_project(root_dirpath, config, *, force=False)[source]
Initialise a fantasm project at
root_dirpath.Writes
fantasm.toml. For theversionslayout, also ensures the versions directory exists (creating it with a.gitkeepif it didn’t); theexplicitlayout creates nothing else. Returns thefantasm.tomlpath.Raises
FileExistsErroriffantasm.tomlalready exists andforce=False. Other files inroot_dirpathare left untouched, so it is safe to run inside an existing repository.- Parameters:
root_dirpath (Path)
config (ProjectInitConfig)
force (bool)
- Return type:
- fantasm.api.project.list_versions(versions_dirpath, prefixes)[source]
List version directories matching any of the configured prefixes.
Only entries whose name is exactly
{prefix}-{version_id}for one of the prefixes are reported. Sorted by directory name.- Parameters:
- Return type:
- fantasm.api.project.parse_binary_option(text)[source]
Split a
PATH[@ADDR]option value.ADDRis hexadecimal, with an optional&,$or0xprefix. A trailing@...that isn’t an address is left as part of the path. RaisesValueErrorfor an address above&FFFF.
- fantasm.api.project.project_relative(path, root_dirpath)[source]
pathrelative toroot_dirpath(POSIX form) when inside it, else absolute.
- fantasm.api.project.render_fantasm_toml(config)[source]
Render the
fantasm.tomlcontent for a new project.Output is hand-formatted (with comments) rather than serialised by a TOML library, so the file is still readable when a human edits it later.
- Parameters:
config (ProjectInitConfig)
- Return type:
- fantasm.api.project.render_target_entry(spec)[source]
Render one
[[targets]]block (ending in a newline).- Parameters:
spec (TargetSpec)
- Return type: