Ketoy
Reference

KBC Opcodes

KBC is a register-based bytecode. The interpreter dispatches **112 opcodes** via a `while`/`when` loop. Each opcode is one byte; operands follow.

This reference is for compiler-plugin authors, tooling builders, and anyone debugging a .ktx bundle. You don't need to know any of this to write @KetoyComposable functions.


Encoding overview

  • All multi-byte integers in the instruction stream are big-endian.
  • Registers are addressed by single-byte index (u8, 0–255).
  • The string pool, function table, modifier table are referenced by 16-bit indices (u16).
  • Adapter IDs / constructor IDs / capability IDs are 16-bit shorts.

Loads & moves (0x00–0x0A)

HexMnemonicOperandsEffect
0x00NOPNo-op.
0x01LOAD_INTdst:u8, value:i32regs[dst] = value
0x02LOAD_LONGdst:u8, value:i64
0x03LOAD_FLOATdst:u8, value:f32
0x04LOAD_DOUBLEdst:u8, value:f64
0x05LOAD_BOOL_TRUEdst:u8regs[dst] = true
0x06LOAD_BOOL_FALSEdst:u8regs[dst] = false
0x07LOAD_NULLdst:u8regs[dst] = null
0x08LOAD_STRINGdst:u8, poolIdx:u16Loads from the bundle's string pool.
0x09LOAD_UNITdst:u8regs[dst] = Unit
0x0AMOVEdst:u8, src:u8regs[dst] = regs[src]

Arithmetic (0x0B–0x1F)

Double: 0x0B–0x0EADD_DOUBLE, SUB_DOUBLE, MUL_DOUBLE, DIV_DOUBLE. Int: 0x10–0x15ADD_INT, SUB_INT, MUL_INT, DIV_INT, MOD_INT, NEG_INT. Long: 0x16–0x1BADD_LONG, SUB_LONG, MUL_LONG, DIV_LONG, MOD_LONG, NEG_LONG. Float: 0x1C–0x1FADD_FLOAT, SUB_FLOAT, MUL_FLOAT, DIV_FLOAT.

All three-register form: dst:u8, lhs:u8, rhs:u8 (NEG: dst:u8, src:u8).

Divide-by-zero on DIV_INT / MOD_INT / DIV_LONG / MOD_LONG throws KBCRuntimeException. Float / double divide produces Infinity / NaN per IEEE-754. The Tier-1 JIT skips DIV/MOD opcodes because the exception-wrap contract is non-trivial in DEX.

Comparison & nullability (0x20–0x29)

HexMnemonicOperands
0x20CMP_EQdst:u8, lhs:u8, rhs:u8 — uses equals
0x21CMP_NEQsame
0x22CMP_LTsame
0x23CMP_LTEsame
0x24CMP_GTsame
0x25CMP_GTEsame
0x26CMP_REF_EQdst, lhs, rhs — reference equality (===)
0x27IS_NULLdst:u8, src:u8
0x28IS_NOT_NULLdst:u8, src:u8
0x29NOTdst:u8, src:u8 — Boolean negation

Control flow (0x30–0x38)

HexMnemonicOperands
0x30JUMPtargetPC:i32
0x31JUMP_IF_TRUEcond:u8, targetPC:i32
0x32JUMP_IF_FALSEcond:u8, targetPC:i32
0x33JUMP_IF_NULLreg:u8, targetPC:i32
0x34JUMP_IF_NOT_NULLreg:u8, targetPC:i32
0x35RETURNsrc:u8 — 0xFF sentinel returns Unit
0x36THROWreg:u8
0x37TRY_CATCHhandlerPC:i32, catchTypeIdx:u16, exDst:u8
0x38END_TRY— pops handler stack

