Uxn complete
Saturday 1st August 2026
A complete Uxn CPU implementation. Bytes are held in Ints and and masked with and 0xff and and 0xffff instead of using Kotlin's experimental unsigned types, everything else is pretty straightforward other than some individual lines with heavy bitwise operations (but they become clearer the more you work with them). There's just three files holding four classes (a simple error type class is held at the bottom of Uxn.kt). Each opcode operation was extracted to a small function, I felt it was better to have the cleaner code than to inline it all into one massive switch/when statement, it was certainly easier to write the code that way without the noise from other operations floating around the periphery. I think this is largely correct but I've only tried a few roms found online, more issues might come out when I try a partial Varvara implementation.
Uxn.kt
package yorkshire.systems.kuxn.cpu
import yorkshire.systems.kuxn.cpu.UxnFault.Companion.Error
/**
* Uxn CPU: 8 bit dual-stack VM with 64KB of memory plus a 256 byte device page
*
* Bytes are held in Ints and masked with `and 0xff` and `and 0xffff` instead of using
* Kotlin's experimental unsigned types
*/
class Uxn {
val ram = IntArray(0x10000)//64KB
val workingStack = Stack("working-stack")
val returnStack = Stack("return-stack")
var programCounter = 0
//Varvara io:
val devicePage = IntArray(0x100)//256 bytes
var onDeviceOutput: (port: Int) -> Unit = {}//Called after CPU write
var onDeviceInput: (port: Int) -> Unit = {}//Called before CPU read
var onFault: (UxnFault) -> Unit = { throw it }
private var sourceStack = workingStack
private var destinationStack = returnStack
private var isShort: Boolean = false
fun load(rom: ByteArray, address: Int = 0x0100) {
if (address < 0 || address + rom.size > ram.size) throw UxnFault("rom", Error.OVERFLOW)
rom.indices.forEach { i ->
ram[address + i] = rom[i].toInt() and 0xff
}
}
//Big endian memory access
private fun read16(a: Int): Int = (ram[a and 0xffff] shl 8) or ram[(a + 1) and 0xffff]
private fun write16(a: Int, value: Int) {
ram[a and 0xffff] = (value ushr 8) and 0xff
ram[(a + 1) and 0xffff] = value and 0xff
}
private fun signByte(value: Int) = value.toByte().toInt()
fun eval(vector: Int) {
if (vector == 0) return
programCounter = vector
try {
evalLoop()
} catch (fault: UxnFault) {
onFault(fault)
}
}
//break or an exception will end the loop:
private fun evalLoop() {
while (true) {
val instruction = ram[programCounter]
programCounter = (programCounter + 1) and 0xffff
val opcode = instruction.opcode()
val isKeep = instruction.isKeep()
val isReturn = instruction.isReturn()
isShort = instruction.isShort()
sourceStack = if (isReturn) returnStack else workingStack
destinationStack = if (isReturn) workingStack else returnStack
sourceStack.beginInstruction(isKeep)
//instruction with top 3 bits removed will be 0 for all immediate instructions:
if (opcode == 0) {
when (instruction) {
BRK -> return
JCI -> jumpConditionalImmediate()
JMI -> jumpImmediate()
JSI -> jumpStashReturnImmediate()
else -> literal(isReturn)
}
continue
}
when (opcode) {
INC -> increment()
POP -> pop()
NIP -> nip()
SWP -> swap()
ROT -> rotate()
DUP -> duplicate()
OVR -> over()
EQU -> equal()
NEQ -> notEqual()
GTH -> greaterThan()
LTH -> lessThan()
JMP -> jump()
JCN -> jumpConditional()
JSR -> jumpStashReturn()
STH -> stash()
LDZ -> loadZeroPage()
STZ -> storeZeroPage()
LDR -> loadRelative()
STR -> storeRelative()
LDA -> loadAbsolute()
STA -> storeAbsolute()
DEI -> deviceInput()
DEO -> deviceOutput()
ADD -> add()
SUB -> sub()
MUL -> mul()
DIV -> div()
AND -> and()
ORA -> ora()
EOR -> eor()
SFT -> shift()
}
}
}
//region immediate opcode methods
fun jumpConditionalImmediate() {
val cond = sourceStack.pop8()
val off = read16(programCounter)
programCounter = (programCounter + 2) and 0xffff
if (cond != 0) programCounter = (programCounter + off) and 0xffff
}
fun jumpImmediate() {
val off = read16(programCounter)
programCounter = (programCounter + 2 + off) and 0xffff
}
fun jumpStashReturnImmediate() {
val off = read16(programCounter)
returnStack.push16((programCounter + 2) and 0xffff)
programCounter = (programCounter + 2 + off) and 0xffff
}
fun literal(isReturn: Boolean) {
val toStack = if (isReturn) returnStack else workingStack
when {
isShort -> {
toStack.push16(read16(programCounter))
programCounter = (programCounter + 2) and 0xffff
}
else -> {
toStack.push8(ram[programCounter])
programCounter = (programCounter + 1) and 0xffff
}
}
}
//endregion
//region standard opcode methods
private fun increment() {
sourceStack.push(sourceStack.pop(isShort) + 1, isShort)
}
private fun pop() {
sourceStack.pop(isShort)
}
private fun nip() {
val b = sourceStack.pop(isShort)
sourceStack.pop(isShort)
sourceStack.push(b, isShort)
}
private fun swap() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(b, isShort)
sourceStack.push(a, isShort)
}
private fun rotate() {
val c = sourceStack.pop(isShort)
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(b, isShort)
sourceStack.push(c, isShort)
sourceStack.push(a, isShort)
}
private fun duplicate() {
val a = sourceStack.pop(isShort)
sourceStack.push(a, isShort)
sourceStack.push(a, isShort)
}
private fun over() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a, isShort)
sourceStack.push(b, isShort)
sourceStack.push(a, isShort)
}
private fun equal() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push8(if (a == b) 1 else 0)
}
private fun notEqual() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push8(if (a != b) 1 else 0)
}
private fun greaterThan() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push8(if (a > b) 1 else 0)
}
private fun lessThan() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push8(if (a < b) 1 else 0)
}
private fun jump() {
val a = sourceStack.pop(isShort)
programCounter = when {
isShort -> a
else -> (programCounter + signByte(a)) and 0xffff
}
}
private fun jumpConditional() {
val a = sourceStack.pop(isShort)
val cond = sourceStack.pop8()
if (cond != 0) {
programCounter = when {
isShort -> a
else -> (programCounter + signByte(a)) and 0xffff
}
}
}
private fun jumpStashReturn() {
val a = sourceStack.pop(isShort)
destinationStack.push16(programCounter)
programCounter = when {
isShort -> a
else -> (programCounter + signByte(a)) and 0xffff
}
}
private fun stash() {
destinationStack.push(sourceStack.pop(isShort), isShort)
}
private fun loadZeroPage() {
val address = sourceStack.pop8()
sourceStack.push(
when {
isShort -> read16(address)
else -> ram[address]
}, isShort)
}
private fun storeZeroPage() {
val address = sourceStack.pop8()
val value = sourceStack.pop(isShort)
when {
isShort -> write16(address, value)
else -> ram[address and 0xffff] = value and 0xff
}
}
private fun loadRelative() {
val address = (programCounter + signByte(sourceStack.pop8())) and 0xffff
sourceStack.push(
when {
isShort -> read16(address)
else -> ram[address]
}, isShort)
}
private fun storeRelative() {
val address = (programCounter + signByte(sourceStack.pop8())) and 0xffff
val value = sourceStack.pop(isShort)
when {
isShort -> write16(address, value)
else -> ram[address] = value and 0xff
}
}
private fun loadAbsolute() {
val address = sourceStack.pop16()
sourceStack.push(
when {
isShort -> read16(address)
else -> ram[address]
}, isShort)
}
private fun storeAbsolute() {
val address = sourceStack.pop16()
val value = sourceStack.pop(isShort)
when {
isShort -> write16(address, value)
else -> ram[address and 0xffff] = value and 0xff
}
}
private fun deviceInput(){
val port = sourceStack.pop8()
when {
isShort -> {
onDeviceInput(port)
val hi = devicePage[port]
onDeviceInput((port + 1) and 0xff)
val lo = devicePage[(port + 1) and 0xff]
sourceStack.push16((hi shl 8) or lo)
}
else -> {
onDeviceInput(port)
sourceStack.push8(devicePage[port])
}
}
}
private fun deviceOutput() {
val port = sourceStack.pop8()
val value = sourceStack.pop(isShort)
when {
isShort -> {
devicePage[port] = (value ushr 8) and 0xff
onDeviceOutput(port)
devicePage[(port + 1) and 0xff] = value and 0xff
onDeviceOutput((port + 1) and 0xff)
}
else -> {
devicePage[port] = value and 0xff
onDeviceOutput(port)
}
}
}
private fun add() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a + b, isShort)
}
private fun sub() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a - b, isShort)
}
private fun mul() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a * b, isShort)
}
private fun div() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(if (b == 0) 0 else a / b, isShort)
}
private fun and() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a and b, isShort)
}
private fun ora() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a or b, isShort)
}
private fun eor() {
val b = sourceStack.pop(isShort)
val a = sourceStack.pop(isShort)
sourceStack.push(a xor b, isShort)
}
private fun shift() {
val control = sourceStack.pop8()
val a = sourceStack.pop(isShort)
val res = (a ushr (control and 0x0f)) shl ((control ushr 4) and 0x0f)
sourceStack.push(res, isShort)
}
//endregion
}
class UxnFault(source: String, val error: Error) :
RuntimeException("$source ${error.name.lowercase()}") {
companion object {
enum class Error { UNDERFLOW, OVERFLOW }
}
}
Stack.kt
package yorkshire.systems.kuxn.cpu
import yorkshire.systems.kuxn.cpu.UxnFault.Companion.Error
class Stack(private val name: String) {
val data = IntArray(0x100)
var pointer = 0
private var keep = false
private var keepPointer = 0
fun beginInstruction(keep: Boolean) {
this.keep = keep
keepPointer = pointer
}
fun push(value: Int, short: Boolean) = if (short) push16(value) else push8(value)
fun pop(short: Boolean) = if (short) pop16() else pop8()
fun push8(value: Int) {
if (pointer >= data.size) throw UxnFault(name, Error.OVERFLOW)
data[pointer] = value and 0xff
pointer++
}
fun push16(value: Int) {
push8(value ushr 8)
push8(value)
}
fun pop8(): Int = if (keep) {
if (keepPointer <= 0) throw UxnFault(name, Error.UNDERFLOW)
data[--keepPointer]
} else {
if (pointer <= 0) throw UxnFault(name, Error.UNDERFLOW)
data[--pointer]
}
fun pop16(): Int {
val lo = pop8()
val hi = pop8()
return (hi shl 8) or lo
}
}
Opcodes.kt
package yorkshire.systems.kuxn.cpu
/**
* bit 7 6 5 4 3 2 1 0
* k r 2 └───────┘ base opcode (0x00..0x1f)
* │ │ └─────────── short (2)
* │ └───────────── return (r)
* └─────────────── keep (k)
*
* See: https://wiki.xxiivv.com/site/uxntal_reference.html
*/
//Immediate opcodes, these opcodes have no modes:
const val BRK = 0x00 //Break: Ends the evaluation of the current vector. This opcode has no modes
const val JCI = 0x20 //Jump Conditional Immediate: Pops a byte from the working stack and if it is not zero, moves the PC to a relative address at a distance equal to the next short in memory, otherwise moves PC+2, it is written using the ?label format
const val JMI = 0x40 //Jump Immediate: Moves the PC to a relative address at a distance equal to the next short in memory. It is written using the !label format
const val JSI = 0x60 //Jump Stash Return Immediate: Pushes PC+2 to the return-stack and moves the PC to a relative address at a distance equal to the next short in memory. A plain label name resolves to a JSI operation
const val LIT = 0x80 //Literal: Pushes the next bytes in memory, and moves the PC forward by the same number of bytes (i.e: 1 byte if short mode is off or 2 bytes if it is on). The LIT opcode always has the keep mode active
//Standard opcodes:
const val INC = 0x01 //Increment: Increments the value at the top of the stack, by 1: a > a+1
const val POP = 0x02 //Pop: Removes the value at the top of the stack. POPk is the canonical NOP.
const val NIP = 0x03 //Nip: Removes the second value from the stack. This is practical to truncate a short into a byte: a b > b
const val SWP = 0x04 //Swap: Exchanges the first and second values at the top of the stack: a b > b a
const val ROT = 0x05 //Rotate: Rotates three values at the top of the stack, to the left, wrapping around: a b c > b c a
const val DUP = 0x06 //Duplicate: Duplicates the value at the top of the stack: a > a a
const val OVR = 0x07 //Over: Duplicates the second value at the top of the stack: a b > a b a
const val EQU = 0x08 //Equal: Pushes 01 to the stack if the two values at the top of the stack are equal, 00 otherwise: a b > a==b
const val NEQ = 0x09 //Not Equal: Pushes 01 to the stack if the two values at the top of the stack are not equal, 00 otherwise: a b > a!=b
const val GTH = 0x0a //Greater Than: Pushes 01 to the stack if the second value at the top of the stack is greater than the value at the top of the stack, 00 otherwise: a b > a>b
const val LTH = 0x0b //Lesser Than: Pushes 01 to the stack if the second value at the top of the stack is lesser than the value at the top of the stack, 00 otherwise: a b > a<b
const val JMP = 0x0c //Jump: Moves the PC by a relative distance equal to the signed byte on the top of the stack, or to an absolute address in short mode
const val JCN = 0x0d //Jump Conditional: If the byte preceding the address is not 00, moves the PC by a signed value equal to the byte on the top of the stack, or to an absolute address in short mode
const val JSR = 0x0e //Jump Stash Return: Pushes the PC to the return-stack and moves the PC by a signed value equal to the byte on the top of the stack, or to an absolute address in short mode
const val STH = 0x0f //Stash: Moves the value at the top of the stack to the return stack. Note that with the r-mode, the stacks are exchanged and the value is moved from the return stack to the working stack
const val LDZ = 0x10 //Load Zero-Page: Pushes the value at an address within the first 256 bytes of memory, to the top of the stack
const val STZ = 0x11 //Store Zero-Page: Writes a value to an address within the first 256 bytes of memory
const val LDR = 0x12 //Load Relative: Pushes a value at a relative address in relation to the PC, within a range between -128 and +127 bytes, to the top of the stack
const val STR = 0x13 //Store Relative: Writes a value to a relative address in relation to the PC, within a range between -128 and +127 bytes
const val LDA = 0x14 //Load Absolute: Pushes the value at an absolute address, to the top of the stack
const val STA = 0x15 //Store Absolute: Writes a value to an absolute address
const val DEI = 0x16 //Device Input: Device Input: Pushes a value from the device page, to the top of the stack. The target device might capture the reading to trigger an I/O event
const val DEO = 0x17 //Device Output: Writes a value to the device page. The target device might capture the writing to trigger an I/O event
const val ADD = 0x18 //Add: Pushes the sum of the two values at the top of the stack.: a b > a+b
const val SUB = 0x19 //Subtract: Pushes the difference of the first value minus the second, to the top of the stack: a b > a-b
const val MUL = 0x1a //Multiply: Pushes the product of the first and second values at the top of the stack: a b > a*b
const val DIV = 0x1b //Divide: Pushes the quotient of the first value over the second, to the top of the stack. A division by zero pushes zero on the stack. The rounding direction is toward zero: a b > a/b
const val AND = 0x1c //And: Pushes the result of the bitwise operation AND, to the top of the stack: a b > a&b
const val ORA = 0x1d //Or: Pushes the result of the bitwise operation OR, to the top of the stack: a b > a|b
const val EOR = 0x1e //Exclusive Or: Pushes the result of the bitwise operation XOR, to the top of the stack: a b > a^b
const val SFT = 0x1f //Shifts the bits of the second value of the stack to the left or right, depending on the control value at the top of the stack. The low nibble of the control value indicates how many bits to shift right, and the high nibble, how many bits to shift left. The rightward shift is done first
//Mode flags:
const val FLAG_SHORT = 0x20 //2
const val FLAG_RETURN = 0x40 //r
const val FLAG_KEEP = 0x80 //k
//Int extensions:
fun Int.opcode(): Int = this and 0x1f
fun Int.isShort(): Boolean = this and FLAG_SHORT != 0
fun Int.isReturn(): Boolean = this and FLAG_RETURN != 0
fun Int.isKeep(): Boolean = this and FLAG_KEEP != 0
fun Int.setShort(): Int = checkModable() or FLAG_SHORT
fun Int.setReturn(): Int = checkModable() or FLAG_RETURN
fun Int.setKeep(): Int = checkBaseOpcode() or FLAG_KEEP
//Keep can only be set on standard opcodes. LIT already has the keep bit
private fun Int.checkBaseOpcode(): Int {
if (opcode() == 0) throw IllegalArgumentException(
"Can't set keep mode on immediate/LIT opcode: 0x${toString(16).padStart(2, '0')}"
)
return this
}
//Short and Return can be set on any standard opcode, plus LIT
private fun Int.checkModable(): Int {
if (opcode() == 0 && !isKeep()) throw IllegalArgumentException(
"Can't set short/return mode on immediate opcode: 0x${toString(16).padStart(2, '0')}"
)
return this
}