The Fe team is happy to announce the release of Fe 26.2.0!
This release is focused on gas and bytecode-size optimizations, along with some language, standard library, and tooling improvements. Highlights are below; the full changelog is here: v26.2.0
The motivating test cases for many of the optimizations are Fe ports of the Ethereum beacon deposit contract and the Uniswap v3 core contracts. Gas and bytecode size are quite a bit better than in previous releases. The deposit contract can be seen here: deposit_contract.fe. Gas and bytecode size comparisons with the Solidity implementation are below (fe -O2 vs solc --via-IR).
bytecode size fe solc fe vs solc
-----------------------------------------------
init 2562 3082 -520 (-16.9%)
runtime 2478 2844 -366 (-12.9%)
call gas fe solc fe vs solc
------------------------------------------------------------
deposit#0 79719 81089 -1370 (-1.7%)
deposit#1 65088 66629 -1541 (-2.3%)
deposit#2 45507 46877 -1370 (-2.9%)
get_deposit_root(after#2) 102985 109170 -6185 (-5.7%)
get_deposit_count(after#2) 23616 24099 -483 (-2.0%)Immutable contract fields
Fe now supports Solidity-style immutable contract fields. These are set during contract
initialization and never change during the lifetime of the contract. The values are appended to the end of the runtime bytecode, and read at runtime with a CODECOPY operation, which is much cheaper than storage access.
pub contract Token uses (ctx: Ctx) {
// Immutable: written once during init, embedded in the contract code
owner: Address
// Mutable: lives in storage
mut store: TokenStore
init(initial_supply: u256) uses (ctx, mut owner, mut store) {
owner = ctx.caller()
store.total_supply = initial_supply
store.balances.set(key: ctx.caller(), value: initial_supply)
}
recv TokenMsg {
Owner -> Address uses (owner) {
owner
}
TotalSupply -> u256 uses (store) {
store.total_supply
}
BalanceOf { account } -> u256 uses (store) {
store.balances.get(key: account)
}
}
}
Note that an immutable field is bound as a mut effect in the init block (uses (mut owner)).
Immutable fields must be initialized in init; failure to do so is a compile-time error.
assert! built-in
Fe now has an assert! built-in pseudo-macro:
fn validate_pubkey_len(pubkey_len: usize) {
assert!(pubkey_len == 48, "invalid pubkey length")
}
assert!(condition) reverts with Solidity-compatible Panic(0x01) when the
condition is false. assert!(condition, "message") reverts with
Solidity-compatible Error(string) data, matching the shape tools already
understand from Solidity's require.
This functionality was previously implemented in the standard library as assert and
assert_msg functions; however, calling a normal function always evaluates its arguments
and this led to unnecessary gas usage on the non-error path. Making this a compiler built-in
allows us to build the error string on the error path. In Rust, this would be accomplished with
a macro; Fe doesn't (yet?) have macros.
assert! also works in const fn: if an assertion fails at compile time, the compiler
reports the failing assertion and includes the message when one was provided.
Associated const items in impl blocks
Inherent impl blocks can now define associated const items.
pub struct Packed<const BITS: u256> {}
impl<const BITS: u256> Packed<BITS> {
const LANES: u256 = 256 / BITS
const LANE_MASK: u256 = (1 << BITS) - 1
pub fn lanes(self) -> u256 {
Self::LANES
}
}
Inherent consts take precedence over trait consts of the same name,
with the trait const still reachable via a qualified path like
<Foo as SomeTrait>::X.
Optimization levels
The fe CLI now defaults to the -O1 optimization level. This gives
considerably faster compile times for large projects, with little loss in
bytecode quality relative to -O2.
Previously, -O1 and -O2 were effectively the same. Now -O1 uses a cheaper
stack-shuffling search, while still running the full optimization pipeline.
This gets close to -O2 gas and bytecode size while substantially reducing
compile time for large projects. -O2 keeps the more expensive exact stack-shuffling
search. -O0 is now much faster because it skips the main optimizer and uses a greedy
heuristic for stack shuffling.
Machine EVM lowering
Sonatina's EVM backend now lowers through a lower-level "machine EVM" instruction set before final bytecode emission. Fe generates high-level Sonatina IR with full type information, including struct, enum, and array types and operations. This representation is optimized, then lowered to the low-level IR, which is optimized again by the same passes.
Running the optimizer on both representations gives the compiler more room to clean up stack use, memory use, and repeated generated code after the main pipeline has already run. The bytecode and memory improvements below mostly fall out of this two-stage approach.
EVM bytecode improvements
PUSH shrinking
Large EVM constants are now emitted in shorter forms when a small runtime operation can replace a much larger literal. This is especially useful for ABI selectors and masks, which appear frequently in generated wrapper code, error paths, and integer cleanup logic.
For example, a selector stored in the high four bytes of an ABI word, previously pushed as a 32-byte literal:
PUSH32 0x4e487b7100000000000000000000000000000000000000000000000000000000
is now emitted as a short selector plus a shift:
PUSH4 0x4e487b71
PUSH1 0xe0
SHL
A similar transformation applies to low-bit masks:
PUSH20 0xffffffffffffffffffffffffffffffffffffffff
becomes:
PUSH0
NOT
PUSH1 0x60
SHR
These transformations trade a little runtime gas for reduced bytecode size, and
are currently applied at all optimization levels. In a future release this will
depend on the optimization level, with -Os most aggressively saving bytes and
-O2 sacrificing bytes for runtime gas.
Code and data deduplication
Sonatina now deduplicates function bodies that lower to identical EVM bytecode after optimization. Previously, two functions were only considered the same when their Sonatina IR was identical, which left some bloat when distinct functions optimized down to the same bytecode. The backend also removes repeated constant-only helper parameters when every private call passes the same value, and deduplicates terminal return/revert code, which shrinks error-handling paths.
Memory optimizations
Several improvements focus on temporary memory:
- Private temporary buffers can be placed in fixed scratch slots when their lifetimes don't overlap.
- Return and revert payloads can be built at fixed addresses instead of going through the heap allocator.
- Known free-pointer bounds are reused across internal calls, avoiding redundant allocator setup.
- Unnecessary overflow checks are dropped for offsets inside statically sized EVM allocations.
This matters most in code with many generated helpers, ABI paths, and error
payloads. In the checked-in differential deposit-contract benchmark, Fe's deposit
calls are now slightly cheaper than the best Solidity variant in the comparison
run, and get_deposit_root is noticeably cheaper.
Const data and large arrays
Fe 26.2 benefits from several improvements to const array handling in Sonatina.
Fe lowers constant aggregates as shared const references in Sonatina IR, and Sonatina decides how to materialize the const values.
Previously, a const array was naively stored in each function that used it.
Now, these are stored as a single bytecode data region, dramatically reducing
bytecode size for large lookup tables that are used in several places. Static array
access can be optimized down to a PUSH of the constant value. Some dynamic uses of const
arrays can be optimized as well. For example, [10, 20, 30, 40] indexed by i
in a loop compiles to i*10 + 10 with no const region.
Sonatina will also deduplicate const data, and coalesce CODECOPYs of individual elements
into larger region copies where it's beneficial.
ABI encode/decode efficiency
The std library implementation of Solidity-compatible calldata ABI encoding/decoding
is now much more efficient. On the receive-side, we now avoid redundant memory copies
before decoding. Many small helper functions in the implementation have been marked #[inline(always)],
which avoids internal call overhead on hot paths. (Ideally Sonatina would inline these
automatically; the inlining heuristics need more work.)
A couple related fixes here too: ABI encoding for payloads with multiple dynamic fields, and for negative signed integers narrower than 256 bits was incorrrect.
#[must_use] and fallible precompiles
This release adds support for a Rust-like #[must_use] attribute, which can be attached
to any type definition or function. If a value of a must_use type or the return value
of a must_use function isn't used, it will result in a compile-time error:
error[8-0082]: unused value of type `Result<PrecompileError, Option<u256>>`
┌─ src/verify.fe:9:5
│
9 │ ecrecover(hash: hash, v: v, r: r, s: s)
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use this value or explicitly discard it with `let _ = ...`
│
= this type is marked `#[must_use]`
This pairs with a change to the EVM crypto precompiles. std::evm::crypto now includes wrapper functions
for all crypto-related precompiles (ecrecover, sha256, ripemd160, identity, Blake2F, KZG
point evaluation, BLS12-381, and P-256); these wrappers return
Result<PrecompileError, T> instead of reverting when the precompile
call fails. Result is now marked #[must_use], and thus can't be accidentally ignored.
Calling .unwrap() will revert if the Result is an Err.
Standard library additions
Option::ok_orandOption::ok_or_else(mirroring Rust's API). This can be combined withResult::unwrapto attach a typed error to anOptionunwrap so reverts carry meaningful Solidity-compatible error data instead ofPanic(0x01).StorageMapkeys now support all primitive integer types (u8–u128,usize,i8–i256,isize) andbool, in addition tou256,Address, and tuples.core::numgains theBoundedtrait (T::min()/T::max()asconst fn), and theAbs/UnsignedAbstraits for signed-magnitude operations with explicit stances on theT::MINedge case.- New
String<N>utilities for concatenation and equality in both const and runtime code. - Lossless
bool-to-integer casts withasare now allowed. This is useful for hand-optimized branchless code.
Tooling
Contract metadata
fe build --emit metadata writes a Solidity-standard metadata.json
for each contract. This is used by verifiers like Sourcify to
reproduce the build and verify the deployed bytecode.
fe doc improvements
msgvariants are now shown on the parentmsgpage- contract docs now have init and message-handler sections
#[test]functions are now hidden from generated docs by default (restore them with--include-tests).- The doc viewer now exposes CSS variables for fonts, weights, and layout, for easy retheming.
Try it!
Fe 26.2.0 is available now for Linux, macOS, and Windows. Let us know what you think!
- fe-lang.org
- Write your first contract: Get hands-on in minutes
- Zulip: Join the conversation