Skip to content

Optimization: type-aware recv codegen, zero-copy views, #[abi] structs#1451

Draft
micahscopes wants to merge 23 commits into
masterfrom
optimization-consolidated
Draft

Optimization: type-aware recv codegen, zero-copy views, #[abi] structs#1451
micahscopes wants to merge 23 commits into
masterfrom
optimization-consolidated

Conversation

@micahscopes

Copy link
Copy Markdown
Collaborator

Summary

Gas optimizations for the ABI decode path and contract entry points. The builder now uses CTFE to query trait constants and emit specialized code per msg type.

These optimizations will need a total refactor once compile-time reflection + builder rework land (stay tuned).

Benchmark results (rosetta-fe, O2):

Contract Function Before After Delta vs Solidity
Math mulDiv_simple 6483 5682 -12.4% -0.3% (beats)
AMM getK 9894 9658 -2.4% -0.7% (beats)
Merkle computeRoot 21602 10513 -51.3% +3.9%
Merkle verify 38393 16099 -58.1% +7.0%
Gov execute 32856 32350 -1.5% -4.2% (beats)

Changes

  • Type-aware recv wrappers: builder queries AbiSize::HEAD_SIZE and Encode::DIRECT_ENCODE via CTFE. Static-only msg types get direct calldataload instead of malloc + decode
  • View types: ArrayView<T, N>, BytesView, StringView with default type params for zero-copy calldata access
  • Lazy field loading: per-field strategy (Direct / Decode / LazyDynamicView) based on mutability analysis
  • #[abi] attribute: auto-derives AbiSize, Encode<Sol>, Decode<Sol> for user structs
  • Binary-search dispatch: auto-selected for contracts with >3 selectors
  • Type checker fix: method generics on default-type-param structs were clobbered by the struct's default

Notes

  • Overlaps with abi decode gas optimization #1448 (ABI decode optimization) in the recv/synthetic builder area. Will need coordination on merge order
  • Const predicates prototype (where { const_expr }) included but separate from the gas work
  • This work led into an ongoing detour on effect-driven derive functionality (generating trait impls via uses capabilities), which is making good strides and will land separately

micahscopes and others added 23 commits May 15, 2026 02:06
Add #[inline(always)] to SolDecoder and SolEncoder trait impl
methods (read_word, write_word, accessors, alloc) and to core ABI
dispatch functions (decode_payload_root, decode_field, encode_root,
encode_field, etc). These are small, hot functions that the inliner
skips under its default budget cap (~24 instructions), leaving
residual overflow checks that could otherwise be eliminated by
range analysis after inlining.
Add #[dispatch(linear)] and #[dispatch(binary_search)] contract
attributes to override the selector dispatch strategy. Without an
attribute, auto-select based on selector count: linear for 3 or fewer,
binary search for 4 or more.

Linear dispatch (O(n) if-else chain) is cheaper for small contracts.
Binary search (O(log n) comparison tree) is cheaper for contracts with
many selectors. This addresses review feedback requesting the strategy
be configurable rather than hardcoded.

Adds DispatchAttr enum to HIR, DispatchStrategy to MIR, and threads
the strategy through to the synthetic body builder.
The ERC20 fixture has 14 selectors, exceeding the auto-selection
threshold (>3). The snapshot now reflects the binary-search dispatch
tree instead of the previous linear br_table.
Teach the synthetic builder to query msg_ty field types and specialize
recv wrapper code generation for static all-word-scalar parameter types.

For handlers like fn transfer(to: Address, amount: u256), the compiler
now emits direct calldataload at known offsets (4, 36, ...) instead of
the generic malloc + calldatacopy + SolDecoder + decode_fn path. For
single word-scalar return types, emit direct mstore + return(0, 32)
instead of calling encode_single_root_alloc.

Adds DirectCalldataLoad and DirectScalarReturn variants to
RuntimeInputPlan and RuntimeReturnPlan. Falls back to the generic path
for dynamic types or non-word fields.
Three correctness fixes for the recv wrapper optimization:

