Skip to content

v26.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Sep 08:59
· 16 commits to master since this release

Changelog

26.4.0 (2026-09-30)

Features

  • Added experimental host-native executable builds for standalone Fe files on
    x86-64 Linux and AArch64 macOS. Build Fe with the cranelift feature and pass
    --backend native to produce an executable from pub fn main() -> i32. (#1549)
  • Track ownership and borrows through tuples, structs, arrays, pointer aliases, and helper calls with a structural borrow checker. Borrow information is preserved when references are returned inside aggregates, so overlapping live borrows are rejected after tuple destructuring as well as after a direct return. (#1557)
  • Validate generic calls' ownership and borrow requirements when their concrete trait implementations are selected. Checks that depend on an unresolved implementation remain explicitly pending; executable calls without a concrete implementation or a compiler-provided effect contract are rejected. (#1557)
  • Extended the experimental native backend to whole workspaces, including dependencies. fe test --backend native runs tests as native executables, and std::io provides character input and output through the host. (#1565)
  • Native programs can receive process arguments by declaring pub fn main(argc: i32, argv: **u8) -> i32 and reading them through the bounds-checked std::native::Args. std::native::cpu_clock_ticks returns the process CPU time. (#1566)
  • Added std::native::ByteBuffer, an explicitly owned heap byte buffer for native programs with fallible growth that keeps existing data, zero initialization, overlap-safe copying and an explicit release. (#1580)
  • Const functions can take immutable trait providers through uses and supply them with with, including forwarding through generic const functions, calls to const trait methods, and ref borrows of a provider such as ref self method calls. Mutable effects, type-keyed (storage) effects and extern functions with effects are rejected. (#1582)
  • Const functions can borrow their own locals and parameters with mut and ref, so helpers that update a value in place, or take a mut argument, now evaluate at compile time. Returning a borrow of a local from the function that owns it is rejected during evaluation. (#1582)
  • Add core::text::concat_slice for runtime-length string slices and TextBuilder for incremental byte-preserving text construction, also available through std::text.
  • Add std::evm::checksum_address for ERC-55 checksummed address formatting. Expand regression coverage of the existing core::num::isqrt across power-of-two and maximum-square boundaries.
  • Add std::evm::clones helpers for ERC-1167 initcode, CREATE/CREATE2 deployment and deterministic address prediction.
  • Add overflow-safe EIP-150 gas-budget admission and Call::call_with_min_gas, with a caller reserve, prepaid input memory and no automatic returndata copy.
  • Added Call::try_call_into and Call::try_static_into, which copy at most the capacity of a caller-provided buffer so oversized returndata cannot force a large copy, plus Call::send_value for plain value transfers and Call::try_call_raw for forwarding raw calldata. Call::static, Call::try_static and Address::static now only need a read-only Call effect. Added std::evm::erc165 with OpenZeppelin ERC165Checker semantics and Address::from_word_truncate.
  • Added core::num::leading_zeros and core::num::trailing_zeros for u256. Both return 256 for zero. leading_zeros compiles to the EVM CLZ instruction (EIP-7939) and to a branch-free bit search for native targets; both functions are also available in constant evaluation.
  • Added core::panic_code(code), which reverts with the Solidity Panic(uint256) payload, and the core::panics::PANIC_* constants (re-exported from std::evm::panic). Out-of-bounds core::ptr accesses now revert with Panic(0x32) instead of INVALID. On native targets the call traps.
  • Added core::text (re-exported as std::text) with concat, decimal, hex, hex_upper and base64 for building DynString values, plus DynString::slice, DynString::from_word_prefix, DynString::zeroed and Eq for Bytes and DynString.
  • Added std::evm::SolSlot for Solidity storage layouts at runtime slots: read/write access a packed state variable at a byte offset without touching its neighbours (for bool, Address, all integer widths including sol::Int24 etc., and FixedBytes<N>), and read_bytes/read_string/write_bytes/write_string use Solidity's bytes/string storage layout. Also added std::evm::SolMapping, which derives mapping slots like Solidity for a runtime root, including nested mappings. Custom-width Solidity ints now implement StorageKey.
  • Added std::evm::keccak_words([..]) for hashing a fixed list of words and std::evm::create2_address(deployer, salt, init_code_hash). keccak_packed and encode_packed now write each value directly at its packed width instead of going through the Packed builder, which makes them about four times cheaper, and custom-width Solidity ints (sol::Int24 etc.) now implement EncodePacked.
  • Added std::evm::merkle for keccak Merkle proofs: verify, process_proof and hash_pair_sorted match OpenZeppelin's MerkleProof for sorted-pair trees, and verify_indexed/process_indexed_proof check positional proofs where bit i of the leaf index picks the side at level i (as in Seaport bulk order signatures). Proofs are read in place from a MemSlice<u256> or a decoded bytes32[]/uint256[] (DynArray<Bytes32>/DynArray<u256>).
  • Added full-precision mul_div, mul_div_ceil, checked_mul_div, checked_mul_div_ceil and full_mul to core::num. They compute a * b / d with a 512-bit intermediate product, so results are exact whenever the quotient fits in a u256. mul_div and mul_div_ceil fail like checked arithmetic (division by zero, or overflow when the quotient does not fit), and the checked_ variants return None instead. addmod and mulmod are now available as core::num::addmod and core::num::mulmod too; std::evm::addmod and std::evm::mulmod remain available.
  • Added non-reverting token helpers that return a classified std::evm::TokenCall outcome (Ok, Reverted, BadReturn, NoCode) so callers can raise their own errors: erc20::try_transfer, try_transfer_from and try_approve (Solady/OpenZeppelin semantics, bounded 32-byte returndata copy), plus the new std::evm::erc721::try_transfer_from and std::evm::erc1155::try_safe_transfer_from / try_safe_batch_transfer_from. erc721::check_on_received classifies an onERC721Received hook as a ReceiverCheck (Accepted, WrongMagic, BadReturn, Reverted, NoCode).
  • Fields and array elements reached through a temporary pointer, such as f().value or (*f()).items[i], can now be assigned, compound-assigned and borrowed with ref or mut, just like those reached through a pointer variable.

Bugfixes

  • Fix resolution and validation of default type and const arguments, including dependent defaults in generic function and method calls.

    Fix compiler panics and incorrect trait dispatch when using functions and enum variant constructors as values. (#1535)

  • Fix copying arrays returned from functions so that modifying a copy no longer changes the original array. This also fixes copies of nested arrays while preserving explicit reference semantics. (#1555)

  • Allow generic constants to contain array repeats with symbolic lengths and indices and use them in branches, enum matches, casts, and type-level expressions. Evaluation now preserves bounds checks, operand failures, and assertion diagnostics through specialization. (#1556)

  • Preserve native ref and mut references' addresses, layouts, and aliasing when storing them in pointer slots or aggregate fields and returning them through helpers. Mutations through a stored reference continue to affect the original referent, including when helpers allocate memory while the reference is live. (#1557)

  • Fix internal compiler errors in native-reference slot assignments, valid compound assignments, and Copy reads from reference-returning calls. Invalid mutations through shared references and mutable field borrows through immutable owned receivers now produce source diagnostics. (#1557)

  • Reserve StorageMap's complete hashing buffer before invoking custom key encoders, preventing allocations during encoding from overwriting the key. Custom StorageKey implementations must now provide encoded_len(self) -> u256 and make write_key return () instead of the encoded length. (#1557)

  • Reject uses of moved non-Copy values through pointers, including pointers returned by calls and projections into their fields or array elements. Also reject native-reference loads from uninitialized or byte-overwritten slots: typed stores can initialize a reference slot, while zeroing or copying raw bytes cannot establish a valid reference. (#1557)

  • Account for persistent and transient state access during external calls, including callbacks. Live state borrows now conflict with CALL, DELEGATECALL, CREATE, and CREATE2; STATICCALL conflicts with mutable state borrows while allowing shared state borrows. (#1557)

  • Event entries in the generated JSON ABI now include "anonymous": false, so strict ABI consumers such as Foundry's Alloy parser can deserialize them. (#1562)

  • Fixed string literal escapes being kept verbatim. The escapes \", \\, \n, \r and \t are now decoded, so escaped strings have the correct byte length and contents in constants and generated code. Invalid escapes are reported at their source location. (#1563)

  • Fixed repeated effect calls through a temporary trait-effect provider in a with block, which could falsely move the provider or update separate copies of it. Every call in the block now uses the same provider. (#1564)

  • Fix verification of payable value-returning handlers while requiring ABI wrappers to call their planned return helper and return-type specialization. (#1567)

  • Fix ABI decoding of malformed arrays so overflowing offsets and lengths revert with empty data instead of an arithmetic panic. (#1570)

  • Fix EVM compilation of unchecked signed negation, including wrapping negation of the minimum signed value. (#1571)

  • Fix JSON ABI state mutability so call-local memory effects alone do not prevent a function from being marked pure. (#1572)

  • Include reachable custom errors in generated JSON ABIs, including errors raised through panic_with_value, Result::unwrap(), and overloaded operators. (#1573)

  • Fix a compiler panic during JSON ABI generation for contracts that call generic helpers such as encode_msg_calldata. (#1574)

  • Fixed calls to shared methods on non-Copy fields, such as frame.memory.capacity(), falsely consuming the field and rejecting later uses of the containing value. (#1581)

  • Accept ABI dynamic byte and string payloads without trailing padding while preserving bounded reads and copies, zeroing owned padding, and keeping decoded arrays word-aligned for re-encoding. (#1587)

  • Native tests on macOS no longer fail intermittently with "Operation not permitted" when the runner stops a finished test's process group. (#1590)

  • Fixed compilation of recursive raw pointer types such as struct Node { value: u256, next: *Node }, which passed type checking but then failed during lowering. Borrow checking of programs that use such types now also terminates. (#1609)

  • Allow bounded raw static calls through a read-only Call effect, preserving view mutability in the generated ABI.

  • Calling a function that declares a return value but always diverges (for example one that ends in core::panic()) in tail position no longer fails Sonatina IR verification with IR0601.

  • Comparison operators such as == and < no longer move a non-Copy right-hand operand; a == b now borrows b as a.eq(b) does.

  • Diagnose invalid constant declaration types, including oversized inline strings, before constant evaluation instead of reaching a compiler panic.

  • Fix continue in for loops so the loop moves on to the next element. Previously continue jumped back without advancing, so the loop revisited the same element.

  • Fix fe build failing in MIR lowering when emitting an #[event] struct with no fields. Such events now emit a LOG1 with keccak256("Name()") as the only topic and empty data, matching Solidity.

  • Fix a bare call such as digest(..) inside an impl or trait resolving to the enclosing impl's or trait's own function of the same name instead of the module-level function. Like in Rust, functions of an impl or trait are no longer in scope as bare names inside it; call them as Self::name(..) or self.name(..), and the "undefined variable" error now points this out. Associated consts remain usable unqualified.

  • Fix a compiler panic ("optional CTFE fold invariant failed: constant integer exceeds its declared type") when constant evaluation folded bitwise-not or other integer operations whose result has a view type, such as x == !(0 as u256). Integer views now keep the width and signedness of the viewed integer during constant evaluation.

  • Fix a compiler panic when implicitly borrowing a record literal, such as an Address literal used in an equality comparison.

  • Fix an internal borrow-check error when reading a field through a pointer returned by a call, such as pick(a, b).value. Also fix silently lost writes when a mut self method call or a with binding reaches a value through a temporary pointer, such as (*f()).set(1) or with ((*f()).counter) { ... }: the call or effect now updates the pointee instead of a copy.

  • Fix compilation of errors and events with long names by splitting their generated signature strings into supported chunks.

  • Fix layout checking rejecting types that repeat a generic type through ordinary fields, such as a struct holding Option<Argument> where Argument holds Option<u256>. Such layouts are finite. Types whose arguments grow, such as Growing<T> containing Growing<(T, T)>, are still reported as expanding.

  • Fix parsing of generic arguments that start with a qualified path, such as Wrapped<<T as Model>::Point>. The << is no longer mistaken for a left shift, so the argument now belongs to Wrapped in type position, in expression position, and in a method call such as wrapped.pick<<T as Model>::Point>(value).

  • Fix the Solidity ABI encoding of msg variants that return a tuple with dynamic elements, such as -> (DynString, Bytes32, Address). The return value is now encoded as a parameter list, like the values of a Solidity function with multiple return values (returns (string, bytes32, address)), instead of as a single tuple behind an extra leading offset word. Typed calls and std::abi::sol::decode_output decode tuple returns the same way, and the generated JSON ABI lists one output per tuple element. This changes the returndata of dynamic tuple returns; static tuples encode the same bytes as before. A single dynamic return such as -> DynString or an #[abi] struct is unchanged.

  • Fixed a compiler panic when resolving associated types on generic functions with implicit effect parameters by preserving each explicit parameter's original source index during lowering.

  • Reject integer expression literals larger than 256 bits during type checking instead of silently truncating them during EVM lowering.

  • Report U::Name and Self::Name as not found when only the bound on a different type parameter declares Name. For example, in fn f<T: Arrow, U>(_ x: U::Dom) and in trait Eval<T: Arrow> { fn eval(_ x: Self::Dom) }, the name used to resolve silently to T::Dom.

  • Report malformed sol("...") selector signatures on msg variants (unbalanced parentheses, invalid or empty function names, empty argument types, whitespace) as fe check errors instead of silently using the keccak of the malformed string as the selector and failing only later during JSON ABI emission.

  • Report overflowing dynamic ABI offsets as decode errors instead of arithmetic panics.

  • Resolve Self::Name inside a trait when Name is an associated type of one of its supertraits, such as Self::Dom in trait Eval: Arrow. Bounds in scope at the reference, such as a method's own where Self: Extra, are considered alongside the inherited ones, and the reference is reported as ambiguous when they name different associated types. Reaching a single declaration through several supertraits is not ambiguous, even when only some of them bind it with an equality.

  • Reverts (including assert! with a message) now compile for native targets, where they trap. Previously they failed with an unsupported-terminator error.

  • Run fe test and the contract test harness under Osaka rules, the EVM revision Fe compiles for. Previously tests executed with Prague rules, so Osaka instructions such as CLZ halted with NotActivated. The Osaka per-transaction gas cap (EIP-7825) is lifted for tests.

  • String literals longer than 31 bytes can now be used as tuple parts of core::keccak, std::abi::sol and std::io::write/writeln arguments, e.g. core::keccak(("OrderComponents(...)", "OfferItem(...)")), which hashes the concatenated bytes. Oversized string literals elsewhere now report the inline string capacity error instead of a compiler invariant failure during constant evaluation.

  • Teach the tree-sitter grammar the generic arguments that start with a qualified path, such as Wrapped<<M as Model>::Point>::new(point). The compiler already accepted these in expression position while the grammar reported an error, so editors flagged valid code. A << that is not followed by a qualified path is still a left shift.

  • The init code a contract deploys with create<B> / create2<B> (and reads with Contract::init_code_offset/len) is now exactly the B.bin artifact that fe build writes for B. Previously the embedded copy of B was compiled together with the embedding contract, so shared helpers, symbol names and inlining budgets could change it, and CREATE2 addresses derived off-chain from B.bin did not match the deployed address. Each contract is now compiled on its own and embedded as its final bytes, which also makes a contract's bytecode independent of other contracts in the same file.

  • FixedBytes<N> (Bytes1 through Bytes32) now implements Copy, so a bytesN value such as a conduit key can be read from a view parameter or used more than once without a move conflict.

  • decode_bytes_view_at (and decode_bytes_view) now revert with empty returndata, like Solidity's ABI decoder, instead of panicking with Panic(0x11) when an offset or length is so large that its sum with the payload position overflows.

  • fe fmt no longer joins a bare qualified first generic argument onto the opening <, which turned Wrapped< <T as Model>> into Wrapped<<T as Model>> and changed how it parsed. It now formats as Wrapped< <T as Model> >.

Performance improvements

  • Reduced the cost of DynArray::get for single-word static elements by relying on the frame validation already done at construction. Bounds checks and canonical-value validation are unchanged. In a Merkle proof benchmark this cuts gas for a 16-sibling proof by about 46% and runtime bytecode by about 28%. (#1559)
  • Constant string values, such as ERC-20 name() and symbol() getters, are now folded during ABI encoding instead of computing their length at runtime. In an ERC-20 benchmark this shrinks runtime bytecode by about 21% and cuts the gas of name() from 3,861 to 857. (#1560)
  • Helper functions reached from several recv arms or modules are now emitted once instead of once per caller, which substantially shrinks contracts with many entry points. (#1618)
  • Reduced compile times by caching and sharing work in semantic analysis, trait solving and borrow checking, and by lowering only the function parameters a caller needs.
  • Reuse the outcome of a speculative parse at a position instead of repeating it. Disambiguating generic arguments from shifts and comparisons re-parsed the same nested syntax once per enclosing level, so deeply nested arguments such as Wrap<<Wrap<<T as Model>::Point> as Model>::Point> cost roughly five times more per level of depth. A 12-level type took about 96 seconds to parse and now takes milliseconds. Incomplete and malformed nesting, which is what an editor sees while the code is still being typed, is reused the same way.

Internal Changes - for Fe Contributors

  • Rebuild the embedded core and std libraries when a file is added to ingots/core or ingots/std, including in builds that use a compiler cache. (#1582)