For faster performance, Acorn programs are compiled into byte-code prior to efficient interpretation. Byte-code consists of 32-bit instructions. One byte is the op code, one byte is the A operand, and the remaining two bytes vary depending on the op code: separate B and C byte-size operands, a single unsigned 16-bit Bx operand, or a signed 16-bit sBx operand.

The A operand always refers to one of 256 registers, all located on the current frame's data stack. The contents of register A is specified below as R(A). Register 0 is always 'self'. Register 1 and up hold any fixed parameters passed to the program. Above the fixed parameters are all the local variables. The rest of the registers are a temporary working area.

Load

Use one of the basic load op codes to put a value into register A (and sometimes those that follow).

LoadReg
R(A) := R(B)
LoadRegs
R(A .. A+C-1) := R(B .. B+C-1)
LoadLit
R(A) := Literals(Bx)
All literals found in the Acorn program are collected in an ordered list. This retrieves a literal number, string (copy) or symbol into a register.
LoadLitX
R(A) := Literals(extra arg) {+ EXTRAARG(Ax)}
Use when there are more than 65536 literals using data in the following instruction.
LoadPrim
R(A) := B==0? null, B==1? false, B==2? true
For loading one of three primitive values, without requiring a literal.
LoadNulls
R(A), R(A+1), ..., R(A+B) := null
For loading multiple nulls in consecutive locations.
LoadVararg
R(A), R(A+1), ..., R(A+B-1) := vararg
if (B == 0xFF) then use actual number of varargs and set top. This retrieves any variable arguments passed to the program.

Global and Closure variables

These provide access to the thread's global variables or the current method's closure variables. The name of the global variable is a symbol stored in the literal list. The closure variables are numbered: 0=get method, 1=set method, 2=first closure variable, etc.

GetGlobal
R(A) := Globals(Literals(Bx))
SetGlobal
Globals(Literals(Bx)) := R(A)
GetClosure
R(A) := Closure(Bx))
SetClosure
Closure(Bx) := R(A)

Property Access

These provide access to a value's properties.

GetMeth
R(A) := R(A).*R(A+1), if R(A) is not executable
GetProp
R(A) := R(A).*R(A+1)
SetProp
R(A) := R(A).*R(A+1) = R(A+2)
GetActProp
R(A .. A+C-1) := R(A).R(A+1)
SetActProp
R(A) := R(A).R(A+1) = R(A+2)

Jumps

These provide various ways to transfer control to something other than the next instruction, based on specific conditions. sBx is a signed 16-bit number (0x8000 bias) indicating how much the target instruction precedes or follows the next one.

Jump
ip += sBx
JNull, JNNull
if R(A)==null then ip += sBx
JTrue, JFalse
if R(A) then ip += sBx
JLt, JLe, JGt ..
if R(A) is some combination of <0, >0, 0 or null then ip += sBx. There are 12 variants: JEq, JNe, JLt, JLe, JGt, JGe, JEqN, JNeN, JLtN, JLeN, JGtN, JGeN. The 6 that end with 'N' accept null as a valid value for a positive test. Typically, use after doing a <=> method.
JSame, JDiff
if R(A)!===R(A+1) then ip+=sBx.

Method Calls and Returns

All calls are object-oriented, with register A pointing to a symbol representing the method and register A+1 being the 'self' value whose type determines the method to execute.

LoadStd
R(A+1) := R(B); R(A) = StdMeth(C)
An efficient short-hand to load both 'self' and the symbol for a common method (e.g., '()').
GetCall
R(A .. A+C-1) := R(A+1).R(A)(R(A+1) .. A+B-1))
if (B == 0xFF) then B = top. If (C == 0xFF), then top is set to last_result+1, so next open instruction (CALL, RETURN, SETLIST) may use top.
SetCall
R(A .. A+C-1) := R(A+1).R(A)(R(A+1) .. A+B-1))
Like Call, but uses the closure's set method, with R(A+1) the value to be set. if (B == 0xFF) then B = top. If (C == 0xFF), then top is set to last_result+1, so next open instruction (CALL, RETURN, SETLIST) may use top.
Return
retTo(0 .. wanted) := R(A .. A+B-1); Return
if (B == 0xFF), it uses Top-1 rather than A+B-1
Caller sets where to return values to and how many wanted (if 0xFF, all return values)
Yield
retTo(0 .. wanted) := R(A .. A+B-1); nexpected := C. Yield
if (B == 0xFF), it uses Top-1 rather than A+B-1
Caller sets where to return values to and how many wanted (if 0xFF, all return values)
Tailcall
return R(A)(R(A+1), ... ,R(A+B-1))
A nice way to combine return and a call in one instruction without requiring an extra call frame. Especially valuable for recursive functions.

Iterator Op Codes

The following op codes make it easier to repetitively deal with iterators (as Acorn's each statement does):

EachPrep
R(A) := R(B).Each
If R(B) is executable, this loads R(B) into R(A). Otherwise, it loads R(B).Each into R(A).
EachSplat
R(A+1) := R(A)/null, R(A+2) := ...[R(A)], R(A):=R(A)+1
This acts like an 'each' iterator for ..., retrieving next value from varargs.
EachCall
R(A+1 .. A+C) := R(A+1).R(A)(R(A+1) .. A+B-1))
(like GetCall above, except it preserves R(A)'s value if (B == 0xFF) then B = top. If (C == 0xFF), then top is set to last_result+1, so next open instruction (CALL, RETURN, SETLIST) may use top.

Example

By way of example, the recursive Acorn factorial method:

Fact: [a]
	return a if self<=0
	(self-1).Fact(self*(a&&1))

Might compile into these byte-code instructions:

 0: loadstd 2,0,'<=>'
 1: loadlit 4,0  ;0
 2: getcall 2,2,1
 3: jgt +1 ;goes to instruction 5
 4: return 1,1
 5: loadlit 2,1  ;'Fact'
 6: loadstd 3,0,'-'
 7: loadlit 5,2  ;1
 8: getcall 3,2,1
 9: loadstd 4,0,'*'
10: jfalse 1,+2
11: loadreg 6,1
12: jmp +1
13: loadlit 6,2 ;1
14: getcall 4,2,1
15: tailcall 2,2,1

Acknowledgements

The Acorn byte-code was inspired by those for Lua, but reflects that the byte codes are pretty much just moving data around and transferring program control. All operations are object-oriented calls instead of hard-wired. The Lua byte-codes are described in these resources: