Imported from haydenflinner/rustc_codegen_pliron (
AGENTS.md). Install upstream withnpx skills add haydenflinner/rustc_codegen_pliron. Copyright stays with the author.
Agent instructions
Layout
rustc_codegen_pliron is a rustc codegen backend:
rustc MIR → rustc_codegen_ssa → pliron LLVM dialect → Cranelift →
cranelift-object (+ rsasm for asm!/global_asm!) → system linker.
pliron-clif-opt/— workspace crate holding all Cranelift-IR passes (&mut Functionpeepholes, loops, DSE, ...). Stable Rust, norustc_private—cargo test -p pliron-clif-optruns pass unit tests without building the dylib. Re-exported viapub use pliron_clif_opt::*socrate::<pass>paths still work insrc/.pliron-ir-opt/— pliron-LLVM-dialect passes (inline, sroa, memcpyopt, spec, nnret, nocapture, nowrite, phisimp, instcombine, retarg, constload, domcheck),state::State(the side tables lowering reads),types.rslayout helpers,has_body/address_taken. Stable Rust. The bottom-up function summaries (nowrite, nocapture, nnret, retarg, nounwind, willreturn) are Datalog rules in anascent!block:runwalks the bodies once to fill input relations, the rules derive the "bad" side (writes, captured, may be null, may unwind, may diverge) as a least fixpoint, and the table is everything not derived. Add a rule rather than another loop.pliron-clif-check/—clif-checkbinary: replays and reduces differential-checker reports without rustc (see "Miscompile hunting").pliron-cfg/— one-shotPLIRON_*env parse shared by both opt crates (PassConfig,env_*,pass_enabled,bisect).src/lower.rs— object lowering;internal_fnsselectsCallConv::Tailfor local functionssrc/eh/mod.rs—.eh_frame/ LSDA emission; Mach-O needs__TEXT,__gcc_except_tab,ARM64_RELOC_POINTER_TO_GOTpersonality relocs, and extern symbol refs (ld64 reads CFI relocs by symbol index)src/objmerge.rs— splices rsasm output into the dest object; rsasm emits the destination's binary format, so flags/relocs pass through verbatimsrc/asm.rs— emits ELF- or Mach-O-flavored assembly per targetsrc/hot.rs— PLIRON_HOT thunks, x86-64 ELF asm onlysrc/intrinsic.rs—black_boxis a real barrier: it emits a call to a never-defined__pliron_bbv_<ty>(values) /__pliron_bbm(Refs and ZSTs) viacall_bb, which flags theCallInfo;lower.rsrewrites those calls into the CLIFblack_boxop (vendored Cranelift patch 22 —can_store+other_side_effects, i.e. LLVM'sr,~{memory}asm, and each ISA lowers it to zero instructions). The bodiless callee keeps pliron's own IR passes opaque (nowrite/nocapture/retarg are all conservative), so the IR-level call is never folded away before it can become the CLIF op. wasm has no CLIF path, so it keeps the alloca + volatile-load fallback — do not "simplify"black_boxback to a pass-through; cg_clif's FIXME is not a model.vendor/cranelift-codegen— patched upstream tree; every local change is markedPLIRON:in source and numbered inPLIRON_PATCH.md.
Build / test
Pinned toolchain: rust-toolchain.toml (nightly-2026-10-06, needs
rustc-dev, rust-src, llvm-tools-preview).
cargo build→target/debug/librustc_codegen_pliron.{so,dylib}; incremental after apliron-clif-opt/srcedit is ~2scargo check|test -p pliron-clif-opt(or-p pliron-ir-opt) — the opt crates build on stable Rust: sub-second typecheck, pass unit tests with no dylib link and no rustc./test.sh— smoke tests (nostd, std, unwind, asm, proc-macro, wasm if node is installed);--sysrootalso rebuilds std via-Zbuild-std;--moldcross-links the std test to*-linux-muslwith mold (PATH or a siblingmoldcheckout built withcargo build --release) and runs it under docker/qemu when available — the pure-Rust link path, since mold is ELF-only (no Mach-O)harness/run.py [--tier 0|1] [--accept]— runs suites (smoke with stock-rustc output diff, object determinism, ui) and fails on regressions againstharness/expectations/; plan and status intest-harness.mdbench/run.sh [criterion args]— builds thepliron-benchcriterion target (bench/benches/kernels.rs) with stock rustc and with the pliron backend (cargo rustc --bench, flag scoped to the bench crate), saves the LLVM run as thellvmbaseline, then prints per-kernel significance stats. Args forward to both criterion binaries (a name filter selects a subset). Results table:docs/performance.md.bench/wasm/run.sh [name-filter]— wasm leg: compiles the same kernels (#[path]-included intobench/wasm/lib.rs, cdylib) to wasm32-unknown-unknown twice — stock rustc + wasm-ld, and the pliron backend +tools/pliron-wasm-ldwith-Zbuild-std— then times both modules in node (bench/wasm/run.mjs). Criterion can't run on wasm (it re-execs the bench binary per benchmark), so run.mjs does its own warmup/calibration/sampling and prints the same kernel/LLVM/pliron/ change table.- Pushing: this is a personal fork that may never be upstreamed — commit
and push completed work freely; no confirmation or personal-diff-review
protocol applies. Do not add
Co-Authored-Bytrailers. - Disk cleanup: worktree source dirs are small — the weight is each
worktree's
target/. Reclaim space withcargo clean(or removetarget/) inside a worktree rather thangit worktree remove, which permanently loses uncommitted changes. tests/ui_run_pass.py [RUST_CHECKOUT] [FILTER]— runs directive-free//@ run-passtests from a rustc checkout (defaults:$RUST_CHECKOUT, then arustdir next to this repo, else~/work/rust)
Miscompile hunting
PLIRON_DIFFCHECK=1runs each function's CLIF before and after the pipeline incranelift-interpreteron random inputs (pointer params get sandboxed memory; calls become recorded deterministic stubs) and compares returns, calls and memory writes. On a mismatch it replays step by step to name the step and savesbefore.clif/after.clif/report.txtunderPLIRON_DIFFCHECK_DIR(defaultpliron-diffcheck/).=stepchecks every step. Also_FN=<substr>,_TRIALS,_FUEL,_SEED,_PANIC=1,_VERBOSE=1(one summary line per function). Code:pliron-clif-opt/src/diffcheck.rs; tests:pliron-clif-opt/tests/. Traps and anything the sandbox can't model are skipped, never reported.cargo run --release -p pliron-clif-check -- step <step> before.clifreplays a report in about a second (pipeline,pair,corpus <dir>,steps;--printshows both functions). Reduce a repro by deleting instructions while it still printsMISMATCH, then add it as a test.harness/bisect.py -- <cmd>binary-searchesPLIRON_OPT_BISECTfor the first bad step application when a program misbehaves at run time, and prints the step and function it touched.PLIRON_OPT_BISECTunit numbers race across parallel codegen units, so the reported step/fn is only deterministic with-C codegen-units=1— always bisect under it.cranelift-interpreteris vendored with small fixes; seevendor/cranelift-interpreter/PLIRON_PATCH.md.
MSVC-free Windows links (PLIRON_CRTFREE)
PLIRON_CRTFREE=1 makes the backend emit the static-CRT pieces
msvcrt/vcruntime normally supply — mainCRTStartup/wmainCRTStartup/
WinMainCRTStartup/wWinMainCRTStartup (exec) or _DllMainCRTStartup
(dylib), _tls_used+_tls_start/_tls_end/_tls_index (the PE TLS
directory — _tls_start is a plain byte in unsuffixed .tls, the
bookkeeping object is first input so it anchors the template),
_pliron_tls_cbs, _fltused, the type_info vftable slot, and
__security_cookie/__security_check_cookie/__GSHandlerCheck (GS is a
stub: check-cookie compares against the sentinel, the SEH handler returns
ExceptionContinueSearch). Every emitted object gets a .drectve with
/DEFAULTLIB:ucrtbase /DEFAULTLIB:vcruntime140.
Combined with synthesized import libraries, this links with zero MSVC/WinSDK files:
uv run --no-project tools/mkwinlibs/mkwinlibs.py <OUTDIR>
PLIRON_CRTFREE=1 rustc -O foo.rs -Zcodegen-backend=.../rustc_codegen_pliron.dll -Clinker=rust-lld -Lnative=<OUTDIR>
tools/mkwinlibs/mkwinlibs.py dumps System32 DLL export tables via
llvm-readobj --coff-exports and builds .lib files with
rust-lld -flavor link -def: — kernel32/ntdll/userenv/ws2_32/dbghelp/
msvcrt/ucrtbase/vcruntime140/ole32/advapi32/shell32/user32/bcrypt/
bcryptprimitives plus synchronization.lib (api-set forwarders). Names
unspellable in .def (C++ operators, =X.#N ordinal forwards) are skipped —
rustc objects never import them. Verified end-to-end: the criterion bench
builds and runs with zero VS inputs.
Platform notes
-
windows-msvc (x86_64):
is_pic=false, Cranelifttls_model="coff".src/eh/mod.rsemits.pdata/.xdata(serializedUNWIND_INFO+RUNTIME_FUNCTION,RelocationKind::ImageOffset→ADDR32NB) instead of.eh_frame. Unwinding is emulated like the wasm backend: TLS__pliron_ehstate word, flag checks after calls,pliron.eh.exn/rethrow, and_CxxThrowException/_Unwind_Resumeinterception. Catch works when std + panic_unwind are also backend-built (-Zbuild-std, run viatest.sh --sysroot); with stock std, real SEH throws unwind through our frames (correct.pdata) but ourcatch_unwindcan't intercept them —tests/std/main.rsgates those asserts on--cfg pliron_stock_seh.MSVC i128/f128 libcall ABI: args byval through pointers, return in xmm0 —
libcall()in lower.rs adapts (__udivti3,__floattisf,__rust_i128_*). rsasm COFF:.linkonce,.seh_*,3f@IMGRELlocal-label modifiers; LLVM$$escapes become$(AT&T) or nothing (Intel syntax). Compiler-stub symbols (__pliron_llvm_*) get per-CGU-unique names on COFF (no weak syms). -
aarch64:
CallConv::Tailcan't take sret params; lower.rs keeps sret functions on the platform ABI.ArgumentPurpose::StructArgument(byval) is unsupported by cranelift-aarch64, but rustc never emits on-stack indirect args for AAPCS64. -
The asm test covers x86-64 and aarch64; PLIRON_HOT is x86-64/ELF only.
-
Cross-target codegen: cranelift-codegen is built
host-arch-only (Cargo.toml), so the dylib can't emit other architectures —error: cranelift: Support for this target is disabled. Flip toall-archto cross-compile (x86_64-apple-darwin links via ld64 and runs under Rosetta; x86_64-macho__eh_frameuses SUBTRACTOR+UNSIGNED pairs, see src/eh/mod.rs). Still needs the target std component and a target linker (mold for ELF). -
Self-hosting:
selfhost/setup.sh [RUST_CHECKOUT] [WORKTREE]creates a worktree at the pinned nightly commit, appliesbootstrap.patch, and rsyncs this repo intocompiler/rustc_codegen_pliron. Defaults: siblingrust/rust-shdirs (or~/work/rust/~/work/rust-shon the Linux box). On Linux it appends a[target.*] linker = cc-wildstanza; on macOS the systemldis used.selfhost/bench.shis Linux-only. -
selfhost/verify.sh [WORKTREE] [SUITE]— run a compiletest suite under the pliron-built stage2 rustc and the LLVM-built stage1 baseline, then diff failures: stage2-only entries are pliron regression candidates. Handles Darwin'srust-objcopyPATH requirement and watchdog-kills rustc stuck idle (miscompiled-rustc deadlocks). Run it after optimizer changes that touch stage2 codegen.
Optimizer complexity rules
Stage2 rustc functions are large enough that any O(item × fn) pass becomes a multi-minute blowup. Invariants that keep passes linear:
- Never call a
layout.blocks()/inst_valueswhole-function scan (or CFG/domtree rebuild) inside a per-inst/per-edge/per-candidate loop. "Who usesv" isuses::Uses(sites/by_block/is_used/escapes/in_block,Uses::buildshares the walk with extra per-use tables); mutation-aware loops use incremental count maps — seememspec,reap_dead,rebind_exports. resolve_aliases/value_defwalk alias chains linearly; passes that re-alias (merges,change_to_alias) extend them.compact_aliasesruns after every mutating step — new passes should still prefer resolved lookups batched over per-use resolves.- Alias chains can dead-end at reserved/tombstoned targets;
resolve_aliases/value_defpanic on them (unchecked index). Iteratingdfg.values()or consuming stale pairs must usetry_resolve_aliases/try_value_def. - Fixpoint loops need an explicit progress bound or budget
(
MAX_ACC_WALK,MAX_OPS,MAX_REGION_EXTRA,seensets) — a conservative bail leaves the function unoptimized, never wrong. They must also converge incrementally: seed a worklist and update counts/reachability per insertion (closed_region'spendingpred-counts) — never rescan the whole function per round. - Overlapping per-item walks are one walk: N forward cones through the
same blocks = one backward cone from the interesting nodes
(
closed_region's leak check); K nested subgraph queries = one memoized walk with a shared budget (AccMemo).
Fast iteration / debugging knobs
./test.sh --only a,b,c— run a subset (names in the test.sh header: nostd, std, bcheck, loopvec, ...). The script alwayscargo builds first, but incremental rebuild is ~2s.PLIRON_*env gates flip passes without rebuilding the dylib:PLIRON_<PASS>=0disables,PLIRON_OPT_BISECT=Ntruncates the pass budget,PLIRON_OPT_BISECT_DEBUG=1logs gate decisions,PLIRON_PIPELINE=a,bselects/orders pipeline steps,PLIRON_PAR=0serializes codegen (also forced by OPT_BISECT).PLIRON_INSTCOMBINE=eggruns the legacy e-graph instcombine for A/B (default is the worklist peephole, ~50x faster with identical folds).PLIRON_STATS=1— per-pass rewrite counts per function.PLIRON_FACTS=<substr>— print what nowrite/nocapture/nnret/retarg/ nounwind inferred for functions whose symbol contains the substring (./test.sh --only factschecks a few againsttests/facts/main.rs).PLIRON_TIMING=1— per-CGU per-step wall time, sorted (stderr).PLIRON_DSE_PROF=1— per-functiondsephase timing on stderr: param-src, isolated-scan, op decode, fixpoint pops/time, kill sweep.-Zself-profilerecords the backend's work aspliron_*generic activities (pliron_iropt,pliron_ir_<pass>,pliron_pipeline/pliron_compileper fn, one event per pipeline step name) — the same clock and viewer as rustc's ownLLVM_*activities.PLIRON_TRACE=<env-filter>— the one logging switchboard. All diagnostics aretracingevents/spans onpliron::<module>targets (bare messages on stderr — no level/target/time prefix, so stats, facts, and timing lines stay verbatim-greppable). Span targets:pliron::module/pliron::phase(per-CGU phases),pliron::iropt,pliron::fn,pliron::pipeline,pliron::step(every pipeline step, span name = step name); event targets follow the pass file stem (pliron::jumpthread,pliron::slp,pliron::guardelim, ...) plus report channelspliron::stats/pliron::timing/pliron::facts/pliron::dump.plironenables all of them. (EnvFilter's[{field=x}]syntax only gates events inside a matching span — it cannot suppress a span's own close line.) Unset = no subscriber installed; each site costs one atomic load. The legacyPLIRON_*diagnostic knobs still work:tracing_init(src/lib.rs) maps each set knob to a target+level directive inKNOB_TARGETS— e.g.PLIRON_JT_PROF=1→pliron::jumpthread=info,PLIRON_XPACK_DEBUG=1→pliron::slp=debug,PLIRON_UNROLL_DEBUG=3→pliron::unroll=trace. Knob-gated prints becametracing::debug!/info!andif env_set(...)/var_os(...)print gates becametracing::enabled!(target: ..., Level::..). Keep it that way: noeprintln!for diagnostics —eprintln!only for output that must appear with no subscriber (unknown-step errors, panic paths, diffcheck reports). Behavioral knobs (PLIRON_XPACK_NOCACHE,PLIRON_WASM_NOSHL,PLIRON_PIPEFN, substring filters likePLIRON_CLIF=<substr>) stayenv_*reads. For sub-phase timing inside a pass, prefer an enteredtracing::info_span!(target: "pliron::<pass>", "name", field = ...)over a newPLIRON_*_PROFenv var — close lines render nested and parse with the same scraper. Example:guardelim.rsgefn/gefix/geprove.samply record --save-only -o prof.json -- cargo rustc ...— sampling profile of the whole build (rustc + the dylib frames symbolize);samply load prof.jsonopens Firefox Profiler. Use this first for "where is time going" before adding any instrumentation.PLIRON_STEPCACHE=1— content-addressed pipeline cache in$PLIRON_CACHE_DIR(default~/.cache/pliron-steps). Two levels per function: a whole-pipeline entry (warm reruns are ~50x faster) and per-step entries for the expensive passes (Step.cached), so a gate flip (PLIRON_X=0) invalidates only what it actually changed — repeat bisect runs at the same config hit fully. Keys include the crate build hash, so a backend edit cold-misses everything.PLIRON_CACHE_LOG=1prints hit/miss per step. Caveats: cold runs pay ~1.3x for hashing+storing; hits replay results rather than re-executing, so in-passtracing::debug!prints don't fire on hits (aggregatedPLIRON_STATScounters replay exactly).PLIRON_CLIF=<substr>— dump CLIF after every step for functions whose name contains the substring;PLIRON_VERIFY=fullruns the cranelift verifier after each step; any other truthy value verifies once after the pipeline, then retries per-step on failure for attribution.- Wasm always goes via FnLower + the clif-opt pipeline then
CLIF→waffle (
src/wasm.rs::clif_emit); the older dialect→waffle path was removed (PLIRON_WASM_CLIF=0now just warns it is ignored).PLIRON_WASM_STUBLOG=<file>writes stub reasons per function;PLIRON_WASM_VERBOSE=1/PLIRON_WASM_PANIC=1list stubs / unmute panic locations;PLIRON_WASM_DUMP=<substr>dumps waffle bodies. - Quick single-file check (no test.sh):
rustc +nightly-2026-10-06 -Zcodegen-backend=$PWD/target/debug/librustc_codegen_pliron.dylib file.rs -o out - Gotcha: build-level caching does not track
PLIRON_*env or the backend dylib. Empirically-Cincrementalre-runs our backend every invocation (work-product.os are stored but codegen queries rerun), so rustc-incremental stale reuse is not the risk — cargo's own fingerprinting is: unchangedRUSTFLAGSmeans aPLIRON_*flip never re-invokes rustc. When bisecting undercargo build/-Zbuild-std,cargo clean -p <pkg>or use a fresh target dir after gate changes. - Dev profile: our crates at
opt-level=1, deps at O3, vendoredcranelift-codegenat O3 (flip to 1 for cheap rebuilds while patching ISLE/-meta — package-profile overrides can't come from the env, use--config 'profile.dev.package.cranelift-codegen.opt-level=1'),debug="line-tables-only"(revert for lldb variable inspection).
