Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix D: Runtime ABI

This appendix describes the final native compiler and fun-rt-24. Supported hosts use 64-bit words and pointers: Apple Silicon macOS and x86-64/AArch64 Linux. The ABI is internal to this compiler edition. There is no user-written FFI.

Words and Objects

Every source value crossing a call boundary is an LLVM i64 and a Rust u64. A low bit of one denotes an immediate. A low bit of zero denotes an aligned object address, except for temporary zero placeholders during initialization.

ValueEncoding
Int(n << 1) | 1, signed 63-bit two’s complement
CharUnicode scalar shifted left one bit, low bit set
Nullary constructor(tag << 1) | 1
Heap valueAddress of its header, aligned to eight bytes

The expected type determines the interpretation. Thus False, [], Nothing, and () may all have word value 1 without being interchangeable.

header:  [ payload size: 32 bits | constructor tag: 24 bits | kind: 8 bits ]
object:  [ header ][ payload word 0 ][ payload word 1 ] ...

Size counts payload words. Constructor tags are local to their data type and follow declaration order. Fixed Prelude tags are False=0, True=1, []=0, (:)=1, Nothing=0, Just=1, ()=0, and LT=0, EQ=1, GT=2.

KindIDPayloadTraced fields
CON0Constructor fieldsAll
CLOSURE1Slow code address, raw arity, capturesCaptures
PAP2Closure word, raw argument count, argumentsClosure and arguments
DOUBLE3Binary64 bitsNone
STRING4Raw byte length, UTF-8 bytes padded to wordsNone
FORWARD5Destination address during collectionCollector-only marker

There is no heap allocation for a nullary constructor. Every movable object therefore has room for a forwarding address. Strings count bytes in storage and Unicode scalars in length-style primitives.

Entries and Application

EntryConvention and signature
Known functiontailcc i64 f(i64 env, i64 a1, ..., i64 an)
Slow adapterC: u64 slow(u64 closure, const u64 *args)
General applyC: u64 rt_apply(u64 closure, usize n, const u64 *args)
Small applyC: u64 rt_applyN(u64 closure, u64 a1, ..., u64 aN), N=1..4
PAP constructionC: u64 rt_make_pap(u64 closure, usize n, const u64 *args)
Recursive capture patchC: void rt_closure_set(u64 closure, usize index, u64 value)

The slow adapter receives exactly the function’s syntactic arity. The general apply routine flattens PAP arguments, invokes exact calls, or applies remaining arguments to a returned function. Only known tail calls have a guaranteed native tail transfer. Crossings between C and tailcc retain their ABI adapter frame.

All arguments are strict and already evaluated. Raw argument arrays must remain readable for the call. A collector may relocate their values, so persistent buffers belong to a root owner. A slow entry loads its arguments and establishes its generated root frame before any allocation.

Allocation and Roots

ExportContract
void rt_init()Initialize Boehm and register the calling runtime thread
u64 *rt_alloc(usize words)Reserve scanned storage, including header
u8 *rt_alloc_atomic(usize bytes)Reserve pointer-free storage
void rt_use_copying()Select the single-threaded moving heap before initialization
void rt_roots_push(u64 *slots, usize n)Register a writable, initialized generated root range
void rt_roots_pop()Remove the current generated root range
void rt_global_root(u64 *slot)Register a static global for the process lifetime

Allocation can collect before returning. The caller initializes the entire new object before any further allocation. Native startup registers globals before installing functions or evaluating values. Static string objects are immutable and stationary. The Boehm backend also retains their address table.

Rust’s Roots and GlobalRoot owners register stable buffers until drop. The moving collector updates their cells. Borrowed string slices and unrooted copies of pointer words must not survive an allocation. Primitive exports decode strings into owned Rust values before allocating their result.

Primitives, Errors, and Exit

Every prim_* operation has a C export named rt_prim_*. Each argument and the return value is a word, including a unit result. Integer and Double arithmetic may instead be emitted inline. Nontrivial operations share their safe Rust implementation with the interpreters.

Primitive familyArgumentsResult
intAdd/Sub/Mul/Quot/Rem/Div/ModTwo Int wordsInt word
intNegIntInt
intEq/Lt/LeTwo Int wordsBool immediate
dblAdd/Sub/Mul/Div/PowTwo boxed DoublesBoxed Double
dblNeg/Sqrt/Exp/Log/Sin/CosBoxed DoubleBoxed Double
dblEq/Lt/LeTwo boxed DoublesBool immediate
intToDbl, dblFloor/Round/TruncateSource numeric valueConverted numeric value
chrOrd, chrChr, chrEq/LtCharacter/integer operandsInteger/character/Bool
strAppend/Length/Eq/CompareOne or two StringsString/Int/Bool/Int
strUnpack, strPackString or character listCharacter list or String
showInt/Double/Char/StringOne value of the named typeString
readIntStringMaybe Int
putStr, getLineString or unitUnit or String
traceString message, arbitrary valueThe same value
error, exitString or IntDoes not return

Integer results wrap modulo 2⁶³. Floating conversion saturates to the Int range, with NaN becoming zero. Zero division, invalid characters, EOF on getLine, and failed matches are runtime errors.

ExportBehavior
rt_error(u64 string)Flush stdout, write fun: MESSAGE, exit 1
rt_pattern_fail(const char *file, u32 line, u32 col)Report source location, exit 1
rt_div_zero()Report fun: divide by zero, exit 1
rt_exit(i32 code)Flush stdout and terminate

Generated C main initializes the runtime, evaluates globals in dependency order, applies source main to unit word 1, and exits successfully. A runtime panic aborts rather than unwinding through generated code.