Internals — Cranelift native backend (slice 8)¶
mty-codegen-cranelift translates the slice-6 MtyIR into Cranelift
IR and produces either a JIT'd in-process fn pointer (mty run)
or a host-format object file linked into an executable
(mty build --target native).
It is the slice-8 default native backend — see A46 for why LLVM is feature-gated.
Pipeline¶
.sd → AST → HIR → typed HIR → borrow-checked → MtyIR
↓
Monomorphizer
↓
mty-codegen-cranelift
┌────────┴────────┐
│ │
JIT Object
│ │
fn-ptr .o
│ │
transmute linker
│ │
run executable
Crate layout¶
| Module | Responsibility |
|---|---|
lib.rs |
re-exports |
error.rs |
CodegenError, CompileResult |
artifact.rs |
NativeArtifact, BuildMode |
abi.rs |
call-conv selection, MtyIR-type → cranelift type |
layout.rs |
size / align / field-offset for ADTs |
runtime_imports.rs |
C-ABI symbols the runtime exposes |
mono.rs |
generic-fn monomorphization (slice-8: strip-and-defer) |
lower.rs |
per-fn IR builder |
jit.rs |
cranelift-jit driver |
object.rs |
cranelift-object driver + linker invocation |
Type lowering¶
| MtyIR type | Cranelift type | Notes |
|---|---|---|
Bool |
I8 |
|
Int(I8/U8) |
I8 |
|
Int(I16/U16) |
I16 |
|
Int(I32/U32/IntInfer) |
I32 |
unsuffixed defaults to i32 |
Int(I64/U64/ISize/USize) |
I64 |
64-bit host only in slice 8 |
Int(I128/U128) |
I128 |
|
Float(F32) |
F32 |
|
Float(F64/FloatInfer) |
F64 |
|
Char |
I32 |
UCS-4 codepoint |
Duration / Size |
I64 |
nanoseconds / bytes |
Str / String / Bytes |
I64 (ptr) |
passed by ptr in slice 8; (len) lives in caller-known register |
Ref<T> / RawPtr<T> |
I64 |
host pointer |
Cap |
I64 |
opaque runtime handle |
Dyn |
I64 |
only ptr; vtable elided in slice 8 |
Unit / Never |
(omitted) |
Aggregates (Tuple / Array / Adt) flatten to a pointer in the
slice-8 calling convention — caller allocates a stack slot, passes
its address.
Calling convention (A52)¶
- SystemV on linux / macOS / non-Windows.
- WindowsFastcall on Windows.
extern cfns: same ABI as host (cranelift handles).- JIT: cranelift requires
is_pic = false; object mode usesis_pic = truefor relocatability.
Runtime bridge (12 imports)¶
The JIT'd code calls back into the runtime via twelve C-ABI
symbols (see runtime_imports::RUNTIME_IMPORTS):
| Symbol | Signature | Purpose |
|---|---|---|
mty_runtime_log |
(ptr, len) |
log("...") |
mty_runtime_print |
(ptr, len) |
print("...") |
mty_runtime_panic |
(ptr, len) |
trap with message |
mty_runtime_arena_push |
() -> handle |
open arena frame |
mty_runtime_arena_pop |
(handle) |
close arena frame |
mty_runtime_alloc |
(size, align, zero) -> ptr |
bump-allocate in top arena |
mty_runtime_budget_charge |
(bytes) -> ok? |
charge against budget |
mty_runtime_send |
(target, msg, payload) |
fire-and-forget |
mty_runtime_ask |
(target, msg, payload, deadline_ms) -> reply |
sync request |
mty_runtime_spawn |
(agent_id) -> handle |
start an agent |
mty_runtime_extern_call |
(name_ptr, name_len, args) |
call libc fn |
mty_runtime_log_i64 |
(value) |
debug log for ints |
The runtime registers these via JITBuilder::symbol(name, addr) at
finalize time. AOT mode declares them as imports and lets the host
linker resolve against libsdust_runtime.so / .dylib / .lib
(v0.2 work; slice 8 ships JIT only for the runtime bridge).
Conservative-by-default lowering¶
The FnLower raises CodegenError::Unsupported(reason) when a MtyIR
shape can't be lowered. The driver catches Unsupported in mty run
and falls back to the interpreter, so the user sees correct behaviour
transparently. v0.2 closed the major MtyIR-coverage gaps:
- integer / bool / float / char arithmetic & comparisons (incl. float-int bitcast for MtyIR-typeck mismatches)
let/=/if/goto/return/unreachable/panic- direct fn-to-fn calls (with unit-arg filtering + per-param coercion)
log/print/panicvia the runtime ABI bridge- string constants via the literal pool
- ADT construct + destructure (struct + enum literals; field reads; variant-field reads; switch-variant lowered as load-tag + brif chain)
?propagation —Term::TryReturnErrmaterialisesResult::Errin a fresh stack slot and returns its address- Tuple init + read
- Agent
send/ask/spawnroute through runtime stubs (compiled handlers are a v0.2.x follow-up; the runtime still drives dispatch through the interpreter for now) EffectInvokeroutes throughextern_callstubMethodCall/IndexReadlower to best-effort runtime calls- Result constructor extern fns (
Ok/Err) emit ADT init
Out-of-scope (still interp fallback):
dyn Traitvtables- closure capture (lambdas-with-environment)
- inline cap dispatch (cap method calls)
- arbitrary
extern { fn }resolution (requires shared-lib symbol resolution beyond the slice-8 stub)
Aggregate ABI¶
| Shape | Cranelift representation |
|---|---|
| Bool / Int(small) / Char / Float | passed by value |
| Aggregate (struct/enum/tuple/array) | passed by pointer (i64 address) |
Str/String/Bytes |
(ptr, len) pair, passed as pointer |
| Reference / RawPtr | i64 pointer |
The function signature is built from the MtyIR types; the lowerer adds
stack slots lazily when a MtyIR local is first written to via
AdtInit/TupleInit. Parameters that are aggregate-typed keep the
caller's pointer untouched.
Enum layout: [u32 tag][padding to max-payload-align][payload bytes],
matching the layout module's natural-alignment scheme. Struct layout:
same minus the tag. See aggregate.rs.
JIT driver¶
let prog = sdust_sir::lower_package(&pkg, &typed);
let mono = sdust_codegen_cranelift::Monomorphizer::new(&prog).run();
let syms = sdust_runtime::codegen_abi::symbol_table();
let syms = symbols_from(...);
let jc = build_jit(&mono, &syms)?;
jc.call_main(); // transmute fn-ptr; respects Unit-vs-Int return shape
Object + linker¶
let obj = compile_object(&prog, &obj_path)?;
let exe = link_executable(&obj, &exe_path, BuildMode::Release)?;
Linker discovery (A52, extended in v0.2):
1. $MTY_LINKER if set, otherwise $STARDUST_LINKER (legacy spelling,
one-shot deprecation warning).
2. Windows: clang.exe, clang, gcc.exe, gcc, cc.exe, then
lld-link.exe, lld-link. (We deliberately do not probe bare
link on Windows because the MSYS coreutils /usr/bin/link.exe
shim shadows MSVC's real linker.)
3. Unix: cc, gcc, clang, then ld.lld, lld. clang is
preferred because it can drive both GNU and macOS link lines via
the same frontend.
4. The MSYS/Git-Bash /usr/bin/link.exe (hardlink helper) is actively
skipped if found in PATH.
If none found, compile_object succeeds but link_executable is
skipped and the caller is told to set $MTY_LINKER.
If a linker is found and returns a non-zero status, mty build now
reports a link error with the object path and linker stderr. That path
is intentionally separate from the missing-linker case so unresolved
symbols and bad linker arguments fail loudly instead of being reported
as a successful object-only build.
Why clang first¶
clang (and lld underneath it) speaks every host's linker line:
on Linux it drives ld.bfd/ld.gold/ld.lld directly; on macOS it
hands off to the system ld64; on Windows it embeds lld-link which
honours MSVC arg syntax. That makes the cranelift backend's link
step a single code path across hosts.
Monomorphization (A49)¶
Slice-8 MVP: Monomorphizer::run() clones the program and strips
generic fns (any fn whose params or return use SirTy::Param). The
resulting program contains only fully-concrete fns. Programs that
exercise generics fall through to the interpreter via the slice-8
Unsupported path.
Real per-(fn, type-args) specialization is v0.2 work. The slice-8 choice is deliberate: it ships a working compiler for monomorphic programs without blocking on the generic-substitution machinery, which interacts subtly with effect rows and trait dispatch.
Future work¶
- Full ADT lowering (struct construct/project, enum tag-and-payload, array elem load/store)
?propagation lowering (compose a Result over the next block)- Method/trait dispatch via vtables
- Agent dispatch through compiled handler fn-ptrs (replaces the slice-7-via-interpreter per-turn callback)
- DWARF debug info (cranelift's stub support → useful frames)
- ThinLTO-style cross-fn optimization (v0.2 LLVM)
v0.36 T1 — Native codegen fixes¶
Three correctness fixes landed in v0.36 T1:
- U8 widening. Loads of
U8values into a wider register now zero-extend instead of inheriting cranelift's default sign-extend forI8. Previously aVec[U8]element read into a 32-bit register returned a sign-extended negative value for any byte ≥ 0x80. - Dynamic log lowering.
log(<runtime-string>)(where the operand is computed rather than a string literal) now emits the three-argument runtime trampoline directly. Previously dynamic-log call sites bailed to the interpreter, which brokemty build's native-binary path for any program using runtime-built log messages. Test coverage:crates/mty-driver/tests/native_dynamic_log.rs. - Hex / binary / octal literal suffixes.
0xFF_u8,0b1010_u8,0o77_u8now lower with the correct integer type. The parser had the suffix; the lowering pass was discarding it for non-decimal literals and falling back toIntInfer.
See docs/reference/cli/mty-build.md
for the user-facing surface.
v0.36 T4 — Cranelift segment rename¶
The object writer emits a segment named b"mighty" in the metadata
section (renamed from the legacy b"stardust"). The legacy spelling
is still accepted by accepted_segment_name for compat with external
inspectors. See rename-compat.md.