From 6e0dfdcbb1135818e568c36c84ce9a47b3eb6404 Mon Sep 17 00:00:00 2001 From: LJ <81748770+elle-j@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:39:15 +0200 Subject: [PATCH] Update newyork optimizer in compiler book (#549) ## Description Updates the compiler book to reflect the state of the newyork optimizer as of resolc `v1.3.0`. --- book/src/developer_guide/newyork_ir.md | 301 +++++++------ book/src/developer_guide/newyork_optimizer.md | 91 ++-- book/src/user_guide/cli.md | 12 +- book/src/user_guide/std_json.md | 6 + docs/404.html | 18 +- docs/developer_guide.html | 18 +- docs/developer_guide/architecture.html | 18 +- docs/developer_guide/contributing.html | 18 +- docs/developer_guide/cross_compilation.html | 18 +- docs/developer_guide/newyork_ir.html | 311 ++++++++------ docs/developer_guide/newyork_optimizer.html | 101 +++-- docs/developer_guide/target.html | 18 +- docs/developer_guide/testing.html | 18 +- docs/faq.html | 18 +- docs/index.html | 18 +- docs/print.html | 405 ++++++++++-------- docs/revive_runner.html | 18 +- docs/roadmap.html | 18 +- docs/searcher-c2a407aa.js | 2 +- docs/searchindex-339ac392.js | 1 + docs/searchindex-a6a49a5a.js | 1 - docs/user_guide.html | 18 +- docs/user_guide/cli.html | 26 +- docs/user_guide/differences.html | 18 +- docs/user_guide/installation.html | 18 +- docs/user_guide/js.html | 18 +- docs/user_guide/rust_libraries.html | 18 +- docs/user_guide/std_json.html | 21 +- docs/user_guide/tooling.html | 18 +- docs/welcome.html | 18 +- 30 files changed, 1048 insertions(+), 554 deletions(-) create mode 100644 docs/searchindex-339ac392.js delete mode 100644 docs/searchindex-a6a49a5a.js diff --git a/book/src/developer_guide/newyork_ir.md b/book/src/developer_guide/newyork_ir.md index 7d242a8..f157614 100644 --- a/book/src/developer_guide/newyork_ir.md +++ b/book/src/developer_guide/newyork_ir.md @@ -4,11 +4,15 @@ A per-operation reference for the newyork IR: textual syntax, operand and result ## How to read this reference -This appendix enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor. +This reference page enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor. Operations are grouped by function (memory and storage writes, pure expressions, control flow, and so on) rather than alphabetically. Jump to a specific operation from the [operation index](#operation-index) below, or use the sidebar. -Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either `Expression` or `Statement` in `ir.rs`. The textual rendering used by debug dumps and by this appendix is produced by the printer in `printer.rs`. Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations. +Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either `Expression` or `Statement` in `ir.rs`. The textual rendering used by debug dumps and by this reference page is produced by the printer in `printer.rs`. + +> [!NOTE] +> +> Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations. ### Entry format @@ -38,6 +42,26 @@ Syntax templates in each entry use the following conventions: | `/* … */` | Debug-only annotations the printer attaches to certain operations (memory region tag, static-slot hint, etc.). | | `…` | Repetition: "more entries of the same shape." Used in vector operand lists (`$arg_0, $arg_1, …`) and in multi-line block bodies (`{ … }`). | +For instance, this template: + +```text +let $result[: ] := and($lhs[: ], $rhs[: ]) +``` + +prints as: + +```text +let v2: i8 := and(v0, v1: i8) +``` + +`$result` rendered as `v2` with an `i8` type suffix, `$lhs` as `v0` at the default `i256` (type suffix omitted), and `$rhs` as `v1` with an `i8` type suffix. + +> [!NOTE] +> +> **A value's printed width is use-driven.** Type inference assigns each value a forward width from its definition, then widens it to satisfy its uses. The type suffix shown for a value in an example (such as `i8`) is therefore only illustrative — a short example may not show the uses that determine it, and the same operation can appear with a wider suffix, or none (it is omitted for the default `i256`), in another program. +> +> For instance, a value used as a memory offset widens to `i64`; as an address (a `call` target, `extcodesize`) to `i160`; stored as a full word (an `mstore`/`sstore` value) to `i256`; and an `add`/`mul` operand up to the `i64` register width. + ### Operation index #### Pure expressions @@ -197,17 +221,17 @@ Every value in the IR carries a `Type`. The operation entries below refer to wid ### `Type` -The umbrella enum. Three variants: +The umbrella enum, with these variants: | Variant | Printed as | Description | |---|---|---| -| `Int(BitWidth)` | `i1`, `i8`, …, `i256` | An integer at one of seven widths; see [BitWidth](#bitwidth). | +| `Int(BitWidth)` | `i1`, `i8`, …, `i256` | An integer at one of the [BitWidth](#bitwidth) widths. | | `Ptr(AddressSpace)` | `ptr`, `ptr`, `ptr`, `ptr` | A pointer tagged with its address space; see [AddressSpace](#addressspace). | | `Void` | `void` | Unit type. Used for statements that produce no value and for `void`-returning functions. | ### `BitWidth` -The seven rungs of integer width. Newly minted values default to `I256`; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused. +The rungs of integer width. Newly minted values default to `I256`; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused. | Variant | Printed as | Typical use | |---|---|---| @@ -262,9 +286,9 @@ A compile-time constant value with a declared type. New literals minted by the t #### Example ```text -let v0 := 0x2a // 42 at the default i256 -let v1 := 0x1: i1 // boolean true -let v2 := 0x80: i64 // narrowed by type inference +let v0: i8 := 0x2a +let v1: i1 := 0x1 // boolean true +let v2: i64 := 0x80 ``` #### Operands @@ -497,7 +521,7 @@ let v2 := sdiv(v0, v1) | Result | Purity | |---|---| -| `width(lhs)` | Pure | +| `max(width(lhs), width(rhs))` — a negative divisor can push the result to full width | Pure | #### Annotations @@ -583,12 +607,12 @@ None. #### Description -Modular exponentiation: `(lhs ^ rhs) mod 2^256`. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of `rhs`). +Modular exponentiation: `(base ^ exponent) mod 2^256`. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of `exponent`). #### Syntax ```text -exp($lhs[: ], $rhs[: ]) +exp($base[: ], $exponent[: ]) ``` #### Example @@ -601,8 +625,8 @@ let v2 := exp(v0, v1) | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Base. | -| `rhs` | `i256` | Exponent. | +| `base` | `i256` | Base. | +| `exponent` | `i256` | Exponent. | #### Result and purity @@ -632,7 +656,8 @@ and($lhs[: ], $rhs[: ]) ```text let v2 := and(v0, v1) -let v3 := and(v0, 0xff) // type inference narrows result to i8 +let v3: i8 := 0xff +let v4: i8 := and(v0, v3: i8) ``` #### Operands @@ -737,21 +762,21 @@ Logical left shift. Operand order follows EVM: `shl(shift, value)` computes `val #### Syntax ```text -shl($lhs[: ], $rhs[: ]) +shl($shift[: ], $value[: ]) ``` #### Example ```text -let v2 := shl(v0, v1) // v1 shifted left by v0 bits +let v2 := shl(v0, v1) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Shift amount in bits. | -| `rhs` | `i256` | Value to shift. | +| `shift` | `i256` | Shift amount in bits. | +| `value` | `i256` | Value to shift. | #### Result and purity @@ -774,7 +799,7 @@ Logical right shift. Operand order follows EVM: `shr(shift, value)` computes `va #### Syntax ```text -shr($lhs[: ], $rhs[: ]) +shr($shift[: ], $value[: ]) ``` #### Example @@ -787,14 +812,14 @@ let v2 := shr(v0, v1) | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Shift amount in bits. | -| `rhs` | `i256` | Value to shift. | +| `shift` | `i256` | Shift amount in bits. | +| `value` | `i256` | Value to shift. | #### Result and purity | Result | Purity | |---|---| -| If `lhs` is a known constant `k`: tier holding `256 - k` bits (or `i1` for `k ≥ 256`). Otherwise: `width(rhs)`. | Pure | +| If `shift` is a known constant `k`: tier holding `256 - k` bits (or `i1` for `k ≥ 256`). Otherwise: `width(value)`. | Pure | #### Annotations @@ -811,7 +836,7 @@ Arithmetic (signed) right shift. Operand order follows EVM: `sar(shift, value)` #### Syntax ```text -sar($lhs[: ], $rhs[: ]) +sar($shift[: ], $value[: ]) ``` #### Example @@ -824,14 +849,14 @@ let v2 := sar(v0, v1) | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Shift amount in bits. | -| `rhs` | `i256` | Value to shift, treated as signed. | +| `shift` | `i256` | Shift amount in bits. | +| `value` | `i256` | Value to shift, treated as signed. | #### Result and purity | Result | Purity | |---|---| -| Same shape as [`shr`](#shr) forward inference (constant shift narrows the result; non-constant falls back to `width(rhs)`). | Pure | +| `width(value)` — unlike [`shr`](#shr), sign-extension means a constant shift cannot narrow the result | Pure | #### Annotations @@ -854,7 +879,7 @@ lt($lhs[: ], $rhs[: ]) #### Example ```text -let v2 := lt(v0, v1) +let v2: i1 := lt(v0, v1) ``` #### Operands @@ -891,7 +916,7 @@ gt($lhs[: ], $rhs[: ]) #### Example ```text -let v2 := gt(v0, v1) +let v2: i1 := gt(v0, v1) ``` #### Operands @@ -928,7 +953,7 @@ slt($lhs[: ], $rhs[: ]) #### Example ```text -let v2 := slt(v0, v1) +let v2: i1 := slt(v0, v1) ``` #### Operands @@ -965,7 +990,7 @@ sgt($lhs[: ], $rhs[: ]) #### Example ```text -let v2 := sgt(v0, v1) +let v2: i1 := sgt(v0, v1) ``` #### Operands @@ -1002,7 +1027,7 @@ eq($lhs[: ], $rhs[: ]) #### Example ```text -let v2 := eq(v0, v1) +let v2: i1 := eq(v0, v1) ``` #### Operands @@ -1033,21 +1058,21 @@ Extract a single byte from a 256-bit word. `byte(i, x)` returns the *i*-th byte #### Syntax ```text -byte($lhs[: ], $rhs[: ]) +byte($index[: ], $word[: ]) ``` #### Example ```text -let v2 := byte(v0, v1) // v0 = byte index, v1 = word +let v2: i8 := byte(v0, v1) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Byte position; `0` = most significant byte. Values `≥ 32` yield `0`. | -| `rhs` | `i256` | Source word. | +| `index` | `i256` | Byte position; `0` = most significant byte. Values `≥ 32` yield `0`. | +| `word` | `i256` | Source word. | #### Result and purity @@ -1070,21 +1095,21 @@ Sign-extend an integer from a byte position. Per EVM, `signextend(b, x)` treats #### Syntax ```text -signextend($lhs[: ], $rhs[: ]) +signextend($byte_position[: ], $value[: ]) ``` #### Example ```text -let v2 := signextend(v0, v1) // v0 = byte position, v1 = value +let v2 := signextend(v0, v1) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `lhs` | `i256` | Byte position of the sign byte (0–31). | -| `rhs` | `i256` | Source value. | +| `byte_position` | `i256` | Byte position of the sign byte (0–31). | +| `value` | `i256` | Source value. | #### Result and purity @@ -1189,7 +1214,7 @@ iszero($operand[: ]) #### Example ```text -let v1 := iszero(v0) +let v1: i1 := iszero(v0) ``` #### Operands @@ -1297,8 +1322,8 @@ truncate>($value[: ]) #### Example ```text -let v1 := truncate(v0) -let v2 := truncate(v1) +let v1: i64 := truncate(v0) +let v2: i8 := truncate(v1: i64) ``` #### Operands @@ -1395,7 +1420,7 @@ None. #### Description -Compute the Keccak-256 hash of `length` bytes of emulated EVM linear memory starting at `offset`. The general-purpose hashing primitive; the two specialized variants below cover the common scratch-space patterns more compactly. +Compute the Keccak-256 hash of `length` bytes of emulated EVM linear memory starting at `offset`. The general-purpose hashing primitive; the specialized variants below cover the common scratch-space patterns more compactly. #### Syntax @@ -1432,7 +1457,7 @@ None. #### Description -Compound hash of two 256-bit words. Equivalent to `mstore(0, word0); mstore(32, word1); keccak256(0, 64)` but emitted as a single outlined call after the keccak-fusion pass recognizes the pattern. The mapping-key idiom; see also [`mapping_sload`](#mapping_sload). +Compound hash of two 256-bit words. Equivalent to `mstore(0, word0); mstore(32, word1); keccak256(0, 64)` but emitted as a single outlined call after `mem_opt`'s keccak fusion recognizes the pattern. The mapping-key idiom; see also [`mapping_sload`](#mapping_sload). #### Syntax @@ -1469,7 +1494,7 @@ None. #### Description -Compound hash of a single 256-bit word. Equivalent to `mstore(0, word0); keccak256(0, 32)` but emitted as a single outlined call after the keccak-fusion pass. +Compound hash of a single 256-bit word. Equivalent to `mstore(0, word0); keccak256(0, 32)` but emitted as a single outlined call after `mem_opt`'s keccak fusion. #### Syntax @@ -1516,7 +1541,7 @@ caller() #### Example ```text -let v0 := caller() +let v0: i160 := caller() ``` #### Operands @@ -1584,7 +1609,7 @@ origin() #### Example ```text -let v0 := origin() +let v0: i160 := origin() ``` #### Operands @@ -1618,7 +1643,7 @@ address() #### Example ```text -let v0 := address() +let v0: i160 := address() ``` #### Operands @@ -1675,7 +1700,7 @@ None. #### Description -Remaining gas at the point of evaluation. Modelled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect `gas` as a barrier. +Remaining gas at the point of evaluation. Modeled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect `gas` as a barrier. #### Syntax @@ -1686,7 +1711,7 @@ gas() #### Example ```text -let v0 := gas() +let v0: i64 := gas() ``` #### Operands @@ -1720,7 +1745,7 @@ msize() #### Example ```text -let v0 := msize() +let v0: i64 := msize() ``` #### Operands @@ -1754,7 +1779,7 @@ coinbase() #### Example ```text -let v0 := coinbase() +let v0: i160 := coinbase() ``` #### Operands @@ -1788,7 +1813,7 @@ timestamp() #### Example ```text -let v0 := timestamp() +let v0: i64 := timestamp() ``` #### Operands @@ -1822,7 +1847,7 @@ number() #### Example ```text -let v0 := number() +let v0: i64 := number() ``` #### Operands @@ -1890,7 +1915,7 @@ gaslimit() #### Example ```text -let v0 := gaslimit() +let v0: i64 := gaslimit() ``` #### Operands @@ -2035,7 +2060,7 @@ let v1 := blockhash(v0) | Name | Type | Notes | |---|---|---| -| `number` | `i256` | Block number; forward analysis widens to at least `i64`. | +| `number` | `i256` | Block number; forward analysis widens to `i256`. | #### Result and purity @@ -2168,7 +2193,7 @@ calldatasize() #### Example ```text -let v0 := calldatasize() +let v0: i64 := calldatasize() ``` #### Operands @@ -2191,7 +2216,7 @@ None. #### Description -Length of the most recently returned data buffer from a sub-call, in bytes. Modelled as pure per IR but reflects the last `ExternalCall` / `Create` result; consumers must respect that ordering. +Length of the most recently returned data buffer from a sub-call, in bytes. Modeled as pure per IR but reflects the last `ExternalCall` / `Create` result; consumers must respect that ordering. #### Syntax @@ -2202,7 +2227,7 @@ returndatasize() #### Example ```text -let v0 := returndatasize() +let v0: i64 := returndatasize() ``` #### Operands @@ -2236,7 +2261,7 @@ codesize() #### Example ```text -let v0 := codesize() +let v0: i64 := codesize() ``` #### Operands @@ -2270,7 +2295,7 @@ extcodesize($address[: ]) #### Example ```text -let v1 := extcodesize(v0: i160) +let v1: i64 := extcodesize(v0: i160) ``` #### Operands @@ -2378,8 +2403,8 @@ mload($offset[: ]) [/* */] #### Example ```text -let v1 := mload(v0) -let v2 := mload(v3) /* free_ptr */ +let v1 := mload(v0: i64) +let v2: i32 := mload(v3: i64) /* free_ptr */ ``` #### Operands @@ -2485,7 +2510,7 @@ None. The IR does not track a static slot for `tload`. #### Description -Compound load for a Solidity mapping element. Equivalent to `mstore(0, key); mstore(32, slot); sload(keccak256(0, 64))` but emitted as a single outlined call after the `compound_outlining` pass recognizes the pattern (it fuses a `keccak256_pair` — itself produced by `mem_opt`'s keccak fusion — followed by an `sload` whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load. +Compound load for a Solidity mapping element. Equivalent to `mstore(0, key); mstore(32, slot); sload(keccak256(0, 64))` but emitted as a single outlined call after the `mapping_access_outlining` pass recognizes the pattern (it fuses a `keccak256_pair` — itself produced by `mem_opt`'s keccak fusion — followed by an `sload` whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load. #### Syntax @@ -2569,7 +2594,7 @@ datasize("") #### Example ```text -let v0 := datasize("MyContract_deployed") +let v0: i64 := datasize("MyContract_deployed") ``` #### Operands @@ -2641,7 +2666,7 @@ linkersymbol("") #### Example ```text -let v0 := linkersymbol("contracts/Library.sol:L") +let v0: i160 := linkersymbol("contracts/Library.sol:L") ``` #### Operands @@ -2701,7 +2726,7 @@ let v4, v5 := returns_two(v0) // multi-return via let multi-binding ## Memory and storage writes -The six operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses. +The operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses. ### `mstore` @@ -2763,7 +2788,7 @@ mstore8($offset[: ], $value[: ]) [/* */] #### Example ```text -mstore8(v0, v1: i8) // value narrowed to i8 by type inference +mstore8(v0, v1: i8) ``` #### Operands @@ -2804,7 +2829,7 @@ mcopy($dest[: ], $src[: ], $length[: ]) #### Example ```text -mcopy(v0, v1, v2) // dest, src, length +mcopy(v0, v1, v2) ``` #### Operands @@ -2910,7 +2935,7 @@ None. Unlike `sstore`, the IR does not track a static slot for `tstore`: transie #### Description -Compound store for a Solidity mapping element. Equivalent to the three-operation sequence `mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value)` but emitted as a single outlined statement after the `compound_outlining` pass recognizes the pattern (it fuses a `keccak256_pair` followed by an `sstore` whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement. +Compound store for a Solidity mapping element. Equivalent to `mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value)` but emitted as a single outlined statement after the `mapping_access_outlining` pass recognizes the pattern (it fuses a `keccak256_pair` followed by an `sstore` whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement. #### Syntax @@ -2921,14 +2946,14 @@ mapping_sstore($key[: ], $slot[: ], $value[: ]) #### Example ```text -mapping_sstore(v0: i160, v1, v2) // address key, declared slot, value +mapping_sstore(v0, v1, v2) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `key` | `i256` | Mapping key. Often narrowed to `i160` for address keys. | +| `key` | `i256` | Mapping key. The outlining pass force-widens it to `i256`, so it always prints at full width, even for address keys. | | `slot` | `i256` | The mapping's declared storage slot. Typically a small constant. | | `value` | `i256` | The value to store at the computed storage location. | @@ -2944,7 +2969,7 @@ None. `mapping_sstore` deliberately drops the `static_slot` annotation that the ## Bulk copies -Multi-byte memory copies from the five EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. All five take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes. +Multi-byte memory copies from the EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. They all take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes. ### `codecopy` @@ -3008,7 +3033,7 @@ extcodecopy(v0: i160, v1, v2, v3) | Name | Type | Notes | |---|---|---| -| `address` | `i256` | Account whose code to read; narrows to `i160`. | +| `address` | `i256` | Account whose code to read; forward analysis widens to at least `i160`. | | `dest` | `i256` | Destination byte offset in linear memory. | | `offset` | `i256` | Source byte offset in the external code. | | `length` | `i256` | Number of bytes to copy. | @@ -3139,7 +3164,7 @@ None. ## Bindings and wrappers -The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in the appendix's earlier sections appears on the right-hand side of one of these statements (almost always `let`). +The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in this reference's earlier sections appears on the right-hand side of one of these statements (almost always `let`). ### `let` @@ -3185,7 +3210,7 @@ None. #### Description -Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a user-defined function call (`Expression::Call`) whose return values the source code discarded, or another Yul expression statement that does not have a dedicated `Statement::` variant. EVM external calls (`call`, `delegatecall`, etc.) and contract creation (`create`, `create2`) translate to dedicated `Statement::ExternalCall` and `Statement::Create` variants, not through this wrapper. +Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a zero-return (void) user-defined function call (`Expression::Call`) evaluated for its side effects, or the discarded void result of a Yul builtin used as a statement (a value-producing expression is bound by a `let` instead). EVM external calls (`call`, `delegatecall`, etc.) and contract creation (`create`, `create2`) translate to dedicated `Statement::ExternalCall` and `Statement::Create` variants, not through this wrapper. #### Syntax @@ -3196,7 +3221,7 @@ $expression #### Example ```text -keccak256(v0, v1) // hash computed but not bound to a value +update_balance(v0) // void function called for its side effects ``` #### Operands @@ -3232,7 +3257,7 @@ setimmutable("", $value[: ]) #### Example ```text -setimmutable("MyContract.owner", v0: i160) +setimmutable("MyContract.owner", v0) ``` #### Operands @@ -3255,7 +3280,7 @@ setimmutable("MyContract.owner", v0: i160) ## Structured control flow -The IR's control flow is structured: `if`, `switch`, and `for` are statements with explicit nested regions, each carrying input values and yielding output values. The three jump-like statements (`break`, `continue`, `leave`) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow. +The IR's control flow is structured: `if`, `switch`, and `for` are statements with explicit nested regions, each carrying input values and yielding output values. The jump-like statements (`break`, `continue`, `leave`) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow. ### `if` @@ -3274,13 +3299,14 @@ if $condition[: ] [[$input_0, $input_1, …]] { … } [else { … }] #### Example ```text -if v0 { +if v0: i1 { sstore(v1, v2) } -let v5, v6 := if v3 [v1, v2] { - let v7 := add(v2, 0x1) - yield v1, v7 +let v5, v6 := if v3: i1 [v1, v2] { + let v7: i64 := 0x1 // add widens its operands to the i64 register width + let v8 := add(v2, v7: i64) + yield v1, v8 } else { yield v1, v2 } @@ -3373,30 +3399,35 @@ Structured loop with explicit loop-carried variables. Each iteration evaluates ` ```text for { $variable_0 := $initial_0[, …] } - [// condition_statements: + [// condition statements: …] - $condition - { // post + condition: $condition + post [($post_input_variable_0[, …])] { … } -{ - … body … -} + body { + … body … + } ``` #### Example ```text -for { v1 := 0x0 } - lt(v1, 0xa) - { // post - let v3 := add(v1, 0x1) - yield v3 +let v0: i1 := 0x0 +let v6 := for { v1 := v0: i1 } + // condition statements: + let v2: i8 := 0xa + condition: lt(v1, v2: i8) + post (v3) { + let v4: i64 := 0x1 + let v5 := add(v3, v4: i64) + yield v5 + } + body { + sstore(v1, v1) + 0x0: void + yield v1 } -{ - sstore(v1, v2) - yield v1 -} ``` #### Operands @@ -3405,7 +3436,7 @@ for { v1 := 0x0 } |---|---|---| | `initial_values` | `Vec` | Starting values for the loop-carried variables. | | `loop_variables` | `Vec` | SSA ids visible inside condition, body, and post. | -| `condition_statements` | `Vec` | Statements evaluated each iteration *before* the condition expression; emitted into the loop header block. Printed only when non-empty, behind a `// condition_statements:` comment. | +| `condition_statements` | `Vec` | Statements evaluated each iteration *before* the condition expression; emitted into the loop header block. Printed only when non-empty, behind a `// condition statements:` comment. | | `condition` | `Expression` | Re-evaluated each iteration; non-zero continues, zero exits. | | `body` | `Region` | Loop body; yields current loop-carried values. | | `post_input_variables` | `Vec` | Input SSA ids for the post region (one per loop-carried variable); receive the body's yielded values merged with continue-site values via phi nodes in the LLVM codegen. | @@ -3439,12 +3470,12 @@ break #### Example ```text -if v0 { break } +if v0 { break [v1, v2] } ``` #### Operands -None in the printed form; the IR's `values: Vec` field carries the loop-carried values internally. +The loop-carried `values: Vec` print in brackets when non-empty (e.g. `break [v1, v2]`). #### Result and purity @@ -3473,12 +3504,12 @@ continue #### Example ```text -if v0 { continue } +if v0 { continue [v1, v2] } ``` #### Operands -None in the printed form; the IR's `values` field carries the loop-carried values internally. +The loop-carried `values` print in brackets when non-empty (e.g. `continue [v1, v2]`). #### Result and purity @@ -3568,7 +3599,7 @@ None. ## External interaction -Statements that cross the contract boundary: external calls (four kinds), contract creation (two kinds), and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses. +Statements that cross the contract boundary: external calls, contract creation, and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses. ### `call` @@ -3587,20 +3618,20 @@ let $result := call($gas[: ], $address[: ], $value[: ], $args_ #### Example ```text -let v8 := call(v0, v1: i160, v2, v3, v4, v5, v6) +let v8 := call(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `gas` | `i256` | Gas to forward to the target. | -| `address` | `i256` | Callee address; narrows to `i160`. | +| `gas` | `i256` | Gas to forward to the target; forward analysis widens to at least `i64`. | +| `address` | `i256` | Callee address; forward analysis widens to at least `i160`. | | `value` | `i256` | Wei to transfer with the call. | -| `args_offset` | `i256` | Calldata source offset in linear memory. | -| `args_length` | `i256` | Calldata length in bytes. | -| `ret_offset` | `i256` | Return-data destination offset in linear memory. | -| `ret_length` | `i256` | Maximum return-data length. | +| `args_offset` | `i256` | Calldata source offset in linear memory; forward analysis widens to at least `i64`. | +| `args_length` | `i256` | Calldata length in bytes; forward analysis widens to at least `i64`. | +| `ret_offset` | `i256` | Return-data destination offset in linear memory; forward analysis widens to at least `i64`. | +| `ret_length` | `i256` | Maximum return-data length; forward analysis widens to at least `i64`. | #### Result and purity @@ -3618,7 +3649,7 @@ None. #### Description -Deprecated EVM opcode that executes the callee's code in the caller's context but with the callee's storage. Preserved for Solidity compatibility; new code should use [`delegatecall`](#delegatecall). +Deprecated EVM opcode that executes the callee's code in the caller's context but with the callee's storage. Not supported by the newyork backend (codegen rejects it); use [`delegatecall`](#delegatecall) instead. #### Syntax @@ -3629,7 +3660,7 @@ let $result := callcode($gas[: ], $address[: ], $value[: ], $a #### Example ```text -let v8 := callcode(v0, v1: i160, v2, v3, v4, v5, v6) +let v8 := callcode(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64) ``` #### Operands @@ -3663,7 +3694,7 @@ let $result := delegatecall($gas[: ], $address[: ], $args_offset[: < #### Example ```text -let v7 := delegatecall(v0, v1: i160, v2, v3, v4, v5) +let v7 := delegatecall(v0: i64, v1: i160, v2: i64, v3: i64, v4: i64, v5: i64) ``` #### Operands @@ -3697,7 +3728,7 @@ let $result := staticcall($gas[: ], $address[: ], $args_offset[: ], $offset[: ], $length[: ]) #### Example ```text -let v4 := create(v0, v1, v2) +let v4 := create(v0, v1: i64, v2: i64) ``` #### Operands @@ -3739,8 +3770,8 @@ let v4 := create(v0, v1, v2) | Name | Type | Notes | |---|---|---| | `value` | `i256` | Wei to transfer to the new contract. | -| `offset` | `i256` | Linear-memory offset of the init code. | -| `length` | `i256` | Length of the init code in bytes. | +| `offset` | `i256` | Linear-memory offset of the init code; forward analysis widens to at least `i64`. | +| `length` | `i256` | Length of the init code in bytes; forward analysis widens to at least `i64`. | #### Result and purity @@ -3769,7 +3800,7 @@ let $result := create2($value[: ], $offset[: ], $length[: ], $ #### Example ```text -let v5 := create2(v0, v1, v2, v3) +let v5 := create2(v0, v1: i64, v2: i64, v3) ``` #### Operands @@ -3803,16 +3834,16 @@ log($offset[: ], $length[: ][, $topic_0[: ], …]) #### Example ```text -log0(v0, v1) -log2(v0, v1, v2, v3) // two topics +log0(v0: i64, v1: i64) // no topics +log2(v0: i64, v1: i64, v2, v3) // two topics ``` #### Operands | Name | Type | Notes | |---|---|---| -| `offset` | `i256` | Data source offset in linear memory. | -| `length` | `i256` | Data length in bytes. | +| `offset` | `i256` | Data source offset in linear memory; forward analysis widens to at least `i64`. | +| `length` | `i256` | Data length in bytes; forward analysis widens to at least `i64`. | | `topics` | `Vec` | Zero to four indexed topic values; the length determines the mnemonic suffix. | #### Result and purity @@ -3827,7 +3858,7 @@ None. ## Termination -Statements that end the current call frame. Three plain forms (`return`, `revert`, `stop`), two unconditional traps (`invalid`, `selfdestruct`), and three outlined revert variants that encode common Solidity error patterns into single nodes that can be deduplicated across call sites. +Statements that end the current call frame. Plain forms (`return`, `revert`, `stop`), unconditional traps (`invalid`, `selfdestruct`), and outlined revert variants (`panic_revert`, `error_string_revert`, `custom_error_revert`) that encode common Solidity error patterns into single nodes that can be deduplicated across call sites. ### `return` @@ -3846,15 +3877,15 @@ return($offset[: ], $length[: ]) #### Example ```text -return(v0, v1) +return(v0: i64, v1: i64) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `offset` | `i256` | Return-data source offset. | -| `length` | `i256` | Return-data length. | +| `offset` | `i256` | Return-data source offset; forward analysis widens to at least `i64`. | +| `length` | `i256` | Return-data length; forward analysis widens to at least `i64`. | #### Result and purity @@ -3883,15 +3914,15 @@ revert($offset[: ], $length[: ]) #### Example ```text -revert(v0, v1) +revert(v0: i64, v1: i64) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `offset` | `i256` | Revert-data source offset. | -| `length` | `i256` | Revert-data length. | +| `offset` | `i256` | Revert-data source offset; forward analysis widens to at least `i64`. | +| `length` | `i256` | Revert-data length; forward analysis widens to at least `i64`. | #### Result and purity @@ -3995,7 +4026,7 @@ selfdestruct(v0: i160) | Name | Type | Notes | |---|---|---| -| `address` | `i256` | Recipient of the contract's balance; narrows to `i160`. | +| `address` | `i256` | Recipient of the contract's balance; forward analysis widens to at least `i160`. | #### Result and purity @@ -4086,7 +4117,7 @@ None — the string length and data are compile-time fields, not SSA operands. #### Description -Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero to three argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset `0`. +Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero or more argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset `0`. #### Syntax @@ -4097,14 +4128,14 @@ custom_error_revert(0x, [$arg_0, $arg_1, …]) #### Example ```text -custom_error_revert(0xa28c4c1100000000000000000000000000000000000000000000000000000000, [v0, v1]) +custom_error_revert(0xa28c4c11, [v0, v1]) ``` #### Operands | Name | Type | Notes | |---|---|---| -| `arguments` | `Vec` | 0–3 argument values; the selector is a compile-time field. | +| `arguments` | `Vec` | Zero or more argument values; the selector is a compile-time field. | #### Result and purity @@ -4116,4 +4147,4 @@ custom_error_revert(0xa28c4c1100000000000000000000000000000000000000000000000000 | Source field | Printed as | |---|---| -| `selector: BigUint` | The 4-byte error selector shifted left by 224 bits, printed in hex in the first syntax position. | +| `selector: BigUint` | The 4-byte error selector in hex, in the first syntax position. The selector is stored left-shifted by 224 bits; the printer right-shifts it back and prints the bare 4-byte value. | diff --git a/book/src/developer_guide/newyork_optimizer.md b/book/src/developer_guide/newyork_optimizer.md index 68b30bf..b027554 100644 --- a/book/src/developer_guide/newyork_optimizer.md +++ b/book/src/developer_guide/newyork_optimizer.md @@ -3,7 +3,7 @@ The `newyork` crate (`crates/newyork/`) introduces an additional intermediate representation (IR) layer between Yul and LLVM IR. It enables domain-specific optimizations that neither `solc` nor LLVM can perform on their own, because they lack semantic knowledge about the cross-domain compilation from EVM to PolkaVM. > [!NOTE] -> The newyork optimizer is experimental. It is gated behind the `RESOLC_USE_NEWYORK=1` environment variable (for standard JSON mode) or the `--newyork` CLI flag, and not yet enabled by default. +> The newyork optimizer is experimental. It is gated behind the `--newyork` CLI flag or the `settings.polkavm.newyork` field in standard JSON input, and not yet enabled by default. ## Motivation @@ -31,13 +31,15 @@ Yul AST ──────────► │ newyork IR │ 3. dedup (exact + fuzzy) │ │ 4. mem_opt + fmp_prop + keccak_fold │ │ 5. simplify (pass 2) │ - │ 6. compound_outlining + guard_narrow │ + │ 6. mapping_access_outlining + guard_narrow │ │ 7. simplify (pass 3) │ │ 8. dedup (exact + fuzzy, pass 2) │ │ ── recursive on subobjects ── │ - │ 9. heap_opt (analysis) │ - │ 10. type_inference (4 iterative rounds) │ - │ 11. validate │ + │ 9. type_inference (iterative narrowing) │ + │ 10. late inline loop: inline, simplify, outline, │ + │ guard-narrow, simplify, dedup, narrow │ + │ 11. heap_opt (analysis) │ + │ 12. validate │ └──────────────────────────────────────────────────┘ ``` @@ -51,15 +53,16 @@ The optimizer runs the following passes in order: 6. **Compound outlining** -- detects `keccak256_pair` + `sload`/`sstore` sequences and fuses them into `MappingSLoad`/`MappingSStore` IR nodes, eliminating intermediate hash values. **Guard narrowing** -- detects `if gt(val, MASK) { revert }` and `iszero(eq(val, and(val, MASK)))` patterns and inserts AND-mask narrowing, giving type inference proof that values fit in fewer bits. 7. **Simplify** (pass 3) -- propagates opportunities created by compound outlining and guard narrowing. 8. **Function deduplication** (pass 2) -- catches new duplicates exposed by guard narrowing and compound outlining canonicalization. -9. **Heap analysis** -- analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking. -10. **Type inference** -- narrows 256-bit values to smaller widths (`I1`, `I8`, `I32`, `I64`, `I128`, `I160`) where provable. Runs iteratively for up to 4 cascading refinement rounds, combining forward min-width propagation, backward use-context demands, transparent-operation demand propagation, and interprocedural parameter/return narrowing. -11. **Validation** -- checks SSA well-formedness (use-before-def, multiple definitions), yield count consistency, and function reference correctness. +9. **Type inference** -- narrows 256-bit values to smaller widths (`I1`, `I8`, `I32`, `I64`, `I128`, `I160`) where provable. Runs iteratively for up to 4 cascading refinement rounds, combining forward min-width propagation, backward use-context demands, transparent-operation demand propagation, and interprocedural parameter/return narrowing. +10. **Late inline loop** -- now that narrowing and simplification have shrunk wrapper functions below the inline thresholds, re-runs inlining, simplification, mapping access outlining, guard narrowing, deduplication, and type inference to collect the residual opportunities. +11. **Heap analysis** -- analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking. +12. **Validation** -- checks SSA well-formedness (use-before-def, multiple definitions), yield count consistency, and function reference correctness. -Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-11 run on the full object tree. +Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-12 run on the full object tree. ## IR design -The newyork IR is an SSA form with structured control flow, inspired by MLIR's SCF dialect. Key design choices: +The newyork IR is an SSA form with structured control flow, inspired by [MLIR's SCF dialect](https://mlir.llvm.org/docs/Dialects/SCFDialect/). Key design choices: - **Explicit types with address spaces**: Every value carries a bit-width (`I1`, `I8`, `I32`, `I64`, `I128`, `I160`, `I256`) and pointers carry address space information (`Heap`, `Stack`, `Storage`, `Code`). All values start as `I256` and are narrowed by type inference. - **Pure expressions vs. effectful statements**: Expressions compute values without side effects; statements perform memory, storage, or control flow effects. This separation simplifies analysis and rewriting. @@ -75,7 +78,7 @@ For per-operation detail — printed syntax, operand and result types, and more EVM operates on 256-bit words, but most values in practice fit in 32 or 64 bits. The type inference pass performs bidirectional analysis: - **Forward**: computes minimum width from literal values and operation semantics (e.g., `add(I64, I8)` produces `I65`, rounded up to `I128`). -- **Backward use tracking**: classifies each value's uses into 9 context categories (`MemoryOffset`, `MemoryValue`, `StorageAccess`, `Comparison`, `Arithmetic`, `FunctionArg`, `FunctionReturn`, `ExternalCall`, `General`). All categories conservatively demand the full `I256` width by default; the categorisation is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — `mload(2^128)` aliased to `mload(0)` because the bounds check ran on an already-truncated value (commit `ccca38df`). +- **Backward use tracking**: classifies each value's uses into 9 context categories (`MemoryOffset`, `MemoryValue`, `StorageAccess`, `Comparison`, `Arithmetic`, `FunctionArg`, `FunctionReturn`, `ExternalCall`, `General`). All categories conservatively demand the full `I256` width by default; the categorization is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — `mload(2^128)` aliased to `mload(0)` because the bounds check ran on an already-truncated value (commit `ccca38df`). - **Transparent demand propagation**: for modular-arithmetic operations (`Add`, `Sub`, `Mul`, `And`, `Or`, `Xor`), propagates narrow demands backward through operands, exploiting the property that `trunc(op(a,b), N) == op(trunc(a,N), trunc(b,N))`. - **Interprocedural**: iteratively narrows function parameter and return types in up to four rounds, combining four narrowing strategies — body-driven parameter narrowing, caller-driven parameter narrowing, forward-based return narrowing, and demand-based return narrowing — and re-running full inference between rounds. Parameters are clamped to at least `I32` (XLEN on PolkaVM). @@ -219,24 +222,24 @@ Additionally, common exit patterns (revert with constant length, zero-value retu ### Integration test contracts -Reproducible with `cargo test --package revive-integration -- codesize`. The `main` column is the value committed to `crates/integration/codesize.json` on `main`; the `newyork` column is the value produced by the same test with `RESOLC_USE_NEWYORK=1` set, currently committed on this branch. +Reproducible with `cargo test --package revive-integration -- codesize` for the *Via Yul IR* column (`crates/integration/codesize.json`) and `cargo test --package revive-integration --features newyork -- codesize` for the *Via newyork IR* column (`crates/integration/codesize_newyork.json`). -| Contract | main (bytes) | newyork (bytes) | Reduction | +| Contract | Via Yul IR (bytes) | Via newyork IR (bytes) | Reduction | |---|---|---|---| -| Baseline | 870 | 479 | −44.9% | -| Computation | 2,418 | 1,376 | −43.1% | -| DivisionArithmetics | 9,327 | 7,192 | −22.9% | -| ERC20 | 17,160 | 10,138 | −40.9% | -| Events | 1,662 | 1,279 | −23.0% | -| FibonacciIterative | 1,427 | 949 | −33.5% | -| Flipper | 2,240 | 1,123 | −49.9% | -| SHA1 | 8,009 | 6,286 | −21.5% | +| Baseline | 838 | 493 | −41.2% | +| Computation | 2,368 | 1,217 | −48.6% | +| DivisionArithmetics | 11,444 | 7,370 | −35.6% | +| ERC20 | 18,057 | 8,726 | −51.7% | +| Events | 1,614 | 909 | −43.7% | +| FibonacciIterative | 1,373 | 969 | −29.4% | +| Flipper | 2,205 | 1,058 | −52.0% | +| SHA1 | 7,830 | 6,264 | −20.0% | ### OpenZeppelin contracts -Measured by running `oz-tests/oz.sh` against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot — there is no committed measurement file in the repo, so these may drift as the optimizer evolves; rerun the script for fresh figures. +Measured against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot. -| Contract | newyork (bytes) | +| Contract | Via newyork IR (bytes) | |---|---| | oz_gov | 81,840 | | erc721 | 52,634 | @@ -250,13 +253,15 @@ Measured by running `oz-tests/oz.sh` against real-world contracts generated with For comparison, building the same contracts without the newyork optimizer at the equivalent snapshot produced **563,526** bytes total — a reduction of about **−43%** across the corpus. -Per-contract reductions in the integration suite range from roughly **−21%** (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimise) to nearly **−50%** (Flipper, where the optimiser strips away most of Solidity's dispatch and storage-access scaffolding). +Per-contract reductions in the integration suite range from roughly **−20%** (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimize) to about **−52%** (Flipper, where the optimizer strips away most of Solidity's dispatch and storage-access scaffolding). ## Development history and challenges -The newyork optimizer was developed over roughly three months — from early February 2026 through early May 2026 — largely through AI-assisted pair programming with Claude. The development progressed through several distinct phases: +The first version of the newyork optimizer was authored collaboratively and reviewed extensively by the `revive` maintainers, as well as Claude Opus, Claude Fable, Qwen, Minimax and Deepseek LLMs, over a span of many months — from early February 2026 through mid-June 2026. -**Phase 1 -- Initial scaffolding**: The first draft established the core IR data structures, Yul-to-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline. +The development progressed through several distinct phases: + +**Phase 1 -- Initial scaffolding**: The first draft established the core IR data structures, Yul-to-newyork-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline. **Phase 2 -- Optimization passes**: Once the baseline was stable, optimization passes were added iteratively: inlining, simplification, memory optimization, function deduplication, keccak256 fusion, and type inference. Each pass was validated against differential tests comparing EVM and PVM execution. @@ -267,6 +272,10 @@ The newyork optimizer was developed over roughly three months — from early Feb **Phase 4 -- Measuring and tuning**: Systematic measurement of OpenZeppelin contracts revealed which optimizations had the most impact and which approaches regressed performance. +Throughout development the optimizer was validated against the existing integration and differential test suites (containing over 30,000 test cases), which run each contract on both EVM and PVM and assert identical state changes. + +The newyork compiler pipeline introduced no new regressions over these test suites. This was achieved by careful manual reviews and many LLM bughunt loops. Additionally, a final security review by Anthropic's Fable 5 LLM found no remaining soundness issues. As with any new compiler feature, it should still be treated as experimental as of now. + ### Approaches that did not work | Approach | Outcome | @@ -275,6 +284,9 @@ The newyork optimizer was developed over roughly three months — from early Feb | NoInline on `__revive_int_truncate` | +62% regression: PolkaVM call overhead exceeds inline cost | | Native FMP memory (inline sbrk) | Mixed: small contracts improved, large ones regressed from sbrk bloat | | Shared overflow trap block | Mixed: prevented LLVM from eliminating individual dead overflow checks | +| Aggressive IR-level single-call inlining | Regressed large contracts (ERC20 +6.1%): merged bodies become monolithic functions LLVM can't optimize, so large functions are deferred to LLVM's inliner instead | +| Type-inference narrowing of `mload(0x40)` to I32 | Regressed small contracts (+252 bytes): conflicts with the codegen FMP range proof; the bound is exposed via a trunc→zext pair instead | +| Full simplifier re-run after `mem_opt` | Mixed: helped small ERC20 (−293 bytes) but regressed the OZ stablecoin (+72 bytes); replaced by a targeted keccak-only fold | These results highlight a recurring theme: interacting well with LLVM's own optimization passes is critical. Optimizations at the IR level can inadvertently inhibit LLVM's downstream passes, sometimes causing surprising regressions. @@ -282,26 +294,23 @@ These results highlight a recurring theme: interacting well with LLVM's own opti The following opportunities have been identified but are not yet implemented: -- **Bitwise algebraic simplifications**: `BitAnd`, `BitOr`, `BitXor` identity patterns fall through without simplification. -- **Cross-control-flow memory optimization**: Memory state is conservatively cleared at `if`/`switch`/`for` boundaries. Preserving state across simple branches would enable more load-after-store eliminations. +- **Memory optimization across loop boundaries**: Tracked memory state is cleared around `for` loop condition, body, and post blocks, so load-after-store eliminations do not carry across loop iterations. Preserving loop-invariant state would recover more eliminations. - **Adaptive inlining thresholds**: Current thresholds are static constants. Profile-guided or contract-size-aware heuristics could improve decisions for diverse contract sizes. -- **Extended fuzzy deduplication**: The current pass only compares functions by structure of `Let` bindings. Extending to consider literals inside `MStore`, `Return`, `Revert`, and `Log` statements would find more deduplication opportunities. -- **Type checking in validation**: The validator checks SSA well-formedness and structural correctness, but does not yet verify type consistency of operations (the `TypeMismatch` error variant exists but is not yet wired). +- **Extended fuzzy deduplication**: The current pass only parameterizes literals in `Let` bindings and `SStore` slots. Extending it to consider literals inside `MStore`, `Return`, `Revert`, and `Log` statements would find more deduplication opportunities. +- **Type checking in validation**: The validator checks SSA well-formedness and structure, but not operation type consistency. Type discipline is maintained by construction (type inference and codegen), with LLVM's IR verifier as the backstop. - **Loop variable narrowing**: Loop-carried variables are conservatively widened to `I256`. Reaching a fixed-point across loop iterations could allow narrower types for simple counters. +- **Functions with `leave` inside a `for` loop are not inlined**: the IR-level inliner defers such functions to LLVM's inliner, so they miss the interprocedural constant propagation and width narrowing the IR-level pass provides. -## Environment variables +## Debug output -A small set of environment variables controls or inspects the newyork pipeline. Only `RESOLC_USE_NEWYORK` affects generated bytecode; the others are read-only inspection knobs used while debugging the optimizer. +Passing `--debug-output-dir ` makes the newyork pipeline write IR and analysis artifacts for each compiled contract into that directory. The dumps are produced automatically whenever the directory is set. -| Variable | Effect | +| File | Content | |---|---| -| `RESOLC_USE_NEWYORK=1` | Routes Yul lowering through the newyork pipeline. Equivalent to passing `--newyork` on the command line; the CLI flag and this variable are OR-ed by `resolve_use_newyork` (`crates/resolc/src/lib.rs`). | -| `RESOLC_DEBUG_IR` | When set, prints the translated newyork IR for every object to stderr. Additionally writes `/.newyork.txt` whenever the debug config carries an output directory. | -| `RESOLC_DEBUG_HEAP` | When set, appends per-object heap-analysis details — native regions/offsets, taintedness, dynamic escapes, escaping ranges — to `/resolc_heap_debug.log`. Requires the debug config to carry an output directory. | -| `NEWYORK_DUMP_IR` | When set, writes the IR for every translated object to `/tmp/newyork_ir_.txt` from inside `translate_yul_object` (`crates/newyork/src/lib.rs`). Independent of `RESOLC_DEBUG_IR` — fires before codegen and needs no output directory. | -| `RESOLC_DEBUG_BLOB` | Test harness only. Dumps the compiled PVM blob to `/tmp/debug_blob_.pvm` and the LLVM IR debug directory to `/tmp/debug_llvm_newyork` or `/tmp/debug_llvm_yul`. Used by `crates/resolc/src/test_utils.rs` when comparing newyork against the Yul path. | - -All of these gate on presence/value at the start of compilation; flipping them mid-run has no effect. +| `.newyork` | Final optimized IR, annotated with the inferred type widths | +| `.snapshot.newyork` | IR snapshot taken before the late passes (only when captured during translation) | +| `.heap.newyork` | Heap analysis summary (native regions/offsets, taintedness, escapes, dynamic accesses) | +| `.mem.newyork` | Memory optimization counters (loads/stores eliminated, keccak fusions, FMP loads eliminated) | ## Module reference @@ -316,7 +325,7 @@ All of these gate on presence/value at the start of compilation; flipping them m | `type_inference.rs` | Bidirectional integer width narrowing with transparent demand propagation | | `mem_opt.rs` | Load-after-store elimination, keccak256 fusion, FMP propagation | | `heap_opt.rs` | Heap access pattern analysis, alignment tracking, byte-swap elimination | -| `compound_outlining.rs` | Mapping access pattern detection and fusion (`keccak256_pair` + `sload`/`sstore`) | +| `mapping_access_outlining.rs` | Mapping access pattern detection and fusion (`keccak256_pair` + `sload`/`sstore`) | | `guard_narrow.rs` | Guard pattern detection and AND-mask narrowing insertion | | `validate.rs` | IR well-formedness checks (SSA, yields, function references) | | `printer.rs` | Human-readable IR pretty printer with configurable output | diff --git a/book/src/user_guide/cli.md b/book/src/user_guide/cli.md index 918d6b7..51b8a04 100644 --- a/book/src/user_guide/cli.md +++ b/book/src/user_guide/cli.md @@ -11,7 +11,7 @@ We aim to keep the `resolc` CLI usage close to `solc`. There are a few things an -O, --optimization ``` -`resolc` exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies wiedly between different optimization levels. +`resolc` exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies widely between different optimization levels. (This is independent of `--newyork` which selects the IR lowering pipeline.) Valid levels are the following: - `0`: No optimizations are applied. @@ -23,6 +23,13 @@ Valid levels are the following: By default, `-Oz` is applied. +### newyork IR pipeline +```bash +--newyork +``` + +Enables the [newyork optimizer](../developer_guide/newyork_optimizer.md) to reduced compiled contract code size, by routing Yul lowering through the **experimental** newyork IR pipeline instead of the standard Yul-to-LLVM path. Composes with `--yul`, `--combined-json`, and the default Solidity mode. In standard JSON mode this flag is rejected; enable the pipeline via the [`settings.polkavm.newyork`](std_json.md#settingspolkavmnewyork) input field instead. Off by default. + ### Stack size ```bash --stack-size @@ -69,7 +76,7 @@ Specify the path to the `solc` executable. By default, the one in `${PATH}` is u --debug-output-dir ``` -Dump all intermediary compiler artifacts to files in the specified directory. This includes the YUL IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. Useful for debugging and development purposes. +Dump all intermediary compiler artifacts to files in the specified directory. This includes the Yul IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. When the newyork pipeline is active, the [newyork IR is additionally dumped](../developer_guide/newyork_optimizer.md#debug-output) (the final IR, a pre-late-pass snapshot, and heap and memory optimization data). Useful for debugging and development purposes. ### Debug info ```bash @@ -110,4 +117,3 @@ Example: > [!NOTE] > > Tooling is supposed to take care of this. In the future, we may append explicit linkage data to simplify the deploy time linking feature. - diff --git a/book/src/user_guide/std_json.md b/book/src/user_guide/std_json.md index ac17496..5ccad81 100644 --- a/book/src/user_guide/std_json.md +++ b/book/src/user_guide/std_json.md @@ -10,6 +10,12 @@ Used to configure PVM specific compiler settings. A boolean value allowing to enable debug information. Corresponds to `resolc -g`. +### `settings.polkavm.newyork` + +A boolean value allowing to enable the experimental [newyork IR pipeline](../developer_guide/newyork_optimizer.md) for Yul lowering. Corresponds to `resolc --newyork`. Off by default. + +The output JSON includes which pipeline actually ran, via the top-level `resolc_pipeline` field (`"newyork"` or `"yul"` for the standard Yul-to-LLVM pipeline). + ### The `settings.polkavm.memoryConfig` object Used to apply PVM specific memory configuration settings. diff --git a/docs/404.html b/docs/404.html index 532f791..70e1867 100644 --- a/docs/404.html +++ b/docs/404.html @@ -36,7 +36,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-a6a49a5a.js"; + window.path_to_searchindex_js = "searchindex-339ac392.js"; @@ -201,6 +201,22 @@ + + @@ -212,6 +212,22 @@ + + @@ -245,6 +245,22 @@ + + @@ -258,6 +258,22 @@ + + @@ -231,6 +231,22 @@ make test-wasm + + @@ -177,9 +177,13 @@

newyork IR reference

A per-operation reference for the newyork IR: textual syntax, operand and result types, purity, region and static-slot annotations, and examples.

How to read this reference

-

This appendix enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor.

+

This reference page enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor.

Operations are grouped by function (memory and storage writes, pure expressions, control flow, and so on) rather than alphabetically. Jump to a specific operation from the operation index below, or use the sidebar.

-

Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either Expression or Statement in ir.rs. The textual rendering used by debug dumps and by this appendix is produced by the printer in printer.rs. Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations.

+

Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either Expression or Statement in ir.rs. The textual rendering used by debug dumps and by this reference page is produced by the printer in printer.rs.

+
+

Note

+

Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations.

+

Entry format

Each operation entry has the same shape:

@@ -216,6 +220,18 @@
+

For instance, this template:

+
let $result[: <type>] := and($lhs[: <type>], $rhs[: <type>])
+
+

prints as:

+
let v2: i8 := and(v0, v1: i8)
+
+

$result rendered as v2 with an i8 type suffix, $lhs as v0 at the default i256 (type suffix omitted), and $rhs as v1 with an i8 type suffix.

+
+

Note

+

A value’s printed width is use-driven. Type inference assigns each value a forward width from its definition, then widens it to satisfy its uses. The type suffix shown for a value in an example (such as i8) is therefore only illustrative — a short example may not show the uses that determine it, and the same operation can appear with a wider suffix, or none (it is omitted for the default i256), in another program.

+

For instance, a value used as a memory offset widens to i64; as an address (a call target, extcodesize) to i160; stored as a full word (an mstore/sstore value) to i256; and an add/mul operand up to the i64 register width.

+

Operation index

Pure expressions

Constants and variables
@@ -370,21 +386,21 @@

Type system

Every value in the IR carries a Type. The operation entries below refer to widths (i1…i256), address spaces (ptr<heap>, etc.), and memory regions (scratch, etc.) by their printed form; this section is the reference for those names.

Type

-

The umbrella enum. Three variants:

+

The umbrella enum, with these variants:

- +
VariantPrinted asDescription
Int(BitWidth)i1, i8, …, i256An integer at one of seven widths; see BitWidth.
Int(BitWidth)i1, i8, …, i256An integer at one of the BitWidth widths.
Ptr(AddressSpace)ptr<heap>, ptr<stack>, ptr<storage>, ptr<code>A pointer tagged with its address space; see AddressSpace.
VoidvoidUnit type. Used for statements that produce no value and for void-returning functions.

BitWidth

-

The seven rungs of integer width. Newly minted values default to I256; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused.

+

The rungs of integer width. Newly minted values default to I256; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused.

@@ -441,9 +457,9 @@
0x<hex>[: <type>]
 

Example

-
let v0 := 0x2a              // 42 at the default i256
-let v1 := 0x1: i1           // boolean true
-let v2 := 0x80: i64         // narrowed by type inference
+
let v0: i8 := 0x2a
+let v1: i1 := 0x1           // boolean true
+let v2: i64 := 0x80
 

Operands

None — literals are leaves.

@@ -664,7 +680,7 @@ let v2 := 0x80: i64 // narrowed by type inference
- +
ResultPurity
width(lhs)Pure
max(width(lhs), width(rhs)) — a negative divisor can push the result to full widthPure
@@ -743,9 +759,9 @@ let v2 := 0x80: i64 // narrowed by type inference

exp

(Expression::Binary with BinaryOperation::Exp)

Description

-

Modular exponentiation: (lhs ^ rhs) mod 2^256. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of rhs).

