The x86-64 back end

Larva's own code generator for x86-64 Windows - a baseline compiler with no register allocator, Win64 calls and unwind info, SEH exceptions, GC safepoints and stack maps, COFF objects and in-memory code.

Last updated 21 min read Edit on GitHub

On this page

Larva.X64 turns the IR straight into x86-64 machine code, without LLVM. It is a baseline compiler: every local lives in a stack slot, and each statement is evaluated through a few scratch registers. It needs no register allocator and no optimizer, so it compiles quickly, and every value has an address at every call, which is what a garbage collector needs to find its roots.

 LinkResult / IrModule (one program, no imports)
        │  Monomorphizer.Run(…, GenericsMode.Specialize)
        │  Compilable.Stub (functions over unsupported types)
        ▼
 X64Codegen.Generate ── one FunctionCompiler per function, in parallel
        │                   (own Assembler: code, labels, relocations,
        │                    unwind info, scope table, stack roots)
        ▼  laid out at 16-byte boundaries
 X64Program (code, functions, relocations, static data)
        ├──► CoffWriter ─► .obj ─► MSVC link + larva_rt ─► .exe / .dll
        └──► X64Image   ─► executable memory in this process
File What it holds
X64Backend X64Backend.Compile (verify, specialize generics, generate), X64Options, X64Result, X64Program (code, functions, relocations, data; ToObject, EmitObject, Load)
X64Codegen the code generator: value shapes, unwind info, and FunctionCompiler (frames, statements, terminators, calls, exceptions, wide numbers, places, ownership, pins, polls)
Assembler an x86-64 encoder for the subset the code generator emits: Reg, Xmm, Mem, Cond, Label, Relocation
CoffWriter the COFF object: .text, .xdata, .pdata, .llvm_stackmaps, .rdata, .data, symbols and relocations
X64Image loads an X64Program into the running process, as a linker and loader would

When it is used

Where How
larva build --backend x64 An executable or DLL, linked with the C runtime by MSVC like an LLVM build. A project selects it with backend: x64 in its BUILD chunk.
larva run --backend x64 The same, then runs it.
larva native --backend x64 -o out.obj <objects>… A COFF object from linked .lvo objects (--exe links an executable).
Binding profiles (larva bind --profile) On Windows x64 the profile program is compiled with this back end and run from memory (below).
The test suites Every language feature runs "in LVM, x64 and LLVM" and must give the same output, in memory and as executables.

Why it exists: compile speed. LLVM at -O3 spends most of a build optimizing and emitting; this back end spends tens of milliseconds generating code. The design document measured a whole build of a small program at about 1 s with LLVM O3 (optimize 317 ms, emit 316 ms, link ~200 ms) against 365 ms in total with the x64 back end. It is also the fastest way to get machine code into memory without loading LLVM, and an independent second implementation of every IR feature, which keeps the LLVM back end and the VM honest.

It is not for fast programs: see limits.

Only Windows x64 is targeted (COFF, Win64 calling convention, SEH). Linux and macOS builds use LLVM.

Code generation

X64Codegen.Generate first leaves out what it cannot compile: shared generic functions (native code always specializes, GenericsMode.Specialize), declarations (defined in another native image and called by symbol), and functions over a type the back end rejects (X64Result.Skipped; since Restorations 12 and 13 every primitive is supported, so this is empty in practice). Then each function compiles into an Assembler of its own, on several threads (Parallel.For); the pieces are laid out in order at 16-byte boundaries with int3 padding. Code inside a function is position independent (branches are relative), so only calls and RIP-relative loads need relocations.

Frames and registers

Every function has the same prologue, which its unwind info describes:

push rbp
mov  rbp, rsp
sub  rsp, N          ; N patched once the frame is known, 16-aligned

Below rbp come, in order of first use: one slot per IR local (aligned to 8, up to 16), one byte per drop flag, one 8-byte pin slot per borrow inside a moving gc object, the exception slots, and scratch slots reused statement by statement (Temp). Above the slots is the outgoing argument area (at least the 32 bytes of shadow space). The epilogue is lea rsp, [rbp]; pop rbp; ret.

Registers:

Register Use
rbp frame pointer; every slot is [rbp − offset]
rax, rcx, rdx evaluating a statement; rax holds results and addresses
r8, r9 third and fourth arguments; high halves of 128-bit pairs
r11 the owning gc object of the last address computed (for the write barrier)
xmm0, xmm1 (to xmm5) floats; xmm0–xmm3 for float arguments

Only volatile registers are used besides rbp, so the prologue saves nothing else and the unwind info stays fixed.

Value shapes

X64Codegen.Shape decides how each IR type is held:

Kind Types Held as
Unit () nothing
Scalar bool, integers up to 64 bits, &T, gc T, box T, pointers, fn, T? canonical 64-bit value once loaded (sign- or zero-extended by the type)
Float f16, f32, f64 xmm; f16 is converted to f32 with F16C (vcvtph2ps / vcvtps2ph) to compute
Aggregate structs, fixed arrays bytes in the frame
Wide i128, u128, f128 (16 bytes); i256, u256, f256 (32 bytes) 64-bit limbs in the frame, lowest first

Calling convention

Calls between Larva functions and to C follow the Microsoft x64 convention:

  • The first four arguments go in rcx, rdx, r8, r9, or xmm0–xmm3 by position; the rest on the stack above the shadow space.
  • Aggregates of 1, 2, 4 or 8 bytes travel in a register; larger ones by reference to a copy. Wide numbers always travel by reference.
  • Results come back in rax or xmm0; a result that does not fit comes back through a hidden pointer.
  • On entry, StoreParameters copies incoming arguments (registers or the caller's stack) into their local slots.
  • Runtime services (intrinsics) take their arguments in rcx, rdx, r8, r9 (an f64 as its bits, a bool widened) and return in rax. Atomics are runtime calls too.
  • Under reference counting, gc handles are passed at +0: the caller keeps its reference for the call and the callee borrows it, as in the LLVM back end.

Statements and checks

Each statement loads its operands into scratch slots (Operand, Read), computes in rax/rcx/rdx or xmm0/xmm1, and stores the result to its place (Store, with a write barrier when needed). Array indexing is bounds-checked unless BoundsAnalysis proves the index in range (X64Options.BoundsCheckElimination, on by default; larva native --no-bce turns it off). Failed checks jump to out-of-line panics shared by the function, which call larva_panic(code, …): 1 for an index out of bounds, 2 for division by zero, 4 for a bad length, 5 for a null value.

Wide integers and floats

Restoration 12 in the design document made 128- and 256-bit integers work on all back ends:

  • add, sub, and, or, xor, not, negation and the six comparisons run limb by limb, chaining the carry (the movs in between leave the flags alone);
  • mul is the schoolbook product, keeping only the low limbs;
  • division, remainder and shifts call the runtime: larva_wide_udiv, urem, sdiv, srem, shl, lshr, ashr with (a*, b*, result*, limbs).

Restoration 13 added f16, f128 and f256. f128 and f256 are wide values computed by the runtime's soft-float routines: larva_float_add, sub, mul, div, rem, larva_float_cmp(a*, b*, limbs, cond) and larva_float_conv for every conversion that involves a wide float. Negation flips the top bit. The routines exist twice, bit for bit equal: in larva_rt.c for executables and in the in-process runtime (NativeRuntime, Larva.Native) for code loaded into memory.

Garbage collection

The back end emits the same GC cooperation as the IL lowering, with the GC of each function's domain (CodeGcOf), and calls the same larva_* runtime.

Safepoints

A tracing GC can only collect at a safepoint. Loop headers poll, and so does the entry of a function that can be re-entered without a loop (Safepoints.EntryPolls). A poll is one load and a branch; the runtime is called only when its flag says a collection is due:

mov  rax, qword ptr [__imp_larva_poll_pending]
mov  eax, dword ptr [rax]
test rax, rax
je   skip
call larva_poll
skip:

The flag is reached as an import of the runtime. Making safepoints a load and a branch instead of a runtime call is what brought a loop of 50 M struct method calls from 342 ms to 42 ms in the design document's early measurements (LLVM; the x64 back end uses the same poll). Loop back edges from exception handlers do not count as loops: a handler starts by reading the exception, and no collection may run before that.

Stack roots

Because every local is in its frame slot, a function's roots are simply the slots whose type may hold a tracked handle (StackRoots.MayHoldTracked): StackRoot(register 6 = rbp, offset, size). They are written to an .llvm_stackmaps section in the format LLVM emits (version 3; StackMapFormat in Larva.Native), so one runtime reader serves both back ends. Code loaded into memory registers them with the in-process runtime (NativeRuntime.RegisterCode), which scans them precisely. In executables, the C runtime walks the stack and registers conservatively, accepting interior pointers.

Barriers, pins, counting

GC capability Emitted code
Barriers (generational) after a store of handles into a gc object (owner in r11), read the owner's age byte just before its payload; only if it is old (1), call larva_gc_wb(owner, slot, size)
Moving a pin slot per borrow inside a gc object, nulled at entry, larva_pin / larva_unpin
Reference counting larva_gc_retain / larva_gc_release; the last read of an unborrowed local transfers its reference (the IL's rule)
Regions larva_region_enter, larva_region_exit, and larva_region_depth / larva_region_unwind on exception paths

Allocation calls larva_gc_new (larva_gc_new_in for another domain, larva_gc_finalizer with a finalizer) and larva_gc_array; boxes and raw cells use larva_alloc and larva_free. With tracing, objects are created without a type descriptor and stay conservative: the collector scans them word by word and never moves them. When nothing in the program traces, objects and arrays carry a descriptor ({ i32 size, i32 stride, i32 flags, i16 handles, i16 cells, i32 offsets[] }, the same as the LLVM back end) and are created with larva_gc_alloc / larva_gc_array, so a counted object that dies releases what it holds.

Exceptions

Exceptions are Windows SEH with __C_specific_handler, as in the LLVM back end:

  • throw calls RaiseException(0xE04C5641, EXCEPTION_NONCONTINUABLE, 2, [payload, type id]). The payload is a heap cell holding the value (larva_exception_cell); the type id is the same in every native image of the program (0 means any Larva exception). The code's low bytes spell LVA.
  • Every call, throw or resume site that can be caught or needs cleanup gets scope-table entries covering its call, plus a nop after it so the return address falls inside the range.
  • One entry per catch clause, with a generated filter (int filter(EXCEPTION_POINTERS*, frame), placed after the function) that checks the code and the type id and stores payload and type into the handling frame.
  • When the frame owns something, a last "any Larva exception" entry lands on cleanup code that releases what the frame owns, unpins, closes its regions and raises the exception again.
  • UNWIND_INFO uses rbp as the frame register; with scopes, it is followed by the handler RVA and the scope table, whose RVAs are fixups (UnwindFixup) filled when the image is laid out.

The exc program from the IL page (a thrower that throws its argument when negative, and a caller that catches an i32 and negates it), compiled with this back end. The nop after the call, the frees of the exception cell on each path, the landing that copies the matched payload, and the raise at the end are all visible:

Terminal
larva native --backend x64 --entry exc::caller -o exc.obj exc.lvo
compiled 1 object(s) to exc.obj (obj, direct x86-64 back end, 2 functions; bounds checks: 0 kept, 0 eliminated)
dumpbin /nologo /disasm exc.obj
…
exc::caller:
  0000000000000000: 55                 push        rbp
  0000000000000001: 48 89 E5           mov         rbp,rsp
  0000000000000004: 48 81 EC 90 00 00  sub         rsp,90h
                    00
  000000000000000B: 89 4D F0           mov         dword ptr [rbp-10h],ecx
  000000000000000E: 31 C0              xor         eax,eax
  0000000000000010: 48 89 45 E0        mov         qword ptr [rbp-20h],rax
  0000000000000014: 48 8D 45 F0        lea         rax,[rbp-10h]
  0000000000000018: 48 63 48 00        movsxd      rcx,dword ptr [rax]
  000000000000001C: 48 89 4D A0        mov         qword ptr [rbp-60h],rcx
  0000000000000020: 48 8B 4D A0        mov         rcx,qword ptr [rbp-60h]
  0000000000000024: E8 00 00 00 00     call        exc::thrower
  0000000000000029: 90                 nop
…
  00000000000000C8: 48 8B 45 D0        mov         rax,qword ptr [rbp-30h]
  00000000000000CC: 48 8B 55 C8        mov         rdx,qword ptr [rbp-38h]
  00000000000000D0: 48 89 45 B0        mov         qword ptr [rbp-50h],rax
  00000000000000D4: 48 89 55 B8        mov         qword ptr [rbp-48h],rdx
  00000000000000D8: 48 B9 41 56 4C E0  mov         rcx,0E04C5641h
                    00 00 00 00
  00000000000000E2: 48 C7 C2 01 00 00  mov         rdx,1
                    00
  00000000000000E9: 49 C7 C0 02 00 00  mov         r8,2
                    00
  00000000000000F0: 4C 8D 4D B0        lea         r9,[rbp-50h]
  00000000000000F4: E8 00 00 00 00     call        RaiseException

Reading the generated code

larva native writes LLVM's assembly with --emit asm, but this back end has no text output: it encodes machine code directly.

Terminal
larva native --backend x64 --emit asm -o app.s geom.lvo app.lvo
larva: the x64 back end emits objects only (--emit obj, no Polly)
larva native --backend x64 -o app.obj geom.lvo app.lvo
compiled 2 object(s) to app.obj (obj, direct x86-64 back end, 8 functions; bounds checks: 0 kept, 0 eliminated)

A disassembler reads the object. The trace program from the IL page has a loop that allocates (churn); here it is with MSVC's dumpbin:

Terminal
larva native --backend x64 --entry trace::keep -o trace.obj trace.lvo
compiled 1 object(s) to trace.obj (obj, direct x86-64 back end, 2 functions; bounds checks: 0 kept, 0 eliminated)
dumpbin /nologo /disasm trace.obj
…
trace::churn:
  0000000000000100: 55                 push        rbp
  0000000000000101: 48 89 E5           mov         rbp,rsp
  0000000000000104: 48 81 EC 70 00 00  sub         rsp,70h
                    00
  000000000000010B: 48 89 4D F0        mov         qword ptr [rbp-10h],rcx
  000000000000010F: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_larva_poll_pending]
                    00
  0000000000000116: 8B 40 00           mov         eax,dword ptr [rax]
  0000000000000119: 48 85 C0           test        rax,rax
  000000000000011C: 0F 84 05 00 00 00  je          0000000000000127
  0000000000000122: E8 00 00 00 00     call        larva_poll
  0000000000000127: 31 C0              xor         eax,eax
  0000000000000129: 48 89 45 C0        mov         qword ptr [rbp-40h],rax
  000000000000012D: 48 8D 45 E8        lea         rax,[rbp-18h]
  0000000000000131: 48 89 C1           mov         rcx,rax
  0000000000000134: 48 8B 55 C0        mov         rdx,qword ptr [rbp-40h]
  0000000000000138: 48 89 51 00        mov         qword ptr [rcx],rdx
  000000000000013C: E9 00 00 00 00     jmp         0000000000000141
  0000000000000141: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_larva_poll_pending]
                    00
  0000000000000148: 8B 40 00           mov         eax,dword ptr [rax]
  000000000000014B: 48 85 C0           test        rax,rax
  000000000000014E: 0F 84 05 00 00 00  je          0000000000000159
  0000000000000154: E8 00 00 00 00     call        larva_poll
  0000000000000159: 48 8D 45 E8        lea         rax,[rbp-18h]
  000000000000015D: 48 8B 48 00        mov         rcx,qword ptr [rax]
  0000000000000161: 48 89 4D C0        mov         qword ptr [rbp-40h],rcx
  0000000000000165: 48 8D 45 F0        lea         rax,[rbp-10h]
  0000000000000169: 48 8B 48 00        mov         rcx,qword ptr [rax]
  000000000000016D: 48 89 4D B0        mov         qword ptr [rbp-50h],rcx
  0000000000000171: 48 8B 45 C0        mov         rax,qword ptr [rbp-40h]
  0000000000000175: 48 8B 4D B0        mov         rcx,qword ptr [rbp-50h]
  0000000000000179: 48 39 C8           cmp         rax,rcx
  000000000000017C: 0F 9C C0           setl        al
