API reference

The entire public API is re-exported from the top-level dasmos package, so from dasmos import Disassembler, Align is the intended import path. The sub-module documentation below is for readers who want the full picture.

Top-level package

dasmos — an extensible tracing disassembler.

The top-level package re-exports the public API. Consumers should import from dasmos rather than reaching into sub-modules.

class dasmos.Align(*values)[source]

Where a comment or annotation appears relative to a line.

Most driver-script calls use BEFORE_LABEL (the default) or INLINE; the others are rarer.

AFTER_LABEL = 'after_label'

Standalone, between the line’s labels and the code.

AFTER_LINE = 'after_line'

Standalone, just below the code line.

BEFORE_LABEL = 'before_label'

Standalone, above the line’s labels and code.

BEFORE_LINE = 'before_line'

Standalone, just above the code (after any labels).

INLINE = 'inline'

Trailing on the same line as the code, after the operands.

class dasmos.Annotation(text: str, align: Align = Align.BEFORE_LABEL, priority: int | None = None)[source]

A raw assembler-source string at a specific address.

Unlike Comment, the text is emitted verbatim — no comment prefix is added. Use this for emitting assembler directives the driver wants inline (e.g. an explicit equb).

class dasmos.AnnotationStore[source]

Per-disassembly registry of Comment and Annotation entries keyed by binary address.

A single address can carry multiple entries; insertion order is preserved. The renderer is responsible for iterating entries in the right order and at the right rendering position.

add(binary_addr, entry: Comment | Annotation | Banner | DecodedAnnotation) → None[source]

Append entry at binary_addr.

Issues a UserWarning when the (address, alignment, type) triple already has at least one prior entry — a second comment at the same place is almost always a driver-script bug (a copy-paste, or an attempt to override an earlier comment that ends up appending instead). Both entries are still stored; the warning lets the user notice and decide.

get(binary_addr) → list[Comment | Annotation | Banner | DecodedAnnotation][source]

Return a list of entries at binary_addr (insertion order).

get_for_align(binary_addr, align: Align) → list[Comment | Annotation | Banner | DecodedAnnotation][source]

Return entries at binary_addr whose alignment matches.

is_auto_suppressed_at(binary_addr) → bool[source]

Return True if any annotation at binary_addr carries the suppresses_auto flag.

Env analysers and setup() methods consult this before attaching their auto-generated annotation, giving driver scripts a uniform way to crowd out env-supplied auto-comments when the driver already documents the address authoritatively. Align-agnostic — a driver Comment with suppresses_auto=True in any position blocks env auto-attach at that address.

class dasmos.Banner(title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, align: Align = Align.BEFORE_LABEL, auto_generated: bool = False, priority: int | None = None)[source]

A multi-line decorated comment block at a specific address.

Renders as a renderer-defined separator (Beebasm uses a row of asterisks) followed by the title and description text, providing strong visual separation in the disassembled listing.

The two main use cases:

  • Subroutine headers: d.subroutine(addr, name, title=..., description=...) registers the entry point AND attaches a Banner.

  • Data-region labels: d.banner(addr, title=..., description=...) adds a Banner without registering an entry point.

class dasmos.Comment(text: str, align: Align = Align.BEFORE_LABEL, word_wrap: bool = True, indent: int = 0, auto_generated: bool = False, suppresses_auto: bool = False, priority: int | None = None)[source]

A user comment at a specific address.

The text is stored verbatim; the renderer applies the assembler’s comment prefix (; for Beebasm) at render time.

class dasmos.Cpu(name: str, **kwargs)[source]

Base class for CPU (processor) plug-ins.

Concrete subclasses model a specific processor — its instruction encoding, addressing modes, register set, and the semantics the tracing disassembler needs to follow control flow.

abstract property address_space_size: int

The number of distinct addressable bytes for this CPU.

0x10000 for 16-bit address spaces (6502, 6809, Z80); larger for 32-bit CPUs to come (ARM, 32016). The orchestrator uses this when sizing its MemoryImage and MoveManager.

initial_state() → CpuState | None[source]

Return a fresh CpuState for this CPU, or None to opt out of state tracking.

The Disassembler calls this once at the start of its post- trace state-computation pass. Override in CPU plug-ins that want their state tracked.

abstractmethod opcodes() → dict[int, Opcode][source]

The CPU’s instruction table.

Maps opcode byte (0x00–0xFF) to an Opcode. Bytes not in the dict are treated as undefined by the trace engine — for the NMOS 6502 plug-in the undocumented opcodes are deliberately omitted.

update_state(state, opcode: Opcode, addr: int, memory) → None[source]

Advance state to reflect executing opcode at the binary address addr. Mutates state in place.

