Blog

How Larva works inside

What happens between larva build and a running program? Larva is a compiler platform. It owns everything a program needs except the language it is written in: a typed program model, the lowering of that model to a low-level IR, a bytecode and a VM, native back ends, a runtime with several garbage collectors, project files, a package manager and native bindings. This post follows one small program along that road, one stage at a time. Every stage has a chapter in the internals book, and the links take you there.

The road at a glance

You can watch the stages go by with --timings:

Terminal
larva build --timings -o hello.exe hello.ant
hello.exe (llvm back end, O3, entry hello::Hello.$Module.Main)
timings: System.Core (compiled library) 83 ms, frontend 1536 ms, HIR link 54 ms, lowering 619 ms, IR checks 226 ms, IR link 93 ms, native (llvm) 1551 ms; total 4173 ms

Each name is one step: the language pack's frontend turns source into HIR, the HIR link merges it with System.Core and other libraries, lowering turns HIR into Larva IR, the IR checks verify it, the IR link produces one program, and a back end generates code. All of this happens in memory, inside the larva process. larva build writes no intermediate files. (These timings come from a working copy of larva. The installed larva is compiled ahead of time and is several times faster in every phase.) The pipeline diagram shows the whole picture.

Here is the program we will follow:

Antril
import System;

into namespace Pipeline
{
	public interface IShape
	{
		Area() : f64;
	}

	public class Square : IShape
	{
		f64 side;

		public:
			new(f64 side) { this.side = side; }
			Area() : f64 => side * side;
	}

	Scale(f64 factor) : func<f64> : f64 => x => x * factor;

	Main()
	{
		IShape shape = new Square(3);
		func<f64> : f64 twice = Scale(2);
		Console.WriteLine(twice(shape.Area()));
	}
}

It has an interface, a class, a closure and a call through an interface. Each of these is decided somewhere specific on the road.

1. The frontend is a language pack

larva does not know Antril's syntax, or any other language's. A language is a language pack: a native library that answers JSON requests through two C functions, larva_language_call and larva_language_free. The compile request takes source files and returns an HIR assembly plus diagnostics. Other requests write HIR back as source (source), drive larva types, show and search (catalog), and answer the editor (lsp.*).

The frontend resolves names, overloads, types and inference, and nothing more. How a virtual call dispatches or how a closure is built is not its decision. Before a pack is loaded, larva checks every file against the hashes in its manifest and verifies the signature. Project templates are written in HIR too, so larva new can write the same template in any installed language.

Read: Language packs, especially the ABI and Templates are HIR.

2. HIR, the truth of the program

Larva HIR is the model every frontend writes. It sits above IR: it still has classes, interfaces, virtual calls, lambdas, try, patterns and generics, so that Larva can lower them with full context. It sits below source: names are resolved, every expression has a type, overloads are chosen, and operators of user types are calls to their methods. You can see a type as HIR:

Terminal
larva show --lang hir Pipeline.Square pipeline.ant
public class Pipeline.Square : System.Any, Pipeline.IShape
{
    field side: f64
    constructor Square(side: f64)
    method Area() -> f64
}

The split between HIR and IR is the central decision of Larva's design. Before it, the Antril compiler lowered every feature straight to the low-level IR, and libraries were referenced by reparsing the text of their declarations. Now every frontend, tool and library speaks HIR. The HIR linker merges the program, System.Core, other libraries and bindings into one closed assembly.

Read: Larva HIR, from Why HIR exists to Linking HIR assemblies.

3. .lva: libraries as metadata

HIR has a binary form, .lva. It is what a frontend returns, what larva lib writes and what a Hive package carries. A Larva library .dll, System.Core.dll included, is the same bytes wrapped in a minimal PE image, in a section named .lva. The format has one main goal: referencing a library should cost only its metadata. Method bodies sit in their own block at the end and are decoded only when something asks for them. Strings and types are stored once in shared tables. Writing an assembly and reading it back gives exactly the same HIR.

Read: The .lva format and its design goals.

4. The catalog and language views

Tools that show code to people all need the same things: types by namespace, members (inherited ones too), search, and a way to write a type in the reader's language. Larva.Hir.Services provides these once, over HIR. A language contributes a view, which writes HIR as its own source, and a document analysis. In return it gets larva types, larva show, larva search, completion, hover and go to definition into libraries. This is why larva show Pipeline.Square prints the class back as Antril, decompiled from HIR.

Read: The catalog and language views.

5. Lowering: where classes stop existing

Lowering turns the linked HIR into Larva IR, and it is written once, in Larva, for every language. Classes become structs with a vtable. Square.Area gets a slot, and the call through IShape goes through an interface table, with eight fixed entries in every vtable for fast lookup. Generics stay generic in IR, with requirements on their parameters, and are resolved only after linking. The native back ends specialize them, and the VM can share one body between type arguments.

The closure returned by Scale becomes an ordinary object: a Func1 pointing at a function made for the lambda, plus an environment object that holds the captured factor. Iterators and async functions become objects too. Their frames become state machines in a single IR pass, the coroutine pass. try/catch becomes unwind edges, and imports become native calls. The rule is to lower a feature to ordinary IR whenever that is enough, so that every check and every back end gets it for free.

