Live Coding in ASM80: run your assembly without leaving the editor

Here's a Z80 loop. Open ASM80, paste it in, and just watch.
; <LIVESTART maxT=500000>
; <SEED A=0x00, B=10, HL=0x4000>
loop:
add a, b
djnz loop ; <TRACE A, B>
; <LIVESTOP A, B, HL>
See the text after the arrows? You didn't type that. It showed up on its own (grey, slightly translucent, sitting right next to your code) the moment your fingers stopped moving. That's ghost text, and it's the result of the IDE assembling your source, spinning up a Z80 emulator, and running the whole thing in the background. Change B=10 to B=5 and the numbers update before you finish reaching for the next key.
That's Live Coding. Let me walk through what's actually going on.
The example, line by line
LIVESTART opens a live coding session. The maxT=500000 parameter sets a T-state budget; the emulator will stop after half a million T-states regardless of where it is. This prevents runaway loops from locking up the IDE. Default is 1000, which is fine for short snippets but too tight for anything with a real loop.
SEED injects values into CPU registers before execution begins. Here I'm setting A to zero, B to 10 (the loop counter for DJNZ), and HL to 0x4000. Every register you don't mention keeps its default (zero on session start, or whatever value it had if you're mid-session). You can use decimal or 0x hex notation — B=10 and B=0x0A are the same thing.
The loop body is two instructions: ADD A, B adds B into A, and DJNZ loop decrements B and jumps back if B isn't zero. Classic Z80 pattern.
TRACE on the DJNZ line captures the CPU state every time the program counter passes through that address. Since this is inside a loop, the ghost text shows aggregated results: 10× means the emulator passed through 10 times, avg 44 T/cycle is the average T-state cost per iteration, and A:0A B:00 are the final values when the loop exited.
LIVESTOP ends the session. The ghost text here shows total T-states for the entire session and the final values of any registers or addresses you listed. The register list is optional — ; <LIVESTOP> alone is valid, it just won't show register values in the ghost text.
How the machine works
Every time you change a character in your source, the IDE kicks off a pipeline: parse, assemble, load into emulator, run from LIVESTART to LIVESTOP (or until maxT is exhausted, or an ASSERT fails). The results get painted as ghost text next to each annotation. The whole cycle typically finishes in single-digit milliseconds for short programs, so it genuinely feels instant.
If your edit introduces an assembly error — misspelled mnemonic, bad operand, unresolved label — the IDE can't produce a binary, so it can't run the emulator. But it doesn't blank out the ghost text. Instead, the text stays in place and turns grey, meaning "this is stale, from the last successful run." You can still read it while you fix your typo. The moment the source assembles cleanly again, the ghost text goes live with fresh values.
You can have multiple live sessions in a single file. Each LIVESTART/LIVESTOP pair is independent. They can't overlap or nest — that's a hard rule — but otherwise you can scatter them through a file to test different sections of your code in isolation.
A note about LIVESTOP: it's optional. If you omit it, the session runs until it exhausts its T-state budget or hits a failing ASSERT. This is fine for quick experiments, but for anything you want to keep, an explicit LIVESTOP makes the intent clearer.
SEED, MEMSTATE, and deterministic runs
Assembly code doesn't exist in a vacuum. Your registers hold values, your memory contains data, your I/O ports have state. If you want Live Coding to show you meaningful results, you need to tell it what state to start from. That's what SEED, MEMSTATE, and PORTSTATE are for.
SEED handles registers:
; <SEED A=0xFF, B=10, HL=0x4000, SP=0xFFFF>
MEMSTATE writes bytes into RAM at a specific address:
; <MEMSTATE 0x4000: FF 00 AB CD>
; <MEMSTATE *: 21 FF FF 3E>
The * address means "continue from where the previous MEMSTATE left off." So if the first line wrote four bytes starting at 0x4000, the second line writes four more bytes starting at 0x4004. Handy for setting up longer data blocks without counting addresses by hand.
PORTSTATE writes to the I/O port address space:
; <PORTSTATE FE: BF>
This puts 0xBF at port 0xFE; useful if your code reads from hardware and you want to simulate a particular input.
Placement matters. If SEED or MEMSTATE appears in the header — before the first actual instruction — it sets the initial state for the session. If it appears inline, mid-code, it acts as a one-shot injection: the emulator applies it exactly when the program counter passes through that address. This lets you simulate things like "at this point in execution, new data arrives on port 0xFE."
The reason all of this matters: deterministic runs. If every piece of state is explicitly set, the results are reproducible. Same source, same ghost text, every time. No "it works on my machine" surprises, because there's no ambient machine state to interfere.
How to insert annotations
You don't need to memorize any of this syntax. Type ; < in the editor and autocomplete shows you every available annotation — TRACE, SEED, EXPECT, MEMSTATE, all of them — with parameter hints. Pick one, fill in the arguments, done.
Or right-click anywhere in the editor and select "Insert Live Annotation" from the context menu. The IDE writes the ; <...> wrapper for you.
The annotations live inside comments, so your assembler ignores them completely. They're metadata for the IDE, not instructions for the CPU. Your assembled binary is identical whether annotations are present or not.
Ghost diff and the hover panel
Run that first example again, then change B=10 to B=12. The ghost text updates, and here's what you'll notice: the values that changed since the previous run light up in amber. Values that stayed the same are dimmed. At a glance, you can see exactly what your edit affected.
This is surprisingly useful when you're tweaking loop bounds or modifying an algorithm. You don't need to remember what the values were before — the color tells you what moved.
Hover over any ghost text and a panel pops up showing the complete register file at that snapshot: A, F, B, C, D, E, H, L, the shadow registers, IX, IY, SP, PC, flags — everything the CPU holds. You don't need to add every register to your TRACE annotation; just hover when you need the full picture.
For TRACE inside a loop, the ghost text also includes the iteration count and average T-states per cycle. This is a quick way to check loop performance without manual counting: if you expect 10 iterations at roughly 44 T per cycle and the ghost text says 10× avg 44 T/cycle, your mental model matches reality.
Assertions: EXPECT and ASSERT
Ghost text shows you what happened. Assertions tell the IDE what should happen.
; <LIVESTART maxT=100000>
; <SEED HL=0x4000>
; <MEMSTATE 0x4000: 01 02 03 04>
ld a, (hl) ; A = 01
add a, a ; A = 02
ld (hl), a ; (0x4000) = 02
; <EXPECT A=0x02> ● green
; <MEMEXPECT 0x4000: 02 02 03 04> ● green
; <ASSERT A=0x02>
nop ; <LIVESTOP A> → [18 T] A:02
EXPECT is a soft check. If the condition holds, you get a green dot in the gutter. If it doesn't, a red dot and ghost text showing what the actual value was. Either way, the emulator keeps running. ASSERT is the hard version: if the condition fails, emulation halts right there, the line background tints red, and the ghost text shows you the mismatch.
MEMEXPECT and MEMASSERT do the same thing for memory contents. You specify an address and expected bytes (up to 8):
; <MEMEXPECT 0x4000: 02 02 03 04>
If any byte differs, the failing bytes are highlighted in red in the ghost text, giving you a byte-level diff. No guessing which part of your memory block went wrong.
PORTEXPECT and PORTASSERT complete the picture for I/O ports:
; <PORTEXPECT 0x10: FF>
; <PORTASSERT 0x10: FF>
The practical difference between EXPECT and ASSERT comes down to this: scatter EXPECTs liberally through your code as sanity checks. Use ASSERT for the one invariant that, if violated, means nothing downstream makes sense. EXPECT says "I'd like this to be true." ASSERT says "if this isn't true, stop wasting cycles."
ON N: targeting a specific loop pass
Sometimes you don't care about every pass through a loop. You want the CPU state on exactly the 5th iteration and nothing else. That's ON N:
; <LIVESTART maxT=500000>
; <SEED B=10>
loop:
inc a
; <ON 5 TRACE A, B>
; <ON 10 EXPECT A=0x0A>
djnz loop
; <LIVESTOP>
ON 5 TRACE A, B fires only on the 5th time the program counter hits that address. Every other pass is ignored. The ghost text shows a single-pass snapshot, not an aggregate.
Combine it with EXPECT and you've got a targeted assertion: "on iteration 10, A must be 0x0A." If your loop logic goes wrong on a specific pass, you'll catch it without wading through all the other iterations.
One edge case: if the loop runs fewer times than your ON target, the ghost text tells you so — something like "loop ran only 7 times" — instead of silently showing nothing. This is helpful when you're debugging early-exit conditions and the loop terminates sooner than expected.
All supported CPUs
Everything I've described works identically across every CPU ASM80 supports. The annotation syntax is the same, only the register names change to match the target architecture.
A SEED for a 6502 file uses A, X, Y, SP. A SEED for a 6809 uses A, B, X, Y, U, S, DP. The annotations adapt; your workflow doesn't change.
What this actually is
Live Coding isn't a debugger. There's no stepping, no breakpoints, no "run to cursor." It's closer to having unit tests embedded directly in your source code that execute continuously as you type. Each annotation is either an observation ("show me A and B here") or an expectation ("A must be 0x02 here"). The feedback loop is measured in milliseconds, not in "save, switch to terminal, run, squint at hex dump."
The annotations stay in your source. They're comments, so they survive assembly, survive version control, survive being shared with someone else. If another person opens your file in ASM80, they see the same ghost text, the same green dots, the same assertions. It's documentation that verifies itself.
Change a constant and every annotation in the file re-evaluates instantly. You know before you even move to the next line whether your code still does what you think it does. And if it doesn't, the ghost text is right there telling you what it actually did instead.
That's the whole point: write code, state what you expect, see the results. If the ghost text agrees with your head, keep going. If it doesn't, you just learned something, and you learned it before you even left the line.