+

Modular exponentiation: (base ^ exponent) mod 2^256. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of exponent).

Syntax

-
exp($lhs[: <type>], $rhs[: <type>])
+
exp($base[: <type>], $exponent[: <type>])
 

Example

let v2 := exp(v0, v1)
@@ -757,8 +773,8 @@ let v2 := 0x80: i64         // narrowed by type inference
 NameTypeNotes
 
 
-lhsi256Base.
-rhsi256Exponent.
+basei256Base.
+exponenti256Exponent.
 
 
 
@@ -784,7 +800,8 @@ let v2 := 0x80: i64         // narrowed by type inference
 

Example

let v2 := and(v0, v1)
-let v3 := and(v0, 0xff)     // type inference narrows result to i8
+let v3: i8 := 0xff
+let v4: i8 := and(v0, v3: i8)
 

Operands

@@ -886,10 +903,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

Description

Logical left shift. Operand order follows EVM: shl(shift, value) computes value << shift. Shifts ≥ 256 produce 0.

Syntax

-
shl($lhs[: <type>], $rhs[: <type>])
+
shl($shift[: <type>], $value[: <type>])
 

Example

-
let v2 := shl(v0, v1)       // v1 shifted left by v0 bits
+
let v2 := shl(v0, v1)
 

Operands

@@ -898,8 +915,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Shift amount in bits. -rhsi256Value to shift. +shifti256Shift amount in bits. +valuei256Value to shift.
@@ -921,7 +938,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

Description

Logical right shift. Operand order follows EVM: shr(shift, value) computes value >> shift with zero-fill from the left. Shifts ≥ 256 produce 0.

Syntax

-
shr($lhs[: <type>], $rhs[: <type>])
+
shr($shift[: <type>], $value[: <type>])
 

Example

let v2 := shr(v0, v1)
@@ -933,8 +950,8 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
 NameTypeNotes
 
 
-lhsi256Shift amount in bits.
-rhsi256Value to shift.
+shifti256Shift amount in bits.
+valuei256Value to shift.
 
 
 
@@ -945,7 +962,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 ResultPurity -If lhs is a known constant k: tier holding 256 - k bits (or i1 for k ≥ 256). Otherwise: width(rhs).Pure +If shift is a known constant k: tier holding 256 - k bits (or i1 for k ≥ 256). Otherwise: width(value).Pure @@ -956,7 +973,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

Description

Arithmetic (signed) right shift. Operand order follows EVM: sar(shift, value) shifts value right by shift bits, preserving the sign bit. Shifts ≥ 256 saturate to 0 for non-negative values and to -1 (all-ones) for negative values.

Syntax

-
sar($lhs[: <type>], $rhs[: <type>])
+
sar($shift[: <type>], $value[: <type>])
 

Example

let v2 := sar(v0, v1)
@@ -968,8 +985,8 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
 NameTypeNotes
 
 
-lhsi256Shift amount in bits.
-rhsi256Value to shift, treated as signed.
+shifti256Shift amount in bits.
+valuei256Value to shift, treated as signed.
 
 
 
@@ -980,7 +997,7 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
 ResultPurity
 
 
-Same shape as shr forward inference (constant shift narrows the result; non-constant falls back to width(rhs)).Pure
+width(value) — unlike shr, sign-extension means a constant shift cannot narrow the resultPure
 
 
 
@@ -994,7 +1011,7 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
 
lt($lhs[: <type>], $rhs[: <type>])
 

Example

-
let v2 := lt(v0, v1)
+
let v2: i1 := lt(v0, v1)
 

Operands

@@ -1029,7 +1046,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
gt($lhs[: <type>], $rhs[: <type>])
 

Example

-
let v2 := gt(v0, v1)
+
let v2: i1 := gt(v0, v1)
 

Operands

@@ -1064,7 +1081,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
slt($lhs[: <type>], $rhs[: <type>])
 

Example

-
let v2 := slt(v0, v1)
+
let v2: i1 := slt(v0, v1)
 

Operands

@@ -1099,7 +1116,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
sgt($lhs[: <type>], $rhs[: <type>])
 

Example

-
let v2 := sgt(v0, v1)
+
let v2: i1 := sgt(v0, v1)
 

Operands

@@ -1134,7 +1151,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
eq($lhs[: <type>], $rhs[: <type>])
 

Example

-
let v2 := eq(v0, v1)
+
let v2: i1 := eq(v0, v1)
 

Operands

@@ -1166,10 +1183,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

Description

Extract a single byte from a 256-bit word. byte(i, x) returns the i-th byte of x with byte 0 being the most significant. If i ≥ 32, the result is 0.

Syntax

-
byte($lhs[: <type>], $rhs[: <type>])
+
byte($index[: <type>], $word[: <type>])
 

Example

-
let v2 := byte(v0, v1)      // v0 = byte index, v1 = word
+
let v2: i8 := byte(v0, v1)
 

Operands

@@ -1178,8 +1195,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Byte position; 0 = most significant byte. Values ≥ 32 yield 0. -rhsi256Source word. +indexi256Byte position; 0 = most significant byte. Values ≥ 32 yield 0. +wordi256Source word.
@@ -1201,10 +1218,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

Description

Sign-extend an integer from a byte position. Per EVM, signextend(b, x) treats byte b of x as the most significant byte of a smaller signed integer and extends its sign through the upper bytes.

Syntax

-
signextend($lhs[: <type>], $rhs[: <type>])
+
signextend($byte_position[: <type>], $value[: <type>])
 

Example

-
let v2 := signextend(v0, v1)  // v0 = byte position, v1 = value
+
let v2 := signextend(v0, v1)
 

Operands

@@ -1213,8 +1230,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Byte position of the sign byte (0–31). -rhsi256Source value. +byte_positioni256Byte position of the sign byte (0–31). +valuei256Source value.
@@ -1311,7 +1328,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
iszero($operand[: <type>])
 

Example

-
let v1 := iszero(v0)
+
let v1: i1 := iszero(v0)
 

Operands

@@ -1413,8 +1430,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
truncate<i<bits>>($value[: <type>])
 

Example

-
let v1 := truncate<i64>(v0)
-let v2 := truncate<i8>(v1)
+
let v1: i64 := truncate<i64>(v0)
+let v2: i8 := truncate<i8>(v1: i64)
 

Operands

@@ -1511,7 +1528,7 @@ let v2 := truncate<i8>(v1)

keccak256

(Expression::Keccak256)

Description

-

Compute the Keccak-256 hash of length bytes of emulated EVM linear memory starting at offset. The general-purpose hashing primitive; the two specialized variants below cover the common scratch-space patterns more compactly.

+

Compute the Keccak-256 hash of length bytes of emulated EVM linear memory starting at offset. The general-purpose hashing primitive; the specialized variants below cover the common scratch-space patterns more compactly.

Syntax

keccak256($offset[: <type>], $length[: <type>])
 
@@ -1546,7 +1563,7 @@ let v2 := truncate<i8>(v1)

keccak256_pair

(Expression::Keccak256Pair)

Description

-

Compound hash of two 256-bit words. Equivalent to mstore(0, word0); mstore(32, word1); keccak256(0, 64) but emitted as a single outlined call after the keccak-fusion pass recognizes the pattern. The mapping-key idiom; see also mapping_sload.

+

Compound hash of two 256-bit words. Equivalent to mstore(0, word0); mstore(32, word1); keccak256(0, 64) but emitted as a single outlined call after mem_opt’s keccak fusion recognizes the pattern. The mapping-key idiom; see also mapping_sload.

Syntax

keccak256_pair($word0[: <type>], $word1[: <type>])
 
@@ -1581,7 +1598,7 @@ let v2 := truncate<i8>(v1)

keccak256_single

(Expression::Keccak256Single)

Description

-

Compound hash of a single 256-bit word. Equivalent to mstore(0, word0); keccak256(0, 32) but emitted as a single outlined call after the keccak-fusion pass.

+

Compound hash of a single 256-bit word. Equivalent to mstore(0, word0); keccak256(0, 32) but emitted as a single outlined call after mem_opt’s keccak fusion.

Syntax

keccak256_single($word0[: <type>])
 
@@ -1620,7 +1637,7 @@ let v2 := truncate<i8>(v1)
caller()
 

Example

-
let v0 := caller()
+
let v0: i160 := caller()
 

Operands

None.

@@ -1670,7 +1687,7 @@ let v2 := truncate<i8>(v1)
origin()
 

Example

-
let v0 := origin()
+
let v0: i160 := origin()
 

Operands

None.

@@ -1695,7 +1712,7 @@ let v2 := truncate<i8>(v1)
address()
 

Example

-
let v0 := address()
+
let v0: i160 := address()
 

Operands

None.

@@ -1740,12 +1757,12 @@ let v2 := truncate<i8>(v1)

gas

(Expression::Gas)

Description

-

Remaining gas at the point of evaluation. Modelled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect gas as a barrier.

+

Remaining gas at the point of evaluation. Modeled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect gas as a barrier.

Syntax

gas()
 

Example

-
let v0 := gas()
+
let v0: i64 := gas()
 

Operands

None.

@@ -1770,7 +1787,7 @@ let v2 := truncate<i8>(v1)
msize()
 

Example

-
let v0 := msize()
+
let v0: i64 := msize()
 

Operands

None.

@@ -1795,7 +1812,7 @@ let v2 := truncate<i8>(v1)
coinbase()
 

Example

-
let v0 := coinbase()
+
let v0: i160 := coinbase()
 

Operands

None.

@@ -1820,7 +1837,7 @@ let v2 := truncate<i8>(v1)
timestamp()
 

Example

-
let v0 := timestamp()
+
let v0: i64 := timestamp()
 

Operands

None.

@@ -1845,7 +1862,7 @@ let v2 := truncate<i8>(v1)
number()
 

Example

-
let v0 := number()
+
let v0: i64 := number()
 

Operands

None.

@@ -1895,7 +1912,7 @@ let v2 := truncate<i8>(v1)
gaslimit()
 

Example

-
let v0 := gaslimit()
+
let v0: i64 := gaslimit()
 

Operands

None.

@@ -2013,7 +2030,7 @@ let v2 := truncate<i8>(v1) NameTypeNotes -numberi256Block number; forward analysis widens to at least i64. +numberi256Block number; forward analysis widens to i256.
@@ -2122,7 +2139,7 @@ let v2 := truncate<i8>(v1)
calldatasize()
 

Example

-
let v0 := calldatasize()
+
let v0: i64 := calldatasize()
 

Operands

None.

@@ -2142,12 +2159,12 @@ let v2 := truncate<i8>(v1)

returndatasize

(Expression::ReturnDataSize)

Description

-

Length of the most recently returned data buffer from a sub-call, in bytes. Modelled as pure per IR but reflects the last ExternalCall / Create result; consumers must respect that ordering.

+

Length of the most recently returned data buffer from a sub-call, in bytes. Modeled as pure per IR but reflects the last ExternalCall / Create result; consumers must respect that ordering.

Syntax

returndatasize()
 

Example

-
let v0 := returndatasize()
+
let v0: i64 := returndatasize()
 

Operands

None.

@@ -2172,7 +2189,7 @@ let v2 := truncate<i8>(v1)
codesize()
 

Example

-
let v0 := codesize()
+
let v0: i64 := codesize()
 

Operands

None.

@@ -2197,7 +2214,7 @@ let v2 := truncate<i8>(v1)
extcodesize($address[: <type>])
 

Example

-
let v1 := extcodesize(v0: i160)
+
let v1: i64 := extcodesize(v0: i160)
 

Operands

@@ -2299,8 +2316,8 @@ let v2 := truncate<i8>(v1)
mload($offset[: <type>]) [/* <region> */]
 

Example

-
let v1 := mload(v0)
-let v2 := mload(v3) /* free_ptr */
+
let v1 := mload(v0: i64)
+let v2: i32 := mload(v3: i64) /* free_ptr */
 

Operands

@@ -2418,7 +2435,7 @@ let v2 := sload(v3) /* slot: 0x0 */

mapping_sload

(Expression::MappingSLoad)

Description

-

Compound load for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sload(keccak256(0, 64)) but emitted as a single outlined call after the compound_outlining pass recognizes the pattern (it fuses a keccak256_pair — itself produced by mem_opt’s keccak fusion — followed by an sload whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load.

+

Compound load for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sload(keccak256(0, 64)) but emitted as a single outlined call after the mapping_access_outlining pass recognizes the pattern (it fuses a keccak256_pair — itself produced by mem_opt’s keccak fusion — followed by an sload whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load.

Syntax

mapping_sload($key[: <type>], $slot[: <type>])
 
@@ -2492,7 +2509,7 @@ let v2 := sload(v3) /* slot: 0x0 */
datasize("<id>")
 

Example

-
let v0 := datasize("MyContract_deployed")
+
let v0: i64 := datasize("MyContract_deployed")
 

Operands

None — the identifier is a quoted string literal in the syntax position, not an operand.

@@ -2560,7 +2577,7 @@ let v2 := sload(v3) /* slot: 0x0 */
linkersymbol("<path>")
 

Example

-
let v0 := linkersymbol("contracts/Library.sol:L")
+
let v0: i160 := linkersymbol("contracts/Library.sol:L")
 

Operands

None — the path is a quoted string literal in the syntax position.

@@ -2631,7 +2648,7 @@ let v4, v5 := returns_two(v0) // multi-return via let multi-binding

Memory and storage writes

-

The six operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses.

+

The operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses.

mstore

(Statement::MStore)

Description

@@ -2687,7 +2704,7 @@ mstore(v4, v5) /* free_ptr */ // offset is exactly 0x40
mstore8($offset[: <type>], $value[: <type>]) [/* <region> */]
 

Example

-
mstore8(v0, v1: i8)             // value narrowed to i8 by type inference
+
mstore8(v0, v1: i8)
 

Operands

@@ -2732,7 +2749,7 @@ mstore(v4, v5) /* free_ptr */ // offset is exactly 0x40
mcopy($dest[: <type>], $src[: <type>], $length[: <type>])
 

Example

-
mcopy(v0, v1, v2)               // dest, src, length
+
mcopy(v0, v1, v2)
 

Operands

@@ -2844,12 +2861,12 @@ sstore(v2, v3) /* slot: 0x0 */

mapping_sstore

(Statement::MappingSStore)

Description

-

Compound store for a Solidity mapping element. Equivalent to the three-operation sequence mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value) but emitted as a single outlined statement after the compound_outlining pass recognizes the pattern (it fuses a keccak256_pair followed by an sstore whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement.

+

Compound store for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value) but emitted as a single outlined statement after the mapping_access_outlining pass recognizes the pattern (it fuses a keccak256_pair followed by an sstore whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement.

Syntax

mapping_sstore($key[: <type>], $slot[: <type>], $value[: <type>])
 

Example

-
mapping_sstore(v0: i160, v1, v2)        // address key, declared slot, value
+
mapping_sstore(v0, v1, v2)
 

Operands

@@ -2858,7 +2875,7 @@ sstore(v2, v3) /* slot: 0x0 */ NameTypeNotes -keyi256Mapping key. Often narrowed to i160 for address keys. +keyi256Mapping key. The outlining pass force-widens it to i256, so it always prints at full width, even for address keys. sloti256The mapping’s declared storage slot. Typically a small constant. valuei256The value to store at the computed storage location. @@ -2878,7 +2895,7 @@ sstore(v2, v3) /* slot: 0x0 */

Annotations

None. mapping_sstore deliberately drops the static_slot annotation that the original sstore may have carried, because the fused statement’s effective slot is the keccak hash of the key and the declared slot, which is never a compile-time constant.

Bulk copies

-

Multi-byte memory copies from the five EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. All five take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes.

+

Multi-byte memory copies from the EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. They all take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes.

codecopy

(Statement::CodeCopy)

Description

@@ -2932,7 +2949,7 @@ sstore(v2, v3) /* slot: 0x0 */ NameTypeNotes -addressi256Account whose code to read; narrows to i160. +addressi256Account whose code to read; forward analysis widens to at least i160. desti256Destination byte offset in linear memory. offseti256Source byte offset in the external code. lengthi256Number of bytes to copy. @@ -3061,7 +3078,7 @@ sstore(v2, v3) /* slot: 0x0 */

Annotations

None.

Bindings and wrappers

-

The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in the appendix’s earlier sections appears on the right-hand side of one of these statements (almost always let).

+

The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in this reference’s earlier sections appears on the right-hand side of one of these statements (almost always let).

let

(Statement::Let)

Description

@@ -3101,12 +3118,12 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu

Expression statement

(Statement::Expression)

Description

-

Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a user-defined function call (Expression::Call) whose return values the source code discarded, or another Yul expression statement that does not have a dedicated Statement:: variant. EVM external calls (call, delegatecall, etc.) and contract creation (create, create2) translate to dedicated Statement::ExternalCall and Statement::Create variants, not through this wrapper.

+

Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a zero-return (void) user-defined function call (Expression::Call) evaluated for its side effects, or the discarded void result of a Yul builtin used as a statement (a value-producing expression is bound by a let instead). EVM external calls (call, delegatecall, etc.) and contract creation (create, create2) translate to dedicated Statement::ExternalCall and Statement::Create variants, not through this wrapper.

Syntax

$expression
 

Example

-
keccak256(v0, v1)           // hash computed but not bound to a value
+
update_balance(v0)          // void function called for its side effects
 

Operands

@@ -3140,7 +3157,7 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu
setimmutable("<key>", $value[: <type>])
 

Example

-
setimmutable("MyContract.owner", v0: i160)
+
setimmutable("MyContract.owner", v0)
 

Operands

@@ -3176,7 +3193,7 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu

Structured control flow

-

The IR’s control flow is structured: if, switch, and for are statements with explicit nested regions, each carrying input values and yielding output values. The three jump-like statements (break, continue, leave) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow.

+

The IR’s control flow is structured: if, switch, and for are statements with explicit nested regions, each carrying input values and yielding output values. The jump-like statements (break, continue, leave) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow.

if

(Statement::If)

Description

@@ -3185,13 +3202,14 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu
if $condition[: <type>] [[$input_0, $input_1, …]] { … } [else { … }]
 

Example

-
if v0 {
+
if v0: i1 {
     sstore(v1, v2)
 }
 
-let v5, v6 := if v3 [v1, v2] {
-    let v7 := add(v2, 0x1)
-    yield v1, v7
+let v5, v6 := if v3: i1 [v1, v2] {
+    let v7: i64 := 0x1          // add widens its operands to the i64 register width
+    let v8 := add(v2, v7: i64)
+    yield v1, v8
 } else {
     yield v1, v2
 }
@@ -3283,27 +3301,32 @@ default {
 

Structured loop with explicit loop-carried variables. Each iteration evaluates condition_statements followed by condition; if the condition is non-zero, the body region runs, then the post region runs, and the loop iterates. Loop-carried variables are passed as SSA values through each region. break exits the loop and continue jumps to the post region.

Syntax

for { $variable_0 := $initial_0[, …] }
-    [// condition_statements:
+    [// condition statements:
         …]
-    $condition
-    { // post
+    condition: $condition
+    post [($post_input_variable_0[, …])] {
         …
     }
-{
-    … body …
-}
+    body {
+        … body …
+    }
 

Example

-
for { v1 := 0x0 }
-    lt(v1, 0xa)
-    { // post
-        let v3 := add(v1, 0x1)
-        yield v3
+
let v0: i1 := 0x0
+let v6 := for { v1 := v0: i1 }
+    // condition statements:
+    let v2: i8 := 0xa
+    condition: lt(v1, v2: i8)
+    post (v3) {
+        let v4: i64 := 0x1
+        let v5 := add(v3, v4: i64)
+        yield v5
+    }
+    body {
+        sstore(v1, v1)
+        0x0: void
+        yield v1
     }
-{
-    sstore(v1, v2)
-    yield v1
-}
 

Operands

@@ -3314,7 +3337,7 @@ default { initial_valuesVec<Value>Starting values for the loop-carried variables. loop_variablesVec<ValueId>SSA ids visible inside condition, body, and post. -condition_statementsVec<Statement>Statements evaluated each iteration before the condition expression; emitted into the loop header block. Printed only when non-empty, behind a // condition_statements: comment. +condition_statementsVec<Statement>Statements evaluated each iteration before the condition expression; emitted into the loop header block. Printed only when non-empty, behind a // condition statements: comment. conditionExpressionRe-evaluated each iteration; non-zero continues, zero exits. bodyRegionLoop body; yields current loop-carried values. post_input_variablesVec<ValueId>Input SSA ids for the post region (one per loop-carried variable); receive the body’s yielded values merged with continue-site values via phi nodes in the LLVM codegen. @@ -3344,10 +3367,10 @@ default {
break
 

Example

-
if v0 { break }
+
if v0 { break [v1, v2] }
 

Operands

-

None in the printed form; the IR’s values: Vec<Value> field carries the loop-carried values internally.

+

The loop-carried values: Vec<Value> print in brackets when non-empty (e.g. break [v1, v2]).

Result and purity

@@ -3369,10 +3392,10 @@ default {
continue
 

Example

-
if v0 { continue }
+
if v0 { continue [v1, v2] }
 

Operands

-

None in the printed form; the IR’s values field carries the loop-carried values internally.

+

The loop-carried values print in brackets when non-empty (e.g. continue [v1, v2]).

Result and purity

@@ -3452,7 +3475,7 @@ leave // returns nothing (void function)

Annotations

None.

External interaction

-

Statements that cross the contract boundary: external calls (four kinds), contract creation (two kinds), and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses.

+

Statements that cross the contract boundary: external calls, contract creation, and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses.

call

(Statement::ExternalCall with CallKind::Call)

Description

@@ -3461,7 +3484,7 @@ leave // returns nothing (void function)
let $result := call($gas[: <type>], $address[: <type>], $value[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
 

Example

-
let v8 := call(v0, v1: i160, v2, v3, v4, v5, v6)
+
let v8 := call(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64)
 

Operands

@@ -3470,13 +3493,13 @@ leave // returns nothing (void function)
- - + + - - - - + + + +
NameTypeNotes
gasi256Gas to forward to the target.
addressi256Callee address; narrows to i160.
gasi256Gas to forward to the target; forward analysis widens to at least i64.
addressi256Callee address; forward analysis widens to at least i160.
valuei256Wei to transfer with the call.
args_offseti256Calldata source offset in linear memory.
args_lengthi256Calldata length in bytes.
ret_offseti256Return-data destination offset in linear memory.
ret_lengthi256Maximum return-data length.
args_offseti256Calldata source offset in linear memory; forward analysis widens to at least i64.
args_lengthi256Calldata length in bytes; forward analysis widens to at least i64.
ret_offseti256Return-data destination offset in linear memory; forward analysis widens to at least i64.
ret_lengthi256Maximum return-data length; forward analysis widens to at least i64.
@@ -3496,12 +3519,12 @@ leave // returns nothing (void function)

callcode

(Statement::ExternalCall with CallKind::CallCode)

Description

-

Deprecated EVM opcode that executes the callee’s code in the caller’s context but with the callee’s storage. Preserved for Solidity compatibility; new code should use delegatecall.

+

Deprecated EVM opcode that executes the callee’s code in the caller’s context but with the callee’s storage. Not supported by the newyork backend (codegen rejects it); use delegatecall instead.

Syntax

let $result := callcode($gas[: <type>], $address[: <type>], $value[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
 

Example

-
let v8 := callcode(v0, v1: i160, v2, v3, v4, v5, v6)
+
let v8 := callcode(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64)
 

Operands

Same shape as call.

@@ -3526,7 +3549,7 @@ leave // returns nothing (void function)
let $result := delegatecall($gas[: <type>], $address[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
 

Example

-
let v7 := delegatecall(v0, v1: i160, v2, v3, v4, v5)
+
let v7 := delegatecall(v0: i64, v1: i160, v2: i64, v3: i64, v4: i64, v5: i64)
 

Operands

Same shape as call minus the value operand.

@@ -3551,7 +3574,7 @@ leave // returns nothing (void function)
let $result := staticcall($gas[: <type>], $address[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
 

Example

-
let v7 := staticcall(v0, v1: i160, v2, v3, v4, v5)
+
let v7 := staticcall(v0: i64, v1: i160, v2: i64, v3: i64, v4: i64, v5: i64)
 

Operands

Same shape as call minus the value operand.

@@ -3576,7 +3599,7 @@ leave // returns nothing (void function)
let $result := create($value[: <type>], $offset[: <type>], $length[: <type>])
 

Example

-
let v4 := create(v0, v1, v2)
+
let v4 := create(v0, v1: i64, v2: i64)
 

Operands

@@ -3586,8 +3609,8 @@ leave // returns nothing (void function) valuei256Wei to transfer to the new contract. -offseti256Linear-memory offset of the init code. -lengthi256Length of the init code in bytes. +offseti256Linear-memory offset of the init code; forward analysis widens to at least i64. +lengthi256Length of the init code in bytes; forward analysis widens to at least i64.
@@ -3612,7 +3635,7 @@ leave // returns nothing (void function)
let $result := create2($value[: <type>], $offset[: <type>], $length[: <type>], $salt[: <type>])
 

Example

-
let v5 := create2(v0, v1, v2, v3)
+
let v5 := create2(v0, v1: i64, v2: i64, v3)
 

Operands

Same as create plus salt: i256.

@@ -3637,8 +3660,8 @@ leave // returns nothing (void function)
log<N>($offset[: <type>], $length[: <type>][, $topic_0[: <type>], …])
 

Example

-
log0(v0, v1)
-log2(v0, v1, v2, v3)            // two topics
+
log0(v0: i64, v1: i64)                    // no topics
+log2(v0: i64, v1: i64, v2, v3)            // two topics
 

Operands

@@ -3647,8 +3670,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Data source offset in linear memory. -lengthi256Data length in bytes. +offseti256Data source offset in linear memory; forward analysis widens to at least i64. +lengthi256Data length in bytes; forward analysis widens to at least i64. topicsVec<Value>Zero to four indexed topic values; the length determines the mnemonic suffix. @@ -3667,7 +3690,7 @@ log2(v0, v1, v2, v3) // two topics

Annotations

None.

Termination

-

Statements that end the current call frame. Three plain forms (return, revert, stop), two unconditional traps (invalid, selfdestruct), and three outlined revert variants that encode common Solidity error patterns into single nodes that can be deduplicated across call sites.

+

Statements that end the current call frame. Plain forms (return, revert, stop), unconditional traps (invalid, selfdestruct), and outlined revert variants (panic_revert, error_string_revert, custom_error_revert) that encode common Solidity error patterns into single nodes that can be deduplicated across call sites.

return

(Statement::Return)

Description

@@ -3676,7 +3699,7 @@ log2(v0, v1, v2, v3) // two topics
return($offset[: <type>], $length[: <type>])
 

Example

-
return(v0, v1)
+
return(v0: i64, v1: i64)
 

Operands

@@ -3685,8 +3708,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Return-data source offset. -lengthi256Return-data length. +offseti256Return-data source offset; forward analysis widens to at least i64. +lengthi256Return-data length; forward analysis widens to at least i64.
@@ -3711,7 +3734,7 @@ log2(v0, v1, v2, v3) // two topics
revert($offset[: <type>], $length[: <type>])
 

Example

-
revert(v0, v1)
+
revert(v0: i64, v1: i64)
 

Operands

@@ -3720,8 +3743,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Revert-data source offset. -lengthi256Revert-data length. +offseti256Revert-data source offset; forward analysis widens to at least i64. +lengthi256Revert-data length; forward analysis widens to at least i64.
@@ -3805,7 +3828,7 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -addressi256Recipient of the contract’s balance; narrows to i160. +addressi256Recipient of the contract’s balance; forward analysis widens to at least i160.
@@ -3894,12 +3917,12 @@ log2(v0, v1, v2, v3) // two topics

custom_error_revert

(Statement::CustomErrorRevert)

Description

-

Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero to three argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset 0.

+

Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero or more argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset 0.

Syntax

custom_error_revert(0x<hex>, [$arg_0, $arg_1, …])
 

Example

-
custom_error_revert(0xa28c4c1100000000000000000000000000000000000000000000000000000000, [v0, v1])
+
custom_error_revert(0xa28c4c11, [v0, v1])
 

Operands

@@ -3908,7 +3931,7 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -argumentsVec<Value>0–3 argument values; the selector is a compile-time field. +argumentsVec<Value>Zero or more argument values; the selector is a compile-time field.
@@ -3930,7 +3953,7 @@ log2(v0, v1, v2, v3) // two topics Source fieldPrinted as -selector: BigUintThe 4-byte error selector shifted left by 224 bits, printed in hex in the first syntax position. +selector: BigUintThe 4-byte error selector in hex, in the first syntax position. The selector is stored left-shifted by 224 bits; the printer right-shifts it back and prints the bare 4-byte value.
@@ -3970,6 +3993,22 @@ log2(v0, v1, v2, v3) // two topics + + @@ -178,7 +178,7 @@

The newyork crate (crates/newyork/) introduces an additional intermediate representation (IR) layer between Yul and LLVM IR. It enables domain-specific optimizations that neither solc nor LLVM can perform on their own, because they lack semantic knowledge about the cross-domain compilation from EVM to PolkaVM.

Note

-

The newyork optimizer is experimental. It is gated behind the RESOLC_USE_NEWYORK=1 environment variable (for standard JSON mode) or the --newyork CLI flag, and not yet enabled by default.

+

The newyork optimizer is experimental. It is gated behind the --newyork CLI flag or the settings.polkavm.newyork field in standard JSON input, and not yet enabled by default.

Motivation

The EVM and PolkaVM are fundamentally different machines:

@@ -206,13 +206,15 @@ Yul AST ──────────► │ newyork IR │ 3. dedup (exact + fuzzy) │ │ 4. mem_opt + fmp_prop + keccak_fold │ │ 5. simplify (pass 2) │ - │ 6. compound_outlining + guard_narrow │ + │ 6. mapping_access_outlining + guard_narrow │ │ 7. simplify (pass 3) │ │ 8. dedup (exact + fuzzy, pass 2) │ │ ── recursive on subobjects ── │ - │ 9. heap_opt (analysis) │ - │ 10. type_inference (4 iterative rounds) │ - │ 11. validate │ + │ 9. type_inference (iterative narrowing) │ + │ 10. late inline loop: inline, simplify, outline, │ + │ guard-narrow, simplify, dedup, narrow │ + │ 11. heap_opt (analysis) │ + │ 12. validate │ └──────────────────────────────────────────────────┘

The optimizer runs the following passes in order:

@@ -225,13 +227,14 @@ Yul AST ──────────► │ newyork IR
  • Compound outlining – detects keccak256_pair + sload/sstore sequences and fuses them into MappingSLoad/MappingSStore IR nodes, eliminating intermediate hash values. Guard narrowing – detects if gt(val, MASK) { revert } and iszero(eq(val, and(val, MASK))) patterns and inserts AND-mask narrowing, giving type inference proof that values fit in fewer bits.
  • Simplify (pass 3) – propagates opportunities created by compound outlining and guard narrowing.
  • Function deduplication (pass 2) – catches new duplicates exposed by guard narrowing and compound outlining canonicalization.
  • -
  • Heap analysis – analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking.
  • Type inference – narrows 256-bit values to smaller widths (I1, I8, I32, I64, I128, I160) where provable. Runs iteratively for up to 4 cascading refinement rounds, combining forward min-width propagation, backward use-context demands, transparent-operation demand propagation, and interprocedural parameter/return narrowing.
  • +
  • Late inline loop – now that narrowing and simplification have shrunk wrapper functions below the inline thresholds, re-runs inlining, simplification, mapping access outlining, guard narrowing, deduplication, and type inference to collect the residual opportunities.
  • +
  • Heap analysis – analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking.
  • Validation – checks SSA well-formedness (use-before-def, multiple definitions), yield count consistency, and function reference correctness.
  • -

    Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-11 run on the full object tree.

    +

    Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-12 run on the full object tree.

    IR design

    -

    The newyork IR is an SSA form with structured control flow, inspired by MLIR’s SCF dialect. Key design choices:

    +

    The newyork IR is an SSA form with structured control flow, inspired by MLIR’s SCF dialect. Key design choices:

    • Explicit types with address spaces: Every value carries a bit-width (I1, I8, I32, I64, I128, I160, I256) and pointers carry address space information (Heap, Stack, Storage, Code). All values start as I256 and are narrowed by type inference.
    • Pure expressions vs. effectful statements: Expressions compute values without side effects; statements perform memory, storage, or control flow effects. This separation simplifies analysis and rewriting.
    • @@ -244,7 +247,7 @@ Yul AST ──────────► │ newyork IR

      EVM operates on 256-bit words, but most values in practice fit in 32 or 64 bits. The type inference pass performs bidirectional analysis:

      • Forward: computes minimum width from literal values and operation semantics (e.g., add(I64, I8) produces I65, rounded up to I128).
      • -
      • Backward use tracking: classifies each value’s uses into 9 context categories (MemoryOffset, MemoryValue, StorageAccess, Comparison, Arithmetic, FunctionArg, FunctionReturn, ExternalCall, General). All categories conservatively demand the full I256 width by default; the categorisation is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — mload(2^128) aliased to mload(0) because the bounds check ran on an already-truncated value (commit ccca38df).
      • +
      • Backward use tracking: classifies each value’s uses into 9 context categories (MemoryOffset, MemoryValue, StorageAccess, Comparison, Arithmetic, FunctionArg, FunctionReturn, ExternalCall, General). All categories conservatively demand the full I256 width by default; the categorization is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — mload(2^128) aliased to mload(0) because the bounds check ran on an already-truncated value (commit ccca38df).
      • Transparent demand propagation: for modular-arithmetic operations (Add, Sub, Mul, And, Or, Xor), propagates narrow demands backward through operands, exploiting the property that trunc(op(a,b), N) == op(trunc(a,N), trunc(b,N)).
      • Interprocedural: iteratively narrows function parameter and return types in up to four rounds, combining four narrowing strategies — body-driven parameter narrowing, caller-driven parameter narrowing, forward-based return narrowing, and demand-based return narrowing — and re-running full inference between rounds. Parameters are clamped to at least I32 (XLEN on PolkaVM).
      @@ -360,30 +363,30 @@ can observe it.

      Additionally, common exit patterns (revert with constant length, zero-value returns) are deduplicated into shared LLVM basic blocks, saving hundreds of instruction copies in large contracts.

      Codesize results

      Integration test contracts

      -

      Reproducible with cargo test --package revive-integration -- codesize. The main column is the value committed to crates/integration/codesize.json on main; the newyork column is the value produced by the same test with RESOLC_USE_NEWYORK=1 set, currently committed on this branch.

      +

      Reproducible with cargo test --package revive-integration -- codesize for the Via Yul IR column (crates/integration/codesize.json) and cargo test --package revive-integration --features newyork -- codesize for the Via newyork IR column (crates/integration/codesize_newyork.json).

      - + - - - - - - - - + + + + + + + +
      Contractmain (bytes)newyork (bytes)Reduction
      ContractVia Yul IR (bytes)Via newyork IR (bytes)Reduction
      Baseline870479−44.9%
      Computation2,4181,376−43.1%
      DivisionArithmetics9,3277,192−22.9%
      ERC2017,16010,138−40.9%
      Events1,6621,279−23.0%
      FibonacciIterative1,427949−33.5%
      Flipper2,2401,123−49.9%
      SHA18,0096,286−21.5%
      Baseline838493−41.2%
      Computation2,3681,217−48.6%
      DivisionArithmetics11,4447,370−35.6%
      ERC2018,0578,726−51.7%
      Events1,614909−43.7%
      FibonacciIterative1,373969−29.4%
      Flipper2,2051,058−52.0%
      SHA17,8306,264−20.0%

      OpenZeppelin contracts

      -

      Measured by running oz-tests/oz.sh against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot — there is no committed measurement file in the repo, so these may drift as the optimizer evolves; rerun the script for fresh figures.

      +

      Measured against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot.

      - + @@ -399,10 +402,11 @@ can observe it.

      Contractnewyork (bytes)
      ContractVia newyork IR (bytes)
      oz_gov81,840

      For comparison, building the same contracts without the newyork optimizer at the equivalent snapshot produced 563,526 bytes total — a reduction of about −43% across the corpus.

      -

      Per-contract reductions in the integration suite range from roughly −21% (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimise) to nearly −50% (Flipper, where the optimiser strips away most of Solidity’s dispatch and storage-access scaffolding).

      +

      Per-contract reductions in the integration suite range from roughly −20% (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimize) to about −52% (Flipper, where the optimizer strips away most of Solidity’s dispatch and storage-access scaffolding).

      Development history and challenges

      -

      The newyork optimizer was developed over roughly three months — from early February 2026 through early May 2026 — largely through AI-assisted pair programming with Claude. The development progressed through several distinct phases:

      -

      Phase 1 – Initial scaffolding: The first draft established the core IR data structures, Yul-to-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline.

      +

      The first version of the newyork optimizer was authored collaboratively and reviewed extensively by the revive maintainers, as well as Claude Opus, Claude Fable, Qwen, Minimax and Deepseek LLMs, over a span of many months — from early February 2026 through mid-June 2026.

      +

      The development progressed through several distinct phases:

      +

      Phase 1 – Initial scaffolding: The first draft established the core IR data structures, Yul-to-newyork-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline.

      Phase 2 – Optimization passes: Once the baseline was stable, optimization passes were added iteratively: inlining, simplification, memory optimization, function deduplication, keccak256 fusion, and type inference. Each pass was validated against differential tests comparing EVM and PVM execution.

      Phase 3 – Soundness hardening: Several type inference and narrowing approaches turned out to be unsound and had to be reworked:

        @@ -411,6 +415,8 @@ can observe it.

      • Backward demand-driven narrowing required multiple iterations to become provably safe.

      Phase 4 – Measuring and tuning: Systematic measurement of OpenZeppelin contracts revealed which optimizations had the most impact and which approaches regressed performance.

      +

      Throughout development the optimizer was validated against the existing integration and differential test suites (containing over 30,000 test cases), which run each contract on both EVM and PVM and assert identical state changes.

      +

      The newyork compiler pipeline introduced no new regressions over these test suites. This was achieved by careful manual reviews and many LLM bughunt loops. Additionally, a final security review by Anthropic’s Fable 5 LLM found no remaining soundness issues. As with any new compiler feature, it should still be treated as experimental as of now.

      Approaches that did not work

      @@ -422,6 +428,9 @@ can observe it.

      + + +
      NoInline on __revive_int_truncate+62% regression: PolkaVM call overhead exceeds inline cost
      Native FMP memory (inline sbrk)Mixed: small contracts improved, large ones regressed from sbrk bloat
      Shared overflow trap blockMixed: prevented LLVM from eliminating individual dead overflow checks
      Aggressive IR-level single-call inliningRegressed large contracts (ERC20 +6.1%): merged bodies become monolithic functions LLVM can’t optimize, so large functions are deferred to LLVM’s inliner instead
      Type-inference narrowing of mload(0x40) to I32Regressed small contracts (+252 bytes): conflicts with the codegen FMP range proof; the bound is exposed via a trunc→zext pair instead
      Full simplifier re-run after mem_optMixed: helped small ERC20 (−293 bytes) but regressed the OZ stablecoin (+72 bytes); replaced by a targeted keccak-only fold
      @@ -429,30 +438,28 @@ can observe it.

      Known limitations and future work

      The following opportunities have been identified but are not yet implemented:

        -
      • Bitwise algebraic simplifications: BitAnd, BitOr, BitXor identity patterns fall through without simplification.
      • -
      • Cross-control-flow memory optimization: Memory state is conservatively cleared at if/switch/for boundaries. Preserving state across simple branches would enable more load-after-store eliminations.
      • +
      • Memory optimization across loop boundaries: Tracked memory state is cleared around for loop condition, body, and post blocks, so load-after-store eliminations do not carry across loop iterations. Preserving loop-invariant state would recover more eliminations.
      • Adaptive inlining thresholds: Current thresholds are static constants. Profile-guided or contract-size-aware heuristics could improve decisions for diverse contract sizes.
      • -
      • Extended fuzzy deduplication: The current pass only compares functions by structure of Let bindings. Extending to consider literals inside MStore, Return, Revert, and Log statements would find more deduplication opportunities.
      • -
      • Type checking in validation: The validator checks SSA well-formedness and structural correctness, but does not yet verify type consistency of operations (the TypeMismatch error variant exists but is not yet wired).
      • +
      • Extended fuzzy deduplication: The current pass only parameterizes literals in Let bindings and SStore slots. Extending it to consider literals inside MStore, Return, Revert, and Log statements would find more deduplication opportunities.
      • +
      • Type checking in validation: The validator checks SSA well-formedness and structure, but not operation type consistency. Type discipline is maintained by construction (type inference and codegen), with LLVM’s IR verifier as the backstop.
      • Loop variable narrowing: Loop-carried variables are conservatively widened to I256. Reaching a fixed-point across loop iterations could allow narrower types for simple counters.
      • +
      • Functions with leave inside a for loop are not inlined: the IR-level inliner defers such functions to LLVM’s inliner, so they miss the interprocedural constant propagation and width narrowing the IR-level pass provides.
      -

      Environment variables

      -

      A small set of environment variables controls or inspects the newyork pipeline. Only RESOLC_USE_NEWYORK affects generated bytecode; the others are read-only inspection knobs used while debugging the optimizer.

      +

      Debug output

      +

      Passing --debug-output-dir <path> makes the newyork pipeline write IR and analysis artifacts for each compiled contract into that directory. The dumps are produced automatically whenever the directory is set.

      - + - - - - - + + + +
      VariableEffect
      FileContent
      RESOLC_USE_NEWYORK=1Routes Yul lowering through the newyork pipeline. Equivalent to passing --newyork on the command line; the CLI flag and this variable are OR-ed by resolve_use_newyork (crates/resolc/src/lib.rs).
      RESOLC_DEBUG_IRWhen set, prints the translated newyork IR for every object to stderr. Additionally writes <output_directory>/<object>.newyork.txt whenever the debug config carries an output directory.
      RESOLC_DEBUG_HEAPWhen set, appends per-object heap-analysis details — native regions/offsets, taintedness, dynamic escapes, escaping ranges — to <output_directory>/resolc_heap_debug.log. Requires the debug config to carry an output directory.
      NEWYORK_DUMP_IRWhen set, writes the IR for every translated object to /tmp/newyork_ir_<object>.txt from inside translate_yul_object (crates/newyork/src/lib.rs). Independent of RESOLC_DEBUG_IR — fires before codegen and needs no output directory.
      RESOLC_DEBUG_BLOBTest harness only. Dumps the compiled PVM blob to /tmp/debug_blob_<contract>.pvm and the LLVM IR debug directory to /tmp/debug_llvm_newyork or /tmp/debug_llvm_yul. Used by crates/resolc/src/test_utils.rs when comparing newyork against the Yul path.
      <file-stem>.newyorkFinal optimized IR, annotated with the inferred type widths
      <file-stem>.snapshot.newyorkIR snapshot taken before the late passes (only when captured during translation)
      <file-stem>.heap.newyorkHeap analysis summary (native regions/offsets, taintedness, escapes, dynamic accesses)
      <file-stem>.mem.newyorkMemory optimization counters (loads/stores eliminated, keccak fusions, FMP loads eliminated)
      -

      All of these gate on presence/value at the start of compilation; flipping them mid-run has no effect.

      Module reference

      @@ -469,7 +476,7 @@ can observe it.

      - + @@ -513,6 +520,22 @@ can observe it.

      + + @@ -229,6 +229,22 @@ + + @@ -261,6 +261,22 @@ export PATH=/path/to/go-ethereum/build/bin/:$PATH + + @@ -224,6 +224,22 @@ + + @@ -227,6 +227,22 @@ + + @@ -237,7 +237,7 @@ Please find our binary releas

      LLVM optimization levels

      -O, --optimization <OPTIMIZATION>
       
      -

      resolc exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies wiedly between different optimization levels.

      +

      resolc exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies widely between different optimization levels. (This is independent of --newyork which selects the IR lowering pipeline.)

      Valid levels are the following:

      By default, -Oz is applied.

      +

      newyork IR pipeline

      +
      --newyork
      +
      +

      Enables the newyork optimizer to reduced compiled contract code size, by routing Yul lowering through the experimental newyork IR pipeline instead of the standard Yul-to-LLVM path. Composes with --yul, --combined-json, and the default Solidity mode. In standard JSON mode this flag is rejected; enable the pipeline via the settings.polkavm.newyork input field instead. Off by default.

      Stack size

      --stack-size <STACK_SIZE>
       
      @@ -281,7 +285,7 @@ Please find our binary releas

      Debug artifacts

      --debug-output-dir <DEBUG_OUTPUT_DIRECTORY>
       
      -

      Dump all intermediary compiler artifacts to files in the specified directory. This includes the YUL IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. Useful for debugging and development purposes.

      +

      Dump all intermediary compiler artifacts to files in the specified directory. This includes the Yul IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. When the newyork pipeline is active, the newyork IR is additionally dumped (the final IR, a pre-late-pass snapshot, and heap and memory optimization data). Useful for debugging and development purposes.

      Debug info

      -g
       
      @@ -351,6 +355,9 @@ Please find our binary releas

      Used to configure PVM specific compiler settings.

      settings.polkavm.debugInformation

      A boolean value allowing to enable debug information. Corresponds to resolc -g.

      +

      settings.polkavm.newyork

      +

      A boolean value allowing to enable the experimental newyork IR pipeline for Yul lowering. Corresponds to resolc --newyork. Off by default.

      +

      The output JSON includes which pipeline actually ran, via the top-level resolc_pipeline field ("newyork" or "yul" for the standard Yul-to-LLVM pipeline).

      The settings.polkavm.memoryConfig object

      Used to apply PVM specific memory configuration settings.

      settings.polkavm.memoryConfig.heapSize

      @@ -649,7 +656,7 @@ contract Test is InnerContract {

      The newyork crate (crates/newyork/) introduces an additional intermediate representation (IR) layer between Yul and LLVM IR. It enables domain-specific optimizations that neither solc nor LLVM can perform on their own, because they lack semantic knowledge about the cross-domain compilation from EVM to PolkaVM.

      Note

      -

      The newyork optimizer is experimental. It is gated behind the RESOLC_USE_NEWYORK=1 environment variable (for standard JSON mode) or the --newyork CLI flag, and not yet enabled by default.

      +

      The newyork optimizer is experimental. It is gated behind the --newyork CLI flag or the settings.polkavm.newyork field in standard JSON input, and not yet enabled by default.

      Motivation

      The EVM and PolkaVM are fundamentally different machines:

      @@ -677,13 +684,15 @@ Yul AST ──────────► │ newyork IR │ 3. dedup (exact + fuzzy) │ │ 4. mem_opt + fmp_prop + keccak_fold │ │ 5. simplify (pass 2) │ - │ 6. compound_outlining + guard_narrow │ + │ 6. mapping_access_outlining + guard_narrow │ │ 7. simplify (pass 3) │ │ 8. dedup (exact + fuzzy, pass 2) │ │ ── recursive on subobjects ── │ - │ 9. heap_opt (analysis) │ - │ 10. type_inference (4 iterative rounds) │ - │ 11. validate │ + │ 9. type_inference (iterative narrowing) │ + │ 10. late inline loop: inline, simplify, outline, │ + │ guard-narrow, simplify, dedup, narrow │ + │ 11. heap_opt (analysis) │ + │ 12. validate │ └──────────────────────────────────────────────────┘

      The optimizer runs the following passes in order:

      @@ -696,13 +705,14 @@ Yul AST ──────────► │ newyork IR
    • Compound outlining – detects keccak256_pair + sload/sstore sequences and fuses them into MappingSLoad/MappingSStore IR nodes, eliminating intermediate hash values. Guard narrowing – detects if gt(val, MASK) { revert } and iszero(eq(val, and(val, MASK))) patterns and inserts AND-mask narrowing, giving type inference proof that values fit in fewer bits.
    • Simplify (pass 3) – propagates opportunities created by compound outlining and guard narrowing.
    • Function deduplication (pass 2) – catches new duplicates exposed by guard narrowing and compound outlining canonicalization.
    • -
    • Heap analysis – analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking.
    • Type inference – narrows 256-bit values to smaller widths (I1, I8, I32, I64, I128, I160) where provable. Runs iteratively for up to 4 cascading refinement rounds, combining forward min-width propagation, backward use-context demands, transparent-operation demand propagation, and interprocedural parameter/return narrowing.
    • +
    • Late inline loop – now that narrowing and simplification have shrunk wrapper functions below the inline thresholds, re-runs inlining, simplification, mapping access outlining, guard narrowing, deduplication, and type inference to collect the residual opportunities.
    • +
    • Heap analysis – analyzes memory access patterns (alignment, static offsets, taintedness, escaping regions) to determine which accesses can use native little-endian layout, skipping byte-swap operations. Uses GCD-based alignment propagation and per-region taint tracking.
    • Validation – checks SSA well-formedness (use-before-def, multiple definitions), yield count consistency, and function reference correctness.
    • -

      Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-11 run on the full object tree.

      +

      Steps 1-8 run recursively on subobjects (deployed contract code), where optimization impact is greatest. Steps 9-12 run on the full object tree.

      IR design

      -

      The newyork IR is an SSA form with structured control flow, inspired by MLIR’s SCF dialect. Key design choices:

      +

      The newyork IR is an SSA form with structured control flow, inspired by MLIR’s SCF dialect. Key design choices:

      • Explicit types with address spaces: Every value carries a bit-width (I1, I8, I32, I64, I128, I160, I256) and pointers carry address space information (Heap, Stack, Storage, Code). All values start as I256 and are narrowed by type inference.
      • Pure expressions vs. effectful statements: Expressions compute values without side effects; statements perform memory, storage, or control flow effects. This separation simplifies analysis and rewriting.
      • @@ -715,7 +725,7 @@ Yul AST ──────────► │ newyork IR

        EVM operates on 256-bit words, but most values in practice fit in 32 or 64 bits. The type inference pass performs bidirectional analysis:

        • Forward: computes minimum width from literal values and operation semantics (e.g., add(I64, I8) produces I65, rounded up to I128).
        • -
        • Backward use tracking: classifies each value’s uses into 9 context categories (MemoryOffset, MemoryValue, StorageAccess, Comparison, Arithmetic, FunctionArg, FunctionReturn, ExternalCall, General). All categories conservatively demand the full I256 width by default; the categorisation is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — mload(2^128) aliased to mload(0) because the bounds check ran on an already-truncated value (commit ccca38df).
        • +
        • Backward use tracking: classifies each value’s uses into 9 context categories (MemoryOffset, MemoryValue, StorageAccess, Comparison, Arithmetic, FunctionArg, FunctionReturn, ExternalCall, General). All categories conservatively demand the full I256 width by default; the categorization is what enables the interprocedural phase to selectively relax the demand for narrowed function arguments. Earlier versions narrowed directly from the use category, but that was unsound for memory offsets — mload(2^128) aliased to mload(0) because the bounds check ran on an already-truncated value (commit ccca38df).
        • Transparent demand propagation: for modular-arithmetic operations (Add, Sub, Mul, And, Or, Xor), propagates narrow demands backward through operands, exploiting the property that trunc(op(a,b), N) == op(trunc(a,N), trunc(b,N)).
        • Interprocedural: iteratively narrows function parameter and return types in up to four rounds, combining four narrowing strategies — body-driven parameter narrowing, caller-driven parameter narrowing, forward-based return narrowing, and demand-based return narrowing — and re-running full inference between rounds. Parameters are clamped to at least I32 (XLEN on PolkaVM).
        @@ -831,30 +841,30 @@ can observe it.

        Additionally, common exit patterns (revert with constant length, zero-value returns) are deduplicated into shared LLVM basic blocks, saving hundreds of instruction copies in large contracts.

        Codesize results

        Integration test contracts

        -

        Reproducible with cargo test --package revive-integration -- codesize. The main column is the value committed to crates/integration/codesize.json on main; the newyork column is the value produced by the same test with RESOLC_USE_NEWYORK=1 set, currently committed on this branch.

        +

        Reproducible with cargo test --package revive-integration -- codesize for the Via Yul IR column (crates/integration/codesize.json) and cargo test --package revive-integration --features newyork -- codesize for the Via newyork IR column (crates/integration/codesize_newyork.json).

      type_inference.rsBidirectional integer width narrowing with transparent demand propagation
      mem_opt.rsLoad-after-store elimination, keccak256 fusion, FMP propagation
      heap_opt.rsHeap access pattern analysis, alignment tracking, byte-swap elimination
      compound_outlining.rsMapping access pattern detection and fusion (keccak256_pair + sload/sstore)
      mapping_access_outlining.rsMapping access pattern detection and fusion (keccak256_pair + sload/sstore)
      guard_narrow.rsGuard pattern detection and AND-mask narrowing insertion
      validate.rsIR well-formedness checks (SSA, yields, function references)
      printer.rsHuman-readable IR pretty printer with configurable output
      - + - - - - - - - - + + + + + + + +
      Contractmain (bytes)newyork (bytes)Reduction
      ContractVia Yul IR (bytes)Via newyork IR (bytes)Reduction
      Baseline870479−44.9%
      Computation2,4181,376−43.1%
      DivisionArithmetics9,3277,192−22.9%
      ERC2017,16010,138−40.9%
      Events1,6621,279−23.0%
      FibonacciIterative1,427949−33.5%
      Flipper2,2401,123−49.9%
      SHA18,0096,286−21.5%
      Baseline838493−41.2%
      Computation2,3681,217−48.6%
      DivisionArithmetics11,4447,370−35.6%
      ERC2018,0578,726−51.7%
      Events1,614909−43.7%
      FibonacciIterative1,373969−29.4%
      Flipper2,2051,058−52.0%
      SHA17,8306,264−20.0%

      OpenZeppelin contracts

      -

      Measured by running oz-tests/oz.sh against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot — there is no committed measurement file in the repo, so these may drift as the optimizer evolves; rerun the script for fresh figures.

      +

      Measured against real-world contracts generated with the OpenZeppelin Wizard. The numbers below are a development snapshot.

      - + @@ -870,10 +880,11 @@ can observe it.

      Contractnewyork (bytes)
      ContractVia newyork IR (bytes)
      oz_gov81,840

      For comparison, building the same contracts without the newyork optimizer at the equivalent snapshot produced 563,526 bytes total — a reduction of about −43% across the corpus.

      -

      Per-contract reductions in the integration suite range from roughly −21% (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimise) to nearly −50% (Flipper, where the optimiser strips away most of Solidity’s dispatch and storage-access scaffolding).

      +

      Per-contract reductions in the integration suite range from roughly −20% (SHA1, where the bulk of the work is the SHA-1 inner loop and offers little to optimize) to about −52% (Flipper, where the optimizer strips away most of Solidity’s dispatch and storage-access scaffolding).

      Development history and challenges

      -

      The newyork optimizer was developed over roughly three months — from early February 2026 through early May 2026 — largely through AI-assisted pair programming with Claude. The development progressed through several distinct phases:

      -

      Phase 1 – Initial scaffolding: The first draft established the core IR data structures, Yul-to-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline.

      +

      The first version of the newyork optimizer was authored collaboratively and reviewed extensively by the revive maintainers, as well as Claude Opus, Claude Fable, Qwen, Minimax and Deepseek LLMs, over a span of many months — from early February 2026 through mid-June 2026.

      +

      The development progressed through several distinct phases:

      +

      Phase 1 – Initial scaffolding: The first draft established the core IR data structures, Yul-to-newyork-IR translation, and LLVM codegen. Early commits focused on getting a correct round-trip through the new pipeline.

      Phase 2 – Optimization passes: Once the baseline was stable, optimization passes were added iteratively: inlining, simplification, memory optimization, function deduplication, keccak256 fusion, and type inference. Each pass was validated against differential tests comparing EVM and PVM execution.

      Phase 3 – Soundness hardening: Several type inference and narrowing approaches turned out to be unsound and had to be reworked:

        @@ -882,6 +893,8 @@ can observe it.

      • Backward demand-driven narrowing required multiple iterations to become provably safe.

      Phase 4 – Measuring and tuning: Systematic measurement of OpenZeppelin contracts revealed which optimizations had the most impact and which approaches regressed performance.

      +

      Throughout development the optimizer was validated against the existing integration and differential test suites (containing over 30,000 test cases), which run each contract on both EVM and PVM and assert identical state changes.

      +

      The newyork compiler pipeline introduced no new regressions over these test suites. This was achieved by careful manual reviews and many LLM bughunt loops. Additionally, a final security review by Anthropic’s Fable 5 LLM found no remaining soundness issues. As with any new compiler feature, it should still be treated as experimental as of now.

      Approaches that did not work

      @@ -893,6 +906,9 @@ can observe it.

      + + +
      NoInline on __revive_int_truncate+62% regression: PolkaVM call overhead exceeds inline cost
      Native FMP memory (inline sbrk)Mixed: small contracts improved, large ones regressed from sbrk bloat
      Shared overflow trap blockMixed: prevented LLVM from eliminating individual dead overflow checks
      Aggressive IR-level single-call inliningRegressed large contracts (ERC20 +6.1%): merged bodies become monolithic functions LLVM can’t optimize, so large functions are deferred to LLVM’s inliner instead
      Type-inference narrowing of mload(0x40) to I32Regressed small contracts (+252 bytes): conflicts with the codegen FMP range proof; the bound is exposed via a trunc→zext pair instead
      Full simplifier re-run after mem_optMixed: helped small ERC20 (−293 bytes) but regressed the OZ stablecoin (+72 bytes); replaced by a targeted keccak-only fold
      @@ -900,30 +916,28 @@ can observe it.

      Known limitations and future work

      The following opportunities have been identified but are not yet implemented:

        -
      • Bitwise algebraic simplifications: BitAnd, BitOr, BitXor identity patterns fall through without simplification.
      • -
      • Cross-control-flow memory optimization: Memory state is conservatively cleared at if/switch/for boundaries. Preserving state across simple branches would enable more load-after-store eliminations.
      • +
      • Memory optimization across loop boundaries: Tracked memory state is cleared around for loop condition, body, and post blocks, so load-after-store eliminations do not carry across loop iterations. Preserving loop-invariant state would recover more eliminations.
      • Adaptive inlining thresholds: Current thresholds are static constants. Profile-guided or contract-size-aware heuristics could improve decisions for diverse contract sizes.
      • -
      • Extended fuzzy deduplication: The current pass only compares functions by structure of Let bindings. Extending to consider literals inside MStore, Return, Revert, and Log statements would find more deduplication opportunities.
      • -
      • Type checking in validation: The validator checks SSA well-formedness and structural correctness, but does not yet verify type consistency of operations (the TypeMismatch error variant exists but is not yet wired).
      • +
      • Extended fuzzy deduplication: The current pass only parameterizes literals in Let bindings and SStore slots. Extending it to consider literals inside MStore, Return, Revert, and Log statements would find more deduplication opportunities.
      • +
      • Type checking in validation: The validator checks SSA well-formedness and structure, but not operation type consistency. Type discipline is maintained by construction (type inference and codegen), with LLVM’s IR verifier as the backstop.
      • Loop variable narrowing: Loop-carried variables are conservatively widened to I256. Reaching a fixed-point across loop iterations could allow narrower types for simple counters.
      • +
      • Functions with leave inside a for loop are not inlined: the IR-level inliner defers such functions to LLVM’s inliner, so they miss the interprocedural constant propagation and width narrowing the IR-level pass provides.
      -

      Environment variables

      -

      A small set of environment variables controls or inspects the newyork pipeline. Only RESOLC_USE_NEWYORK affects generated bytecode; the others are read-only inspection knobs used while debugging the optimizer.

      +

      Debug output

      +

      Passing --debug-output-dir <path> makes the newyork pipeline write IR and analysis artifacts for each compiled contract into that directory. The dumps are produced automatically whenever the directory is set.

      - + - - - - - + + + +
      VariableEffect
      FileContent
      RESOLC_USE_NEWYORK=1Routes Yul lowering through the newyork pipeline. Equivalent to passing --newyork on the command line; the CLI flag and this variable are OR-ed by resolve_use_newyork (crates/resolc/src/lib.rs).
      RESOLC_DEBUG_IRWhen set, prints the translated newyork IR for every object to stderr. Additionally writes <output_directory>/<object>.newyork.txt whenever the debug config carries an output directory.
      RESOLC_DEBUG_HEAPWhen set, appends per-object heap-analysis details — native regions/offsets, taintedness, dynamic escapes, escaping ranges — to <output_directory>/resolc_heap_debug.log. Requires the debug config to carry an output directory.
      NEWYORK_DUMP_IRWhen set, writes the IR for every translated object to /tmp/newyork_ir_<object>.txt from inside translate_yul_object (crates/newyork/src/lib.rs). Independent of RESOLC_DEBUG_IR — fires before codegen and needs no output directory.
      RESOLC_DEBUG_BLOBTest harness only. Dumps the compiled PVM blob to /tmp/debug_blob_<contract>.pvm and the LLVM IR debug directory to /tmp/debug_llvm_newyork or /tmp/debug_llvm_yul. Used by crates/resolc/src/test_utils.rs when comparing newyork against the Yul path.
      <file-stem>.newyorkFinal optimized IR, annotated with the inferred type widths
      <file-stem>.snapshot.newyorkIR snapshot taken before the late passes (only when captured during translation)
      <file-stem>.heap.newyorkHeap analysis summary (native regions/offsets, taintedness, escapes, dynamic accesses)
      <file-stem>.mem.newyorkMemory optimization counters (loads/stores eliminated, keccak fusions, FMP loads eliminated)
      -

      All of these gate on presence/value at the start of compilation; flipping them mid-run has no effect.

      Module reference

      @@ -940,7 +954,7 @@ can observe it.

      - + @@ -952,9 +966,13 @@ can observe it.

      newyork IR reference

      A per-operation reference for the newyork IR: textual syntax, operand and result types, purity, region and static-slot annotations, and examples.

      How to read this reference

      -

      This appendix enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor.

      +

      This reference page enumerates every operation the newyork IR supports. It is a lookup, not a walkthrough: each entry is self-contained and intended to be reachable by anchor.

      Operations are grouped by function (memory and storage writes, pure expressions, control flow, and so on) rather than alphabetically. Jump to a specific operation from the operation index below, or use the sidebar.

      -

      Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either Expression or Statement in ir.rs. The textual rendering used by debug dumps and by this appendix is produced by the printer in printer.rs. Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations.

      +

      Every operation appears in two places in the codebase. The canonical Rust definition is a variant of either Expression or Statement in ir.rs. The textual rendering used by debug dumps and by this reference page is produced by the printer in printer.rs.

      +
      +

      Note

      +

      Treat the printed syntax as a debug surface, not a stable input language: there is no parser for it, and printer details change when passes add new annotations.

      +

      Entry format

      Each operation entry has the same shape:

      @@ -991,6 +1009,18 @@ can observe it.

      type_inference.rsBidirectional integer width narrowing with transparent demand propagation
      mem_opt.rsLoad-after-store elimination, keccak256 fusion, FMP propagation
      heap_opt.rsHeap access pattern analysis, alignment tracking, byte-swap elimination
      compound_outlining.rsMapping access pattern detection and fusion (keccak256_pair + sload/sstore)
      mapping_access_outlining.rsMapping access pattern detection and fusion (keccak256_pair + sload/sstore)
      guard_narrow.rsGuard pattern detection and AND-mask narrowing insertion
      validate.rsIR well-formedness checks (SSA, yields, function references)
      printer.rsHuman-readable IR pretty printer with configurable output
      +

      For instance, this template:

      +
      let $result[: <type>] := and($lhs[: <type>], $rhs[: <type>])
      +
      +

      prints as:

      +
      let v2: i8 := and(v0, v1: i8)
      +
      +

      $result rendered as v2 with an i8 type suffix, $lhs as v0 at the default i256 (type suffix omitted), and $rhs as v1 with an i8 type suffix.

      +
      +

      Note

      +

      A value’s printed width is use-driven. Type inference assigns each value a forward width from its definition, then widens it to satisfy its uses. The type suffix shown for a value in an example (such as i8) is therefore only illustrative — a short example may not show the uses that determine it, and the same operation can appear with a wider suffix, or none (it is omitted for the default i256), in another program.

      +

      For instance, a value used as a memory offset widens to i64; as an address (a call target, extcodesize) to i160; stored as a full word (an mstore/sstore value) to i256; and an add/mul operand up to the i64 register width.

      +

      Operation index

      Pure expressions

      Constants and variables
      @@ -1145,21 +1175,21 @@ can observe it.

      Type system

      Every value in the IR carries a Type. The operation entries below refer to widths (i1…i256), address spaces (ptr<heap>, etc.), and memory regions (scratch, etc.) by their printed form; this section is the reference for those names.

      Type

      -

      The umbrella enum. Three variants:

      +

      The umbrella enum, with these variants:

      - +
      VariantPrinted asDescription
      Int(BitWidth)i1, i8, …, i256An integer at one of seven widths; see BitWidth.
      Int(BitWidth)i1, i8, …, i256An integer at one of the BitWidth widths.
      Ptr(AddressSpace)ptr<heap>, ptr<stack>, ptr<storage>, ptr<code>A pointer tagged with its address space; see AddressSpace.
      VoidvoidUnit type. Used for statements that produce no value and for void-returning functions.

      BitWidth

      -

      The seven rungs of integer width. Newly minted values default to I256; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused.

      +

      The rungs of integer width. Newly minted values default to I256; type inference narrows them down to one of the lower rungs when it can prove the upper bits are zero or unused.

      @@ -1216,9 +1246,9 @@ can observe it.

      0x<hex>[: <type>]
       

      Example

      -
      let v0 := 0x2a              // 42 at the default i256
      -let v1 := 0x1: i1           // boolean true
      -let v2 := 0x80: i64         // narrowed by type inference
      +
      let v0: i8 := 0x2a
      +let v1: i1 := 0x1           // boolean true
      +let v2: i64 := 0x80
       

      Operands

      None — literals are leaves.

      @@ -1439,7 +1469,7 @@ let v2 := 0x80: i64 // narrowed by type inference
      - +
      ResultPurity
      width(lhs)Pure
      max(width(lhs), width(rhs)) — a negative divisor can push the result to full widthPure
      @@ -1518,9 +1548,9 @@ let v2 := 0x80: i64 // narrowed by type inference

      exp

      (Expression::Binary with BinaryOperation::Exp)

      Description

      -

      Modular exponentiation: (lhs ^ rhs) mod 2^256. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of rhs).

      +

      Modular exponentiation: (base ^ exponent) mod 2^256. The most expensive arithmetic opcode in EVM (variable gas cost proportional to the byte length of exponent).

      Syntax

      -
      exp($lhs[: <type>], $rhs[: <type>])
      +
      exp($base[: <type>], $exponent[: <type>])
       

      Example

      let v2 := exp(v0, v1)
      @@ -1532,8 +1562,8 @@ let v2 := 0x80: i64         // narrowed by type inference
       NameTypeNotes
       
       
      -lhsi256Base.
      -rhsi256Exponent.
      +basei256Base.
      +exponenti256Exponent.
       
       
       
    @@ -1559,7 +1589,8 @@ let v2 := 0x80: i64 // narrowed by type inference

    Example

    let v2 := and(v0, v1)
    -let v3 := and(v0, 0xff)     // type inference narrows result to i8
    +let v3: i8 := 0xff
    +let v4: i8 := and(v0, v3: i8)
     

    Operands

    @@ -1661,10 +1692,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

    Description

    Logical left shift. Operand order follows EVM: shl(shift, value) computes value << shift. Shifts ≥ 256 produce 0.

    Syntax

    -
    shl($lhs[: <type>], $rhs[: <type>])
    +
    shl($shift[: <type>], $value[: <type>])
     

    Example

    -
    let v2 := shl(v0, v1)       // v1 shifted left by v0 bits
    +
    let v2 := shl(v0, v1)
     

    Operands

    @@ -1673,8 +1704,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Shift amount in bits. -rhsi256Value to shift. +shifti256Shift amount in bits. +valuei256Value to shift.
    @@ -1696,7 +1727,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

    Description

    Logical right shift. Operand order follows EVM: shr(shift, value) computes value >> shift with zero-fill from the left. Shifts ≥ 256 produce 0.

    Syntax

    -
    shr($lhs[: <type>], $rhs[: <type>])
    +
    shr($shift[: <type>], $value[: <type>])
     

    Example

    let v2 := shr(v0, v1)
    @@ -1708,8 +1739,8 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
     NameTypeNotes
     
     
    -lhsi256Shift amount in bits.
    -rhsi256Value to shift.
    +shifti256Shift amount in bits.
    +valuei256Value to shift.
     
     
     
    @@ -1720,7 +1751,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 ResultPurity -If lhs is a known constant k: tier holding 256 - k bits (or i1 for k ≥ 256). Otherwise: width(rhs).Pure +If shift is a known constant k: tier holding 256 - k bits (or i1 for k ≥ 256). Otherwise: width(value).Pure
    @@ -1731,7 +1762,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

    Description

    Arithmetic (signed) right shift. Operand order follows EVM: sar(shift, value) shifts value right by shift bits, preserving the sign bit. Shifts ≥ 256 saturate to 0 for non-negative values and to -1 (all-ones) for negative values.

    Syntax

    -
    sar($lhs[: <type>], $rhs[: <type>])
    +
    sar($shift[: <type>], $value[: <type>])
     

    Example

    let v2 := sar(v0, v1)
    @@ -1743,8 +1774,8 @@ let v3 := and(v0, 0xff)     // type inference narrows result to i8
     NameTypeNotes
     
     
    -lhsi256Shift amount in bits.
    -rhsi256Value to shift, treated as signed.
    +shifti256Shift amount in bits.
    +valuei256Value to shift, treated as signed.
     
     
     
    @@ -1755,7 +1786,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 ResultPurity -Same shape as shr forward inference (constant shift narrows the result; non-constant falls back to width(rhs)).Pure +width(value) — unlike shr, sign-extension means a constant shift cannot narrow the resultPure
    @@ -1769,7 +1800,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    lt($lhs[: <type>], $rhs[: <type>])
     

    Example

    -
    let v2 := lt(v0, v1)
    +
    let v2: i1 := lt(v0, v1)
     

    Operands

    @@ -1804,7 +1835,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    gt($lhs[: <type>], $rhs[: <type>])
     

    Example

    -
    let v2 := gt(v0, v1)
    +
    let v2: i1 := gt(v0, v1)
     

    Operands

    @@ -1839,7 +1870,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    slt($lhs[: <type>], $rhs[: <type>])
     

    Example

    -
    let v2 := slt(v0, v1)
    +
    let v2: i1 := slt(v0, v1)
     

    Operands

    @@ -1874,7 +1905,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    sgt($lhs[: <type>], $rhs[: <type>])
     

    Example

    -
    let v2 := sgt(v0, v1)
    +
    let v2: i1 := sgt(v0, v1)
     

    Operands

    @@ -1909,7 +1940,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    eq($lhs[: <type>], $rhs[: <type>])
     

    Example

    -
    let v2 := eq(v0, v1)
    +
    let v2: i1 := eq(v0, v1)
     

    Operands

    @@ -1941,10 +1972,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

    Description

    Extract a single byte from a 256-bit word. byte(i, x) returns the i-th byte of x with byte 0 being the most significant. If i ≥ 32, the result is 0.

    Syntax

    -
    byte($lhs[: <type>], $rhs[: <type>])
    +
    byte($index[: <type>], $word[: <type>])
     

    Example

    -
    let v2 := byte(v0, v1)      // v0 = byte index, v1 = word
    +
    let v2: i8 := byte(v0, v1)
     

    Operands

    @@ -1953,8 +1984,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Byte position; 0 = most significant byte. Values ≥ 32 yield 0. -rhsi256Source word. +indexi256Byte position; 0 = most significant byte. Values ≥ 32 yield 0. +wordi256Source word.
    @@ -1976,10 +2007,10 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8

    Description

    Sign-extend an integer from a byte position. Per EVM, signextend(b, x) treats byte b of x as the most significant byte of a smaller signed integer and extends its sign through the upper bytes.

    Syntax

    -
    signextend($lhs[: <type>], $rhs[: <type>])
    +
    signextend($byte_position[: <type>], $value[: <type>])
     

    Example

    -
    let v2 := signextend(v0, v1)  // v0 = byte position, v1 = value
    +
    let v2 := signextend(v0, v1)
     

    Operands

    @@ -1988,8 +2019,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8 NameTypeNotes -lhsi256Byte position of the sign byte (0–31). -rhsi256Source value. +byte_positioni256Byte position of the sign byte (0–31). +valuei256Source value.
    @@ -2086,7 +2117,7 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    iszero($operand[: <type>])
     

    Example

    -
    let v1 := iszero(v0)
    +
    let v1: i1 := iszero(v0)
     

    Operands

    @@ -2188,8 +2219,8 @@ let v3 := and(v0, 0xff) // type inference narrows result to i8
    truncate<i<bits>>($value[: <type>])
     

    Example

    -
    let v1 := truncate<i64>(v0)
    -let v2 := truncate<i8>(v1)
    +
    let v1: i64 := truncate<i64>(v0)
    +let v2: i8 := truncate<i8>(v1: i64)
     

    Operands

    @@ -2286,7 +2317,7 @@ let v2 := truncate<i8>(v1)

    keccak256

    (Expression::Keccak256)

    Description

    -

    Compute the Keccak-256 hash of length bytes of emulated EVM linear memory starting at offset. The general-purpose hashing primitive; the two specialized variants below cover the common scratch-space patterns more compactly.

    +

    Compute the Keccak-256 hash of length bytes of emulated EVM linear memory starting at offset. The general-purpose hashing primitive; the specialized variants below cover the common scratch-space patterns more compactly.

    Syntax

    keccak256($offset[: <type>], $length[: <type>])
     
    @@ -2321,7 +2352,7 @@ let v2 := truncate<i8>(v1)

    keccak256_pair

    (Expression::Keccak256Pair)

    Description

    -

    Compound hash of two 256-bit words. Equivalent to mstore(0, word0); mstore(32, word1); keccak256(0, 64) but emitted as a single outlined call after the keccak-fusion pass recognizes the pattern. The mapping-key idiom; see also mapping_sload.

    +

    Compound hash of two 256-bit words. Equivalent to mstore(0, word0); mstore(32, word1); keccak256(0, 64) but emitted as a single outlined call after mem_opt’s keccak fusion recognizes the pattern. The mapping-key idiom; see also mapping_sload.

    Syntax

    keccak256_pair($word0[: <type>], $word1[: <type>])
     
    @@ -2356,7 +2387,7 @@ let v2 := truncate<i8>(v1)

    keccak256_single

    (Expression::Keccak256Single)

    Description

    -

    Compound hash of a single 256-bit word. Equivalent to mstore(0, word0); keccak256(0, 32) but emitted as a single outlined call after the keccak-fusion pass.

    +

    Compound hash of a single 256-bit word. Equivalent to mstore(0, word0); keccak256(0, 32) but emitted as a single outlined call after mem_opt’s keccak fusion.

    Syntax

    keccak256_single($word0[: <type>])
     
    @@ -2395,7 +2426,7 @@ let v2 := truncate<i8>(v1)
    caller()
     

    Example

    -
    let v0 := caller()
    +
    let v0: i160 := caller()
     

    Operands

    None.

    @@ -2445,7 +2476,7 @@ let v2 := truncate<i8>(v1)
    origin()
     

    Example

    -
    let v0 := origin()
    +
    let v0: i160 := origin()
     

    Operands

    None.

    @@ -2470,7 +2501,7 @@ let v2 := truncate<i8>(v1)
    address()
     

    Example

    -
    let v0 := address()
    +
    let v0: i160 := address()
     

    Operands

    None.

    @@ -2515,12 +2546,12 @@ let v2 := truncate<i8>(v1)

    gas

    (Expression::Gas)

    Description

    -

    Remaining gas at the point of evaluation. Modelled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect gas as a barrier.

    +

    Remaining gas at the point of evaluation. Modeled as a pure expression for IR purposes; in practice it changes between evaluations, so any simplifier that deduplicates pure expressions must respect gas as a barrier.

    Syntax

    gas()
     

    Example

    -
    let v0 := gas()
    +
    let v0: i64 := gas()
     

    Operands

    None.

    @@ -2545,7 +2576,7 @@ let v2 := truncate<i8>(v1)
    msize()
     

    Example

    -
    let v0 := msize()
    +
    let v0: i64 := msize()
     

    Operands

    None.

    @@ -2570,7 +2601,7 @@ let v2 := truncate<i8>(v1)
    coinbase()
     

    Example

    -
    let v0 := coinbase()
    +
    let v0: i160 := coinbase()
     

    Operands

    None.

    @@ -2595,7 +2626,7 @@ let v2 := truncate<i8>(v1)
    timestamp()
     

    Example

    -
    let v0 := timestamp()
    +
    let v0: i64 := timestamp()
     

    Operands

    None.

    @@ -2620,7 +2651,7 @@ let v2 := truncate<i8>(v1)
    number()
     

    Example

    -
    let v0 := number()
    +
    let v0: i64 := number()
     

    Operands

    None.

    @@ -2670,7 +2701,7 @@ let v2 := truncate<i8>(v1)
    gaslimit()
     

    Example

    -
    let v0 := gaslimit()
    +
    let v0: i64 := gaslimit()
     

    Operands

    None.

    @@ -2788,7 +2819,7 @@ let v2 := truncate<i8>(v1) NameTypeNotes -numberi256Block number; forward analysis widens to at least i64. +numberi256Block number; forward analysis widens to i256.
    @@ -2897,7 +2928,7 @@ let v2 := truncate<i8>(v1)
    calldatasize()
     

    Example

    -
    let v0 := calldatasize()
    +
    let v0: i64 := calldatasize()
     

    Operands

    None.

    @@ -2917,12 +2948,12 @@ let v2 := truncate<i8>(v1)

    returndatasize

    (Expression::ReturnDataSize)

    Description

    -

    Length of the most recently returned data buffer from a sub-call, in bytes. Modelled as pure per IR but reflects the last ExternalCall / Create result; consumers must respect that ordering.

    +

    Length of the most recently returned data buffer from a sub-call, in bytes. Modeled as pure per IR but reflects the last ExternalCall / Create result; consumers must respect that ordering.

    Syntax

    returndatasize()
     

    Example

    -
    let v0 := returndatasize()
    +
    let v0: i64 := returndatasize()
     

    Operands

    None.

    @@ -2947,7 +2978,7 @@ let v2 := truncate<i8>(v1)
    codesize()
     

    Example

    -
    let v0 := codesize()
    +
    let v0: i64 := codesize()
     

    Operands

    None.

    @@ -2972,7 +3003,7 @@ let v2 := truncate<i8>(v1)
    extcodesize($address[: <type>])
     

    Example

    -
    let v1 := extcodesize(v0: i160)
    +
    let v1: i64 := extcodesize(v0: i160)
     

    Operands

    @@ -3074,8 +3105,8 @@ let v2 := truncate<i8>(v1)
    mload($offset[: <type>]) [/* <region> */]
     

    Example

    -
    let v1 := mload(v0)
    -let v2 := mload(v3) /* free_ptr */
    +
    let v1 := mload(v0: i64)
    +let v2: i32 := mload(v3: i64) /* free_ptr */
     

    Operands

    @@ -3193,7 +3224,7 @@ let v2 := sload(v3) /* slot: 0x0 */

    mapping_sload

    (Expression::MappingSLoad)

    Description

    -

    Compound load for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sload(keccak256(0, 64)) but emitted as a single outlined call after the compound_outlining pass recognizes the pattern (it fuses a keccak256_pair — itself produced by mem_opt’s keccak fusion — followed by an sload whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load.

    +

    Compound load for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sload(keccak256(0, 64)) but emitted as a single outlined call after the mapping_access_outlining pass recognizes the pattern (it fuses a keccak256_pair — itself produced by mem_opt’s keccak fusion — followed by an sload whose key has a single consumer). Only valid when the intermediate hash is used exclusively by this load.

    Syntax

    mapping_sload($key[: <type>], $slot[: <type>])
     
    @@ -3267,7 +3298,7 @@ let v2 := sload(v3) /* slot: 0x0 */
    datasize("<id>")
     

    Example

    -
    let v0 := datasize("MyContract_deployed")
    +
    let v0: i64 := datasize("MyContract_deployed")
     

    Operands

    None — the identifier is a quoted string literal in the syntax position, not an operand.

    @@ -3335,7 +3366,7 @@ let v2 := sload(v3) /* slot: 0x0 */
    linkersymbol("<path>")
     

    Example

    -
    let v0 := linkersymbol("contracts/Library.sol:L")
    +
    let v0: i160 := linkersymbol("contracts/Library.sol:L")
     

    Operands

    None — the path is a quoted string literal in the syntax position.

    @@ -3406,7 +3437,7 @@ let v4, v5 := returns_two(v0) // multi-return via let multi-binding

    Memory and storage writes

    -

    The six operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses.

    +

    The operations in this section all modify external state: emulated EVM linear memory, persistent storage, or transient storage. They are statements (not expressions) and they are never pure. Simplification and deduplication never reorder them with respect to each other or with respect to reverts; the memory passes treat them as the side-effect boundary for their analyses.

    mstore

    (Statement::MStore)

    Description

    @@ -3462,7 +3493,7 @@ mstore(v4, v5) /* free_ptr */ // offset is exactly 0x40
    mstore8($offset[: <type>], $value[: <type>]) [/* <region> */]
     

    Example

    -
    mstore8(v0, v1: i8)             // value narrowed to i8 by type inference
    +
    mstore8(v0, v1: i8)
     

    Operands

    @@ -3507,7 +3538,7 @@ mstore(v4, v5) /* free_ptr */ // offset is exactly 0x40
    mcopy($dest[: <type>], $src[: <type>], $length[: <type>])
     

    Example

    -
    mcopy(v0, v1, v2)               // dest, src, length
    +
    mcopy(v0, v1, v2)
     

    Operands

    @@ -3619,12 +3650,12 @@ sstore(v2, v3) /* slot: 0x0 */

    mapping_sstore

    (Statement::MappingSStore)

    Description

    -

    Compound store for a Solidity mapping element. Equivalent to the three-operation sequence mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value) but emitted as a single outlined statement after the compound_outlining pass recognizes the pattern (it fuses a keccak256_pair followed by an sstore whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement.

    +

    Compound store for a Solidity mapping element. Equivalent to mstore(0, key); mstore(32, slot); sstore(keccak256(0, 64), value) but emitted as a single outlined statement after the mapping_access_outlining pass recognizes the pattern (it fuses a keccak256_pair followed by an sstore whose key has a single consumer). Only valid when the intermediate hash is not observed by any other statement.

    Syntax

    mapping_sstore($key[: <type>], $slot[: <type>], $value[: <type>])
     

    Example

    -
    mapping_sstore(v0: i160, v1, v2)        // address key, declared slot, value
    +
    mapping_sstore(v0, v1, v2)
     

    Operands

    @@ -3633,7 +3664,7 @@ sstore(v2, v3) /* slot: 0x0 */ NameTypeNotes -keyi256Mapping key. Often narrowed to i160 for address keys. +keyi256Mapping key. The outlining pass force-widens it to i256, so it always prints at full width, even for address keys. sloti256The mapping’s declared storage slot. Typically a small constant. valuei256The value to store at the computed storage location. @@ -3653,7 +3684,7 @@ sstore(v2, v3) /* slot: 0x0 */

    Annotations

    None. mapping_sstore deliberately drops the static_slot annotation that the original sstore may have carried, because the fused statement’s effective slot is the keccak hash of the key and the declared slot, which is never a compile-time constant.

    Bulk copies

    -

    Multi-byte memory copies from the five EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. All five take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes.

    +

    Multi-byte memory copies from the EVM-accessible byte sources (code, external code, returndata, embedded data, and calldata) into emulated EVM linear memory. They all take the same shape: a destination memory offset, a source offset, and a length. They are effectful and act as opaque barriers to the memory passes.

    codecopy

    (Statement::CodeCopy)

    Description

    @@ -3707,7 +3738,7 @@ sstore(v2, v3) /* slot: 0x0 */ NameTypeNotes -addressi256Account whose code to read; narrows to i160. +addressi256Account whose code to read; forward analysis widens to at least i160. desti256Destination byte offset in linear memory. offseti256Source byte offset in the external code. lengthi256Number of bytes to copy. @@ -3836,7 +3867,7 @@ sstore(v2, v3) /* slot: 0x0 */

    Annotations

    None.

    Bindings and wrappers

    -

    The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in the appendix’s earlier sections appears on the right-hand side of one of these statements (almost always let).

    +

    The statements that bind SSA values, hold loose expressions evaluated for their side effects, and write to immutable storage. Every pure expression in this reference’s earlier sections appears on the right-hand side of one of these statements (almost always let).

    let

    (Statement::Let)

    Description

    @@ -3876,12 +3907,12 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu

    Expression statement

    (Statement::Expression)

    Description

    -

    Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a user-defined function call (Expression::Call) whose return values the source code discarded, or another Yul expression statement that does not have a dedicated Statement:: variant. EVM external calls (call, delegatecall, etc.) and contract creation (create, create2) translate to dedicated Statement::ExternalCall and Statement::Create variants, not through this wrapper.

    +

    Wraps an expression evaluated for its observable consequences but whose value is not bound. Typically a zero-return (void) user-defined function call (Expression::Call) evaluated for its side effects, or the discarded void result of a Yul builtin used as a statement (a value-producing expression is bound by a let instead). EVM external calls (call, delegatecall, etc.) and contract creation (create, create2) translate to dedicated Statement::ExternalCall and Statement::Create variants, not through this wrapper.

    Syntax

    $expression
     

    Example

    -
    keccak256(v0, v1)           // hash computed but not bound to a value
    +
    update_balance(v0)          // void function called for its side effects
     

    Operands

    @@ -3915,7 +3946,7 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu
    setimmutable("<key>", $value[: <type>])
     

    Example

    -
    setimmutable("MyContract.owner", v0: i160)
    +
    setimmutable("MyContract.owner", v0)
     

    Operands

    @@ -3951,7 +3982,7 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu

    Structured control flow

    -

    The IR’s control flow is structured: if, switch, and for are statements with explicit nested regions, each carrying input values and yielding output values. The three jump-like statements (break, continue, leave) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow.

    +

    The IR’s control flow is structured: if, switch, and for are statements with explicit nested regions, each carrying input values and yielding output values. The jump-like statements (break, continue, leave) are scoped to their nearest enclosing construct. Nested blocks create lexical scope without otherwise changing control flow.

    if

    (Statement::If)

    Description

    @@ -3960,13 +3991,14 @@ let v4, v5 := if v2 [v0, v1] { … } else { … } // multi-binding from a valu
    if $condition[: <type>] [[$input_0, $input_1, …]] { … } [else { … }]
     

    Example

    -
    if v0 {
    +
    if v0: i1 {
         sstore(v1, v2)
     }
     
    -let v5, v6 := if v3 [v1, v2] {
    -    let v7 := add(v2, 0x1)
    -    yield v1, v7
    +let v5, v6 := if v3: i1 [v1, v2] {
    +    let v7: i64 := 0x1          // add widens its operands to the i64 register width
    +    let v8 := add(v2, v7: i64)
    +    yield v1, v8
     } else {
         yield v1, v2
     }
    @@ -4058,27 +4090,32 @@ default {
     

    Structured loop with explicit loop-carried variables. Each iteration evaluates condition_statements followed by condition; if the condition is non-zero, the body region runs, then the post region runs, and the loop iterates. Loop-carried variables are passed as SSA values through each region. break exits the loop and continue jumps to the post region.

    Syntax

    for { $variable_0 := $initial_0[, …] }
    -    [// condition_statements:
    +    [// condition statements:
             …]
    -    $condition
    -    { // post
    +    condition: $condition
    +    post [($post_input_variable_0[, …])] {
             …
         }
    -{
    -    … body …
    -}
    +    body {
    +        … body …
    +    }
     

    Example

    -
    for { v1 := 0x0 }
    -    lt(v1, 0xa)
    -    { // post
    -        let v3 := add(v1, 0x1)
    -        yield v3
    +
    let v0: i1 := 0x0
    +let v6 := for { v1 := v0: i1 }
    +    // condition statements:
    +    let v2: i8 := 0xa
    +    condition: lt(v1, v2: i8)
    +    post (v3) {
    +        let v4: i64 := 0x1
    +        let v5 := add(v3, v4: i64)
    +        yield v5
    +    }
    +    body {
    +        sstore(v1, v1)
    +        0x0: void
    +        yield v1
         }
    -{
    -    sstore(v1, v2)
    -    yield v1
    -}
     

    Operands

    @@ -4089,7 +4126,7 @@ default { initial_valuesVec<Value>Starting values for the loop-carried variables. loop_variablesVec<ValueId>SSA ids visible inside condition, body, and post. -condition_statementsVec<Statement>Statements evaluated each iteration before the condition expression; emitted into the loop header block. Printed only when non-empty, behind a // condition_statements: comment. +condition_statementsVec<Statement>Statements evaluated each iteration before the condition expression; emitted into the loop header block. Printed only when non-empty, behind a // condition statements: comment. conditionExpressionRe-evaluated each iteration; non-zero continues, zero exits. bodyRegionLoop body; yields current loop-carried values. post_input_variablesVec<ValueId>Input SSA ids for the post region (one per loop-carried variable); receive the body’s yielded values merged with continue-site values via phi nodes in the LLVM codegen. @@ -4119,10 +4156,10 @@ default {
    break
     

    Example

    -
    if v0 { break }
    +
    if v0 { break [v1, v2] }
     

    Operands

    -

    None in the printed form; the IR’s values: Vec<Value> field carries the loop-carried values internally.

    +

    The loop-carried values: Vec<Value> print in brackets when non-empty (e.g. break [v1, v2]).

    Result and purity

    @@ -4144,10 +4181,10 @@ default {
    continue
     

    Example

    -
    if v0 { continue }
    +
    if v0 { continue [v1, v2] }
     

    Operands

    -

    None in the printed form; the IR’s values field carries the loop-carried values internally.

    +

    The loop-carried values print in brackets when non-empty (e.g. continue [v1, v2]).

    Result and purity

    @@ -4227,7 +4264,7 @@ leave // returns nothing (void function)

    Annotations

    None.

    External interaction

    -

    Statements that cross the contract boundary: external calls (four kinds), contract creation (two kinds), and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses.

    +

    Statements that cross the contract boundary: external calls, contract creation, and event log emission. All produce or rely on external state and act as barriers to memory and storage analyses.

    call

    (Statement::ExternalCall with CallKind::Call)

    Description

    @@ -4236,7 +4273,7 @@ leave // returns nothing (void function)
    let $result := call($gas[: <type>], $address[: <type>], $value[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
     

    Example

    -
    let v8 := call(v0, v1: i160, v2, v3, v4, v5, v6)
    +
    let v8 := call(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64)
     

    Operands

    @@ -4245,13 +4282,13 @@ leave // returns nothing (void function)
    - - + + - - - - + + + +
    NameTypeNotes
    gasi256Gas to forward to the target.
    addressi256Callee address; narrows to i160.
    gasi256Gas to forward to the target; forward analysis widens to at least i64.
    addressi256Callee address; forward analysis widens to at least i160.
    valuei256Wei to transfer with the call.
    args_offseti256Calldata source offset in linear memory.
    args_lengthi256Calldata length in bytes.
    ret_offseti256Return-data destination offset in linear memory.
    ret_lengthi256Maximum return-data length.
    args_offseti256Calldata source offset in linear memory; forward analysis widens to at least i64.
    args_lengthi256Calldata length in bytes; forward analysis widens to at least i64.
    ret_offseti256Return-data destination offset in linear memory; forward analysis widens to at least i64.
    ret_lengthi256Maximum return-data length; forward analysis widens to at least i64.
    @@ -4271,12 +4308,12 @@ leave // returns nothing (void function)

    callcode

    (Statement::ExternalCall with CallKind::CallCode)

    Description

    -

    Deprecated EVM opcode that executes the callee’s code in the caller’s context but with the callee’s storage. Preserved for Solidity compatibility; new code should use delegatecall.

    +

    Deprecated EVM opcode that executes the callee’s code in the caller’s context but with the callee’s storage. Not supported by the newyork backend (codegen rejects it); use delegatecall instead.

    Syntax

    let $result := callcode($gas[: <type>], $address[: <type>], $value[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
     

    Example

    -
    let v8 := callcode(v0, v1: i160, v2, v3, v4, v5, v6)
    +
    let v8 := callcode(v0: i64, v1: i160, v2, v3: i64, v4: i64, v5: i64, v6: i64)
     

    Operands

    Same shape as call.

    @@ -4301,7 +4338,7 @@ leave // returns nothing (void function)
    let $result := delegatecall($gas[: <type>], $address[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
     

    Example

    -
    let v7 := delegatecall(v0, v1: i160, v2, v3, v4, v5)
    +
    let v7 := delegatecall(v0: i64, v1: i160, v2: i64, v3: i64, v4: i64, v5: i64)
     

    Operands

    Same shape as call minus the value operand.

    @@ -4326,7 +4363,7 @@ leave // returns nothing (void function)
    let $result := staticcall($gas[: <type>], $address[: <type>], $args_offset[: <type>], $args_length[: <type>], $ret_offset[: <type>], $ret_length[: <type>])
     

    Example

    -
    let v7 := staticcall(v0, v1: i160, v2, v3, v4, v5)
    +
    let v7 := staticcall(v0: i64, v1: i160, v2: i64, v3: i64, v4: i64, v5: i64)
     

    Operands

    Same shape as call minus the value operand.

    @@ -4351,7 +4388,7 @@ leave // returns nothing (void function)
    let $result := create($value[: <type>], $offset[: <type>], $length[: <type>])
     

    Example

    -
    let v4 := create(v0, v1, v2)
    +
    let v4 := create(v0, v1: i64, v2: i64)
     

    Operands

    @@ -4361,8 +4398,8 @@ leave // returns nothing (void function) valuei256Wei to transfer to the new contract. -offseti256Linear-memory offset of the init code. -lengthi256Length of the init code in bytes. +offseti256Linear-memory offset of the init code; forward analysis widens to at least i64. +lengthi256Length of the init code in bytes; forward analysis widens to at least i64.
    @@ -4387,7 +4424,7 @@ leave // returns nothing (void function)
    let $result := create2($value[: <type>], $offset[: <type>], $length[: <type>], $salt[: <type>])
     

    Example

    -
    let v5 := create2(v0, v1, v2, v3)
    +
    let v5 := create2(v0, v1: i64, v2: i64, v3)
     

    Operands

    Same as create plus salt: i256.

    @@ -4412,8 +4449,8 @@ leave // returns nothing (void function)
    log<N>($offset[: <type>], $length[: <type>][, $topic_0[: <type>], …])
     

    Example

    -
    log0(v0, v1)
    -log2(v0, v1, v2, v3)            // two topics
    +
    log0(v0: i64, v1: i64)                    // no topics
    +log2(v0: i64, v1: i64, v2, v3)            // two topics
     

    Operands

    @@ -4422,8 +4459,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Data source offset in linear memory. -lengthi256Data length in bytes. +offseti256Data source offset in linear memory; forward analysis widens to at least i64. +lengthi256Data length in bytes; forward analysis widens to at least i64. topicsVec<Value>Zero to four indexed topic values; the length determines the mnemonic suffix. @@ -4442,7 +4479,7 @@ log2(v0, v1, v2, v3) // two topics

    Annotations

    None.

    Termination

    -

    Statements that end the current call frame. Three plain forms (return, revert, stop), two unconditional traps (invalid, selfdestruct), and three outlined revert variants that encode common Solidity error patterns into single nodes that can be deduplicated across call sites.

    +

    Statements that end the current call frame. Plain forms (return, revert, stop), unconditional traps (invalid, selfdestruct), and outlined revert variants (panic_revert, error_string_revert, custom_error_revert) that encode common Solidity error patterns into single nodes that can be deduplicated across call sites.

    return

    (Statement::Return)

    Description

    @@ -4451,7 +4488,7 @@ log2(v0, v1, v2, v3) // two topics
    return($offset[: <type>], $length[: <type>])
     

    Example

    -
    return(v0, v1)
    +
    return(v0: i64, v1: i64)
     

    Operands

    @@ -4460,8 +4497,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Return-data source offset. -lengthi256Return-data length. +offseti256Return-data source offset; forward analysis widens to at least i64. +lengthi256Return-data length; forward analysis widens to at least i64.
    @@ -4486,7 +4523,7 @@ log2(v0, v1, v2, v3) // two topics
    revert($offset[: <type>], $length[: <type>])
     

    Example

    -
    revert(v0, v1)
    +
    revert(v0: i64, v1: i64)
     

    Operands

    @@ -4495,8 +4532,8 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -offseti256Revert-data source offset. -lengthi256Revert-data length. +offseti256Revert-data source offset; forward analysis widens to at least i64. +lengthi256Revert-data length; forward analysis widens to at least i64.
    @@ -4580,7 +4617,7 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -addressi256Recipient of the contract’s balance; narrows to i160. +addressi256Recipient of the contract’s balance; forward analysis widens to at least i160.
    @@ -4669,12 +4706,12 @@ log2(v0, v1, v2, v3) // two topics

    custom_error_revert

    (Statement::CustomErrorRevert)

    Description

    -

    Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero to three argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset 0.

    +

    Outlined Solidity custom-error revert. Encodes the error selector (left-shifted by 224 bits) and zero or more argument values into scratch memory and reverts. No FMP load is needed; the encoding uses the scratch region at offset 0.

    Syntax

    custom_error_revert(0x<hex>, [$arg_0, $arg_1, …])
     

    Example

    -
    custom_error_revert(0xa28c4c1100000000000000000000000000000000000000000000000000000000, [v0, v1])
    +
    custom_error_revert(0xa28c4c11, [v0, v1])
     

    Operands

    @@ -4683,7 +4720,7 @@ log2(v0, v1, v2, v3) // two topics NameTypeNotes -argumentsVec<Value>0–3 argument values; the selector is a compile-time field. +argumentsVec<Value>Zero or more argument values; the selector is a compile-time field.
    @@ -4705,7 +4742,7 @@ log2(v0, v1, v2, v3) // two topics Source fieldPrinted as -selector: BigUintThe 4-byte error selector shifted left by 224 bits, printed in hex in the first syntax position. +selector: BigUintThe 4-byte error selector in hex, in the first syntax position. The selector is stored left-shifted by 224 bits; the printer right-shifts it back and prints the bare 4-byte value.
    @@ -4851,6 +4888,22 @@ make test-wasm + + @@ -239,6 +239,22 @@ + + @@ -212,6 +212,22 @@ + + @@ -216,6 +216,22 @@ + + @@ -183,7 +183,7 @@

    LLVM optimization levels

    -O, --optimization <OPTIMIZATION>
     
    -

    resolc exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies wiedly between different optimization levels.

    +

    resolc exposes the optimization level setting for the LLVM backend. The performance and size of compiled contracts varies widely between different optimization levels. (This is independent of --newyork which selects the IR lowering pipeline.)

    Valid levels are the following:

    • 0: No optimizations are applied.
    • @@ -194,6 +194,10 @@
    • z: Aggressively optimize for code size.

    By default, -Oz is applied.

    +

    newyork IR pipeline

    +
    --newyork
    +
    +

    Enables the newyork optimizer to reduced compiled contract code size, by routing Yul lowering through the experimental newyork IR pipeline instead of the standard Yul-to-LLVM path. Composes with --yul, --combined-json, and the default Solidity mode. In standard JSON mode this flag is rejected; enable the pipeline via the settings.polkavm.newyork input field instead. Off by default.

    Stack size

    --stack-size <STACK_SIZE>
     
    @@ -227,7 +231,7 @@

    Debug artifacts

    --debug-output-dir <DEBUG_OUTPUT_DIRECTORY>
     
    -

    Dump all intermediary compiler artifacts to files in the specified directory. This includes the YUL IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. Useful for debugging and development purposes.

    +

    Dump all intermediary compiler artifacts to files in the specified directory. This includes the Yul IR, optimized and unoptimized LLVM IR, the ELF object and the PVM assembly. When the newyork pipeline is active, the newyork IR is additionally dumped (the final IR, a pre-late-pass snapshot, and heap and memory optimization data). Useful for debugging and development purposes.

    Debug info

    -g
     
    @@ -303,6 +307,22 @@ + + @@ -304,6 +304,22 @@ contract Test is InnerContract { + + @@ -231,6 +231,22 @@ Please find our binary releas + + @@ -219,6 +219,22 @@ + + @@ -220,6 +220,22 @@ + + @@ -180,6 +180,9 @@

    Used to configure PVM specific compiler settings.

    settings.polkavm.debugInformation

    A boolean value allowing to enable debug information. Corresponds to resolc -g.

    +

    settings.polkavm.newyork

    +

    A boolean value allowing to enable the experimental newyork IR pipeline for Yul lowering. Corresponds to resolc --newyork. Off by default.

    +

    The output JSON includes which pipeline actually ran, via the top-level resolc_pipeline field ("newyork" or "yul" for the standard Yul-to-LLVM pipeline).

    The settings.polkavm.memoryConfig object

    Used to apply PVM specific memory configuration settings.

    settings.polkavm.memoryConfig.heapSize

    @@ -285,6 +288,22 @@ + + @@ -222,6 +222,22 @@ + + @@ -227,6 +227,22 @@ + +