Goal
Turn assembled bytes back into source. CassoCore has no disassembler today — the only occurrences of "disassemble" in the tree are comments in Cpu.h and Microcode.h about opcode tables.
This closes the build loop in reverse. Three of the four steps already work:
CassoCli disk list mydisk.dsk # A 002 HELLO / B 002 PROG / A 002 STARTUP
CassoCli disk get mydisk.dsk PROG --out back.bin # PROG: loads at $6000, 33 bytes
CassoCli disk get mydisk.dsk STARTUP --basic --out back.bas # 10 PRINT CHR$ (4);"BRUN PROG"
back.bin comes back byte-identical, and the load address survives — DOS 3.3 does not record it in the catalog, but a B file carries it in its own 4-byte header and get reports it. What is missing is the step that turns back.bin into prog.a65.
Three customers, one component
All three need the same "bytes at an address → one rendered instruction" primitive. Build it once, in core, where a test can reach it.
Code versus data is undecidable in general
Recursive descent from a known entry point is the right starting algorithm, and on ordinary code it is close to sufficient. It is not theoretically complete, and the 6502 hits every exception in ordinary Apple II code rather than in contrived cases:
- Indirect jumps.
JMP ($06) and jump tables. The target is a runtime value; static analysis cannot name it.
- Computed RTS. Push an address,
RTS to it. A standard 6502 dispatch idiom, and invisible to a flow walk.
- Self-modifying code. Writing an operand byte before executing it. The bytes on disk are not the bytes that run.
- Inline data after
JSR. The callee pops the return address, reads the string that follows the call, and adjusts the return. The bytes after a JSR are data, and only the callee's behavior says so.
- Entry points nobody declared. Interrupt vectors, callbacks handed to ROM.
Deciding reachability with computed control flow is the halting problem. So the design should not pretend otherwise: it should be verifiable rather than provably complete.
The oracle makes it tractable
The output has a property that can be checked: assemble it and compare bytes.
prog.a65 --assemble--> prog.bin --put--> disk --get--> back.bin --disassemble--> back.a65
back.a65 --assemble--> must equal back.bin, byte for byte
If it round-trips, the disassembly is correct as a reproduction, even where a block of real code was rendered as .byte because nothing reached it. That is a weaker claim than "we identified all the code" and it is the one that can actually be tested, on any binary, without a human reading the output.
This is the same guarantee --basic already makes for Applesoft, and it should be asserted the same way.
Casso can do better than static analysis
The unique advantage here is that this repository contains an emulator. Cpu already keeps a circular instruction trace (Cpu.h:195, --trace), stamped with interrupt-dispatch tags.
So: run the binary and record which addresses actually executed. A trace-informed disassembly resolves indirect jumps, computed RTS and self-modifying code for the paths taken, which static analysis cannot do at all. Static recursive descent and emulator coverage are complementary — the walk finds code the trace never reached, the trace finds code the walk could not follow.
Nothing else in the loop needs building for this: HeadlessHost already drives the CPU with no window.
Sketch
Disassembler in CassoCore: bytes + origin → rendered instructions. Reads the opcode tables the assembler already owns, so it is dialect-aware for free, and honors assemblerHidden in Microcode.h so undocumented opcodes render honestly.
- Recursive descent from declared entry points; everything unreached emitted as
.byte.
- Optional trace input to widen the reachable set.
- Caller-supplied hints: extra entry points, explicit data ranges, label names.
- CLI surface — a
disasm mode, plus --org for a raw file with no header to carry the address.
Acceptance
Goal
Turn assembled bytes back into source.
CassoCorehas no disassembler today — the only occurrences of "disassemble" in the tree are comments inCpu.handMicrocode.habout opcode tables.This closes the build loop in reverse. Three of the four steps already work:
back.bincomes back byte-identical, and the load address survives — DOS 3.3 does not record it in the catalog, but aBfile carries it in its own 4-byte header andgetreports it. What is missing is the step that turnsback.binintoprog.a65.Three customers, one component
disk get→ sourceAll three need the same "bytes at an address → one rendered instruction" primitive. Build it once, in core, where a test can reach it.
Code versus data is undecidable in general
Recursive descent from a known entry point is the right starting algorithm, and on ordinary code it is close to sufficient. It is not theoretically complete, and the 6502 hits every exception in ordinary Apple II code rather than in contrived cases:
JMP ($06)and jump tables. The target is a runtime value; static analysis cannot name it.RTSto it. A standard 6502 dispatch idiom, and invisible to a flow walk.JSR. The callee pops the return address, reads the string that follows the call, and adjusts the return. The bytes after aJSRare data, and only the callee's behavior says so.Deciding reachability with computed control flow is the halting problem. So the design should not pretend otherwise: it should be verifiable rather than provably complete.
The oracle makes it tractable
The output has a property that can be checked: assemble it and compare bytes.
If it round-trips, the disassembly is correct as a reproduction, even where a block of real code was rendered as
.bytebecause nothing reached it. That is a weaker claim than "we identified all the code" and it is the one that can actually be tested, on any binary, without a human reading the output.This is the same guarantee
--basicalready makes for Applesoft, and it should be asserted the same way.Casso can do better than static analysis
The unique advantage here is that this repository contains an emulator.
Cpualready keeps a circular instruction trace (Cpu.h:195,--trace), stamped with interrupt-dispatch tags.So: run the binary and record which addresses actually executed. A trace-informed disassembly resolves indirect jumps, computed
RTSand self-modifying code for the paths taken, which static analysis cannot do at all. Static recursive descent and emulator coverage are complementary — the walk finds code the trace never reached, the trace finds code the walk could not follow.Nothing else in the loop needs building for this:
HeadlessHostalready drives the CPU with no window.Sketch
DisassemblerinCassoCore: bytes + origin → rendered instructions. Reads the opcode tables the assembler already owns, so it is dialect-aware for free, and honorsassemblerHiddeninMicrocode.hso undocumented opcodes render honestly..byte.disasmmode, plus--orgfor a raw file with no header to carry the address.Acceptance
.a65in the test corpus: assemble → disassemble → assemble produces identical bytes..byterather than as nonsense instructions, and says which addresses it could not follow.???.