Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checked emitter and Rust build costs

This follows the Rust-source proposal. The experiment in experiments/rust-source-backend/emitter/ generates Rust from the real checked frontend. It is deliberately smaller than the ownership-rich hand-lowered probes: its purpose is to test a real emission boundary and the cost of compiling the resulting source, not to declare a production backend.

Implemented experiment

Build the developer driver with cargo build --locked --release --features rust-source-spike --example rust_source_spike --bin plenty. The hidden emission API is absent from normal builds. It loads a normal Plenty module, runs frontend and independent operation/ownership checking on the complete input, then selects explicit export roots and their reachable functions. There is no second parser, alternative Plenty type system, public backend flag, or shared-IR redesign.

AreaAdmitted slice
API/storageNative signed/unsigned integers through 64 bits, bool, unit results
ComputationChecked add/sub/mul, floor division/modulo, wrapping add/sub/mul, integer conversions, comparisons
Control flowStructured branches, scalar matches, short circuiting, while, break/continue, returns and direct calls
GenericsConcrete scalar specializations already produced by Plenty; no generated Rust generic functions
RecursionSelf-tail calls become an explicit loop with evaluated argument temporaries; other recursion is rejected
Rust cooperationExplicit checked stub → Rust companion replacement, called from a native Rust host through an exported Plenty function
DiagnosticsSource-located unsupported operations; sidecar line map demonstrated against JSON rustc type errors

Every accepted value is a Copy scalar. This avoids inventing a storage, allocation or lifetime model. References, records, enums/Results, dynamic owners, floats, generators and concurrency are rejected, not silently converted into Rust types. Input imports still use Plenty’s normal resolver; only reachable admitted functions are emitted. All input bodies are checked before pruning.

The checked Op representation is sufficient for this slice: typed slots, structured control flow, specializations and source sites survive lowering. Snapshot temporaries preserve evaluation order; scalar branch values become Rust tuple joins. Already-validated ownership-only markers have no emitted action for this Copy-only storage. That does not establish that the same approach suffices for references or destruction.

Generated Rust has no dependencies beyond the installed standard library, no unsafe code, heap marshalling, trait-object dispatch, macros or derives. Checked failures call an aborting helper, and the generated module requires panic=abort. It does not reproduce Plenty’s trap wording/status, stack guards, runtime caller traces or debugger source information. A companion is an explicit integration contract, not automatic discovery of arbitrary Rust APIs.

Measurement method

run.py checks the semantic slice and bench.py measures it. Every benchmark variant compiles a real Rust library, compiles/links a Rust host, and runs it; its exit-status checksum must match the same Plenty workload compiled through Cranelift. An edited incremental library is checked against a freshly compiled native program too. The source generator creates 1, 100 or 1,000 scalar functions, each with twelve bounded multiply/add/modulo steps, plus an aggregate loop.

The Rust library boundary prevents the host’s fixed input from being folded through the whole library. Library compilation and the tiny host’s compilation/linking are separate measurements; the latter is not a pure linker timer. --emit=metadata is another full invocation to bound checking costs, not an exact phase subtraction. No linker-only or peak-memory claim is made.

Each artifact-cold sample starts a new compiler process without incremental reuse. OS pages, the installed Rust sysroot and Plenty runtime artifacts are warm. The native baseline sets PLENTY_CACHE=off; it is not Plenty’s warm compiled-function-cache path. Explicit Rust incremental results distinguish first build, unchanged source and a real first-function body edit.

All Rust variants use edition 2024, aborting panics, no debug information, embed-bitcode=no, the installed default linker, and no cross-crate LTO, nightly options, third-party compiler cache or external crates. The rustc codegen documentation describes codegen-unit parallelism, incremental reuse and omitting unneeded embedded bitcode. The table below is measured evidence on this machine, not a promise that these choices dominate for every crate. Rustc’s default local ThinLTO across optimized codegen units remains enabled.

Observed build tradeoffs

Recorded 2026-10-24 on a Ryzen 7 7840HS, Linux x86_64, rustc 1.99.0 (b940084d7, LLVM 23.1.1), Plenty base be665bb0 plus this experiment. Three samples per cell; compiler processes were restricted to CPUs 0–7 (four physical cores). The small/flat development runs were quiet. During the large optimized/grouped runs, another worker performed validation on separate cores 4–7 (CPUs 8–15). Later validation exposed substantial tmpfs occupancy (roughly 32 GB) and a full 8 GB swap allocation; completed build artifacts were moved off tmpfs before the final measurements. Per-sample memory pressure and frequencies were not recorded for these exploratory snapshots, so shared thermals and memory contention can confound their ratios. The shape effects exceed sample spread, but that alone does not rule out changing machine load. Raw samples are flat-measurements.json and grouped-measurements.json in the experiment directory.

FunctionsNative dev compile/linkRust inline-check library, opt 0Rust helper library, opt 0Rust host compile/link after helper library
141 ms33 ms24 ms49 ms
10055 ms509 ms154 ms46 ms
1,000229 ms5,300 ms1,452 ms48 ms

