Skip to content

feat: experimental Rust MIR frontend - #1365

Open
greenhat wants to merge 9 commits into
nextfrom
rust-mir-frontend
Open

feat: experimental Rust MIR frontend#1365
greenhat wants to merge 9 commits into
nextfrom
rust-mir-frontend

Conversation

@greenhat

Copy link
Copy Markdown
Contributor

The scope of this PR is deliberately small so it is easier to review: it translates a single add function end to end and nothing more. It lays the bedrock — the crate layout, the driver, the translator skeleton, and the test approach — on which the next stages build (see frontend/rust-mir/roadmap.md).

What this adds

A new experimental crate midenc-frontend-rust-mir that reads Rust MIR and builds HIR from it directly, without the WebAssembly path. The frontend translates every local, non-generic function; every unsupported MIR construct is a hard error that names the construct. Tests evaluate the translated function with the HIR interpreter (add(1, 2) == 3), both in the raw form and after SSA recovery.

The branch also fixes a real bug in the shared Local2Reg pass, found by this frontend's output pattern: promoting chained local-to-local copies left references to erased values, and the verifier did not catch it. The fix comes with a regression test that fails without it.

Trade-offs

How the MIR is obtained. The frontend links against rustc and runs the compiler in-process through rustc_public, the stable-MIR API, behind a standalone driver. rustc stops after metadata (--emit=metadata), so the MIR stays in memory and no MIR text is parsed. The alternative — a rustc codegen-backend dylib, which would give us the compiler's own mono-item collector for whole-program discovery — was deliberately not taken: it inverts the control flow (cargo would drive midenc instead of midenc calling a frontend), couples us to the unstable CodegenBackend trait, and adds a toolchain-parity dylib to distribute. The translator consumes only rustc_public types, so this decision is revisitable at the whole-program stage without rewriting the translator; the roadmap records the decision point.

No SSA construction in the frontend. Each MIR local becomes a local slot (definitions store, uses load, no phi nodes), and the existing local2reg pass recovers SSA. This keeps the translator trivial at the cost of depending on that pass — which is what surfaced the Local2Reg bug above.

Local2Reg records (store, stored value) pairs during its initial walk and then promotes locals in ascending index order. When a promoted load feeds a store to a later local, that promotion rewrites the store operand, and the recorded value for the later local becomes a dangling reference to an erased op. The pass then emitted uses of undefined values, and the verifier did not catch them.

Read the stored value from the store op at promotion time, so the chains of slot-to-slot copies that a frontend emits under the slot strategy promote correctly.

Add a regression test that builds the two-local copy chain, applies the pass, and checks the printed IR: the chain must collapse to the entry value, and no store, load, or dangling reference may remain. The test fails without the fix, because the verifier alone does not reject the bad IR.
The Rust MIR frontend links against rustc in-process through rustc_public, which needs the compiler rlibs that the rustc-dev component provides.
Add midenc-frontend-rust-mir, a frontend that reads Rust MIR in-process through rustc_public (the stable-MIR API) and builds HIR from it directly, without the WebAssembly path. The crate is an experiment to validate that a MIR frontend can remove the losses of the Wasm path.

The translator lowers every local non-generic function with the slot strategy: one local slot per MIR local, stores for definitions, loads for uses, and no phi construction; the existing local2reg pass recovers SSA. The v1 surface covers integer and boolean types, wrapping addition, whole-local moves and copies, integer constants, and the return terminator; every other MIR construct produces an error that names the construct.

Golden tests snapshot the raw HIR and the post-local2reg HIR of a u32 add function, and a negative test checks the error for an unsupported f64 parameter. The package is private and stays out of the release surface.
…erpreter

The golden files show the shape of the translated IR, but they do not show that the IR runs. Close that gap with the HIR interpreter: evaluate the translated add function with the arguments 1 and 2 and assert the result 3, both for the raw slot form and for the SSA form that the local2reg pass produces. This proves that the slot strategy and the SSA recovery each give executable IR.
The interpreter tests already cover the raw slot form and the promoted SSA form of the translated function, and they assert behavior instead of the exact printed IR. The golden snapshots added churn on every printer or numbering change without more coverage, so remove them together with the expect-test dependency.
Document the durable facts a contributor must know to work on this crate: the development-environment changes (the rustc-dev toolchain component and the rustc_private nightly feature) and the two-part rust-analyzer setup (the rustc_private package metadata plus the editor-side rustc.source = "discover" setting), with VS Code and Neovim examples. Point to the README from the metadata comment in Cargo.toml, because the metadata alone looks sufficient but is not.
@greenhat
greenhat marked this pull request as ready for review August 31, 2026 09:50
Record the stages beyond the current smoke test: function bodies, in-crate calls, Felt and the SDK, pipeline integration, an execution proof, whole-program translation, and an honest data layout. The whole-program stage carries the driver decision point: keep the standalone driver with an own reachability walk, or adopt a codegen-backend dylib for the compiler's mono-item collector. The translator is unaffected in either case, which is why the decision can wait until that stage.
The release lint rejects private packages that inherit the workspace version: they are never published, so a version that tracks a release domain is misleading. Pin the crate at 0.1.0 like the other private packages.
CI could compile midenc-frontend-rust-mir but could not link its test binary: rust-lld did not find libLLVM. rustc passes only <sysroot>/lib/rustlib/<host>/lib to the linker, and the LLVM shared library lands there through the llvm-tools component; the copy that the rustc component ships in <sysroot>/lib is not on the search path. Local builds worked because llvm-tools happened to be installed outside the toolchain file.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Miden examples benchmark

Candidate b42abc95e2cc compared with next 332636cd2394. Lower is better.

example VM cycles (vs next) MAST size (vs next)
auth-component-no-auth n/a 6,300B (~0%)
auth-component-rpo-falcon512 n/a 12,654B (~0%)
basic-wallet n/a 7,982B (~0%)
basic-wallet-tx-script n/a 13,556B (~0%)
collatz 5,263 (~0%) 2,401B (~0%)
counter-contract n/a 13,099B (~0%)
counter-note n/a 3,197B (~0%)
fibonacci 869 (~0%) 3,219B (~0%)
is-prime 2,332,731 (~0%) 5,864B (~0%)
p2id-note n/a 17,024B (~0%)
p2id-tx-script n/a 11,455B (~0%)
p2ide-note n/a 13,103B (~0%)
storage-example n/a 15,214B (~0%)

SVG flamegraphs and compiled packages are attached to the workflow run.

@djolertrk djolertrk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice @greenhat! How structural CF is being represented at MIR level? or it supports unstructured one only?

@greenhat

Copy link
Copy Markdown
Contributor Author

Nice @greenhat! How structural CF is being represented at MIR level? or it supports unstructured one only?

pub enum TerminatorKind {
    Goto {
        target: BasicBlockIdx,
    },
    SwitchInt {
        discr: Operand,
        targets: SwitchTargets,
    },
    Resume,
    Abort,
    Return,
    Unreachable,
    Drop {
        place: Place,
        target: BasicBlockIdx,
        unwind: UnwindAction,
    },
    Call {
        func: Operand,
        args: Vec<Operand>,
        destination: Place,
        target: Option<BasicBlockIdx>,
        unwind: UnwindAction,
    },
    Assert {
        cond: Operand,
        expected: bool,
        msg: AssertMessage,
        target: BasicBlockIdx,
        unwind: UnwindAction,
    },
    InlineAsm {
        template: String,
        operands: Vec<InlineAsmOperand>,
        options: String,
        line_spans: String,
        destination: Option<BasicBlockIdx>,
        unwind: UnwindAction,
    },
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants