- Larva
- Cómo funciona Larva
- Objetos, módulos y enlace
Esta página aún no está traducida al español. Leer la versión en inglés
Objects, modules and linking
Larva IR objects (.lvo) and linked modules (.lvm), the IR linker and how it resolves symbols, link-once code and GC domains, the module splitter and native images, and the larva link, dis, run and native commands.
En esta página
A Larva program is linked at the level of Larva IR: whole-program, before any machine code exists. The linker takes verified modules, resolves their symbols, groups them by GC mode, and hands one merged module to monomorphization and a back end. This page covers the two file forms around it and the commands that work on them.
| Form | Extension | Holds | Made by | Read by |
|---|---|---|---|---|
| Object | .lvo |
One verified IR module, the GC mode it was checked for, declarations and dependencies | ObjectFile.Save |
larva link, larva native, larvaobj: dependencies |
| Module | .lvm |
A linked program lowered to IL, with types, layouts and metadata: everything the VM needs, nothing from the compiler | larva link |
larva run, larva dis |
Libraries are distributed as HIR (.lva inside a .dll, see The .lva format), not as IR. The design document settled this when it moved the toolchain onto HIR: .lva is what ships, and .lvo stays as Larva's internal form of compiled IR. larva build does all of its IR work in memory: it lowers, checks, makes an object, links and generates code without writing a .lvo. No larva command writes a .lvo from source today; objects come from the IR library (Compiler.CompileObject and ObjectFile.Save), and the commands below accept them.
The pipeline
IrModule (lowered)
|
Compiler.CompileObject: type check, region check, borrow check
|
LarvaObject ----ObjectFile.Write----> name.lvo
|
other objects --> Linker.Link (LinkOptions)
|
LinkResult.Merged (one IrModule, global ids, Domains)
|
Monomorphizer (from the roots)
/ \
IlLowering native / wasm back ends
| (LLVM, x64, Wasm)
IlModule --ModuleFile.Write--> name.lvm
LinkOptions.LowerToIl decides whether the linker also lowers to IL. Native and WebAssembly builds turn it off and compile LinkResult.Merged themselves: on a large program, lowering to IL would be most of the link.
.lvo objects
LarvaObject is a verified module with the GC mode it was checked for:
| Member | What it is |
|---|---|
Module |
The IrModule, with Name set: it qualifies its symbols (geom::make). |
Gc |
The GcConfig the module was verified for: mode, capabilities, plugin name. |
Declarations |
Source declarations (path and text, no bodies) a frontend compiles other code against. Empty for a plain object. |
Dependencies |
Modules it imports from, by name (System.lvo for System). |
NativeIds |
When the module was also built as a native library: the ids its code reads from slots (see native images). |
The file (ObjectFile, version 3):
file = "LRVO" version:byte flags:byte payload (flags bit 0: payload is Brotli-compressed)
payload = gcMode:4 capabilities:uv plugin:name module (module: the IR binary form)
count:uv { path:name text:name } (declarations)
count:uv { dependency:name }
[native:uv count:uv { id:name }]
The payload is the nibble stream of the IR binary form. Objects are shipped, so Write compresses the payload with Brotli at its densest setting (quality 11, a 16 MB window) and keeps the result only when it is smaller. Versions 1 and 2 are still read.
xxd geom.lvo
00000000: 4c52 564f 0301 1fd9 0020 c47f fe5a 7fee LRVO..... ...Z..
…
LRVO, version 3, flags 1: compressed. The whole object, five functions and a struct, is 197 bytes.
Two objects
The examples on this page use two small objects. geom exports a struct Point, functions make and norm1, and two generic functions, max<T: numeric> and identity<T>; its IR is printed in full in Larva IR. app imports them and has its own abs and an unused dead:
module app
import struct Point { x: i32, y: i32 } from geom::Point // struct#0
// fn#0
import fn make(_1: i32, _2: i32) -> Point from geom::make;
// fn#1
import fn norm1(_1: &Point) -> i32 from geom::norm1;
// fn#2
import fn max<T: numeric>(_1: T, _2: T) -> T from geom::max;
// fn#3
import fn identity<T>(_1: T) -> T from geom::identity;
// fn#4
fn abs(_1: i32) -> i32 {
let mut _0: i32; // return
bb0: {
_0 = const 1000_i32;
return;
}
}
// fn#5
fn dead() -> i32 {
let mut _0: i32; // return
bb0: {
_0 = const 0_i32;
return;
}
}
// fn#6
fn main() -> i32 {
let mut _0: i32; // return
let _1: Point; // p
let _2: &Point; // r
let _3: i32; // n
let _4: i32; // m
let _5: i64; // w
let _6: i32; // k
let _7: i32; // i
let _8: i32;
bb0: {
_1 = make(const 3_i32, const -4_i32) -> bb1;
}
bb1: {
_2 = &_1;
_3 = norm1(move _2) -> bb2;
}
bb2: {
_4 = max::<i32>(copy _3, const 5_i32) -> bb3;
}
bb3: {
_5 = max::<i64>(const 10_i64, const 20_i64) -> bb4;
}
bb4: {
_6 = abs(const 1_i32) -> bb5;
}
bb5: {
_7 = identity::<i32>(copy _4) -> bb6;
}
bb6: {
_8 = Sub(copy _6, const 1000_i32);
_0 = Add(copy _7, copy _8);
return;
}
}
An import is a declaration: a signature with Linkage.Import, a Symbol naming the export, and no blocks. app was type checked and borrow checked against these declarations alone; the linker then checks the declarations against the definitions, which makes those checks hold for the linked program.
The linker
Linker.Link(objects, options) runs a session in five steps.
1. Modules. Every module needs a name, and no name may appear twice. Each object's GcConfig is matched to a GC domain: domain 0 is the program's GC (LinkOptions.Compilation.Gc); an object checked for another mode joins the domain with the same mode, capabilities and plugin, or opens a new one when MixedDomains allows it. Otherwise it is an error: "module x was checked for GC mode …, but the program uses …".
2. Symbols. Every definition gets a global identity:
| Linkage | Linked name | Visible to other modules |
|---|---|---|
Internal |
module::name |
no |
Export |
module::name |
yes |
LinkOnce |
*::name (or *@N::name) |
yes; one copy kept |
Import |
none: resolved to an export |
Globals get storage only in their defining module. Each struct and function also records the domain of its module.
3. Imports. Each import resolves to the export with its symbol. A missing one is "unresolved symbol"; an internal one is "x exists but is not exported".
4. Rewriting and shapes. Bodies are rewritten from module-local indices to global ids (IdMap; static data of all objects is concatenated, so data indices shift). Then every import is compared with its definition: an imported struct must have the exact fields and generics, an imported function the exact signature: generics with constraints, region parameters, argument and result types with their regions. Whether a function is unsafe is not part of it: that is about its body, not its callers. Every kept link-once item is compared with its other copies the same way.
5. Roots. The entry is module::name, or a bare name exactly one module defines. The roots are the entry, every function with #[reflect(full)], every export when building a library, and every function with a C export name. Monomorphization starts from them, so code nothing reaches is dropped and each generic function is instantiated once per set of type arguments, whichever module asked.
LinkOptions:
| Option | Default | Meaning |
|---|---|---|
Compilation |
defaults | Generics mode, reflection, optimization, and the program's GC (domain 0). |
Entry |
main |
The entry function. |
Library |
false | Keep every export; no entry needed. |
CLibrary |
false | A native library for C: start from the functions with a C name; no entry needed. |
MixedDomains |
false | Let objects of other GC modes in, each as a domain. |
LowerToIl |
true | Also lower to IL (for .lvm and the VM). |
LinkResult carries the IL module (when lowered), the errors, Merged (the whole program after symbol resolution, before monomorphization), Roots and the entry's qualified name.
Link-once code
Some code is written by the compiler itself into every module that needs it: the root object struct, nullable wrappers, boxes. It is marked LinkOnce, and the linker keeps one copy per name, checking that the others have the same shape. With several GC domains a single copy is not always right: a Manual domain's objects are raw pointers, so its Larva.Object is not the same struct as a tracing domain's. OnceKey decides:
*::name, shared, for domain 0, for any domain whose objects have the same shape (every domain with a collector usesgchandles), and for items that are the same in every mode: nogc,box, pointers, borrows or structs in their fields or locals, and no allocation or region in their body;*@N::name, one copy per domain, for the others.
Errors
larva link -o bad.lvm app.lvo
link error: unresolved symbol `geom::Point`
--> app: struct Point
link error: unresolved symbol `geom::make`
--> app: fn make
link error: unresolved symbol `geom::norm1`
--> app: fn norm1
link error: unresolved symbol `geom::max`
--> app: fn max
link error: unresolved symbol `geom::identity`
--> app: fn identity
larva link -o x.lvm --entry abs geom.lvo app.lvo
link error: entry `abs` is ambiguous: geom::abs, app::abs
--> abs
larva link -o geom-only.lvm geom.lvo
link error: no entry function `main` (build a library to link without one)
--> main
Both modules have an abs, internal to each; they never clash, because symbols are qualified by module.
GC domains, as the linker sees them
Each project can choose its own GC (gc: in BUILD). The design document's goal is that projects with the same GC share it, and projects with different GCs still call each other and use each other's objects. Its central decision is that the domain lives in the object, not in the type: a gc T handle is the same type in every domain, and each object's header carries a domain byte. A domain in the type (gc<D> T) was rejected because every signature that receives foreign objects would change, generics would multiply per domain, and a List<T> could not hold objects from two domains.
For the linker this means grouping, not rejecting. Stage 1 of the design made CheckModules assign domains instead of refusing a second GC mode; later stages made code generation follow them. In the merged module:
IrModule.Domainslists theGcConfigof each domain, domain 0 first;IrFunction.DomainandStructDef.Domainindex it. With one domain the list is that one GC and every index is 0, so single-GC programs compile exactly as before.GcOf(domain, program)is a domain's GC. IL lowering and the native back ends compile each function with the GC of its domain: retain and release, write barriers, regions, inline allocation.CodeGcOfadds reference counting to a domain's code when any domain of the program counts, so a counted handle stored or dropped anywhere keeps its count.DomainByteis the header byte for objects a domain allocates: the domain number in 6 bits, plus0x40(UncountedDomain) when that domain does not count references, so retain and release skip its objects at run time.TracesandAny(capability)ask whether a capability is in play anywhere: a function of a counting domain still records stack roots when another domain traces, because the collector walks every frame.- After linking,
RegionChecker.CheckMixedrejects passing a value that carries an arena region to a function of another domain.
larva build links a dependency whose gc: differs as its own domain: it compiles that project separately (its own HIR link with System.Core, lowered with its GC and every member exported), makes an object for its GC, and links it into the program with MixedDomains on. The program reaches it through declarations of its static methods whose signatures carry no handles (numbers, bools, pointers to them). Here App (the default tracing GC) uses a library Counting whose project says gc: refcount:
larva build --timings
…
timings: System.Core (compiled library) 72 ms, Counting (RefCount) 4040 ms, frontend 334 ms, HIR link 29 ms, lowering 105 ms, IR checks 171 ms, IR link 258 ms, native (x64) 1435 ms; total 6467 ms
bin\App.exe
5050
larva link links objects for one GC only: the program's GC is the first object's, and objects checked for another mode are refused. Run-time behavior of domains (headers, cross-domain references, collectors per domain) is in GC domains.
.lvm modules
larva link lowers the linked program to IL and saves it as a .lvm (ModuleFile, version 1), the VM's executable form:
file = "LRVA" version:byte body
body = gc entry:sv types fields constants witnesses functions metadata data globals (nibble stream)
gc = mode:4 capabilities:uv plugin:name
types = n:uv type* fields = n:uv {owner:type field:uv}*
constants = n:uv {type bits:sv}* witnesses = n:uv {type:uv layout}*
layout = size:uv align:uv flags:4 gc:uv offset:uv*
function = name args:uv witnesses:uv argTypes localTypes roots safepoints handlers body:bytes
roots = n:uv {isArg:4 slot:uv hasOffsets:4 [offsets]}*
safepoint = op:uv roots handler = start:uv end:uv hasType:4 [type:uv] target:uv
metadata = bytes (reflection) bytes = n:uv byte*
Types are written in the IR's type encoding. Each function carries its GC roots, its safepoints (with the roots live at each) and its exception handlers next to its IL. A module is not compressed: it is read once, by the program that runs it. The IL itself is described in IL and the VM.
larva link -o prog.lvm geom.lvo app.lvo
linked 2 object(s) into prog.lvm (8 functions)
larva run prog.lvm
7
Eight functions: app::main, geom::make, geom::norm1, app::abs, geom::abs, the shared geom::identity, and geom::max twice, specialized for i32 and i64 (it compares, so it cannot be shared). app::dead is gone: nothing reaches it. main returns 3 + 4 from norm1, max keeps 7, and abs(1) - 1000 adds 0.
With --library, every export is a root and no entry is needed. Generic exports are not roots on their own: they are instantiated by whoever uses them.
larva link -o lib.lvm --library geom.lvo
linked 1 object(s) into lib.lvm (3 functions)
larva dis
larva dis prints a module: its GC, entry, type and field tables, witnesses, reflection metadata, and every function's IL with its safepoints.
larva dis prog.lvm
.gc Default (Tracing, Moving, Barriers)
.entry fn.0 (app::main)
.type type.0 = i32
.type type.1 = struct#0
.type type.2 = &struct#0
.type type.3 = i64
.type type.4 = bool
.type type.5 = T0
.field fld.0 = struct#0.0
.field fld.1 = struct#0.1
.witness type.0 = { size 4, align 4, copy|unmanaged|fixedsize, gc [] }
.reflect struct#0 geom::Point (layout) { x @0: i32, y @4: i32 } [size 8, align 4, copy|unmanaged|fixedsize, gc []]
.function app::main (args: 0, locals: [type.0, type.1, type.2, type.0, type.0, type.3, type.0, type.0, type.0])
.safepoint #2: [] ; call fn.1
.safepoint #5: [] ; call fn.2
.safepoint #7: [] ; call fn.3
.safepoint #11: [] ; call fn.4
.safepoint #14: [] ; call fn.5
.safepoint #18: [] ; call fn.6
app::main: ; 28 ops · 67 nibbles · 34 bytes
0002 36 ldc 3
0004 37 ldc -4
0006 71 call fn.1
0008 11 stloc loc.1
000A F861 stk.ldloca loc.1
000E 72 call fn.2
…
.function geom::identity (witnesses: 1, args: 1, locals: [type.5])
.gcroots loc.0 [witness], arg.0 [witness]
geom::identity: ; 2 ops · 4 nibbles · 2 bytes
0001 20 ldarg arg.0
0003 8 ret
…
geom::identity is the shared instance: one body for every T, taking a witness (the type's size, alignment and pointer map) as a hidden argument, so its roots are described by that witness.
larva run
larva run <module.lvm> [--gc-stress] [args...] runs the entry function on the VM and prints its result, if it has one. Arguments are parsed as integers, floats or true/false. --gc-stress makes the collector run far more often, which turns memory bugs into crashes close to their cause. ```shell
$ larva run prog.lvm --gc-stress
7
With `LARVA_STATS=1` in the environment, the VM also prints its counters to standard error:
```shell
$ larva run prog.lvm
7
steps 87, calls 9, safepoints 8, collections 0 (reclaimed 0), retain/release 0/0, barriers 0, pins 0
larva native
larva native links objects and compiles them to native code without going through IL:
| Option | Default | Meaning |
|---|---|---|
-o <out> |
required | The output file. |
--exe |
off | A whole executable: the program, entry wrappers and the C runtime, linked with the system linker. |
--backend llvm|x64 |
llvm |
LLVM, or Larva's own x86-64 back end (objects only, no --emit). |
--emit obj|asm|ll |
obj |
An object file, assembly, or LLVM IR. |
-O0 … -O3 |
-O2 |
LLVM's optimization level. |
--entry <name> |
main |
The entry function. |
--library |
off | Link as a library (every export kept). |
--no-bce |
off | Keep every bounds check (no bounds-check elimination). |
--polly, --polly-opt, --polly-plugin |
off | Run LLVM's Polly loop optimizer through an external opt. |
larva native -o prog.ll --emit ll geom.lvo app.lvo
compiled 2 object(s) to prog.ll (ll, x86_64-pc-windows-msvc, O2; bounds checks: 0 kept, 0 eliminated)
larva native -o prog.obj geom.lvo app.lvo
compiled 2 object(s) to prog.obj (obj, x86_64-pc-windows-msvc, O2; bounds checks: 0 kept, 0 eliminated)
larva native -o prog.obj --backend x64 geom.lvo app.lvo
compiled 2 object(s) to prog.obj (obj, direct x86-64 back end, 8 functions; bounds checks: 0 kept, 0 eliminated)
larva native -o prog.exe --exe geom.lvo app.lvo
linked 2 object(s) into prog.exe (llvm back end, O2, entry app::main)
prog.exe
7
larva native -o prog2.exe --exe --backend x64 geom.lvo app.lvo
linked 2 object(s) into prog2.exe (x64 back end, entry app::main)
prog2.exe
7
Symbols keep their linked names (app::main), so LLVM IR is easy to read. LLVM inlined everything into main:
define noundef i32 @"app::main"() local_unnamed_addr #0 personality ptr @__C_specific_handler {
entry:
ret i32 7
}
; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(none)
define %"geom::Point" @"geom::make"(i32 %0, i32 %1) local_unnamed_addr #0 {
entry:
%result.fca.0.insert = insertvalue %"geom::Point" poison, i32 %0, 0
%result.fca.1.insert = insertvalue %"geom::Point" %result.fca.0.insert, i32 %1, 1
ret %"geom::Point" %result.fca.1.insert
}
; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read)
define i32 @"geom::norm1"(ptr noalias readonly captures(none) %0) local_unnamed_addr #1 {
entry:
%1 = load i32, ptr %0, align 4
%common.ret.op.i = tail call range(i32 0, -2147483647) i32 @llvm.abs.i32(i32 %1, i1 false)
%2 = getelementptr inbounds nuw i8, ptr %0, i64 4
%3 = load i32, ptr %2, align 4
%common.ret.op.i2 = tail call range(i32 0, -2147483647) i32 @llvm.abs.i32(i32 %3, i1 false)
%4 = add i32 %common.ret.op.i2, %common.ret.op.i
ret i32 %4
}
The executable printed its entry's i32 result; see the known gap on exit codes in Native builds.
Native symbols and the entry
In object files, functions get a larva. prefix so they cannot clash with C symbols (NativeSymbols). A mangled function (private members), an operator or an overload (whose names carry parameter types) is named larva.$ and 16 hex digits of a 64-bit FNV-1a hash of its linked name instead: stable between builds, short, and valid in an export list. Names that start with larva_ belong to the runtime and stay as they are; a function with a C export name keeps that name.
An executable's main is in the C runtime, which calls wrappers that ExecutableEntry adds to the program: larva_entry (up to four integer arguments, converted to the entry's parameter types, and the result widened to i64), larva_entry_f64 for float results, and larva_entry_info, which packs the arity, the kind of result, whether the entry reads its own command line, and two GC facts: whether anything traces (if not, the runtime makes no young objects) and whether some code counts references.
The module splitter
ModuleSplitter shapes a module for distribution:
Trimkeeps only what given roots reach (structs, functions, globals and static data, found by walking each item through anIdMapthat records every id it meets) and renumbers the rest in order. A module's own implementations of requirements stay: generic code linked later may need them.TrimLibrarytrims a library to what its exported and internal items reach: link-once copies and imports nobody uses go away.Partitionsplits a module into one module per owner (a namespace): each defines and exports its own items, imports what it uses from the others asowner::name, carries the link-once items it needs, and is trimmed.Declarationreduces a function to its signature, for another image to call.
Only Declaration is used by the toolchain today (by native images, below); trimming and partitioning are not wired to any larva command.
Native images
NativeImages cuts a linked program into native images: one DLL per library module (a namespace) and an executable that loads them. Every image is compiled from the whole linked program, but:
- non-generic functions of other images are only declared, and called by symbol;
- generic code is instantiated by each image that uses it, like C++ templates;
- another image's globals are read and written through accessors it exports (
G.$get,G.$set); - a library cannot number types and classes, since only the program sees them all, so its
typeidandclassidread slots (IdSlots). The library exports$ids.set, and its object records a manifest of what each slot holds (NativeIds, as portable type names,TypeNames). The executable's$native.initfills every library's slots at start-up, before the entry; - exception types get ids that are a hash of their name, the same in every image, so an exception thrown in one DLL is caught by type in another.
Not used by a larva command yet: the per-namespace DLL build (NativeLibraryBuilder in Larva.Driver, and its POSIX counterpart) exists and is tested, but larva build --dll builds a single library instead. See Native builds.
Editar esta página en GitHub Reportar un problema