Janet 1.16.1-87f8fe1 Documentation
(Other Versions: 1.15.0 1.13.1 1.12.2 1.11.1 1.10.1 1.9.1 1.8.1 1.7.0 1.6.0 1.5.1 1.5.0 1.4.0 1.3.1 )

The Janet Abstract Machine

The Janet language is implemented on top of an abstract machine (AM). The compiler converts Janet data structures to this bytecode, which can then be efficiently executed from inside a C program. To understand Janet bytecode, it is useful to understand the abstractions used inside the Janet AM, as well as the C types used to implement these features.

The stack = the fiber

A Janet fiber is the type used to represent multiple concurrent processes in Janet. It is basically a wrapper around the idea of a stack. The stack is divided into a number of stack frames (JanetStackFrame * in C), each of which contains information such as the function that created the stack frame, the program counter for the stack frame, a pointer to the previous frame, and the size of the frame. Each stack frame also is paired with a number of registers.

X: Slot

X - Stack Top, for next function call.
Frame next
X - Stack 0
Frame 0
X - Stack -1
Frame -1
X - Stack -2
Frame -2
Bottom of stack

Fibers also have an incomplete stack frame for the next function call on top of their stacks. Making a function call involves pushing arguments to this temporary stack frame, and then invoking either the CALL or TCALL instructions. Arguments for the next function call are pushed via the PUSH, PUSH2, PUSH3, and PUSHA instructions. The stack of a fiber will grow as large as needed, although by default Janet will limit the maximum size of a fiber's stack. The maximum stack size can be modified on a per-fiber basis.

The slots in the stack are exposed as virtual registers to instructions. They can hold any Janet value.


All functions in Janet are closures; they combine some bytecode instructions with 0 or more environments. In the C source, a closure (hereby the same as a function) is represented by the type JanetFunction *. The bytecode instruction part of the function is represented by JanetFuncDef *, and a function environment is represented with JanetFuncEnv *.

The function definition part of a function (the 'bytecode' part, JanetFuncDef *), also stores various metadata about the function which is useful for debugging, as well as constants referenced by the function.

C functions

Janet uses C functions to bridge to native code. A C function (JanetCFunction * in C) is a C function pointer that can be called like a normal Janet closure. From the perspective of the bytecode instruction set, there is no difference in invoking a C function and invoking a normal Janet function.

Bytecode format

Janet bytecode operates on an array of identical registers that can hold any Janet value (Janet * in C). Most instructions have a destination register, and 1 or 2 source registers. Registers are simply indices into the stack frame, which can be thought of as a constant-sized array.

Each instruction is a 32-bit integer, meaning that the instruction set is a constant-width RISC instruction set like MIPS. The opcode of each instruction is the least significant byte of the instruction. The highest bit of this leading byte is reserved for debugging purpose, so there are 128 possible opcodes encodable with this scheme. Not all of these possible opcodes are defined, and undefined opcodes will trap the interpreter and emit a debug signal. Note that this means an unknown opcode is still valid bytecode, it will just put the interpreter into a debug state when executed.

X - Payload bits
O - Opcode bits

   4    3    2    1
| XX | XX | XX | OO |

Using 8 bits for the opcode leaves 24 bits for the payload, which may or may not be utilized. There are a few instruction variants that divide these payload bits.

These instruction variants can be further refined based on the semantics of the arguments. Some instructions may treat an argument as a slot index, while other instructions will treat the argument as a signed integer literal, an index for a constant, an index for an environment, or an unsigned integer. Keeping the bytecode fairly uniform makes verification, compilation, and debugging simpler.

Instruction reference

A listing of all opcode values can be found in janet.h. The Janet assembly short names can be found in src/core/asm.c. In this document, we will refer to the instructions by their short names as presented to the assembler rather than their numerical values.

Each instruction is also listed with a signature, which are the arguments the instruction expects. There are a handful of instruction signatures, which combine the arity and type of the instruction. The assembler does not do any type-checking per closure, but does prevent jumping to invalid instructions and failure to return or error.


Reference table

