ares/docs/llvm.md
2022-03-23 11:59:45 -05:00

9.0 KiB

Compiling Nock to LLVM

Nock formulae are nouns, which are binary trees whose leaves are natural numbers. The LLVM IR is a mostly sequential language comprised of operations arranged into basic blocks and functions.

In order to generate LLVM IR which executes a Nock formula, we can conceptually lower Nock to NockIR. NockIR is a mostly-sequential language which contains a set of instructions optimized as a target for Nock. It abstracts memory management and is intended to run with the stack allocator described in memory.md.

The NockIR VM

The machine semantically has three registers. Two of these registers, sub and res, contain noun values. The third, pc, contains a function pointer.

  • sub contains the subject of the current Nock computation.
  • res stores the result of a completed Nock computation.
  • pc can semantically be read to saved on the stack, and set to jump.

It is important to note that we do not actually implement pc directly in LLVM, rather we create new functions at save points and tail call to jump.

The machine also has a stack with the number of "slots" in a frame specified per-frame when it is pushed. Each slot can hold a noun, the first slot (slot 0) can optionally hold a function pointer. We reference slots in the current frame as frame[n] where n is the 0-based index of the slot.

We denote the NockIR for a nock formula as ir[x] where x is a formula.

NockIR provides the following instructions:

Instruction Operation
axe[x] res := /[x sub]
cel[s] res := [frame[s] res]
puh[x] push a frame with x slots
pop pop a frame
put[s] frame[s] := res
get[s] res := frame[s]
sub sub := res
noc sub := /[2 res]; res := /[3 res]
sav[s] frame[s] := sub
reo[s] sub := frame[s]
con[x] res := x
cl? if res is a cell, res := 0 else res := 1
inc res := res + 1 (crash if cell)
eq?[s] if frame[s] == res then res :=0 else res := 1
edt[x] res := #[x res sub]
ext sub := [res sub]
lnt pc := ir[res]
lnk frame[0] := pc; pc := ir[res]
don pc = frame[0]
br0[x,y] if res is 0, pc = x, if res is 1, pc = y, crash otherwise
spy External jump to a runtime-provided function producing a noun.
hns[b] Look up a static hint in the hint table, jump if entry
hnd[b] Look up a dynamic hint from res in the table, jump if entry.

The translation of Nock to NockIR takes the Nock formula plus two extra input bits, both of which are 0 if unspecified:

Code generation Generated code
ir[0 0 [[b c] d]] puh[1]; ir[1 1 [b c]]; put[0]; ir[0 1 d]; cel[0]; pop; don
ir[s 1 [[b c] d]] puh[1]; ir[1 1 [b c]]; put[0]; ir[s 1 d]; cel[0]; pop
ir[0 0 [0 b]] axe[b]; don
ir[s 1 [0 b]] axe[b]
ir[0 0 [1 x]] con[x]; don
ir[s 1 [1 x]] con[x]; don
ir[0 0 [2 b c]] puh[1]; ir[1 1 c]; put[0]; ir[0 1 b]; cel[0]; pop; noc; lnt
ir[0 1 [2 b c]] puh[2]; ir[1 1 c]; put[1]; ir[0 1 b]; cel[1]; noc; lnk; pop
`ir[1 1 [2 b c]] puh[2]; ir[1 1 c]; put[1]; ir[1 1 b]; cel[1]; sav[1]; noc; lnk; reo[1]; pop
ir[0 0 [3 b]] ir[0 1 b]; cl?; don
ir[s 1 [3 b]] ir[s 1 b]; cl?
ir[0 0 [4 b]] ir[0 1 b]; inc; don
ir[s 1 [4 b]] ir[s 1 b]; inc;
ir[0 0 [5 b c]] puh[1]; ir[1 1 b]; put[0]; ir[0 1 c]; eq?[0]; pop; don
ir[s 1 [5 b c]] puh[1]; ir[1 1 b]; put[0]; ir[s 1 c]; eq?[0]; pop
ir[s t [6 b c d]] ir[1 1 b]; br0[ir[s t c] ir[s t d]]
ir[0 t [7 b c]] ir[0 1 b]; sub; ir[0 t c]
ir[1 1 [7 b c]] puh[1]; ir[0 1 b]; sav[0]; sub; ir[0 1 c]; reo[0]; pop
ir[0 t [8 b c]] ir[1 1 b]; ext; ir[0 t c]
ir[1 1 [8 b c]] puh[1]; ir[0 1 b]; sav[0]; ext; ir[0 1 c]; reo[0]; pop
ir[0 0 [9 b c]] ir[0 1 c]; sub; axe[b]; lnt
ir[0 1 [9 b c]] puh[1]; ir[0 1 c]; sub; axe[b]; lnk; pop
ir[1 1 [9 b c]] puh[2]; sav[1]; ir[0 1 c]; sub; axe[b]; lnk; reo[1]; pop
ir[0 0 [10 [b c] d]] puh[1]; ir[1 1 d]; put[0]; ir[0 1 c]; reo[0]; edt[b]; pop; don
ir[0 1 [10 [b c] d]] puh[1]; ir[1 1 d]; put[0]; ir[0 1 c]; reo[0]; edt[b]; pop;
ir[1 1 [10 [b c] d]] `puh[2]; sav[1]; ir[0 1 d]; put[0]; reo[1]; ir[0 1 c]; reo[0]; edt[b]; pop
ir[s t [11 [b c] d]] ir[1 1 b]; hnd[b]; ir[s t d]
`ir[s t [11 b c]] hns[b]; ir[s t c]
`ir[0 0 [12 b c]] `puh[1]; ir[1 1 b]; put[0]; ir[0 1 c]; cel[0]; spy; pop; don
`ir[s 1 [12 b c]] `puh[1]; ir[1 1 b]; put[0]; ir[s 1 c]; cel[0]; spy; pop

From NockIR to LLVM IR

NockIR is not intended to be generated separately. Rather, each NockIR instruction is implemented as a builder for some sequence of LLVM IR.

The registers for the memory allocator (the stack and frame pointers) and the VM (the subject and result) are implemented in the LLVM IR by making each basic block an LLVM function. Within a function, each mutation to a register results in a new SSA register, and the previous registers are not used after the new assignment. Branching is accomplished by means of a conditional tail cail to basic blocks which contain static, unconditional jumps to the common suffix of the branch, or don instructions otherwise.

Calling convention for basic blocks

We employ the cc 10 convention, supplied for use by the Glasgow Haskell Compiler, as it matches our own needs. This ensures no registers are spilled to the (LLVM) stack, that parameters are passed in registers, and that tail calls are always tail-call-optimized, i.e. compiled to jumps.

Each function representing a basic block, takes the current stack pointer, current frame pointer, current subject, and current value of the result register as arguments.

Instructions which need to be implemented as functions, such as axe or pop, take these 4 arguments, plus a function pointer to tail-call to continue the basic block.

Calls and returns

The NockIR provides the lnt and lnk instructions for tail and non-tail calls, respectively. The lnt instruction is relatively the simpler: it invokes the runtime system to generate or fetch cached code for the noun in the res register, then executes a dynamic jump to this code. The lnk instruction will be followed by another basic block, thus, it first saves the function pointer for that basic block in frame[0]; and then jumps to the code in question. The don instruction simply looks up the function pointer in frame[0] and jumps to it. Thus the outer frame for a computation installs a handler, matching the calling convention for basic blocks, which returns the result noun to the runtime system.

axe

The axe instruction looks up an axis in the subject and places it in the result register. It may crash if the axis is not valid for the noun in the subject register, the axis is 0, or the axis is a cell.

(TBW: other instructions)