rg-asm
"I'll have what she's having!"
rg-asm is a universal assembler for Atari ST / TT / Falcon development.
It supports a wide range of assembler dialects, and can create executables, linker objects or binary blobs and has support for all the 680x0 series CPUs (including coldfire),
But don't we already have assemblers for the atari? Why another one?
The impetus for this came from my many years writing assembly code across a range of different assemblers.
I started writing assembly "back in the day"(tm), and travelled the world of atari assemblers, bringing my code with me. From days in the dessert of the original devpac, through the blockbuster sequel (devpac 2) and the much maligned godfather 3-esque finale, through flirtations with turbo assembler, to bridging the boundary to C with assemblers for lattice c, pure assembler and vasm. With each migration, code had to be changed and modified, new bugs emerged. And once you migrated, you had to do the same process if you wanted to go back.
Wouldn't it be nice if there was an assembler that worked with all these dialects? No more migrations?
I have also recently been trawling through ggn's excellent corpus of atari code at https://github.com/ggnkua/Atari_ST_Sources, an important historical artifact for the scene.
But although the code is there, actually getting this to compile requires a plethora of different tools, or modifications to this historical set of code.
Wouldn't it be nice if there was a single assembler that could assemble all this historical code?
There was also a set of conversations I had with Havok and others at the recent Sommarhack demo party about the cultural importance of the demoscene. Some European countries are not starting to recognise the demoscene as a being a part of their national cultural heritage. We have an unique artform that is worth preserving, and many of the original pieces of code that make the games and demos we love is no longer useable.
In the games industry, when we complete a game, we make a "closing kit" where we bundle the code up with all the tools needed to build it, so if some in the future needs to port the title to a new platform or remaster it, they have a fully working starting point. We don't have that for atari productions.
So here we are, in the 2k26, with a new assembler for atari code; a universal assembler, that assembles all your (and other folks) old code, no matter what hairy old assembler it was originally written in.
And of course, being an rg prod, it has a whole range of features, including a bunch of metrics that show you cycle costs (either per line or per function), and is aware of instruction pairing.
It can even optimise and re-order your code for you in pairing efficient ways!
This is the first in a new series of productivity tools for atari development from rg. Keep your eyes on those blue, blue skies.
Contents
Part 1 — Command-line tool interface Example usage · Output files · Options at a glance · Options in detail · Size optimisations · Instruction reordering · Dialects · Errors & exit status
Part 2 — Assembler syntax Comments · Labels · Numbers & expressions · Sections · Data & storage · Alignment · Symbols & equates · org · Macros · Conditionals · Repeats · Includes · Linkage · Directive reference
Part 3 — JSON schema
Global --json mode · Envelope · The schema
Also: Greetings · Final notes · License
Documentation
Part 1 — Command-line tool interface
Example usage
rg-asm [OPTIONS] <inputs>...
<inputs> are .S / .ASM / .I source files and/or .o object files, in any
combination. By default, rg-asm assembles and links them into a GEMDOS
executable; any .o input is only meaningful to the linker, so its presence
implies an executable.
# Assemble and link into a GEMDOS executable — the default. -> MAIN.PRG
rg-asm MAIN.S UTIL.S
# Objects instead, to link with C later — one .o per source
rg-asm --obj MAIN.S UTIL.S
# A raw flat image: no header, no symbols, no relocations (boot sectors, blobs)
rg-asm --raw BOOT.S # -> BOOT.BIN
# Link pre-built objects into a named executable
rg-asm MAIN.o UTIL.o -o GAME.TOS
# Assemble and link in one step, choosing the output name
rg-asm MAIN.S UTIL.o -o GAME.TOS
# Realistic build: CPU, defines, include path, external symbol sidecar
rg-asm -m 68000 -D DEBUG=1 -I GODLIB CODE/MAIN.S CODE/GFX.S \
-o BIN/DEBUG/GAME.TOS --sym=extern
# 68030 Falcon build
rg-asm -m 68030 -I GODLIB CODE/MAIN.S -o GAME.TOS
# ColdFire Firebee build
rg-asm --cf-firebee game.s -o GAME.TOS
# Strict assembly of new code, one warning category promoted to an error
rg-asm new.s --warn-level=3 -Werror=undefined-equate -o NEW.TOS
# GenPC flat image with OVR reloc table and trailing-zero strip
rg-asm --dialect=genpc --raw --reloc --strip-bss BOOT.S
CLI syntax:
- Short flags take a space-separated value (
-o FILE,-m 68000,-I DIR,-D NAME) - Long flags require
=(--dialect=vasm,--sym=extern,--warn-level=3) - Optional flags have a default value or can explicitly specifiy
=VALUEe.g. (--obj=gst)
Output files
For header layouts, object-format magics, and relocation details, see the dedicated output formats reference.
| Extension | Content |
|---|---|
.o | ELF32 relocatable object (assemble-only output; not executable). |
.TOS / .PRG | Atari GEMDOS executable: header + text + data + BSS + optional symbol table. Big-endian; GEMDOS-style (bitfield-encoded) relocations. |
.BIN | Raw flat image (--raw): no header, symbols, or relocations. |
.sym | Plain-text symbol sidecar, written only with --sym=extern. |
Source files are read as UTF-8, falling back to ISO-8859-1 (Latin-1) for the accented characters common in Atari ST source comments.
Options at a glance
| Group | Options |
|---|---|
| Input / output | <inputs>... · -o <FILE> / --output=<FILE> · -I / --include=<DIR> · -D / --define=<NAME[=VALUE]> |
| Output kind | --prg (default) · --obj[=KIND] · --raw · --reloc · --strip-bss · --no-emit |
| Target | -m <CPU> / --cpu=<CPU> · --fpu=<FPU> · --cf-isa=<REV> · --cf-firebee · --cf-* |
| Dialect | --dialect=<D> · --strict-dialect · --allmp · --c-precedence |
| Symbols & linkage | --sym=<FMT> · --strip · --sym-locals · --sym-abs=<STYLE> · --sym-order=<ORDER> · --pic · --relax · --allow-undefined · --long-addresses · --error-on-undefined · --sozobon-long-names · --rgcc-tag · --prg-flags=<FLAGS> |
| Vendor CLI | --errors=<FILE> · --header=<FILE> · --equ-file=<FILE> |
| Optimisation | -O/--opt-size (default — patterns) · --no-opt-size · --size-opt=TOKEN (repeatable) · --opt-reorder · --opt-forward-branches / --no-opt-forward-branches · --opt-relax-passes=N |
| Listing / cycles | --listing / --listing=<FILE> · --listing-cpus=<LIST> · --cycles / --cycles=<FILE> · --functions · --json · -q/--quiet |
| Diagnostics | --warn-level=<0..3> · -W<spec> / --warn=<spec> · -w / --no-warnings · --list-warnings |
| Presentation | --color=<WHEN> · --ascii · --banner=<STYLE> |
| Info | -h/--help · -V/--version · --dump-tokens |
Options in detail
Input / output
| Option | Meaning |
|---|---|
<inputs>... | Source (.S/.ASM/.I) and/or object (.o) files, any mix. Assembled in order, then linked. |
-o <FILE> / --output=<FILE> | Output path, honoured verbatim. Defaults derive from the input: <input>.PRG / .o / .BIN per output kind. With --obj/--raw and multiple inputs, -o is rejected — omit it for per-file names. |
-I <DIR> | Add an include search path (repeatable). Applies to INCLUDE / INCBIN. |
-D <NAME[=VALUE]> | Define an equate on the command line (repeatable). -DDEBUG sets DEBUG=1; VALUE must be an integer. Visible to IFD/IFND and as a symbol everywhere. |
Output kind
This determines what is emitted from the assembler:
| Flag | Output | Default name | Addresses resolved by |
|---|---|---|---|
--prg (default) | GEMDOS executable | <input>.PRG | GEMDOS PEXEC (relocation table) |
--obj[=KIND] | linkable object, one per source | <input>.o | The linker |
--raw | raw flat image — no header, symbols or relocations | <input>.BIN | Unresolved (should be PC relative or ORG absolute) |
| --reloc | With --raw, append a GenPC/OVR flat relocation table after the image (as68 -r) | (same as --raw) | sc68 loader / host tools |
| --strip-bss | With --raw, strip trailing zero bytes from the flat image (sc68 -t / --strip-bss) | (same as --raw) | Smaller boot blobs |
The --obj=KIND object format selects which linker the object targets:
KIND | Object format | Consumed by |
|---|---|---|
elf (default) | ELF32 big-endian m68k | vlink / vbcc / gcc-binutils |
gst | GST linkable object (classic Devpac/HiSoft .O) | Devpac / HiSoft linkers |
purec | Pure C object (Atari group 1) | Pure C / Lattice C / AHCC linkers (pc alias) |
aout | BSD/MiNT a.out relocatable | MiNT / a.out toolchains |
dri | DRI / Alcyon object ($601A + word-parallel relocs) | Alcyon, RMAC family, GenST |
lout (mwc) | Mark Williams C l.out | MWC as |
seka | Andelos / K-Seka linkable | K-Seka |
hunk (amiga) | Lattice Amiga hunk object | Lattice Amiga tools |
As raw has no relocation table, you should either make it PC-relative or use org.
org sets an absolute origin for --raw and is ignored by other output types.
.PRG/.TOS/.APP/.GTP/.ACC are ultimately the same — the extension
only tells the GEM desktop how to launch it. The default extension for executables is .PRG
Target CPU / FPU
-m <CPU> / --cpu=<CPU> selects the 680x0 target instruction set (default 68000).
Mutually exclusive with the ColdFire selectors (--cf-isa / --cf-firebee).
<CPU> | Adds over the previous model |
|---|---|
68000 | Baseline instruction set (the plain ST). |
68010 | VBR, loop mode, MOVEC/MOVES, MOVE from CCR. |
68020 | Full 32-bit: bit-field ops, MULx.L/DIVx.L, CAS/CAS2, scaled-index and memory-indirect addressing. |
68030 | On-chip MMU (PMOVE/PTEST); the integer set matches the 68020. |
68040 | Integrated FPU and MMU; MOVE16 burst copy. |
68060 | Superscalar 68040-class core; same user integer/FPU set (some rare ops trap to emulation). |
--fpu=<FPU> attaches an external FPU coprocessor to a CPU without an on-chip one
(68000–68030). The 68040/68060 have an integrated FPU selected automatically from
--cpu, so --fpu is ignored with a 040/060 target.
<FPU> | Meaning |
|---|---|
none (default) | No FPU; floating-point instructions are rejected. |
68881 | Original 68881 coprocessor — the full 6888x floating-point instruction set. |
68882 | Pin-compatible faster part; identical instruction set to the 68881. |
ColdFire
ColdFire has two configuration axes; the ISA and feature modules.
Selecting any --cf-isa (or --cf-firebee) makes the build a ColdFire target — it replaces --cpu.
Selecting the target — either of these makes the build a ColdFire target:
| Option | Meaning |
|---|---|
--cf-isa=<a|aplus|b|c> | Select a ColdFire target at this integer ISA revision (see the sub-table). Its presence is the ColdFire selector. |
--cf-firebee | "Firebee" V4e preset (MCF547x/MCF548x): --cf-isa=b + divide + EMAC + USP + FPU. --cf-isa/--cf-* layer on top. |
The ISA revision is a cumulative ladder — each level is a superset of the one above it:
<REV> | Core | Hardware divide | Adds over the previous revision |
|---|---|---|---|
a | ISA_A | off | Baseline: a subset of the 68000 plus selected 68020 enhancements. |
aplus | ISA_A+ | on | A handful of instructions, including TAS.B. |
b | ISA_B | on | Re-introduces CMP.B/.W, CMPA.W and the 32-bit Bcc.L; adds MOV3Q/BYTEREV/MVS/MVZ/FF1/SATS. |
c | ISA_C | on | The full integer superset (V1 core): BITREV/INTOUCH/STLDSR. |
Hardware divide defaults on for aplus/b/c and off for a. MOVE to/from USP is gated
by the separate USP module (below), not by the ISA revision.
Feature modules — each has an enable and a disable, so a preset can be trimmed (e.g.
--cf-firebee --cf-no-fpu is the Firebee bundle without the FPU). If both are given, off wins.
| Option | Module |
|---|---|
--cf-div / --cf-no-div | Hardware-divide unit (DIVU/DIVS/REMU/REMS). |
--cf-mac / --cf-no-mac | MAC unit (single accumulator). |
--cf-emac / --cf-no-emac | EMAC unit (four accumulators). |
--cf-usp / --cf-no-usp | Separate user stack pointer (MOVE to/from USP). |
--cf-fpu / --cf-no-fpu | On-chip FPU. |
Dialect
--dialect=<D> selects the source syntax variant. It has its own section: see
Dialects.
When you pass an explicit --dialect=<name> (not the implicit default
vasm), rg-asm merges encode/link preset knobs for that dialect (Alcyon
long addresses, Devpac1 strip, RMAC sym discovery, …). Output kind stays
--prg unless you also pass --obj, --raw, or --no-emit — dialect alone
does not silently retarget to objects or flat binaries. Regression/oracle
harnesses pass the full vendor argv (--raw, --obj=dri, …) explicitly; see
CLI presets.
| Option | Meaning |
|---|---|
--strict-dialect | Reject dialect-specific superset features (TurboAss macros, …). With --dialect=gas or mri, also promote unknown-mnemonic, directive-dropped, and parser-heuristic to hard errors (LLVM/rustc ingest safety). |
--allmp | Mot -allmp: enable \a..\z as macro arguments 10..35 (Devpac already on). |
--c-precedence | With --dialect=rmac, use C operator precedence (-4 / --c-precedence). |
Symbols & linkage
| Option | Meaning |
|---|---|
--sym=<dri|gst|extern> | Symbol-table format for the .TOS (see the sub-table). Default gst. |
--strip | Strip the symbol table from the output .TOS. |
--sym-locals | Include local (non-exported) labels and equates in the symbol table — helps recover local names when disassembling. Off by default. Under --dialect=rmac/madmac, DRI uses bare .local names, omits linker __*, types equates as 0xC000 and .abs labels as 0x8000, and emits symbols in discovery order unless overridden below. |
--sym-abs=<vlink|rmac> | Absolute/equate DRI type style: vlink → 0x8200 (DEFINED|TEXT), rmac → 0xC000 (DEFINED|EQUATED). Default follows --dialect (rmac/madmac → rmac). |
--sym-order=<alpha|discovery> | Symbol-table entry order. Default follows --dialect (rmac/madmac → discovery). |
--pic | Enforce PC-relative mode; error on any absolute relocation. |
--relax | GAS -mrelax: emit relocations for same-section PC-relative branches instead of resolving them at assembly time, so the linker can fix them during size-changing relaxation. Only meaningful for relocatable object output. |
--allow-undefined | Do not error on undefined symbols at link time. |
--long-addresses | AS68 -l: emit absolute address constants as longwords (disables abs.l→abs.w size opts). |
--error-on-undefined | vasm -x: treat undefined symbol references as hard errors at assembly time. |
--sozobon-long-names | Sozobon jas -8: do not truncate DRI symbol names to 8 characters. |
--rgcc-tag | Stamp the RGCC toolchain-provenance trampoline at the start of TEXT (off by default). |
--prg-flags=<FLAGS> | GEMDOS header flags (comma-separated): fastload, fastram, fastalloc, private, global, super, readable. |
<FMT> | Symbol table | Notes |
|---|---|---|
dri | DRI/TOS format embedded in the executable | Understood by most Atari debuggers; symbol names are truncated to 8 characters. |
gst (default) | GST extended format embedded in the executable | Longer names; required by some profilers. |
extern | None embedded — a plain-text .sym sidecar is written | nm-style listing next to the executable; keeps the .TOS itself table-free. |
Vendor CLI passthrough
| Option | Meaning |
|---|---|
--errors=<FILE> | Write diagnostics to FILE (rmac -e, Josy -E). |
--header=<FILE> | Metacomco HDR=: prepend this file to each assembly source before parse. |
--equ-file=<FILE> | Metacomco EQU=: load equates from FILE (name=value or name equ value lines). |
Optimisation
There is one size master switch, one speed knob, and two branch-sizing knobs:
| Option | Default | Meaning |
|---|---|---|
-O, --opt-size | on | Enable the dialect's size-opt preset — see size optimisations. |
--no-opt-size | — | Disable all size transforms (instruction selection, peeps, span sizing) for layout-stable output. |
--opt-reorder | off | Reorder instructions for speed, not size — see reordering. |
--opt-forward-branches | dialect | Allow auto-shortening of forward unsized branches (vasm-class default on; RMAC/MADMAC default off). |
--no-opt-forward-branches | — | Only auto-shorten backward unsized branches (host rmac +O2 behaviour). |
--opt-relax-passes=N | 50 | Cap size-relaxation re-passes after the first pass-1 layout. |
--size-opt=TOKEN | — | RMAC-style per-bit toggles applied after the master switch (--size-opt=+O4, --size-opt=~Oall, …). Repeatable / comma-separated. Unknown tokens are a hard error — see size optimisations. |
Given --opt-size/--no-opt-size both on one command line, the later wins. Same for
--opt-forward-branches / --no-opt-forward-branches.
In source, Devpac OPT O+ / O- and RMAC .opt "+Oall" / "~Oall" flip the whole
preset; RMAC .opt "+On" / "~On" (n = 0…9) toggles one transform. A line-leading
! (RMAC/MADMAC) suppresses every size opt for that instruction only.
On the command line, --size-opt=TOKEN applies the same RMAC-style toggles after the
master switch (-O / --no-opt-size). Valid tokens (case-insensitive): O+, O-,
+Oall, ~Oall, +O0…+O9, ~O0…~O9. Numbers outside 0…9 that RMAC ignores on
56001 / PC-relative opts (+O10, +O11, +O30, …) are accepted as no-ops. Any other
string is rejected with a non-zero exit (fail closed).
Size optimisations
Your source stays as written; only the emitted bytes change. The default (vasm) preset
only enables transforms that are faster or neutral on every 680x0, including
JMP/JSR → BRA/BSR (vasm -phxass). RMAC/MADMAC enable LEA d(An),An → ADDQ
(RMAC +O4, slower on 68030+) but keep JSR/JMP as written — matching host
rmac, where +O2 only shortens already-written branches. Explicit size suffixes
(.s, .w, .l) always win over the flag.
Cycle counts match the --cpu target (default 68000), as reported by --listing.
Shorter encodings — a smaller form of the same instruction (gated by the master
switch / per-bit flags; --no-opt-size keeps the long form):
| Original | Optimised | Cycles | Bytes | ||
|---|---|---|---|---|---|
| before | after | before | after | ||
move.l #42,d0 | MOVEQ #42,D0 | 12 | 4 | 6 | 2 |
add.l #4,d1 | ADDQ.L #4,D1 | 14 | 8 | 6 | 2 |
sub.w #3,d2 | SUBQ.W #3,D2 | 8 | 4 | 4 | 2 |
clr.l d2 | MOVEQ #0,D2 | 6 | 4 | 2 | 2 |
adda.l #200,a0 | LEA (200,A0),A0 | 14 | 8 | 6 | 4 |
MOVEQ applies for N ∈ −128..=127 and .L only; ADDQ/SUBQ for N ∈ 1..=8; the LEA
form when n fits signed 16-bit. Note clr.l d2 is the one row that saves no bytes — it is a pure
cycle win.
Instruction rewrites — a different, shorter instruction with identical effect and identical
condition codes. Applied under -O:
| Original | Optimised | Cycles | Bytes | ||
|---|---|---|---|---|---|
| before | after | before | after | ||
cmp.w #0,d0 | TST.W D0 | 8 | 4 | 4 | 2 |
cmpi.l #0,d1 | TST.L D1 | 14 | 4 | 6 | 2 |
or.l #0,d2 | TST.L D2 | 14 | 4 | 6 | 2 |
move.l #0,a0 | SUBA.L A0,A0 | 12 | 8 | 6 | 2 |
EOR/EORI #0,Dn folds to TST as well, on the same terms. The rewrites hold because each
original computes its destination unchanged and sets N/Z from it with V=C=0 — exactly
TST's effect — while MOVE #0,An and SUBA.L An,An both leave An = 0 without touching the
CCR. Only literal #0 immediates fire, so a symbol that happens to evaluate to zero is left
alone.
Span-dependent sizing — the forms whose smallest encoding depends on how far the target is.
The assembler picks the shortest form that still reaches, growing across passes until the section
stabilises (--opt-relax-passes=N caps the loop). With --opt-forward-branches (vasm default)
unsized forward branches may shrink; with --no-opt-forward-branches (RMAC/MADMAC default) only
backward unsized branches auto-shorten. --no-opt-size forces the largest encoding of each:
| Original | Optimised | Cycles | Bytes | ||
|---|---|---|---|---|---|
| before | after | before | after | ||
bra done (near) | BRA.S | 10 | 10 | 4 | 2 |
jmp handler (near) | BRA.S | 12 | 10 | 6 | 2 |
move.w $ffff8800,d0 | (xxx).W | 16 | 12 | 6 | 4 |
The unoptimised forms here are BRA.W, JMP (xxx).L (which also carries a relocation) and
(xxx).L. The jmp/jsr row is the separate JmpToBranch transform (on in the vasm
preset, off under --dialect=rmac); RMAC +O2 only covers the bra/bsr/bcc row.
Cross-section, external, out-of-range, register-indirect, and forced-long operands stay at their
largest encoding. Symbol references never take the abs.w shortcut.
Instruction reordering
On the 68000/010, some adjacent instruction pairs execute in fewer cycles than the
sum of their parts (instruction pairing — the second instruction's fetch overlaps
the first's execution). With --opt-reorder, rg-asm permutes a straight-line run of
independent instructions so that pairable ones land next to each other — the same
instructions in a faster order, a free speed-up. It targets the 68000/010 pairing
model. The pass is opt-in and off by default because reordering changes
instruction order and therefore the emitted bytes.
It only ever reorders when doing so is provably safe and strictly faster:
- Dependency-safe. Two instructions are swapped only when it changes nothing
observable — register and memory hazards are respected. The condition codes (
CCR) are tracked precisely: aCCRwrite is a barrier only when it is live (read before the next write), so the many 68000 ALU/move ops that writeCCRbut whose flags are dead can still be reordered — which is what makes the pass useful on real code. Nothing reorders across a live condition test. - Never churns without benefit. The pass keeps the original order unless a reordering strictly increases total pairing, so enabling it never perturbs the bytes of code it can't speed up.
Scope and eligibility. Scheduling is confined to a basic block — any label or
control-flow instruction (BRA/Bcc/BSR/JMP/JSR/RTS/DBcc/…) ends the run.
Within a run, only instructions with a reorderable shape participate: register,
immediate, absolute, and register-indirect operands. Excluded (they end or sit out the
run): PC-relative operands, MOVEM register lists, control registers (SR/CCR),
FPU registers, and 68020 memory-indirect modes. To keep a permutation
layout-neutral, a run holds only same-size instructions, and any instruction that
emits a relocation (a symbol reference like #label or (label).l) stays put and
ends the run.
Worked example. A shift pairs with a following MOVE/ADD/SUB/logic when both
instructions cost 4n+2 cycles — here LSL.L #1,Dn (10) and ADD.L (An),Dn (14). As
written, the two shifts sit together, so only one pair forms (-4 = 4 cycles saved
off the base cost):
; cyc pair
lsl.l #1,d0 ; 10 —
lsl.l #1,d1 ; 10 —
add.l (a0),d2 ; 14 -4 ← lsl→add pairs
add.l (a1),d3 ; 14 —
rts ; 16 — total: 60
The four are independent (distinct registers, distinct memory), so --opt-reorder
interleaves them — each shift now precedes an add, forming two pairs (8 cycles
saved):
; cyc pair
lsl.l #1,d0 ; 10 —
add.l (a0),d2 ; 14 -4 ← lsl→add pairs
lsl.l #1,d1 ; 10 —
add.l (a1),d3 ; 14 -4 ← lsl→add pairs
rts ; 16 — total: 56
Same five instructions, same result in every register — 60 → 56 cycles purely from
order. (Cycle counts are the 68000 figures from --listing; cyc is the un-paired base cost
and the pair column shows the reduction each pairing applies, so a paired
instruction's effective cost is cyc + pair — e.g. 14 -4 = 10.)
--opt-reorder is ignored under --pic (a same-section reference can resolve to a
relocation-free PC-relative displacement that the run's safety check can't detect as
position-dependent) and when a cycle listing is requested with --listing (listing rows are
captured at emit time, before reordering).
Listing & cycle analysis
| Option | Meaning |
|---|---|
--listing / --listing=<FILE> | Write a cycle-annotated listing. --listing → stdout; --listing=<FILE> → file. On an interactive terminal --listing renders as a boxed, coloured, per-function table (see below); a file, a pipe, --json, -q, or --color=never write the plain text form — columns address · cyc · pair · opcodes · source, where cyc is the primary CPU's un-paired base cost and pair the pairing reduction (-4/—). Without --functions, the plain form ends with a per-function cycle summary; with --functions, that footer is omitted. |
--no-emit | Assemble (and link, for --prg) without writing a .o / .PRG / .BIN. Listings, --cycles, --functions, and the summary still run. Conflicts with -o / --output. |
--listing-cpus=<LIST> | Comma-separated CPUs to cost against, e.g. 68000,68030. The first is the primary (drives the per-line columns); all appear in the summary. Defaults to the --cpu target. |
--cycles / --cycles=<FILE> | Write a deterministic function → cycles per CPU snapshot (sorted, tab-separated). --cycles → stdout; --cycles=<FILE> → file. Commit as a CI cycle-regression baseline. Usable without --listing. |
--functions | Add a per-function bytes/cycles table to the post-assembly summary (interactive stderr only; for --prg the functions are folded from every source). With --listing, suppresses the listing's per-function cycle footer to avoid duplication. |
--json | Emit one pretty-printed JSON document on stdout for every run (success or failure). See Global --json mode. |
-q, --quiet | Suppress the default summary (terminal auto-detect). Also suppresses the startup masthead unless --banner overrides. |
Presentation
| Option | Meaning |
|---|---|
--color=<auto|always|never> | When to colourise interactive stderr output (honours NO_COLOR; default auto). |
--ascii | Draw the summary table with ASCII characters instead of Unicode box-drawing glyphs. |
--banner=<none|line|logo> | Startup masthead. Omitted → auto: logo on a colour terminal, one-line banner otherwise. Suppressed by -q / --json. |
Cycles come from the built-in 680x0 timing tables; instructions produced by macro expansion are shown as the macro-call line and are not individually cycle-costed (their bytes still count toward sizes).
rg-asm --obj -o game.o --listing=game.lst --listing-cpus=68000,68030 game.s
rg-asm --obj -o game.o --listing --listing-cpus=68000,68030 game.s # listing on stdout
rg-asm --json --obj -o game.o --listing-cpus=68000,68030 game.s | jq '.summaries[0].total_cycles'
On an interactive terminal, --listing on stdout renders as a boxed table: one cycle column per
CPU, a spanning per-function band with size and a static cycle sum, and the source line normalized
(tabs collapsed). Expensive instructions are tinted (yellow at ≥40 cycles, red at ≥100) so the cost
sinks stand out. The pair column reports 68000 bus-pairing — the prefetch/execution overlap, an
analysis unique to rg-asm — as a green ↓N on every line where a saving fires:
┌──────────┬───────┬───────┬──────┬─────────────┐
│ addr │ bytes │ 68000 │ pair │ asm │
├──────────┴───────┴───────┴──────┴─────────────┤
│ Loop 6 B · 28 cyc │
├──────────┬───────┬───────┬──────┬─────────────┤
│ 00000000 │ B2 80 │ 6 │ · │ cmp.l d0,d1 │
│ 00000002 │ 64 FC │ 6 │ ↓4 │ bcc.s Loop │
│ 00000004 │ 4E 75 │ 16 │ · │ rts │
└──────────┴───────┴───────┴──────┴─────────────┘
The per-function cycle figure is a static sum of the listed instructions; it does not count loop iterations or branch frequency, so it is not a per-frame runtime cost.
Every global label — code or data — becomes a band (data bands show size only, since cycles are
meaningless for data). A switch into a data or bss section draws a full-width ── DATA ──
banner; the first text section, sitting directly under the header, gets none.
The plain form (file, pipe, --json, -q, --color=never) is byte-stable and carries no escape
codes or box glyphs. --json --listing adds cpus and per-line cycles_per_cpu fields.
Diagnostics & warnings
rg-asm tags every compatibility leniency with a warning category whose policy
(ignore / warn / error) is configurable GCC-style. Warnings appear on success
as well as failure. Flags apply in order: --warn-level → -w → each -W
left-to-right.
CLI flags
| Option | Meaning |
|---|---|
--warn-level=<0..3> | Base verbosity before any -W flags (default 1). See warning levels. |
-w | Suppress all warnings (a later -W re-enables categories one at a time). |
--list-warnings | Print every category name and exit. |
Warning levels
symbol-redef, syntax-error, operand-error, expr-error, immediate-range,
pic-reloc, file-io, macro-error, and layout-error are always hard errors
at every level — they are not affected by --warn-level. Everything else is
levelled by the table below.
The four levels form a monotonic ladder — each step raises the severity of exactly one tier, so no two settings behave alike:
| Level | Name | Policy | Typical use |
|---|---|---|---|
| 0 | silent | ignore all | Quiet batch runs |
| 1 | regular | warn on the wrong-output tier | Default for hand assembly |
| 2 | verbose | warn on all categories | Toolchain / build logs |
| 3 | strict | error on the wrong-output tier; still warn on compatibility | CI correctness gate |
Level 3 is a correctness gate, not a zero-tolerance one: anything that means the emitted bytes are
wrong is fatal, while dialect-compatibility quirks (whose object is still what the author intended)
stay warnings. To make everything fatal — the nuclear option — add -Werror, which promotes every
visible warning (so --warn-level=2 -Werror and --warn-level=3 -Werror both error on all
categories).
Category tiers
Per-category policy at each level (before any -W overrides). Rows are grouped by
tier; see warning levels for the level summary.
The dividing line is whether the emitted object differs from what the source asked for:
- Wrong-output tier — either the source asked for something we could not give it
(
undefined-equate,unknown-mnemonic,ea-mode,cpu-availability), or we silently invented an operand:expr-fallbacksubstitutes displacement0,equr-unresolvedsubstitutes registerD0. Those two fire only in pass 2, where forward references are already resolved, so they never mean "not yet known" — they mean the bytes are wrong. The whole tier warns by default and errors under--warn-level=3. - Compatibility tier — the source means one thing, rg-asm accepts a dialect quirk or an
over-permissive spelling of it, and the emitted object is what the author intended. Quiet until
--warn-level=2; a warning even at level 3, until-Werror.
| Category | L0 | L1 | L2 | L3 | L3 -Werror |
|---|---|---|---|---|---|
undefined-equate, unknown-mnemonic, ea-mode, cpu-availability | — | warn | warn | error | error |
expr-fallback, equr-unresolved, directive-dropped | — | warn | warn | error | error |
parser-heuristic, directive-ignored, nonstandard-size, implicit-extern | — | — | warn | warn | error |
symbol-redef | error | error | error | error | error |
syntax-error, operand-error, expr-error, immediate-range, pic-reloc, file-io, macro-error, layout-error | error | error | error | error | error |
— = ignored (no diagnostic). Internal encoder faults (branch relaxation,
synthetic encode, diagnostic-cap suppression) use a non-listable internal
category and are always errors regardless of warn-level.
-W specs
Repeatable; each spec adjusts one category (or all of them). <cat> is any name
from categories or rg-asm --list-warnings.
| Spec | Effect | Example |
|---|---|---|
-W<cat> | Emit category <cat> as a warning | -Wimplicit-extern |
-Wno-<cat> | Ignore category <cat> | -Wno-cpu-availability |
-Werror | Promote every visible warning to an error (ignored categories stay ignored) | -Werror |
-Werror=<cat> | Promote one category to an error | -Werror=symbol-redef |
Example: -w -Wcpu-availability -Werror=symbol-redef silences everything, turns
CPU availability back on as a warning, and makes redefinition a hard error.
Warning categories
Category (-W…) | Meaning |
|---|---|
cpu-availability | Instruction or addressing mode requires a higher CPU |
directive-dropped | Directive dropped with layout/data effect (ignored ORG, INCBIN in BSS, failed .assert, …) |
directive-ignored | Dialect/compatibility directive note (GenST era gate, FAIL warn) |
ea-mode | Effective-address mode not allowed for this operand |
equr-unresolved | Operand fell back because an EQUR-aliased register was unresolved |
expr-error | Hard expression-evaluation failure (hard error at every warn-level) |
expr-fallback | Sub-expression could not be evaluated and fell back silently |
file-io | Include or incbin file I/O failure (hard error at every warn-level) |
immediate-range | Immediate operand out of range (hard error at every warn-level) |
layout-error | Section layout, ORG, or reservation fault (hard error at every warn-level) |
implicit-extern | Symbol not declared in this file, taken as external (a typo looks like this) |
macro-error | Macro / REPT / IRP expansion fault (hard error at every warn-level) |
nonstandard-size | Non-standard or missing operation size (hard error when emitted as error_kind) |
operand-error | Instruction arity or operand shape fault (hard error at every warn-level) |
parser-heuristic | Parser compatibility heuristic (col-1 label, ::, trailing-comment recovery, …) |
pic-reloc | PIC-mode relocation not allowed (hard error at every warn-level) |
symbol-redef | Symbol or equate redefined (hard error at every warn-level) |
syntax-error | Lexer or parser syntax fault (hard error at every warn-level) |
undefined-equate | Equate could not be resolved (often a missing include) |
unknown-mnemonic | Unknown mnemonic, macro, or directive |
Link-time undefined symbols are separate — see --allow-undefined;
they are not controlled by -W or --warn-level.
Information
| Option | Meaning |
|---|---|
-h, --help | Full help (-h short summary, --help long). |
-V, --version | Print the version and exit. With --json, emit the schema-1 envelope ("version" is the package version) instead of the plain name ver line. |
--dump-tokens | Dump the token stream to stderr per input (debugging). |
Dialects
--dialect=<D> selects the source syntax variant, applied per file — default
vasm; auto heuristically detects each file (see Auto-detection).
Twenty-three assembly dialects ship as stable --dialect=<name> flags today,
plus auto. Avena is the remaining public-tour assembler without a CLI
flag yet; the as68 card on dialects.html
uses the same genpc preset as Ziggy/OVR GenPC. The AST and encoder are shared
across them. Exclusions: Falcon DSP56001, 6502, Apollo 68080.
Chronological tour (same order and dates as the public page): dialects.html.
--dialect | Assembler | Author / company | Era | Status | Distinguishing syntax |
|---|---|---|---|---|---|
alcyon | DR AS68 / Alcyon | Digital Research | 1985 | available | no macros; --obj=dri for DRI objects |
seka | K-Seka / A-SEKA | Kuma Computers Ltd. | 1986 | available | colon-required labels |
gstasm | GST-ASM | GST Holdings Ltd. | 1986 | available | distinct from --obj=gst; BSS era warning |
metacomco | Metacomco Macro Assembler | Metacomco Ltd. | 1986 | available | ST SDK Motorola syntax |
madmac | Atari MADMAC | Landon Dyer; Atari Corp. | ~1986 | available | ?N macros, ^^defined, .iif, name:: exports; .align n = n bytes |
devpac1 | HiSoft GenST v1 / Devpac ST v1 | HiSoft Systems | 1986 | available | C precedence; warns on GenST2+ directives |
assempro | AssemPro | Data Becker / Abacus; Peter Schultz | 1986 | available | \LOCAL backslash locals between globals |
mwc | Mark Williams C as | Mark Williams Company | 1986 | available | / comments; .prvi sections |
aztec | Aztec C assembler | Manx Software Systems | 1987–88 | available | column-1 CSEG/DSEG sections |
devpac2 | HiSoft GenST2 / Devpac ST v2 | HiSoft Systems | 1988 | available | C precedence; warns on GenST3-only directives |
turboasm | TurboAss / Omikron.Assembler | Markus Fritze & Sören Hellwig | ©1989 / ~1990 | available | @@ locals, named macro params, BLKB/BLKW/BLKL |
gfaasm | GFA Assembler | GFA Systemtechnik (+ Ostrowski) | 1988 | available | path/include; colon labels |
sozobon | Sozobon jas | Sozobon, Limited (Johann Ruegg et al.) | ~1989 | available | @ octal radix in dc; Alcyon-compatible backend |
lattice | Lattice C ASM (HiSoft LC5) | HiSoft Systems | ~1990 | available | CSECT name,type; no implicit insn align after byte data; @func entry symbols |
purec | Pure C PASM | Application Systems Heidelberg; AHCC (Henk Robbers) | ~1989–91 → | available | colon-required labels, .GLOBL/.EXTERN; fastcall registers |
genpc | GenPC / as68 | Overlanders (Ziggy Stardust; Ben) | 1991 / 1999 | available | brace {/} macros; Devpac-style text/end; --raw + optional --reloc/--strip-bss |
devpac3 | HiSoft GenST3 / Devpac 3 | HiSoft Systems | 1992 | available | C precedence; full GenST3 surface |
devpac | alias → devpac3 | HiSoft Systems | 1992 | available | same as devpac3 |
brainstorm | Brainstorm Assemble | Brainstorm (FR) | 1993–95 | available | PARGS Pascal-order stack params (not CARGS) |
josy | Josy | Volker Hemsen | 1995 | available | Sozobon jas fork; Line-A needs # immediate; preset --cpu=68020 --fpu=68881 |
| — | Avena Assembler | Avena (Jet / Fried) | 1996 | soon | PART/ENDPART folding markup (Falcon editor); no --dialect flag yet |
| — | as68 (Overlanders PC cross) | Overlanders (Ben / Ziggy) | 1999 | — | tour card only — use --dialect=genpc (same syntax family) |
gas | GNU as (m68k) | FSF / GNU | ~1998+ | available | | comments, 0x/0b literals, %d0 register prefix, .byte/.globl/.macro |
mri | GAS MRI / Microtec mode | Microtec Research; FSF/GNU | ~1980s → GAS --mri | available | * comments, Motorola ops, C precedence |
vasm | vasm Motorola syntax | Frank Wille; Volker Barthelmann | ~2002– | available | default — $/% literals, */; comments, .local labels, \1/\@ macros |
rmac | rmac (modern MadMac) | Landon Dyer (MADMAC); SubQMod; Shamus Young + GGN | 2008 / 2011– | available | .68000 CPU dirs, LTR precedence, RMAC .opt strings; .phrase/.dphrase/.qphrase; .long = align-4 (use dc.l for data) |
auto | — | — | — | available | heuristic per-file detection (best-effort) |
Era matches dialects.html card dates (~ = approximate).
Status available = stable --dialect=<name>; soon = public tour only (Avena). Tour-only rows without a flag are marked —.
See Dialect preset merge for how explicit --dialect interacts with
--prg / --obj / --raw.
Key cross-dialect differences
Abbreviations: dp1/dp2/dp3 = devpac1/devpac2/devpac3. ✓ = native or
enabled for that dialect; — = not native; · = partial / alias spellings.
This matrix covers the original sixteen-dialect tour subset (vasm through mri);
newer dialects (assempro, mwc, gfaasm, sozobon, brainstorm, josy,
genpc) are listed in the table above — see each dialect's oracle doc under
docs/TECH/ for feature notes.
| Feature | vasm | dp1 | dp2 | dp3 | lattice | purec | turbo | gas | madmac | rmac | alcyon | metacomco | gstasm | seka | aztec | mri |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
* line comment | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| line comment | — | — | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — |
$FF / %bin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
0xFF / 0b | — | — | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — |
| Label colon optional | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
.name local | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | 1:/.L | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
@@name local | — | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — |
\1 macro arg | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
| Named macro params | — | — | — | — | — | — | · | ✓ | — | — | — | — | — | — | — | — |
MACRO/ENDM | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | .macro | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
XDEF/XREF | ✓ | ✓ | ✓ | ✓ | ✓ | · | ✓ | .globl | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
TEXT/DATA/BSS | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | .text | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
DC.B/W/L | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | .byte | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| vasm expr precedence | ✓ | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| GenST era warnings | — | ✓ | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — |
?N/?0 MADMAC macros | — | — | — | — | — | — | — | — | ✓ | ✓ | — | — | — | — | — | — |
^^defined / .iif | — | — | — | — | — | — | — | — | ✓ | ✓ | — | — | — | — | — | — |
name:: export | — | — | — | — | — | — | — | — | ✓ | ✓ | — | — | — | — | — | — |
.68000 CPU directive | — | — | — | — | — | — | — | — | — | ✓ | — | — | — | — | — | — |
| LTR expr precedence | — | — | — | — | — | — | — | — | — | ✓ | — | — | — | — | — | — |
CSECT | — | — | — | — | ✓ | — | — | — | — | — | — | — | — | — | — | — |
CSEG/DSEG | — | — | — | — | — | — | — | — | — | — | — | — | — | — | ✓ | — |
@ octal (@777) | — | ✓ | ✓ | ✓ | — | — | — | — | — | — | — | — | — | — | — | — |
%d0 register prefix | — | — | — | — | — | — | — | ✓ | — | — | — | — | — | — | — | — |
| Implicit insn word-align | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Notes: purec · = also .GLOBL/.EXTERN. turbo · = named macro params are an
rg-asm ingestion superset (Fritze's Turbo-Ass had no macros).
metacomco, gstasm, and mri share the Motorola baseline
above; their dialect flags mainly gate diagnostics (metacomco comma+space DC trap,
gstasm BSS-era warning, mri Microtec MRI compatibility). lattice skips implicit
word-alignment before instructions after odd-length data.
Several features are mutually exclusive and cannot be auto-detected reliably —
prefer an explicit --dialect for production builds:
*— a comment at line start (vasm/Devpac) but never a comment (multiply / current-PC) in GAS.- Local labels —
.name(vasm/Devpac) vs@@name(TurboAsm) vs GAS numeric1:/1b/1f/ dropped.Llocals. - Macro args — positional
\1..\9(Motorola family) vs named\name(GAS/TurboAsm ingestion superset).
Auto-detection
--dialect=auto inspects each file's content (a few fast string searches, once per
file) and picks a dialect:
| Signal in source | Dialect |
|---|---|
0x/0X/0b/0B literals, %d/%a/%sp/%pc prefixes, leading /* or |, .macro/.text/.data | gas |
.68000/.68020/…, .68881, RMAC .opt "+O strings | rmac |
^^defined, ^^referenced, .iif (no RMAC markers above) | madmac |
col-1 CSEG / DSEG | aztec |
@@ local labels | turboasm |
CSECT directive | lattice |
| none of the above | vasm |
These dialects have no reliable positive marker in the auto detector — name them explicitly for production builds:
devpac1/devpac2/devpac3 (alias devpac), purec, alcyon, metacomco,
gstasm, seka, mri, assempro, gfaasm, mwc, sozobon, brainstorm,
josy, genpc.
Known false positives: 0x or %d inside a string/comment triggers gas; a
| used as bitwise-OR (rare — Motorola code uses ! or the OR mnemonic).
devpac3 cannot be distinguished from devpac2/vasm by content alone.
Errors & exit status
Diagnostics go to stderr with file, line, and column; assembly errors report the location in the original (pre-include) file, link errors the object and section:
main.s:42:8: error: undefined symbol 'gfx_init'
Exit code is 0 on success, 1 on any error (assembly/link failure, missing
input, or a warning promoted via --warn-level=3 / -Werror).
Part 2 — Assembler syntax
This section documents the default vasm Motorola dialect. Other dialects differ
mainly in comment/label/literal spelling (see Dialects); the directives
below are shared. A line is:
[label] [mnemonic|directive [operands]] [comment]
Fields are whitespace-separated; a label may begin in any column.
Comments
* Full-line comment — only when * is the first non-space character
; Comment, anywhere on the line
move.w d0,d1 ; trailing comment
Mid-line * is multiplication or the current-PC symbol, not a comment. { and }
at column 1 or inside a trailing * comment (demo-source box-drawing) are skipped to
end-of-line.
Labels
GlobalLabel: ; normal label; colon required unless followed by EQU/MACRO
GlobalLabel EQU 4 ; colon optional before EQU / MACRO / SET / = / EQUR
.local: ; local label — scoped to the preceding global label
.loop\@: ; macro-invocation-unique local (the \@ suffix)
@fastcall_label ; @-prefixed label (VBCC fastcall ABI); no colon required
A local label (.name) is private to the most recent non-local label, so short
names like .loop / .done can repeat freely between routines.
Numbers & expressions
$FF00 ; hexadecimal
%10101010 ; binary
1234 ; decimal
'A' ; character literal — packs up to 4 bytes ('AB' -> $4142)
1.5e3 ; floating-point (DC.S / DC.D / DC.X only)
dc.l label+4 ; + - addition / subtraction
dc.l $FF & %10 ; & | ^ ~ bitwise and / or / xor / not
dc.l a << 2 ; << >> shifts
dc.l a * 4 / 2 ; * / multiply / divide
dc.l >symbol ; > high word of value
dc.l <symbol ; < low byte of value
dc.l * + 2 ; * current PC
Precedence, low → high: | < ^ < & < <</>> < +/- < *//.
Sections (text / data / bss)
TEXT ; code section (.text)
DATA ; initialised data (.data)
BSS ; zero-initialised / reserved (.bss)
SECTION name,"ax" ; named section with flags
Three standard sections plus arbitrary named sections. Code and initialised data
carry bytes into the file; BSS only reserves space (DS/DCB there emit no bytes).
Data & storage
DC.B 1,2,3,"string" ; define bytes (strings allowed)
DC.W $FFFF,label ; define words
DC.L label,label+4 ; define longwords
DC.S 1.5 / DC.D / DC.X ; single / double / extended float
DS.B 16 ; reserve 16 bytes (zero-filled)
DS.W 4 ; reserve 4 words
DS.L 0 ; reserve nothing — used only to align (see below)
DCB.W 8,$FFFF ; 8 copies of the value $FFFF (count,value)
BLKB / BLKW / BLKL 8 ; DS aliases (Turbo Asm spelling)
DC defines initialised data; DS reserves (zeroed) storage; DCB/BLK* fill a
block with a repeated value.
Alignment
EVEN ; align to a 2-byte boundary
ALIGN 2 ; align to 2^2 = 4 bytes (arg is a power of two)
CNOP 0,4 ; advance to (offset) past the next (align) boundary
Unaligned word/long access bus-errors on the 68000, so keep code and word/long data
EVEN. CNOP offset,align aligns to align bytes then adds offset.
Symbols & equates
FOO EQU $8000 ; immutable constant
BAR = FOO+4 ; = is a synonym for EQU
COUNT SET 0 ; reassignable — may be redefined later
COUNT SET COUNT+1
scratch EQUR a3 ; register alias — 'scratch' now means a3
; Structure / offset layout (RS counter and OFFSET share the idea)
RSRESET ; reset the RS counter to 0
x_pos RS.W 1 ; x_pos = 0, counter += 2
y_pos RS.W 1 ; y_pos = 2, counter += 2
size RS.B 0 ; size = 4 (current counter, reserve nothing)
OFFSET 0 ; struct-layout mode: labels take offsets, emit nothing
fld_a DS.L 1 ; fld_a = 0
fld_b DS.W 1 ; fld_b = 4
TEXT ; leave offset mode by switching section
EQU/= bind a name once; SET may be reassigned; EQUR aliases a register;
RS/RSSET/RSRESET and OFFSET build struct field offsets without emitting bytes.
Note: write
OFFSET 0, not a bareOFFSET— a bare directive inherits the stale position from a prior include.
org
ORG $8000 ; set an absolute origin
ORG is only meaningful for --raw output, where the emitted bytes are the
bytes that run and no relocation exists — there it sets the absolute origin. For
--prg and --obj, addresses are resolved later (by the loader or linker), so ORG
is ignored. See Output kind.
Macros
memset MACRO ; name before the MACRO keyword
; \1..\9 = positional args; \0 = all args joined by ", "; \@ = unique suffix
move.l \1,a0
move.w \2,d0
.loop\@:
move.b \3,(a0)+
dbra d0,.loop\@
ENDM
memset buffer,#255,#0 ; invocation: comma-separated args
\1…\9 expand to the call arguments, \0 to all of them, \@ to a per-call
unique suffix (use it on local labels so repeated calls don't collide). MEXIT
leaves a macro early; ENDM closes the definition.
Under --dialect=genpc, macros use brace bodies instead of ENDM:
testmac macro
{
nop
}
testmac
\@ still supplies a per-call unique suffix inside the brace body.
Conditional assembly
IFD SYM ; assembled if SYM is defined
IFND SYM ; ... if not defined
IFEQ expr ; ... if expr == 0 (also IFNE, IFGT, IFGE, IFLT, IFLE)
IFC "a","b" ; ... if strings equal (also IFNC)
ELSEIF expr
ELSE
ENDIF ; ENDC is an accepted alias
Defines from -D and any EQU/SET symbol are visible to the conditionals.
Repeat blocks
REPT 4 ; assemble the body 4 times
nop
ENDR
IRP reg,d0,d1,d2 ; once per item, with 'reg' substituted
clr.l reg
ENDR
IRPC ch,ABC ; once per character of the string
dc.b 'ch'
ENDR
Includes
INCLUDE "defs.i" ; textually include another source file
INCBIN "sprite.raw" ; embed a binary file's bytes verbatim
Both search the -I paths. INCLUDE pulls in source (parsed in the current
dialect); INCBIN copies raw bytes into the current section.
Linkage
XDEF sym [,sym…] ; export symbol(s) to the linker (GLOBAL/PUBLIC alias)
XREF sym [,sym…] ; import external symbol(s) (EXTERN alias)
COMM sym, size ; BSS common: reserve + export (GAS .comm)
LCOMM sym, size ; BSS common, keep local (GAS .lcomm)
XDEF makes a label visible to other objects; XREF declares one defined elsewhere.
GLOBAL, GLOBL, and PUBLIC are accepted spellings of XDEF; EXTERN of XREF.
COMM / LCOMM (GAS dialects only — --dialect=gas, or auto-detected) reserve
size bytes in BSS for a named symbol. COMM also exports it; LCOMM keeps it
local. Neither switches the current section: the assembler resumes where it was.
Directive reference
| Directive | Purpose |
|---|---|
TEXT DATA BSS SECTION | Select the output section. |
ORG | Absolute origin (raw output only). |
DC.B/.W/.L/.S/.D/.X | Define initialised data. |
DS.B/.W/.L, BLKB/BLKW/BLKL | Reserve (zeroed) storage. |
DCB.B/.W/.L | Fill a block with a repeated value. |
EVEN ALIGN CNOP | Alignment. |
EQU = SET EQUR | Define constants / register aliases. |
RS RSSET RSRESET SO FO OFFSET | Struct- and frame-offset layout (emit no bytes). |
MACRO ENDM MEXIT | Macro definition and early exit. |
REPT IRP IRPC ENDR | Repeat blocks. |
IF* ELSE ELSEIF ENDIF/ENDC | Conditional assembly. |
INCLUDE INCBIN | Pull in source / binary. |
XDEF XREF GLOBAL GLOBL PUBLIC EXTERN COMM LCOMM | Symbol linkage. |
OPT LIST NOLIST | Assembler options / listing control. |
FAIL PRINT | Force an error / print a message during assembly. |
IDNT OUTPUT END | Module name / output name / end of source. |
Part 3 — JSON schema
Global --json mode
--json emits one schema-versioned JSON document on stdout for every run —
assembly success, link failure, --list-warnings, or empty inputs. No banner or user
tables on stderr in this mode; diagnostics, summaries, and file metadata are fields in
the envelope.
With --json, stdout carries exactly one JSON value per run (pretty-printed). Failures
still emit a JSON document ("status": "error") on stdout and use exit code 1. stderr is
silent for normal reporting in this mode.
File outputs (-o, --listing=<file>, --cycles=<file>, .sym sidecars) are still written to
disk; paths and byte sizes appear under "outputs".
| Output | User mode | --json |
|---|---|---|
| Post-assembly summary | stderr | "summaries" / "linked_summary" |
Cycle listing (--listing) | stdout (text) | "listings" (structured lines) |
Cycle snapshot (--cycles) | stdout (text) | "cycle_snapshots" (text) |
--list-warnings | stdout (table) | "warnings_catalog" in envelope |
| Startup banner | stderr | suppressed |
Success lines (wrote …) | stderr | "outputs" |
| Warnings / errors | stderr | "diagnostics" |
| Request | User mode | --json |
|---|---|---|
-V / --version | rg-asm X.Y.Z on stdout | envelope "status": "ok" — version in top-level "version" |
Listing to stdout (--listing) | .lst text on stdout | "listings" — structured lines[] per source |
Snapshot to stdout (--cycles) | tab-separated text on stdout | "cycle_snapshots" — { source, text } |
Listing to file (--listing=game.lst) | file on disk | file on disk + "outputs" entry |
| Stream | User mode (default) | --json |
|---|---|---|
| stdout | clean, or listing/snapshot text from bare --listing / --cycles | exactly one JSON value |
| stderr | banner, summaries, warnings, status lines | silent for normal reporting |
rg-asm --json --obj -o game.o game.s | jq .
rg-asm --json --list-warnings | jq '.warnings_catalog[].name'
rg-asm --json --obj -o game.o game.s | jq '.summaries[0].total_cycles'
rg-asm --json --listing --obj -o game.o game.s | jq '.listings[0].lines[] | select(.offset != null)'
Envelope (schema 1)
Top-level object. Always present: schema (1), tool ("rg-asm"), version, status
("ok" | "error"), command, outputs (array), summaries (array), diagnostics
(array). Optional fields are omitted when empty.
| Field | When present | Contents |
|---|---|---|
error | "status": "error" | { "code", "message" } — top-level failure |
linked_summary | --prg link succeeded | Linked executable summary (same shape as summaries[]) |
warnings_catalog | --list-warnings | [ { "name", "description" }, … ] |
tokens | --dump-tokens | Per-source lexer token dumps |
listings | --listing | Per-source structured listing (see below) |
cycle_snapshots | --cycles | Per-source snapshot text (tab-separated, same as file format) |
Per-source summaries live in the top-level summaries array (same object shape as
linked_summary).
command
Echo of the effective CLI invocation:
| Key | Type | Meaning |
|---|---|---|
cpu | string | Target CPU, e.g. "68000" |
fpu | string | FPU class, e.g. "none" |
dialect | string | e.g. "vasm" |
optimize | bool | Size optimisation enabled (-O) |
inputs | string[] | Input paths |
output | string | null | -o path |
output_kind | string | e.g. "prg", "obj:elf", "obj:gst", "raw" |
listing | string | null | --listing=<FILE> path; null when --listing writes to stdout or listing not requested |
cycles | string | null | --cycles path |
functions | bool | --functions (user per-function table only; JSON always includes functions[]) |
outputs[]
Each file written during the run:
{ "path": "game.PRG", "kind": "executable", "bytes": 576 }
kind is one of executable, object, listing, cycles, sym, raw, file.
diagnostics[]
{
"severity": "warning",
"kind": "cpu-availability",
"file": "game.s",
"line": 2,
"col": 18,
"message": "…"
}
kind, file, line, and col are omitted when not applicable.
listings[] (--listing)
Structured cycle listing — not the user .lst text. One entry per assembled source:
{
"source": "game.s",
"primary_cpu": "68000",
"cpus": ["68000", "68030"],
"lines": [
{
"offset": 0,
"opcodes": "20 3C 12 34 56 78",
"cycles": 12,
"cycles_per_cpu": [12, 12],
"pairing": 0,
"line": " move.l #$12345678,d0"
},
{
"opcodes": "",
"line": "start:"
}
]
}
Each lines[] entry:
| Field | Type | Meaning |
|---|---|---|
offset | number | omitted | Section-relative address (decimal); omitted on label-only context lines |
opcodes | string | Space-separated hex byte pairs (JSON has no hex number type) |
cycles | number | omitted | Effective cycle cost on primary_cpu; omitted on non-instruction rows |
cycles_per_cpu | array | omitted | Effective cycle cost per entry in cpus (same order); omitted on non-instruction rows and when only one CPU was requested (then cycles carries it) |
pairing | number | omitted | Bus-pairing saving (0 or 4) on primary_cpu; omitted on non-instruction rows |
line | string | Original source line (trimmed trailing whitespace) |
The top-level cpus array names every CPU requested with --listing-cpus, in order; it is the
column key for cycles_per_cpu.
--listing=<file> still writes the user .lst text to disk; bare --listing with --json
populates listings.
cycle_snapshots[] (--cycles)
{ "source": "game.s", "text": "# rg-asm cycle snapshot\n…" }
Plain text in text (same format as --cycles=<file>). --cycles=<file> still writes the file.
tokens[] (--dump-tokens)
{
"source": "game.s",
"dialect": "Vasm",
"entries": [ { "index": 0, "kind": "Ident", "text": "start" }, … ]
}
The schema
A JSON Schema (draft 2020-12) for the --json envelope ("schema": 1) — the machine-readable
form of the Global --json mode prose above. Empty optional fields are
omitted from the output.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "rg-asm --json output (schema 1)",
"type": "object",
"required": ["schema", "tool", "version", "status", "command", "outputs", "summaries", "diagnostics"],
"additionalProperties": false,
"properties": {
"schema": { "const": 1 },
"tool": { "const": "rg-asm" },
"version": { "type": "string" },
"status": { "enum": ["ok", "error"] },
"error": { "$ref": "#/$defs/error" },
"command": { "$ref": "#/$defs/command" },
"outputs": { "type": "array", "items": { "$ref": "#/$defs/output" } },
"summaries": { "type": "array", "items": { "$ref": "#/$defs/summary" } },
"linked_summary": { "$ref": "#/$defs/summary" },
"diagnostics": { "type": "array", "items": { "$ref": "#/$defs/diagnostic" } },
"warnings_catalog": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
"tokens": { "type": "array", "items": { "$ref": "#/$defs/sourceTokens" } },
"listings": { "type": "array", "items": { "$ref": "#/$defs/sourceListing" } },
"cycle_snapshots": { "type": "array", "items": { "$ref": "#/$defs/sourceText" } }
},
"$defs": {
"error": {
"type": "object",
"required": ["code", "message"],
"additionalProperties": false,
"properties": {
"code": { "type": "string" },
"message": { "type": "string" }
}
},
"command": {
"type": "object",
"required": ["cpu", "fpu", "dialect", "optimize", "inputs", "output", "output_kind", "listing", "cycles", "functions"],
"additionalProperties": false,
"properties": {
"cpu": { "type": "string" },
"fpu": { "type": "string" },
"dialect": { "type": "string" },
"optimize": { "type": "boolean" },
"inputs": { "type": "array", "items": { "type": "string" } },
"output": { "type": ["string", "null"] },
"output_kind": { "type": "string" },
"listing": { "type": ["string", "null"] },
"cycles": { "type": ["string", "null"] },
"functions": { "type": "boolean" }
}
},
"output": {
"type": "object",
"required": ["path", "kind", "bytes"],
"additionalProperties": false,
"properties": {
"path": { "type": "string" },
"kind": { "enum": ["executable", "object", "listing", "cycles", "sym", "raw", "file"] },
"bytes": { "type": "integer", "minimum": 0 }
}
},
"summary": {
"type": "object",
"required": ["source", "sizes", "cpus", "total_cycles", "functions"],
"additionalProperties": false,
"properties": {
"source": { "type": "string" },
"sizes": {
"type": "object",
"required": ["text", "data", "bss", "total"],
"additionalProperties": false,
"properties": {
"text": { "type": "integer", "minimum": 0 },
"data": { "type": "integer", "minimum": 0 },
"bss": { "type": "integer", "minimum": 0 },
"total": { "type": "integer", "minimum": 0 },
"sym": { "type": "integer", "minimum": 0 },
"reloc": { "type": "integer", "minimum": 0 },
"binary": { "type": "integer", "minimum": 0 }
}
},
"cpus": { "type": "array", "items": { "type": "string" } },
"total_cycles": { "type": "array", "items": { "type": "integer", "minimum": 0 } },
"functions": { "type": "array", "items": { "$ref": "#/$defs/function" } }
}
},
"function": {
"type": "object",
"required": ["name", "bytes", "cycles"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"bytes": { "type": "integer", "minimum": 0 },
"cycles": { "type": "array", "items": { "type": "integer", "minimum": 0 } }
}
},
"diagnostic": {
"type": "object",
"required": ["severity", "message"],
"additionalProperties": false,
"properties": {
"severity": { "type": "string" },
"kind": { "type": "string" },
"file": { "type": "string" },
"line": { "type": "integer", "minimum": 0 },
"col": { "type": "integer", "minimum": 0 },
"message": { "type": "string" }
}
},
"warning": {
"type": "object",
"required": ["name", "description"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"description": { "type": "string" }
}
},
"sourceText": {
"type": "object",
"required": ["source", "text"],
"additionalProperties": false,
"properties": {
"source": { "type": "string" },
"text": { "type": "string" }
}
},
"sourceListing": {
"type": "object",
"required": ["source", "primary_cpu", "lines"],
"additionalProperties": false,
"properties": {
"source": { "type": "string" },
"primary_cpu": { "type": "string" },
"lines": { "type": "array", "items": { "$ref": "#/$defs/listingLine" } }
}
},
"listingLine": {
"type": "object",
"required": ["opcodes", "line"],
"additionalProperties": false,
"properties": {
"offset": { "type": "integer", "minimum": 0 },
"opcodes": { "type": "string" },
"cycles": { "type": "integer", "minimum": 0 },
"pairing": { "type": "integer", "minimum": 0 },
"line": { "type": "string" }
}
},
"sourceTokens": {
"type": "object",
"required": ["source", "dialect", "entries"],
"additionalProperties": false,
"properties": {
"source": { "type": "string" },
"dialect": { "type": "string" },
"entries": { "type": "array", "items": { "$ref": "#/$defs/token" } }
}
},
"token": {
"type": "object",
"required": ["index", "kind", "text"],
"additionalProperties": false,
"properties": {
"index": { "type": "integer", "minimum": 0 },
"kind": { "type": "string" },
"text": { "type": "string" }
}
}
}
}
Greetings
No comprehensive technical manual would be complete without a list of greetings.
So big shout outs to the folks still keeping the atari scene ticking over in the 2k26
Aggression · Avena · Cerebral Vortex · Cream · Defence Force · Dekadence · DHS
Dune · Effect · Ephidrena · Evolution · Extream · HMD · Holocaust · KÜA
Lamers · LineOut · LoUD · Marquee Design · MEC · MPS · MSB · New Beat · Newline
NoExtra · Omega · OVR · Oxygene · Paradox · PHF · Sector One · smfx · SYNC
TPT · XiA
Final Notes
This is an early release of rg-asm, so of course there are likely to be bugs and issues.
Please reach out and report any that you find or share any suggestions you have for improvements.
License
rg-asm is dual-licensed under the MIT License or the Apache License 2.0, at your option.
The full license texts ship with this release as LICENSE-MIT and LICENSE-APACHE. You may use,
copy, modify, and redistribute rg-asm under either license's terms. Contributions are accepted
under the same dual license unless stated otherwise.
Copyright (c) 1994–2026 Reservoir Gods
♥ made with love for the scene ♥