# Command-line tools for the assembler

ASM80 lives in the browser, and for most people that's fine. But if you want to run the assembler from a Makefile, a CI pipeline, or a script that also compiles C code for a Z80, clicking "Build" in a browser tab isn't going to cut it. That's what [`asm80-cli`](https://www.npmjs.com/package/@asm80/cli) is for.

One npm install gives you four commands:

```plaintext
npm install -g asm80-cli
```

After that, you have `asm80`, `asm80-link`, `asm80-ar`, and `asm80-run` in your PATH. They share the same assembler core as the browser IDE — same syntax, same directives, same output. No surprises. The code that turns your mnemonics into bytes is literally the same JavaScript module, just invoked from Node instead of a `<script>` tag.

## asm80

The assembler. Give it a source file and it produces Intel HEX (or Motorola S-Record for 6800/6809). The CPU is detected from the file extension — `.z80` for Z80, `.a80` for 8080, `.a65` for 6502, and so on — so there's no flag for that. You just write:

```plaintext
asm80 hello.z80
```

and get `hello.hex`. If you want a relocatable object module instead (for linking later), pass `--module`:

```plaintext
asm80 --module mylib.a80
```

This produces `mylib.obj80`. Note the extension: it's `.obj` plus a CPU suffix, i.e. `.objz80`, `.obj80`, `.obj65`, `.obj09`. The linker needs to know what it's linking. Add `--no-lst` if you don't care about the listing file. The listing is mostly useful for debugging — it shows you the generated bytes next to each source line, so you can spot when a relative jump is out of range or an operand got encoded differently than you expected.

## asm80-link

The linker takes a YAML recipe with a `.lnk` extension in the same format the ASM80 IDE uses and produces a single `.hex` file from multiple object modules. A typical `.lnk` looks like this:

```yaml
segments:
  CSEG: '0x0000'
  DSEG: '0x8000'
vars:
  BIOS_PRINT: '0x5'
modules:
  - crt0.objz80
  - main.objz80
library:
  - z80-runtime.objz80
entrypoint: __start
```

The `segments:` section tells the linker where to place code and data. `CSEG` is the code segment, `DSEG` is for initialized data, the addresses are absolute, so you're laying out your binary exactly as it will sit in memory. If your ROM starts at `0x0000` and your RAM at `0x8000`, those are the numbers you put here.

The `vars:` section injects symbols at link time, which is handy for BIOS entry points or hardware addresses you don't want hardcoded in your source. Your assembly code can reference `BIOS_PRINT` with an `EXTERN` directive, and the linker fills in the actual value. No magic numbers scattered across files.

Modules listed under `library:` only contribute code that's actually referenced, same as a static library in a C toolchain. If your runtime library defines 30 routines and your program calls two of them, only those two end up in the final hex. Modules listed under `modules:`, on the other hand, are always included in full — that's where your main code and startup routines go.

The `entrypoint:` field names the symbol where execution begins. The linker makes sure this address ends up as the first thing in the output, so the CPU starts running the right code after reset.

```plaintext
asm80-link project.lnk
```

## asm80-ar

If you're building a library from several object modules, `asm80-ar` packages them together using a `.lbr` recipe:

```yaml
name: mylib
version: 0.1.0
modules:
  - mathops.obj80
  - stringutils.obj80
```

The output extension depends on the CPU: `.lib80` for 8080, `.libz80` for Z80, `.lib65` for 6502, `.lib09` for 6809 etc. This mirrors the `.obj` convention, everything is tagged with what CPU it belongs to, so you can't accidentally link Z80 code into a 6502 project. (You'd be amazed how often that comes up.)

```plaintext
asm80-ar mylib.lbr
```

The result is a single file you can reference in any `.lnk` recipe under the `library:` key. Build the library once, use it in every project that needs it.

## asm80-run

It's a command-line emulator. Hand it a source file or a `.hex` and it assembles (if needed), loads, and runs:

```plaintext
asm80-run hello.z80
```

Serial output goes to stdout, so you can pipe it, diff it, test it. The `-T` flag sets a tick limit, which is useful for automated tests where you don't want a runaway loop eating your CI minutes:

```plaintext
asm80-run -T 100000 hello.z80
```

For hardware configuration (memory layout, serial port addresses, peripherals) it reads `.emu` files, the same format the IDE uses. If you've already set up your project in the browser, the same config works here. Here's what a real `.emu` file looks like - this one comes from the IC80 test suite:

```yaml
cpu: z80
memory:
  rom:
    from: '0x0000'
    to: '0x7FFF'
  ram:
    from: '0x8000'
    to: '0xFFFF'
serial:
  type: 6850
  mapped: port
  control: '0xDE'
  data: '0xDF'
```

The `cpu:` field tells the emulator which CPU to spin up: `z80`, `8080`, `6502`, `6809`, ... Everything else follows from that choice: the instruction decoder, the register set, the interrupt model.

The `memory:` block defines the address space layout. `rom:` marks the region that gets loaded from the hex file and is read-only at runtime. Writes to ROM addresses are silently ignored, just like on real hardware. `ram:` marks the read-write region. In this config, the lower 32K is ROM and the upper 32K is RAM, which is a classic split for small Z80 systems. The emulator doesn't care about gaps, if you only define ROM from `0x0000` to `0x3FFF` and RAM from `0xC000` to `0xFFFF`, everything in between is unmapped and reads back as `0xFF`.

The `serial:` section is where it gets useful for testing. `type: 6850` tells the emulator to simulate a Motorola 6850 ACIA, the most common serial chip in homebrew 8-bit designs. The `mapped: port` field means the ACIA is accessed through I/O port instructions (`IN`/`OUT` on Z80) rather than memory-mapped I/O. The `control:` and `data:` fields give the port addresses: `0xDE` for the status/control register and `0xDF` for the data register. When your code writes a byte to port `0xDF`, it shows up on stdout. When it reads the status register at `0xDE`, the emulator reports whether there's input waiting. This is exactly how real 6850 hardware works — the emulator just replaces the physical UART with a pipe to your terminal.

So when `asm80-run` executes your program and the code sends characters out through the emulated 6850, those characters land in your shell. That's what makes it possible to write tests that check actual program output: assemble, link, run, capture stdout, compare.

## All four in action: testing a C compiler

Here's where it gets concrete. IC80 is a C compiler I'm working on that targets 8-bit CPUs. Its test suite (`run-test.ps1`) exercises the entire pipeline with all four tools:

1.  IC80 compiles a `.c` file to Z80 assembly
    
2.  `asm80` assembles the `.z80` source and the runtime (`crt0.z80`, `libc.z80`) into `.objz80` modules
    
3.  The script generates a `.lnk` recipe and `asm80-link` links everything into a `.hex`
    
4.  `asm80-run` loads the hex, runs the emulator with a tick limit, and the script checks the serial output against expected results
    

The tick limit matters here. Each test case gets 100,000 ticks, enough to run any reasonable C test program to completion, but short enough that an infinite loop bails out in milliseconds instead of hanging the build. If the output doesn't match, the test fails. If the emulator hits the tick limit before the program terminates, the test also fails.

The whole thing runs in a few seconds. No browser, no GUI, no clicking. Just a script that compiles C, assembles Z80, links, runs, and tells you if the output is correct. That's the kind of thing `asm80-cli` was built for.