The separate Plenty check/emission process adds approximately 2/13/125 ms for these helper cases. These are independently measured stages, not totals from a single timer. Generating one monomorphic helper per arithmetic operation/type reduces repeated Rust checking work and emitted source size (1,000 functions: 6.4 MB inline versus 2.8 MB with helpers). One versus sixteen codegen units did not materially help the flat development case. A separate Rust metadata-only invocation took about 1.26 s versus 1.45 s for the helper library, suggesting that much of this cost precedes native code generation. This is not an exact phase partition.

Flat optimized output exposes a much larger problem. The experiment also emits groups of 32 sorted function symbols into Rust modules, and optionally applies #[inline(never)] to source functions while leaving arithmetic helpers eligible for normal optimization. This is not a Plenty module cache. Body edits preserve these groups; inserting symbols can renumber them.

Helper emission, opt 3100 functions: library build1,000 functions: library build1,000 functions: execution
Flat, ordinary inlining1,081 ms81,920 ms4.78 ms
Groups of 32391 ms4,074 ms4.89 ms
Flat, source functions not inlined758 ms10,255 ms6.16 ms
Groups of 32, source functions not inlined337 ms2,901 ms4.60 ms

Execution includes process startup and 100 aggregate iterations. Native release took 5.27 ms in the flat snapshot; these tiny absolute times do not establish a general Rust runtime advantage. Development helper execution took 21.89 ms versus native development’s 10.45 ms. Full binary sizes, including linked runtime material, are in the snapshots; they are not generated .text sizes.

Grouping and inlining control offer a credible way to avoid pathological optimized build costs without sacrificing this workload’s runtime. The exact LLVM attribution was not profiled; these emission comparisons suggest a useful direction, but the machine conditions prevent attributing the full ratio to one change or a specific optimizer pass. Grouping made the development build slower here, so there is no single winning configuration.

For the 1,000-function development library, explicit incremental reuse reduced the flat first/unchanged/body-edit rustc times to 2,148/795/1,092 ms. Grouped output measured 1,655/656/697 ms. The edit timings are single observations, with a checksum checked against a fresh native build. Add emission and host compile/link for an application edit; these are not full rebuild times or Cargo no-op timings. Rust still reads and checks the generated crate.

Final end-to-end comparison

finalist-measurements.json adds a single timer around checking/emission, Rust library compilation and host compilation/linking, with three independent samples. After the user corrected the CPU frequency limitation, the two finalist configurations and matching native baselines were rerun in a coordinated quiet window on 2026-10-24. Sampled frequencies were 3.25–4.09 GHz. Workloads, three repetitions, 100 aggregate iterations, CPU affinity and compiler flags are unchanged. The raw snapshot records the command, source commit and environment. The initial exploratory emission-shape measurements were not rerun.

FunctionsNative devRust helper dev, end to endNative releaseRust grouped/non-inlined release, end to end
142 ms83 ms42 ms94 ms
10061 ms246 ms65 ms343 ms

The previous roughly 544 MHz results are preserved separately in finalist-throttled-measurements.json and superseded by this table; the two hardware regimes are not pooled. The fresh samples support practical subsecond builds for these small Rust libraries, while the native path remains faster. These two small synthetic workloads do not set a general build-time budget.

Recommendation: continue the Rust-hosted library experiment with compact monomorphic helpers and an explicit development/optimized emission policy. Small libraries can have practical build times, and stable function grouping can prevent very expensive optimization. This evidence does not make rustc a replacement for Cranelift’s fast build path. Native ownership-bearing API emission remains the next semantic gate; more backend breadth should not be inferred from the scalar benchmark.

Validation and limits

The differential suite covers all admitted integer widths, narrowing/widening, wrapping arithmetic, negative floor/modulo and MIN % -1, branch values, short-circuit skipped traps, loop exits, scalar pattern matching, specialized generics, unit calls and a 100,001-step self-tail recursion with swapped arguments. An independent Python oracle additionally checks every one of the 65,280 nonzero-divisor i8 pairs for floor/modulo, in native dev/release and generated Rust opt 0/3 with both helper shapes. The sole overflowing quotient is skipped there and checked separately as a fatal case; its modulo is still checked. Overflow, zero division/modulo, division overflow and checked casts fail in both native and generated debug/release builds. Exact signal numbers are not compared.

Rust → generated Plenty → Rust companion execution is checked with native integer signatures. A wrong Rust companion type produces a real rustc error mapped back to its Plenty declaration. Invalid Plenty in an unexported body still fails frontend checking; references, unsupported numeric types and unsupported recursion are rejected. A default unwinding Rust build is rejected.

These measurements cannot establish owner cleanup, native Rust borrowing, fallible allocation, collection interoperability or arbitrary crate support. They qualify a restricted compiler experiment; the broader proposal’s adoption criteria remain open. Scheduling stays in B20.