Skip to content

exp: in-process LLVM backend — stop shipping IR as text (Phase 0: feasible, byte-identical objects) #7241

Description

@proggeramlug

Branch: exp/llvm-inprocess · Write-up: docs/llvm-inprocess-experiment.md on that branch.

Perry ships LLVM IR as text to a user-supplied clang -c — a runtime, unpinned LLVM dependency (1.4 GB transient IR on the Claude Code bundle, #4880 whole-module -Os demotion, Apple clang 21 vs LLVM 22 parse skew, errors as line numbers in gigabyte files). This experiment builds the module pipeline in-process via the LLVM C API, exactly like the exp/stackmap-viability pattern: isolated branch, opt-in, default path untouched.

Phase 0 answer (feasibility: CONFIRMED)

  • Bindings: inkwell 0.9.0 + llvm-sys 221 for the few unwrapped calls (LLVMSetGC, LLVMParseCommandLineOptions, LLVMGetVersion). Nothing Perry's IR needs was inexpressible: NaN-box constants keep exact payload bits through f64::from_bits → const_float (verified end-to-end by running the compiled artifact), inline asm with exact constraint strings, module-level asm, appending @llvm.used, and gc "statepoint-example" all construct, verify, and emit.
  • LLVM version: 22 (llvm22-1 / Homebrew llvm 22.1.4). Picking 22 removes the version-skew problem — no second toolchain parses our output anymore.
  • Discovery: LLVM_SYS_221_PREFIX (or llvm-config on PATH) at build time.
  • Contributor cost: none by default. The llvm-inprocess cargo feature is off; the default build links no LLVM and is byte-for-byte unchanged. Enabling it needs LLVM 22 dev libs and adds ~171 MB to a static-linked perry-dev binary.

Parity evidence

  • On the same IR + flags, the in-process pipeline emits an object byte-identical to clang -c (Homebrew clang 22).
  • 77-test gap-corpus A/B through an unmodified perry (shim as PERRY_LLVM_CLANG; both caches already key on it): 76/77 outputs byte-identical; the 1 diff is a pre-existing perry-ext-http SIGABRT whose stderr embeds the OS thread id (differs run-to-run on the same arm).
  • 26-test slice through the integrated PERRY_LLVM_INPROCESS=1 backend (one binary, flag-only A/B, liveness asserted per compile): 25/26 identical, same pre-existing panic family for the remainder.
  • .ll → parse → print round-trip diff classes are all mechanical (alignment made explicit, attr-group renumbering, float formatting, headers) — the normalization catalogue for a future construction-parity harness.

What's on the branch

  • experiments/llvm-inprocess-spike/ — standalone spike: builder-API demo, clang-argv shim, corpus A/B script.
  • crates/perry-codegen/src/inprocess.rs behind the llvm-inprocess feature: compile_ll_to_object gets an in-process implementation (memory-buffer parse → verify → default<O_n> → object-to-memory; zero disk transit). It interprets the same build_clang_compile_plan argv as the clang path, so backend decisions can't drift independently; unknown flags are a hard error. PERRY_LLVM_INPROCESS participates in build-cache and object-cache keys; a featureless build fails loudly under the flag; first use prints a liveness witness line.

Honest negatives / open work

  • Compile-time win not demonstrated at small scale (245 KB–421 KB modules, loaded host): subprocess spawn isn't the bottleneck; -O3 is. The claimed wins live at bundle scale (parse + disk transit), in version control, and in what the seam unlocks (per-function opt vs Compile-time blowup on wide object literals (2100 keys ≈ 2 min, 3000 keys > 7 min) #4880, DWARF via DIBuilder, in-process RS4GC for gc: RewriteStatepointsForGC over managed-pointer SSA — the measured-only road to native-root file-size parity #7174). Those need a quiet box, ≥25 GB disk, and the gap suite green under the flag before any number is quoted.
  • The brief's instruction-level seam ("lowering ↔ IR construction") is not where this codebase's seam is: the emitter is ~340k lines of string-oriented code, and compile_ll_to_object is the boundary the code itself documents as "the seam". Recommendation in the write-up: land the transport seam opt-in, profile string-build cost through it at scale, and only then decide the construction rewrite.
  • -fno-math-errno has no C-API equivalent; measured byte-identical on macOS (Darwin defaults errno-free) — must be re-measured on Linux.
  • cl::opt state is process-global; Windows/COFF and cross-targets untested; llvm-sys/inkwell version pins are ours to maintain.

Next steps

  1. Gap suite (full) under PERRY_LLVM_INPROCESS=1, macOS + Linux.
  2. Claude Code bundle compile through the flag on a quiet box: fill the measurement table (wall/user, peak RSS, transient disk, binary size/sections, runtime + RSS vs pinned Node).
  3. Coordinate with gc: RewriteStatepointsForGC over managed-pointer SSA — the measured-only road to native-root file-size parity #7174 before scheduling RewriteStatepointsForGC in-process — that branch owns the statepoint work.
  4. Decide per-function opt levels + DIBuilder scope from the measured profile.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions