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) orINLINE; 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 explicitequb).
- class dasmos.AnnotationStore[source]
Per-disassembly registry of
CommentandAnnotationentries 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
entryatbinary_addr.Issues a
UserWarningwhen 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_addrwhose alignment matches.
- is_auto_suppressed_at(binary_addr) bool[source]
Return True if any annotation at
binary_addrcarries thesuppresses_autoflag.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 withsuppresses_auto=Truein 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.
0x10000for 16-bit address spaces (6502, 6809, Z80); larger for 32-bit CPUs to come (ARM, 32016). The orchestrator uses this when sizing itsMemoryImageandMoveManager.
- initial_state() CpuState | None[source]
Return a fresh
CpuStatefor this CPU, orNoneto 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 anOpcode. 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
stateto reflect executingopcodeat the binary addressaddr. Mutatesstatein place.memoryis the disassembler’sMemoryImageso the update can read the operand byte(s) when relevant (e.g.LDA #immreads the immediate fromaddr + 1).No-op default — CPU plug-ins override.
- 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
IntermediateRepresentationviadisassemble().- add_move(dest_runtime_addr, src_binary_addr, length: int, *, name: str | None = None) Move[source]
Register a relocation. Returns the
Movehandle.The Move is itself a context manager —
with move: ...scopes annotations under it. Passname=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.mdC2/C3 for the rationale.auto_generatedmarks this banner as env-attached metadata rather than user-authored content (parallel to the same kwarg oncomment()). 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 sametitle/descriptionkwargs.
- byte(runtime_addr, length: int = 1, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]
Mark
lengthbytes atruntime_addras raw bytes.Pass
override=Trueto clear any conflicting classification in the range first (see_override_clear()); otherwise an overlap raisesClassificationError.
- char_literal(runtime_addr, *, move: Move | None = None)[source]
Sugar for
format_hint(runtime_addr, FormatHint.CHAR).Declares that the operand byte at
runtime_addris intended as an ASCII character literal. Renderers express this in their target assembler’s grammar — beebasm emitsASC("c")or'c'(per the renderer’schar_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) andruntime_addr_hi(high half — defaults toruntime_addr_lo + 1for the common adjacent-bytes case), computes the target as(hi << 8) | lo) + offset, registersentry(target, name=label_name), and sets per-byte expression overrides so the source bytes render symbolically (equw <label>orequb < (<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.aligncontrols 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;INLINEappends it to the code line itself; the other positions (AFTER_LABEL,BEFORE_LINE,AFTER_LINE) cover less-common arrangements.auto_generatedmarks 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 insetup()(e.g."Sideways ROM header"at &8000) should set this.suppresses_automarks this comment as authoritative: env analysers andsetup()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 beforeuse_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_idif 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
constantssection.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&7cwould suddenly render aslda osbyte_clear_escape— a false positive.The
BeebasmRendereremits constants as equates in their own block alongside the label-equate table.If
namematches a deprecated alias registered by an active environment (seeregister_deprecated_constant_name()), aDeprecationWarningis 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(). TheConstantrecords here also appear as optional labels inlabels(for asm equate emission).
- classmethod create(cpu: str | Cpu, **kwargs: Any) Disassembler[source]
Construct a
Disassemblerwith a string-named CPU plug-in or with an explicit instance.Strings are resolved via Stevedore from the
dasmos.cpuentry-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
paramsand return a callable that builds invocations of it.paramsis a list of parameter names; insidebodyrefer to them withdasmos.expr.param().emitis 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.sfunctionused inline in.byte pack("LDA"), or a beebasmMACROemitting the byte, invokedpack "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:
Trace — follow control flow from every registered entry point, classifying each instruction.
Leftover classification — bytes loaded but not reached by trace (and not pre-classified by the user) become
Byte1-byte items.Reference analysis — walk every classified opcode and record use sites against any label at the operand target.
Auto-label generation (when
auto_labels_enabled) — synthesise names for referenced addresses without explicit names. Names land in the LabelManager withis_autogenerated=Trueso 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
nameis given, also defines a label at the same runtime address (any extra kwargs are forwarded tolabel()).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_romclassifying a non-JMPentry slot as data. The trace cannot reclassify already-classified bytes, so a plainentry()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 aUserWarning— 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 afteruse_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_addrwith the givenexpression.expressionis preferably an assembler-neutralExprbuilt 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 plainstris still accepted for backward compatibility — it is wrapped as aRawnode and emitted verbatim (via the renderer’s dialect adapter), exactly as before.runtime_addris the runtime address of the operand byte — typically one past the opcode for a single-byte-opcode CPU like the 6502. For example, anLDA #$03instruction at&8000has its operand byte at&8001; callingd.expr(0x8001, ref(num_lives) + 1)makes the renderer emitlda #num_lives + 1for 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(seeset_file_header()).
- fill(runtime_addr, length: int, value: int | None = None, *, move: Move | None = None, override: bool = False)[source]
Mark a run of
lengthidentical bytes atruntime_addr.valueis the byte the fill should expand to. If omitted, it’s inferred from the loaded memory (the byte atruntime_addr) — the natural shape for runs of the same value the binary already contains. Ifvalueis supplied AND disagrees with the loaded byte, raisesDisassemblerError.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- format_hint(runtime_addr, hint: FormatHint, *, move: Move | None = None)[source]
Mark the operand byte at
runtime_addrwith 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():exprsubstitutes a concrete text expression;format_hintdeclares intent and leaves the syntax to the renderer. When both are registered at the same address,exprwins (the user’s explicit text is more specific than a category).See
dasmos.core.format_hint.FormatHintfor the full set of hints.char_literal()is a sugar for theCHARcase.
- 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.hooksfor the bundled hooks (stringhi_hook,stringz_hook, …) and the protocol details.Also registers
nameas an optional label atruntime_addrso 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
lengthbytes atruntime_addras 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 (beebasmincbin "<path>"; 64tass.binary "<path>") instead of a wall ofequblines. 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 topathnext 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.pathis the include path as it should appear in the directive (typically a bare filename beside the listing). Passoverride=Trueto clear a conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- 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,Yetc., where the byte actually touched isbase + registerandbaseitself is never read or written.Behaves like
optional_label()(the name is emitted only if referenced, so thebase,Xoperand can resolve) but asserts the base (b) access flag on the label. That keeps anydescription=/group=/length=the author supplies — so the base is still documented — and the address takes its place on the memory map carryingaccessincludingb(a base-only row:bwith nor/wsays 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 derivesbautomatically 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_addrgets the explicit, always-emitted labelname(so theanchor±karithmetic always resolves), plus anydescription/group/access— exactly aslabel()would, so the anchor takes its normal place on the memory map.window=(lo, hi)is the inclusive offset range the region owns (lotypically ≤ 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=Trueeach gap gets a distinct identifier (name_m<k>/name_p<k>) instead of the defaultname±karithmetic form.Windows must be disjoint; an overlap with an already-declared region raises
RegionError. Seedocs/design/reference-kinds-memo.md.
- property index_regions: list[IndexRegion]
Indexing regions registered via
index_region(), in declaration order. Each anchor also appears inlabels.
- inkey_code(runtime_addr, *, move: Move | None = None)[source]
Sugar for
format_hint(runtime_addr, FormatHint.INKEY).Declares that the operand byte at
runtime_addris the negative-X form of an Acorn INKEY scan code (the byte loaded into X forOSBYTE &79/OSBYTE &81to scan for a specific key by its internal number). Renderers express the byte symbolically as(255 - inkey_key_<name>) EOR 128when 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_addrcarriessuppresses_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. WrapsAnnotationStore.is_auto_suppressed_at()with the same runtime→binary address resolution ascomment()andlabel(), so envs can pass the natural runtime address.
- load(filepath, binary_addr, md5sum: str | None = None)[source]
Load a binary file at
binary_addr. Delegates toload.
- 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
*RUNprogram loads at one address but may begin execution at another (itsexec_addr), and the filesystem records areload_addrdistinct 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
savedirective (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 sofantasm verify— unaffected.
Every argument is optional; an omitted address is simply not recorded.
reload_addrdefaults 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(seeprogram()).
- register_data_type(name: str, data_type: DataType) None[source]
Register a custom
DataTypeundernamesotyped_data()can resolve it by string.Environments call this from
setupto contribute domain types (e.g.bbc_basic_6502registers"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 withold_nameemit aDeprecationWarningcitingnew_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 tocode_ptr()withoffset=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).titleanddescriptionare Markdown, exactly like every driver comment (seecomment()): 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
lengthbytes atruntime_addras a string.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- 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 with0x0Das the terminator; the terminator byte is included in the classified span (used.byteif 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
0byte; 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). Whenis_entry_pointis 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
titleordescription(oron_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 inlabels.
- 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_typemay be:a string — resolved from the registry populated by
register_data_type()(typically by an environment);a
DataTypeinstance — used directly (no registration needed);a bare callable
bytes -> str | DecodedValue— for a one-off decode; this form requires an explicitlengthsince a plain function carries no width.
The width is implied by the type (a
DataTypeowns itslength); passlengthonly to validate, and a disagreeing value raises. The region is classified asByte(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.commentis an optional human note rendered after the decoded value.override=Trueclears any conflicting classification in the range first (see_override_clear()).
- use_environment(env, **kwargs) None[source]
Activate an environment plug-in on this disassembler.
envmay be a string (the registered name, resolved via Stevedore) or anEnvironmentinstance.Any extra keyword arguments are forwarded to the Environment subclass constructor — only valid when
envis 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_environmentmore 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 AFTERload()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
lengthbytes atruntime_addras 16-bit words.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- exception dasmos.DisassemblerError[source]
Raised on misuse of the
Disassemblerorchestration.
- 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 returnfull_description().
- 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.
- 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 (
%01010101in 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..9rule); an explicitDECIMALhint widens that choice to the whole byte range.
- HEX = 'hex'
Render in base 16 with the assembler’s hex sigil (
&xxfor beebasm,$xxfor ca65 / ACME,0xXXfor 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) orOSBYTE &81(INKEY) when scanning for a specific key by its internal number rather than its ASCII representation. The byte valueXdecodes viakey_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 128so 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_mosenv 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(seeDisassembler.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(seeDisassembler.program()).
- render(renderer: Renderer | str, **kwargs: Any) Output[source]
Render this IR via the named or supplied renderer.
renderermay be:a string — the renderer plug-in name; resolved via Stevedore using
create_renderer(). Extrakwargsare forwarded to the renderer’s constructor.a
Rendererinstance — used directly;kwargsare ignored.
- property subroutines
Subroutine metadata records registered via
Disassembler.subroutine(). Returned in registration order.
- write_included_binaries(dirpath) list['Path'][source]
Write each
include_binaryregion’s owned bytes to its declared path, underdirpath.Every
IncludedBinaryregion 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 resolveincbin/.binaryand 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. RaisesValueErrorif 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
Classificationso it can be stored in theClassificationStorealongsideByte,Word, etc.Renderer-agnostic: carries the abstract
operationandaddressing_modeenum members plus a CPU-agnosticFlowControl. Renderers translate(operation, addressing_mode)to their preferred mnemonic spelling — typically via a per-pair lookup table that falls back todefault_mnemonic().Frozen and hashable: a single
Opcodeinstance 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
valueif it’s a string; otherwise itsname. Plug-in authors should give theirOperationenum members string values (the canonical lowercase MOS form for 6502:"lda","jsr", …).
- length() int[source]
Total bytes consumed by this instruction (opcode byte + operand bytes). Method form for parity with the other
Classificationsubclasses.
- 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_throughis 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.memoryis theMemoryImage;binary_addris the binary address of the opcode byte (anintfor ergonomics, internally interpreted as aBinaryAddr).
- 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’sJMP (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.Renderer(name: str, **kwargs)[source]
Extension boundary for output renderers.
Concrete renderers consume an
IntermediateRepresentationand produce anOutputsubclass.- 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
datais exposed for direct structured access viadata.__str__serialises it to JSON with the configured indent; if a different serialisation is wanted, usedatadirectly 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 aStructuredOutput.
- class dasmos.TextOutput(text: str)[source]
Output produced by a text-syntax renderer (Beebasm, ca65, …).
- 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.
- address_link_hex(hex_str: str) str[source]
Format the hex from an
address:cross-reference URI (seedasmos.core.markdown_asm) as this assembler’s literal when the?hexflag 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 throughhex2()/hex4(), to match the established comment-link convention.
- abstractmethod char_literal(n: int) str | None[source]
A single-character literal for the byte
n, orNone.
- abstractmethod code_start(start_addr, end_addr, first: bool) list[str][source]
Lines emitted when starting to render a code block.
- abstractmethod cpus_supported() list[str][source]
Names of the CPU plug-ins this renderer can target.
- abstractmethod explicit_label(name: str, value, offset: int | None = None, align_column: int = 0) str[source]
Render an
name = valuedefinition.
- force_abs_instruction(instruction: str, prefix: str, operand: str, suffix: str) str | None[source]
Instruction text that forces absolute (16-bit) addressing, or
Nonewhen the assembler has no syntax for it.Called for an instruction encoded with absolute addressing whose operand is below
&100and whose mnemonic also has a zero-page form — the case a size-optimising assembler would otherwise shrink to two bytes.instructionis the mnemonic,operandthe resolved symbol,prefix/suffixthe surrounding mode punctuation (""/",x"/",y"). 64tass returnssta @w name; the defaultNonemakes 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
Nonewhen 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 forforce_abs_instruction(). 64tass returnslda @b name; the defaultNonemakes the renderer emit the instruction as data instead.
- abstractmethod inline_label(name: str) str[source]
Render an inline label definition (e.g.
.fooorfoo:).
- 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-orgidiom 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
srcand emittedsrc_label(if any); this switches the PC into the relocated destination range so the block’s bytes resolve as if running atdest. Beebasm emits anorg &<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 onlydest).
- 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.
- 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
Cpuinstance.- 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.
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; theDisassembler.create()factory accepts a string and resolves via Stevedore.D-009:
disassemble()returns anIntermediateRepresentation; 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:
objectA 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 separateconstantssection in structured output. TheDisassembler.constant()method ALSO callsDisassembler.optional_label()so the asm equate emission is unchanged from the legacyconstant() → optional_labelporter 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:
objectThe orchestration class.
Owns one instance of each manager class; delegates the flat driver-script API to those managers; produces an
IntermediateRepresentationviadisassemble().- add_move(dest_runtime_addr, src_binary_addr, length: int, *, name: str | None = None) Move[source]
Register a relocation. Returns the
Movehandle.The Move is itself a context manager —
with move: ...scopes annotations under it. Passname=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.mdC2/C3 for the rationale.auto_generatedmarks this banner as env-attached metadata rather than user-authored content (parallel to the same kwarg oncomment()). 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 sametitle/descriptionkwargs.
- byte(runtime_addr, length: int = 1, cols: int | None = None, *, move: Move | None = None, override: bool = False)[source]
Mark
lengthbytes atruntime_addras raw bytes.Pass
override=Trueto clear any conflicting classification in the range first (see_override_clear()); otherwise an overlap raisesClassificationError.
- char_literal(runtime_addr, *, move: Move | None = None)[source]
Sugar for
format_hint(runtime_addr, FormatHint.CHAR).Declares that the operand byte at
runtime_addris intended as an ASCII character literal. Renderers express this in their target assembler’s grammar — beebasm emitsASC("c")or'c'(per the renderer’schar_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) andruntime_addr_hi(high half — defaults toruntime_addr_lo + 1for the common adjacent-bytes case), computes the target as(hi << 8) | lo) + offset, registersentry(target, name=label_name), and sets per-byte expression overrides so the source bytes render symbolically (equw <label>orequb < (<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.aligncontrols 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;INLINEappends it to the code line itself; the other positions (AFTER_LABEL,BEFORE_LINE,AFTER_LINE) cover less-common arrangements.auto_generatedmarks 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 insetup()(e.g."Sideways ROM header"at &8000) should set this.suppresses_automarks this comment as authoritative: env analysers andsetup()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 beforeuse_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_idif 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
constantssection.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&7cwould suddenly render aslda osbyte_clear_escape— a false positive.The
BeebasmRendereremits constants as equates in their own block alongside the label-equate table.If
namematches a deprecated alias registered by an active environment (seeregister_deprecated_constant_name()), aDeprecationWarningis 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(). TheConstantrecords here also appear as optional labels inlabels(for asm equate emission).
- classmethod create(cpu: str | Cpu, **kwargs: Any) Disassembler[source]
Construct a
Disassemblerwith a string-named CPU plug-in or with an explicit instance.Strings are resolved via Stevedore from the
dasmos.cpuentry-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
paramsand return a callable that builds invocations of it.paramsis a list of parameter names; insidebodyrefer to them withdasmos.expr.param().emitis 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.sfunctionused inline in.byte pack("LDA"), or a beebasmMACROemitting the byte, invokedpack "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:
Trace — follow control flow from every registered entry point, classifying each instruction.
Leftover classification — bytes loaded but not reached by trace (and not pre-classified by the user) become
Byte1-byte items.Reference analysis — walk every classified opcode and record use sites against any label at the operand target.
Auto-label generation (when
auto_labels_enabled) — synthesise names for referenced addresses without explicit names. Names land in the LabelManager withis_autogenerated=Trueso 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
nameis given, also defines a label at the same runtime address (any extra kwargs are forwarded tolabel()).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_romclassifying a non-JMPentry slot as data. The trace cannot reclassify already-classified bytes, so a plainentry()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 aUserWarning— 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 afteruse_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_addrwith the givenexpression.expressionis preferably an assembler-neutralExprbuilt 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 plainstris still accepted for backward compatibility — it is wrapped as aRawnode and emitted verbatim (via the renderer’s dialect adapter), exactly as before.runtime_addris the runtime address of the operand byte — typically one past the opcode for a single-byte-opcode CPU like the 6502. For example, anLDA #$03instruction at&8000has its operand byte at&8001; callingd.expr(0x8001, ref(num_lives) + 1)makes the renderer emitlda #num_lives + 1for 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(seeset_file_header()).
- fill(runtime_addr, length: int, value: int | None = None, *, move: Move | None = None, override: bool = False)[source]
Mark a run of
lengthidentical bytes atruntime_addr.valueis the byte the fill should expand to. If omitted, it’s inferred from the loaded memory (the byte atruntime_addr) — the natural shape for runs of the same value the binary already contains. Ifvalueis supplied AND disagrees with the loaded byte, raisesDisassemblerError.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- format_hint(runtime_addr, hint: FormatHint, *, move: Move | None = None)[source]
Mark the operand byte at
runtime_addrwith 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():exprsubstitutes a concrete text expression;format_hintdeclares intent and leaves the syntax to the renderer. When both are registered at the same address,exprwins (the user’s explicit text is more specific than a category).See
dasmos.core.format_hint.FormatHintfor the full set of hints.char_literal()is a sugar for theCHARcase.
- 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.hooksfor the bundled hooks (stringhi_hook,stringz_hook, …) and the protocol details.Also registers
nameas an optional label atruntime_addrso 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
lengthbytes atruntime_addras 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 (beebasmincbin "<path>"; 64tass.binary "<path>") instead of a wall ofequblines. 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 topathnext 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.pathis the include path as it should appear in the directive (typically a bare filename beside the listing). Passoverride=Trueto clear a conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- 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,Yetc., where the byte actually touched isbase + registerandbaseitself is never read or written.Behaves like
optional_label()(the name is emitted only if referenced, so thebase,Xoperand can resolve) but asserts the base (b) access flag on the label. That keeps anydescription=/group=/length=the author supplies — so the base is still documented — and the address takes its place on the memory map carryingaccessincludingb(a base-only row:bwith nor/wsays 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 derivesbautomatically 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_addrgets the explicit, always-emitted labelname(so theanchor±karithmetic always resolves), plus anydescription/group/access— exactly aslabel()would, so the anchor takes its normal place on the memory map.window=(lo, hi)is the inclusive offset range the region owns (lotypically ≤ 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=Trueeach gap gets a distinct identifier (name_m<k>/name_p<k>) instead of the defaultname±karithmetic form.Windows must be disjoint; an overlap with an already-declared region raises
RegionError. Seedocs/design/reference-kinds-memo.md.
- property index_regions: list[IndexRegion]
Indexing regions registered via
index_region(), in declaration order. Each anchor also appears inlabels.
- inkey_code(runtime_addr, *, move: Move | None = None)[source]
Sugar for
format_hint(runtime_addr, FormatHint.INKEY).Declares that the operand byte at
runtime_addris the negative-X form of an Acorn INKEY scan code (the byte loaded into X forOSBYTE &79/OSBYTE &81to scan for a specific key by its internal number). Renderers express the byte symbolically as(255 - inkey_key_<name>) EOR 128when 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_addrcarriessuppresses_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. WrapsAnnotationStore.is_auto_suppressed_at()with the same runtime→binary address resolution ascomment()andlabel(), so envs can pass the natural runtime address.
- load(filepath, binary_addr, md5sum: str | None = None)[source]
Load a binary file at
binary_addr. Delegates toload.
- local_label(runtime_addr, name: str, start_addr, end_addr, **kwargs)[source]
Define a label scoped to
[start_addr, end_addr).
- 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
*RUNprogram loads at one address but may begin execution at another (itsexec_addr), and the filesystem records areload_addrdistinct 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
savedirective (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 sofantasm verify— unaffected.
Every argument is optional; an omitted address is simply not recorded.
reload_addrdefaults 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(seeprogram()).
- register_data_type(name: str, data_type: DataType) None[source]
Register a custom
DataTypeundernamesotyped_data()can resolve it by string.Environments call this from
setupto contribute domain types (e.g.bbc_basic_6502registers"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 withold_nameemit aDeprecationWarningcitingnew_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 tocode_ptr()withoffset=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).titleanddescriptionare Markdown, exactly like every driver comment (seecomment()): 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
lengthbytes atruntime_addras a string.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- 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 with0x0Das the terminator; the terminator byte is included in the classified span (used.byteif 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
0byte; 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). Whenis_entry_pointis 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
titleordescription(oron_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 inlabels.
- 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_typemay be:a string — resolved from the registry populated by
register_data_type()(typically by an environment);a
DataTypeinstance — used directly (no registration needed);a bare callable
bytes -> str | DecodedValue— for a one-off decode; this form requires an explicitlengthsince a plain function carries no width.
The width is implied by the type (a
DataTypeowns itslength); passlengthonly to validate, and a disagreeing value raises. The region is classified asByte(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.commentis an optional human note rendered after the decoded value.override=Trueclears any conflicting classification in the range first (see_override_clear()).
- use_environment(env, **kwargs) None[source]
Activate an environment plug-in on this disassembler.
envmay be a string (the registered name, resolved via Stevedore) or anEnvironmentinstance.Any extra keyword arguments are forwarded to the Environment subclass constructor — only valid when
envis 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_environmentmore 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 AFTERload()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
lengthbytes atruntime_addras 16-bit words.Pass
override=Trueto clear any conflicting classification in the range first; otherwise an overlap raisesClassificationError.
- exception dasmos.disassembler.DisassemblerError[source]
Bases:
DasmosErrorRaised on misuse of the
Disassemblerorchestration.
- 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:
objectSubroutine 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:
objectRead-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(seeDisassembler.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(seeDisassembler.program()).
- render(renderer: Renderer | str, **kwargs: Any) Output[source]
Render this IR via the named or supplied renderer.
renderermay be:a string — the renderer plug-in name; resolved via Stevedore using
create_renderer(). Extrakwargsare forwarded to the renderer’s constructor.a
Rendererinstance — used directly;kwargsare ignored.
- property subroutines
Subroutine metadata records registered via
Disassembler.subroutine(). Returned in registration order.
- write_included_binaries(dirpath) list['Path'][source]
Write each
include_binaryregion’s owned bytes to its declared path, underdirpath.Every
IncludedBinaryregion 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 resolveincbin/.binaryand 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. RaisesValueErrorif two regions declare the same path with differing bytes (a genuine inconsistency the author must resolve).
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:
Explicit driver calls — e.g.
Disassembler.char_literal()for theCHARhint. The driver author knows the byte’s intent even when no auto-detection heuristic could.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:
EnumAbstract 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 (
%01010101in 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..9rule); an explicitDECIMALhint widens that choice to the whole byte range.
- HEX = 'hex'
Render in base 16 with the assembler’s hex sigil (
&xxfor beebasm,$xxfor ca65 / ACME,0xXXfor 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) orOSBYTE &81(INKEY) when scanning for a specific key by its internal number rather than its ASCII representation. The byte valueXdecodes viakey_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 128so 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_mosenv 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:
objectPer-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 (unlikeExpressionRegistrywhere multiple passes can re-register the same expression text).- add(binary_addr, hint: FormatHint) None[source]
Register
hintatbinary_addr.Replaces any previously-registered hint at the same address.
- get(binary_addr) FormatHint[source]
Return the hint registered at
binary_addr.Raises
KeyErrorif there is none — useget_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.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 128keeps its grouping rather than collapsing to255 - x EOR 128.
- class dasmos.expr.Int(value: int, radix: Radix = Radix.AUTO)[source]
An integer literal.
radixcontrols 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 asemit("byte"/"word"). Not an expression — a top-level definition the renderer emits once and eachMacroCallrefers 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 aParamrender 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
Intliteral 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
Rawso they render exactly as before (through the renderer’stranslate_expressionadapter). 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
stringatindex(0-based) — an integer value. When both operands are constant it folds to a character-literalInt; otherwise each backend renders its own string-indexing syntax (64tasss[i], beebasmASC(MID$(s, i+1, 1))).
- class dasmos.expr.StrLen(string: Expr)[source]
The length of
string— an integer. Folds to anIntwhenstringis constant.
- class dasmos.expr.StrSlice(string: Expr, start: Expr, stop: Expr | None = None)[source]
A substring of
stringfromstarttostop(exclusive;stopNonemeans “to the end”) — a string value. Folds to aStrwhen 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.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: anExpris returned unchanged, anintbecomesInt, and astrbecomesRaw(the back-compat path). Used by the IR stores so both structured and legacy string values are accepted uniformly.
- 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.lit(value: int, radix: Radix = Radix.AUTO) Int[source]
An integer literal with an explicit
radix(default AUTO).
- dasmos.expr.macro_arg(value) Expr[source]
Coerce a macro-call argument: a
strbecomes a string literalStr(macro args are values, not dialect expressions), anintanInt, and anExprpasses through.
- 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.string(text: str) Str[source]
A string literal — index it (
string("BRK")[0]) or slice it to build character/substring expressions.
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.Expr[source]
Bases:
objectAbstract 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:
ExprExplicit parenthesisation the author wrote for readability, even where operator precedence makes it redundant. Always renders its parentheses, so
(255 - x) EOR 128keeps its grouping rather than collapsing to255 - x EOR 128.
- class dasmos.core.expr.Int(value: int, radix: Radix = Radix.AUTO)[source]
Bases:
ExprAn integer literal.
radixcontrols how it is spelled.
- class dasmos.core.expr.MacroCall(name: str, args: tuple)[source]
Bases:
ExprAn 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:
objectA macro definition:
name(params) -> body, whose value is emitted asemit("byte"/"word"). Not an expression — a top-level definition the renderer emits once and eachMacroCallrefers to.
- class dasmos.core.expr.Param(name: str)[source]
Bases:
ExprA 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 aParamrender 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:
EnumHow an
Intliteral 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:
ExprA pre-formatted expression string in the beebasm/py8dis dialect.
The back-compat / escape-hatch leaf: today’s string-based drivers are wrapped as
Rawso they render exactly as before (through the renderer’stranslate_expressionadapter). Opaque to structural consumers — JSON emits the string as-is.
- class dasmos.core.expr.Ref(runtime_addr: int)[source]
Bases:
ExprThe 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:
ExprA 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:
ExprThe character code of
stringatindex(0-based) — an integer value. When both operands are constant it folds to a character-literalInt; otherwise each backend renders its own string-indexing syntax (64tasss[i], beebasmASC(MID$(s, i+1, 1))).
- class dasmos.core.expr.StrLen(string: Expr)[source]
Bases:
ExprThe length of
string— an integer. Folds to anIntwhenstringis constant.
- class dasmos.core.expr.StrSlice(string: Expr, start: Expr, stop: Expr | None = None)[source]
Bases:
ExprA substring of
stringfromstarttostop(exclusive;stopNonemeans “to the end”) — a string value. Folds to aStrwhen its operands are constant.
- class dasmos.core.expr.Sym(name: str)[source]
Bases:
ExprA 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.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: anExpris returned unchanged, anintbecomesInt, and astrbecomesRaw(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
efor name tables, dedup, and the JSONtextfield. Uses the historical beebasm-flavoured spelling (&hex,AND/EOR). ARefrenders as a bare hex address (no labels to consult here); resolved names come from a renderer’srender_expressioninstead.
- dasmos.core.expr.char(value: int) Int[source]
An integer literal rendered as a character literal (
'A').
- 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.lit(value: int, radix: Radix = Radix.AUTO) Int[source]
An integer literal with an explicit
radix(default AUTO).
- dasmos.core.expr.macro_arg(value) Expr[source]
Coerce a macro-call argument: a
strbecomes a string literalStr(macro args are values, not dialect expressions), anintanInt, and anExprpasses 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.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
Paraminewith its bound expression frombindings(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.
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.StructuredOutput(data: Any, *, indent: int | None = 2)[source]
Bases:
OutputOutput produced by a data-emitting renderer (Json, …).
The underlying
datais exposed for direct structured access viadata.__str__serialises it to JSON with the configured indent; if a different serialisation is wanted, usedatadirectly and serialise yourself.
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:
ProtocolProtocol every CPU’s
EnumAddressingModemember 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:
ExtensionBase 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.
0x10000for 16-bit address spaces (6502, 6809, Z80); larger for 32-bit CPUs to come (ARM, 32016). The orchestrator uses this when sizing itsMemoryImageandMoveManager.
- initial_state() CpuState | None[source]
Return a fresh
CpuStatefor this CPU, orNoneto 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 anOpcode. 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
stateto reflect executingopcodeat the binary addressaddr. Mutatesstatein place.memoryis the disassembler’sMemoryImageso the update can read the operand byte(s) when relevant (e.g.LDA #immreads the immediate fromaddr + 1).No-op default — CPU plug-ins override.
- exception dasmos.cpu.CpuExtensionError[source]
Bases:
ExtensionErrorException raised when a CPU extension cannot be loaded.
- class dasmos.cpu.FlowControl(*values)[source]
Bases:
EnumHow 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:
ClassificationAn entry in a CPU’s instruction table — and itself a
Classificationso it can be stored in theClassificationStorealongsideByte,Word, etc.Renderer-agnostic: carries the abstract
operationandaddressing_modeenum members plus a CPU-agnosticFlowControl. Renderers translate(operation, addressing_mode)to their preferred mnemonic spelling — typically via a per-pair lookup table that falls back todefault_mnemonic().Frozen and hashable: a single
Opcodeinstance 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
valueif it’s a string; otherwise itsname. Plug-in authors should give theirOperationenum members string values (the canonical lowercase MOS form for 6502:"lda","jsr", …).
- length() int[source]
Total bytes consumed by this instruction (opcode byte + operand bytes). Method form for parity with the other
Classificationsubclasses.
- 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_throughis 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.memoryis theMemoryImage;binary_addris the binary address of the opcode byte (anintfor ergonomics, internally interpreted as aBinaryAddr).
- property operand_length: int
Bytes consumed by the operand (delegates to the addressing mode).
- class dasmos.cpu.OperandKind(*values)[source]
Bases:
EnumThe 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’sJMP (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.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
Cpuinstance.- Raises:
CpuExtensionError – If the CPU cannot be loaded.
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 underdasmos.rendererderives 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. ReturnsTextOutput.StructuredRenderer— for renderers that produce structured data (Json, …). ReturnsStructuredOutput.
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
rendererrather thanassemblerbecause the JSON renderer isn’t a text-syntax assembler.pending_assertionsandoutput_filenameare 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]
-
Extension boundary for output renderers.
Concrete renderers consume an
IntermediateRepresentationand produce anOutputsubclass.- abstractmethod render(ir: IntermediateRepresentation) T[source]
Produce output from an IR.
- exception dasmos.renderer.RendererExtensionError[source]
Bases:
ExtensionErrorException 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 aStructuredOutput.
- 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 (seedasmos.core.markdown_asm) as this assembler’s literal when the?hexflag 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 throughhex2()/hex4(), to match the established comment-link convention.
- abstractmethod char_literal(n: int) str | None[source]
A single-character literal for the byte
n, orNone.
- abstractmethod code_start(start_addr, end_addr, first: bool) list[str][source]
Lines emitted when starting to render a code block.
- abstractmethod cpus_supported() list[str][source]
Names of the CPU plug-ins this renderer can target.
- abstractmethod explicit_label(name: str, value, offset: int | None = None, align_column: int = 0) str[source]
Render an
name = valuedefinition.
- force_abs_instruction(instruction: str, prefix: str, operand: str, suffix: str) str | None[source]
Instruction text that forces absolute (16-bit) addressing, or
Nonewhen the assembler has no syntax for it.Called for an instruction encoded with absolute addressing whose operand is below
&100and whose mnemonic also has a zero-page form — the case a size-optimising assembler would otherwise shrink to two bytes.instructionis the mnemonic,operandthe resolved symbol,prefix/suffixthe surrounding mode punctuation (""/",x"/",y"). 64tass returnssta @w name; the defaultNonemakes 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
Nonewhen 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 forforce_abs_instruction(). 64tass returnslda @b name; the defaultNonemakes the renderer emit the instruction as data instead.
- abstractmethod inline_label(name: str) str[source]
Render an inline label definition (e.g.
.fooorfoo:).
- 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-orgidiom 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
srcand emittedsrc_label(if any); this switches the PC into the relocated destination range so the block’s bytes resolve as if running atdest. Beebasm emits anorg &<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 onlydest).
- 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.
- 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.
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 labelsacorn-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:
ExtensionBase 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 whenenvironments=is passed). Implementations register labels, classifications, comments, hooks, etc. via the public driver-script API ondisassembler.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
Environmentinstance.- 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.
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 returnfull_description().
- 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.
- 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 65c02resolves to the registered65C02plug-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.
Exceptions
Exception hierarchy for the dasmos package.
- exception dasmos.exceptions.DasmosError[source]
Bases:
ExceptionBase class for all exceptions raised by the dasmos package.
- exception dasmos.exceptions.MacroRenderError[source]
Bases:
DasmosErrorRaised 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.)