asm8080
v1.0.30
Published
Intel 8080 two-pass assembler
Downloads
773
Readme
asm8080
Intel 8080 two-pass assembler written in TypeScript.
Built primarily to assemble the Radio-86RK monitor ROM, but works with any Intel 8080 source.
Playground
Try it in the browser: begoon.github.io/asm8
- Live assembly listing — addresses and hex bytes appear in the gutter, wrap at four bytes (click
…for the full dump). - Multi-tab editor. Each tab holds its own filename and source; all tabs and the active index persist in
localStorage. - Built-in example list is runtime-loaded from
docs/examples.js, a plain<script>that assignswindow.asm8Examples = [{ name, filename }, ...]before the bundle runs. A different deployment (e.g. the copy embedded in rk86-js-v2-svelte) can ship a different dropdown without rebuildingplayground.js— just editexamples.js. Loading an example always opens a new tab. uploadreads an.asmfile into a new tab.- download as dropdown picks the output —
.asm(default, writes the current source) or one of.rk/.rkr/.pki/.gam/.bin(writes the assembled bytes). Tape formats prepend a 4-byte big-endian start/end header (plus an extraE6hsync byte for.pki/.gam) and appendE6 + 2-byte checksum. Binary payloads are always packed tight (min(start)..max(end), gaps zero-filled), soorg 3000hprograms stay compact. The choice is persisted inlocalStorage. - run button (or
Ctrl/Cmd+R) boots the assembled.rkin the emulator. Cross-origin (standalone playground → rk86.ru) passes the file as adata:URL in?run=; same-origin embeds hand off throughlocalStorage["asm8-handoff:<uuid>"]+?handoff=<uuid>to avoid Chrome's URL-length cap on large programs. A same-origin embed activates that path by settingwindow.asm8EmulatorUrl = "../"inindex.htmlbefore the module script. - Dark / light theme toggle.
Build locally with just playground — regenerates docs/build-info.ts and bundles docs/playground.ts with Bun.
Install
npm install asm8080CLI
Run directly from npm (no install required):
npx asm8080 <source.asm> [more.asm ...] [--split] [--format <ext>] [-l] [-o <dir>]
bunx asm8080 <source.asm> [more.asm ...] [--split] [--format <ext>] [-l] [-o <dir>]Multiple input files are concatenated in the order given and assembled as if they were one file. The first filename determines the output <base>.
Options:
--split— one file per section, named<base>-<sectionname>.bin(or<base>-XXXX-XXXX.binfor unnamed sections). If there is only one section, it's written as<base>.<format>.--format <ext>— output envelope for the single-file case.bin(default) emits the raw payload;rk/rkr/pki/gamwrap it as a Radio-86RK tape file:| ext | layout | | ------------ | --------------------------------------------------------------------- | |
rk,rkr|start_hi start_lo end_hi end_lo‖ payload ‖E6 cs_hi cs_lo| |pki,gam|E6‖start_hi start_lo end_hi end_lo‖ payload ‖E6 cs_hi cs_lo|Addresses are big-endian and
endis inclusive. Checksum is the monitor'schksumroutine (F82Ah) — every byte except the last feeds both halves of a 16-bit sum (lo += b, hi += b + carry); the last byte adds to the low half only. For tape formats the payload is packed tight frommin(start)..max(end)(no leading zero fill)..binkeeps its legacy "load at address 0" layout. Combining a non-bin format with--splitand more than one section is a hard error.--trailer-padding [N]— injectNzero bytes between the payload and theE6 cs_hi cs_lotrailer of a tape-file envelope (rk/rkr/pki/gam). Useful when a loader needs a quiet gap before re-syncing on the checksum marker.Ndefaults to2when the flag is given without a number; omitting the flag entirely yields the legacy no-padding layout. Ignored for--format bin. Padding is not included in the checksum.bun run asm8.ts prog.asm --format rk --trailer-padding # 2 zeros bun run asm8.ts prog.asm --format rk --trailer-padding 5 # 5 zeros-l— generate listing (.lst), symbol table (.sym), section map (.map), and structured listing (.json) files-o <dir>— output directory (created if needed)-v,--version— print version-h,--help— show help
Default output is a single file <base>.bin containing all sections placed at their addresses, with zeros filling any gaps (including in front of the first section if its ORG isn't 0). The file is sized to the end of the last section — not padded to 64 KB. Overlapping sections are an error. With --split, each section is written as a separate file without padding.
bunx asm8080 prog.asm --format rk # prog.rk
bunx asm8080 prog.asm --format gam -o out # out/prog.gamA section map is printed to stdout:
F800-FFFF 2048 bytesWith -l, a listing file (.lst) and a symbol table file (.sym) are generated alongside the binary output. Each source line in .lst is prefixed with its address and emitted bytes:
F800: C3 36 F8 start: jmp entry_start
F803: 3E 8A mvi a, 8AhThe .sym file contains one symbol per line, sorted alphabetically:
ENTRY_START F836
START F800The .map file summarizes the section layout:
F800-FFFF 2048 bytes
Total: 2048 bytes in 1 sectionThe .json file contains the same information in a machine-readable form, split into code, symbols, and map, with a top-level version field:
{
"version": 2,
"code": [
{
"line": 4,
"label": "start",
"op": "mvi",
"addr": "0100",
"length": 2,
"bytes": ["3E", "42"],
"chars": [">", "B"],
"arg1": { "text": "a", "type": "reg", "value": 7 },
"arg2": { "text": "42h", "type": "imm8", "value": 66 },
"comment": "; load"
},
{
"line": 7,
"label": "msg",
"op": "db",
"addr": "0106",
"length": 3,
"bytes": ["48", "69", "00"],
"chars": ["H", "i", "."],
"data": {
"kind": "db",
"parts": [
{
"text": "'Hi'",
"bytes": ["48", "69"],
"values": [72, 105],
"chars": ["H", "i"]
},
{ "text": "0", "bytes": ["00"], "values": [0], "chars": ["."] }
]
}
}
],
"symbols": { "MSG": "0106", "START": "0100" },
"map": {
"sections": [{ "start": "0100", "end": "0108", "size": 9 }],
"total": 9
}
}code entries
Every entry has line (1-based source line). The rest is optional and applies where meaningful:
addr— 4-char hex address of the instruction or directivelength— bytes produced by this linebytes— 2-char hex per emitted byte, one element per bytechars— printable char per byte (1:1 withbytes,"."for non-printable)label,op(lowercase),comment(;-prefixed, verbatim)arg1/arg2— see Argument shape below (instructions,org,equ,section)data— see Data directives below (db/dw/ds)
Argument shape
Each instruction operand is an object:
{
text: string, // verbatim operand text from the source
type: "reg" | "regpair" | "imm8" | "imm16"
| "addr16" | "port8" | "rst" | "name",
value?: number // evaluated numeric value (absent for type "name")
}value semantics per type:
| type | value |
| --------- | --------------------------------------------------------------------------- |
| reg | i8080 3-bit register field: B=0, C=1, D=2, E=3, H=4, L=5, M=6, A=7 |
| regpair | i8080 2-bit rp field: BC=0, DE=1, HL=2, SP=3, PSW=3 (use text to split) |
| imm8 | 8-bit immediate (MVI, ADI …, CPI) |
| imm16 | 16-bit immediate (LXI rp, n16; also used for ORG, EQU) |
| addr16 | 16-bit absolute address (JMP, CALL, conditional J*/C*, LDA/STA, …) |
| port8 | 8-bit port number (IN, OUT) |
| rst | RST vector index 0..7 |
| name | identifier (section name); no value emitted |
Example: LXI H, 1234h →
"arg1": { "text": "H", "type": "regpair", "value": 2 },
"arg2": { "text": "1234h", "type": "imm16", "value": 4660 }SP and PSW share encoding 3; disambiguate by reading text or op.
Data directives (db / dw / ds)
db and dw produce a parts array, one element per comma-separated source segment. Each part:
{
text: string, // verbatim source fragment
bytes: string[], // 2-char hex bytes (DW is little-endian: "dw 1234h" -> ["34","12"])
values: number[], // db: byte values (0..255); dw: word values (0..65535)
chars: string[] // 1:1 with bytes, "." for non-printable
}ds has no literal bytes — instead:
"data": { "kind": "ds", "size": 16 } // ds 16
"data": { "kind": "ds", "size": 16, "fill": 255 } // ds 16 (0FFh)Versioning
The top-level "version": 2 field is stable. Future schema changes will bump this number; consumers should read version and branch on it.
API
import { asm } from "asm8080";
const sections = asm(source);
// Section[] — each section has: start, end, data, name?Each org directive creates a new section. The section name directive names it.
Assembler features
Two-pass assembly. Forward references resolve for both labels and
equ(including chainedequ-to-equexpressions — unresolved entries are iteratively re-evaluated to a fixpoint after the first pass)Case-insensitive mnemonics, registers, and symbols
All documented Intel 8080 instructions
Directives:
org,section,db,dw,ds,equ,end,include,if/else/endif,proc/endp/return— each may also be written with a leading dot (.org,.db, etc.) for compatibility with other assemblersinclude "path.asm"— inline another source file at this point. The path is resolved relative to the including file's directory; nested includes follow the same rule. Self- and circular-include chains are rejected. Errors inside an included file report that file's path and line. CLI-only — callingasm(source)from the browser playground (no filesystem) without supplying anAsmOptions.readIncludemakesincludethrow "include is not supported in this environment".; main.asm org 0 include "defs.inc" ; defines FOO mvi a, FOO hlt endds NreservesNbytes filled with 0;ds N (F)reservesNbytes filled with byte valueFNumber formats: decimal (
255), hex withhsuffix (0FFh)Character literals:
'A'(usable anywhere a byte value is expected)Strings in
db:db "hello"ordb 'hello'Escape sequences in strings and character literals:
\\,\",\',\n(0Ah),\r(0Dh),\t(09h),\0(00h). Example:db "line\r\n", '\0'. Unknown escapes like\xare an error.Expressions:
+,-,*,/,%,|,&,^,~,<<,>>,()with C precedenceLOW(expr)/HIGH(expr)— extract low or high byte of a 16-bit value$— current address (at the start of the current instruction or directive)Local labels:
@name:or.name:— scoped to the most recent non-local label.foo: ... @loop:defines the symbolfoo@loop;foo: ... .loop:definesfoo.loop. Withinfoo's scope,jmp @loop/jmp .loopresolves to that symbol. A colon is required, just as for normal labels (this also disambiguates.loop:from directives like.org/.db).
delay:
mvi b, 10
@loop: dcr b
jnz @loop
ret
delay2:
mvi b, 10
.loop: dcr b
jnz .loop
ret- Multiple statements per line joined with
/or\(spaces required on both sides), up to 10 per line:
push h / push b / push d
pop d \ pop b \ pop hThe two separators are interchangeable and can be mixed on the same line. To disambiguate from division, the split only fires when a valid instruction name (or directive, optionally dotted) follows the separator. So mvi a, 10 / 2 is still treated as division (10 / 2 = 5).
Conditional assembly: .if / .else / .endif
A flag-driven preprocessor expands these into i8080 conditional jumps. .if <flag> skips the body when <flag> is false. Supported flags: Z NZ C NC PO PE P M, plus aliases == (→ Z) and <> (→ NZ). Blocks nest. The leading dot is optional — if / else / endif work the same.
; if A == 11h: mov a, b
cpi 11h
.if ==
mov a, b
.endif
; if A >= 10 (unsigned): mov a, b else mov a, c
cpi 10
.if NC
mov a, b
.else
mov a, c
.endifEach .if reserves label names @_if_<N>_else / @_if_<N>_exit under the enclosing non-local label, so avoid label names starting with @_if_. Keep an entire .if/.endif block inside a single non-local scope — introducing a new top-level label between the jump and its target will break label resolution.
Procedures: .proc / .endp / .return
A procedure auto-saves and restores register pairs around its body. Syntax: <name> .proc [reg, reg, ...] where each register is one of PSW B D H (the four pushable pairs); separators may be commas or whitespace. The preprocessor emits the label, pushes in listed order at entry, and pops in reverse order followed by RET at .endp. Procedures cannot nest. The leading dot is optional — proc / endp / return work the same.
.return compiles to a jump to a shared exit block at .endp that does the reverse pops and RET. As a special case, when .proc has no register list, .return degrades to a bare RET. A plain ret (or conditional rz/rnz/…) inside a .proc body skips the pops and corrupts the stack — use .return for early exit.
; save PSW and HL, do work, auto-restore + RET at .endp
abc .proc psw, h
lxi h, buf
mov a, m
.endp
; early exit with .return — length in B on exit
strlen .proc b, h
mvi b, 0
loop: mov a, m
cpi 0
.if Z
.return
.endif
inr b
inx h
jmp loop
.endpTests
bun testtests/asm8.test.ts— assemblesmonitor.asmand verifies byte-identical output againstmon32.bintests/asm8-instructions.test.ts— exercises all 8080 instruction encodings, directives, expressions, labels, and edge cases
Reference files
target/monitor.asm— Radio-86RK monitor ROM sourcetarget/mon32.bin— expected binary output (2048 bytes, F800-FFFF)
License
MIT