Read: Lowering HIR to IR (see Virtual dispatch and Shared or specialized), then Lowering language features for closures and the coroutine pass.

6. Larva IR, where memory is explicit

Larva IR is small and low level on purpose: structs, gc handles, places, basic blocks and terminators. Everything native is reached through a fixed table of intrinsics, the same in every back end and in the VM. Because the IR is so small, every check and every back end works on one simple language, whatever language the program came from.

This is also where memory becomes visible: object headers, handles, borrows. The IR checks run here: the type checker, the region checker for region { } blocks in arena mode, safepoints and bounds facts.

Read: Larva IR and its analyses.

7. The borrow checker

Antril's &T and &mut T references are checked here, on IR rather than on source. That gives one checker for every language, and it checks exactly the code that will run. For each function it proves initialization, moves, mutability, conflicting borrows and escapes, with two more rules for objects that many handles can reach. Functions are checked independently and in parallel, generic ones once before they are instantiated. The mode comes from borrowcheck: (off, warn or enforce). The language server runs the same check as you type.

Read: The borrow checker, starting with What it proves.

8. Objects and linking

Larva links at the IR level, over the whole program, before any machine code exists. The linker resolves symbols, shares link-once code, assigns GC domains and chooses the entry wrapper. Then the monomorphizer makes generics concrete, starting from the roots. Libraries ship as HIR, not as IR. The .lvo (one verified module) and .lvm (a linked program) files exist for tools like larva link, larva dis and larva native, but larva build never writes them.

Read: Objects, modules and linking.

9. Four back ends

The linked program goes to one of these:

  • The IL and the VM. Larva IL is a compact stack bytecode encoded in 4-bit units, run by an interpreter. It is not the fast path. It is the reference: it needs no toolchain at all, and the other back ends are compared with it. See The IL and the VM.
  • LLVM, the default and the optimizer. Larva calls LLVM in process through its C API, with the libraries shipped next to larva. It is the only back end that targets Linux, macOS and WebAssembly through --target wasm-llvm. See The LLVM back end.
  • x86-64, Larva's own baseline compiler for Windows. It has no register allocator and no optimizer: every local lives in a stack slot. That makes it fast to compile and gives every value an address at every call, which is what a collector needs to find its roots. See The x86-64 back end.
  • WebAssembly, either through LLVM or with Larva's own back end. Both write app.wasm, app.js and app.html. Today only the LLVM path is dependable. See WebAssembly.

For a native program, the object file is linked with the runtime by the system linker (MSVC on Windows) into an .exe or .dll. See Native executables and libraries.

10. The runtime

Compiled code calls a small runtime to allocate, collect, throw, print, use files and sockets, run threads, and do arithmetic on number types the processor lacks. There are four implementations of the same interface: larva_rt.c for native programs, NativeRuntime inside larva's own process, larva_rt.js for WebAssembly, and the VM. They share one object layout and one table of intrinsics, so a program gives the same output wherever it runs. On Windows, the C runtime is compiled once per machine, cached, and linked without the C library. A hello world imports only kernel32.dll and ntdll.dll.

Read: The native runtime, from Four runtimes, one interface to The runtime without a C library.

11. Garbage collection, and domains

Each gc: mode is a set of capabilities. Back ends never ask which mode is in use. They ask whether a capability is present (tracing, moving, barriers, reference counting, regions, manual free), and each capability turns on one kind of generated code. The default collector is generational. New objects are allocated by bumping a pointer in a nursery of 64 KB blocks, survivors are copied out in parallel, and anything a stack points at is pinned rather than moved.

GC domains are how one program mixes modes. A tracing program can call a reference-counted library, and each side keeps its own rules. The domain is not part of the type: one byte of every object's header says which domain owns it. Only static, non-generic functions over numbers, bools and pointers cross between domains. Text and bytes cross as copies, through System.Exchange.

Read: Garbage collection and GC domains.

12. Around the compiler

The same libraries power the rest of the toolchain:

  • Projects. .queen files are read by a parser that never throws: every problem becomes a QN diagnostic. Settings are merged in layers: the command line wins over the project's BUILD chunk, which wins over larva config. larva run compares write times to decide whether to rebuild. See Projects and builds.
  • Hive. Packages are signed zip archives, checked file by file, and builds resolve them from the cache without the network. See Hive internals.
  • Bindings. larva bind keeps what a native library exports apart from how its functions are declared (a .bind description or a C header). The two meet in an ordinary HIR assembly of [Import] methods, with direct C calls and no marshalling layer. See Bindings internals.
  • The language server. One larva lsp serves every installed language. larva keeps the protocol, the projects, the decompiled files and the borrow checker. The language pack answers anything that needs its syntax. See The language server.
  • The installers. larva is compiled ahead of time for each system. System.Core is compiled once, when the installer is built, so an installation ships no source. See The installers.

Read the book

This post only skims each stage. The book describes the code as it is, and each page says where it falls short of the design. The main gaps are collected in What is not implemented yet. The book goes all the way down, with real listings of HIR, IR, IL, LLVM IR and machine code. Start with Architecture and follow the chapters in order. Questions about the design, or ideas for a language of your own on Larva, are welcome on the forum.

All posts · Atom feed · RSS

Esc