Skip to content

v26.6.0

Latest

Choose a tag to compare

@github-actions github-actions released this 10 Nov 17:55
· 5 commits to master since this release

Changelog

26.6.0 (2026-11-10)

Features

  • Consecutive contract fields narrower than a slot now share storage slots, as Solidity packs state variables: mut owner: Address, mut paused: bool, mut decimals: u8 take one slot instead of three. A packed field is read and written in place, keeping the rest of its slot. Fields placed with #[slot(e)], structs and other aggregates still take whole slots. This moves existing contract fields to new slots and offsets, so data stored by a contract compiled with an earlier version must be migrated. (#1826)

  • Solidity value types now pack into shared storage slots inside structs, tuples and map entries, like bool and the narrow integers already do: Address takes 20 bytes, FixedBytes<N> takes N, the odd-width integers such as Uint24 take their width, and any other newtype (a struct with a single field) packs like its field. struct Rec { owner: Address, flag: bool, stamp: u64 } now takes one slot, as in Solidity. Writing a value its bytes cannot hold, such as a FixedBytes<4> whose val has more than four bytes, reverts. This moves fields of existing structs, tuples and map values to new offsets, so data stored by a contract compiled with an earlier version must be migrated. (#1826)

  • Owned values are destroyed exactly once when their owner gives them up: at the end of its scope, when overwritten, or for the fields a partial move leaves behind. A type releases what it holds in a core::ops::Drop implementation; core::mem::drop destroys a value early and core::mem::forget gives it up without destroying it. MemVec, MemArray, MemBuffer and FixedMemBuffer free their allocation and destroy their elements, and the new core::ptr::Box<T> holds one value in an allocation of its own. Operations that allocate gain fallible forms, such as try_with_capacity, try_reserve, MemVec::try_push and core::ptr::try_alloc_bytes, and core::ptr::dealloc frees raw allocations on native targets. A type may hold itself through a generic type that keeps its argument behind a pointer, as in struct Tree { children: MemVec<Tree> }. See docs/language/destruction.md. (#1870)

  • &= and ^= now work on bool. (#1882)

  • With the wasm Cargo feature, fe build --backend wasm compiles a file to a WebAssembly module (one linear memory, 32-bit usize) and a manifest of its imports and exports. Every non-generic pub fn at the root of the file is an export; besides bool and integers of at most 64 bits, an export can take std::wasm::HostText and return std::wasm::Reply, and extern functions become imports. fe test --backend wasm runs each test as its own module under Node.js. std::wasm provides the root effect std::wasm::Host, whose capabilities Log, Clock, Abort and the read-only files contract Files exports and tests take in uses, and pure Fe mocks of each. target = "wasm" in fe.toml selects this backend for fe check and fe test. See "WebAssembly output" in CLI.md. (#1904)

  • Added core::hash, a streaming Keccak-256 that hashes borrowed bytes without allocating, on any target. (#1909)

  • core::hint::black_box now works on native targets. Each use costs a call and a memory round trip. (#1911)

  • Native extern declarations can name any C function, including those core calls (malloc, free, write, fflush and abort), as long as every declaration of a symbol has the same C signature. Conflicting declarations are an error that names both. An #[unsafe(export)] function can replace a function that native code imports, such as malloc. (#1913)

  • DynArray::from_mem_vec builds an array from a MemVec it consumes, destroying each element once it is encoded. Bytes::zeroed allocates a value to be filled through span, like DynString::zeroed. (#1914)

  • core::hash adds SHA-256: Sha256, sha256 and sha256_word work like their Keccak-256 counterparts and run in software on every target. On the EVM, std::evm::crypto::sha256 still uses the cheaper precompile. (#1927)

  • core::text parsers and std::evm::parse_address accept any core::text::Text, implemented for String<N>, DynString, MemSpan, MemBuffer and std::env::Arg, so a process argument or a JSON string can be parsed without copying it: parse_uint(args.get(1)).

    MemSpan gains starts_with, ends_with, index_of, trim, split and lines: for (i, field) in row.split(", ").enumerate() { .. }. The searches and split take a core::text::Pattern, which is a byte (b',') or any Text except that split can't yet take a MemSpan. (#1928)

  • loop { .. } repeats its body until a break. A loop or while true that no break leaves never completes, so a function can end in one that returns from inside it, with no core::panic() after it. (#1937)

  • is_some, is_none, is_ok, is_err and unwrap work on the optional accesses projections return, such as v.get(i) and v.last(): v.get(0).unwrap() reads the element, v.last().unwrap() = x writes it, and node.child(0).unwrap().child(1) chains. On an empty variant unwrap panics as it does on a value. (#1938)

  • core::map::HashMap<K, V> is a hash table that owns its keys and values, in the prelude. get gives the value in place (mut?), insert and remove return the old value, get_or_insert gives the entry's value for update (counts.get_or_insert(word, 0) += 1), and map[key] panics on a missing key. Lookups take any key type the map's key compares with, so a DynString key is found with a literal or a span. for v in map visits the values, and keys() and entries() drive loops over keys and (key, value) pairs. Keys implement the new core::hash::Hash trait, which feeds a Hasher; FoldHasher is the map's. (#1939)

  • Byte spans, MemBuffer and DynString are ordered as text, and every MemSlice of Ord elements is ordered element by element. MemSlice, MemArray and MemVec have a sort method that sorts elements of any Ord type in place by swapping them, so a MemVec<DynString> or MemVec<MemBuffer> can be sorted; core::sort::sort still requires Copy elements. (#1940)

  • core::text::decode_hex decodes hex digits of either case, with or without a 0x prefix, to Bytes: decode_hex("0x00ff"). It returns None for an odd number of digits or a non-digit byte. (#1940)

  • An out-of-range array index now reverts with Panic(0x32), as in Solidity, instead of reverting without data. (#1941)

  • UFixed::parse and Fixed::parse read decimal text such as 0.25 or -1.5, with at most the type's number of fraction digits: Wad::parse("0.3"). UFixed and Fixed implement std::io::Display, writing the shortest exact text without trailing zeros, so format!(Wad::from_ratio(5, 2)) is 2.5. (#1942)

  • core::num::leading_zeros and trailing_zeros accept any integer type and return the count in that type. On native targets a u64 count is a single instruction. (#1966)

  • MemSlice::as_chunks<N>() views a slice as N-element arrays and the elements after them, as Rust's as_chunks does. An index into the chunks is checked against their count alone, so a loop over them needs no other check, and constant indices into a chunk need none. word_at(k) on byte chunks reads chunk k as the high bytes of a word. (#1971)

  • DynString::span and Bytes::span are mut?: a mut span writes the payload in place, leaving its length and padding unchanged, so a string allocated with zeroed can be filled without raw pointers. (#1971)

  • A byte span (MemSpan), a MemBuffer or a command-line argument (std::env::Arg) compares with a string, such as a literal: args.get(1) == "serve" checks a command-line argument. Slices (MemSlice<T>) compare with each other element by element.

  • A native pub fn main() may return nothing, and exits with 0.

  • A trait effect now provides its supertraits, and theirs in turn: in a function that uses (evm: mut Super), a call that needs Ctx or Call finds them. A supertrait does not provide its subtraits.

  • Byte literals, such as b'0' and b'\n', are u8 values.

  • Items and modules can be compiled conditionally with #[cfg(..)], which keeps an item only when its predicate holds: target = "evm" or "native", target_os = "linux" or "macos", or target_family = "unix". An item with several cfg attributes needs all of them to hold, and #![cfg(..)] at the top of a module applies to all of its items and to the modules in its directory. Native builds take the operating system from the host; EVM code and SP1 guests have none. cfg on fields, variants, associated items, extern functions and for loops is an error.

  • Native panics now report on stderr and abort, after flushing output written before them: panicked: arithmetic overflow for a Solidity panic code, the message of an assert! that has one, other revert data in hex, and panicked for core::panic. They used to stop at a bare SIGILL, losing buffered output. Native fe test now supports #[test(should_revert)], checking panic = ... and selector = ... against the revert data as on EVM. SP1 guests still trap.

  • Native programs receive standard output, error and input as effects. On a native target, a test or a top-level pub fn main takes its effects from the new root effect std::native::Host, as contracts take theirs from Evm: pub fn main() -> i32 uses (out: mut Stdout). std::io provides the sealed capabilities Stdout, Stderr and Stdin, and Stdio, which provides all three; the builtin macros print!, println!, eprint! and eprintln!, which write the text of each Display argument (integers, strings, bytes, bool) in one unbuffered write, and format!, which returns it in a MemBuffer; read_to_end; and StdinReader, which reads standard input a byte or a line at a time. std::env provides Args (count(), get(i)) and Env (lookup(name)), and std::process provides Exit (exit(status)). A test or main that asks for an effect its target's root effect does not provide, such as Evm on a native target or Stdout on the EVM, is now a type error.

  • The builtin macros write!(mut buf, a, b, ..) and writeln! append the text of each Display argument to an existing MemBuffer, writeln! then a newline, as format! builds a new one. The buffer is written as a mut argument and may be a local, a field or an effect, as in uses (text: mut MemBuffer).

  • The language server checks a project for the target in its fe.toml, and rechecks when that changes.

  • #[unsafe(export)] on a free pub fn makes a native build define the function under its own name with the C calling convention, kept even when nothing in Fe calls it, so C or Rust code linked with the build can call it. An exported function cannot be generic or take uses effects, and its parameters and result must be i32, u32, i64, u64, usize, isize or raw pointers. EVM and SP1 builds reject #[unsafe(export)].

  • MemBuffer::try_new(len) returns a buffer of len zero bytes, or None when the allocation fails.

  • MemVec::swap_remove(index) removes and returns an element in constant time, moving the last element into its place.

  • Option<T> compares with == and != when T does: two options are equal when both are None or both hold equal values.

  • core::num::to_be and from_be accept any integer type, the new to_le and from_le convert to and from little-endian order, and the new swap_bytes reverses the bytes of any integer. MemSpan and MemBuffer gain read_be, read_le, write_be and write_le, which read or write an integer of any width at any byte offset, reverting unless all its bytes are in range.

  • fe build --backend native --emit object writes <name>.o and --emit staticlib writes lib<name>.a, a library of the #[unsafe(export)] functions that C and Rust programs link like any static library. A library needs no main. See "Native output" in CLI.md.

  • fe build --backend native links host code into an executable: --link <PATH> adds an object file or archive, and --link-lib <NAME> a library as -l<NAME>. A Fe extern function can now be implemented in C or Rust, which can call the executable's #[unsafe(export)] functions back.

  • fe build --emit sizes writes <contract>.sizes.txt, the code bytes of each function in a contract's runtime and init sections, largest first.

  • std::fs gives native programs on Linux and macOS the host's files and directories through the sealed capability Fs, which main and tests take from std::native::Host: uses (fs: Fs) to read and uses (fs: mut Fs) to also change files. Fs provides read, write, append, create_dir_all, metadata (file type and size), read_dir (entry names and types) and canonicalize. A path is any Display value. A failure returns a std::io::Error holding the host's errno, which kind() classifies (NotFound, PermissionDenied, AlreadyExists, NotADirectory, IsADirectory, InvalidInput, Other) and which displays as the host's message.

  • std::process runs other programs on Linux and macOS through the sealed capability Spawn, which main and tests take from std::native::Host with uses (proc: mut Spawn). A Command names the program and sets its arguments, environment (env, env_remove), directory, standard streams and timeout: proc.output(Command::new("git").arg("status").stdout(Redirect::Capture)). Standard input is inherited, Null or Bytes; standard output and error are inherited, Null, captured, written to a File, or, for standard error, sent to standard output. spawn starts a Child and wait waits for it, while wait_any serves several children at once and returns the index of one that has ended. An Output holds the Status (an exit status, a signal, or TimedOut), the captured bytes, and the Usage: user and system CPU time and peak resident memory. A failure returns a std::io::Error.

  • std::time gives native programs on Linux and macOS the host's clocks through the sealed capability Clock, which main and tests take from std::native::Host with uses (clock: Clock). instant() reads the monotonic clock as an Instant, and now() the wall clock as the Duration since the Unix epoch. A Duration counts nanoseconds in a u64, with from_secs, from_millis, from_micros and from_nanos, the matching as_* methods, +, - and comparisons; Instant::duration_since gives the time between two readings, and instant + duration a later one.

  • target in fe.toml sets a project's target, "evm" or "native", on an ingot or a workspace. fe check, fe build and fe test use it unless --backend is passed.

Bugfixes

  • Selector checks and JSON ABI emission honor scalar and array SolCompat::SOL_TYPE declarations on structs with custom ABI codecs. (#1827)
  • A Snapshot receiver that is also provided as an effect, as in with (Handle = handle) { handle.read() } where read uses mut Handle, is now copied before the call instead of being rejected with an access conflict (16-0001). Writes through the effect change the provided place, not the receiver the method reads. (#1828)
  • Calling through a mutable storage Address, such as target.call(..) on a mut target: Address contract field in a recv arm, or through a function that uses it as an effect, no longer fails with an access conflict (16-0001). Address is Snapshot, so it is copied before the call. (#1828)
  • At -O 1 and -O 2, a checked operation whose result is unused, such as let m = b - (a & 255), now checks the operand as written. The optimizer dropped the & 255 and checked b - a, which reverted with Panic(0x11) for a = 2000 and b = 1000. An ^ operand was dropped the same way, so 2 - (a ^ 5) with a = 1 checked 2 - 1 and did not revert. (#1853)
  • On native targets, read_word and write_word of MemSpan and MemBuffer, and core::abi's word reads and writes, use big-endian byte order as on EVM; they used the target's native order. core::num::to_be and from_be convert a word between big-endian and the target's order. (#1870)
  • Option::is_some, Option::is_none, Result::is_ok and Result::is_err read the value instead of consuming it, so querying an Option or Result that owns a value leaves it usable. (#1870)
  • Division and remainder by zero in unchecked code now panic with Panic(0x12), as in checked code and Solidity, instead of returning zero. (#1882)
  • Unchecked arithmetic now applies to Wei, Duration and Timestamp, and to any type that implements the wrapping operator traits (WrappingAdd, WrappingSub, WrappingMul, and the new WrappingPow and WrappingNeg); operators on other types stay checked. A function's #[arithmetic(unchecked)] now also covers the closures and constant expressions in its body. (#1882)
  • Contracts with the same name in different modules of one ingot can be built and tested. When a name is shared, the artifacts are named by module path, for example lib__a__C.bin; --contract accepts that name, or a source name that only one contract has. Same-named tests in different modules each run their own code. settings.compilationTarget in metadata records the contract's path in its source file, such as inner::C. (#1890)
  • Reading a field through a raw pointer stored in a struct held in a local, such as unsafe { holder.node.value }, no longer fails with an internal access-checking error. (#1894)
  • On native targets, Bytes::word_at, and word_at and byte_at on a MemSpan byte source, no longer read up to 31 bytes past the end of the allocation when they reach the last bytes of a span. Such a read could fault when the allocation ended at unmapped memory. (#1903)
  • A contract field whose type names an associated type, such as mut v: <Nar as Tr>::Out or mut g: G<<Nar as Tr>::Out>, no longer fails to build with ContractFieldTypeMismatch. (#1908)
  • Fixed three native bugs. core::keccak, std::evm::keccak256 and keccak_words of data known only at runtime no longer fail to compile. A failed allocation now reverts with Panic(0x41) instead of trapping. std::evm::packed no longer truncates sizes above 64 bits, which let a packed_width of 2**64 write past its allocation. (#1909)
  • A use of an associated const found through a bound with an associated type binding, such as T::LEN under T: Len<Item = u8>, now matches the same constant reached another way. Array lengths built from such a use no longer fail with "expected [u8; T::LEN], but [u8; T::LEN] is given". (#1910)
  • Const bodies in a generic parameter's default no longer crash the compiler when the owner has a bound naming that parameter or a later one, such as T: Has<1> for trait Has<const N: usize, const L: usize = { N }>, or when the body is nested in the default, such as the array length in const L: usize = { let a: [u8; { N + 1 }] = [0; N + 1]; N + 1 }. A default that reads something only a bound on a later parameter provides is now rejected where it is declared. (#1910)
  • std::evm::byte and signextend, and the wrapping methods of std::abi::sol signed integers, no longer fail to compile on native targets. (#1911)
  • Using a pointer to a type that grows without bound, such as *Grow<u256> for struct Grow<T> { next: *Grow<[T; 1]> }, as a value now reports only the type's definition error instead of overflowing the compiler's stack. (#1912)
  • decode_base64, decode_base58, String<N> ABI decoding, checksum_address, std::evm::rlp, std::evm::merkle, mapping_slot, keccak_packed and clone_initcode no longer leak a scratch buffer on native targets. (#1914)
  • An associated type binding on anything but a trait, such as take<Item = u8>() or let _: G<Item = u8>, is now reported instead of silently accepted. (#1917)
  • An invalid type nested in a written type, such as W<G<W>>, is now reported once, where it's written, instead of again for each type around it. (#1917)
  • A name that two glob imports bring in is now reported as ambiguous instead of as not found in paths with generic arguments, such as dup<u8>(), and in generic arguments, such as K<N>. (#1917)
  • Generic arguments written in a body's paths and method calls are now checked as written types are. take<Missing>() and <G<Missing> as Tr>::f() were accepted, as was a private type in take<m::Hidden>(), and G<Missing>::mk() and W<Missing> { x: 0 } reported a missing method or an expected record instead of the argument's error. A shape argument in a written type, such as Sh<(mut u256,)>, is no longer rejected. (#1917)
  • A conditional or match with a diverging branch, such as if c { return 1 } else { zero() }, no longer gives its other branches the type !; their type is inferred from how the result is used. A binding of a value of type !, such as let w = if c { return 1 } else { return 2 }, takes its type from its uses too. (#1923)
  • A parameter of a known product shape, such as _ s: Pair::Step<ref> with shape Step = (own u256, mut? u256), can now be passed on, returned or captured by a closure whole instead of crashing the compiler. A closure copies its parts' values, and rejects a non-Copy part behind an access. (#1926)
  • A call whose effect row depends on which impl a string literal picks, such as fire(make(", ")) where make is generic over a trait implemented for both String<N> and DynString, no longer fails with a false "missing effect row" error (8-0106). (#1928)
  • A ref or mut binding of a part of a named product parameter now opens an access, as a binding on any place does. let (a, mut b) = s; let (c, mut d) = s with overlapping uses, and let (a, mut b) = s; bump(s); b += a, are now access conflicts instead of being accepted. let x = ref s.1 and let (ref x, ref y) = s.1 no longer fail with an internal error, nor does let i = s.1 for a Copy struct behind a mut part. Reading a non-Copy part with let c = s.1 now moves it, as through any access. (#1929)
  • A mut pattern binding of a packed storage field now writes to the field when the pattern is on a projection's result (let Small { mut a, .. } = s.me(), or in a match, if let or for) or is a refutable if let, let else or while let; the writes were silently lost. A projection called after && in a condition now runs the code after its yield, which was skipped. (#1933)
  • An access granted by a projection in storage can now be passed through a function that returns it, as in mut id(mut p.a_mut()) or mut pick(p.both()); this failed in MIR lowering with "the access checker places result component .. in Memory". (#1933)
  • ? in a method of a generic type that returns an Option or Result of an access, such as fn get(mut self) -> Option<mut T>, no longer fails with an internal access checking error (16-0004). (#1934)
  • A method called on the result of ? now opens the receiver's projection after the call's arguments, as it does for other receivers: cells.get(i)?.set(next(mut i)) gets the cell at the i that next leaves, and if get returns None, the arguments run before the early exit. (#1938)
  • Reading an enum from storage, transient storage, calldata or raw memory whose tag names no variant now reverts with Panic(0x21), as in Solidity. It used to reach an INVALID instruction, which consumes all remaining gas. (#1941)
  • The mut part of a product parameter, such as s.1 for s: Pair::Step<mut> with shape Step = (own u256, mut? u256), can now be assigned (s.1 = v, s.1 += v) and passed as mut s.1, as destructuring with let (_, mut b) = s already allowed. These were rejected with 8-0018 and 8-0066. Owned and ref parts stay read-only, and the error now names them instead of suggesting var s. (#1944)
  • StoragePackedArray no longer writes outside the array. Word w of the array at slot s now lies at keccak256(w ++ s), the entry w of a StorageMap<u256, u256> at s, as in Solady's LibMap and OpenZeppelin's BitMaps. It used to lie at keccak256(s) + w, so a large index wrapped around the slot space and could overwrite another contract field or a map's entries. This changes the array's storage layout: values written by an earlier release are not found at their new slots. (#1948)
  • An if let or while let pattern with | alternatives can now bind an access or a non-Copy value, as in if let Some((0 | 1, mut x)) = p.get() or if let (Some(t), Kind::A | Kind::B(0)) = make(). These failed with an internal access checking error (16-0004) or a use-after-move error. (#1949)
  • Report known non-scalar core::hint::black_box arguments during type checking, with a source location, instead of failing later during code generation. (#1955)
  • Warn when fe build emits runtime code or initcode exceeding the EIP-170 or EIP-3860 deployment limits. (#1956)
  • Fix an internal access-checking error when storing whole array rows whose lengths are const-function expressions of const generics. (#1957)
  • Report unsupported fixed-array event and custom-error fields with array-specific guidance instead of the tuple hint. (#1958)
  • Explain when a name in a type-level const expression resolves to a runtime parameter, including guidance for parameter/function name clashes. (#1959)
  • A recv arm field whose pattern destructures, such as V { t: (x, y) } or V { pair: Pair { n, .. } }, binds what the pattern names; before, compiling the arm failed with internal error 16-0004. (#1970)
  • A let ... else whose else block contains an if, a loop or another nested scope keeps the pattern's bindings; before, using them after the statement reported "undefined variable".
  • A contract field whose layout fails, such as one of an unresolved type, no longer makes every other field an unknown effect: only the bad field's error is reported, and the other fields still resolve in uses clauses and handler bodies.
  • A fixed array index out of bounds now panics natively with index out of bounds, not reverted without data.
  • A method call that a trait's impls tell apart by their trait arguments, such as c.get(true) with Get<u256> and Get<bool> impls, can now be an operator's operand; it failed with "type must be known" unless its result was annotated.
  • A msg variant or #[abi] struct field named like a constant in scope now decodes to its value. The decoded variant held the constant instead, so an arm V { n: x } got the constant in x, and an #[abi] struct was rejected with a refutable let on its declaration.
  • A negative integer constant in a pattern now matches its value. With const NEG: i256 = -2, the pattern NEG matched 2 and not -2.
  • A non-exhaustive match on a record variant or struct now shows which fields the uncovered values have, such as Not covered: `Shape::Rect { wide: false, .. }` , instead of only Shape::Rect { .. }.
  • A pattern naming a constant that isn't an integer or bool, such as a struct, enum or string constant, is now an error (8-0154), as is one whose value depends on generic parameters, like T::C (8-0155). A constant that fails to evaluate is now reported at each pattern that names it, like G<0>::C with const C: u256 = 1 / N; one whose definition is invalid or depends on the pattern itself is 8-0156. In a match these patterns failed later in compilation. In a let, a parameter or a recv arm they were accepted and matched anything: a recv arm V { pair: FIXED } ran for every V.
  • A recv arm whose pattern may not match every value of its variant is now an error (8-0153). With a constant n in scope, the arm V { n, k } ran for every V, and its n was the constant instead of the field. A literal field pattern, which the parser rejected, is reported by this check.
  • A string literal on the left of an operator, as in "x" == s, takes its type from the operator's impl instead of failing with "type must be known".
  • A tuple-variant or record pattern whose name doesn't resolve is now an error. Bar(y) => .. with no Bar in scope was accepted and then crashed the backend, and Foo { a } => .. was accepted whenever the matched value's type was a struct named Foo, even with that struct out of scope. A pattern naming an ambiguous variant, such as Some(x) with both a glob-imported and the prelude's Some, reports the ambiguity, and the field bindings of an unresolved record pattern no longer also report themselves as undefined. A bare name in a pattern that is ambiguous, such as None imported both by a glob and by the prelude, is reported as ambiguous instead of becoming a binding that matches anything. A unit pattern whose path names a method, like S::m, is reported instead of failing later in compilation.
  • An integer literal that does not fit its type, such as let x: u8 = 300 or a 256 pattern matched against a u8, is now rejected; it was silently truncated. As in Rust, a usize or isize literal must fit the type on the target being compiled for: 0x100000000 is a usize on the EVM and native targets, but not on WebAssembly.
  • Fix a native heap overflow when ABI-encoding an error or message with no fields.
  • Native code now loads and stores std::evm memory words big-endian, as the EVM does. A custom error's selector was lost from native revert data, and an encoded string literal had a garbled length.
  • Result::unwrap now works natively on error types without a Sol encoding, such as std::io::Error, and panics without revert data. On EVM, fe check reports such a call at its source location instead of failing during code generation.
  • core::panic_code no longer crashes unoptimized native programs with SIGSEGV: it wrote its revert data to address 0.
  • fe check reports a native main with the wrong return type, and extern or #[unsafe(export)] functions with types C can't pass, at their source locations.
  • fe check, fe test and fe build now report type errors in the built-in core and std ingots when the code being compiled reaches the affected function, for example while editing ingots/core with a debug build of fe. Previously such errors surfaced as an internal error like "semantic body is blocked by upstream causes" or "cannot structurally repack u256 as usize", or fe check passed and only code generation failed. The real diagnostics are now shown under "Errors in dependency" with the file in core or std.

Performance improvements

  • On native targets, core::num::leading_zeros compiles to a count-leading-zeros instruction on the highest nonzero 64-bit limb instead of an eight-step 256-bit shift ladder; trailing_zeros uses it too. fevm's CLZ handler shrinks from 578 instructions to 61. (#1895)
  • MemBuffer appends are faster, and growth takes the larger of double the capacity and the request, so a large append no longer over-allocates. MemVec grows the same way. String len, Display and comparison with a span no longer work a byte at a time. (#1915)
  • Native code keeps panics and other cold paths out of hot loops, including during register allocation, and tests small sets of constants with a bit mask instead of a jump table. On EVM, an allocation that is never read is now removed even when the function also uses scratch memory, as panic paths do. (#1921)
  • Native copies of enums that mix narrow and word-sized payloads no longer stall on the word they share. Destroying a large JSON document of numbers takes about 30% less time. (#1930)
  • Assigning over an enum in memory, such as list[i] = x, reads only its tag to find what to destroy, instead of copying the enum for each variant it tests. (#1930)
  • Checking instantiation limits no longer slows down superlinearly in programs with large cycles of mutually recursive functions, such as an interpreter whose opcode handlers dispatch to one another. fevm's conformance driver builds in 20 s and 4.7 GB of memory instead of 55 s and 13.3 GB. (#1960)
  • A local array read at a computed index, such as block[i] in a loop, is now stored in memory once, where it is defined. Each read used to copy the whole array into a new allocation first. (#1961)
  • On native targets, values passed to view parameters and read-only effects are copied less often. (#1963)
  • core::text::base64 reads each three-byte group at once and looks digits up in a table, so encoding on EVM costs about 65 gas per input byte instead of 284. (#1968)
  • On native targets, including SP1, usize, isize, raw memory pointers and references are 64 bits, the width of the target's addresses; they were 256-bit words, so every index, length and pointer was a four-limb value. A loop counting in usize runs about 30 times faster, and a MemVec scan about 45 times faster. MemVec and MemBuffer take 24 bytes and Box 8, and a main(argc: i32, argv: **u8) receives the host's argv without a copy.

Deprecations and Removals

  • Memory owners now destroy what they hold, which changes the meaning of some existing code. v[i] = x and set destroy the element they overwrite, so fill the uninitialized elements of MemArray::new_uninit or core::ptr::alloc_array through raw places (*array.ptr_at(i) = x). On native targets, a pointer into an owner dangles once the owner is destroyed: keep the owner alive, or core::mem::forget it. MemArray::into_bytes and into_buffer require Copy elements. std::native::ByteBuffer and FixedStack are removed in favor of MemBuffer and MemVec, which need no release call: indexing replaces byte_at and set_byte, and copy_within and copy_from are on buffer.span() and its slices. (#1870)
  • In #[arithmetic(unchecked)] code, an overflowing operator on a type parameter or associated type requires its wrapping trait: a + b on T: Add is now an error, and needs T: WrappingAdd to wrap, or a.add(b) to stay checked. Where the mode comes from fe.toml, such an operator without the bound calls Add. It used to wrap when instantiated at an integer, as x.add(y) on an integer did in unchecked code; both now panic on overflow. (#1882)
  • WrappingAdd, WrappingSub and WrappingMul take their operands own, as Add, Sub and Mul do, so an operator moves the same operands in checked and unchecked code. Implementations change fn wrapping_add(self, _ other: T) to fn wrapping_add(own self, _ other: own T). (#1882)
  • An SP1 guest that allocates now fails to link instead of failing to compile, because SP1's runtime has no malloc. (#1913)
  • DynString, Bytes and DynArray own their allocations and free them when dropped, so they no longer leak on native targets. They are no longer Copy: use clone() for a second value, or a view parameter to read one without moving it. from_abi_tail now takes ownership of the allocation it is given, so a pointer into another owner, such as a MemBuffer, must be copied first. DynString::as_bytes, whose result outlived the string, is replaced by into_bytes, which takes the string and keeps its allocation. PackedUserOperation is no longer Copy, and functions that send a Bytes or DynArray in a message, such as the ERC-721 and ERC-1155 helpers, take it by own. DynArray::from_slice and dyn_array_from no longer need T: Copy. Encode::encode and encode_field read their value through a view, so an implementation writes fn encode(self, _ out: AbiFrame) in place of own self, and binds a tuple's parts by ref. Fixed arrays encode and decode without T: Copy, so #[abi], #[event] and #[error] structs can hold fixed arrays of non-Copy elements. std::evm::clones::clone_initcode returns a FixedMemBuffer<55> that owns the code, in place of a span of memory that nothing owned. (#1914)
  • Renamed DynString::payload_span and Bytes::payload_span to span, matching MemBuffer, MemArray and Arg. (#1928)
  • Calling a function declared in an extern block now requires an unsafe block or function, unless the declaration is a const fn or belongs to core or std: nothing checks what foreign code does, so a call such as memset(p, 0, n) could corrupt memory from safe code.
  • Only the root module's pub fn main(), without parameters, is a program entry, on every target. A single-file EVM build no longer enters its other parameterless functions, so a file without contracts or pub fn main() builds nothing, and a main that is not pub, takes parameters or lies in an inline mod is an ordinary function.
  • MemBuffer::alloc and MemBuffer::alloc_uninit are renamed MemBuffer::new and MemBuffer::new_uninit.
  • as casts must be lossless on every target, where usize and isize take the target's width (32 bits on WebAssembly, 64 on native targets, 256 on EVM): u8, u16 and u32 convert to usize, usize to u256, and a memory pointer to and from usize, but u64 as usize, u256 as usize, usize as u32 and usize as u64 are rejected. Use downcast(), downcast_saturate() or downcast_unchecked() instead. A state pointer such as StorPtr<T> still converts to and from its u256 slot. core::size_of returns usize, and core::ptr's allocation sizes, copy lengths and offsets (alloc_bytes, offset, offset_bytes, zero_bytes, copy_raw) are usize.
  • std::io's character I/O is gone: HostIo, host(), the Read and Write traits, read_char, write_char, write_bytes, write and writeln. Use the Stdout, Stderr and Stdin effects with print!, println! or StdinReader instead. A native main takes no parameters: pub fn main(argc: i32, argv: **u8) and std::native::Args::new are gone; declare uses (args: Args) from std::env instead.