ADR-0077: LLVM-backed target system
Status
Implemented (2026-05-03).
Summary
Replace the three-variant Target enum in gruel-target with a struct backed by target_lexicon::Triple, accepting any LLVM-supported triple from the command line. Expand the Gruel-language Arch and Os enums from 2 variants each to cover the full set of practically relevant LLVM targets. Remove dead code left over from the custom ELF backend. Wire the compilation target into inkwell so --target actually produces cross-compiled output.
Context
gruel-target was written when Gruel had a custom x86-64 ELF backend. At that time, enumerating three targets (x86-64 Linux, AArch64 Linux, AArch64 macOS) was sufficient and the Target type carried ELF-specific metadata (elf_machine, default_base_addr, macos_min_version) needed by the hand-written linker.
Since ADR-0024 the backend is LLVM (via inkwell). LLVM supports dozens of architectures out of the box, but two bugs were never fixed during the migration:
Cross-compilation is silently broken. All three
TargetMachinecreation sites ingruel-codegen-llvm/src/codegen.rscallTargetMachine::get_default_triple()— the host machine's triple — instead of the triple passed via--target. Passing--target aarch64-linuxon an x86-64 host produces x86-64 object files.@target_arch()and@target_os()reflect the host, not the compile target. Both intrinsic handlers callTarget::host().arch()/Target::host().os()instead ofself.options.target.arch()/self.options.target.os(). This means conditional compilation based on@target_arch()cannot be used with--target.
Additional problems introduced by the old design:
elf_machine,default_base_addr,macos_min_versionare ELF-writer debris; nothing outsidegruel-target's own test suite calls them.ArchandOsingruel-targethave 2 variants each; the Gruel-languageArchandOsenums (ingruel-builtins) likewise have only 2 variants, meaning programs compiled to Windows or RISC-V have no way to write platform-conditional code.
Decision
1. Replace Target with a target_lexicon-backed struct
Add target-lexicon = "0.12" to gruel-target. Replace the enum with:
Target is no longer Copy (because target_lexicon::Triple is not Copy). Call sites that relied on implicit copy are updated to use Clone or take references.
Target::from_str accepts any string that target_lexicon::Triple can parse.
Target::host() uses target_lexicon::HOST (a compile-time constant provided by the crate) rather than #[cfg(...)] ladders.
Target::all() returns the curated list of "blessed" targets — those that Gruel explicitly tests and supports. These are the same three as today; adding a new blessed target is a one-line change to the constant list.
The CLI --target flag still defaults to Target::host(), accepts any valid triple, and emits a warning if the triple is not in the blessed list.
2. Derive arch() and os() from the lexicon
The compiler-internal Arch and Os types in gruel-target expand to cover everything target_lexicon models. New variants are derived from the parsed triple rather than hardcoded match arms. The full set Gruel needs today:
Arch: X86, X86_64, Arm, Aarch64, Riscv32, Riscv64, Wasm32, Wasm64
Os: Linux, Macos, Windows, Freestanding, Wasi
The existing is_elf() and is_macho() helpers (used by the linker) are kept; they are derived from triple.binary_format.
3. Remove dead custom-backend methods
Delete from gruel-target:
elf_machine()— ELF machine constant; LLVM handles this internallydefault_base_addr()— load address; the LLVM linker script owns thismacos_min_version()— Mach-O version encoding; the system linker handles itpage_size(),stack_alignment(),pointer_size()— useful concepts, but currently called by nobody outside tests; delete now, re-add when actually needed by a pass
4. Wire the target triple into inkwell
Add target: Target to CodegenInputs (and compile_bitcode_to_object). All three TargetMachine creation sites in codegen.rs replace:
let target_triple = get_default_triple;
with:
let target_triple = create;
For cross-compilation, LLVM must be built with the required backend enabled. For targets whose LLVM backend is not compiled into the host's LLVM installation, codegen returns a clear error rather than silently falling back to the host.
LlvmTarget::initialize_native is replaced with initialize_all when the compile target differs from the host, so all backends are available.
5. Expand Arch and Os in the Gruel language
Update gruel-builtins to expand ARCH_ENUM and OS_ENUM. New variants are appended to preserve existing variant indices (existing programs remain correct):
Arch:
0: X86_64 (existing)
1: Aarch64 (existing)
2: X86 (new)
3: Arm (new)
4: Riscv32 (new)
5: Riscv64 (new)
6: Wasm32 (new)
7: Wasm64 (new)
Os:
0: Linux (existing)
1: Macos (existing)
2: Windows (new)
3: Freestanding (new)
4: Wasi (new)
@target_arch() and @target_os() in sema are updated to:
- Use
self.options.target.arch()/self.options.target.os()(the compile target) - Map each variant to its correct index via the expanded enums
Implementation Phases
Phase 1: Rewrite
gruel-targetwithtarget_lexicon- Add
target-lexicondependency togruel-target/Cargo.toml - Replace
Targetenum withTarget(target_lexicon::Triple)newtype struct - Implement
Target::host()viatarget_lexicon::HOST - Implement
Target::all()as a const-defined blessed list - Implement
Target::from_strby delegating totarget_lexicon - Expand compiler-internal
ArchandOsenums (8 and 5 variants) - Implement
arch()andos()via lexicon field mapping - Update
is_elf()/is_macho()viatriple.binary_format - Update call sites that relied on
Target: Copyto useClone/&Target - All existing tests must pass; add tests for new triples
- Add
Phase 2: Remove dead custom-backend methods
- Delete
elf_machine,default_base_addr,macos_min_version,page_size,stack_alignment,pointer_sizefromTarget - Delete their tests
- (Already accomplished as part of the Phase 1 rewrite — the new module omits these methods and their tests.)
- Delete
Phase 3: Wire target into inkwell
- Add
target: Targetfield toCodegenInputsandcompile_bitcode_to_object - Update
gruel-compilerto populate the field fromoptions.target - Replace
get_default_triple()at all three sites incodegen.rs - Use
initialize_allwhen cross-compiling (target arch ≠ host arch) - Add integration test: compile to a non-host target and verify the object file header matches the expected machine type (via
readelf/otool)
- Add
Phase 4: Expand Gruel-language
Arch/Osand fix intrinsics- Expand
ARCH_ENUM.variantsandOS_ENUM.variantsingruel-builtins - Update the variant-index match arms in
analyze_target_arch_intrinsic/analyze_target_os_intrinsicto useself.options.target(notTarget::host()) - Extend the variant-index match arms in the constant-fold path (lines 8718–8737 in
sema/analysis.rs) - Add spec tests covering the new variants and cross-compilation behavior
- Expand
Consequences
Positive
--targetcross-compilation actually works end-to-end@target_arch()/@target_os()reflect the compile target, enabling correct conditional compilation in cross-compiled builds- Arbitrary LLVM triples are accepted; users can target platforms Gruel doesn't officially bless
- The hardcoded ELF debris is gone; the codebase no longer pretends the custom backend exists
- Adding a new blessed target is a one-line change plus codegen/linker wiring
Negative
TargetlosesCopy; call sites need minor updates (clone()or&Target)- Cross-compilation requires the target's LLVM backend to be compiled into the host's LLVM install — the same constraint as
rustc
Open Questions
- Should the warning for unblessed triples be a hard error or just informational? (Leaning: informational warning, since adventurous users should be able to experiment)
- Does
initialize_allmeaningfully increase link time against LLVM? If so, we may want to initialize only the specific backend viaTarget::initialize_<arch>.
Future Work
- Windows cross-compilation additionally requires
lldorlink.exe; this ADR only covers the codegen step, not the Windows-specific linker flags - CPU feature detection (
--target-cpu,-march): currently hard-coded to"generic". Worth exposing once we have users who need micro-architecture tuning. - Tier classification for blessed targets (tier 1: tested in CI; tier 2: builds but not CI)
References
target-lexiconcrate — Bytecodealliance's LLVM triple parser used by Wasmtime and Cranelift- Rust's
TargetTripleinrustc_target::spec— the enum + JSON approach Rust uses - Zig's
std.Target— the structured (Cpu + Os + Abi) approach Zig uses gruel-codegen-llvm/src/codegen.rslines 269, 303, 341 — the three broken sitesgruel-air/src/sema/analysis.rslines 11491, 11536 — the broken intrinsic handlers