- Restrict DirectCalldataLoad to compiler-generated msg structs only.
  Manual MsgVariant impls may reorder fields in encode/decode, so
  direct calldataload at canonical offsets would read wrong data.

- Emit Cast from u256 to the field's actual scalar class after
  calldataload. The EVM opcode returns a raw 256-bit word but the
  runtime verifier requires matching scalar representations (e.g.
  u64 = Int{bits:64}, bool = Bool).

- Restrict DirectScalarReturn to u256/usize only. Smaller types
  and signed types don't match the mstore verifier's requirement
  of ScalarRepr::Int{bits:256, signed:false}.
Replace the is_abi_word_type() heuristic with actual queries of
AbiSize::HEAD_SIZE, AbiSize::IS_DYNAMIC, and Encode::DIRECT_ENCODE
via compile-time evaluation from the MIR layer.

Add eval_trait_assoc_const() which bridges MIR to HIR CTFE: resolves
trait impls, creates SemanticInstanceKey, and evaluates via
eval_const_instance. Three query helpers: query_head_size,
query_is_dynamic, query_direct_encode.

DirectCalldataLoad now stores per-field HEAD_SIZE values instead of
a flat field count, computing calldata offsets from accumulated sizes.
DirectScalarReturn checks DIRECT_ENCODE on the return type.

This is the "compiler reads what the type system already knows" root
cause fix: the synthetic builder now uses the same const trait values
that Fe library authors define, rather than a parallel type-structure
heuristic.
ArrayView stores a ByteInput source and the byte offset where an array
starts in ABI-encoded data. Elements are read on demand via get(index),
paying only for what you use. Generic over ByteInput, so
ArrayView<u256, 32, CallData> is a zero-copy view over calldata,
consistent with BytesView<CallData> and StringView<CallData>.

Also adds skip_bytes() to AbiDecoder and skip_field() utility for
advancing past fields without materializing them.

The event encoding optimization (46ee975) was not cherry-picked as
master already uses encode_abi_payload/encode_alloc which subsumes it.
The Decode impl for ArrayView<T, N, I> has a type mismatch: d.input()
returns D::Input but ArrayView needs I, with no way to tie them.

Add SkippedArray<T, N> which stores only start offset, impls Decode
trivially (just advances cursor), and provides with_input() to bind a
byte source later. This enables the msg-parameter pattern:

    msg Msg { proof: SkippedArray<u256, 32>, leaf: u256 }

The decoder skips the 32-element array (zero copy) and the handler
binds a CallData source: proof.with_input(CallData { base: 0 })

This is the pattern that achieved 2x Merkle improvement in ralph-loop.
Follow the SkippedArray pattern for dynamic ABI types. SkippedBytes
decodes by reading the length word and recording position without
allocating or copying data. SkippedString wraps SkippedBytes.

Handler calls with_input(CallData::new()) to get BytesView<CallData>
or StringView<CallData> for direct calldata reads.

Usage:
  msg Msg { data: SkippedBytes, name: SkippedString }
  // handler: data.with_input(calldata) -> BytesView<CallData>

Includes ABI descriptor support for correct Solidity JSON emission
and std re-exports.
Adds LazyCalldataLoad plan for message types with mixed scalar/aggregate
fields. Non-mut scalar fields load directly via calldataload at their ABI
offset, SkippedArray fields pass the calldata byte offset through, and
remaining fields go through the standard decode pipeline.

Adds FieldLoadStrategy enum (Direct/Decode/SkipWithOffset), helper
functions for per-field strategy determination, and LazyCalddataLoad
codegen in synthetic.rs.

Consolidated from lazy-access branch commits 4fb116d85, f0b6e3b50, d9b6c8b30.
Add support for where { const_expr } syntax in Fe where clauses.
Const predicates are boolean expressions evaluated via CTFE at
monomorphization time. If the expression evaluates to false,
compilation fails with a diagnostic.

Implementation spans parser (WhereConstPredicate AST node), HIR
(const_predicates field on WhereClauseId), lowering (Body for const
predicate expressions), diagnostics (ConstPredicateFailed), and
call-site CTFE evaluation in callable.rs.