Exception handler type-filtering is currently catch-all (KNOWN_ISSUES #2): the catchTypeIdx is recorded but not matched against the thrown value's type, so multi-catch collapses to the first handler. Rethrow works correctly.

Object model (0x40–0x4B)

HexMnemonicOperands
0x40NEW_INSTANCEdst:u8, classNameIdx:u16
0x41GET_FIELDdst:u8, obj:u8, fieldNameIdx:u16
0x42SET_FIELDobj:u8, fieldNameIdx:u16, value:u8
0x43INVOKE_VIRTUALdst:u8, obj:u8, methodIdx:u16, argc:u8, args:u8*
0x44INVOKE_STATICdst:u8, methodIdx:u16, argc:u8, args:u8*
0x45INSTANCEOFdst:u8, src:u8, typeIdx:u16
0x46OR_INTdst:u8, lhs:u8, rhs:u8
0x47AND_INTsame
0x48XOR_INTsame
0x49SHL_INTsame
0x4ASHR_INTsame
0x4BUSHR_INTsame

Bitwise opcodes were added in 0.3.x to support K2's $default / $changed bitfield merging, which the Compose plugin emits heavily.

Capability calls (0x50–0x52)

HexMnemonicOperands
0x50INVOKE_CAPABILITYdst:u8, capId:u16, argc:u8, args:u8*
0x51INVOKE_CAPABILITY_VOIDcapId:u16, argc:u8, args:u8*
0x52INVOKE_CAPABILITY_SUSPENDdst:u8, capId:u16, argc:u8, args:u8* — sets up suspension

After INVOKE_CAPABILITY_SUSPEND, a SUSPEND_POINT typically follows.

Collections (0x60–0x6A)

HexMnemonicOperands
0x60LIST_NEWdst:u8
0x61LIST_ADDlist:u8, value:u8
0x62LIST_GETdst:u8, list:u8, idx:u8
0x63LIST_SIZEdst:u8, list:u8
0x64LIST_FOR_EACHlist:u8, elemReg:u8, bodyEndPC:i32
0x68MAP_NEWdst:u8
0x69MAP_PUTmap:u8, key:u8, value:u8
0x6AMAP_GETdst:u8, map:u8, key:u8

Coroutines (0x70–0x79)

HexMnemonicOperands
0x70SUSPEND_POINTcontId:u16
0x71RESUME_VALUEdst:u8
0x72RESUME_EXCEPTIONreg:u8
0x73LAUNCHfnIdx:u16, argc:u8, args:u8*, scopeReg:u8
0x74ASYNCdst:u8, fnIdx:u16, argc:u8, args:u8*, scopeReg:u8
0x75AWAITdst:u8, deferred:u8
0x76WITH_CONTEXTdispatcherCapId:u16, fnIdx:u16, argc:u8, args:u8*, dst:u8
0x77FLOW_EMITflow:u8, value:u8
0x78FLOW_COLLECTflow:u8, collectorFnIdx:u16, dst:u8
0x79COLLECT_AS_STATEdst:u8, flow:u8, initial:u8

Compose runtime (0x80–0x88)

HexMnemonicOperands
0x80COMPOSE_CALL(placeholder — COMPOSABLE_CALL is the real adapter dispatch at 0xB0)
0x81COMPOSE_REMEMBERdst, key
0x82COMPOSE_STATEdst, initial
0x83COMPOSE_DERIVEDdst, computeFnIdx
0x84COMPOSE_SIDE_EFFECTeffectFnIdx
0x85COMPOSE_LAUNCHED_EFFECTkeyReg, bodyFnIdx
0x86COMPOSE_DISPOSABLE_EFFECTkeyReg, bodyFnIdx
0x87COMPOSE_AMBIENT_READdst, ambientCapId
0x88COMPOSE_KEYkeyReg, bodyFnIdx

Strings (0x90–0x93)

HexMnemonicOperands
0x90STRING_CONCATdst:u8, count:u8, regs:u8*
0x91STRING_LENGTHdst:u8, src:u8
0x92STRING_SUBSTRdst:u8, src:u8, start:u8, end:u8
0x93INT_TO_STRINGdst:u8, src:u8

Every + / template / multi-arg concat collapses to one STRING_CONCAT opcode.

Casts & boxing (0xA0–0xAB)

HexMnemonicOperands
0xA0CASTdst, src, typeIdx — throws on mismatch
0xA1SAFE_CASTdst, src, typeIdx — null on mismatch
0xA2UNBOX_INTdst, src
0xA3BOX_INTdst, src
0xA4UNBOX_LONGdst, src
0xA5BOX_LONGdst, src
0xA6UNBOX_FLOATdst, src
0xA7BOX_FLOATdst, src
0xA8UNBOX_DOUBLEdst, src
0xA9BOX_DOUBLEdst, src
0xAAUNBOX_BOOLEANdst, src
0xABBOX_BOOLEANdst, src

Adapter dispatch (0xB0–0xB1)

The two opcodes that bridge KBC into Compose / typed object construction.

HexMnemonicOperands
0xB0COMPOSABLE_CALLadapterId:u16, paramCount:u8, [paramIdx:u8 + KBCValue]*
0xB1CONSTRUCT_JVMdst:u8, adapterId:u16, paramCount:u8, [paramIdx:u8 + KBCValue]*

Sparse parameter encoding: only non-default params are encoded. Each appears as (paramIdx: u8, value: KBCValue) where KBCValue is a tag-byte-prefixed payload (see Compose Tokens for the tag space). Slots not encoded resolve via the adapter's source default or the registered paramDefault.

Debug (0xF0–0xF1)

HexMnemonicOperands
0xF0DEBUG_PRINTreg:u8
0xF1BREAKPOINT

Only emitted with debugMode = true in the Gradle DSL.


Decoding a .ktx file

The ketoy analyze <path> CLI command dumps the section table; pass --json for a machine-readable form. Inside Kotlin:

kotlin
import dev.ketoy.bundle.KtxReader
import dev.ketoy.bytecode.KBCInstructionDecoder

val bytes = File("main.ktx").readBytes()
val bundle = KtxReader(verifySignature = false).read(bytes)

for ((i, fn) in bundle.functions.withIndex()) {
    println("fn[$i] ${fn.name} — ${fn.code.size} bytes, ${fn.localCount} locals")
    val decoded = KBCInstructionDecoder.decodeAll(fn.code, bundle.stringPool)
    decoded.forEach { println("  $it") }
}

Each KBCInstruction sealed-class variant carries typed operand fields.


Operand layout cheatsheet

Register     u8        (0..255 per function)
String pool  u16 idx   (0..65535)
Function     u16 idx   (0..65535)
Adapter id   u16
Cap id       u16
PC           i32       (byte offset within function code)
Int literal  i32
Long literal i64
Float lit    f32 (4 bytes)
Double lit   f64 (8 bytes)

The full opcode list lives in KBCOpcode.kt; human-readable descriptors in KBCOpcodeInfo.kt; the typed data class variants in KBCInstruction.kt.

Next: .ktx Bundle Format →