Pure Rust Toolchain — Implementation Plan
Status: architecture locked, post-audit. Ready to sequence.
Goal
A Rust development stack with zero LLVM and zero C/C++ in any component the project ships or invokes as a build-time tool. Rust source → rustc frontend → Trident codegen → Rust linker → executable. Vendor-provided OS runtime is the only non-Rust code in the picture.
What "Pure Rust" Means Here
The distinction that matters: runtime vs build-time.
Runtime dependencies are fine. The OS the user already runs is not part of our toolchain. The Linux kernel is C; macOS libSystem is C; Apple dyld is C++ — these are vendor-shipped components the user installed when they installed the OS. Calling them via FFI declarations (the way honeycrisp wraps IOSurface, IOKit, Metal) is no different from issuing syscall instructions on Linux. The OS boundary is the OS boundary.
Build-time tools must be pure Rust. Anything we invoke during compilation — compilers, linkers, assemblers, code generators, build scripts — is our infrastructure. Using Apple's ld64 (C++) as our linker means our toolchain still ships C++ work. Using LLVM (C++) for codegen means the same. These are not OS components; they're tools we choose, and the choice must be Rust.
This is why the Mach-O linker is in scope, not deferred. libSystem.dylib is the OS; ld64 is a C++ compiler tool. Pure-Rust on macOS requires replacing the latter, not the former.
By this definition:
- In scope: rustc frontend (already Rust), codegen, linker, loader (Linux), allocator, standard library, build scripts.
- Out of scope (legitimately): kernel, libSystem.dylib, dyld, kernel32.dll, hardware microcode. These are OS or silicon.
- Excluded targets: Windows (per user decision); deferred until macOS + Linux are proven.
Locked Architectural Decisions
These are the answers to the design questions worked out in the discussion. The rest of the plan flows from them.
-
Trident is the only codegen backend. No Cranelift, no LLVM, no codegen_gcc. Trident's
compile/directory already has 14K LOC of native machine-code emitters; we reuse the encoding infrastructure and add LIR-driven instruction selection on top. -
Pipeline: rustc frontend → MIR (in-memory) → mir2lir (in-process) → LIR → native bytes → Mach-O / ELF → executable. The frontend (parse, type-check, borrow-check, monomorphize, MIR) stays standard rustc; we plug in at the codegen interface as a
rustc_privateplugin likerustc_codegen_craneliftdoes. No serialization anywhere — no JSON, no TOML, no binary IR format. rustc gives us MIR viaTyCtxt; we lower it in-process. MIR and LIR are both flat 3-address with virtual registers — they map 1:1 structurally. The cost ofrustc_privatecoupling is version-pinning (we ship rustc + plugin as a pair); the benefit is no format design, no parsing overhead, and direct access to type information when needed. TIR remains the convergence IR for Trident's.trisource and for stack-based proof VMs (Triton, Miden, SP1); it is NOT on the native codegen path. -
nox stays frozen as the proof VM. The existing
mir2nox.rs(717 LOC in trident/) stays as the optional proof pathway for code that wants STARK proofs. The native codegen path does not go through nox. This preserves nox's 16-pattern proof property and avoids dragging tree-rewriting semantics into general Rust compilation. Same logic that excludes TIR from the native path: nox is structurally wrong (tree-rewriting), TIR is structurally wrong (stack-based) — both excellent in their domains, both inappropriate as a way station for flat 3-address MIR going to flat 3-address LIR. -
Parallel new LIR backends, with shared encoders. Existing
compile/<isa>.rsfiles (nox-driven, Goldilocks field arithmetic) stay untouched. Newcompile/<isa>_lir.rsfiles consume&[LIROp]and emitVec<u8>for standard Rust u64 semantics. Both files import a sharedcompile/<isa>_encoders.rsmodule containing pure opcode emitters (~70 LOC per ISA). Confirmed by prototyping arm64.rs: only 27% of the existing file is reusable for LIR (the encoders); 43% is Goldilocks field reduction and 20% is nox-tree CF decoding — neither has an LIR analog. Refactor-in-place would fight the existing structure; parallel is cleaner and ~1,480 LOC total for three ISAs. -
Full standard Rust accepted. The toolchain must compile arbitrary Rust crates (cargo, ripgrep, etc.), not just
rs-subset code. This requires LIR extensions: atomics (~200 LOC LIR + ~300 LOC per backend lowering) and thread-local storage (~minimal on macOS — pthread_*, harder on Linux — segment registers). Inline assembly is already a Tier 0 LIR op (LIROp::Asm { lines: Vec<String> }), no LIR extension needed — only backend lowering for token-stream splicing. -
panic = abortfor MVP. Landing pads + DWARF CFI deferred to a later phase. Cargo test runs each#[test]in a forked process viaposix_spawnuntil then. Document and accept the divergence from upstream cargo test behavior. -
macOS-first via honeycrisp. aarch64-apple-darwin is the only target until the pipeline is proven end-to-end. Linux follows.
-
Rs subset preferred for new code we write. The
rslint suite (no heap, no dyn, bounded async, deterministic) applies to cyber-stack code. Standard Rust acceptance is for everything else. -
rsc-buildreplaces cargo, doesn't patch it. Compact build tool, ~4,700 LOC, handles 80% of cases via Cargo.toml format for compatibility. Drops: alternative registries, complex feature unification,[patch]/[replace], build.rs orchestration (refuses or runs explicitly with--allow-build-script), cargo publish/doc/bench/install, cross-compilation, multiple build profiles. Usestoml,pubgrub,ureq+rustls,tar,miniz_oxide— all pure Rust. -
Linux static-only by default. No
ld.soreplacement. Every binary statically links libstd. Drops the entire 8K-LOC pure-Rust dynamic loader effort. Cost: larger binaries (~5MB vs ~500KB). Benefit: -8 sessions, reproducibility, attack-surface reduction. Dynamic linking can be added later if needed; the v1 toolchain doesn't require it. -
Mach-O linker scope: ~3K LOC, not 15K. Uses the
objectcrate (pure Rust) for Mach-O format I/O — saves most of what looked like format work. Phase 1 emits a monolithic single-binary Mach-O directly (~500 LOC, no multi-object linking). Phase 2 adds the full multi-object linker (~2,500 LOC additional) using theobjectcrate writer, with our own symbol resolution / layout / relocation / dyld-binding / code-signing. -
compiler-builtins purification: focused, not exhaustive. Port only the ~20 intrinsics actually invoked by typical Rust programs on aarch64 + x86_64. The remaining residue stays as upstream's asm/C until someone hits it. ~500 LOC, not 2K.
-
Extended honeycrisp: scoped to std's minimum surface. open/close/read/write, pthread_create/join,
__ulock_wait/__ulock_wakefor Mutex, getrandom, getenv/setenv, exit, fork+execve, getcwd, time. ~3K LOC of FFI declarations + safe wrappers. Not the full kqueue + mach ports + dispatch surface — those go in if/when needed.
Pipeline (Locked)
Rust source
│ rustc frontend (parse, typeck, borrowck, monomorphize) existing, ~1M LOC, untouched
▼
MIR (post-monomorphization, concrete types only; flat CFG, 3-address)
in-memory, via TyCtxt
│ rustc_codegen_trident plugin NEW, ~2,000 LOC
│ → mir2lir (in-process function call) NEW, ~2,000 LOC
▼
LIR (53 ops, flat CF, 3-address, virtual regs; Asm already in Tier 0;
extended with atomics + TLS in Tier 1)
│ RegisterLowering trait existing trait in trident/
▼
<isa>_lir.rs + <isa>_encoders.rs NEW, ~1,480 LOC total
▼ Vec<u8> raw machine code
│ Mach-O / ELF writer (object crate handles format I/O) NEW glue, ~800 LOC each
▼
.o file (or monolithic executable for single-binary case)
│ Mach-O linker (NEW, macOS) / wild linker (ELF, Linux) Mach-O ~3K LOC; wild existing
▼
executable (static-only on Linux; dyld-linked on macOS)
│ dyld (vendor, macOS) vendor-shipped, no ld-rs needed
▼ process runtime, linked against:
│ - libSystem via honeycrisp-extended FFI (macOS) ~3,000 LOC FFI declarations
│ - rustix raw syscalls (Linux, static) existing
│ - talc allocator existing
│ - libstd with macOS or Linux backend existing core, alloc, std + our backends
Why MIR → LIR direct, not MIR → TIR → LIR:
- MIR is flat-CFG + 3-address + virtual locals.
- LIR is flat-CF + 3-address + virtual
Reg(u32). - TIR is stack-based + structural CF (
IfElse/IfOnly/Loopnested bodies). - Routing MIR through TIR would lift flat → stack-structural, then immediately lower stack-structural → flat in TIR → LIR. The structure MIR already has gets discarded and rebuilt. The
tir_to_lirconverter insrc/ir/lir/convert.rsis currentlytodo!()— going direct from MIR avoids depending on it. - TIR stays the right IR for: Trident's own
.trisource compilation, stack VM proof targets (Triton, Miden, SP1) viaStackLowering, GPU targets viaKIR.
Optional side-channels (orthogonal to native codegen):
Proofs: MIR → mir2nox (existing, 717 LOC) → nox formula → zheng prover → STARK proof
Stack VMs: TIR → StackLowering → assembly text (Triton, Miden, SP1)
GPU kernels: TIR → KIR → KernelLowering → PTX / MSL / SPIR-V
Trident source: .tri → AST → TIR (this is what TIR was designed for)
Programs opt into proofs by compiling through mir2nox and shipping the proof alongside the native binary. The native binary itself comes from the mir2lir path; proofs are an additive artifact.
Trident's Role — Verified
Trident's IR layering (src/ir/) gives us the right entry point for a Rust codegen backend. The full layering Trident maintains is:
┌──→ StackLowering → assembly for stack VMs (Triton, Miden, SP1)
AST (Trident source) ─→ TIR──→ KIR → KernelLow → GPU code for PTX, Metal, SPIR-V
└──→ TreeLowering → nox for nox/Nock tree VMs
LIR ←─→ RegisterLowering → bytes for x86_64, arm64, rv64
LIR is reached two ways: from TIR (via the tir_to_lir converter, currently todo!()), or directly from any flat-3-address input. For rs's MIR-driven codegen we enter LIR directly via mir2lir, because MIR is already flat-3-address — going through TIR would lift to stack form and immediately lower back.
The RegisterLowering trait exists today in src/ir/lir/lower/mod.rs. LIR has 53 operations covering everything needed for general Rust codegen — including Asm passthrough at Tier 0, full arithmetic / I/O / memory / hash / events / storage at Tier 1. Tiers 2-3 (proof-specific) are unused by the native path. The mir-format crate in rs/ already serializes MIR to JSON. mir2nox (717 LOC) proves the MIR consumer pattern works.
We're not inventing architecture — we're extending the LIR consumer path. Trident's own source ingestion uses TIR; rs's MIR ingestion uses LIR. Both converge at LIR for register-machine native codegen.
Existing Trident inventory we reuse:
- IR infrastructure (TIR, LIR, KIR, Tree): ~10,000 LOC across
src/ir/ - TIR optimizer + builder + encoder: existing
- RegisterLowering trait: ~30 LOC
- Existing nox-driven backends (x86_64, arm64, rv64, rv32, rvv, thumb2, wasm, ebpf, ptx, etc.): 14,331 LOC — untouched, reused only for their encoding tables via the shared
<isa>_encodersmodules - Cost models, verifier, package store, deploy infrastructure: existing
What's new for the LIR codegen path:
<isa>_encoders.rsper ISA — extracted pure encoders (~70 LOC each)<isa>_lir.rsper ISA — LIR consumer + instruction selection (~400 LOC each)- mir2tir — the MIR → TIR bridge (~4,000 LOC)
- TIR extensions for atomics / asm / TLS (~500 LOC in TIR + lowerings)
Component Inventory
Embed (already pure Rust, production-grade)
| Component | Crate / project | Coverage |
|---|---|---|
| rustc frontend (parser, HIR, type-check, borrow-check, MIR, monomorphization) | rust-lang/rust | universal — untouched |
| std core, alloc | rust-lang/rust | universal |
| libm (pure Rust math) | rust-lang/compiler-builtins/libm | universal |
| Linux syscalls | rustix linux_raw backend | x86_64, aarch64, riscv64, others |
| Apple-framework FFI surface (compute) | honeycrisp (~/cyber/honeycrisp/) |
IOSurface, IOKit, CoreFoundation, Metal, libobjc, AMX, NEON |
| Trident IR infrastructure | ~/cyber/trident/src/ir/ + src/compile/ |
TIR, LIR, KIR, native emitters (encoding tables) |
| Cryptography | RustCrypto suite | universal |
| TLS | rustls | universal |
| Networking | hyper, tokio, mio, h2 | universal |
| Allocator | talc | no_std, embedded, WASM, general |
| Compression | miniz_oxide, zstd-rs (pure mode), lz4_flex | universal |
| Hashing | hashbrown (SwissTable), ahash | universal |
| Serialization | serde | universal |
| Regex | regex | universal |
| Date/time | jiff | universal |
| Linker (ELF, Linux) | wild | x86_64-linux, aarch64-linux, riscv64-linux |
| Git operations | gitoxide | replaces libgit2 |
| HTTP client | reqwest + rustls | replaces libcurl |
| SSH | russh | replaces libssh2 |
| WASM runtime | wasmtime (with Cranelift removed) or wasmi | for WASM execution if needed |
Create (new code, with LOC)
| Component | LOC | What it does |
|---|---|---|
| mir2lir | 2,000 | Rust MIR (in-memory via TyCtxt) → LIR ops. Direct mapping — both are flat 3-address with virtual registers. In-process function call, no serialization. |
<isa>_lir.rs (arm64, x86_64, rv64) |
1,250 | LIR consumers: instruction selection from &[LIROp] to native bytes |
<isa>_encoders.rs shared modules |
230 | Pure opcode emitters extracted from existing compile/<isa>.rs |
| rustc_codegen_trident plugin | 2,000 | Hooks Trident into rustc's codegen_ssa interface as rustc_private plugin |
| LIR extensions (atomics, TLS) | 300 | New Tier 1 LIR ops. Asm is already in Tier 0, no extension needed. |
| Per-backend LIR-extension lowerings | 900 | ~300 LOC per backend for atomics, TLS, and Asm splicing |
ELF object writer (using object crate) |
800 | Glue around the object crate's ELF writer — symbols, relocs, sections |
Mach-O object writer (using object crate) |
800 | Same pattern, Mach-O format. The object crate handles the binary format I/O. |
| Mach-O linker (Phase 1 monolithic + Phase 2 multi-object) | 3,000 | Pure-Rust replacement for Apple ld64. Uses object crate for format I/O; our own symbol resolution, layout, relocation processing, dyld binding (eager only), ad-hoc code signing. In scope because ld64 is C++ build-time tooling, not OS runtime. |
| Extended honeycrisp / Darwin ABI surface | 3,000 | std-minimum FFI surface: open/close/read/write, pthread, ulock for Mutex, getrandom, getenv/setenv, exit, fork+execve, getcwd, time. Pattern from honeycrisp. |
| compiler-builtins focused purification | 500 | Port only the ~20 intrinsics actually invoked by typical Rust programs on aarch64 + x86_64. The remaining residue stays as upstream's asm/C. |
rsc-build (compact cargo replacement) |
4,700 | Build tool: build/run/test/check, Cargo.toml-compatible, uses pubgrub/toml/ureq/tar. Drops registries, complex features, build.rs orchestration, publish/doc/bench/install, cross-compilation. |
| Sealed bootstrap binary + provenance bundle | 500 | One-time build + signed cache. Replaces mrustc dependency going forward. |
pure-rust-check audit tool |
2,000 | Scans Cargo.lock + build artifacts, reports any non-Rust input |
build.rs scanner |
1,000 | Detects when crates shell out to cc / bindgen / non-Rust toolchain |
| CI + reproducibility verifier | 500 | GitHub Actions + reproducibility check |
Total new code: 23,480 LOC — well inside the 50K envelope after audit. (Down from 47,680 pre-audit: -8K from dropping ld-rs, -12K from Mach-O linker right-sizing, -5K from rsc-build aggressive scope, -2K from extended honeycrisp scope, -1.5K from focused compiler-builtins purge, -1K each from mir2lir / rustc_codegen_trident leaner estimates, -1.4K from ELF/Mach-O writers using object crate.)
Why Mach-O Linker Is In Scope
I originally marked it deferred. That was wrong. The reasoning:
Apple's ld64 is a C++ build-time tool, not an OS runtime component. The user installs it as part of the Xcode Command Line Tools — separate from the macOS itself. A "pure Rust toolchain" that invokes ld64 is shipping C++ work in its critical path; the toolchain is not actually pure Rust.
The runtime-vs-build-time distinction is what makes libSystem acceptable (it's the OS; user has it; we declare FFI) and ld64 unacceptable (it's a build tool; we invoke it; replacing it is our job).
This parallels the Linux situation exactly: wild replaces the GNU ld (C/C++ build tool) for the same reason. On Linux we don't get to say "well GNU ld is everywhere, let's just use it." We replace it because it's our linker, not the OS's. Same logic for Mach-O.
Concretely, the Mach-O linker is the difference between:
- ✓ "rustc → Trident codegen → Mach-O object writer → Rust Mach-O linker → Mach-O executable → dyld (vendor) → libSystem (vendor)"
- ✗ "rustc → Trident codegen → Mach-O object writer → ld64 (Apple C++) → Mach-O executable → dyld → libSystem"
The first is a pure-Rust toolchain on macOS. The second isn't. The Mach-O linker is on the critical path; deferring it means the entire macOS phase produces a non-pure toolchain. That defeats the project goal.
Effort: 15,000 LOC is realistic. Mach-O is well-documented (Apple publishes the format spec via xnu source). The work is: format reading/writing, segment/section layout, indirect symbol tables, weak/lazy binding, code signing (ad-hoc signature — SHA-256 over LC_CODE_SIGNATURE), dyld stub generation, LC_BUILD_VERSION setup. wild's object-file infrastructure is forkable (~5K of wild's code maps over to Mach-O concepts); the rest is Mach-O-specific.
Out of scope
| Component | Why |
|---|---|
| Windows toolchain | User decision — macOS + Linux first |
| Replacing libSystem.dylib | It's the OS, not the toolchain |
| Replacing dyld | Same — vendor-shipped OS component, not a build tool |
| Replacing Linux kernel | OS, not toolchain |
| Pure-Rust source bootstrap (rewriting mrustc in Rust) | Multi-year project; sealed-binary approach makes it unnecessary |
panic = unwind support (landing pads, DWARF CFI) |
Deferred per Decision 3b; panic = abort for MVP |
Native cargo test semantics with catch_unwind |
Same — fork-per-test workaround until landing pads land |
| LLVM as fallback | Decision 1 — Trident only |
| Cranelift as fallback | Decision 1 — Trident only |
Phases
Each phase produces a usable artifact. Estimates use 1 session = 3 focused hours.
Implementation Status (as of 2026-05-26)
| Phase | Code Status | Deliverable Status |
|---|---|---|
| 0 — Inventory | COMPLETE | pure-rust-check crate, 575 LOC |
| 1 — macOS MVP | Code complete | 8 integration tests written; nightly-2025-11-26 toolchain not installed — cannot verify |
| 2 — Mach-O linker | Code complete | macho-linker wired end-to-end; ripgrep not compiled — not verified |
| 3 — Rust coverage | Partial | TLS ✓, atomics ✓, asm ✓, Len/Repeat rvalues ✓, unchecked intrinsics ✓; deliverable not met |
| 4 — Darwin ABI | darwin-sys written | Not wired into std; libc still used; deliverable not met |
Phase 3 gaps remaining: multi-byte Repeat fill correctness, float↔int cast (needs FP regs in LIR), rare statement kinds (non-blocking), deliverable verification.
Phase 4 gap: requires nightly toolchain + modifying std source + build-std to verify.
Phase 1/2 gap: nightly-2025-11-26 toolchain must be installed on this machine.
Phase 0 — Inventory and gating (2 sessions)
- Catalog every direct + transitive C/C++ dependency of cargo + rustc on macOS aarch64.
- Write
pure-rust-check: scans Cargo.lock + build artifacts, reports non-Rust inputs. - Define the gate: "the toolchain compiles itself + hello-world + ripgrep with zero non-Rust source."
Deliverable: pure-rust-check crate, baseline report.
Phase 1 — macOS aarch64 MVP via Trident + honeycrisp (9 sessions)
Goal: hello-world compiled by rustc with zero LLVM, linked by a pure-Rust Mach-O writer, runs on Apple Silicon.
- Extract
arm64_encoders.rsfrom existingcompile/arm64.rs. ~70 LOC. (1 session) - Write
arm64_lir.rs— LIR consumer for arm64. ~400 LOC. (2 sessions) - Write
mir2lirMVP subset — enough LIR ops to handle hello-world (integer arithmetic, Branch/Jump, Call/Return, Memory ops for the print buffer, Asm for the syscall stub). ~800 LOC. (2 sessions) - Write
rustc_codegen_tridentplugin skeleton —rustc_privateintegration, drives mir2lir → arm64_lir in-process. ~1,000 LOC. (2 sessions) - Monolithic Mach-O emitter — single binary directly from Trident output, no .o intermediate, no multi-object linker. Uses
objectcrate for the Mach-O writer. ~500 LOC. (1 session) - Glue: minimal honeycrisp surface for
write(1, buf, n)+exit(0). ~200 LOC. (1 session)
Deliverable: pure-rs-toolchain directory with a Trident-backed rustc that produces a Mach-O executable running on aarch64-apple-darwin, printing "Hello, world!". No LLVM, no ld64, no clang. Verified with pure-rust-check.
Phase 2 — Full Mach-O linker (5 sessions, ~2,500 LOC of the 3K total)
Build out the multi-object Mach-O linker for real workloads. Uses object crate for format I/O; our work is symbol resolution, layout, relocs, dyld binding, code signing.
- Multi-object linking — symbol resolution + section layout + relocation application (ARM64_RELOC_*). (2 sessions)
- dyld binding opcodes (eager only, LC_DYLD_INFO_ONLY bind stream). (1 session)
- Code signing (ad-hoc SHA-256 super-blob; format documented in Apple xnu source). (1 session)
- LC_BUILD_VERSION, LC_MAIN, LC_LOAD_DYLIB, LC_SYMTAB, LC_DYSYMTAB plumbing. Compile ripgrep end-to-end — verify with
pure-rust-check. (1 session)
Deliverable: A pure-Rust Mach-O linker handling real-world dependency graphs. Ripgrep compiles and runs.
Phase 3 — Standard Rust feature coverage (8 sessions)
Make the toolchain handle arbitrary Rust crates, not just hello-world shape.
- Atomics: new LIR Tier 1 ops + arm64_lir lowering for load/store/swap/cas/fetch-add/fetch-sub. (2 sessions)
- Inline asm: arm64_lir token-stream splicing for the existing
LIROp::Asm(LIR side needs no extension). (1 session) - TLS via libSystem pthread_getspecific. (1 session)
- Drop glue,
dyn Traitvtable dispatch, closures. (2 sessions) - Full mir2lir coverage — all MIR rvalue kinds, all terminators, panic=abort lowering. (2 sessions)
Deliverable: cargo + ripgrep + serde + tokio compile and run.
Phase 4 — Extended honeycrisp / Darwin ABI surface (3 sessions)
Extend honeycrisp from compute frameworks to the minimum surface std needs. Scoped tight — no kqueue / mach ports / dispatch unless a real workload actually needs them.
- Process + filesystem: fork, posix_spawn, exit, waitpid, open, close, read, write, stat, mkdir, unlink, lseek. (1 session)
- Threading + sync: pthread_create/join,
__ulock_wait/__ulock_wakefor Mutex (post-Sonoma) withpsynch_*fallback. (1 session) - Misc: getrandom, getenv/setenv, getcwd, time. (1 session)
Deliverable: std macOS backend built entirely on honeycrisp-extended FFI. The libc crate is no longer a dependency of std for the surface std needs.
Phase 5 — compiler-builtins focused purification (2 sessions)
Port only the intrinsics actually invoked by typical Rust programs on aarch64 + x86_64 (about 20 of them). Remaining residue stays as upstream's asm/C until a real workload hits one.
- Audit which intrinsics our representative workloads (hello-world, ripgrep, tokio, serde, hyper, rsc-build itself) actually call. Port those. (1 session)
- Verify with
pure-rust-checkthat no compiler-builtins C/asm is reached by the audited workloads. (1 session)
Deliverable: compiler-builtins in our build artifacts contains no C/asm code for the workloads we test against.
Phase 6 — rsc-build (compact cargo replacement, 6 sessions)
Write a lean build tool that handles the cyber stack's needs without dragging in cargo's 150K LOC of accumulated features. ~4,700 LOC, uses pubgrub for resolution and the toml/ureq/tar/miniz_oxide crates for everything else.
- CLI + Cargo.toml reader + lock file r/w. ~1,000 LOC. (1 session)
- Dependency resolution via
pubgrub; crates.io index reader + ureq+rustls fetch + tar.gz extract. ~1,300 LOC. (1 session) - Build graph computation + parallel rustc spawning + output capture. ~1,100 LOC. (1 session)
- Test runner (fork-per-test); workspace traversal (basic single-level). ~800 LOC. (1 session)
- Error reporting, lock conflict resolution, cache directory management. ~500 LOC. (1 session)
- Verify with
pure-rust-check: rsc-build's build artifacts and self-build are 100% pure Rust. (1 session)
What it explicitly doesn't do: cargo publish/doc/bench/install/vendor/package, alternative registries, [patch]/[replace], build.rs orchestration (refuses or runs with --allow-build-script), cross-compilation, multiple profiles beyond dev/release, complex feature unification edge cases.
Deliverable: rsc-build binary, ~4,700 LOC, replaces cargo for the cyber stack. Existing crates with build.rs are rejected unless explicitly opted in.
Phase 7 — Sealed bootstrap (4 sessions)
Build the pure-Rust rustc once (using mrustc + LLVM for the initial seed), sign and cache it as stage0. All future builds bootstrap from this cached binary.
- Audit reproducibility of the build. (2 sessions)
- Define seal format + signing keys + attestation chain. (1 session)
- Publish stage0 with provenance. (1 session)
Deliverable: pure-rust-rustc-stage0 — sealed binary + provenance bundle. mrustc dependency drops out of the chain.
Phase 8 — Linux follow-on, static-only (5 sessions)
Bring up Linux as a second target. Static-only — no dynamic linker. Every binary statically links libstd; no /lib/ld-linux.so needed.
- ELF object writer (using
objectcrate) + wild integration. ~800 LOC. (1 session) - x86_64_encoders.rs + x86_64_lir.rs. ~600 LOC. (2 sessions)
- rv64_encoders.rs + rv64_lir.rs. ~410 LOC. (1 session)
- std Linux backend via rustix (already pure Rust — wiring + static-link config). (1 session)
Deliverable: Toolchain produces static ELF for x86_64-linux, aarch64-linux, riscv64-linux. No dynamic linking, no ld-rs. Dynamic support deferred until a real use case demands it.
Phase 9 — Audit, CI, certification (4 sessions)
- CI:
pure-rust-checkruns on every commit; fails on non-Rust input. (1 session) - Reproducibility: identical bytes across machines. (2 sessions)
- Public dashboard: % of popular crates that build clean. (1 session)
Deliverable: Continuous proof that the toolchain is and stays pure.
Total Estimate
| Phase | Sessions | LOC delivered |
|---|---|---|
| 0 — Inventory + audit tooling | 2 | 3,500 |
| 1 — macOS aarch64 MVP | 9 | ~3,200 |
| 2 — Full Mach-O linker | 5 | ~2,500 |
| 3 — Standard Rust coverage | 8 | ~2,200 |
| 4 — Extended Darwin ABI | 3 | 3,000 |
| 5 — compiler-builtins focused purge | 2 | 500 |
6 — rsc-build |
6 | 4,700 |
| 7 — Sealed bootstrap | 4 | 500 |
| 8 — Linux follow-on (static-only) | 5 | 1,800 |
| 9 — Audit + CI | 4 | 500 |
| Total | 48 sessions | ~22,400 LOC new + ~25,000 LOC reused from trident/honeycrisp |
48 sessions = ~144 focused hours. At one session/day: ~10 weeks solo. With phases 5, 6, 8 parallelizable to a second engineer: ~7 weeks on the critical path.
Critical path to macOS pure-Rust toolchain: Phases 0 → 1 → 2 → 3 → 4 = 27 sessions, ~5.5 weeks. After Phase 4, macOS is a pure-Rust target end-to-end. Phases 5-7 are cleanup; Phase 8 is Linux expansion; Phase 9 is continuous.
Critical path to first lit pixel (hello-world via Trident plugin, no LLVM, single Mach-O executable): Phase 1 alone = 9 sessions, ~2 weeks. This is the milestone that proves the architecture works before committing to the rest.
Post-audit reduction: original 71 sessions / 44,400 LOC → 48 sessions / 22,400 LOC. The audit cut:
- ld-rs entirely (−8K LOC, −5 sessions) — static-only Linux
- Mach-O linker right-sized using
objectcrate (−12K LOC, −3 sessions) - rsc-build vs cargo patching trade (−1.5K patch, +4.7K rewrite = net +3.2K but cleaner)
- mir2lir / rustc_codegen_trident leaner (−2K LOC)
- compiler-builtins focused on used-only (−1.5K LOC, −2 sessions)
- extended honeycrisp scoped (−2K LOC, −1 session)
- ELF/Mach-O writers using object crate (−2.4K LOC)
- No MIR serialization (plugin model) (−460 LOC, −1 session)
Hard Blockers and Open Questions
-
Mach-O linker correctness. ~3K LOC after audit. Architecture is sound (uses
objectcrate for format I/O, our work is symbol resolution + relocs + dyld binding + code signing). Mitigation: Phase 1 ships a 500-LOC monolithic single-binary Mach-O emitter that doesn't need multi-object linking — proves the format works before committing to Phase 2's full linker. Differential test againstld64output in Phase 2. -
LIR coverage of MIR semantics. mir2lir is 2K LOC of estimate post-audit. Most MIR constructs map naturally because both IRs are flat 3-address. Harder cases: drop glue across panic-edges, vtables for
dyn Trait, async state machines (already lowered by rustc, but resulting MIR is large). The estimate may need +50% buffer once Phase 3 begins; the audit can't know how many MIR constructs we'll discover. -
rustc_private brittleness. Plugin model couples us to a specific rustc version. Each upstream rustc release may break compatibility. Mitigation: pin rustc version, ship the rustc+plugin pair as one unit, gate upgrades through deliberate validation. Same cost cranelift backend pays.
-
Apple framework deprecation. libSystem ABI is stable but Apple deprecates and occasionally removes calls. Honeycrisp-extended needs a per-macOS-version compatibility matrix. Adds maintenance burden, not blocked.
-
Static-only Linux limitations. Some crates assume
dlopenexists (e.g., Wayland clients, plugin systems). These won't work under static-only. Mitigation: document the constraint loudly; addld-rslater if a real workload needs it (the ~8K-LOC effort is shelved, not lost). -
Performance. Trident's existing arm64.rs has not been benchmarked against LLVM for general Rust code (it's been validated for Goldilocks field arithmetic only). The LIR backends are new code. Expect 2-5× slower than LLVM-built binaries initially. Acceptable for a v1 toolchain; optimization is a separate workstream.
-
panic = abort divergence from upstream cargo test. Fork-per-test workaround handles the test-isolation case but doesn't replicate
catch_unwindsemantics for code that legitimately uses it. Document, accept, revisit when adding landing pads. -
rsc-buildecosystem fit. ~4.7K LOC is aggressive. Some crates may use Cargo.toml features rsc-build doesn't implement (e.g.,[patch]for local overrides, custom build profiles). Mitigation: rsc-build refuses unsupported features with clear errors; documents the supported subset; users requiring full cargo can use upstream cargo for their non-cyber-stack work. -
build.rsescape hatch. rsc-build refusesbuild.rsby default; opt-in via--allow-build-script. Some legitimate crates use build.rs for non-C reasons (e.g., generating Rust code from .proto files). The policy is conservative — reject first, allow with explicit opt-in.
What I'd Build First
Phase 0 + Phase 1: 11 sessions, ~2.5 weeks to "Rust hello-world compiled by rustc with zero LLVM, runs on Apple Silicon." That milestone validates:
- Trident as a viable rustc backend (Decision 1)
- mir2lir as a workable bridge in-process via rustc plugin (Decisions 2 + 9)
- Parallel new LIR backends as the right architecture (Decision 4 from prototype)
objectcrate carries Mach-O format I/O successfully (Decision 11 scoping)- macOS-first focus is correct (Decision 7)
- honeycrisp pattern scales to std-required surface (Decision 13)
If any of those don't hold after Phase 1, replan. The plan beyond Phase 1 is provisional.
References (verified May 2026)
- Trident IR:
~/cyber/trident/src/ir/— TIR (tir/mod.rs, 431 LOC), LIR (lir/mod.rs, 398 LOC), KIR, Tree - Trident backends:
~/cyber/trident/src/compile/— 14,331 LOC, including x86_64.rs (582), arm64.rs (612), rv64.rs (583), mir2nox.rs (717), etc. - mir-format crate:
~/cyber/rs/mir-format/— 460 LOC, Rust MIR → JSON - honeycrisp:
~/cyber/honeycrisp/— Apple Silicon hardware drivers, established FFI pattern - rustix: https://github.com/bytecodealliance/rustix — Linux raw syscalls
- wild linker: https://github.com/davidlattimore/wild — ELF, Linux only
- talc allocator: https://github.com/SFBdragon/talc — pure Rust, no_std
- gitoxide: https://github.com/Byron/gitoxide — replaces libgit2
- rustls: https://github.com/rustls/rustls — replaces openssl for TLS
- compiler-builtins: https://github.com/rust-lang/compiler-builtins — 93.3% Rust, 3.3% asm, 0.2% C
- mrustc (current bootstrap): https://github.com/thepowersgang/mrustc — used once to build stage0, then retired
- Mach-O format spec: Apple xnu source (
/EXTERNAL_HEADERS/mach-o/loader.h)