Consolidated from const-predicates branch commit 1e2ceba.
Replace SkippedArray/SkippedBytes/SkippedString with unified naming
using default type parameters:

- ArrayView<T, N, I = ()>: unbound when I=(), bound when I: ByteInput
- BytesView<I = ()>: unbound when I=(), bound when I: ByteInput
- StringView<I = ()>: unbound when I=(), bound when I: ByteInput

Rename with_input() to over() for binding a byte source.
Unbound views impl Decode for use in msg fields.
Bound views have element/byte access methods.

Example: msg { proof: ArrayView<u256, 32> }
Handler: let view = proof.over(CallData::with_base(4))
Extend lazy access builder to automatically generate zero-copy decode
for non-mut BytesView and StringView parameters. The builder reads the
dynamic offset pointer and length word directly from calldata, constructs
the view without malloc/calddatacopy/SolDecoder.

Add FieldLoadStrategy::LazyDynamicView for dynamic view types. Make
LazyCalldataLoad::decode_fn optional (None when all fields are
Direct/SkipWithOffset/LazyDynamicView, completely eliminating the
decode pipeline).

Codegen: push_dynamic_view_from_calldata() reads head offset, computes
tail position, reads length, constructs BytesView/StringView aggregate
directly from calldata values.

4 new tests, 71 total pass.
Add #[abi] attribute that auto-generates AbiSize, Encode<Sol>, and
Decode<Sol> impls for user-defined structs, enabling them as msg field
types mapped to Solidity tuple encoding.

All fields are encoded regardless of visibility (same as serde).
Generic structs are rejected. Follows the #[error] struct pattern,
reusing msg.rs helpers for impl generation.

Example:
  #[abi]
  pub struct G1Point { pub x: u256, pub y: u256 }

  msg Msg {
    #[selector = sol("verify((uint256,uint256),uint256)")]
    Verify { point: G1Point, value: u256 } -> bool,
  }

New file: crates/hir/src/core/lower/abi_struct.rs (363 lines)
10 files modified for wiring, diagnostics, and analysis pass.
All tests pass (fe-hir, fe-mir, fe, uitest).
StringView<()>::over() was calling self.bytes.over(input) which
triggered a Fe type inference bug where the method-level generic J
was unified with the impl's () type parameter. Bypass by constructing
BytesView<J> directly via from_parts. Also remove stale .snap.new
files superseded by accepted snapshot updates.
When a struct has more type params than the impl block
(e.g. ArrayView<T, N, I=()> with impl<T, N> ArrayView<T, N>),
the extra receiver args were applied to the method's own generics,
clobbering them with the struct's default value.

Fix: only apply parent impl's param count of args from the receiver.
The method's own generics get fresh type variables as intended.

Fixes: ArrayView.over(), BytesView.over(), StringView.over()
@sbillig

sbillig commented May 17, 2026

Copy link
Copy Markdown
Collaborator

@micahscopes following up from your comment on #1448:

I'm working on similar stuff (seemingly complementary) in #1451. I think we should try and get this one here merged first since I'm waiting on the Reflect effect derive stuff to rework that branch.

Would it be useful/feasible to merge some of the things on this branch before the Reflect derive stuff is ready? ABI encode/decode is the main gas issue right now, so it would be nice to land any improvements we can.

@micahscopes

micahscopes commented May 17, 2026

Copy link
Copy Markdown
Collaborator Author

@sbillig I'm not so sure if it makes sense to split it up. We could pull out some of the view types but the parts that make those most useful (automatically) for abi encode/decode are fairly wrapped up in the ctfe code. That said, it's possible that we could get this whole thing in now and overhaul as soon as the new reflection-based derive is done, but I'm not sure that's worth it, the derive stuff is coming along really nicely.

(I'm currently consolidating the new codegen to be based around the desugaring and hir builder stuff used in the contract lowering and this is going going very well)

@micahscopes

Copy link
Copy Markdown
Collaborator Author

C.c. @cburgdorf if you feel up for it take a look here?

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