…
  000000000000020A: 48 C7 C1 08 00 00  mov         rcx,8
                    00
  0000000000000211: 48 C7 C2 04 00 00  mov         rdx,4
                    00
  0000000000000218: E8 00 00 00 00     call        larva_gc_new
  000000000000021D: 48 8B 4D C0        mov         rcx,qword ptr [rbp-40h]
  0000000000000221: 48 89 48 00        mov         qword ptr [rax],rcx
…
  000000000000026D: E9 CF FE FF FF     jmp         0000000000000141
…
  0000000000000292: 48 8B 45 F8        mov         rax,qword ptr [rbp-8]
  0000000000000296: 48 8D 65 00        lea         rsp,[rbp]
  000000000000029A: 5D                 pop         rbp
  000000000000029B: C3                 ret

What the listing shows about the design:

  • The function polls at entry and at the loop header (0x141), the target of the back jump at 0x26D.
  • i < n loads both locals from their slots into scratch slots, then into rax and rcx, compares, and stores the bool back to its slot: every value goes through memory.
  • A jump to the next block is still emitted (jmp 0x141 at 0x13C): there is no layout or peephole pass.
  • gc.new of the 8-byte Point is larva_gc_new(8, 4) (size, alignment), then the value is copied into the new object.
  • Calls and __imp_ loads are 00 00 00 00 placeholders: relocations the linker fills.

