TRS-80 Model I and III · Z80 · 1977–1983
Inside Fugløy
Fugløy takes a forty-year-old TRS-80 program apart into readable Z80 assembly, then puts it back together and proves that not one byte changed.
This page walks through how it does that, one idea at a time. Every byte here is colored by what it turned out to be.
Why it's new
A disassembler that knows its machine
First and foremost, Fugløy is a Z80 disassembler built for one machine: the TRS-80 Model I and Model III. Around that core it adds what the machine's software needs, and it proves every result.
Fugløy's point is doing all of this together, for one machine, and proving the result.
The machine
64K is the whole world
A Z80 can address 65,536 bytes, and on a TRS-80 everything lives in that one range: the BASIC ROM, the keyboard, the screen, and the program you loaded.
Fugløy models memory the same way: one flat array of 65,536 bytes, and a second array that marks which of them a file actually loaded.
The file
A program is a box of records
A .CMD file is not a picture of memory. It is a list of records, each a type byte, a length byte, and a payload: "load these bytes here", "this program is called", "start running there".
Tap a record to read it. This one is six bytes of real Z80 that put the letter A in the top-left corner of the screen.
Go deeper: the length byte that wraps
In a load record the length also counts the two address bytes, and it wraps around. A length of 8 means six data bytes, but a length of 2 means 256 of them, 1 means 255, and 0 means 254.
Real files bend the other rules too: a transfer record with a length of zero, damaged records, padding after the end. Fugløy reads each one by its content, never by the file's extension, and every quirk has its own test.
BASIC on disk
BASIC, squeezed into bytes
Level II BASIC never stores the word PRINT. It stores one byte, B2. All 124 keywords, from END at 80 to the apostrophe at FB, are crunched that way as you type.
Each stored line is a pointer to the next line, the line number, the crunched body, and a 00. Type a line and watch it shrink.
Go deeper: the odd ones
ELSE is stored with a colon in front of it, and a remark apostrophe becomes three bytes, :REM'. A question mark typed for PRINT becomes the PRINT token. Text inside quotes, after REM, and in DATA is copied as typed, so a keyword in a string stays letters.
Telling them apart
Is it BASIC? Ask the grammar
Counting bytes cannot tell code from BASIC, because the commonest Z80 instructions are BASIC keywords too:
So Fugløy reads each line the way BASIC would. Every statement must have the shape its keyword demands, and every byte must be one BASIC could have stored. One wrong byte and the line is code.
About six code lines per file are clean enough to pass the byte check. Only the statement shapes catch those, and they catch every one.
Finding the way in
Following a planted pointer
The code needs an entry point, and BASIC builds one at run time. Line 250 reads where the program sits in memory, adds 14, splits the result into two bytes, and pokes a JP instruction into memory.
Fugløy does not run BASIC. It evaluates just enough of it with symbols in place of numbers, so "wherever the program happens to load, plus 14" comes out as a real address for each file.
Code or data
Follow the flow, not the bytes
The simple way to disassemble is to decode every byte in order, a linear sweep. It turns text into nonsense instructions, because the Z80 happily decodes the letter H as LD C,B.
Fugløy starts where the program starts and follows every jump and call instead. Bytes nothing jumps to stay data, and terminated runs of text become quoted strings.
Asylum · Med Systems Software · 1981
Games that move themselves
Many games load high in memory, out of the disk system's way, then copy themselves down to where they really run. A disassembler that reads the file sees only the copier.
Fugløy's answer is a relocate hint: these file bytes execute over there. It analyzes them at the address they run at, then writes them back where they load, inside zmac's .PHASE, so labels take the running addresses and the bytes still land in their original place.
For Asylum the 123 hints are generated from the loader's own record stream. With them, 61% of the game decodes as code at its running address.
The five-instruction mover
Most of the other games use a simpler copier, like this one from Sea Dragon (1982), which its author released into the public domain in 1995. Fugløy spots it, works out what moves where, and proposes the exact hint lines, without applying anything on its own.
The guarantee
Prove it: put it back together
A listing can look right and still be wrong. So every test ends the same way: assemble Fugløy's output with a real assembler and compare the result with the original, byte for byte.
Your notes
Notes that survive a move
What you learn about a program goes into a hint file: plain text, one fact per line, made to be kept in git. It names routines, marks data, declares tables, adds entry points, and says what moves where.
Every line is pinned to a block and an offset, and a block is recognized by its content: its length plus a fingerprint of its first 64 bytes. Asylum 1b, 1c, and 1d are the same 20,297 bytes loaded at $6300, $A000, and $5200, and one hint file fits all three.
Go deeper: moving a BASIC program
A tokenized BASIC file records the address it was saved from, inside every line pointer. --rebase 7000 rewrites the pointers so the program reads as if saved at $7000, and flags any machine code that still points at the old addresses as stale.
For BASIC the fingerprint reads each pointer as an offset from the start, so hints written before a rebase still match after it.
Phase 3 · Names
From numbers to names
A TRS-80 program talks to the machine by address: $0049 waits for a key, $3C00 is the screen, port $FF is the cassette. Fugløy knows those addresses for the Model I and III, and Model 4's DOS calls, from eight tables, and the listing says what they are.
Nothing here is guessed. Every entry in every table cites the manual and the page it came from, and the workbench opens your own copy of that page if you have added one. Where two manuals name one address differently, as TRSDOS and LDOS do, you choose which DOS's names to use.
These five rows are from Eliminator, one of the four public-domain games that ship with Fugløy.
How it was built
Small steps, each one proven
Fugløy grew in phases, and every phase ended with something that ran and a test suite that said so.
Decoder and loaders
The Z80's instruction tables are generated from the bit patterns of each opcode, never typed by hand, and checked against a reference written independently: 1,792 encodings, every one compared.
- .CMD and .BAS loaders, one test per quirk
- A BASIC detokenizer and a linear-sweep listing
Follow the flow, prove the round trip
Recursive descent, a JSON description of the program, and the zmac emitter, with a harness that reassembles every output and compares images.
Names and notes
Labels for every jump, call, and data reference, and the hint file with content-based block identity.
Hidden code and moving code
The BASIC grammar, the planted-pointer evaluator, strings, table and loader proposals, rebasing, and relocation with .PHASE; Asylum's generated hints and a ROM smoke test.
Names for the ROM, hardware, and DOS
Eight tables of named addresses, 634 entries, each citing the manual and page it came from. A listing says CALL $KBWAIT where it used to say CALL 0049H, and a comment says what the routine does.
A Z80 that passes the exercisers
A headless core, with no screen and no clock of its own, that runs ZEXDOC and ZEXALL to the end and agrees with the disassembler's tables on every opcode's timing.
The workbench
This site: the listing in your browser with names, notes, jumps, exports, and your own manuals opening at the cited page. Nothing leaves your machine.
Write the spec
What must be true when the work is done, and what is deliberately left out.
Plan small tasks
Each task names its files, the interfaces it shares with its neighbors, and its tests.
Test first
Write the test, run it, and watch it fail for the right reason before writing any code.
Make it pass
The smallest change that turns the test green, then the whole suite, real programs included.
Write down every judgment call
A ledger records each decision the plan did not settle, with what it would cost if wrong.
Get a fresh review
A reviewer who did not write the code reads the whole branch before it merges.
The opcode's eight bits split into x, y, and z (with y split again into p and q), and a handful of rules turn those fields into all 1,792 encodings. That is how Fugløy's tables are generated, so a wrong instruction means a wrong rule, fixed once.
Part 2 · planned, not built
From listing to a game that runs anywhere
The disassembler comes first. The listing it produces is also the first step of a longer plan: recompile a game into readable TypeScript and ship it as one file that plays in any modern browser, with no original binary or ROM image inside.
How it would work
The game's own logic becomes TypeScript functions, named by Fugløy's labels, with no Z80 instructions decoded while it runs. A small runtime plays the machine around it: video memory at $3C00 drawn on a canvas, the keyboard matrix at $3800, the ROM routines the game calls, cassette-port sound, and the 30 Hz heartbeat.
Every frame is checked against the emulator. The same keystrokes, at the same cycle, go to the original and to the translation, and their screens are compared; the first difference names the block that is wrong.
The plan targets the browser: one HTML file, or an ES module with a <trs80-game> element. Because the output is plain TypeScript, a Node build that draws the screen in a terminal would be a natural extension, but it is not part of the plan yet.
- Fugløy ships no ROMs, and only four programs: the public-domain games Sea Dragon, Rear Guard, Eliminator, and Armored Patrol, so anyone can try it on real games. You supply the TRS-80 ROM and any other .CMD and .BAS files you want to work on.
- Everything happens on your own machine: the listing, the hint files, and any game built from them stay with you.
- Some games are free to share. Wayne Westmoreland released the TRS-80 games he wrote with Terry Gilman into the public domain in 1995: Sea Dragon, Armored Patrol and its original Tank Zone, Eliminator, Rear Guard, Zaxxon, and a Donkey Kong that was never sold. They make natural first candidates, and four of them already ship with Fugløy.
After the workbench ships as v1 and the plan's kill criteria have been reviewed. The emulator comes first, because it is the oracle every translation is checked against.
What's next
From bytes to names, and beyond
Watch it run
A headless TRS-80 that records which bytes really execute, feeding that back into the listing. Its Z80 core already passes the standard ZEXDOC and ZEXALL instruction exercisers; the TRS-80's devices come next.
An assistant at the keyboard
Lets an AI assistant drive the emulator: break, step, trace, and label.
An assembler and IDE
Edit the zmac source Fugløy writes, assemble it in the browser, and keep the result next to your notes. It comes after the workbench.
Recompilation
Translating a game from your own collection into TypeScript that plays in any browser. How it would work.