memory is the disassembler’s MemoryImage so the update can read the operand byte(s) when relevant (e.g. LDA #imm reads the immediate from addr + 1).

No-op default — CPU plug-ins override.

exception dasmos.CpuExtensionError[source]

Exception raised when a CPU extension cannot be loaded.

exception dasmos.DasmosError[source]

Base class for all exceptions raised by the dasmos package.

class dasmos.Disassembler(cpu: Cpu, *, environments: tuple = (), auto_labels_enabled: bool = True, auto_label_data_prefix: str = 'data_', auto_label_code_prefix: str = 'code_', auto_label_subroutine_prefix: str = 'sub_', auto_label_loop_prefix: str = 'loop_', auto_label_loop_limit: int = 256, auto_label_return_prefix: str | None = 'return_', string_detection_min_length: int | None = 3)[source]

The orchestration class.

Owns one instance of each manager class; delegates the flat driver-script API to those managers; produces an IntermediateRepresentation via disassemble().

add_move(dest_runtime_addr, src_binary_addr, length: int, *, name: str | None = None) → Move[source]

Register a relocation. Returns the Move handle.

The Move is itself a context manager — with move: ... scopes annotations under it. Pass name= to give the move a stable identity in diagnostics, JSON output, and cross-references.

banner(runtime_addr, *, title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, align: Align = Align.BEFORE_LABEL, auto_generated: bool = False, move: Move | None = None) → None[source]

Attach a multi-line decorated comment block at runtime_addr — visual separation only; does not register an entry point.

Use this for data regions you want to mark visually without the trace engine treating the address as code. See docs/design/commands-sweep-memo.md C2/C3 for the rationale.

auto_generated marks this banner as env-attached metadata rather than user-authored content (parallel to the same kwarg on comment()). Suppresses the duplicate-annotation warning when an env stacks a banner alongside a driver- supplied one at the same address.

For a subroutine that needs both visual separation AND entry-point registration, call subroutine() with the same title / description kwargs.

byte(runtime_addr, length: int = 1, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as raw bytes.

Pass override=True to clear any conflicting classification in the range first (see _override_clear()); otherwise an overlap raises ClassificationError.

char_literal(runtime_addr, *, move: Move | None = None)[source]

Sugar for format_hint(runtime_addr, FormatHint.CHAR).

Declares that the operand byte at runtime_addr is intended as an ASCII character literal. Renderers express this in their target assembler’s grammar — beebasm emits ASC("c") or 'c' (per the renderer’s char_literal_style); a future ACME or ca65 renderer would emit 'c'; the JSON renderer surfaces the hint in its structured output.

code_ptr(runtime_addr_lo, runtime_addr_hi: int | None = None, *, offset: int = 0, label_name: str | None = None) → None[source]

Mark two bytes of data as the address of a subroutine and seed the trace from the computed target.

Reads the bytes at runtime_addr_lo (low half) and runtime_addr_hi (high half — defaults to runtime_addr_lo + 1 for the common adjacent-bytes case), computes the target as (hi << 8) | lo) + offset, registers entry(target, name=label_name), and sets per-byte expression overrides so the source bytes render symbolically (equw <label> or equb < (<label>) / equb > (<label>)).

Used for jump tables where the low and high bytes of subroutine addresses are stored in separate parallel tables (see rts_code_ptr() for the RTS-pop-and-INC variant).

comment(runtime_addr, text: str, *, align: Align = Align.BEFORE_LABEL, word_wrap: bool = True, indent: int = 0, auto_generated: bool = False, suppresses_auto: bool = False, move: Move | None = None) → None[source]

Attach a comment at runtime_addr.

align controls where the comment is rendered relative to the code line — BEFORE_LABEL (the default) puts it on its own line above any labels at the address; INLINE appends it to the code line itself; the other positions (AFTER_LABEL, BEFORE_LINE, AFTER_LINE) cover less-common arrangements.

auto_generated marks this comment as env-attached metadata rather than user-authored content. Suppresses the duplicate- comment warning when stacking with a driver-supplied comment at the same address (the warning targets driver authoring bugs, not env-driven layering). Envs attaching address metadata in setup() (e.g. "Sideways ROM header" at &8000) should set this.

suppresses_auto marks this comment as authoritative: env analysers and setup() methods skip attaching their auto- generated annotation at this address when any annotation present has this flag set. Use it when the driver already documents the address authoritatively and the env-supplied text would be redundant. Order matters for env setup: attach the suppressing comment before use_environment(...) so the env’s setup sees the flag.

The runtime address is resolved to a binary address via the active-move stack on the move manager, or via the explicit move_id if given.

constant(value: int, name: str, comment: str | None = None) → None[source]

Register a named value (a constant).

Conceptually distinct from a label: constants are arbitrary named values (magic numbers, OSBYTE call numbers, hardware register addresses) — not necessarily addresses of code or data the disassembler will reach. The structured-output renderers surface them as a dedicated constants section.

Constants are tracked SEPARATELY from labels. Mixing them would cause the hook-registered OSBYTE constants (e.g. osbyte_clear_escape = &7c) to pollute operand resolution at zero-page address &7c: any unrelated code reading from &7c would suddenly render as lda osbyte_clear_escape — a false positive.

The BeebasmRenderer emits constants as equates in their own block alongside the label-equate table.

If name matches a deprecated alias registered by an active environment (see register_deprecated_constant_name()), a DeprecationWarning is emitted pointing at the new replacement name. The constant is still recorded under the old name so the rendered output keeps working — the warning is the only signal that drivers should update.

property constants: list[Constant]

Named values registered via constant(). The Constant records here also appear as optional labels in labels (for asm equate emission).

classmethod create(cpu: str | Cpu, **kwargs: Any) → Disassembler[source]

Construct a Disassembler with a string-named CPU plug-in or with an explicit instance.

Strings are resolved via Stevedore from the dasmos.cpu entry-point namespace. Keyword arguments are partitioned: any kwarg whose name matches a parameter of __init__() (e.g. auto_labels_enabled, auto_label_subroutine_prefix) is forwarded to the Disassembler; the rest go to the CPU plug-in’s constructor. Instances are used directly.

define_macro(name, params, body, *, emit: str = 'byte')[source]

Define a macro computing a value from params and return a callable that builds invocations of it.

params is a list of parameter names; inside body refer to them with dasmos.expr.param(). emit is the data kind the value is emitted as ("byte" / "word") — used by backends without value-functions (beebasm) to wrap the body in the right data directive. The value is rendered natively per backend: a 64tass .sfunction used inline in .byte pack("LDA"), or a beebasm MACRO emitting the byte, invoked pack "LDA" per line.

Example:

m = param("mnem")
pack_lo = d.define_macro("pack_lo", ["mnem"],
    (((m[0] & 0x1F) << 10) + ((m[1] & 0x1F) << 5)
     + (m[2] & 0x1F)) & 0xFF)
d.expr(addr, pack_lo("LDA"))
disassemble() → IntermediateRepresentation[source]

Run the trace loop, classify leftovers, and return an IR.

One-shot: calling twice raises DisassemblerError.

Pipeline:

  1. Trace — follow control flow from every registered entry point, classifying each instruction.

  2. Leftover classification — bytes loaded but not reached by trace (and not pre-classified by the user) become Byte 1-byte items.

  3. Reference analysis — walk every classified opcode and record use sites against any label at the operand target.

  4. Auto-label generation (when auto_labels_enabled) — synthesise names for referenced addresses without explicit names. Names land in the LabelManager with is_autogenerated=True so renderers and other consumers see them as ordinary (optional) labels via the standard label-resolution paths.

entry(runtime_addr, name: str | None = None, *, override: bool = False, **label_kwargs)[source]

Register a code entry point at runtime_addr.

The trace loop will start here. If name is given, also defines a label at the same runtime address (any extra kwargs are forwarded to label()).

The runtime address is resolved to a binary address via the active-move stack on the move manager — entry points outside any move identity-map.

Precedence over eager classifications. Environments (and earlier driver calls) may classify bytes before this call — e.g. acorn_sideways_rom classifying a non-JMP entry slot as data. The trace cannot reclassify already-classified bytes, so a plain entry() on such an address would render the bytes as data and silently do nothing. To make that footgun visible:

  • override=False (the default): if any byte at the target is already classified, emit a UserWarning — the entry will not change how those bytes render.

  • override=True: clear the entire conflicting classification span at the target first, so the trace can claim the bytes as code. An explicit driver call made after use_environment() thus wins over the environment’s classification. Only the rendering changes — the bytes are untouched, so the round-trip oracle still holds.

expr(runtime_addr, expression: Expr | str, *, move: Move | None = None)[source]

Override the rendered operand at runtime_addr with the given expression.

expression is preferably an assembler-neutral Expr built with the DSL:

from dasmos.expr import ref, lo
d.expr(0x8001, ref(num_lives) + 1)
d.expr(0x8020, lo(ref(dispatch_lo) - 1))

Each renderer then emits its own syntax (&/$ hex, AND/&, correct parenthesisation). A plain str is still accepted for backward compatibility — it is wrapped as a Raw node and emitted verbatim (via the renderer’s dialect adapter), exactly as before.

runtime_addr is the runtime address of the operand byte — typically one past the opcode for a single-byte-opcode CPU like the 6502. For example, an LDA #$03 instruction at &8000 has its operand byte at &8001; calling d.expr(0x8001, ref(num_lives) + 1) makes the renderer emit lda #num_lives + 1 for that instruction.

With a raw string the driver is responsible for ensuring any names it references are valid in the rendered output (typically by also defining them via label() or an external constant the assembler resolves).

For renderer-agnostic semantic markers (e.g. “render this byte as an ASCII character / decimal / hex / binary”), use format_hint() or its sugars (char_literal()) instead — those declare intent and let each renderer choose its own syntax.

expr_label(runtime_addr, expression: str, **kwargs)[source]

Register an expression to be used as a label reference.

property file_header: FileHeader | None

The provenance header, or None (see set_file_header()).

fill(runtime_addr, length: int, value: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark a run of length identical bytes at runtime_addr.

value is the byte the fill should expand to. If omitted, it’s inferred from the loaded memory (the byte at runtime_addr) — the natural shape for runs of the same value the binary already contains. If value is supplied AND disagrees with the loaded byte, raises DisassemblerError.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

format_hint(runtime_addr, hint: FormatHint, *, move: Move | None = None)[source]

Mark the operand byte at runtime_addr with a renderer- agnostic format hint — a semantic declaration of how the byte should be interpreted (ASCII character, decimal, hex, binary, octal). Each renderer translates the hint to its assembler- specific syntax.

Hints are weaker than expr(): expr substitutes a concrete text expression; format_hint declares intent and leaves the syntax to the renderer. When both are registered at the same address, expr wins (the user’s explicit text is more specific than a category).

See dasmos.core.format_hint.FormatHint for the full set of hints. char_literal() is a sugar for the CHAR case.

hook_subroutine(runtime_addr, name: str, hook) → None[source]

Register a callable the trace fires when it processes a JSR whose operand resolves to runtime_addr. Used for the inline-string idiom and similar non-standard return conventions.

Hook signature:

def hook(disassembler, jsr_binary_addr) -> int

The hook returns the binary address where execution continues after the JSR. See dasmos.hooks for the bundled hooks (stringhi_hook, stringz_hook, …) and the protocol details.

Also registers name as an optional label at runtime_addr so JSR operands resolve to the symbolic name in the rendered output.

include_binary(runtime_addr, length: int, path: str, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as an external binary include.

Classifies the span as data — the bytes are accounted for and the tracer won’t enter it, exactly like byte() — but tags it so a renderer emits an include directive (beebasm incbin "<path>"; 64tass .binary "<path>") instead of a wall of equb lines. Use it for a region whose source of truth lives outside the disassembly and is regenerated at build time (e.g. detokenised BASIC re-encoded to bytes).

The span still owns its loaded bytes, so nothing is left unaccounted for. Those bytes are the canonical payload: IntermediateRepresentation.write_included_binaries() writes them to path next to the listing, so the assembler reads back exactly the bytes dasmos accounted for and the round-trip stays self-consistent. A project whose own generator must reproduce the region can diff its output against that canonical payload.

path is the include path as it should appear in the directive (typically a bare filename beside the listing). Pass override=True to clear a conflicting classification in the range first; otherwise an overlap raises ClassificationError.

index_base(runtime_addr, name: str, **kwargs)[source]

Define a label for an address used only as an indexing base — the operand of lda base,X / sta base,Y etc., where the byte actually touched is base + register and base itself is never read or written.

Behaves like optional_label() (the name is emitted only if referenced, so the base,X operand can resolve) but asserts the base (b) access flag on the label. That keeps any description= / group= / length= the author supplies — so the base is still documented — and the address takes its place on the memory map carrying access including b (a base-only row: b with no r / w says the literal byte is never touched, only indexed through). This is the intention-revealing way to document a base with no in-image references; a base that is referenced derives b automatically from its reference kinds.

See docs/design/reference-kinds-memo.md.

index_region(anchor_addr, name: str, window: tuple[int, int], *, description: str | None = None, group: str | None = None, access: str | None = None, named_slots: bool = False) → IndexRegion[source]

Declare an indexing region: a window of addresses around an anchor whose in-window neighbours render relative to the anchor label (fsm_sector0-3,X) rather than as bare addresses.

anchor_addr gets the explicit, always-emitted label name (so the anchor±k arithmetic always resolves), plus any description / group / access — exactly as label() would, so the anchor takes its normal place on the memory map. window=(lo, hi) is the inclusive offset range the region owns (lo typically ≤ 0, hi ≥ 0); offset 0 is the anchor itself.

In-window addresses that carry their own explicit label keep it — a region names only the gaps. With named_slots=True each gap gets a distinct identifier (name_m<k> / name_p<k>) instead of the default name±k arithmetic form.

Windows must be disjoint; an overlap with an already-declared region raises RegionError. See docs/design/reference-kinds-memo.md.

property index_regions: list[IndexRegion]

Indexing regions registered via index_region(), in declaration order. Each anchor also appears in labels.

inkey_code(runtime_addr, *, move: Move | None = None)[source]

Sugar for format_hint(runtime_addr, FormatHint.INKEY).

Declares that the operand byte at runtime_addr is the negative-X form of an Acorn INKEY scan code (the byte loaded into X for OSBYTE &79 / OSBYTE &81 to scan for a specific key by its internal number). Renderers express the byte symbolically as (255 - inkey_key_<name>) EOR 128 when the value matches a named INKEY key in the BBC’s scan-code table; bytes that don’t match any named key fall back to a hex literal with a one-time warning.

Use this when the analyser can’t infer the pattern from a nearby JSR osbyte — for example when the scan code lives in a table of pre-computed values, or is loaded into X through a longer pipeline of stores and reads.

is_auto_suppressed_at(runtime_addr, *, move: Move | None = None) → bool[source]

Return True if any annotation at runtime_addr carries suppresses_auto=True.

Env setup() methods call this before attaching their own auto-comments so a driver-supplied authoritative comment can crowd out the env default. Wraps AnnotationStore.is_auto_suppressed_at() with the same runtime→binary address resolution as comment() and label(), so envs can pass the natural runtime address.

label(runtime_addr, name: str, **kwargs)[source]

Define a label. Delegates to add_label().

load(filepath, binary_addr, md5sum: str | None = None)[source]

Load a binary file at binary_addr. Delegates to load.

local_label(runtime_addr, name: str, start_addr, end_addr, **kwargs)[source]

Define a label scoped to [start_addr, end_addr).

property macros: dict[str, MacroDef]

Driver-defined macros, keyed by name (see define_macro()).

optional_label(runtime_addr, name: str, **kwargs)[source]

Define a label that the renderer emits only if it’s referenced.

Typical use: registering names for many out-of-range addresses (zero-page workspace, OS calls, hardware registers) without cluttering the rendered output with definitions for the ones that aren’t actually referenced. Required labels (the default label()) are always emitted; optional labels are emitted only when used.

program(*, load_addr: int | None = None, exec_addr: int | None = None, reload_addr: int | None = None) → None[source]

Declare load-and-run program metadata for the disassembly.

A DFS *RUN program loads at one address but may begin execution at another (its exec_addr), and the filesystem records a reload_addr distinct from where the bytes sit. These are properties of the program, not of any assembler syntax, so they are declared here and each renderer emits what it can express:

  • beebasm appends them to its save directive (save "NAME", start, end, exec, reload), producing a directly *RUN-able file whose header matches the original DFS metadata;

  • the JSON renderer surfaces them structurally;

  • a backend with no equivalent (e.g. 64tass, which saves via -o) warns and omits them, leaving the raw payload — and so fantasm verify — unaffected.

Every argument is optional; an omitted address is simply not recorded. reload_addr defaults to the loaded range’s start when a renderer needs it and it was not declared.

property program_info: ProgramInfo | None

Declared load-and-run program metadata, or None (see program()).

register_data_type(name: str, data_type: DataType) → None[source]

Register a custom DataType under name so typed_data() can resolve it by string.

Environments call this from setup to contribute domain types (e.g. bbc_basic_6502 registers "bbc_float5"), keeping platform-specific decode knowledge out of core — the same pattern as the env-supplied INKEY table. Last-write-wins.

register_deprecated_constant_name(old_name: str, new_name: str) → None[source]

Register a deprecated → replacement alias for a constant name. Subsequent constant() calls with old_name emit a DeprecationWarning citing new_name.

Environments use this to retire historical long-form names without breaking driver scripts that still spell them out.

rts_code_ptr(runtime_addr_lo, runtime_addr_hi: int | None = None, *, label_name: str | None = None) → None[source]

Marks two bytes of data as the address of a subroutine targeted via RTS-pop-then-INC (so the bytes contain target - 1). Equivalent to code_ptr() with offset=1.

set_file_header(title=None, description=None) → None[source]

Set the backend-agnostic provenance header emitted at the top of a listing (and in the JSON meta).

title and description are Markdown, exactly like every driver comment (see comment()): assembler renderers flatten them to comment plaintext; the JSON renderer keeps the source Markdown verbatim. Each renderer applies only its own comment prefix, so the same header renders on every backend. As with any comment, a single newline is a CommonMark soft break — separate provenance lines (a note, an md5, a sha256) want a blank line between paragraphs, or a Markdown list:

d.set_file_header(
    title="Acorn BBC BASIC II",
    description=(
        "Annotated disassembly of the 16 kB BASIC II ROM.\n"
        "\n"
        "- Source md5: `2cc6…`\n"
        "- Source sha256: `45bd…`"
    ),
)

dasmos stores only the text — provenance stays the driver’s, so this is reusable across every project.

string(runtime_addr, length: int, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as a string.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

stringcr(runtime_addr, *, move: Move | None = None) → int[source]

Classify a CR-terminated string starting at runtime_addr; returns the runtime address of the byte right after the terminator. The CR convention (0x0D) is the BBC OS’s standard line-terminator for OSWRCH and command-line input.

Behaves like stringz() but with 0x0D as the terminator; the terminator byte is included in the classified span (use d.byte if you need to classify them differently).

stringz(runtime_addr, *, move: Move | None = None) → int[source]

Classify a NUL-terminated string starting at runtime_addr; returns the runtime address of the byte right after the NUL terminator.

Scans forward through loaded memory until the first 0 byte; classifies the whole span (including the terminator) as a String. Driver scripts use the return value to chain through a sequence of strings:

addr = d.stringz(0x9000)
addr = d.stringz(addr)  # next string follows
subroutine(runtime_addr, name: str | None = None, *, title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, is_entry_point: bool = True, move: Move | None = None) → None[source]

Register a subroutine entry at runtime_addr.

Always records a SubroutineEntry (so structured renderers can surface it). When is_entry_point is True (the default), also seeds the trace AND registers the label as REQUIRED. When False — used by environments to document out-of-image OS calls (osbyte, osword, …) that shouldn’t seed tracing of unloaded memory — the trace is not seeded AND the label is registered as OPTIONAL (so it only emits when actually JSR’d to).

If title or description (or on_entry / on_exit) is given, also attaches a banner-style annotation.

property subroutines: list[SubroutineEntry]

Subroutine metadata records registered via subroutine(). The label name (when given) also appears in labels.

typed_data(runtime_addr, data_type, *, length: int | None = None, comment: str | None = None, move: Move | None = None, override: bool = False) → None[source]

Classify a custom-typed data region: emit the raw bytes for byte-faithful reassembly, and attach the decoded value as a DecodedAnnotation.

data_type may be:

  • a string — resolved from the registry populated by register_data_type() (typically by an environment);

  • a DataType instance — used directly (no registration needed);

  • a bare callable bytes -> str | DecodedValue — for a one-off decode; this form requires an explicit length since a plain function carries no width.

The width is implied by the type (a DataType owns its length); pass length only to validate, and a disagreeing value raises. The region is classified as Byte (so the bytes round-trip exactly — many domain types, like the BBC 5-byte float, have no assembler literal that reassembles to the same bytes); the decoded value rides along as an annotation, never as a literal.

comment is an optional human note rendered after the decoded value. override=True clears any conflicting classification in the range first (see _override_clear()).

use_environment(env, **kwargs) → None[source]

Activate an environment plug-in on this disassembler.

env may be a string (the registered name, resolved via Stevedore) or an Environment instance.

Any extra keyword arguments are forwarded to the Environment subclass constructor — only valid when env is a string, since pre-constructed instances already have their configuration baked in. Use this for envs that take per-activation parameters, e.g.:

d.use_environment("acorn_sideways_rom",
                  rom_title="ANFS ROM 4.21 (variant 1)")

Environments are composable: calling use_environment more than once layers their effects in order. Activating the same environment twice is a harmless no-op (the underlying managers deduplicate registrations).

Constructor sugar: Disassembler.create(cpu=..., environments=[...]) activates each environment immediately in order. Environments that need loaded memory (e.g. one that inspects bytes at the ROM header) should be activated AFTER load() rather than via the constructor kwarg.

word(runtime_addr, length: int = 2, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as 16-bit words.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

exception dasmos.DisassemblerError[source]

Raised on misuse of the Disassembler orchestration.

class dasmos.Extension(name: str, **kwargs)[source]
classmethod brief_description() → str[source]

Pithy single-line summary, suitable for a table cell.

Default implementation returns the first non-empty line of full_description(). Override on the subclass when the docstring’s first line would be too long for a list-style rendering — set this to a separately-authored, deliberately terse summary.

Used by list-<kind> CLI commands.

classmethod describe(*, single_line: bool = False) → str[source]

Convenience wrapper around brief_description() / full_description().

Parameters:

single_line – If True, return brief_description(). Otherwise return full_description().

classmethod dirpath() → Path[source]

The directory path to the extension package.

classmethod entry_point_name() → str[source]

Get the entry point name (key) for this extension class.

This performs a reverse lookup from class to entry point name by searching through all extensions in the appropriate namespace. The result is cached indefinitely since entry point names are immutable.

Returns:

The entry point name for this extension class.

Raises:

ExtensionError – If this class is not registered as an extension.

classmethod full_description() → str[source]

The complete, multi-paragraph description.

Default implementation returns the cleaned class docstring. Override only if the description should come from somewhere other than the docstring (rare).

Used by describe-<kind> CLI commands.

classmethod kind() → str[source]

The kind of extension.

Used to distinguish extension points.

classmethod version() → str[source]

The extension version.

exception dasmos.ExtensionError[source]
class dasmos.FlowControl(*values)[source]

How an opcode affects the trace engine’s notion of control flow.

CPU-agnostic — the trace loop dispatches on this enum to decide where to follow next.

BREAK = 'break'

Software interrupt or trap. Treated conservatively as a path terminator unless the plug-in says otherwise.

CONDITIONAL_BRANCH = 'conditional_branch'

Conditionally transfers control; both target and fall-through are reachable. e.g. BNE / BEQ.

JUMP = 'jump'

Unconditional control transfer; no fall-through. e.g. JMP.

RETURN = 'return'

Terminates the current trace path. e.g. RTS / RTI.

SEQUENTIAL = 'sequential'

Falls through to the next instruction. Most opcodes.

SUBROUTINE_CALL = 'subroutine_call'

Calls a subroutine; control eventually returns to the next instruction. e.g. JSR. Both target and fall-through are reachable.

UNDEFINED = 'undefined'

Undocumented or invalid opcode. Trace terminates here.

class dasmos.FormatHint(*values)[source]

Abstract semantic markers a renderer translates to syntax.

Each value names what a byte represents (a character literal, a base-N integer, a flag word, …); each renderer chooses how to express that in its target assembler’s grammar.

BINARY = 'binary'

Render in base 2 with the assembler’s binary sigil (%01010101 in beebasm; the bit-pattern form is more readable than hex for flag words and bitmasks).

CHAR = 'char'

This byte is intended as an ASCII character literal. Renderers should choose the cleanest character-literal syntax their target assembler offers (ASC("c"), 'c', etc.) or fall back to a hex literal with a warning when the byte cannot be expressed as a character (non-printable bytes etc.).

DECIMAL = 'decimal'

Render in base 10. The renderer’s default may already use decimal for some values (e.g. beebasm’s small-int 0..9 rule); an explicit DECIMAL hint widens that choice to the whole byte range.

HEX = 'hex'

Render in base 16 with the assembler’s hex sigil (&xx for beebasm, $xx for ca65 / ACME, 0xXX for C-style assemblers).

INKEY = 'inkey'

This byte is the negative-X form of an Acorn INKEY scan code — the input to OSBYTE &79 (scan keyboard) or OSBYTE &81 (INKEY) when scanning for a specific key by its internal number rather than its ASCII representation. The byte value X decodes via key_code = 255 - (X EOR 128) to a key number named in the BBC’s INKEY table (inkey_key_ctrl, inkey_key_shift, …). The renderer expresses this as (255 - inkey_key_<name>) EOR 128 so the source reads as the symbolic intent — ldx #(255 - inkey_key_ctrl) EOR 128.

BBC-specific. Other systems with their own scan-code conventions would use a different hint or a renderer-supplied lookup. The renderer lazy-imports the BBC INKEY table from the acorn_mos env when this hint is encountered; bytes that don’t map to any named INKEY key fall back to a hex literal with a one-time warning.

OCTAL = 'octal'

Render in base 8. Some assemblers (notably beebasm) have no native octal syntax; renderers that can’t express it should emit a hex / decimal form plus a clarifying inline comment and warn at render time.

class dasmos.IntermediateRepresentation(disassembler: Disassembler)[source]

Read-only view of a disassembly model.

Returned from disassemble(). Renderers consume this; they should not call mutating methods on the underlying managers.

property constants

Named values registered via Disassembler.constant(). Returned in registration order.

property file_header

The provenance header, or None (see Disassembler.set_file_header()).

property format_hints: FormatHintRegistry

Renderer-agnostic format hints registered via Disassembler.format_hint() / Disassembler.char_literal(). Renderers consult this registry at render time to honour the user’s semantic intent for an operand byte.

property index_regions

Indexing regions registered via Disassembler.index_region(), in declaration order.

property macros: dict

Driver-defined macros (see Disassembler.define_macro()).

property program: ProgramInfo | None

Declared load-and-run program metadata, or None (see Disassembler.program()).

render(renderer: Renderer | str, **kwargs: Any) → Output[source]

Render this IR via the named or supplied renderer.

renderer may be:

  • a string — the renderer plug-in name; resolved via Stevedore using create_renderer(). Extra kwargs are forwarded to the renderer’s constructor.

  • a Renderer instance — used directly; kwargs are ignored.

property subroutines

Subroutine metadata records registered via Disassembler.subroutine(). Returned in registration order.

write_included_binaries(dirpath) → list['Path'][source]

Write each include_binary region’s owned bytes to its declared path, under dirpath.

Every IncludedBinary region owns the bytes it covers; those bytes are the canonical payload the include directive refers to. Writing them next to the listing lets an assembler resolve incbin / .binary and keeps the round-trip self-consistent (the assembler reads back exactly what dasmos accounted for).

Each region’s path is resolved relative to dirpath; parent directories are created as needed. Returns the paths written, in binary-address order. Raises ValueError if two regions declare the same path with differing bytes (a genuine inconsistency the author must resolve).

class dasmos.Opcode(operation: Enum, addressing_mode: AddressingModeMember, flow_control: FlowControl, cycles: int = 0)[source]

An entry in a CPU’s instruction table — and itself a Classification so it can be stored in the ClassificationStore alongside Byte, Word, etc.

Renderer-agnostic: carries the abstract operation and addressing_mode enum members plus a CPU-agnostic FlowControl. Renderers translate (operation, addressing_mode) to their preferred mnemonic spelling — typically via a per-pair lookup table that falls back to default_mnemonic().

Frozen and hashable: a single Opcode instance is shared across every binary address that uses this opcode byte.

default_mnemonic() → str[source]

Canonical mnemonic for this operation, used by __repr__ and as the fallback for renderers without a specific override for this (operation, addressing_mode) pair.

Returns the operation enum member’s value if it’s a string; otherwise its name. Plug-in authors should give their Operation enum members string values (the canonical lowercase MOS form for 6502: "lda", "jsr", …).

is_code(binary_addr=None) → bool[source]

Opcodes always represent code.

length() → int[source]

Total bytes consumed by this instruction (opcode byte + operand bytes). Method form for parity with the other Classification subclasses.

next_addresses(memory, binary_addr: int) → list[int][source]

Compute the next binary addresses to trace from this opcode located at binary_addr.

Returns one or two addresses according to flow control:

  • SEQUENTIAL → [fall_through].

  • JUMP → [target] if the target is computable; otherwise [].

  • SUBROUTINE_CALL → [fall_through, target] (target omitted if not computable; fall_through is where execution returns to after the called routine).

  • CONDITIONAL_BRANCH → [fall_through, target] (target omitted if not computable).

  • RETURN / BREAK / UNDEFINED → [] (control terminates here).

Targets that depend on memory the engine can’t read (e.g. JMP (addr) where the pointer’s bytes aren’t loaded) are silently dropped — the trace path narrows but doesn’t fail.

memory is the MemoryImage; binary_addr is the binary address of the opcode byte (an int for ergonomics, internally interpreted as a BinaryAddr).

property operand_length: int

Bytes consumed by the operand (delegates to the addressing mode).

class dasmos.OperandKind(*values)[source]

The semantics of the byte(s) following the opcode byte.

Used by the trace engine and by renderers to decode and present the operand correctly. CPU-agnostic for now; may grow as exotic addressing modes appear.

ADDRESS_16 = 'address_16'

A 16-bit address (absolute).

ADDRESS_16_INDIRECT = 'address_16_indirect'

A 16-bit address that is itself a pointer — the actual target is found by reading two bytes at that address. Used by the 6502’s JMP (addr) and the 65C02’s JMP (addr,X).

ADDRESS_8 = 'address_8'

An 8-bit address (zero-page).

IMMEDIATE = 'immediate'

A literal value (e.g. LDA #$3 — the operand is the value).

NONE = 'none'

No operand bytes (implied / accumulator).

RELATIVE_OFFSET = 'relative_offset'

A signed 8-bit offset added to the program counter at the next instruction (branches).

class dasmos.Output[source]

Base class for renderer output. Always stringifiable.

class dasmos.Renderer(name: str, **kwargs)[source]

Extension boundary for output renderers.

Concrete renderers consume an IntermediateRepresentation and produce an Output subclass.

abstractmethod render(ir: IntermediateRepresentation) → T[source]

Produce output from an IR.

exception dasmos.RendererExtensionError[source]

Exception raised when a renderer extension cannot be loaded.

class dasmos.StructuredOutput(data: Any, *, indent: int | None = 2)[source]

Output produced by a data-emitting renderer (Json, …).

The underlying data is exposed for direct structured access via data. __str__ serialises it to JSON with the configured indent; if a different serialisation is wanted, use data directly and serialise yourself.

class dasmos.StructuredRenderer(name: str, **kwargs)[source]

Base for structured-data renderers (Json, …).

No additional protocol beyond Renderer.render() — concrete structured renderers consume the IR however they like and return a StructuredOutput.

class dasmos.TextOutput(text: str)[source]

Output produced by a text-syntax renderer (Beebasm, ca65, …).

lines() → list[str][source]

Return the output split on line boundaries.

class dasmos.TextRenderer(name: str, **kwargs)[source]

Base for text-syntax (assembler-source) renderers.

Adds the assembler-syntax protocol every text renderer must implement. The methods grouped under “syntax” below are abstract; those under “policy” have sensible defaults.

Format the hex from an address: cross-reference URI (see dasmos.core.markdown_asm) as this assembler’s literal when the ?hex flag flattens a link into comment text.

Defaults to beebasm’s &-prefixed uppercase form; override for a different sigil (64tass $). Note this deliberately preserves the URI’s hex width and uppercases it, rather than going through hex2()/hex4(), to match the established comment-link convention.

abstractmethod byte_prefix() → str[source]

The directive that emits raw bytes (e.g. equb).

abstractmethod char_literal(n: int) → str | None[source]

A single-character literal for the byte n, or None.

abstractmethod code_end() → list[str][source]

Lines emitted when ending a code block.

abstractmethod code_start(start_addr, end_addr, first: bool) → list[str][source]

Lines emitted when starting to render a code block.

abstractmethod comment_prefix() → str[source]

The character(s) introducing a line comment.

abstractmethod cpus_supported() → list[str][source]

Names of the CPU plug-ins this renderer can target.

abstractmethod disassembly_end() → list[str][source]

Lines emitted at the bottom of the output.

abstractmethod disassembly_start() → list[str][source]

Lines emitted at the top of the output.

abstractmethod explicit_label(name: str, value, offset: int | None = None, align_column: int = 0) → str[source]

Render an name = value definition.

force_abs_instruction(instruction: str, prefix: str, operand: str, suffix: str) → str | None[source]

Instruction text that forces absolute (16-bit) addressing, or None when the assembler has no syntax for it.

Called for an instruction encoded with absolute addressing whose operand is below &100 and whose mnemonic also has a zero-page form — the case a size-optimising assembler would otherwise shrink to two bytes. instruction is the mnemonic, operand the resolved symbol, prefix / suffix the surrounding mode punctuation ("" / ",x" / ",y"). 64tass returns sta @w name; the default None makes the renderer emit the instruction as data instead.

force_zp_instruction(instruction: str, prefix: str, operand: str, suffix: str) → str | None[source]

Instruction text that forces zero-page (8-bit) addressing, or None when the assembler has no syntax for it.

Called for a zero-page instruction whose operand names a label defined later in the listing, when the assembler’s first pass would otherwise guess the absolute form (see AssemblerRenderer.forward_reference_assumes_zero_page). Arguments as for force_abs_instruction(). 64tass returns lda @b name; the default None makes the renderer emit the instruction as data instead.

abstractmethod hex2(n: int) → str[source]

Format an 8-bit value as the renderer’s hex literal.

abstractmethod hex4(n: int) → str[source]

Format a 16-bit value as the renderer’s hex literal.

abstractmethod inline_label(name: str) → str[source]

Render an inline label definition (e.g. .foo or foo:).

abstractmethod pseudopc_end(*, dest: int, src: int, length: int, move_id: int, src_label: str | None, dest_label: str | None) → list[str][source]

Lines leaving a relocated block and restoring the PC.

Beebasm emits the copyblock / clear / restore-org idiom that copies the assembled bytes back to their file position; 64tass emits .here (which restores the compile offset implicitly). This asymmetry — beebasm needs an explicit restore, 64tass does not — is exactly why relocation lives behind this hook rather than in the shared walk.

abstractmethod pseudopc_start(*, dest: int, src: int, length: int, move_id: int, src_label: str | None, dest_label: str | None) → list[str][source]

Lines entering a relocated (pseudo-PC) block.

The shared move walk has already positioned the PC at src and emitted src_label (if any); this switches the PC into the relocated destination range so the block’s bytes resolve as if running at dest. Beebasm emits an org &<dest>; 64tass .logical $<dest>. The full move context is passed so each backend uses what its idiom needs (beebasm labels a descriptive comment; 64tass needs only dest).

abstractmethod set_origin(addr: int) → list[str][source]

Lines that set the assembly program counter to addr.

Used by the shared walk whenever emission needs to (re)position the PC at a plain address outside a code block — resuming after a skipped (move-source) range, positioning a move’s source bytes, or advancing to the loaded range’s end before the boundary marker. Beebasm emits org &<addr>; 64tass *=. The list may include surrounding blank lines for layout.

abstractmethod string_chr(n: int) → str | None[source]

The character n as it appears inside a string literal, or None if the assembler can’t include that character.

abstractmethod string_prefix() → str[source]

The directive that emits a string (e.g. equs).

abstractmethod word_prefix() → str[source]

The directive that emits 16-bit words (e.g. equw).

dasmos.cpu_names() → list[str][source]

Get the names of all available CPU plug-ins.

dasmos.create_cpu(cpu_name: str, **kwargs) → Cpu[source]

Create a CPU instance by name.

Parameters:
  • cpu_name – The name of the CPU to create (e.g. "6502", "65c02").

  • **kwargs – Forwarded to the CPU subclass constructor.

Returns:

A Cpu instance.

Raises:

CpuExtensionError – If the CPU cannot be loaded.

dasmos.create_renderer(renderer_name: str, **kwargs) → Renderer[source]

Create a renderer instance by name.

dasmos.describe_cpu(cpu_name: str, *, single_line: bool = False) → str[source]

Get the description of a CPU plug-in.

dasmos.describe_renderer(renderer_name: str, *, single_line: bool = False) → str[source]

Get the description of a renderer plug-in.

dasmos.renderer_names() → list[str][source]

Get the names of all available renderer plug-ins.

The source-module sections below use :no-index: so the symbols re-exported via dasmos (above) remain the canonical cross-reference targets — the per-module pages exist purely to give readers a module-by-module navigation aid.

The Disassembler

The Disassembler orchestration class.

The Disassembler is the single object a driver script (or CLI invocation) interacts with: it owns one of each manager class (memory, moves, labels, classifications, expressions, config), wires them together with the CPU plug-in’s address-space sizing, and exposes the flat driver-script API documented in docs/design/architecture.md.

This is the skeleton — the trace loop, the leftover-classification pass, and the full driver-script API land in subsequent ports (tasks #16–#19).

Per docs/design/decisions.md:

  • D-004: each model concern is its own manager class held by the Disassembler.

  • D-008: instance constructor takes cpu= directly; the Disassembler.create() factory accepts a string and resolves via Stevedore.

  • D-009: disassemble() returns an IntermediateRepresentation; rendering happens on the IR.

  • D-010: multiple binaries per Disassembler.

  • D-011: flat driver-script methods.

class dasmos.disassembler.Constant(name: str, value: int, comment: str | None = None)[source]

Bases: object

A named value registered via Disassembler.constant().

Distinct from Label — constants are arbitrary named values (magic numbers, OSBYTE call numbers, hardware register addresses) that the driver wants to surface as a separate constants section in structured output. The Disassembler.constant() method ALSO calls Disassembler.optional_label() so the asm equate emission is unchanged from the legacy constant() → optional_label porter rename.

class dasmos.disassembler.Disassembler(cpu: Cpu, *, environments: tuple = (), auto_labels_enabled: bool = True, auto_label_data_prefix: str = 'data_', auto_label_code_prefix: str = 'code_', auto_label_subroutine_prefix: str = 'sub_', auto_label_loop_prefix: str = 'loop_', auto_label_loop_limit: int = 256, auto_label_return_prefix: str | None = 'return_', string_detection_min_length: int | None = 3)[source]

Bases: object

The orchestration class.

Owns one instance of each manager class; delegates the flat driver-script API to those managers; produces an IntermediateRepresentation via disassemble().

add_move(dest_runtime_addr, src_binary_addr, length: int, *, name: str | None = None) → Move[source]

Register a relocation. Returns the Move handle.

The Move is itself a context manager — with move: ... scopes annotations under it. Pass name= to give the move a stable identity in diagnostics, JSON output, and cross-references.

banner(runtime_addr, *, title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, align: Align = Align.BEFORE_LABEL, auto_generated: bool = False, move: Move | None = None) → None[source]

Attach a multi-line decorated comment block at runtime_addr — visual separation only; does not register an entry point.

Use this for data regions you want to mark visually without the trace engine treating the address as code. See docs/design/commands-sweep-memo.md C2/C3 for the rationale.

auto_generated marks this banner as env-attached metadata rather than user-authored content (parallel to the same kwarg on comment()). Suppresses the duplicate-annotation warning when an env stacks a banner alongside a driver- supplied one at the same address.

For a subroutine that needs both visual separation AND entry-point registration, call subroutine() with the same title / description kwargs.

byte(runtime_addr, length: int = 1, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as raw bytes.

Pass override=True to clear any conflicting classification in the range first (see _override_clear()); otherwise an overlap raises ClassificationError.

char_literal(runtime_addr, *, move: Move | None = None)[source]

Sugar for format_hint(runtime_addr, FormatHint.CHAR).

Declares that the operand byte at runtime_addr is intended as an ASCII character literal. Renderers express this in their target assembler’s grammar — beebasm emits ASC("c") or 'c' (per the renderer’s char_literal_style); a future ACME or ca65 renderer would emit 'c'; the JSON renderer surfaces the hint in its structured output.

code_ptr(runtime_addr_lo, runtime_addr_hi: int | None = None, *, offset: int = 0, label_name: str | None = None) → None[source]

Mark two bytes of data as the address of a subroutine and seed the trace from the computed target.

Reads the bytes at runtime_addr_lo (low half) and runtime_addr_hi (high half — defaults to runtime_addr_lo + 1 for the common adjacent-bytes case), computes the target as (hi << 8) | lo) + offset, registers entry(target, name=label_name), and sets per-byte expression overrides so the source bytes render symbolically (equw <label> or equb < (<label>) / equb > (<label>)).

Used for jump tables where the low and high bytes of subroutine addresses are stored in separate parallel tables (see rts_code_ptr() for the RTS-pop-and-INC variant).

comment(runtime_addr, text: str, *, align: Align = Align.BEFORE_LABEL, word_wrap: bool = True, indent: int = 0, auto_generated: bool = False, suppresses_auto: bool = False, move: Move | None = None) → None[source]

Attach a comment at runtime_addr.

align controls where the comment is rendered relative to the code line — BEFORE_LABEL (the default) puts it on its own line above any labels at the address; INLINE appends it to the code line itself; the other positions (AFTER_LABEL, BEFORE_LINE, AFTER_LINE) cover less-common arrangements.

auto_generated marks this comment as env-attached metadata rather than user-authored content. Suppresses the duplicate- comment warning when stacking with a driver-supplied comment at the same address (the warning targets driver authoring bugs, not env-driven layering). Envs attaching address metadata in setup() (e.g. "Sideways ROM header" at &8000) should set this.

suppresses_auto marks this comment as authoritative: env analysers and setup() methods skip attaching their auto- generated annotation at this address when any annotation present has this flag set. Use it when the driver already documents the address authoritatively and the env-supplied text would be redundant. Order matters for env setup: attach the suppressing comment before use_environment(...) so the env’s setup sees the flag.

The runtime address is resolved to a binary address via the active-move stack on the move manager, or via the explicit move_id if given.

constant(value: int, name: str, comment: str | None = None) → None[source]

Register a named value (a constant).

Conceptually distinct from a label: constants are arbitrary named values (magic numbers, OSBYTE call numbers, hardware register addresses) — not necessarily addresses of code or data the disassembler will reach. The structured-output renderers surface them as a dedicated constants section.

Constants are tracked SEPARATELY from labels. Mixing them would cause the hook-registered OSBYTE constants (e.g. osbyte_clear_escape = &7c) to pollute operand resolution at zero-page address &7c: any unrelated code reading from &7c would suddenly render as lda osbyte_clear_escape — a false positive.

The BeebasmRenderer emits constants as equates in their own block alongside the label-equate table.

If name matches a deprecated alias registered by an active environment (see register_deprecated_constant_name()), a DeprecationWarning is emitted pointing at the new replacement name. The constant is still recorded under the old name so the rendered output keeps working — the warning is the only signal that drivers should update.

property constants: list[Constant]

Named values registered via constant(). The Constant records here also appear as optional labels in labels (for asm equate emission).

classmethod create(cpu: str | Cpu, **kwargs: Any) → Disassembler[source]

Construct a Disassembler with a string-named CPU plug-in or with an explicit instance.

Strings are resolved via Stevedore from the dasmos.cpu entry-point namespace. Keyword arguments are partitioned: any kwarg whose name matches a parameter of __init__() (e.g. auto_labels_enabled, auto_label_subroutine_prefix) is forwarded to the Disassembler; the rest go to the CPU plug-in’s constructor. Instances are used directly.

define_macro(name, params, body, *, emit: str = 'byte')[source]

Define a macro computing a value from params and return a callable that builds invocations of it.

params is a list of parameter names; inside body refer to them with dasmos.expr.param(). emit is the data kind the value is emitted as ("byte" / "word") — used by backends without value-functions (beebasm) to wrap the body in the right data directive. The value is rendered natively per backend: a 64tass .sfunction used inline in .byte pack("LDA"), or a beebasm MACRO emitting the byte, invoked pack "LDA" per line.

Example:

m = param("mnem")
pack_lo = d.define_macro("pack_lo", ["mnem"],
    (((m[0] & 0x1F) << 10) + ((m[1] & 0x1F) << 5)
     + (m[2] & 0x1F)) & 0xFF)
d.expr(addr, pack_lo("LDA"))
disassemble() → IntermediateRepresentation[source]

Run the trace loop, classify leftovers, and return an IR.

One-shot: calling twice raises DisassemblerError.

Pipeline:

  1. Trace — follow control flow from every registered entry point, classifying each instruction.

  2. Leftover classification — bytes loaded but not reached by trace (and not pre-classified by the user) become Byte 1-byte items.

  3. Reference analysis — walk every classified opcode and record use sites against any label at the operand target.

  4. Auto-label generation (when auto_labels_enabled) — synthesise names for referenced addresses without explicit names. Names land in the LabelManager with is_autogenerated=True so renderers and other consumers see them as ordinary (optional) labels via the standard label-resolution paths.

entry(runtime_addr, name: str | None = None, *, override: bool = False, **label_kwargs)[source]

Register a code entry point at runtime_addr.

The trace loop will start here. If name is given, also defines a label at the same runtime address (any extra kwargs are forwarded to label()).

The runtime address is resolved to a binary address via the active-move stack on the move manager — entry points outside any move identity-map.

Precedence over eager classifications. Environments (and earlier driver calls) may classify bytes before this call — e.g. acorn_sideways_rom classifying a non-JMP entry slot as data. The trace cannot reclassify already-classified bytes, so a plain entry() on such an address would render the bytes as data and silently do nothing. To make that footgun visible:

  • override=False (the default): if any byte at the target is already classified, emit a UserWarning — the entry will not change how those bytes render.

  • override=True: clear the entire conflicting classification span at the target first, so the trace can claim the bytes as code. An explicit driver call made after use_environment() thus wins over the environment’s classification. Only the rendering changes — the bytes are untouched, so the round-trip oracle still holds.

expr(runtime_addr, expression: Expr | str, *, move: Move | None = None)[source]

Override the rendered operand at runtime_addr with the given expression.

expression is preferably an assembler-neutral Expr built with the DSL:

from dasmos.expr import ref, lo
d.expr(0x8001, ref(num_lives) + 1)
d.expr(0x8020, lo(ref(dispatch_lo) - 1))

Each renderer then emits its own syntax (&/$ hex, AND/&, correct parenthesisation). A plain str is still accepted for backward compatibility — it is wrapped as a Raw node and emitted verbatim (via the renderer’s dialect adapter), exactly as before.

runtime_addr is the runtime address of the operand byte — typically one past the opcode for a single-byte-opcode CPU like the 6502. For example, an LDA #$03 instruction at &8000 has its operand byte at &8001; calling d.expr(0x8001, ref(num_lives) + 1) makes the renderer emit lda #num_lives + 1 for that instruction.

With a raw string the driver is responsible for ensuring any names it references are valid in the rendered output (typically by also defining them via label() or an external constant the assembler resolves).

For renderer-agnostic semantic markers (e.g. “render this byte as an ASCII character / decimal / hex / binary”), use format_hint() or its sugars (char_literal()) instead — those declare intent and let each renderer choose its own syntax.

expr_label(runtime_addr, expression: str, **kwargs)[source]

Register an expression to be used as a label reference.

property file_header: FileHeader | None

The provenance header, or None (see set_file_header()).

fill(runtime_addr, length: int, value: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark a run of length identical bytes at runtime_addr.

value is the byte the fill should expand to. If omitted, it’s inferred from the loaded memory (the byte at runtime_addr) — the natural shape for runs of the same value the binary already contains. If value is supplied AND disagrees with the loaded byte, raises DisassemblerError.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

format_hint(runtime_addr, hint: FormatHint, *, move: Move | None = None)[source]

Mark the operand byte at runtime_addr with a renderer- agnostic format hint — a semantic declaration of how the byte should be interpreted (ASCII character, decimal, hex, binary, octal). Each renderer translates the hint to its assembler- specific syntax.

Hints are weaker than expr(): expr substitutes a concrete text expression; format_hint declares intent and leaves the syntax to the renderer. When both are registered at the same address, expr wins (the user’s explicit text is more specific than a category).

See dasmos.core.format_hint.FormatHint for the full set of hints. char_literal() is a sugar for the CHAR case.

hook_subroutine(runtime_addr, name: str, hook) → None[source]

Register a callable the trace fires when it processes a JSR whose operand resolves to runtime_addr. Used for the inline-string idiom and similar non-standard return conventions.

Hook signature:

def hook(disassembler, jsr_binary_addr) -> int

The hook returns the binary address where execution continues after the JSR. See dasmos.hooks for the bundled hooks (stringhi_hook, stringz_hook, …) and the protocol details.

Also registers name as an optional label at runtime_addr so JSR operands resolve to the symbolic name in the rendered output.

include_binary(runtime_addr, length: int, path: str, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as an external binary include.

Classifies the span as data — the bytes are accounted for and the tracer won’t enter it, exactly like byte() — but tags it so a renderer emits an include directive (beebasm incbin "<path>"; 64tass .binary "<path>") instead of a wall of equb lines. Use it for a region whose source of truth lives outside the disassembly and is regenerated at build time (e.g. detokenised BASIC re-encoded to bytes).

The span still owns its loaded bytes, so nothing is left unaccounted for. Those bytes are the canonical payload: IntermediateRepresentation.write_included_binaries() writes them to path next to the listing, so the assembler reads back exactly the bytes dasmos accounted for and the round-trip stays self-consistent. A project whose own generator must reproduce the region can diff its output against that canonical payload.

path is the include path as it should appear in the directive (typically a bare filename beside the listing). Pass override=True to clear a conflicting classification in the range first; otherwise an overlap raises ClassificationError.

index_base(runtime_addr, name: str, **kwargs)[source]

Define a label for an address used only as an indexing base — the operand of lda base,X / sta base,Y etc., where the byte actually touched is base + register and base itself is never read or written.

Behaves like optional_label() (the name is emitted only if referenced, so the base,X operand can resolve) but asserts the base (b) access flag on the label. That keeps any description= / group= / length= the author supplies — so the base is still documented — and the address takes its place on the memory map carrying access including b (a base-only row: b with no r / w says the literal byte is never touched, only indexed through). This is the intention-revealing way to document a base with no in-image references; a base that is referenced derives b automatically from its reference kinds.

See docs/design/reference-kinds-memo.md.

index_region(anchor_addr, name: str, window: tuple[int, int], *, description: str | None = None, group: str | None = None, access: str | None = None, named_slots: bool = False) → IndexRegion[source]

Declare an indexing region: a window of addresses around an anchor whose in-window neighbours render relative to the anchor label (fsm_sector0-3,X) rather than as bare addresses.

anchor_addr gets the explicit, always-emitted label name (so the anchor±k arithmetic always resolves), plus any description / group / access — exactly as label() would, so the anchor takes its normal place on the memory map. window=(lo, hi) is the inclusive offset range the region owns (lo typically ≤ 0, hi ≥ 0); offset 0 is the anchor itself.

In-window addresses that carry their own explicit label keep it — a region names only the gaps. With named_slots=True each gap gets a distinct identifier (name_m<k> / name_p<k>) instead of the default name±k arithmetic form.

Windows must be disjoint; an overlap with an already-declared region raises RegionError. See docs/design/reference-kinds-memo.md.

property index_regions: list[IndexRegion]

Indexing regions registered via index_region(), in declaration order. Each anchor also appears in labels.

inkey_code(runtime_addr, *, move: Move | None = None)[source]

Sugar for format_hint(runtime_addr, FormatHint.INKEY).

Declares that the operand byte at runtime_addr is the negative-X form of an Acorn INKEY scan code (the byte loaded into X for OSBYTE &79 / OSBYTE &81 to scan for a specific key by its internal number). Renderers express the byte symbolically as (255 - inkey_key_<name>) EOR 128 when the value matches a named INKEY key in the BBC’s scan-code table; bytes that don’t match any named key fall back to a hex literal with a one-time warning.

Use this when the analyser can’t infer the pattern from a nearby JSR osbyte — for example when the scan code lives in a table of pre-computed values, or is loaded into X through a longer pipeline of stores and reads.

is_auto_suppressed_at(runtime_addr, *, move: Move | None = None) → bool[source]

Return True if any annotation at runtime_addr carries suppresses_auto=True.

Env setup() methods call this before attaching their own auto-comments so a driver-supplied authoritative comment can crowd out the env default. Wraps AnnotationStore.is_auto_suppressed_at() with the same runtime→binary address resolution as comment() and label(), so envs can pass the natural runtime address.

label(runtime_addr, name: str, **kwargs)[source]

Define a label. Delegates to add_label().

load(filepath, binary_addr, md5sum: str | None = None)[source]

Load a binary file at binary_addr. Delegates to load.

local_label(runtime_addr, name: str, start_addr, end_addr, **kwargs)[source]

Define a label scoped to [start_addr, end_addr).

property macros: dict[str, MacroDef]

Driver-defined macros, keyed by name (see define_macro()).

optional_label(runtime_addr, name: str, **kwargs)[source]

Define a label that the renderer emits only if it’s referenced.

Typical use: registering names for many out-of-range addresses (zero-page workspace, OS calls, hardware registers) without cluttering the rendered output with definitions for the ones that aren’t actually referenced. Required labels (the default label()) are always emitted; optional labels are emitted only when used.

program(*, load_addr: int | None = None, exec_addr: int | None = None, reload_addr: int | None = None) → None[source]

Declare load-and-run program metadata for the disassembly.

A DFS *RUN program loads at one address but may begin execution at another (its exec_addr), and the filesystem records a reload_addr distinct from where the bytes sit. These are properties of the program, not of any assembler syntax, so they are declared here and each renderer emits what it can express:

  • beebasm appends them to its save directive (save "NAME", start, end, exec, reload), producing a directly *RUN-able file whose header matches the original DFS metadata;

  • the JSON renderer surfaces them structurally;

  • a backend with no equivalent (e.g. 64tass, which saves via -o) warns and omits them, leaving the raw payload — and so fantasm verify — unaffected.

Every argument is optional; an omitted address is simply not recorded. reload_addr defaults to the loaded range’s start when a renderer needs it and it was not declared.

property program_info: ProgramInfo | None

Declared load-and-run program metadata, or None (see program()).

register_data_type(name: str, data_type: DataType) → None[source]

Register a custom DataType under name so typed_data() can resolve it by string.

Environments call this from setup to contribute domain types (e.g. bbc_basic_6502 registers "bbc_float5"), keeping platform-specific decode knowledge out of core — the same pattern as the env-supplied INKEY table. Last-write-wins.

register_deprecated_constant_name(old_name: str, new_name: str) → None[source]

Register a deprecated → replacement alias for a constant name. Subsequent constant() calls with old_name emit a DeprecationWarning citing new_name.

Environments use this to retire historical long-form names without breaking driver scripts that still spell them out.

rts_code_ptr(runtime_addr_lo, runtime_addr_hi: int | None = None, *, label_name: str | None = None) → None[source]

Marks two bytes of data as the address of a subroutine targeted via RTS-pop-then-INC (so the bytes contain target - 1). Equivalent to code_ptr() with offset=1.

set_file_header(title=None, description=None) → None[source]

Set the backend-agnostic provenance header emitted at the top of a listing (and in the JSON meta).

title and description are Markdown, exactly like every driver comment (see comment()): assembler renderers flatten them to comment plaintext; the JSON renderer keeps the source Markdown verbatim. Each renderer applies only its own comment prefix, so the same header renders on every backend. As with any comment, a single newline is a CommonMark soft break — separate provenance lines (a note, an md5, a sha256) want a blank line between paragraphs, or a Markdown list:

d.set_file_header(
    title="Acorn BBC BASIC II",
    description=(
        "Annotated disassembly of the 16 kB BASIC II ROM.\n"
        "\n"
        "- Source md5: `2cc6…`\n"
        "- Source sha256: `45bd…`"
    ),
)

dasmos stores only the text — provenance stays the driver’s, so this is reusable across every project.

string(runtime_addr, length: int, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as a string.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

stringcr(runtime_addr, *, move: Move | None = None) → int[source]

Classify a CR-terminated string starting at runtime_addr; returns the runtime address of the byte right after the terminator. The CR convention (0x0D) is the BBC OS’s standard line-terminator for OSWRCH and command-line input.

Behaves like stringz() but with 0x0D as the terminator; the terminator byte is included in the classified span (use d.byte if you need to classify them differently).

stringz(runtime_addr, *, move: Move | None = None) → int[source]

Classify a NUL-terminated string starting at runtime_addr; returns the runtime address of the byte right after the NUL terminator.

Scans forward through loaded memory until the first 0 byte; classifies the whole span (including the terminator) as a String. Driver scripts use the return value to chain through a sequence of strings:

addr = d.stringz(0x9000)
addr = d.stringz(addr)  # next string follows
subroutine(runtime_addr, name: str | None = None, *, title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, is_entry_point: bool = True, move: Move | None = None) → None[source]

Register a subroutine entry at runtime_addr.

Always records a SubroutineEntry (so structured renderers can surface it). When is_entry_point is True (the default), also seeds the trace AND registers the label as REQUIRED. When False — used by environments to document out-of-image OS calls (osbyte, osword, …) that shouldn’t seed tracing of unloaded memory — the trace is not seeded AND the label is registered as OPTIONAL (so it only emits when actually JSR’d to).

If title or description (or on_entry / on_exit) is given, also attaches a banner-style annotation.

property subroutines: list[SubroutineEntry]

Subroutine metadata records registered via subroutine(). The label name (when given) also appears in labels.

typed_data(runtime_addr, data_type, *, length: int | None = None, comment: str | None = None, move: Move | None = None, override: bool = False) → None[source]

Classify a custom-typed data region: emit the raw bytes for byte-faithful reassembly, and attach the decoded value as a DecodedAnnotation.

data_type may be:

  • a string — resolved from the registry populated by register_data_type() (typically by an environment);

  • a DataType instance — used directly (no registration needed);

  • a bare callable bytes -> str | DecodedValue — for a one-off decode; this form requires an explicit length since a plain function carries no width.

The width is implied by the type (a DataType owns its length); pass length only to validate, and a disagreeing value raises. The region is classified as Byte (so the bytes round-trip exactly — many domain types, like the BBC 5-byte float, have no assembler literal that reassembles to the same bytes); the decoded value rides along as an annotation, never as a literal.

comment is an optional human note rendered after the decoded value. override=True clears any conflicting classification in the range first (see _override_clear()).

use_environment(env, **kwargs) → None[source]

Activate an environment plug-in on this disassembler.

env may be a string (the registered name, resolved via Stevedore) or an Environment instance.

Any extra keyword arguments are forwarded to the Environment subclass constructor — only valid when env is a string, since pre-constructed instances already have their configuration baked in. Use this for envs that take per-activation parameters, e.g.:

d.use_environment("acorn_sideways_rom",
                  rom_title="ANFS ROM 4.21 (variant 1)")

Environments are composable: calling use_environment more than once layers their effects in order. Activating the same environment twice is a harmless no-op (the underlying managers deduplicate registrations).

Constructor sugar: Disassembler.create(cpu=..., environments=[...]) activates each environment immediately in order. Environments that need loaded memory (e.g. one that inspects bytes at the ROM header) should be activated AFTER load() rather than via the constructor kwarg.

word(runtime_addr, length: int = 2, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]

Mark length bytes at runtime_addr as 16-bit words.

Pass override=True to clear any conflicting classification in the range first; otherwise an overlap raises ClassificationError.

exception dasmos.disassembler.DisassemblerError[source]

Bases: DasmosError

Raised on misuse of the Disassembler orchestration.

class dasmos.disassembler.SubroutineEntry(runtime_addr: int, binary_addr: int | None, move_id: int | None, name: str | None, title: str, description: str, on_entry: dict[str, str]=<factory>, on_exit: dict[str, str]=<factory>)[source]

Bases: object

Subroutine metadata registered via Disassembler.subroutine().

Holds the runtime address plus the kwargs the driver supplied; rendering computes fall-through and per-name fields at emission time.

Intermediate representation

Intermediate Representation — the disassembly model exposed to renderers.

The IntermediateRepresentation is what Disassembler.disassemble returns. It is a live read-only wrapper over the Disassembler’s manager classes — there is no deep copy, so it is cheap to obtain and inexpensive to discard.

The contract is “read-only by convention”. The IR exposes the underlying manager objects (which themselves have read methods). Renderers should not call mutating methods on the manager objects; nothing prevents them from doing so but the result is undefined.

The IR is also the single place a renderer is dispatched from:

ir = d.disassemble()
text_output = ir.render("beebasm")          # str-name lookup
json_output = ir.render(JsonRenderer(...))  # explicit instance

See docs/design/architecture.md and docs/design/decisions.md (D-009) for the full rationale.

class dasmos.ir.IntermediateRepresentation(disassembler: Disassembler)[source]

Bases: object

Read-only view of a disassembly model.

Returned from disassemble(). Renderers consume this; they should not call mutating methods on the underlying managers.

property constants

Named values registered via Disassembler.constant(). Returned in registration order.

property file_header

The provenance header, or None (see Disassembler.set_file_header()).

property format_hints: FormatHintRegistry

Renderer-agnostic format hints registered via Disassembler.format_hint() / Disassembler.char_literal(). Renderers consult this registry at render time to honour the user’s semantic intent for an operand byte.

property index_regions

Indexing regions registered via Disassembler.index_region(), in declaration order.

property macros: dict

Driver-defined macros (see Disassembler.define_macro()).

property program: ProgramInfo | None

Declared load-and-run program metadata, or None (see Disassembler.program()).

render(renderer: Renderer | str, **kwargs: Any) → Output[source]

Render this IR via the named or supplied renderer.

renderer may be:

  • a string — the renderer plug-in name; resolved via Stevedore using create_renderer(). Extra kwargs are forwarded to the renderer’s constructor.

  • a Renderer instance — used directly; kwargs are ignored.

property subroutines

Subroutine metadata records registered via Disassembler.subroutine(). Returned in registration order.

write_included_binaries(dirpath) → list['Path'][source]

Write each include_binary region’s owned bytes to its declared path, under dirpath.

Every IncludedBinary region owns the bytes it covers; those bytes are the canonical payload the include directive refers to. Writing them next to the listing lets an assembler resolve incbin / .binary and keeps the round-trip self-consistent (the assembler reads back exactly what dasmos accounted for).

Each region’s path is resolved relative to dirpath; parent directories are created as needed. Returns the paths written, in binary-address order. Raises ValueError if two regions declare the same path with differing bytes (a genuine inconsistency the author must resolve).

Annotations: comments and banners

Comments and annotations attached to binary addresses.

A driver-script call like d.comment(addr, "text") deposits a Comment in an AnnotationStore; the renderer reads the store at render-time and emits each comment at the correct position relative to the code line.

Per docs/design/decisions.md D-020, this is its own manager class held by the Disassembler (alongside MemoryImage, MoveManager, LabelManager, ClassificationStore, …) — separate, focused, instance-scoped.

This first cut handles the load-bearing alignment positions that drive the bulk of the surveyed driver scripts: BEFORE_LABEL (standalone comment above the line), INLINE (trailing on the code line), and BEFORE_LINE / AFTER_LINE for vertical separation. Annotation is the verbatim sibling of Comment for raw assembler-source text the driver wants emitted.

Deferred from this first cut:

  • Markdown-to-plaintext rendering of comment text — lands with the markdown_asm port (task #26).

  • Word-wrapping of long standalone comments — the field is carried but unused; the renderer emits the text as given.

  • Inline-comment column alignment — the renderer uses a fixed trailing offset for now.

  • Per-move-id annotation discrimination — the store is indexed by bare BinaryAddr; the move_id from the Disassembler call is honoured to resolve the address but not stored.

class dasmos.core.annotations.Align(*values)[source]

Bases: Enum

Where a comment or annotation appears relative to a line.

Most driver-script calls use BEFORE_LABEL (the default) or INLINE; the others are rarer.

AFTER_LABEL = 'after_label'

Standalone, between the line’s labels and the code.

AFTER_LINE = 'after_line'

Standalone, just below the code line.

BEFORE_LABEL = 'before_label'

Standalone, above the line’s labels and code.

BEFORE_LINE = 'before_line'

Standalone, just above the code (after any labels).

INLINE = 'inline'

Trailing on the same line as the code, after the operands.

class dasmos.core.annotations.Annotation(text: str, align: Align = Align.BEFORE_LABEL, priority: int | None = None)[source]

Bases: object

A raw assembler-source string at a specific address.

Unlike Comment, the text is emitted verbatim — no comment prefix is added. Use this for emitting assembler directives the driver wants inline (e.g. an explicit equb).

class dasmos.core.annotations.AnnotationStore[source]

Bases: object

Per-disassembly registry of Comment and Annotation entries keyed by binary address.

A single address can carry multiple entries; insertion order is preserved. The renderer is responsible for iterating entries in the right order and at the right rendering position.

add(binary_addr, entry: Comment | Annotation | Banner | DecodedAnnotation) → None[source]

Append entry at binary_addr.

Issues a UserWarning when the (address, alignment, type) triple already has at least one prior entry — a second comment at the same place is almost always a driver-script bug (a copy-paste, or an attempt to override an earlier comment that ends up appending instead). Both entries are still stored; the warning lets the user notice and decide.

get(binary_addr) → list[Comment | Annotation | Banner | DecodedAnnotation][source]

Return a list of entries at binary_addr (insertion order).

get_for_align(binary_addr, align: Align) → list[Comment | Annotation | Banner | DecodedAnnotation][source]

Return entries at binary_addr whose alignment matches.

is_auto_suppressed_at(binary_addr) → bool[source]

Return True if any annotation at binary_addr carries the suppresses_auto flag.

Env analysers and setup() methods consult this before attaching their auto-generated annotation, giving driver scripts a uniform way to crowd out env-supplied auto-comments when the driver already documents the address authoritatively. Align-agnostic — a driver Comment with suppresses_auto=True in any position blocks env auto-attach at that address.

class dasmos.core.annotations.Banner(title: str = '', description: str = '', on_entry: dict[str, str] | None = None, on_exit: dict[str, str] | None = None, align: Align = Align.BEFORE_LABEL, auto_generated: bool = False, priority: int | None = None)[source]

Bases: object

A multi-line decorated comment block at a specific address.

Renders as a renderer-defined separator (Beebasm uses a row of asterisks) followed by the title and description text, providing strong visual separation in the disassembled listing.

The two main use cases:

  • Subroutine headers: d.subroutine(addr, name, title=..., description=...) registers the entry point AND attaches a Banner.

  • Data-region labels: d.banner(addr, title=..., description=...) adds a Banner without registering an entry point.

class dasmos.core.annotations.Comment(text: str, align: Align = Align.BEFORE_LABEL, word_wrap: bool = True, indent: int = 0, auto_generated: bool = False, suppresses_auto: bool = False, priority: int | None = None)[source]

Bases: object

A user comment at a specific address.

The text is stored verbatim; the renderer applies the assembler’s comment prefix (; for Beebasm) at render time.

class dasmos.core.annotations.DecodedAnnotation(decoded: DecodedValue, comment: str | None = None, align: Align = Align.INLINE, auto_generated: bool = True, priority: int | None = None)[source]

Bases: object

The decoded value of a typed-data region (see DataType).

Attached by typed_data() alongside the region’s Byte classification. It is a distinct entry type (not a Comment) so the renderer can place it deterministically — immediately after the raw bytes and before any free-form inline comment — and so the JSON renderer can surface it as a structured field rather than folding it into comment_inline.

comment is an optional human note rendered after the decoded value (e.g. "Euler's number (e)"). Defaults to INLINE — the decoded value of a data region is a trailing annotation on the emitted bytes.

display_text() → str[source]

The single human-readable line both renderers show for this decoded region: "<label> <text>" plus the optional human note. Keeping the composition here (rather than in each renderer) stops the asm and JSON forms from drifting apart.

Format hints: renderer-agnostic operand-format intent

Semantic format hints for operand bytes.

A FormatHint is the abstract semantic intent for a byte (e.g. “this byte is intended as an ASCII character”). It lives in the IR alongside expressions, labels, classifications, etc., and is renderer-agnostic — each renderer translates a hint into its own assembler-specific syntax. For example, a CHAR hint produces:

  • ASC("c") or 'c' from the beebasm renderer (style-dependent);

  • "format_hint": "char" in the JSON renderer’s per-operand record;

  • 'c' from a hypothetical ACME renderer;

  • a hex literal from any renderer when the byte cannot be expressed as a character literal in that assembler’s lexical grammar.

The split between the IR (semantic) and renderer (syntactic) is deliberate: a driver script that says “this byte is a character” expresses intent once and every output format honours it appropriately. Putting the syntax decision in the driver script (via d.expr(addr, 'ASC("c")')) couples the disassembly to a single assembler dialect.

Hints are populated from two sources:

  1. Explicit driver calls — e.g. Disassembler.char_literal() for the CHAR hint. The driver author knows the byte’s intent even when no auto-detection heuristic could.

  2. Auto-detection passes — currently each renderer applies its own auto-detect heuristics to printable immediates without populating the registry; later refactors may move shared heuristics into a single dasmos-side pass that populates hints.

class dasmos.core.format_hint.FormatHint(*values)[source]

Bases: Enum

Abstract semantic markers a renderer translates to syntax.

Each value names what a byte represents (a character literal, a base-N integer, a flag word, …); each renderer chooses how to express that in its target assembler’s grammar.

BINARY = 'binary'

Render in base 2 with the assembler’s binary sigil (%01010101 in beebasm; the bit-pattern form is more readable than hex for flag words and bitmasks).

CHAR = 'char'

This byte is intended as an ASCII character literal. Renderers should choose the cleanest character-literal syntax their target assembler offers (ASC("c"), 'c', etc.) or fall back to a hex literal with a warning when the byte cannot be expressed as a character (non-printable bytes etc.).

DECIMAL = 'decimal'

Render in base 10. The renderer’s default may already use decimal for some values (e.g. beebasm’s small-int 0..9 rule); an explicit DECIMAL hint widens that choice to the whole byte range.

HEX = 'hex'

Render in base 16 with the assembler’s hex sigil (&xx for beebasm, $xx for ca65 / ACME, 0xXX for C-style assemblers).

INKEY = 'inkey'

This byte is the negative-X form of an Acorn INKEY scan code — the input to OSBYTE &79 (scan keyboard) or OSBYTE &81 (INKEY) when scanning for a specific key by its internal number rather than its ASCII representation. The byte value X decodes via key_code = 255 - (X EOR 128) to a key number named in the BBC’s INKEY table (inkey_key_ctrl, inkey_key_shift, …). The renderer expresses this as (255 - inkey_key_<name>) EOR 128 so the source reads as the symbolic intent — ldx #(255 - inkey_key_ctrl) EOR 128.

BBC-specific. Other systems with their own scan-code conventions would use a different hint or a renderer-supplied lookup. The renderer lazy-imports the BBC INKEY table from the acorn_mos env when this hint is encountered; bytes that don’t map to any named INKEY key fall back to a hex literal with a one-time warning.

OCTAL = 'octal'

Render in base 8. Some assemblers (notably beebasm) have no native octal syntax; renderers that can’t express it should emit a hex / decimal form plus a clarifying inline comment and warn at render time.

class dasmos.core.format_hint.FormatHintRegistry[source]

Bases: object

Per-disassembly map from binary address to a FormatHint.

Renderers consult this at render time to discover the user’s semantic intent for an operand byte. The registry is mutated only by the driver-API layer (or by auto-detection passes inside the disassembler, when those are added); renderers must treat it as read-only.

Last-write-wins semantics — add() replaces any prior hint at the same address. Hints are typed and unambiguous, so there is no idempotency concern (unlike ExpressionRegistry where multiple passes can re-register the same expression text).

add(binary_addr, hint: FormatHint) → None[source]

Register hint at binary_addr.

Replaces any previously-registered hint at the same address.

get(binary_addr) → FormatHint[source]

Return the hint registered at binary_addr.

Raises KeyError if there is none — use get_or_none() if absence is expected.

Expressions: the assembler-neutral DSL

The builders (ref, sym, lo, hi, string, param, …) and node types used to author operand and data values that render correctly to every backend. See Assembler-neutral expressions for the guide and Cookbook: expressions and macros for worked examples.

Public expression DSL for driver scripts.

Import the builders here to construct assembler-neutral operand and data values, then pass them to Disassembler.expr() / Disassembler.expr_label():

from dasmos.expr import ref, sym, lo, hi, hexlit

d.expr(0x8020, lo(ref(dispatch_lo) - 1))
d.expr(addr, (ref(a) - ref(b) - 1) & 0xFF)

The node classes (Int, Ref, …) are re-exported too for type annotations and structural inspection. See docs/design/expression-system.md.

class dasmos.expr.BinOp(*values)[source]
class dasmos.expr.Binary(op: 'BinOp', left: 'Expr', right: 'Expr')[source]
class dasmos.expr.Expr[source]

Abstract base for expression nodes.

Supplies Python operator overloads so trees read like arithmetic. Concrete nodes are frozen dataclasses, so they are hashable and comparable by value.

class dasmos.expr.Group(inner: Expr)[source]

Explicit parenthesisation the author wrote for readability, even where operator precedence makes it redundant. Always renders its parentheses, so (255 - x) EOR 128 keeps its grouping rather than collapsing to 255 - x EOR 128.

class dasmos.expr.Int(value: int, radix: Radix = Radix.AUTO)[source]

An integer literal. radix controls how it is spelled.

class dasmos.expr.MacroCall(name: str, args: tuple)[source]

An invocation of a macro, name(args…) — a value expression. Backends with value macros/functions (64tass .sfunction, ca65 .define) render it inline in a data directive; those without (beebasm, acme) render each call as its own statement line (handled by the data-block renderer).

class dasmos.expr.MacroDef(name: str, params: tuple, body: Expr, emit: str = 'byte')[source]

A macro definition: name(params) -> body, whose value is emitted as emit ("byte" / "word"). Not an expression — a top-level definition the renderer emits once and each MacroCall refers to.

class dasmos.expr.Param(name: str)[source]

A macro formal parameter, referenced inside a macro body. Renders as the backend’s parameter reference (both beebasm and 64tass functions use the bare name; 64tass macros would use \name). Being non-constant, string operations on a Param render natively rather than folding — which is exactly how a macro keeps the string argument symbolic in its body.

class dasmos.expr.Radix(*values)[source]

How an Int literal should be spelled.

AUTO = 1

renderer default (0-9 decimal, else hex)

CHAR = 5

a character literal, e.g. 'A'

class dasmos.expr.Raw(text: str)[source]

A pre-formatted expression string in the beebasm/py8dis dialect.

The back-compat / escape-hatch leaf: today’s string-based drivers are wrapped as Raw so they render exactly as before (through the renderer’s translate_expression adapter). Opaque to structural consumers — JSON emits the string as-is.

class dasmos.expr.Ref(runtime_addr: int)[source]

The label at runtime_addr, resolved to its best name at render time (falling back to a hex literal when no name exists). Generalises the disassembler’s late-bound deferred-expression mechanism: a label registered after the reference still surfaces symbolically.

class dasmos.expr.Str(text: str)[source]

A string literal. Not a numeric value on its own — it is the operand of a string operation (StrIndex, StrSlice, StrLen) or a string-typed data value.

class dasmos.expr.StrIndex(string: Expr, index: Expr)[source]

The character code of string at index (0-based) — an integer value. When both operands are constant it folds to a character-literal Int; otherwise each backend renders its own string-indexing syntax (64tass s[i], beebasm ASC(MID$(s, i+1, 1))).

class dasmos.expr.StrLen(string: Expr)[source]

The length of string — an integer. Folds to an Int when string is constant.

class dasmos.expr.StrSlice(string: Expr, start: Expr, stop: Expr | None = None)[source]

A substring of string from start to stop (exclusive; stop None means “to the end”) — a string value. Folds to a Str when its operands are constant.

class dasmos.expr.Sym(name: str)[source]

A bare symbolic identifier not tied to a resolvable address — a constant name, or a name the porter recovered from a legacy string. Emitted verbatim (it is already a valid identifier in every assembler).

class dasmos.expr.Unary(op: 'UnaryOp', operand: 'Expr')[source]
class dasmos.expr.UnaryOp(*values)[source]
BANKBYTE = 5

bank byte (65816), ^x — reserved

HIGHBYTE = 4

high byte, >x / HI(x)

INVERT = 6

bitwise complement, ~x

LOWBYTE = 3

low byte, <x / LO(x)

NEG = 1

arithmetic negation, -x

POS = 2

unary plus, +x

dasmos.expr.as_expr(value) → Expr[source]

Coerce a driver-supplied value to an Expr: an Expr is returned unchanged, an int becomes Int, and a str becomes Raw (the back-compat path). Used by the IR stores so both structured and legacy string values are accepted uniformly.

dasmos.expr.char(value: int) → Int[source]

An integer literal rendered as a character literal ('A').

dasmos.expr.declit(value: int) → Int[source]

An integer literal forced to decimal.

dasmos.expr.fold(e: Expr) → Expr[source]

Constant-fold string operations (and their sub-trees) as far as possible.

StrIndex(Str("BRK"), Int(0)) → Int(ord("B"), CHAR); StrSlice(Str("BRK"), 0, 2) → Str("BR"); StrLen(Str("BRK")) → Int(3). A string op whose operands are not all constant is returned with its children folded but otherwise intact (a backend then renders it natively). Non-string nodes are returned with their children folded so a string op nested inside arithmetic still collapses. Idempotent.

dasmos.expr.group(operand) → Group[source]

Force explicit parentheses around operand.

dasmos.expr.hexlit(value: int) → Int[source]

An integer literal forced to hex (& / $).

dasmos.expr.hi(operand) → Unary[source]

High byte of operand (> / HI).

dasmos.expr.lit(value: int, radix: Radix = Radix.AUTO) → Int[source]

An integer literal with an explicit radix (default AUTO).

dasmos.expr.lo(operand) → Unary[source]

Low byte of operand (< / LO).

dasmos.expr.macro_arg(value) → Expr[source]

Coerce a macro-call argument: a str becomes a string literal Str (macro args are values, not dialect expressions), an int an Int, and an Expr passes through.

dasmos.expr.param(name: str) → Param[source]

A macro formal parameter, for use inside a macro body.

dasmos.expr.raw(text: str) → Raw[source]

Wrap a pre-formatted dialect string as an opaque expression.

dasmos.expr.ref(runtime_addr: int) → Ref[source]

Reference the label at runtime_addr (resolved at render time).

dasmos.expr.str_len(s) → StrLen[source]

The length of a string expression.

dasmos.expr.string(text: str) → Str[source]

A string literal — index it (string("BRK")[0]) or slice it to build character/substring expressions.

dasmos.expr.sym(name: str) → Sym[source]

Reference a bare symbolic name, emitted verbatim.

Assembler-neutral expression trees.

A driver authors an operand or data value as an Expr — a small, immutable tree that carries intent (a label reference, a difference, a low byte) and never one assembler’s syntax. Each text renderer walks the tree and emits its own tokens, hex sigil, and — crucially — its own parenthesisation for its own operator precedence, so beebasm, 64tass and any future backend render the same tree correctly. The JSON renderer emits the tree structurally.

See docs/design/expression-system.md for the full design.

Drivers build trees through the ergonomic DSL re-exported from dasmos.expr (ref, sym, lo, hi, hexlit …) plus Python’s own operators:

lo(ref(0x8130) - 1)                 # low byte of (label at 0x8130 - 1)
(ref(a) - ref(b) - 1) & 0xFF        # masked label difference
(255 - ref(inkey_key_ctrl)) ^ 128   # INKEY form, backend-neutral

For backward compatibility, a plain str passed where an Expr is expected is wrapped as Raw — a pre-formatted string in the beebasm/py8dis dialect, rendered through the renderer’s translate_expression adapter and emitted verbatim by JSON.

class dasmos.core.expr.BinOp(*values)[source]

Bases: Enum

class dasmos.core.expr.Binary(op: 'BinOp', left: 'Expr', right: 'Expr')[source]

Bases: Expr

class dasmos.core.expr.Expr[source]

Bases: object

Abstract base for expression nodes.

Supplies Python operator overloads so trees read like arithmetic. Concrete nodes are frozen dataclasses, so they are hashable and comparable by value.

class dasmos.core.expr.Group(inner: Expr)[source]

Bases: Expr

Explicit parenthesisation the author wrote for readability, even where operator precedence makes it redundant. Always renders its parentheses, so (255 - x) EOR 128 keeps its grouping rather than collapsing to 255 - x EOR 128.

class dasmos.core.expr.Int(value: int, radix: Radix = Radix.AUTO)[source]

Bases: Expr

An integer literal. radix controls how it is spelled.

class dasmos.core.expr.MacroCall(name: str, args: tuple)[source]

Bases: Expr

An invocation of a macro, name(args…) — a value expression. Backends with value macros/functions (64tass .sfunction, ca65 .define) render it inline in a data directive; those without (beebasm, acme) render each call as its own statement line (handled by the data-block renderer).

class dasmos.core.expr.MacroDef(name: str, params: tuple, body: Expr, emit: str = 'byte')[source]

Bases: object

A macro definition: name(params) -> body, whose value is emitted as emit ("byte" / "word"). Not an expression — a top-level definition the renderer emits once and each MacroCall refers to.

class dasmos.core.expr.Param(name: str)[source]

Bases: Expr

A macro formal parameter, referenced inside a macro body. Renders as the backend’s parameter reference (both beebasm and 64tass functions use the bare name; 64tass macros would use \name). Being non-constant, string operations on a Param render natively rather than folding — which is exactly how a macro keeps the string argument symbolic in its body.

class dasmos.core.expr.Radix(*values)[source]

Bases: Enum

How an Int literal should be spelled.

AUTO = 1

renderer default (0-9 decimal, else hex)

CHAR = 5

a character literal, e.g. 'A'

class dasmos.core.expr.Raw(text: str)[source]

Bases: Expr

A pre-formatted expression string in the beebasm/py8dis dialect.

The back-compat / escape-hatch leaf: today’s string-based drivers are wrapped as Raw so they render exactly as before (through the renderer’s translate_expression adapter). Opaque to structural consumers — JSON emits the string as-is.

class dasmos.core.expr.Ref(runtime_addr: int)[source]

Bases: Expr

The label at runtime_addr, resolved to its best name at render time (falling back to a hex literal when no name exists). Generalises the disassembler’s late-bound deferred-expression mechanism: a label registered after the reference still surfaces symbolically.

class dasmos.core.expr.Str(text: str)[source]

Bases: Expr

A string literal. Not a numeric value on its own — it is the operand of a string operation (StrIndex, StrSlice, StrLen) or a string-typed data value.

class dasmos.core.expr.StrIndex(string: Expr, index: Expr)[source]

Bases: Expr

The character code of string at index (0-based) — an integer value. When both operands are constant it folds to a character-literal Int; otherwise each backend renders its own string-indexing syntax (64tass s[i], beebasm ASC(MID$(s, i+1, 1))).

class dasmos.core.expr.StrLen(string: Expr)[source]

Bases: Expr

The length of string — an integer. Folds to an Int when string is constant.

class dasmos.core.expr.StrSlice(string: Expr, start: Expr, stop: Expr | None = None)[source]

Bases: Expr

A substring of string from start to stop (exclusive; stop None means “to the end”) — a string value. Folds to a Str when its operands are constant.

class dasmos.core.expr.Sym(name: str)[source]

Bases: Expr

A bare symbolic identifier not tied to a resolvable address — a constant name, or a name the porter recovered from a legacy string. Emitted verbatim (it is already a valid identifier in every assembler).

class dasmos.core.expr.Unary(op: 'UnaryOp', operand: 'Expr')[source]

Bases: Expr

class dasmos.core.expr.UnaryOp(*values)[source]

Bases: Enum

BANKBYTE = 5

bank byte (65816), ^x — reserved

HIGHBYTE = 4

high byte, >x / HI(x)

INVERT = 6

bitwise complement, ~x

LOWBYTE = 3

low byte, <x / LO(x)

NEG = 1

arithmetic negation, -x

POS = 2

unary plus, +x

dasmos.core.expr.as_expr(value) → Expr[source]

Coerce a driver-supplied value to an Expr: an Expr is returned unchanged, an int becomes Int, and a str becomes Raw (the back-compat path). Used by the IR stores so both structured and legacy string values are accepted uniformly.

dasmos.core.expr.canonical_text(e: Expr) → str[source]

A neutral, IR-free string form of e for name tables, dedup, and the JSON text field. Uses the historical beebasm-flavoured spelling (& hex, AND/EOR). A Ref renders as a bare hex address (no labels to consult here); resolved names come from a renderer’s render_expression instead.

dasmos.core.expr.char(value: int) → Int[source]

An integer literal rendered as a character literal ('A').

dasmos.core.expr.declit(value: int) → Int[source]

An integer literal forced to decimal.

dasmos.core.expr.fold(e: Expr) → Expr[source]

Constant-fold string operations (and their sub-trees) as far as possible.

StrIndex(Str("BRK"), Int(0)) → Int(ord("B"), CHAR); StrSlice(Str("BRK"), 0, 2) → Str("BR"); StrLen(Str("BRK")) → Int(3). A string op whose operands are not all constant is returned with its children folded but otherwise intact (a backend then renders it natively). Non-string nodes are returned with their children folded so a string op nested inside arithmetic still collapses. Idempotent.

dasmos.core.expr.group(operand) → Group[source]

Force explicit parentheses around operand.

dasmos.core.expr.hexlit(value: int) → Int[source]

An integer literal forced to hex (& / $).

dasmos.core.expr.hi(operand) → Unary[source]

High byte of operand (> / HI).

dasmos.core.expr.lit(value: int, radix: Radix = Radix.AUTO) → Int[source]

An integer literal with an explicit radix (default AUTO).

dasmos.core.expr.lo(operand) → Unary[source]

Low byte of operand (< / LO).

dasmos.core.expr.macro_arg(value) → Expr[source]

Coerce a macro-call argument: a str becomes a string literal Str (macro args are values, not dialect expressions), an int an Int, and an Expr passes through.

dasmos.core.expr.param(name: str) → Param[source]

A macro formal parameter, for use inside a macro body.

dasmos.core.expr.raw(text: str) → Raw[source]

Wrap a pre-formatted dialect string as an opaque expression.

dasmos.core.expr.ref(runtime_addr: int) → Ref[source]

Reference the label at runtime_addr (resolved at render time).

dasmos.core.expr.str_len(s) → StrLen[source]

The length of a string expression.

dasmos.core.expr.string(text: str) → Str[source]

A string literal — index it (string("BRK")[0]) or slice it to build character/substring expressions.

dasmos.core.expr.substitute(e: Expr, bindings: dict) → Expr[source]

Replace every Param in e with its bound expression from bindings (keyed by parameter name), recursing through the whole tree. Used to inline-expand a macro: bind the call’s arguments to the definition’s parameters and substitute into the body, yielding an ordinary expression a backend can render without a macro construct. An unbound parameter is left as-is.

dasmos.core.expr.sym(name: str) → Sym[source]

Reference a bare symbolic name, emitted verbatim.

Output: text and structured

Renderer output types.

Every Renderer returns a subclass of Output. Output is always convertible to str via __str__; concrete subclasses can expose richer accessors for callers that want structured data without going through serialisation and back.

This means a uniform write-to-file idiom works for every output type — always pass an explicit encoding (UTF-8 unless you have a specific reason otherwise) so behaviour is identical across platforms; relying on the locale default produces cp1252 on Windows and silently mangles em-dashes:

Path("out.asm").write_text(str(beebasm_output), encoding="utf-8")
Path("out.json").write_text(str(json_output), encoding="utf-8")

…while structured renderers still expose their data:

data: dict = json_output.data
class dasmos.output.Output[source]

Bases: ABC

Base class for renderer output. Always stringifiable.

class dasmos.output.StructuredOutput(data: Any, *, indent: int | None = 2)[source]

Bases: Output

Output produced by a data-emitting renderer (Json, …).

The underlying data is exposed for direct structured access via data. __str__ serialises it to JSON with the configured indent; if a different serialisation is wanted, use data directly and serialise yourself.

class dasmos.output.TextOutput(text: str)[source]

Bases: Output

Output produced by a text-syntax renderer (Beebasm, ca65, …).

lines() → list[str][source]

Return the output split on line boundaries.

CPU extension point

CPU (processor) extension point and the abstract opcode shape.

Each CPU plug-in is a subclass of Cpu registered under the dasmos.cpu entry-point namespace. Concrete CPUs supply the instruction set, addressing modes, and the per-instruction tracing behaviour the disassembly core needs to follow control flow.

The initial target set is NMOS 6502, CMOS 65C02 and MC6809, with ambitions to grow into the wider Acorn lineage (Z80, 80186, 32016, ARM1/2/3/610/710).

Per docs/design/decisions.md D-006, the Cpu plug-in is pure data + small queries — it does not own the trace loop. The abstract methods declared here are the minimum the orchestrator (Disassembler) needs.

The Opcode shape is renderer-agnostic by design (per the conclusion of the stardot.org.uk design discussion linked in docs/design/decisions.md D-021): an opcode in the IR carries operation and addressing_mode enum members from the plug-in’s own enums plus a CPU-agnostic FlowControl for the trace engine, but does not commit to a final mnemonic. That choice belongs to the renderer — different assemblers spell the same opcode differently (LDA #imm in Beebasm vs LDAIM in Acorn MASM; JMP (...) vs JMI).

class dasmos.cpu.AddressingModeMember(*args, **kwargs)[source]

Bases: Protocol

Protocol every CPU’s Enum AddressingMode member must satisfy.

Each member exposes the byte length of the operand (so the trace engine can advance the program counter), the kind of the operand (so the trace engine and renderers know how to decode it), and the ReferenceKind (so use-site tracking knows whether the operand names the accessed address directly or merely an indexing base).

class dasmos.cpu.Cpu(name: str, **kwargs)[source]

Bases: Extension

Base class for CPU (processor) plug-ins.

Concrete subclasses model a specific processor — its instruction encoding, addressing modes, register set, and the semantics the tracing disassembler needs to follow control flow.

abstract property address_space_size: int

The number of distinct addressable bytes for this CPU.

0x10000 for 16-bit address spaces (6502, 6809, Z80); larger for 32-bit CPUs to come (ARM, 32016). The orchestrator uses this when sizing its MemoryImage and MoveManager.

initial_state() → CpuState | None[source]

Return a fresh CpuState for this CPU, or None to opt out of state tracking.

The Disassembler calls this once at the start of its post- trace state-computation pass. Override in CPU plug-ins that want their state tracked.

abstractmethod opcodes() → dict[int, Opcode][source]

The CPU’s instruction table.

Maps opcode byte (0x00–0xFF) to an Opcode. Bytes not in the dict are treated as undefined by the trace engine — for the NMOS 6502 plug-in the undocumented opcodes are deliberately omitted.

update_state(state, opcode: Opcode, addr: int, memory) → None[source]

Advance state to reflect executing opcode at the binary address addr. Mutates state in place.

memory is the disassembler’s MemoryImage so the update can read the operand byte(s) when relevant (e.g. LDA #imm reads the immediate from addr + 1).

No-op default — CPU plug-ins override.

exception dasmos.cpu.CpuExtensionError[source]

Bases: ExtensionError

Exception raised when a CPU extension cannot be loaded.

class dasmos.cpu.FlowControl(*values)[source]

Bases: Enum

How an opcode affects the trace engine’s notion of control flow.

CPU-agnostic — the trace loop dispatches on this enum to decide where to follow next.

BREAK = 'break'

Software interrupt or trap. Treated conservatively as a path terminator unless the plug-in says otherwise.

CONDITIONAL_BRANCH = 'conditional_branch'

Conditionally transfers control; both target and fall-through are reachable. e.g. BNE / BEQ.

JUMP = 'jump'

Unconditional control transfer; no fall-through. e.g. JMP.

RETURN = 'return'

Terminates the current trace path. e.g. RTS / RTI.

SEQUENTIAL = 'sequential'

Falls through to the next instruction. Most opcodes.

SUBROUTINE_CALL = 'subroutine_call'

Calls a subroutine; control eventually returns to the next instruction. e.g. JSR. Both target and fall-through are reachable.

UNDEFINED = 'undefined'

Undocumented or invalid opcode. Trace terminates here.

class dasmos.cpu.Opcode(operation: Enum, addressing_mode: AddressingModeMember, flow_control: FlowControl, cycles: int = 0)[source]

Bases: Classification

An entry in a CPU’s instruction table — and itself a Classification so it can be stored in the ClassificationStore alongside Byte, Word, etc.

Renderer-agnostic: carries the abstract operation and addressing_mode enum members plus a CPU-agnostic FlowControl. Renderers translate (operation, addressing_mode) to their preferred mnemonic spelling — typically via a per-pair lookup table that falls back to default_mnemonic().

Frozen and hashable: a single Opcode instance is shared across every binary address that uses this opcode byte.

default_mnemonic() → str[source]

Canonical mnemonic for this operation, used by __repr__ and as the fallback for renderers without a specific override for this (operation, addressing_mode) pair.

Returns the operation enum member’s value if it’s a string; otherwise its name. Plug-in authors should give their Operation enum members string values (the canonical lowercase MOS form for 6502: "lda", "jsr", …).

is_code(binary_addr=None) → bool[source]

Opcodes always represent code.

length() → int[source]

Total bytes consumed by this instruction (opcode byte + operand bytes). Method form for parity with the other Classification subclasses.

next_addresses(memory, binary_addr: int) → list[int][source]

Compute the next binary addresses to trace from this opcode located at binary_addr.

Returns one or two addresses according to flow control:

  • SEQUENTIAL → [fall_through].

  • JUMP → [target] if the target is computable; otherwise [].

  • SUBROUTINE_CALL → [fall_through, target] (target omitted if not computable; fall_through is where execution returns to after the called routine).

  • CONDITIONAL_BRANCH → [fall_through, target] (target omitted if not computable).

  • RETURN / BREAK / UNDEFINED → [] (control terminates here).

Targets that depend on memory the engine can’t read (e.g. JMP (addr) where the pointer’s bytes aren’t loaded) are silently dropped — the trace path narrows but doesn’t fail.

memory is the MemoryImage; binary_addr is the binary address of the opcode byte (an int for ergonomics, internally interpreted as a BinaryAddr).

property operand_length: int

Bytes consumed by the operand (delegates to the addressing mode).

class dasmos.cpu.OperandKind(*values)[source]

Bases: Enum

The semantics of the byte(s) following the opcode byte.

Used by the trace engine and by renderers to decode and present the operand correctly. CPU-agnostic for now; may grow as exotic addressing modes appear.

ADDRESS_16 = 'address_16'

A 16-bit address (absolute).

ADDRESS_16_INDIRECT = 'address_16_indirect'

A 16-bit address that is itself a pointer — the actual target is found by reading two bytes at that address. Used by the 6502’s JMP (addr) and the 65C02’s JMP (addr,X).

ADDRESS_8 = 'address_8'

An 8-bit address (zero-page).

IMMEDIATE = 'immediate'

A literal value (e.g. LDA #$3 — the operand is the value).

NONE = 'none'

No operand bytes (implied / accumulator).

RELATIVE_OFFSET = 'relative_offset'

A signed 8-bit offset added to the program counter at the next instruction (branches).

dasmos.cpu.cpu_names() → list[str][source]

Get the names of all available CPU plug-ins.

dasmos.cpu.cpu_type(cpu_name: str) → Type[Cpu][source]

Obtain the type of a CPU plug-in by name.

dasmos.cpu.create_cpu(cpu_name: str, **kwargs) → Cpu[source]

Create a CPU instance by name.

Parameters:
  • cpu_name – The name of the CPU to create (e.g. "6502", "65c02").

  • **kwargs – Forwarded to the CPU subclass constructor.

Returns:

A Cpu instance.

Raises:

CpuExtensionError – If the CPU cannot be loaded.

dasmos.cpu.describe_cpu(cpu_name: str, *, single_line: bool = False) → str[source]

Get the description of a CPU plug-in.

Renderer extension point

Renderer extension point.

A renderer turns the disassembly model — the IntermediateRepresentation — into output. Several renderers can run on the same IR: a single trace can produce both a Beebasm assembly source file and a structured JSON document.

Two abstract levels:

  • Renderer — the extension boundary. Every renderer registered under dasmos.renderer derives from this. Defines only one method: render(ir) -> Output.

  • TextRenderer — for renderers that produce assembler-syntax source text (Beebasm, ca65, acme, …). Adds the syntax protocol (hex2, byte_prefix, comment_prefix, …) every text renderer must implement. Returns TextOutput.

  • StructuredRenderer — for renderers that produce structured data (Json, …). Returns StructuredOutput.

Both TextRenderer and StructuredRenderer are conveniences; nothing prevents a renderer from subclassing Renderer directly if neither of the conveniences fits.

Design points (see docs/design/decisions.md D-003, D-009, D-015):

  • The extension is named renderer rather than assembler because the JSON renderer isn’t a text-syntax assembler.

  • pending_assertions and output_filename are per-instance, not class-level, so two renderer instances do not share state.

  • The protocol is declared with @abstractmethod: a plug-in that forgets to implement one of the syntax methods fails at instantiation, not on first call.

class dasmos.renderer.Renderer(name: str, **kwargs)[source]

Bases: Extension, Generic[T]

Extension boundary for output renderers.

Concrete renderers consume an IntermediateRepresentation and produce an Output subclass.

abstractmethod render(ir: IntermediateRepresentation) → T[source]

Produce output from an IR.

exception dasmos.renderer.RendererExtensionError[source]

Bases: ExtensionError

Exception raised when a renderer extension cannot be loaded.

class dasmos.renderer.StructuredRenderer(name: str, **kwargs)[source]

Bases: Renderer[StructuredOutput]

Base for structured-data renderers (Json, …).

No additional protocol beyond Renderer.render() — concrete structured renderers consume the IR however they like and return a StructuredOutput.

class dasmos.renderer.TextRenderer(name: str, **kwargs)[source]

Bases: Renderer[TextOutput]

Base for text-syntax (assembler-source) renderers.

Adds the assembler-syntax protocol every text renderer must implement. The methods grouped under “syntax” below are abstract; those under “policy” have sensible defaults.

address_link_hex(hex_str: str) → str[source]

Format the hex from an address: cross-reference URI (see dasmos.core.markdown_asm) as this assembler’s literal when the ?hex flag flattens a link into comment text.

Defaults to beebasm’s &-prefixed uppercase form; override for a different sigil (64tass $). Note this deliberately preserves the URI’s hex width and uppercases it, rather than going through hex2()/hex4(), to match the established comment-link convention.

abstractmethod byte_prefix() → str[source]

The directive that emits raw bytes (e.g. equb).

abstractmethod char_literal(n: int) → str | None[source]

A single-character literal for the byte n, or None.

abstractmethod code_end() → list[str][source]

Lines emitted when ending a code block.

abstractmethod code_start(start_addr, end_addr, first: bool) → list[str][source]

Lines emitted when starting to render a code block.

abstractmethod comment_prefix() → str[source]

The character(s) introducing a line comment.

abstractmethod cpus_supported() → list[str][source]

Names of the CPU plug-ins this renderer can target.

abstractmethod disassembly_end() → list[str][source]

Lines emitted at the bottom of the output.

abstractmethod disassembly_start() → list[str][source]

Lines emitted at the top of the output.

abstractmethod explicit_label(name: str, value, offset: int | None = None, align_column: int = 0) → str[source]

Render an name = value definition.

force_abs_instruction(instruction: str, prefix: str, operand: str, suffix: str) → str | None[source]

Instruction text that forces absolute (16-bit) addressing, or None when the assembler has no syntax for it.

Called for an instruction encoded with absolute addressing whose operand is below &100 and whose mnemonic also has a zero-page form — the case a size-optimising assembler would otherwise shrink to two bytes. instruction is the mnemonic, operand the resolved symbol, prefix / suffix the surrounding mode punctuation ("" / ",x" / ",y"). 64tass returns sta @w name; the default None makes the renderer emit the instruction as data instead.

force_zp_instruction(instruction: str, prefix: str, operand: str, suffix: str) → str | None[source]

Instruction text that forces zero-page (8-bit) addressing, or None when the assembler has no syntax for it.

Called for a zero-page instruction whose operand names a label defined later in the listing, when the assembler’s first pass would otherwise guess the absolute form (see AssemblerRenderer.forward_reference_assumes_zero_page). Arguments as for force_abs_instruction(). 64tass returns lda @b name; the default None makes the renderer emit the instruction as data instead.

abstractmethod hex2(n: int) → str[source]

Format an 8-bit value as the renderer’s hex literal.

abstractmethod hex4(n: int) → str[source]

Format a 16-bit value as the renderer’s hex literal.

abstractmethod inline_label(name: str) → str[source]

Render an inline label definition (e.g. .foo or foo:).

abstractmethod pseudopc_end(*, dest: int, src: int, length: int, move_id: int, src_label: str | None, dest_label: str | None) → list[str][source]

Lines leaving a relocated block and restoring the PC.

Beebasm emits the copyblock / clear / restore-org idiom that copies the assembled bytes back to their file position; 64tass emits .here (which restores the compile offset implicitly). This asymmetry — beebasm needs an explicit restore, 64tass does not — is exactly why relocation lives behind this hook rather than in the shared walk.

abstractmethod pseudopc_start(*, dest: int, src: int, length: int, move_id: int, src_label: str | None, dest_label: str | None) → list[str][source]

Lines entering a relocated (pseudo-PC) block.

The shared move walk has already positioned the PC at src and emitted src_label (if any); this switches the PC into the relocated destination range so the block’s bytes resolve as if running at dest. Beebasm emits an org &<dest>; 64tass .logical $<dest>. The full move context is passed so each backend uses what its idiom needs (beebasm labels a descriptive comment; 64tass needs only dest).

abstractmethod set_origin(addr: int) → list[str][source]

Lines that set the assembly program counter to addr.

Used by the shared walk whenever emission needs to (re)position the PC at a plain address outside a code block — resuming after a skipped (move-source) range, positioning a move’s source bytes, or advancing to the loaded range’s end before the boundary marker. Beebasm emits org &<addr>; 64tass *=. The list may include surrounding blank lines for layout.

abstractmethod string_chr(n: int) → str | None[source]

The character n as it appears inside a string literal, or None if the assembler can’t include that character.

abstractmethod string_prefix() → str[source]

The directive that emits a string (e.g. equs).

abstractmethod word_prefix() → str[source]

The directive that emits 16-bit words (e.g. equw).

dasmos.renderer.create_renderer(renderer_name: str, **kwargs) → Renderer[source]

Create a renderer instance by name.

dasmos.renderer.describe_renderer(renderer_name: str, *, single_line: bool = False) → str[source]

Get the description of a renderer plug-in.

dasmos.renderer.renderer_names() → list[str][source]

Get the names of all available renderer plug-ins.

dasmos.renderer.renderer_type(renderer_name: str) → Type[Renderer][source]

Obtain the type of a renderer plug-in by name.

Environment extension point

Environment extension point.

An environment supplies pre-existing knowledge about the platform a ROM runs on — typically OS-call labels, hardware-register addresses, vector positions, and (eventually) the comment-emitting hooks that explain OS-call argument conventions. Environments factor knowledge that’s stable across many ROMs (e.g. “the BBC Micro’s MOS provides OSWRCH at &FFEE”) out of every individual driver script.

Environments are composable: a Disassembler can have any number of environments active at once, applied in order. Each Environment.setup() runs against the disassembler at the time of activation; later environments see the cumulative state of all earlier ones. Effects layer additively — registering the same label twice is a harmless no-op (the LabelManager deduplicates), and registering different names at the same address yields aliases.

This factoring lets per-platform knowledge be split into orthogonal slices that drivers mix as needed:

  • acorn-mos — MOS workspace + vectors + OS-call labels

  • acorn-bbc-hardware — BBC Micro hardware addresses (Tube ULA, …)

  • acorn-sideways-rom — sideways-ROM header convention

A driver targeting an Acorn sideways ROM that runs on the host BBC Micro selects the union:

d = Disassembler.create(
    cpu="6502",
    environments=["acorn-mos", "acorn-bbc-hardware", "acorn-sideways-rom"],
)

Each environment is a Stevedore plug-in registered under the dasmos.environment entry-point namespace; third-party packages register additional environments the same way the bundled ones do.

class dasmos.environment.Environment(name: str, **kwargs)[source]

Bases: Extension

Base class for environment plug-ins.

Override setup() to register the labels, comments, hooks, classifications, etc. that constitute the environment.

Environments are usually parameterless — they act on a Disassembler by side effect rather than carrying their own state. Override the constructor only if your environment needs configuration that varies per use site.

abstractmethod setup(disassembler: Disassembler) → None[source]

Apply this environment’s effects to disassembler.

Called by Disassembler.use_environment() (or by the Disassembler constructor when environments= is passed). Implementations register labels, classifications, comments, hooks, etc. via the public driver-script API on disassembler.

Idempotent in spirit: if the same environment is activated twice on the same disassembler, the second call should be a no-op (the underlying managers handle duplicate registrations cleanly — registering the same label twice is harmless).

exception dasmos.environment.EnvironmentExtensionError[source]

Bases: ExtensionError

dasmos.environment.create_environment(name: str, **kwargs) → Environment[source]

Create an Environment instance by name.

Parameters:
  • name – The name of the environment to create (e.g. "acorn-mos").

  • **kwargs – Forwarded to the Environment subclass constructor.

Returns:

An Environment instance.

Raises:

EnvironmentExtensionError – If the environment cannot be loaded.

dasmos.environment.describe_environment(name: str, *, single_line: bool = False) → str[source]

Get the description of an environment plug-in.

dasmos.environment.environment_names() → list[str][source]

Get the names of all available environment plug-ins.

dasmos.environment.environment_type(name: str) → Type[Environment][source]

Obtain the type of an environment plug-in by name.

Extension base

Generic extension / plug-in machinery built on top of stevedore.

Extension classes are loaded via entry points. Each Extension subclass declares a kind (a short identifier such as "cpu" or "assembler") and is registered under a namespace of the form "dasmos.<kind>" in pyproject.toml. Consumers then use create_extension(), extension(), or list_extensions() to load them.

Lifted from asyoulikeit.extension with light modification: the namespace prefix in Extension.entry_point_name() is "dasmos" rather than "asyoulikeit", and the base error type is DasmosError.

class dasmos.extension.Extension(name: str, **kwargs)[source]

Bases: ABC

classmethod brief_description() → str[source]

Pithy single-line summary, suitable for a table cell.

Default implementation returns the first non-empty line of full_description(). Override on the subclass when the docstring’s first line would be too long for a list-style rendering — set this to a separately-authored, deliberately terse summary.

Used by list-<kind> CLI commands.

classmethod describe(*, single_line: bool = False) → str[source]

Convenience wrapper around brief_description() / full_description().

Parameters:

single_line – If True, return brief_description(). Otherwise return full_description().

classmethod dirpath() → Path[source]

The directory path to the extension package.

classmethod entry_point_name() → str[source]

Get the entry point name (key) for this extension class.

This performs a reverse lookup from class to entry point name by searching through all extensions in the appropriate namespace. The result is cached indefinitely since entry point names are immutable.

Returns:

The entry point name for this extension class.

Raises:

ExtensionError – If this class is not registered as an extension.

classmethod full_description() → str[source]

The complete, multi-paragraph description.

Default implementation returns the cleaned class docstring. Override only if the description should come from somewhere other than the docstring (rare).

Used by describe-<kind> CLI commands.

classmethod kind() → str[source]

The kind of extension.

Used to distinguish extension points.

classmethod version() → str[source]

The extension version.

exception dasmos.extension.ExtensionError[source]

Bases: DasmosError

dasmos.extension.create_extension(kind, namespace, name, exception_type=None, **kwargs) → Extension[source]

Instantiate an extension.

Parameters:
  • kind – The kind of extension.

  • namespace – The namespace for the extension.

  • name – The name of the extension to be loaded.

  • exception_type – The exception type to be raised if the extension couldn’t be loaded.

  • **kwargs – Keyword arguments forwarded to the extension constructor.

Returns:

An extension.

dasmos.extension.describe_extension(kind, namespace, name, exception_type=None, *, single_line: bool = False) → str[source]

Describe an extension by name.

Parameters:
  • kind – The kind of extension.

  • namespace – The namespace for the extension.

  • name – The name of the extension.

  • exception_type – The exception type to be raised if the extension couldn’t be loaded.

  • single_line – If True, return only the first non-empty line of the description.

Returns:

A string describing the extension.

dasmos.extension.extension(kind: str, namespace: str, name: str, exception_type: BaseException) → Type[Extension][source]

Get the extension class without instantiating it.

Lookup is case-insensitive: --cpu 65c02 resolves to the registered 65C02 plug-in. Hyphens in the requested name are also normalised to underscores. The exact registered name is what’s used to instantiate the extension.

Parameters:
  • kind – The kind of extension.

  • namespace – The namespace for the extension.

  • name – The name of the extension to be loaded.

  • exception_type – The exception type to be raised if the extension couldn’t be loaded.

Returns:

The type (i.e. class) of an extension.

dasmos.extension.extension_name_from_class(namespace: str, extension_class: Type[Extension]) → str[source]

Get the entry point name for an extension class.

This performs a reverse lookup from class to entry point name by iterating through all extensions in the namespace until finding one whose plugin matches the given class.

Parameters:
  • namespace – The namespace to search (e.g., ‘dasmos.cpu’)

  • extension_class – The extension class to find

Returns:

The entry point name (key) for the extension

Raises:

ExtensionError – If the extension class is not found in the namespace

dasmos.extension.list_dirpaths(namespace)[source]

A mapping of extension names to extension package paths.

dasmos.extension.list_extensions(namespace) → list[str][source]

List the names of the extensions available in a given namespace.

Exceptions

Exception hierarchy for the dasmos package.

exception dasmos.exceptions.DasmosError[source]

Bases: Exception

Base class for all exceptions raised by the dasmos package.

exception dasmos.exceptions.MacroRenderError[source]

Bases: DasmosError

Raised when a macro invocation is malformed — the named macro is not defined, or the argument count does not match the definition’s parameters. (A macro a backend can’t render natively is not an error: dasmos falls back to inline-expanding it.)