The object file

CoffWriter writes what link.exe or lld-link consume:

Section Contents
.text the code; IMAGE_REL_AMD64_REL32 relocations for every call and RIP-relative load
.xdata UNWIND_INFO per function, with __C_specific_handler and its scope table when it has handlers
.pdata the function table: begin, end, unwind info (IMAGE_REL_AMD64_ADDR32NB)
.llvm_stackmaps tracing modes only: the stack roots, with an IMAGE_REL_AMD64_ADDR64 to each function
.rdata / .data static arrays (read-only, length header then elements, aligned to 16) and globals (writable, initial value)

Larva functions are external symbols; runtime functions (larva_*, RaiseException, __C_specific_handler) are undefined externals for the linker. For trace.obj:

Terminal
dumpbin /nologo /symbols trace.obj
…
006 00000000 SECT4  notype       Static       | .llvm_stackmaps
    Section length   A0, #relocs    2, #linenums    0, checksum        0
008 00000000 SECT1  notype ()    External     | trace::keep
009 00000100 SECT1  notype ()    External     | trace::churn
00A 00000000 UNDEF  notype ()    External     | __imp_larva_poll_pending
00B 00000000 UNDEF  notype ()    External     | larva_gc_new
00C 00000000 UNDEF  notype ()    External     | larva_poll
…