add(add dest lhs rhs)$dest = $lhs + $rhs
addim(addim dest lhs im)$dest = $lhs + im
band(band dest lhs rhs)$dest = $lhs & $rhs
bnot(bnot dest operand)$dest = ~$operand
bor(bor dest lhs rhs)$dest = $lhs | $rhs
bxor(bxor dest lhs rhs)$dest = $lhs ^ $rhs
call(call dest callee)$dest = call($callee, args)
clo(clo dest index)$dest = closure(defs[$index])
cmp(cmp dest lhs rhs)$dest = janet_compare($lhs, $rhs)
cncl(cncl dest fiber err)Resume fiber, but raise error immediately
div(div dest lhs rhs)$dest = $lhs / $rhs
divim(divim dest lhs im)$dest = $lhs / im
eq(eq dest lhs rhs)$dest = $lhs == $rhs
eqim(eqim dest lhs im)$dest = $lhs == im
eqn(eqn dest lhs im)$dest = $lhs .== $rhs
err(err message)Throw error $message.
get(get dest ds key)$dest = $ds[$key]
geti(geti dest ds index)$dest = $ds[index]
gt(gt dest lhs rhs)$dest = $lhs > $rhs
gte(gte dest lhs rhs)$dest = $lhs .>= $rhs
gtim(gtim dest lhs im)$dest = $lhs .> im
in(in dest ds key)$dest = $ds[$key] using `in`
jmp(jmp offset)pc += offset
jmpif(jmpif cond offset)if $cond pc += offset else pc++
jmpni(jmpni cond offset)if $cond == nil pc += offset else pc++
jmpnn(jmpnn cond offset)if $cond != nil pc += offset else pc++
jmpno(jmpno cond offset)if $cond pc++ else pc += offset
ldc(ldc dest index)$dest = constants[index]
ldf(ldf dest)$dest = false
ldi(ldi dest integer)$dest = integer
ldn(ldn dest)$dest = nil
lds(lds dest)$dest = current closure (self)
ldt(ldt dest)$dest = true
ldu(ldu dest env index)$dest = envs[env][index]
len(len dest ds)$dest = length(ds)
lt(lt dest lhs rhs)$dest = $lhs < $rhs
lte(lte dest lhs rhs)$dest = $lhs .<= $rhs
ltim(ltim dest lhs im)$dest = $lhs .< im
mkarr(mkarr dest)$dest = call(array, args)
mkbtp(mkbtp dest)$dest = call(tuple/brackets, args)
mkbuf(mkbuf dest)$dest = call(buffer, args)
mkstr(mkstr dest)$dest = call(string, args)
mkstu(mkstu dest)$dest = call(struct, args)
mktab(mktab dest)$dest = call(table, args)
mktup(mktup dest)$dest = call(tuple, args)
mod(mod dest lhs rhs)$dest = $lhs mod $rhs
movf(movf src dest)$dest = $src
movn(movn dest src)$dest = $src
mul(mul dest lhs rhs)$dest = $lhs * $rhs
mulim(mulim dest lhs im)$dest = $lhs * im
neq(neq dest lhs rhs)$dest = $lhs != $rhs
neqim(neqim dest lhs im)$dest = $lhs != $im
next(next dest ds key)$dest = next($ds, $key)
noop(noop)Does nothing.
prop(prop val fiber)Propagate (Re-raise) a signal that has been caught.
push(push val)Push $val on arg
push2(push2 val1 val2)Push $val1, $val2 on args
push3(push3 val1 val2 val3)Push $val1, $val2, $val3, on args
pusha(pusha array)Push values in $array on args
put(put ds key val)$ds[$key] = $val
puti(puti ds index val)$ds[index] = $val
rem(rem dest lhs rhs)$dest = $lhs % $rhs
res(res dest fiber val)$dest = resume $fiber with $val
ret(ret val)Return $val
retn(retn)Return nil
setu(setu env index val)envs[env][index] = $val
sig(sig dest value sigtype)$dest = emit $value as sigtype
sl(sl dest lhs rhs)$dest = $lhs << $rhs
slim(slim dest lhs shamt)$dest = $lhs << shamt
sr(sr dest lhs rhs)$dest = $lhs >> $rhs
srim(srim dest lhs shamt)$dest = $lhs >> shamt
sru(sru dest lhs rhs)$dest = $lhs >>> $rhs
sruim(sruim dest lhs shamt)$dest = $lhs >>> shamt
sub(sub dest lhs rhs)$dest = $lhs - $rhs
tcall(tcall callee)Return call($callee, args)
tchck(tcheck slot types)Assert $slot matches types