NanoDisassembler.kt
src/jvmMain/kotlin/afield/lang/nano/NanoDisassembler.kt · 245 lines
package afield.lang.nano
Reads a Nano bytecode ByteArray back as assembly text, one DisasmLine per instruction.
It walks the program twice. The first walk collects every branch target, so that the second can
put a name on each one and a branch reads as a label rather than as a raw offset. The name is
the target address in hex, so L00 and L0A, and it arrives as its own DisasmLine with an
empty bytes list, which is how NanoVisualization.kt tells a label apart from an instruction
when it is deciding which line to highlight.
DisasmLine.toString is the printable form:
addr: hex hex mnemonic operands ; optional annotation
The annotation is the part the bytes do not say: the signed offset behind a relative branch, and
the absolute address behind jmpa or calla. A byte that decodes to no instruction prints as
??? with its hex value, which is the five of the 256 that NanoVM.kt lists as executing
nothing: 0x03, and 0xFC–0xFF past DIV's last register pair.
class NanoDisassembler
The disassembler. Like the assembler it keeps no state between calls.
class NanoDisassembler {
data class DisasmLine
One output line: its addr, the program bytes it decoded from, and the text to show.
A label line carries the same address as the instruction it precedes and an empty bytes,
so looking a program counter up by addr alone finds both of them.
data class DisasmLine(
val addr: Int,
val bytes: List<Int>,
val text: String
) {
override fun toString(): String
override fun toString(): String {
val hexPart = bytes.joinToString(" ") { it.toString(16).padStart(2, '0') }
.padEnd(8)
return "${addr.toString(16).padStart(2, '0')}: $hexPart $text"
}
}
fun disassemble(program: ByteArray, startAddr: Int = 0): List<DisasmLine>
Disassemble program starting at offset startAddr.
Returns a list of DisasmLines.
fun disassemble(program: ByteArray, startAddr: Int = 0): List<DisasmLine> {
// First pass: collect branch targets so we can emit labels
val branchTargets = mutableSetOf<Int>()
collectBranchTargets(program, startAddr, branchTargets)
val lines = mutableListOf<DisasmLine>()
var i = startAddr
while (i < program.size) {
val op = program[i].toInt() and 0xFF
if (branchTargets.contains(i)) {
// Emit a label pseudo-line (zero bytes, just a label marker)
lines += DisasmLine(i, emptyList(), labelFor(i) + ":")
}
val line = decodeAt(i, op, program, startAddr)
lines += line
i += line.bytes.size
}
return lines
}
fun disassembleToString(program: ByteArray, startAddr: Int = 0): String
Disassemble and join into a single printable string.
fun disassembleToString(program: ByteArray, startAddr: Int = 0): String =
disassemble(program, startAddr).joinToString("\n")
Label naming
private fun labelFor(addr: Int) = "L${addr.toString(16).padStart(2, '0').uppercase()}"
Branch target collection
private fun collectBranchTargets(prog: ByteArray, start: Int, targets: MutableSet<Int>) {
var i = start
while (i < prog.size) {
val op = prog[i].toInt() and 0xFF
val size = instrSize(op, prog, i)
when {
op in 0xE0..0xE9 -> { // relative branches + CALL
if (i + 1 < prog.size) {
val rel = prog[i + 1].toInt().let { if (it > 127) it - 256 else it }
val target = i + 2 + rel
if (target in start until prog.size) targets += target
}
}
op == 0xEA || op == 0xEB -> { // JMPA, CALLA
if (i + 1 < prog.size) {
val target = prog[i + 1].toInt() and 0xFF
if (target in start until prog.size) targets += target
}
}
}
i += size
}
}
Instruction size
private fun instrSize(op: Int, prog: ByteArray, addr: Int): Int = when {
op in 0xC0..0xDF -> 2 // reg + immediate/address
op in 0xE0..0xEB -> 2 // branches
else -> 1
}
Decode single instruction
source — 130 lines, from line 115
private fun decodeAt(addr: Int, op: Int, prog: ByteArray, startAddr: Int): DisasmLine {
fun byte1(): Int = if (addr + 1 < prog.size) prog[addr + 1].toInt() and 0xFF else 0
fun signed1(): Int = byte1().let { if (it > 127) it - 256 else it }
fun regName(r: Int) = "r$r"
fun twoRegs(base: Int): Pair<String, String> {
val n = op - base
return Pair(regName(n shr 2), regName(n and 3))
}
val (text, rawBytes) = when {
op == 0x00 -> Pair("nop", listOf(op))
op == 0x01 -> Pair("hlt", listOf(op))
op == 0x02 -> Pair("ret", listOf(op))
op in 0x04..0x07 -> Pair("inc ${regName(op - 0x04)}", listOf(op))
op in 0x08..0x0B -> Pair("dec ${regName(op - 0x08)}", listOf(op))
op in 0x0C..0x0F -> Pair("not ${regName(op - 0x0C)}", listOf(op))
op in 0x10..0x13 -> Pair("neg ${regName(op - 0x10)}", listOf(op))
op in 0x14..0x17 -> Pair("clr ${regName(op - 0x14)}", listOf(op))
op in 0x18..0x1B -> Pair("out ${regName(op - 0x18)}", listOf(op))
op in 0x1C..0x1F -> Pair("rng ${regName(op - 0x1C)}", listOf(op))
op in 0x20..0x2F -> { val (d, s) = twoRegs(0x20); Pair("mov $d, $s", listOf(op)) }
op in 0x30..0x3F -> { val (d, s) = twoRegs(0x30); Pair("add $d, $s", listOf(op)) }
op in 0x40..0x4F -> { val (d, s) = twoRegs(0x40); Pair("sub $d, $s", listOf(op)) }
op in 0x50..0x5F -> { val (d, s) = twoRegs(0x50); Pair("mul $d, $s", listOf(op)) }
op in 0x60..0x6F -> { val (d, s) = twoRegs(0x60); Pair("and $d, $s", listOf(op)) }
op in 0x70..0x7F -> { val (d, s) = twoRegs(0x70); Pair("or $d, $s", listOf(op)) }
op in 0x80..0x8F -> { val (d, s) = twoRegs(0x80); Pair("xor $d, $s", listOf(op)) }
op in 0x90..0x9F -> { val (d, s) = twoRegs(0x90); Pair("cmp $d, $s", listOf(op)) }
op in 0xA0..0xAF -> { val (d, s) = twoRegs(0xA0); Pair("ldr $d, $s", listOf(op)) }
op in 0xB0..0xBF -> { val (d, s) = twoRegs(0xB0); Pair("str $d, $s", listOf(op)) }
op in 0xC0..0xC3 -> {
val r = regName(op - 0xC0); val imm = byte1()
Pair("ldi $r, 0x${imm.toString(16).padStart(2, '0')}", listOf(op, imm))
}
op in 0xC4..0xC7 -> {
val r = regName(op - 0xC4); val addr2 = byte1()
Pair("lda $r, ${addr2}", listOf(op, addr2))
}
op in 0xC8..0xCB -> {
val r = regName(op - 0xC8); val addr2 = byte1()
Pair("sta $r, ${addr2}", listOf(op, addr2))
}
op in 0xCC..0xCF -> {
val r = regName(op - 0xCC); val imm = signed1()
Pair("addi $r, $imm", listOf(op, byte1()))
}
op in 0xD0..0xD3 -> {
val r = regName(op - 0xD0); val rel = signed1()
Pair("ldrr $r, $rel", listOf(op, byte1()))
}
op in 0xD4..0xD7 -> {
val r = regName(op - 0xD4); val rel = signed1()
Pair("strr $r, $rel", listOf(op, byte1()))
}
op in 0xD8..0xDB -> {
val r = regName(op - 0xD8); val n = byte1()
Pair("shl $r, $n", listOf(op, n))
}
op in 0xDC..0xDF -> {
val r = regName(op - 0xDC); val n = byte1()
Pair("shr $r, $n", listOf(op, n))
}
op == 0xE0 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jmp ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE1 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jz ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE2 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jnz ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE3 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jgt ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE4 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jlt ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE5 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jge ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE6 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jle ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE7 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jc ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE8 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("jnc ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xE9 -> {
val rel = signed1(); val target = addr + 2 + rel
Pair("call ${labelFor(target)} ; ${showRel(rel)}", listOf(op, byte1()))
}
op == 0xEA -> {
val a = byte1()
Pair("jmpa ${labelFor(a)} ; abs 0x${a.toString(16)}", listOf(op, a))
}
op == 0xEB -> {
val a = byte1()
Pair("calla ${labelFor(a)} ; abs 0x${a.toString(16)}", listOf(op, a))
}
// 0xEC–0xFB only: 0xFC–0xFF decode no (dst,src) pair the VM will execute, so they
// belong in the `???` arm rather than rendering as a div against a register r4 that
// does not exist.
op in 0xEC..0xFB -> {
val (d, s) = twoRegs(0xEC)
Pair("div $d, $s", listOf(op))
}
else -> Pair("??? 0x${op.toString(16).padStart(2, '0')}", listOf(op))
}
return DisasmLine(addr, rawBytes, text)
}
private fun showRel(rel: Int) = if (rel >= 0) "+$rel" else "$rel"
}