When larva build --backend x64 makes an executable (ExecutableBuilder), function symbols get the prefix larva. so they cannot clash with C names such as main; the entry wrappers are added; and the object is linked with the runtime larva_rt.c, compiled once by MSVC and kept in the toolchain cache (%LOCALAPPDATA%\Larva\toolchain). Executables link no C library, only kernel32.dll and ntdll.dll, with /NODEFAULTLIB and the runtime's own entry point ("Item 11" of the design document: a hello world is about 58 KB, and start-up dropped by about 12% without the C library). LARVA_MAP=1 also writes a linker .map for crash reports and profiles. See native builds and the runtime.

In-memory code

X64Program.Load() returns an X64Image: the program placed in the running process the way a linker and loader would.

  1. One VirtualAlloc block (read, write, execute) holds the code, a 16-byte jump stub per external symbol, the unwind info, the function table and the static data.
  2. Each stub is jmp [rip + 0] followed by the 64-bit target, so calls reach the runtime with a 32-bit displacement however far it is. Targets come from the in-process runtime (NativeRuntime.Symbols); RaiseException comes from kernel32.dll and __C_specific_handler from ntdll.dll. An __imp_ symbol (such as __imp_larva_poll_pending) stores the address of runtime data in the stub, where a RIP-relative load reaches it.
  3. Relocations are resolved, unwind fixups filled, and the function table registered with RtlAddFunctionTable, so the operating system can unwind through the code and SEH exceptions work.
  4. The stack roots are registered with NativeRuntime.RegisterCode, and the instruction cache is flushed.
  5. image.Address("name") gives a function's entry point; disposing the image deletes the function table, unregisters the code and frees the block.

Binding profiles use this path on Windows x64: compile the profile program, Load(), call the entry, reset the in-process runtime. The tests use it to run every linked program in memory next to the VM and the LLVM JIT.

The LLVM back end has its own in-memory path, Larva.Llvm/JitMemory: a memory manager for LLVM's MCJIT that places all sections of a module in one executable block (so its image base is the block start), registers the module's .pdata with RtlAddFunctionTable on finalization, and hands its .llvm_stackmaps to the same NativeRuntime. Without the function table, the operating system cannot unwind through JIT frames and any SEH exception crashes the process. Windows' CET shadow stack rejects exception continuations into such code unless they are registered, so hosts that run JIT code with exceptions opt out of CET compatibility.

Compile time

A small program (sum.ant below), built twice with each back end by ahead-of-time compiled larva, second runs:

Terminal
larva build --timings --backend x64 -o sum-x64.exe sum.ant
sum-x64.exe (x64 back end, entry sum-x64::Demo.$Module.Main)
timings: System.Core (compiled library) 6 ms, frontend 102 ms, HIR link 10 ms, lowering 149 ms, IR checks 99 ms, IR link 50 ms, native (x64) 527 ms; total 943 ms
larva build --timings -o sum-llvm.exe sum.ant
sum-llvm.exe (llvm back end, O3, entry sum-llvm::Demo.$Module.Main)
timings: System.Core (compiled library) 8 ms, frontend 96 ms, HIR link 11 ms, lowering 134 ms, IR checks 90 ms, IR link 48 ms, native (llvm) 512 ms; total 899 ms

The native step includes linking with MSVC, which dominates both on a small program and varies a lot from run to run (the design document measured it at about 200 ms). LARVA_TIMINGS=1 splits it:

Terminal
LARVA_TIMINGS=1 larva build --backend x64 -o sum-x64.exe sum.ant
…
[timing] type check: 314 ms
[timing] region check: 0 ms
[timing] x64: generate: 21 ms
[timing] link (runtime and linker): 651 ms
sum-x64.exe (x64 back end, entry sum-x64::Demo.$Module.Main)
LARVA_TIMINGS=1 larva build -o sum-llvm.exe sum.ant
…
[timing] monomorphize (specialize): 3 ms
[timing] llvm: build IR (8 functions): 39 ms
[timing] llvm: verify: 20 ms
[timing] llvm: optimize (O3): 205 ms
[timing] llvm: emit object: 216 ms
[timing] link (runtime and linker): 1.418 ms
sum-llvm.exe (llvm back end, O3, entry sum-llvm::Demo.$Module.Main)

Generating code takes 21 ms with this back end and about 480 ms with LLVM (build IR, verify, optimize, emit). The difference grows with the program: LLVM's time is in optimizing and emitting each function, which the x64 back end does not do.

Run time

AntrilRun
import System;

into namespace Demo
{
	Sum(i64 n) : i64
	{
		i64 total = 0;
		loop (i64 i = 0; i < n; i++)
		{
			total += i;
		}
		return total;
	}

	Main()
	{
		Console.WriteLine(Sum(100000000));
	}
}

Both executables print 4999999950000000. The x64 one took 0.9 to 1.6 s over three runs; the LLVM one 16 ms (after a first run of 230 ms), because LLVM keeps i and total in registers and optimizes the loop, while this back end loads and stores both from the frame on every iteration and polls at the loop header.

The design document's measurements, executables against the C runtime:

Benchmark LLVM -O3 x64
Fib(32), recursive calls ~0 ms 1 ms
50 M struct method calls 42 ms 1,503 ms
50 M virtual calls, one object 131 ms 1,446 ms
50 M virtual calls, two objects alternating 782 ms 2,212 ms
10 M objects created 186 ms 643 ms
20 k exceptions 44 ms 40 ms
100 M additions through T : number 8 ms 1,246 ms
20 M Sqrt, Floor, Ceiling, Truncate, Abs, Max 199 ms 3,790 ms

Exceptions cost the same (both go through the operating system's unwinder); calls and arithmetic in loops are where the baseline compiler pays. The last row also shows why the IR is cleaned up before any back end runs: the monomorphizer's constant folding and copy forwarding made generic Floor<f64> the same as the f64 version, and without them x64 was 2.75 times slower still.

Limits compared with LLVM

x64 back end LLVM back end
Registers none allocated: every local in its slot, scratch through memory full register allocation
Optimization none (no inlining, no loop or layout passes; jumps to the next block remain) -O0 … -O3, inlining, vectorization, --cpu
Allocation runtime call for every object small objects and arrays allocated inline from the size class's free list
Generics specialized (as LLVM) specialized
Platforms Windows x64 only (COFF, Win64, SEH) Windows, Linux, macOS (x64 and arm64), WebAssembly
Output COFF object (or memory); no --emit asm / ll, no Polly object, assembly, LLVM IR
Reference counting +0 handles; borrowed field reads not yet +0 handles and borrowed field reads (FieldBorrows)
Stack roots in executables frame slots in .llvm_stackmaps; the C runtime scans conservatively the same
Compile time tens of ms to generate hundreds of ms or more

The design document lists register allocation as what this back end would need to be fast. Planned in the design document, not implemented: borrowed field reads under reference counting in x64.

Its role is the opposite of LLVM's: quick builds while developing, a way to get machine code into memory without loading LLVM, and a second implementation that every feature must agree with. For shipping, build with LLVM (the default).

Was this page helpful?

No address is stored; your user name is, if you are signed in.

Esc