Repository navigation
Roadmap
Lamina is a compiler backend for a small, statically typed IR. It lowers that IR to
a machine-level IR, optimizes it, and writes native assembly, with no LLVM and no
Cranelift underneath. ras assembles the result and weld links it, so the toolchain
can build a program end to end on its own.
This page is the plan. It says what works, what does not, and what each milestone has
to prove. For how the pieces fit together see Architecture, for what the
version numbers and nightly mean see Releases, and for what is already
built on Lamina see Ecosystem.
Contents
| Component | Role |
|---|---|
| lamina | IR, MIR, optimizer, code generation, compiler driver |
| ras | Assembler. The default, with GNU as as the fallback |
| weld | Linker. The default, with GNU ld as the fallback |
| lamina-c | C bindings. Not published. lia_version() returns the crate version |
| lamina-web | Website and documentation |
Each component versions independently. The current numbers are the latest tagged releases on those repositories; see Releases.
A typed IR with a text form and an IRBuilder for constructing modules from Rust.
Integers in both signednesses at 8, 16, 32 and 64 bits. f32 and f64 with
conversions to and from the integers. Pointers, arrays, structs, globals that start
from a string or an element list, switch, the memory intrinsics, atomics including
atomic_cmpxchg, function addresses and calls through them, and @extern declarations
for functions the module does not define. @extern calls into libc on Linux.
A separate machine-level IR keeps target concerns out of the layer frontends see. The
optimizer runs from --opt-level 0 to --opt-level 3. Assembly output exists for the targets in the table
below. The default toolchain is ras and weld, with GNU as and ld as the
fallback. The -g flag writes a line table into the object file. C bindings give
explicit ownership of every allocation. --jit runs a program in a default build. The
C bindings' JIT extension is nightly.
Vectors are in the surface IR: <N x T> as an instruction's type suffix, with the
element-wise arithmetic reusing the scalar opcodes, uniform and per-lane shifts,
splat, extractlane, insertlane, shuffle, and load and store with a vector
suffix. 128-bit vectors lower on x86_64, AArch64 and WebAssembly in a default build.
This is not behind nightly; the auto-vectorizer that finds them for you is.
Thread-local storage works on ELF (local-exec and initial-exec), on Windows x86_64 through the TEB TLS array and _tls_index, and on macOS AArch64 through Mach-O TLV descriptors. Inline assembly lowers on x86_64 (Linux and Windows), AArch64 (Linux and macOS) and s390x. Variadic functions work on System V x86_64, Win64 and AArch64 on Darwin. @section placement works on ELF, COFF and Mach-O. On Mach-O only .text.<name> maps, to __TEXT,__<name>, and the name is limited to 14 characters of [A-Za-z0-9_]. Anything else is refused.
Every property of an architecture is stated once, in one block of one table: pointer width, register width, byte order, stack alignment, argument alignment, interpreter paths, toolchain triples, accepted environments and sub-architecture axis. Adding an architecture is one block and adding a property is one field, and the supported-target list is derived from the same table rather than maintained beside it.
A target is an architecture, an operating system, a sub-architecture and an
environment: <arch>_<os>[_<env>], where the environment is gnu, musl,
lamina, eabi, eabihf or none. The C library, the ELF interpreter, the C runtime
startup objects and the compiler runtime all come from that axis, so s390x_linux
declares glibc rather than inheriting it from whichever cross compiler the build machine
has. A combination an architecture does not accept is refused by name, with the accepted
set in the hint.
lamina is the exception on that list: it names Lamina's own C library, which is not
written. It parses on x86_64 and aarch64 and the link refuses it by name, saying there
is no Lamina libc, no startup objects and no loader, and naming the environments that
do link. It is a declared target, not a feature. It stays that way until the libc is
written, which comes after camina.
Lamina ships its own compiler runtime, and it is now the whole of one. builtins/
holds the 128-bit division, remainder and float-conversion routines and the 64-bit
forms of the same set, written in Lamina IR, compiled per program for the requested
target and linked ahead of -lc. Nothing on any target needs the host libgcc: a
128-bit program on x86_64 and an i64 division on i686 both link against routines the
driver built. Where the host does have a libgcc, the GNU link path still names it
after the compiled objects, so a symbol neither side covers still resolves. The
float-to-integer routines define what libgcc leaves undefined: NaN gives zero and an
out-of-range value saturates, which is what the scalar conversion contract already says
everywhere else.
What the IR means is written down separately from how it is spelled. IR semantics is the normative page: numbered clauses for values, arithmetic, traps, floating point, memory, atomics, I/O, vectors and the target-dependent facts, each saying what an operation computes and which targets refuse it. A clause marked unverified was read out of the implementation rather than run.
A backend either implements an operation or refuses it by name. Computing something
else is the one outcome that is never acceptable, and a run of changes closed the cases
where it still happened. rv64i_linux emitted fadd.d on an ISA with no
floating-point unit and now refuses, naming the extension. i686 answered −128 for
div.i8 -128, -1 and now traps, as every other backend does. A loop that wrote its
induction variable twice was widened by the vectorizer as though the stride were one
element. atomic_cmpxchg checked its failure ordering on some paths and now checks it
on all of them, and an atomic access whose address can be shown to be under-aligned is
refused rather than lowered. Cortex-M refuses an access that declares less than the
natural alignment of its type.
Several things a module can write are now errors instead of quietly meaning something
else. inttoptr's suffix names the integer the pointer is built from, and
inttoptr.ptr, inttoptr.f64 and inttoptr.bool no longer parse. A shift by a
constant count outside the operand's width is a compile error; a count computed at run
time is still masked. @extern on a function that has a body is rejected. A removed
opcode names its replacement in assignment form as well as in statement form.
On x86_64, the same input produces byte-identical assembly across runs.
The benchmark programs in benchmarks/ have a runner that compares them against C
(through clang), Rust, Go and Zig builds.
- 128-bit globals are rejected.
- A vector type may not appear in a declaration: not a parameter, a return type, a
global, a struct field or a
stackalloc. No backend passes one across a call. - 256-bit vectors parse and validate and no backend lowers them.
- The RVV lowering is reachable now —
rv64gcv_linuxturns V on — but nothing has assembled or run its output. - Aggregates passed or returned by value depend on the target. See 0.1.
- 64-bit integers are not lowered on every 32-bit target. wasm32 rejects
i64parameters and returns.riscv32_unknownrejects 64-bit division and float-to-integer conversion: it is freestanding, and a freestanding target links no compiler runtime, so the routine the operation needs is refused at compile time by name rather than left to the linker. The routine exists; nothing puts it on the link line. - The
muslenvironment is declared and not demonstrated. The GNU link path refuses without a musl cross compiler, and theweldpath writes its own interpreter, so a_muslbuild on a glibc host carries the glibc interpreter. No musl binary has been built and run. - armv7 returns a 5-to-8-byte composite in registers. The AAPCS says memory.
- i686 line numbers come from an instruction counter, not the source.
-
-gline tables are dropped when the defaultweldlinks the program.-c gnukeeps them. - The Windows x86_64 path has one open cause of its old bug: no CRT is requested for Windows links. See 0.1.
- Thread-local storage is refused on AArch64 Linux and AArch64 Windows.
- Variadic definitions are still refused on AArch64 Linux, which uses AAPCS64.
- Inline assembly is refused on every target not named under What works.
- A struct of up to four floats (an HFA) goes in
x0-x7on AArch64 instead ofv0-v7. That is consistent for Lamina-to-Lamina calls and disagrees with C. -
weldhas no per-target library search paths, so--target freebsdstill searches Linux paths.
The driver accepts eighteen architectures in a default build and all eighteen emit
code. arm32 was the nineteenth. It is now refused where the target string is parsed,
with a hint naming armv7, so it is no longer an advertised target. Three more — arx64,
riscv128 and lisa — parse only behind the nightly Cargo feature, are refused by
the driver in a default build, and are not available targets. arx64 in particular is
an experimental architecture of this project's own with no silicon and no assembler
behind it; it is not a chip anyone can target.
The table says what the driver accepts and how far each target gets. Run means the output was executed and checked against an expected result. Assembled means a real assembler accepted every instruction, which catches a malformed operand or relocation and nothing more. Compiled means assembly text was produced and read. Run under emulation means the output was linked against a cross C library, executed under QEMU user-mode and checked against an expected result — a real run of the generated code, but not a run on that machine.
| Target | Status | How far it gets | Notes |
|---|---|---|---|
x86_64_linux |
Tested | Run | The reference path, and the only one that goes from .lamina to a running binary without a tool Lamina did not write |
x86_64_windows |
Experimental | Run | Win64. The testcase suite passes on windows-latest. Variadic definitions, thread-local storage and inline assembly lower. The CRT item in 0.1 is still open |
x86_64_freebsd |
Experimental | Compiled | ELF. The other BSDs are refused at the parser with a hint naming this one |
aarch64_macos |
Partial | Run | Apple Silicon, AAPCS64. The testcase suite passes on macos-latest (arm64). Thread-local storage through Mach-O TLV descriptors, variadic definitions and inline assembly lower. Keep production use conservative |
aarch64_linux |
Experimental | Assembled | Lowers inline assembly and everything except a variadic definition and thread-local storage. weld cannot link it, so the link falls back to GNU ld
|
aarch64_windows |
Experimental | Compiled | Refused at assembly: neither ras nor GNU as writes Windows ARM64 objects |
riscv64_unknown |
Experimental | Assembled | LP64D. The one cross target weld links by itself, for a program that needs no C library symbol. rv64gcv turns on the V extension and reaches the RVV lowering |
riscv32_linux |
Experimental | Assembled | 64-bit integers legalized into register pairs. ras writes an ELF32 object for it. Nothing links it: weld does not take riscv32 and no cross ld is installed |
riscv32_unknown |
Experimental | Compiled | The same backend, and no C library, so it refuses the division helpers rather than emit a call that cannot link. ras writes objects for Linux and the BSDs and not for unknown, so this spelling stops at the assembler |
ppc64le_linux |
Experimental | Assembled | Little-endian ELFv2, assembled by ras, which has a PowerPC encoder |
ppc64_linux |
Experimental | Compiled | Big-endian ELFv2, same backend. A separate architecture to ras, which has no encoder for it, so it needs GNU as. ppc64-elfv1 is refused: ELFv1 is a different ABI, not a byte order |
s390x_linux |
Experimental | Run under emulation | 64-bit z/Architecture, big-endian, and the only cross target whose cross C library is installed here. Atomics, bit intrinsics, rotates, variadic definitions, checked arithmetic, TLS and inline assembly all lower; vectors are refused, because nothing implements the z13 vector facility. No register allocator; every virtual register lives in a frame slot. Needs z196, because the bit intrinsics use popcnt. Its va_list is the 32-byte block the s390x ELF ABI defines, so it crosses into C |
mips64_linux, mips64el_linux
|
Experimental | Compiled | N64, both byte orders, one backend. Scalars, aggregates by value, indirect and variadic calls; atomics, bit intrinsics, rotates, vectors, inline assembly and variadic definitions refused. O32 is not implemented and mips is refused with a hint |
loongarch64_linux |
Experimental | Compiled | Nothing here assembles LoongArch, so nothing catches a mistake in the text |
thumbv6m_*, thumbv7m_*
|
Experimental | Compiled | Cortex-M, bare-metal ELF, Thumb only. No floating point, no 64-bit integers, no atomics, no vectors. Both refuse an access that declares less than the natural alignment of its type: ARMv6-M faults on one outright, and ARMv7-M faults on one whenever CCR.UNALIGN_TRP is set, which this backend emits no startup code to clear. ARMv6-M also refuses division |
armv7_linux |
Experimental | Compiled | AAPCS-VFP hard float. Composite return values of 5 to 8 bytes are wrong, see above |
i686_linux |
Experimental | Run | cdecl with x87. Returns a 64-bit integer in %edx:%eax and reaches the 64-bit division and float-conversion routines through the compiler runtime. Refuses atomics and vectors. Traps div.i8 -128, -1 and the other narrow signed overflows, as every other backend does |
wasm32_*, wasm64_*
|
Experimental | Compiled | WebAssembly text. Assembling it needs wat2wasm
|
nvptx64_cuda, amdgcn_amdhsa
|
Experimental | Compiled | GPU text, never linked. A module needs a @kernel function |
arm32_* |
Rejected on purpose | — | Refused where the target string is parsed. arm32 names no ARM revision, floating-point ABI or profile, so there is no machine to generate code for; the hint names armv7, thumbv6m and thumbv7m
|
*_artery |
Planned | — | The OS name parses behind nightly and the driver accepts no target using it. See Artery target
|
The driver accepts more identifiers than this table lists. Recognition is not support: parsing, code generation, assembly, and linking are separate stages, and a recognized name promises nothing about the ones after it.
Tested here has a specific meaning. It means the output is compiled, run and
checked against an expected result on a machine of that architecture. It is not a
judgement about how good the target is. Most cross targets are not run, and the reason
is their missing C libraries rather than a missing emulator — QEMU user-mode is
installed for all of them. s390x is the one cross target whose cross C library is
installed, so it is the one cross target that gets a checked run. The Windows and macOS rows
were checked on windows-latest and macos-latest.
A milestone is a set of outcomes. Versions give the ordering, not a schedule.
| Theme | The one thing it has to prove | |
|---|---|---|
| 0.1 | Correct on the targets we claim | Every target labeled Tested has its output run and checked on a machine of that architecture |
| 0.2 | A backend you can add a target to | Adding an architecture touches one module and a registration line |
| 0.3 | Depth | A regression in generated code fails a check instead of going unnoticed |
| 0.4 | Freestanding targets | A program runs with no OS under it and no libc in it |
| 1.0 | Stable IR and ABI | The IR and the C ABI can be depended on |
Status column below: ○ not started · ◐ in progress · ● done.
0.1 is in progress. Its toolchain default is done and the Linux suite runs. 0.2 and 0.3 have a few items started. 0.4 and 1.0 are not started.
Right now the target table makes claims nothing verifies. This milestone closes that gap and makes the self-built toolchain the default path.
Correctness
| Item | Detail | |
|---|---|---|
| ◐ | Fix Windows output | Three causes, below. The first is fixed, and the second has run: the testcase suite passes on windows-latest. The third is open |
| ◐ | Valid RV64G from the RISC-V backend | Every recorded program assembles under clang --target=riscv64 -march=rv64g, and the argument registers match what clang picks for the same signature. What is left is execution: nothing has run an RV64 binary, so the ABI is checked at the call boundary only. #21
|
| ◐ | Deterministic output on every backend | Fixed on x86_64 by ordering register sets, and checked: three compiles of one program gave identical assembly. The other backends need the same audit, because hash-ordered emission is invisible to a test that only compares program output |
The Windows bug (#20) has three causes:
-
Fixed: the Windows arm routes the byte throughwritebyteemits a Linux syscall.putchar, andwriteandreadhave Windows arms of their own. Every BSD, Redox and later Artery still land on the Linux fallback. -
The PE emitter imports everything fromkernel32.dll.weldno longer guessesmsvcrt.dll. Since weld 0.2.1, an undefined symbol on PE is a link error. The testcase suite passes onwindows-latest. -
No CRT is requested for Windows links. The Windows arm of
build_weld_linker_argsinsrc/mir_codegen/link.rsis still empty, while Linux gets-lcand macOS gets-lSystem. Open.
Language
| Item | Detail | |
|---|---|---|
| ◐ | Aggregates across a function boundary | x86_64 passes an integer aggregate of any size and returns one, in registers up to 16 bytes and through memory or the hidden pointer above that, checked against clang in both directions. aarch64 passes by reference above 16 bytes but cannot yet return that size. riscv, ppc64, s390x, mips64, armv7 and Cortex-M classify one. loongarch64, wasm and the gpu backends reject an aggregate parameter by name rather than guess an ABI. Float and mixed aggregates are not classified anywhere |
| ◐ | External function declarations |
@extern on a signature with no body declares a function the module does not define, and calls to it use the platform ABI. The puts example in Ecosystem runs. What is left is the attributes that go with it. #7
|
This one unblocks more of the rest than anything else here. It lets the I/O builtins stop being per-backend intrinsics with a Linux-shaped fallback, it is what WebAssembly needs before it can do anything observable, and it is how an Artery program will reach the kernel. See where the I/O builtins go.
Toolchain
| Item | Detail | |
|---|---|---|
| ◐ | Move driver work out of weld
|
The driver computes the startup objects, the libraries and the interpreter for its own GNU-style path, from the target rather than from the host, and a cross link that cannot find the target's gcc is an error naming it instead of a binary that runs wrong. On the default weld path weld still supplies CRT objects and still invents an interpreter path, including the hardcoded /lib64/ld-linux-x86-64.so.2. The Lamina driver supplies crt1.o, -lc, --dynamic-linker, and the search paths, derived from the target, the way gcc does before invoking ld. See why
|
| ● |
ras and weld as the default on x86_64 Linux |
The default in lamina --help. If either is missing, the driver falls back to GNU as and ld with a warning |
| ◐ | Split the toolchain flags |
--assembler and --compiler exist. --linker does not, so the separate choice is not there yet. --compiler keeps the toolchain aliases (gnu, clang, msvc, lamina). A named tool beats an alias, an alias beats detection, and an unknown value is an error rather than a silent fallback. Rename AssemblerBackend::Lld, which actually picks clang's integrated assembler, to Clang
|
Testing. See Testing for what the tier numbers mean.
| Item | Detail | |
|---|---|---|
| ● | Run the testcase suite on three platforms (T1) | Every recorded program compiles, runs and produces the expected output on Linux, Windows and macOS |
| ○ | Determinism check (T4) | Compile twice, compare bytes |
Release and docs
| Item | Detail | |
|---|---|---|
| ◐ | First C bindings release | The source layer is done: owned builder, opaque handles, thread-local errors, panic containment, AOT entry points, both headers, nightly JIT, 16 smoke tests. What is missing is publication: archives with checksums, a rebuild of the C examples so exports and headers cannot drift, and the install page. The SDK is built for one target, x86_64 Linux |
| ○ | One story everywhere | The README sells x86_64, AArch64, RISC-V, and WebAssembly as flat capabilities. The website separates tested from experimental. The website is right. The nightly line is still inconsistent: --jit runs in a default build while the docs call the JIT nightly-only |
Header auto-generation stays blocked upstream. cbindgen does not understand
#[unsafe(no_mangle)] under the Rust 2024 edition, so include/lamina.h stays
hand-maintained, and nothing yet rebuilds the C examples to catch drift between them and the exports.
Structure. Adding an architecture should mean writing one module, not editing a dozen
match statements.
| Item | Detail | |
|---|---|---|
| ● | One table per architecture | Every property an architecture states — pointer width, register width, byte order, stack and argument alignment, output artifact, interpreter paths per environment, toolchain triples, accepted environments, Windows library directory, sub-architecture axis — lives in one block. A block missing a field does not compile, the generated lookup has no wildcard arm, and both the architecture list and the supported-target list come from the same table. The remaining per-target match statements are in the emitters, not in the properties |
| ○ | A codegen trait | One interface for what a backend can do: assembly output, binary output, file extension, supported options. #14 |
| ○ | Calling conventions described in MIR | Conventions vary enough between targets to belong in the machine IR as data rather than in per-backend code. WebAssembly differs again. #15 |
| ○ | AArch64 binary emission | The assembly path works. A direct binary path to a linked Linux executable does not, since weld cannot link AArch64 Linux yet |
| ○ | Promote AArch64 |
aarch64_linux to tested and aarch64_macos from partial to tested, once their output is run and checked rather than read |
| ◐ |
ld command-line parity in weld
|
Accepted: -static, -shared, -r, --gc-sections, --rpath, --start-group, --strip-all, --build-id, --Map, -z, and response files. -e is honoured for the entry point and -h is the soname option, as in ld. --target names the target operating system and an unknown value is an error listing the accepted set, rather than a silent default. -static fails when the program needs glibc, because glibc's IFUNC resolvers are not supported. -shared writes an ET_DYN file that nothing has loaded. Missing: -T, --whole-archive, --defsym, -u, --wrap. -T and -static come first, since 0.4 depends on them |
| ● | Archive support in weld
|
Members of a .a are pulled in on demand when named with -l or given as a file, including cycles inside --start-group/--end-group. Lookup uses .a files exclusively |
| ◐ | Hostile input in weld
|
Symbol and relocation reads in the object readers are bounds-checked, the Mach-O symbol table and the ELF RELA stride are validated against the section they came from, and a reservation derived from a header field cannot be unbounded. A malformed object is a link error rather than a panic |
| ◐ | Mach-O objects from ras
|
ras writes x86_64 and arm64 Mach-O objects, and the Lamina driver uses them for x86_64_macos and aarch64_macos. The aarch64_macos ones run on macos-latest. Nothing has run an x86_64_macos one. ELF carries every encoded architecture, COFF/PE is x86_64 only |
| ◐ | Architectures ras encodes |
x86_64, aarch64, riscv32, riscv64 and little-endian ppc64le, with riscv32 writing ELF32 and the rest ELF64. Big-endian ppc64, s390x, mips64, LoongArch64, armv7 and Cortex-M have no encoder and fall back to GNU as
|
| ○ | Relocation parity in weld
|
Even coverage across ELF, Mach-O, and PE instead of the current ELF-first depth, and a PE path with a real import table |
| ● | Support predicates in ras that match the writers |
The object-support predicate now names what each writer does: ELF for every encoded architecture, Mach-O for x86_64 and aarch64, COFF for x86_64 alone. A combination outside that is refused by name instead of advertised |
| ○ | Differential testing (T2, T3) | Assemble with ras and with as, link with weld and with ld, compare. The bar is a program that behaves the same way, not one built from identical bytes |
| ○ | Cross-compilation smoke tests (T6) | Emit assembly for every triple the driver accepts. Unexercised targets are where the panics hide, and the codegen trait makes them cheap to enumerate |
0.1 is about being correct and 0.2 is about being extensible. This one is about giving someone a reason to pick Lamina over the alternatives.
| Item | Detail | |
|---|---|---|
| ◐ | Register allocation everywhere |
--regalloc takes incremental, linear or graph. The default follows what a backend implements rather than what it is called: a backend with a global allocation pass takes linear at --opt-level 1 and above, and one without takes incremental whatever the level. x86_64, aarch64, riscv32, riscv64, ppc64le and ppc64 have the pass; the rest do not. Bringing the remaining backends up and measuring what the choices buy is what is left |
| ◐ | Debug information |
-g writes .file and .loc into the assembly, and ras writes a line table into the object. The line table is correct. The default weld link drops it, so the executable has no source lines. -c gnu keeps it. Only line tables exist. No variable or type information. i686 still gets line numbers from an instruction counter |
| ◐ | Vectorization and scheduling with results | Vectors are in the surface IR and lower on x86_64, AArch64 and WebAssembly in a default build, so a frontend can emit them today. The auto-vectorizer is a separate thing: nightly only, x86_64 only, --opt-level 3 only, and it widens one loop shape that the alias analysis can prove safe. Treat it as unproven. It refuses a loop it cannot show is safe at a list of guards, several of which no test exercises, and one of them was wrong until recently: a loop writing its induction variable a second time was widened as though the stride were one element. It stays experimental until the guards are covered and it shows measured wins |
| ○ | Optimization-level equivalence (T5) | Every program must produce identical output at --opt-level 0 through --opt-level 3. The cheapest check on the list, and it catches a pass that changes behavior instead of speed |
| ○ | Emulated execution (T7) | AArch64, RISC-V 64 and PowerPC64 under QEMU as part of the checked suite rather than by hand, and the rest once their cross C libraries are in place |
| ○ | Performance tracked per commit (T9) | Compile time, generated-code speed, binary size |
| ○ | Internal cleanups | Transform metadata deduplication, splitting the modules that outgrew their file, and an audit of the sign-loss casts in codegen offset arithmetic. Some of those are probably latent bugs rather than style problems |
Compiling for an environment the backend does not already assume. No libc, no C runtime
startup objects, no host to fall back on. Freestanding is a declared property now — the
none environment — rather than a naming convention. What is left is the link and the
code generation it implies. Artery is the driving case.
| Item | Detail | |
|---|---|---|
| ◐ | Hosted and freestanding as a target property | Done at the model layer: freestanding is the none environment, it selects no libc, no startup objects and no compiler runtime, and a module that needs a runtime helper is refused at compile time naming the symbol. An operating system that supplies its own runtime declares it, so Darwin, Windows, WASI and the GPU targets take none by default. What is left is the link: the default weld path still supplies an interpreter of its own |
| ○ | Soft-float code generation and ABI | For targets with the vector units switched off entirely, no SSE or AVX on x86_64 and no NEON on AArch64. Floating point lowers to a support layer and the ABI passes floats in integer registers |
| ○ | No red zone | A backend option, honored everywhere, forbidding use of memory below the stack pointer. The @no_red_zone annotation was removed, since no backend read it; the x86_64 backend never uses the red zone and the other targets have none. Anything with interrupt handlers needs the real option |
| ○ | Strict alignment | No unaligned loads or stores, for targets that trap on them |
| ○ | No unwinding | Under abort-on-panic there are no unwind tables to produce, and none should be required by the emitted code or the link |
| ○ | Linker scripts via -T
|
Section placement, load addresses, and the entry symbol from a script instead of built-in defaults. Same -T work as 0.2, and the reason it is not optional |
A compatibility promise, and one that should not be made early.
| Item | |
|---|---|
| ○ | The IR text form and IRBuilder are stable, with a documented versioning and deprecation policy |
| ○ | The C ABI is stable and versioned independently of the crate |
| ○ | Every tested target has an end-to-end path through code generation, assembly, and linking with no system toolchain required |
| ○ | A correctness suite, differential testing against an established toolchain, and fuzzing that the parser and validator survive (T8) |
| ○ | Debugger integration on more than one platform |
| ○ | A documented bootstrap story |
This cuts across every milestone, so it lives here rather than inside one.
Unit tests and the testcase suite run on Linux, Windows and macOS: every recorded program is compiled, run and its output diffed against the expected one. Codegen metrics are compared against a committed baseline, so a change in instruction, spill, call, branch or frame counts is reported rather than noticed later.
In 0.2.0 the suite passes all 176 cases on ubuntu-latest, windows-latest and macos-latest. The macOS runner is arm64. Before that, macOS passed 10 of 176 and Windows 171.
What still only runs by hand matters more: the differential fuzzers that compare
generated programs against clang, the cross-backend assemble sweep, and the allocator
and optimization-level matrices. They are what found most of the miscompilations fixed
so far, so they belong in T3 and T5 rather than in a shell history.
A target may be labeled Tested only if its output is compiled, run and checked against an expected result on a machine of that architecture. No exceptions. Reading assembly catches a malformed instruction; it does not catch a wrong answer.
Each is cheap once the one before it exists, and each catches something the earlier ones cannot.
| Tier | What it does | What it catches | Milestone |
|---|---|---|---|
| T0 | Unit tests across the workspace | Logic errors inside a function | Have it |
| T1 | Testcase suite: compile, run, diff the recorded programs | Miscompilation, ABI mistakes, bad codegen | 0.1 |
| T4 | Determinism: compile twice, compare assembly bytes | Hash-ordered emission and other irreproducibility | 0.1 |
| T2 | Toolchain matrix: the same programs through as+ld, clang+lld, ras+weld
|
Divergence between our tools and the ones they replace | 0.2 |
| T3 | Differential against an equivalent C program | Answers that are wrong but self-consistently wrong | 0.2 |
| T6 | Cross-compilation smoke: emit assembly for every recognized target name | Panics and unimplemented arms in targets nobody exercises | 0.2 |
| T5 | Optimization equivalence: --opt-level 0 through --opt-level 3 produce identical output |
Passes that change behavior instead of speed | 0.3 |
| T7 | Emulated execution under QEMU | Correctness on targets with no native runner | 0.3 |
| T9 | Compile time, runtime, and binary size recorded per commit | Slow regressions nobody notices | 0.3 |
| T8 | Fuzz the parser and the IR validator | Panics on hostile input | 1.0 |
T5 is the one to add early. It is the cheapest tier here and it catches the most embarrassing class of bug, since a pass that changes a program's output is always wrong and stays invisible until a user reports it.
Not the emulator. QEMU user-mode is installed for every architecture here. What is
missing is the cross C libraries, so most programs never link far enough to hand QEMU
anything, and a freestanding program that does link has no startup code and faults when
its entry function returns. s390x is the one architecture whose cross libc is present,
and it is the one whose recorded programs are built, run and checked under emulation.
A recorded program is skipped there only where the reason is written down against it:
inline_asm_rdtsc reads the x86 timestamp counter, which has no z equivalent, and
vector_operations needs the z13 vector facility, which the backend does not
implement. Inline assembly itself lowers and runs.
AArch64, RISC-V 64 and PowerPC64 cannot be linked here at all: the driver stops with an
error naming the cross gcc it looked for, because the startup objects come from it.
The recorded programs cover arithmetic, control flow, calls, memory, composites, and a set of regression cases named after the bugs that produced them. That last group is the pattern to keep: every bug fixed gets a testcase named after it, added in the same change as the fix. The suite then grows out of real defects instead of guesses about what might break.
After that, roughly in order of value: programs mixing floating point with calls, programs that push the register allocator past spilling, deep recursion, large stack frames, structs passed and returned by value across the ABI boundary, and programs built from several objects so linking gets exercised rather than assumed.
Three questions come up often enough to answer here rather than in issue threads.
On a hosted target, yes. That is what the platform requires, not a compromise. The three platforms differ more than they look.
Linux publishes a stable syscall ABI, so a program with no libc is normal and supported there. Lamina should keep being able to produce one.
macOS treats syscall numbers as unstable and expects everything to go through
libSystem. Emitting a numbered service call works against the platform.
Windows has no stable syscall ABI at all. Numbers change between builds, every
program reaches the kernel through ntdll, and formatted I/O comes from the C runtime
above that. There is no supported alternative, so calling the CRT on Windows is the only
option rather than a preference.
The rule is per-target. Use the platform's supported entry mechanism, and never assume the Linux one is a sensible default for anyone else.
That framing answers a lot of open questions. A drop-in is judged against the thing it replaces. Where the original has a flag, a behavior, or a diagnostic, the replacement matches it instead of inventing something nicer. Where the original declines to do something, the replacement declines too.
ras is already built this way. Its options mirror GAS closely (-o, -I, -L,
-g/--gdwarf-, -K, -J, -Z, -q, -Q, -k, -w, -mbss-plt), so it drops
into a build that expects as.
weld is further off, in two ways.
Several ld options are still missing. weld accepts -o, -e, -L, -l,
--dynamic-linker, -nostdlib, -nodefaultlibs, -static, -shared, --gc-sections,
--rpath, --start-group, --strip-all, --build-id, --Map, -r, -z, --target
and response files.
It does not have -T,
--whole-archive, --defsym, -u, or --wrap. -T is how a linker script is
supplied, and -static is how you link without a dynamic loader, so this gap and the
Artery target are the same gap.
It does the driver's job. ld and lld never link libc on their own. The driver
does. gcc and clang add crt1.o, crti.o, -lc, the dynamic-linker path, and the
search paths before ld runs, and bare ld hello.o -o hello leaving printf
unresolved is correct behavior, not a bug. On Windows, link.exe and lld-link
additionally need an import library to know which DLL a symbol comes from.
weld currently supplies startup objects and an interpreter path itself, hardcoding
/lib64/ld-linux-x86-64.so.2 regardless of architecture. Lamina's link step leans on
that, passing -lc and passing no CRT objects on purpose, with a test asserting their
absence. For a tool whose job is to be a drop-in, that is backwards. The correction:
-
weldlinks what it is given. No implicit CRT objects, no implicit default libraries, no invented interpreter path.--dynamic-linkeris honored when passed and absent when not. - Lamina's driver takes over that work, deriving it from the target, because that is the layer that knows the target.
- Nothing changes for the user.
lamina app.lamina -c weldstill works with no system toolchain installed.
A freestanding link then stops being a special case. It is a target whose driver adds nothing, handed to a linker that never added anything on its own.
writebyte, write and read are intrinsics that each backend expands inline, through
a per-OS match with a fallback arm. That design is the direct cause of the Windows bug
in 0.1, and it cannot express a target like Artery at all.
The destination is an IR with no built-in I/O. A program declares what it calls, the target decides what that resolves to, and the backend emits an ordinary call. That one change unblocks the Windows fix, WebAssembly, and Artery together.
Artery is a from-scratch Rust kernel built alongside Lamina, and the reason 0.4 exists. Supporting it means compiling for an environment with no libc, no startup objects, and no host underneath.
The constraints it imposes, the binary format, how system calls are reached, and the work items to get there are on their own page: Artery target.
- Replacing LLVM for every workload. Lamina is a backend with a small surface, and it stays that way on purpose.
- Exposing Rust struct layouts or MIR internals through the C ABI.
- Making the JIT a plugin mechanism. It exists for language implementations such as dynamic languages and bytecode VMs, not as an extension ABI.
- Forcing one toolchain on downstream projects. A frontend should be free to pick Lamina, LLVM, or something else.
Issues live on SkuldNorniern/lamina.
ras, weld, and lamina-web under
Project-Lamina carry their own issues for
component-specific work.
lamina · ras · weld · lamina-web