Summary
The EVM portable IR backend should not lower directly from ProofForge portable IR into low-level Yul syntax nodes. ProofForge already has a Yul AST inProofForge.Compiler.Yul.AST, and both the LCNF EmitYul path and the
portable IR EVM backend render that AST through Yul.Printer. That AST is
valuable, but it is a syntax AST: it models Yul objects, blocks, statements,
expressions, functions, and literals.
The missing layer is a target-semantic EVM plan between portable IR and that
syntax AST.
The product pipeline for the EVM target is:
Lean.Compiler.LCNF → ProofForge.Compiler.LCNF.EmitYul path is a
Lean-native experimental route that is not the product pipeline. It remains
available for research and equivalence experiments, but new EVM work, CI gates,
and documentation should follow the portable-IR → EVM-plan → Yul pipeline above.
This RFC defines the boundary and migration path for that EVM semantic plan.
It keeps the existing Yul AST as the final syntax layer, but stops treating it
as the place where ABI dispatch, storage layout, helper discovery, event ABI,
cross-call ABI, and metadata construction are all interleaved.
Motivation
The current EVM portable IR backend has grown successfully by adding validated capability coverage: ABI entrypoints, scalar and aggregate expressions, storage slots, mappings, arrays, structs, events, cross-contract calls, artifact metadata, and diagnostics. That progress also exposed a structural problem. Many distinct concerns now meet inside one lowering module:- Type checking and unsupported-capability diagnostics.
- EVM storage layout allocation.
- ABI selector dispatch and calldata guards.
- Return-data encoding.
- Event signature and topic/data encoding.
- Helper function discovery and emission.
- Cross-contract call calldata/returndata packing.
- Artifact metadata and deploy-manifest inputs.
- Low-level Yul expression and statement construction.
Current state
The current code already has a real Yul syntax AST:ProofForge.Compiler.Yul.ASTProofForge.Compiler.Yul.Printer
ProofForge.Backend.Evm.IR.lowerModule : Module -> Except LowerError Yul.ObjectProofForge.Backend.Evm.IR.renderModule : Module -> Except LowerError String
Design goals
- Preserve all existing generated Yul, bytecode, metadata, diagnostics, and smoke tests while the migration is staged.
- Make ABI, storage, helpers, events, cross-calls, and metadata first-class planned artifacts before final Yul syntax generation.
- Keep the existing
Lean.Compiler.Yul.Objectas the final syntax AST passed toYul.Printer. - Make unsupported capabilities fail during validation or semantic planning, not during late syntax rendering.
- Allow future optimizer, audit, and metadata passes to inspect the EVM plan without parsing Yul text or reverse-engineering generic Yul statements.
- Keep the design target-specific. This is not a new global IR replacing the portable IR.
Non-goals
- This RFC does not replace
ProofForge.Compiler.Yul.AST. - It does not introduce a second portable IR.
- It does not require changing the source-facing contract language.
- It does not require a one-shot rewrite of
ProofForge.Backend.Evm.IR. - It does not define Solana, Wasm, Move, or Psy backend plan structures. Those targets may choose different target-plan layers.
Proposed module shape
The EVM backend should be split toward this shape:IR.lean can remain as the public entrypoint while these modules
are introduced. During migration, IR.lowerModule should eventually become:
renderModule can remain:
EVM plan model
The plan should describe the target contract in EVM terms, not generic Yul terms. A sketch:mapValueSlot says “this
is an EVM mapping slot path”; it does not say “call
__proof_forge_map_slot twice.” The ToYul pass decides which helper calls or
inline Yul forms produce that slot.
Planned boundaries
Validation
Validation should own:- Type consistency.
- Target-specific supported/unsupported capability checks.
- Explicit diagnostics for unsupported shapes.
- ABI-facing type restrictions.
- Storage path validity.
Semantic lowering
TheLower pass should own:
- Assigning storage layout.
- Turning portable effects into EVM plan statements.
- Resolving helper requirements.
- Building entrypoint plans with selectors, calldata guards, and return plans.
- Building event and crosscall plans.
- Recording capability ids and metadata inputs.
Yul generation
TheToYul pass should own:
- Turning plan statements into
Lean.Compiler.Yul.Statement. - Emitting helper functions requested by
HelperSet. - Emitting dispatcher
switch. - Emitting memory layout for calldata, returndata, events, hashes, and calls.
- Producing the final
Lean.Compiler.Yul.Object.
ToYul, it is assumed to be valid for EVM.
Metadata
The metadata pass should consumeModulePlan, not re-discover facts from
rendered Yul. This matters for:
abi.entrypointsabi.events- capability lists
- constructor metadata
- bytecode/Yul hashes
- deploy manifest fields
Why this is better than direct portable IR to Yul AST
The existing low-level Yul AST is still necessary, but it is too low-level for backend architecture. A semantic EVM plan gives ProofForge:- A stable review surface for EVM semantics.
- A place to test storage layout and ABI plans before printing Yul.
- A clean point for artifact metadata generation.
- A clean point for helper discovery.
- A future optimizer surface that can reason about EVM concepts instead of raw Yul syntax.
- A clearer path to equivalence checks between the older SDK/LCNF EVM path and the portable IR EVM path.
Migration plan
The migration should be staged and behavior-preserving.Stage 1: Introduce plan data structures
AddProofForge.Backend.Evm.Plan with the semantic structures, plus
constructors for the first narrow surface:
- scalar values
- storage scalar read/write
- map value/presence slots
- entrypoint selector metadata
- helper requirements
Stage 2: Move storage layout planning
Move slot assignment and storage-path planning out ofIR.lean into
Plan/Lower. The first acceptance target should be map and scalar storage
because they already have strong golden Yul and raw Foundry slot validation.
Stage 3: Move entrypoint and ABI planning
Represent dispatcher cases, calldata guards, return encoders, and structuredabi.entrypoints metadata from the plan. The existing ABI scalar and
aggregate smokes should stay byte-for-byte stable unless a deliberate printer
change is made.
Stage 4: Move helper discovery
Replace scattered helper accumulation withHelperSet in the plan. ToYul
should emit helpers deterministically from this set.
Stage 5: Move events and crosscalls
Represent event signatures, topic/data field layouts, and crosscall ABI packing as plan nodes before lowering to Yul. These are the most complex surfaces and should move only after storage and ABI entrypoints prove the pattern.Stage 6: Add plan-level tests
Add tests that inspectModulePlan directly for:
- storage slot formulas
- selected helpers
- entrypoint selector and ABI word counts
- event signatures and topic encodings
- unsupported diagnostics
Acceptance criteria
A migration slice is accepted only when:- Existing golden Yul remains reproducible or the diff is intentional and reviewed.
solc --strict-assemblystill accepts the generated Yul.- The matching Foundry smoke still validates runtime behavior.
proof-forge-artifact.jsonmetadata still validates.- Diagnostics remain explicit for unsupported nodes.
- The plan data can be inspected in a focused Lean test or smoke for the capability moved in that slice.
Open questions
- Should plan tests use Lean equality on structures, or render a stable
.evm-plan.jsonsnapshot for review? - Should
HelperSetbe an inductive set with deterministic ordering, or a computed summary from plan traversal? - Should storage layout be computed before all type validation, or only after validation produces a typed module?
- How much of the existing LCNF
EmitYulpath should share the same semantic EVM plan, if any?
Initial implementation recommendation
Start with storage planning, not ABI or events. The first concrete slice should be:- Add
ProofForge.Backend.Evm.Plan. - Model
StorageLayout,StorageSlotPlan, andHelperSet. - Lower scalar storage and map storage paths into plan nodes.
- Convert those plan nodes to the existing Yul AST without changing generated Yul.
- Add a focused plan test for nested map value and presence slots.
- Keep
just evm-smoke map,just evm-smoke typed-map,just evm-diagnostics, andjust evm-coverageas the validation gate.