Skip to content

v26.5.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 19:57
· 59 commits to master since this release

Changelog

26.5.0 (2026-10-26)

Features

  • Support boolean const where predicates on items, such as fn operation() where allowed(3) {}, const fn bounded<const N: usize>() where N > 0 and struct Bounded<const N: usize> where N > 0. Conditions without generic parameters are evaluated and reported where they are declared, even if the item is unused. Conditions on the generic parameters of functions, structs, enums and inherent methods are checked wherever such an item or type is written or an expression instantiates it, and a generic caller, including an anonymous constant in its signature or body, satisfies one only by stating the same condition. Inside an impl, the conditions of the types in its self type hold without restating them (a trait impl's trait arguments supply none), and a generic type alias assumes those of its aliased type. A condition that needs several lines can be written as a block, where { ... }. Const predicates in traits, generic trait impls and on generic impl blocks themselves are rejected for now. (#1576)
  • DynArray::get now works for every ABI element type, including multi-word static structs and tuples and dynamic elements such as Bytes. The new std::abi::dyn_array_from([..]) builds a DynArray from a fixed array of Copy ABI elements, dynamic ones included (for example a bytes[]), and std::abi::canonical_dyn_array re-encodes a decoded array in canonical layout. (#1606)
  • Added the #[abi] struct attribute. An #[abi] struct gets AbiSize, AbiSpan<Sol>, Encode<Sol> and Decode<Sol> implementations that encode it like the Solidity tuple of its fields, so Solidity structs such as PoolKey can be used in msg fields and return types, in arrays and inside other #[abi] structs without hand-written codecs. When all their fields implement SolCompat, #[abi] structs do too, with the Solidity tuple type of their fields, e.g. (uint8,address,uint256), so they can be used as #[event] and #[error] fields, including in arrays such as Seaport's OrderFulfilled(bytes32,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256,address)[]). (#1606)
  • Added std::evm::eip712 with DOMAIN_TYPEHASH, domain_separator, a digest that needs no effects, and a Domain type that caches the separator in an immutable contract field and derives it anew when the chain id or executing address differs, matching OpenZeppelin's EIP712. std::evm::crypto::eip712_digest now uses the same direct 66-byte hashing. (#1645)
  • Added revert and returndata helpers to std::evm: bubble_revert_or (bubble a failed call's revert data, or revert with a custom error when there is none), encode_error, revert_bytes and revert_empty for raising, revert_selector, has_selector, is_error, decode_error_string and decode_panic (with Solidity catch Error(string) / catch Panic(uint) acceptance rules) for inspecting revert data, and ReturnData, a ByteInput over the current returndata buffer so it can be read or ABI-decoded in place without copying, or copied once into Bytes with ReturnData::to_bytes. (#1646)
  • Added std::evm::signature for signature verification: parse decodes 65-byte and 64-byte EIP-2098 compact ECDSA signatures into an EcdsaSig, recover returns the canonical (low s) signer as an Address and recover_raw accepts anything the ECRecover precompile does. is_valid_erc1271_signature_now and is_valid_signature_now check ERC-1271 contract signatures and ECDSA-or-ERC-1271 signatures with OpenZeppelin SignatureChecker semantics. (#1647)
  • Release binaries now include the native backend (fe build --backend native). Native executables, the backend's default output, can be built with the prebuilt downloads on x86-64 Linux and AArch64 macOS; on the other release targets the native backend is available for --emit ir. (#1660)
  • Added core::fixed with decimal fixed-point numbers whose scale is part of the type: UFixed<D> and Fixed<D> (signed), with the aliases Wad (18 decimals), Ray (27) and SWad (signed 18). +, -, *, / and ** (integer powers) keep the scale; products and quotients use a 512-bit intermediate and round down, mul_up and div_up round up, and rescale<E>() converts between scales explicitly, so Wad * Ray or wad + 5 is a type error. sqrt is exact for any scale, and the 18-decimal types port Solady's expWad, lnWad and powWad bit for bit as SWad::exp, Wad::ln and Wad::powf. Constructors and arithmetic are const fn, so constants such as const FEE: Wad = Wad::from_ratio(3, 1000) are computed at compile time. (#1672)
  • Added core::sort with sorting and searching after Solady's LibSort, generic over both the container and the element: sort, insertion_sort, heap_sort, reverse, swap, is_sorted, is_sorted_unique, lower_bound, binary_search and dedup_sorted work on any collection indexed by position, through Index<usize> (and IndexMut<usize> to reorder) and the new core::ops::Len, whose elements implement Ord and Copy. sort uses insertion sort for up to 16 elements and heapsort above, so it is O(n log n) for every input. Fixed arrays, MemSlice and MemArray qualify, and arrays now implement Index, IndexMut and Len for generic code. std::evm::Address implements Ord by numeric value, so signer lists can be sorted and checked with is_sorted_unique. (#1690)
  • Added std::evm::random with pseudorandom numbers as an effect, after Solady's LibPRNG. Code that needs randomness declares uses (rng: mut Rng) and the caller provides a generator with with (Rng = ...): KeccakRng reproduces Solady's keccak256 chain from a seed such as ctx.prevrandao(), and ReplayRng returns chosen words so tests can drive the same code to specific outcomes. Rng provides next, unbiased uniform and chance, and the generic shuffle (arrays), shuffle_slice (MemSlice) and choose work for any Copy element type. (#1691)
  • Added std::evm::heap::StorageHeap<T>, a binary min-heap in contract storage after Solady's MinHeapLib, for any T that implements Ord and Copy, including packed structs and multi-word values with their own order. It provides push, pop, root, push_pop, replace and enqueue, which keeps the max_len greatest values and reports its outcome as Enqueued::Added, Enqueued::Rejected or Enqueued::Replaced(evicted). Reverse<T> inverts the order, so StorageHeap<Reverse<T>> is a max-heap. pop and replace on an empty heap revert with Solady's HeapIsEmpty(). (#1692)
  • Added word-backed PackedBytes<N> with read-only byte indexing, equality and explicit ordinary-array conversions. ByteWriter<M> checks capacity and each component's byte count while tuple components append into one destination. (#1694)
  • Added std::abi::ArrayView, a lazy view of a Solidity ABI T[] over calldata or any other ByteInput, created with CallData::array_view or decode_array_view_at. It validates offsets and lengths like a Solidity T[] calldata parameter and decodes elements one at a time with get, with zero-copy bytes_at/string_at and nested array_at accessors, so raw-selector recv arms can read large arrays without decoding them into memory. For hand-written decoders, heads_pos, head_word and unchecked_element_pos expose the raw element heads and tail positions without validating element tails. (#1703)
  • Added std::evm::SolArray and std::evm::SolEnumerableSet for Solidity dynamic storage arrays (T[], with small elements packed like Solidity and Solidity's Panic codes) and OpenZeppelin EnumerableSet storage at runtime slots, including SolArray::to_dyn_array and SolEnumerableSet::values to read all elements into an ABI array. SolSlot::load returns a SolWord for reading or updating several packed fields of a slot with a single sload/sstore (written back with SolSlot::store); a SolWord also packs or unpacks any already loaded word. (#1704)
  • Added std::evm::init_code_hash<C>(args) and std::evm::predict_create2<C>(deployer, args, salt), which hash the exact init code create2<C> deploys (the embedded creation code of C followed by its ABI-encoded constructor arguments) and derive the resulting CREATE2 address. (#1706)
  • Added core::num::gcd(a, b), the greatest common divisor of two u256 values computed with Euclid's algorithm. gcd(a, 0) is a (so gcd(0, 0) is 0), it never fails, and it can be used in const contexts. (#1707)
  • Added tuple structs such as struct Month(pub u8), which make newtypes much lighter to write. A tuple struct is constructed by calling its name like a function (Month(10), also generic as in Wrapper(5) or Wrapper<u8>(5)), its fields are accessed and assigned by position (m.0, m.0 = 11), and it is destructured with a tuple pattern in let and match (let Month(raw) = m, Month(2) => 28). Fields are private unless marked pub, and constructing or destructuring a tuple struct outside its module requires all of its fields to be visible. Tuple structs can be generic and have a where clause after the field list (struct Wrapper<T>(pub T) where T: Copy), and they work as contract storage fields. #[event], #[error] and #[abi] remain limited to structs with named fields. (#1716)
  • Added typed ETH amounts and block time to std::evm::units, all in the std prelude: Wei (an amount of native ETH), Timestamp (unix seconds) and Duration (seconds). Amounts, points in time and durations no longer mix: Timestamp + Duration is a Timestamp, Timestamp - Timestamp is a Duration, Wei / Wei is a plain ratio, and Timestamp + Timestamp or Wei + Duration do not compile. Wei and Duration also add and subtract, scale and split by a u256 (price * quantity, pot / winners), take a remainder (value % gwei(1)) and order, and Wei::mul_div computes fees and shares with a 512-bit intermediate. Solidity-style constructors read like unit literals: ether(5), gwei(30), wei(1), days(7), hours(1), along with the constants WEI, GWEI, ETHER, SECOND, MINUTE, HOUR, DAY and WEEK. They are const fns and work in constant initializers, e.g. const LOCK: Duration = weeks(2) + days(3). as_wei() and as_seconds() return the plain number. All three types are a single uint256 in the ABI, so they can be msg fields and return values, #[event] fields (indexed or not), contract fields and StorageMap keys or values. (#1719)
  • Added std::evm::multicall, the counterpart of OpenZeppelin's Multicall: a contract that handles MulticallMsg (multicall(bytes[])) with multicall::multicall(data) lets callers batch several of its messages into one transaction. Each entry is DELEGATECALLed to the contract itself and the returndata of every entry is returned in order; the first failing entry reverts the batch with its revert data unchanged, or with FailedCall() when it reverted without data. (#1721)
  • Added std::evm::erc4337 for ERC-4337 accounts and paymasters on EntryPoint v0.7 and v0.8: the PackedUserOperation ABI struct with unpacking of its gas limits, fees and paymaster fields and its v0.7 and v0.8 user operation hashes (hash_v07, hash_v08), AccountMsg (validateUserOp) and PaymasterMsg (validatePaymasterUserOp, postOp) with PostOpMode, ValidationData packing and unpacking with SIG_VALIDATION_SUCCESS and SIG_VALIDATION_FAILED, require_entry_point, pay_prefund, and the canonical ENTRY_POINT_V07 and ENTRY_POINT_V08 addresses. validate_user_op implements an account's validateUserOp from a UserOpSigner signature check, like OpenZeppelin's Account: it rejects callers other than the EntryPoint, checks the signature, pays the prefund and returns the validation data. (#1721)
  • Added string to number parsing to core::text, matching OpenZeppelin's Strings.tryParse*: parse_uint and parse_int read decimal text as u256 and signed i256, and parse_hex_uint reads hex digits of either case with an optional 0x prefix. std::evm::parse_address parses 0x and 40 hex digits (or the digits alone) into an Address without checking the ERC-55 checksum. All of them return None for invalid input, including empty text and out of range values, for which OpenZeppelin returns 0 or reverts. (#1722)
  • Added core::text::decode_base64, the inverse of base64. It accepts only canonical standard base64 with = padding and returns None for anything else. (#1722)
  • Added Bitcoin base58 to core::text: base58 encodes bytes and decode_base58 decodes text, mapping leading zero bytes to 1 like OpenZeppelin's Base58. Decoding returns None for characters outside the alphabet. (#1722)
  • Added std::evm::rlp for Recursive Length Prefix encoding, the serialization of Ethereum transactions and trie nodes. encode_bytes, encode_uint, encode_address, encode_bool and encode_list (of already encoded items) build Bytes, and decode reads an Item in place, whose as_bytes, as_uint, as_address, as_bool and items accessors return None for the wrong kind of item. Decoding accepts only canonical encodings. (#1722)
  • Fe now has mutable value semantics. References are no longer values: ref and mut are not types, so a struct cannot hold a borrow. Instead they describe how a function or binding accesses a place. A parameter is a read-only view (x: T), a mutable borrow (x: mut T) or owned (x: own T), and let h = mut p.x gives mutable access to p.x until the last use of h. A function that returns ref T or mut T, or a shape such as Option<mut T>, gives its caller access to a place rather than a value, and may yield that access and run code after the caller's last use of it, for example to write a value back. A new access checker replaces the borrow checker: an access lasts until its last use, and two accesses to overlapping places conflict unless both only read, so a function can hold mutable access to two fields of one struct at once. Mutable variables are declared with var x = e, which replaces let mut x = e, and mut, ref and var mark the names a pattern binds, as in Some(mut v) and for mut x in xs. (#1798)
  • Added closures, such as |x| x + 1 or |acc: own u256, x: u256| -> u256 { acc + x }. A closure can be passed to any function that takes a callable, including fold, any, count and find on collections and the filter and map loop adapters (for x in xs.filter(|x| x % 2 == 0)). Functions can be defined to take a callable with a bound such as F: Fn(u256) -> bool and call it as f(x). A named function can be passed wherever a closure can, as in xs.fold(0, add). A closure can also be bound to a variable, stored in a struct or tuple, and used in contract code, where its body can use the contract's storage and other effects; a generic function that calls a closure declares uses F::E so that its callers provide those effects. A closure captures the values it uses by copy or move and cannot modify them, so state is passed through parameters and results instead, as fold does. Closures cannot be used in constant contexts, and cannot capture a ref or mut binding, or a view of a non-Copy type. (#1798)
  • Added unsafe { .. } blocks. Dereferencing a raw pointer or calling an unsafe fn is now an error outside an unsafe block or unsafe fn. A struct or enum field can be declared unsafe when safe code relies on its value, such as a buffer's length: reading it is safe, but setting or mutating it requires unsafe. A dependency's unsafe code, including its unsafe impls, can only be used if fe.toml trusts the dependency with allow_unsafe = true. (#1798)
  • Add EVM-only core::hint::black_box for scalar benchmark inputs and results, preserving measured work and ordering it with gas reads. (#1808)
  • MemVec<T> (in the prelude) is now a growable array of Fe values in memory, for any element type, including non-Copy ones such as MemBuffer. Create one with MemVec::new() or with_capacity, then use push, pop, reserve, truncate, extend_from_slice, get, set, replace, swap, indexing, for loops (including for mut x in v and v.reversed()), the core::sort functions and MemVec::collect. into_array turns it into a fixed-length MemArray without copying. DynArray::from_slice ABI-encodes the elements of a slice such as v.as_slice(), and DynArray::to_mem_array decodes an array into a MemArray. (#1810)
  • DynArray can be indexed (arr[i]) and looped over (for x in arr, arr.enumerate(), arr.reversed()), decoding each element as it is read, and works with the core::sort searches such as binary_search and is_sorted. (#1810)
  • MemArray::from_fn(len, |i| ..) builds an array from a callable. MemSpan::read_word and write_word, and MemBuffer::read_word, read and write a 32-byte word at any byte offset, reverting unless the whole word is in range. (#1810)
  • Added core::mem::replace(mut place, value), which moves value into a mut place and returns the value it held, core::mem::take(mut place), which leaves Default::default() in its place, and Option::take, which leaves None. A value can be moved out of a field, element or mut parameter this way without restoring it by hand. (#1813)
  • Added std::evm::layout with Solidity's storage slot derivations: mapping_slot(root, key) for a mapping's entry, array_data_slot(root) for a dynamic array's elements and a long bytes or string's data, and element_location for an element laid out in lanes or whole slots from a first slot. A library collection's StateIndex::locate can return Location { slot: mapping_slot(root, key), offset: 0 }. (#1813)
  • A string literal longer than 31 bytes whose context does not fix its type is now a DynString instead of an error, so let reason = "a long revert reason ..." works and a long literal can be passed to a generic Encode parameter. Short literals are still String<N>. On the EVM, a DynString literal of 96 bytes or more is copied into memory from the contract code rather than stored a word at a time. (#1819)
  • std::native::ByteBuffer::copy_from copies a byte range from another buffer in one bulk copy. (#1830)
  • Add optional SP1 RV64IM guest compilation with scalar input and public-value effects, ELF linking, and execution/proof integration tests. (#1837)
  • A bound can fix an associated shape at a mode, D: Driver<Step<ref> = ref u256>, so generic code binds, copies and destructures what a D::Step<ref> holds. Each mode is bound on its own: Step<mut> = mut u256 fixes the mut one.
  • A driver names the collection it runs over, type Base, and each position yields a step, shape Step: Enumerate<D>'s is (own usize, D::Step). A loop's pattern binds the step as a let binds a projection's result, so for (i, (j, x)) in xs.enumerate().enumerate() copies what it names, and a closure a driver calls takes the step whole: xs.enumerate().map(|(i, x)| i as u256 * x). The chain methods (enumerate, take, skip, step_by, filter and map) are Driver's own, and generic code bounds a driver's step, D: Driver<Base = C, Step<ref> = ref u256>.
  • A function or closure parameter may be a pattern that takes its argument apart, as a let would: fn sum(_ (a, b): (u256, u256)), pairs.fold(0, |acc, (a, b)| acc + a * b). A function writes _ before the pattern, since it has no label.
  • A function parameter may be a product, which takes several accesses at once, each part in its own mode: fn add_to(_ (mut a, b): (mut u256, ref u256)), called as add_to((mut x, ref y)) or with a projection's result that grants the parts. The parts are checked as separate arguments are, so add_to((mut x, ref x)) is rejected.
  • A trait can declare an associated shape, shape Step, which each implementation gives (shape Step = (own usize, mut? T)), and a method can return it: fn at(mut? self) -> Self::Step. A shape is named at a mode, D::Step<ref> or D::Step<mut>, and a mut? item's own signature names it at the item's mode. A shape's accesses are at most its mode, so an implementation writes mut? rather than mut. Generic code passes a value of an associated shape on whole: it returns it, or passes it to a parameter of the same shape (_ s: D::Step<ref>), at most once, and a bound can take it, as in F: Fn(D::Step<ref>) -> bool.
  • A trait can take a parameter list as a shape parameter and spread it into a method: trait Apply<shape A> { fn apply(self, ..A) -> u256 }, implemented as impl Apply<(own u256, ref u256)> for Adder { fn apply(self, _ a: own u256, _ b: u256) -> u256 { .. } } and called with one argument for each parameter, adder.apply(1, x). Fn and FnMut are such traits: Fn(own A, B) -> R is Fn<(own A, B), Out = R>, and -> _ leaves the result open, as in F: FnMut(u256) -> _. A closure parameter may be a product, |(mut a, b): (mut u256, ref u256)| a += b, and a function with a product parameter can be passed as a callable value: its shape lists the product as one parameter of parts, as in F: FnMut((mut u256, ref u256)) -> _.
  • Add mut? items: a function whose signature writes mut? (fn at(mut? self, _ c: usize) -> mut? T) is used at ref or at mut, and its body is checked once per mode. A call is at ref unless a use of its result asks for mut, such as xs.at(i) = 1, let x = mut xs.at(i) or xs.at(i).bump(), so a mut? function's result must have a mut? part. Trait bounds take a mode: T: mut Index<usize> grants a trait's mut? methods at mut, and an implementation grants it when each of them has a mut mode there, by a mut? method or by a second method of the same name taking mut. A mut? bound (D: mut? Driver), on a function or an implementation, is mut where its item is used at mut; associated-type and Self bounds take mut (type Item: mut Slot). An effect may be mut? too (uses (store: mut? Store)): a call at ref needs only read authority, and one at mut needs the effect mut.

Bugfixes

  • Report #[error] lowering errors with code prefix 13, such as error[13-0004]. They used prefix 16, which borrow-checking errors also use, so two different errors could share a code. (#1576)
  • Report errors in anonymous constants in types and in the generic arguments of types, traits and functions, including in a type parameter's default. [u8; { n }] with a runtime n panicked, and an undefined name there, or in a default argument that an alias drops, such as struct S<T = Drop<{ nope }>> with type Drop<const N: usize> = u8, compiled without an error. (#1576)
  • Parse comparisons separated by commas in call arguments and tuples, such as f(a < b, c > d) and (a < b, c > 0), as comparisons. They were read as generic arguments and failed to parse. A comparison whose > is followed by a token that can continue an expression, as in f(a < b, c > (d)), is still read as generic arguments; parenthesize it, as in (c > (d)). (#1576)
  • A constant that embeds a generic impl's associated constant whose type involves an associated type, such as (<W<u8> as Tr2>::P, true) for type B = (T::A, u16), no longer fails const evaluation with "constant child type differs from its aggregate field type". (#1606)
  • usize and isize now implement SolCompat (as uint256 and int256) and TopicValue, so they can be #[abi] struct fields and #[event] or #[error] fields, including indexed event fields. (#1606)
  • An #[event] or #[error] field whose type has an ABI encoding but no SolCompat implementation, and so no Solidity type name for the signature, is now reported at the field instead of failing during code generation. (#1606)
  • Tuples of 5 to 16 elements can now be DynArray elements, and tuples of 13 to 16 elements can now be ABI-decoded, for example as msg fields. (#1606)
  • Reject recursive types whose field types grow a type argument on each recursion, such as struct Grow<T> { next: *Grow<[T; 1]> }. Previously any use of such a type made checking hang. (#1635)
  • Generic arguments written on an enum variant or a module, such as Choice::Empty<u8> or m<u8>::Item, are now rejected with an "incorrect number of generic arguments" error. They were silently ignored before, and a constant expression in them was never evaluated. (#1656)
  • Fixed a trait method calling the same method on another value, such as self.inner.eq(other.inner) inside impl<T> Eq for Wrap<T>, being rejected with a type mismatch. The call now uses the impl for the value it is called on. (#1657)
  • The compiler no longer crashes with a query cycle when a function has two or more parameters whose types use a generic const call inside a const block, such as a: Holder<{ id<5>() }>::Out, b: Holder<{ id<5>() }>::Out. (#1669)
  • Go-to-definition and references on a msg name in the language server now point at the name as written. (#1674)
  • Calling a precompile through std::evm::crypto (for example ecrecover, sha256 or modexp) while a mutable borrow of contract storage is live, such as inside a mut self method on a contract field, is no longer rejected. Precompile calls need no effects: unlike other static calls, a precompile cannot call back into the contract, so it cannot read its state. (#1682)
  • Fixed runtime serialization of words, fixed strings and nested tuples, including native I/O and EVM core::keccak. Fixed-width padding, truncation and byte order now match constant evaluation. (#1694)
  • An operator on a type with several impls of the operator's trait for different right-hand types, such as impl Div<u256> for Wei and impl Div<Wei> for Wei, no longer requires the trait to be imported. It was rejected with "trait is not in the scope" unless core::ops::Div was in scope, although a type with only one such impl worked without the import. (#1719)
  • An error inside the type of a qualified path, such as Missing in <Wrap<Missing> as M>::P, now points at the failing name instead of the enclosing type or the start of the file. (#1723)
  • An enum can now be a msg field when it implements the ABI codec traits and SolCompat. Its SolCompat impl names its Solidity type, such as uint8 for a Solidity enum, which the selector signature check and the JSON ABI use. Previously every enum field was rejected as an unsupported ABI type, even with hand-written Encode<Sol> and Decode<Sol> impls. (#1724)
  • An operator after an if/else expression now applies to the whole conditional, so if c { 1 } else { 2 } + 3 evaluates to 4 or 5. Before, the operator was parsed into the else branch, so the expression silently computed if c { 1 } else { 2 + 3 }, and indexing a conditional, as in if c { a } else { b }[1], failed to type check. (#1766)
  • A string literal passed to a generic String<N> parameter, such as width("abc") for fn width<const N: usize>(_ s: String<N>) -> usize, now infers N from the literal instead of failing with "type annotation is needed". N is the literal's length unless something else fixes it. A length that cannot be shown to hold the literal is rejected, including a generic one such as M or {M + 1}, which a too-short instantiation used to turn into an internal compiler error. (#1780)
  • Option<T> is now Copy when T is, and Result<E, T> when both E and T are, like tuples and arrays. An Option<u64> field can now be read out of a self view, and let copy = original no longer moves original. (#1785)
  • An ingot inside the directory of an ingot it depends on, such as a tool in tools/ of a library, no longer breaks the library: its items, including those the library re-exports with pub use, now resolve as they do for an ingot next to it. (#1786)
  • Code after an if/else chain no longer gets skipped when the last branch diverges (with return, continue or break) but an earlier branch falls through, such as if a { return 1 } else if b { r = 3 } else { return 2 } followed by more statements. The chain was typed as diverging, so the code after it was compiled as unreachable and execution reverted or trapped. (#1788)
  • A match whose first arm ends in a diverging call, such as core::panic(), no longer rejects the later arms that fall through with "expected !". (#1788)
  • Paths to associated constants and types such as u8::SIZE or Self::SIZE now only consider traits in scope when several traits provide the item, like method calls do. A trait with an item of the same name elsewhere, for example in a dependency, no longer makes such paths ambiguous. (#1789)
  • An associated type defined through an ever-growing projection, such as type Out = <W<(T, T)> as Tr>::Out, no longer makes fe check hang or overflow the stack; it is reported as "type normalization limit exceeded". (#1791)
  • A constant that fails to evaluate in a call's return type, such as S<{N - 5}> with N = 2, is now reported at the call instead of failing later in code generation. (#1791)
  • A bound on an associated type that names no trait, such as type Bar: T2 when T2 does not exist, is now reported. (#1791)
  • fe check now respects --color for diagnostics. (#1791)
  • fe fmt no longer drops pub(ingot) and pub(super), attributes on for loops, or a module's inner attributes. (#1791)
  • Types and trait arguments written inside a qualified path, such as <S as Tr<Foo<NotMarked>>>::Item, are now checked where they are written. (#1791)
  • Reject bytes and string views whose ABI offset or length word is 2**64 or more, as Solidity does, also when the input reports more than 2**64 bytes. (#1792)
  • Reject recursion that continually grows its generic type or const arguments, such as f<T> calling f<(T, T)> or f<N> calling f<{ N + 1 }>, instead of hanging or panicking during compilation. (#1798)
  • Require shared Call authority for staticcall_decode to track read-only reentry, and allow typed STATICCALL methods alongside shared contract-state accesses. (#1800)
  • Inside a trait, an omitted Self default in a trait bound now stands for the bounded type. With trait Combine<Rhs = Self>, fn f<T: Combine>() in a trait means T: Combine<T>, and type A: Combine means A: Combine<A>; both used the enclosing trait's Self before. A Self written in a qualified path's trait arguments inside a trait, as in <Self::A as Combine<Self>>::Out, now stays the trait's Self instead of becoming Self::A. (#1803)
  • A trait bound that names something other than a trait now says so even when it has generic arguments. T: S<u8> for a struct S previously reported a generic-argument count error. (#1803)
  • Go to definition, find references, rename and semantic highlighting now treat a trait bound named like an associated type of the enclosing trait, such as type Decoder: Decoder<Self>, as the trait. (#1803)
  • Find references, rename, highlighting, code lens reference counts and the SCIP and LSIF indexes now record each reference once when it is in a trait's or impl's method signature, an item declared inside a function body, an array length inside a function body or a constant's value. They recorded it two or three times before, so rename edited it more than once. A reference in a constant's type annotation, such as Width in const COPY: Width = 2, is now found too. (#1803)
  • A trait bound on an associated type with the same name as the trait, such as type Decoder: Decoder<Self>, is now enforced. Previously it was rejected with a generic-argument count error and dropped. A trait named like an associated type also resolves in a qualified path inside the trait, such as <Self::Encoder as Encoder>::Output. (#1803)
  • Preserve path names and the line break after a line comment when leading trivia belongs to a nested syntax node, preventing formatting from deleting field types or commenting out following code. Retain comments inside function parameters. (#1806)
  • Omit implicit Solidity built-in errors from generated JSON ABIs so ethers can load contracts with panic paths. (#1807)
  • Find references, rename, highlighting, code lens reference counts and the SCIP and LSIF indexes now record each reference in a static_assert comparison, such as static_assert(A == B), once. They recorded it twice before, so rename edited it twice. (#1815)
  • Find references, rename, highlighting, code lens reference counts and the SCIP and LSIF indexes now resolve an unlabeled uses entry of a contract's init or recv arm, such as uses (mut state), as the compiler does: to the contract field, or to an effect of the contract's own uses. An item of the same name in scope was taken instead, so renaming a function named like one of the contract's effects rewrote the entry. (#1815)
  • Find references, rename, highlighting, code lens reference counts and the SCIP and LSIF indexes no longer take a name bound in a function body for an item. self in a method was recorded as a reference to the module around it, a parameter as a reference to a module of the same name, and a closure parameter, effect or contract init parameter as a reference to any item of the same name, so renaming a function with that name rewrote it. The code generated for a msg's variants also added references to the msg at the variant names. Semantic highlighting now colors these names, and let bindings, as variables; it colored self as a module and a parameter as a function. (#1815)
  • Reaching core::panic_code during constant evaluation reports a "panic in const context" error with the panic code, instead of claiming the call cannot be evaluated at compile time. (#1820)
  • An Address can no longer hold bits above the low 160, which the EVM ignores: two addresses naming the same account are now always equal, the same storage map key and the same event topic. The checks for such bits in checksum_address, clone_initcode, create2_address, rlp::encode_address, packed encoding and SolPacked are removed. (#1820)
  • Report a supertrait that does not name a trait, such as trait A: Missing {}, instead of silently ignoring it. (#1823)
  • Accept a path to a constant or a unit enum variant as a trait's const generic argument (T: Gate<N>), as a type's already accepts one. (#1823)
  • An impl whose associated type is another impl's associated type of the same trait, as in impl<P: Tr> Tr for Wrap<P> { type X = Inner<P>::X } with impl<P: Tr> Tr for Inner<P>, no longer crashes the compiler with a query cycle. (#1825)
  • An implementation whose self type is an associated type, such as impl Tr for <S as Get>::Out, is now an implementation for the type it normalizes to. An external trait can no longer be implemented this way for an external type, an implementation for a type of the ingot is no longer rejected because S is external, one that overlaps an implementation for the normalized type is reported as a conflict, and its methods can take self. (#1829)
  • A bound on an associated type, such as where O::Out: Copy, now finds an implementation in the ingot of the type it projects to. With P a Copy struct of the user's ingot, W<Option<P>> failed impl<O: Unwrap> Check for W<O> where O::Out: Copy, because the search only looked in Option's ingot. (#1829)
  • Fixed a sonatina bug that could cause a buffer allocated before a loop to reuse the memory of an earlier allocation that the loop reads, in optimized builds. (#1832)
  • Method calls now consider the methods of every super-trait of a trait in scope, not only of its direct super-traits. With use a::Top, where Top: Middle and Middle: Base, calling a method of Base no longer reports that its trait is not in scope, and if another trait in scope has a method of the same name, the call is reported as ambiguous instead of silently calling that other trait's method. (#1833)
  • ABI decoding rejects offset and length words of 2**64 or more for every input, as Solidity does. An input is now shorter than 2**64 bytes by its type; before, a custom input reporting a longer length could get such offsets nested in dynamic values accepted. (#1834)
  • A recv arm can name its variant by a longer path, such as self::V { n } in a recv M block. Before, such an arm was rejected with "V is not found" and its field bindings were reported as undefined. An arm path is now checked like any written path, so a variant that isn't visible from the contract, or a generic variant written without its type arguments, is reported. (#1846)
  • A bodyless trait method or extern function can now be followed by an attributed item, and a bodyless trait method by an associated type.
  • A closure whose body uses an effect keyed by a trait, such as RawStorage, no longer crashes the compiler when it is called.
  • A glob import that brings in a module named core or std no longer hides the prelude. With use foo::* and a module foo::std, Address and the other prelude names weren't found.
  • A method call whose receiver's type is inferred later in the body now takes its effects from where it is written: a with block around it provides them, and in a closure they become part of the closure's effects. A call that resolves only after its closure's body, and needs an effect the closure does not use there, is an error (8-0151).
  • A middle segment of a use path no longer reports an ambiguity among values. With use a::* and use b::* both bringing in a function m, and a module m, use m::S reported "m is ambiguous" though only the module can be a middle segment.
  • A path whose later segment names a value where a type is expected now reports that segment. m::f in a type, with f a function in module m, reported "m can't be used as a middle segment of a path" and labeled f as m's definition; it now reports that a type is expected but f is a function, and m::f::X reports that f can't be used as a middle segment.
  • An "is ambiguous" error for a name that three or more glob imports bring into scope now lists every candidate. It listed only two of them, and which two depended on the order of the imports.
  • An import now resolves to what its path names after all imports are resolved, independent of the order in which imports are resolved. A use could keep a name that a glob import brought in even when a named import of the same name, resolved later, shadows it, such as use foo::X when foo has both pub use bar::* and pub use baz::X. A use in a block could likewise bind its first segment to an outer scope's name that the block's own glob import shadows. A glob-imported name used as the first segment of a use path is now reliably reported as ambiguous when an ingot or builtin type has the same name, and imports that depend on each other in a cycle that no single resolution settles, such as use x as y and use y as x in a block whose outer scope has both, are reported as ambiguous instead of resolving arbitrarily.
  • An item that a module brings in through both a private and a pub import is now re-exported. Depending on the order of the uses, use b::S reported "S is not visible" when b had both use super::x::* and pub use super::a::*.
  • Array repeat lengths and array types computed from generic parameters, such as [0; N + 1], [0; word_len(N)], [u8; T::LEN] or a conditional over N, are now accepted. Such a length has one type wherever it is written, including across functions with different bounds and across ingots, an associated type whose length is a braced expression specializes with its impl, and a length that faults for a concrete instantiation is reported where it is written instead of crashing the compiler or failing with an internal error. A computed length or other generic constant that calls a non-const function is reported where the call is written.
  • Const arguments and bounds that were silently dropped or misreported are now checked where they are written: faulting or non-const const arguments in trait references, including those of a qualified path's trait, associated-type bindings and bounds, and effect-key arguments. An invalid type in a trait reference's arguments, such as [u8; true], is reported at the argument instead of dropping the bound or blaming the trait. A fault that only an applied default or an evaluated const fn reveals is reported at that use, pointing to the faulting computation. The arguments of an effect key or an associated-type bound must now satisfy their bounds (uses (p: Guarded<bool>) with T: Gate on Guarded), and so must a type written as an argument its type alias drops (Drop<Guarded<bool>> with type Drop<T> = u8), in every position. A trait whose parameter defaults to Self can now be an effect key: its provider is asked for itself, as in Has<X> for a provider X, instead of the unfulfillable Has<Self>. Associated-type bindings in impl headers are now rejected.
  • Crashes and hangs involving const generic bounds are fixed: a bound whose const argument reads another bound's constant (T: Size + Has<{ T::LEN }>), a bound that needs an enclosing impl's or trait's bounds to select its constant, computed bound arguments on impl and trait methods, and const parameters whose type depends on a computed bound argument. A method bound repeating its impl header's bound is no longer reported as stricter than the trait's.
  • EVM memory miscompiles are fixed: a loop's stack allocation could be reused while a pointer to the previous iteration's object was still live, heap allocations could overlap live objects written after them, and a function could free a buffer it returned.
  • Fixed a compiler crash at -O1 and above when inlining a call that never returns removed later calls in the same block.
  • Native builds no longer fail with unproven escape of local object when a reference to a local object is packed into a tuple or enum that stays in the function, as in let (_, ref n) = cell.peek() at -O0 or the drop of a matched enum payload.
  • Report a name that an effect binds and that a parameter, named or in its pattern, or a recv arm's pattern of the same signature also binds, such as fn f(c: u256) uses (c: Counter) or Set { total } uses (mut total), instead of letting one silently hide the other.

Performance improvements

  • Constant evaluation borrows and reads one element of a constant aggregate without copying the rest, so a loop over a large constant array runs in linear time. (#1620)
  • Reduce contract bytecode size by budgeting receive-handler inlining. (#1677)
  • Reduce ABI struct decoding overhead and generated contract code size. (#1679)
  • Avoid copying immutable aggregates when forwarding them through helper calls. (#1680)
  • Avoid heap buffers when reading immutable contract fields. (#1699)
  • Constants of a one-field struct, such as Wei::zero(), ether(1) or an Address constant, are now passed and stored as the plain word instead of as a reference to a copy in the contract's code. This avoids extra code wherever such a constant is passed to a function that is not inlined, and makes some contracts 32 bytes smaller. (#1719)
  • Checking nested generic types, such as W<W<W<u8>>> or Wrap<<Wrap<T as M>::P> as M>::P>, no longer takes time exponential in the nesting depth: around twenty levels took seconds, and deeper nesting hung fe check and the language server. (#1723)
  • Avoid loading enum payloads when querying tags or checking variants through places. (#1762)
  • Sonatina backend updates, including jump threading through loop conditions and range facts for calldata, return data, code and memory sizes, make the Ethereum deposit contract cheaper: deposit uses about 130 gas less, get_deposit_count 183 and get_deposit_root 109, and its runtime shrinks from 2,360 to 2,281 bytes. (#1798)
  • Checking and building small projects is faster: a function body that declares no items is lowered when it is first needed instead of whenever its module is loaded, so most of core and std is never lowered (fe check on a trivial file drops from 81 ms to 68 ms).
  • Contracts are smaller and use less gas: the backend reorders values on the EVM stack with the fewest swaps, and when a stack layout needs values that are not on the stack, it pushes or reloads just those values where it often spilled the whole stack to memory and reloaded it, also at -O0 (the eight contracts of a Fe port of Seaport shrink by 5.4% at -O1).
  • Contracts that call other contracts or revert with custom errors are smaller and use less gas: messages and errors are encoded in place after their selector instead of being encoded and then copied, revert_error keeps one shared instance per error type, and a bool encodes as one store (the eight contracts of a Fe port of Seaport shrink by 1.7% at -Os).
  • Loops use less gas at -O1 and -O2: each iteration ends in one conditional branch instead of a test at the top and a jump back to it, and when the stack needs reordering for the jump back, that jump sits just before the loop header (verifying a 64-level Merkle proof drops from 11,428 to 10,718 gas).
  • Native compilation reuses unchanged aggregate-initialization facts and enum-proof indexes across snapshots and control-flow joins, and borrows compound type definitions when computing native layouts.
  • Native tests now share one runtime package, object and executable across all source modules in a suite, compiling reachable helpers once while running each test in its own process. Native IR artifacts use <suite>.native.test.sona and <suite>.native.test.rmir; report artifacts live directly under artifacts/native/, with distinct test-<index> directories.
  • The parser is about twice as fast: a failed lookahead no longer scans to the end of the file (a file with thousands of shifts like w << 4 parsed quadratically), scopes no longer allocate, and tokens are lexed into a vector up front (parsing core and std drops from 36 ms to 17 ms).
  • fe build and fe test compile faster and use less CPU: the backend reuses analyses within its passes and optimizes functions in parallel only in large modules, semantic instances are shared across queries, fe test compiles a file's tests in parallel, and release binaries are built with whole-program optimization (running fe test on each of fe's 385 test fixture files takes a quarter less time and half the CPU).

Deprecations and Removals

  • AsBytes::as_bytes() and String<N>::as_bytes() now return PackedBytes<N>; String<N>::from_bytes() accepts packed bytes. Convert ordinary arrays explicitly with PackedBytes::from_array() and .to_array(). Custom AsBytes implementations now implement append_to; the generic __as_bytes intrinsic and implicit serialization of arbitrary integers, arrays, enums and structs have been removed. (#1694)
  • ETH amounts and block timestamps in std are now typed. Ctx::value, Ctx::balance and Ctx::selfbalance return Wei, and Ctx::timestamp returns Timestamp. Every value parameter takes Wei: the Call methods (call, try_call, send_value, raw_call, ...), the Create methods (create, create2, ...), clones::clone and clones::clone_deterministic, as do erc4337::pay_prefund and erc4337::validate_user_op. The missing_account_funds, max_cost and actual_gas_cost fields of erc4337::AccountMsg and erc4337::PaymasterMsg are Wei too. Write value: Wei::zero() in place of value: 0 and value: ether(1) or value: wei(n) for other amounts. ether, gwei, wei and the time unit functions and constants now return Wei and Duration instead of u256; use as_wei() or as_seconds() where a plain number is needed. (#1719)
  • Storage layouts change: narrow struct fields share slots the way Solidity packs struct members; StorageMap values use their type's native layout, and WordRepr and std::evm::word are removed; narrow signed values are stored as their own two's-complement bytes, though old sign-extended words still read back correctly; and storage and transient fields are numbered apart. Storage collections are no longer Copy handles and lose their SALT parameters: a collection takes its slot from the contract field that holds it, so pass collections as mut or view parameters instead of by value. Layout holes are removed: drop = _ defaults, _ arguments and const parameters that only stood for a slot. Effect handles are removed: EffectHandle, EffectHandleMut, core::effect_ref::{AddressSpace, read, write}, and the read and write methods of StorPtr and TStorPtr. Provide a pointer's target with with (mut *p), and declare a transient field as a TSlot<T>, read and written as field.value. (#1798)
  • DynArray moves from core::abi to std::abi (import it from std::abi), and get is an inherent method, so std::abi::DynArrayAccess is removed. The ABI-encoded std::abi::MemVec is removed: build a MemVec or MemArray of values and encode it with DynArray::from_slice. (#1810)
  • Collection lengths, indices and offsets are now usize instead of u256: the lengths, indexing and element and byte offsets of MemArray, MemSlice, MemSpan, MemBuffer, FixedMemBuffer, DynArray, Bytes, DynString, BytesView and StringView. Convert with as u256 where a word is needed. Raw pointer helpers, EVM opcode wrappers and ABI codec positions keep u256. (#1810)
  • Address's inner field is private to std, and std::evm::address_to_word is removed. Build an address with Address::new(word), which fails with Panic(0x11) if word has bits above the low 160 (at compile time for a constant), or Address::from_word_truncate(word), and read its word with address.word(). (#1820)
  • Byte inputs implement core::abi::ByteSource instead of ByteInput, with size returning a u64 in place of len. Every ByteSource is a ByteInput, whose len returns the size as a u256 for code that reads an input; nothing else implements ByteInput. (#1834)
  • The read and mut method pairs of core and std are now single mut? methods: IndexMut is removed (bound mut Index<I> and give index a mut mode instead), MemBuffer and FixedMemBuffer span_mut, MemBuffer::slice_mut, MemArray::as_mut_slice, MemSlice at_mut, split_at_mut, try_split_at_mut and from_raw_parts_mut, and Mutex::value_mut become span, slice, as_slice, at, split_at, try_split_at, from_raw_parts and value, taken at mut where their use needs it (let s = mut buf.span()). MemArray::get_mut becomes get and FixedStack::top_mut becomes top.
  • Driver and Producer lose their base parameter: a driver names its base as type Base, and Identity and Reversed take it, Identity<B>. Driver::Item, Driver::Extra, the Adapt trait, Copied and .copied() are removed; a loop copies a step's Copy elements by its pattern. FromProducer::collect takes the producer's P::Base.
  • core::functional::Fn<T, U> is replaced by Fn<shape A>: write F: Fn(own T) -> U for F: Fn<T, U>, and impl Fn(own T) -> U for X for impl Fn<T, U> for X (its fn call(self, _ x: own T) -> U is unchanged). Option::ok_or_else and Option::unwrap_or_else take a callable without parameters, Fn() -> E, called as f(), instead of a Fn<(), E> called as f(()).

Internal Changes - for Fe Contributors

  • Release notes are now assembled with eisenbote, a towncrier replacement written in Fe and built with the compiler being released, configured in eisenbote.toml. (#1787)