From d00293ed9cc32fd909195f285e2caee9a08c904d Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 14 Oct 2026 21:55:39 -0400 Subject: [PATCH 1/4] bugc: replace eliminated temps in call arguments (CSE) --- .../common-subexpression-elimination.test.ts | 30 +++++++++++++++++++ .../steps/common-subexpression-elimination.ts | 5 ++++ 2 files changed, 35 insertions(+) diff --git a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.test.ts b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.test.ts index 564e1970f..49ff29522 100644 --- a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.test.ts +++ b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { compile } from "#compiler"; import type * as Ir from "#ir"; +import { executeProgram } from "#test/evm/behavioral"; describe("CommonSubexpressionEliminationStep", () => { // `msg.value` before and after a call that is not inlined (it @@ -38,4 +39,33 @@ code { r = g(3); }`; expect(values).toHaveLength(2); }); } + + // `msg.sender` twice: the second one is a call's argument, so the + // call must take the first one's value in its place + const callArgument = `name CallArg; +define { + function f(a: address, n: uint256) -> address { + if (n == 0) { return a; } + return f(a, n - 1); + }; +} +storage { [0] first: address; [1] passed: address; } +create {} +code { + first = msg.sender; + passed = f(msg.sender, 2); +}`; + + for (const level of [2, 3] as const) { + it(`replaces a call's argument (level ${level})`, async () => { + const result = await executeProgram(callArgument, { + calldata: "", + optimizationLevel: level, + }); + expect(result.callSuccess).toBe(true); + const first = await result.getStorage(0n); + expect(first).not.toBe(0n); + expect(await result.getStorage(1n)).toBe(first); + }); + } }); diff --git a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts index 77174ff3e..af127eb88 100644 --- a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts +++ b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts @@ -179,6 +179,11 @@ export class CommonSubexpressionEliminationStep extends BaseOptimizationStep { block.terminator.value, globalReplacements, ); + } else if (block.terminator.kind === "call") { + block.terminator.arguments = block.terminator.arguments.map( + (argument) => + this.applyValueReplacement(argument, globalReplacements), + ); } } }); From f33773837f113c878dcefdcb22d36b19c059d622 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 14 Oct 2026 21:55:43 -0400 Subject: [PATCH 2/4] bugc: never merge a call's continuation into another return --- .../activation-pairing.test.ts.snap | 4 +-- .../optimizer/steps/return-merging.test.ts | 30 +++++++++++++++++++ .../src/optimizer/steps/return-merging.ts | 10 +++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap b/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap index 20a6969d6..4a23c8dd1 100644 --- a/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap +++ b/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap @@ -48,8 +48,8 @@ exports[`the code does not depend on the markers > is the same for every example "advanced/nested-structs.bug at O3": "d141d683602322f4195283df977a6c5618e72e995bfca7d8b285ae3a0f779bb4", "advanced/token-registry.bug at O0": "430a947f02160f905a04efdeea0b5d69fb7aa6be6b2dc54734be26ec0511988a", "advanced/token-registry.bug at O1": "2de1cfe1d642a35a409d46df00f9ebb08ce0c43736020a5aa06cae30983b72e6", - "advanced/token-registry.bug at O2": "does not compile", - "advanced/token-registry.bug at O3": "does not compile", + "advanced/token-registry.bug at O2": "2c0dcbce9d26b8182ce6607aeb480005fc485e7e21c44405be48c451c945c2e3", + "advanced/token-registry.bug at O3": "989c5871f2e3c40b13af199fcc441a7aee8427a7935243af8f152d6758124642", "advanced/voting-system.bug at O0": "e49f6349c9c8fc5d3e14cd737a860891139518b0e0ea73d3c01f314a60e80a30", "advanced/voting-system.bug at O1": "32f9c89a0890e1591b4e1aa29ec2b52d83a13c6ed06c4fc3391245002c505e38", "advanced/voting-system.bug at O2": "3479679f427cb43fcc7288d34666f865d94c20a05f216f719f41a218624823ec", diff --git a/packages/bugc/src/optimizer/steps/return-merging.test.ts b/packages/bugc/src/optimizer/steps/return-merging.test.ts index e5e7d890d..130642b8d 100644 --- a/packages/bugc/src/optimizer/steps/return-merging.test.ts +++ b/packages/bugc/src/optimizer/steps/return-merging.test.ts @@ -4,6 +4,7 @@ import * as Irgen from "#irgen"; import { parse } from "#parser"; import { Result } from "#result"; import * as TypeChecker from "#typechecker"; +import { executeProgram } from "#test/evm/behavioral"; import { ReturnMergingStep } from "./return-merging.js"; import { OptimizationContextImpl } from "../optimizer.js"; @@ -198,4 +199,33 @@ describe("ReturnMergingStep", () => { // Should not merge returns with different values expect(returnBlocksAfter).toBe(returnBlocksBefore); }); + + it("keeps a call's continuation apart", async () => { + // The `return;` after the call continues the call; the other + // `return;` is entered by a jump + const source = `name ReturnAfterCall; +define { + function f(n: uint256) -> uint256 { + if (n == 0) { return 0; } + r = r + 1; + return f(n - 1); + }; +} +storage { [0] r: uint256; } +create {} +code { + if (msg.data.length == 0) { + return; + } + f(2); + return; +}`; + + const result = await executeProgram(source, { + calldata: "0x01", + optimizationLevel: 3, + }); + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(2n); + }); }); diff --git a/packages/bugc/src/optimizer/steps/return-merging.ts b/packages/bugc/src/optimizer/steps/return-merging.ts index d51c6602d..23f0e07e7 100644 --- a/packages/bugc/src/optimizer/steps/return-merging.ts +++ b/packages/bugc/src/optimizer/steps/return-merging.ts @@ -15,7 +15,17 @@ export class ReturnMergingStep extends BaseOptimizationStep { // Group blocks by their return value (or lack thereof) const returnGroups = new Map(); // signature -> block IDs + // A call's continuation is entered only from its call, with the + // callee's return value on the stack, so it is never merged + const continuations = new Set(); + for (const block of func.blocks.values()) { + if (block.terminator.kind === "call") { + continuations.add(block.terminator.continuation); + } + } + for (const [blockId, block] of func.blocks) { + if (continuations.has(blockId)) continue; if (block.terminator.kind === "return") { // Create a signature for the return statement const signature = this.getReturnSignature(block.terminator.value); From 560a4b55f14d7b11877a9f5afff8c8fa6de50561 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 14 Oct 2026 22:00:29 -0400 Subject: [PATCH 3/4] bugc: check continuations in the return merging test, in both orders --- .../optimizer/steps/return-merging.test.ts | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/bugc/src/optimizer/steps/return-merging.test.ts b/packages/bugc/src/optimizer/steps/return-merging.test.ts index 130642b8d..d5d06d646 100644 --- a/packages/bugc/src/optimizer/steps/return-merging.test.ts +++ b/packages/bugc/src/optimizer/steps/return-merging.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from "vitest"; import * as Irgen from "#irgen"; import { parse } from "#parser"; import { Result } from "#result"; +import { compile } from "#compiler"; import * as TypeChecker from "#typechecker"; import { executeProgram } from "#test/evm/behavioral"; @@ -200,10 +201,11 @@ describe("ReturnMergingStep", () => { expect(returnBlocksAfter).toBe(returnBlocksBefore); }); - it("keeps a call's continuation apart", async () => { - // The `return;` after the call continues the call; the other - // `return;` is entered by a jump - const source = `name ReturnAfterCall; + // A call's continuation is entered only from its call, so it is + // neither removed nor entered by a jump. Each `return;` here is a + // block of its own; the order puts the continuation after, then + // before, the other return + const program = (body: string) => `name ReturnAfterCall; define { function f(n: uint256) -> uint256 { if (n == 0) { return 0; } @@ -214,18 +216,63 @@ define { storage { [0] r: uint256; } create {} code { +${body} +}`; + const variants = { + "after the other return": ` if (msg.data.length == 0) { return; } f(2); - return; -}`; + return;`, + "before the other return": ` + if (msg.data.length != 0) { + f(2); + return; + } + return;`, + }; + + for (const [name, body] of Object.entries(variants)) { + it(`keeps a call's continuation apart (${name})`, async () => { + const source = program(body); + + const compiled = await compile({ + to: "ir", + source, + optimizer: { level: 3 }, + }); + if (!compiled.success) throw new Error("compile failed"); + const { blocks } = compiled.value.ir.main; + const continuations = [...blocks.values()].flatMap((block) => + block.terminator.kind === "call" ? [block.terminator.continuation] : [], + ); + expect(continuations).not.toHaveLength(0); + for (const continuation of continuations) { + expect(blocks.has(continuation)).toBe(true); + for (const block of blocks.values()) { + const { terminator } = block; + const targets = + terminator.kind === "jump" + ? [terminator.target] + : terminator.kind === "branch" + ? [terminator.trueTarget, terminator.falseTarget] + : []; + expect(targets).not.toContain(continuation); + } + } - const result = await executeProgram(source, { - calldata: "0x01", - optimizationLevel: 3, + for (const [calldata, expected] of [ + ["", 0n], + ["0x01", 2n], + ] as const) { + const result = await executeProgram(source, { + calldata, + optimizationLevel: 3, + }); + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(expected); + } }); - expect(result.callSuccess).toBe(true); - expect(await result.getStorage(0n)).toBe(2n); - }); + } }); From 165db2b550f9d0f6cbbb8278724e42d6c85b1dbb Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 14 Oct 2026 22:08:21 -0400 Subject: [PATCH 4/4] bugc: test return merging with a void callee; changelog --- packages/bugc/CHANGELOG.md | 10 ++ .../optimizer/steps/return-merging.test.ts | 97 +++++++++++-------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index 1709991b9..fb6e5e4ea 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -56,6 +56,15 @@ support. Changes to the specification itself are tracked in the root ### Fixed +- At optimization level 2 and up, a call's argument may repeat an + earlier computation (as `f(msg.sender)` after a use of + `msg.sender`). Before, common subexpression elimination removed the + repeat but left the call using it, and code generation failed + ("Cannot load value") ([#388]). +- At optimization level 3, a call followed by `return;` compiles when + the function has another `return;`. Before, return merging removed + the call's continuation block, and code generation failed ("Jump + target ... not found") ([#388]). - `push` of bytes in calldata onto an `array` in storage now stores the bytes. Before, it stored the calldata reference's word (offset and length) ([#379]). @@ -553,3 +562,4 @@ First publication. [#379]: https://github.com/ethdebug/format/pull/379 [#383]: https://github.com/ethdebug/format/pull/383 [#384]: https://github.com/ethdebug/format/pull/384 +[#388]: https://github.com/ethdebug/format/pull/388 diff --git a/packages/bugc/src/optimizer/steps/return-merging.test.ts b/packages/bugc/src/optimizer/steps/return-merging.test.ts index d5d06d646..842f0ba72 100644 --- a/packages/bugc/src/optimizer/steps/return-merging.test.ts +++ b/packages/bugc/src/optimizer/steps/return-merging.test.ts @@ -204,14 +204,23 @@ describe("ReturnMergingStep", () => { // A call's continuation is entered only from its call, so it is // neither removed nor entered by a jump. Each `return;` here is a // block of its own; the order puts the continuation after, then - // before, the other return - const program = (body: string) => `name ReturnAfterCall; -define { - function f(n: uint256) -> uint256 { + // before, the other return. The callee returns a value or nothing + // (a call with no `dest`) + const callees = { + value: `function f(n: uint256) -> uint256 { if (n == 0) { return 0; } r = r + 1; return f(n - 1); + };`, + void: `function f(n: uint256) { + if (n == 0) { return; } + r = r + 1; + f(n - 1); + };`, }; + const program = (callee: string, body: string) => `name ReturnAfterCall; +define { + ${callee} } storage { [0] r: uint256; } create {} @@ -234,45 +243,49 @@ ${body} }; for (const [name, body] of Object.entries(variants)) { - it(`keeps a call's continuation apart (${name})`, async () => { - const source = program(body); - - const compiled = await compile({ - to: "ir", - source, - optimizer: { level: 3 }, - }); - if (!compiled.success) throw new Error("compile failed"); - const { blocks } = compiled.value.ir.main; - const continuations = [...blocks.values()].flatMap((block) => - block.terminator.kind === "call" ? [block.terminator.continuation] : [], - ); - expect(continuations).not.toHaveLength(0); - for (const continuation of continuations) { - expect(blocks.has(continuation)).toBe(true); - for (const block of blocks.values()) { - const { terminator } = block; - const targets = - terminator.kind === "jump" - ? [terminator.target] - : terminator.kind === "branch" - ? [terminator.trueTarget, terminator.falseTarget] - : []; - expect(targets).not.toContain(continuation); + for (const [returns, callee] of Object.entries(callees)) { + it(`keeps a call's continuation apart (${name}, ${returns})`, async () => { + const source = program(callee, body); + + const compiled = await compile({ + to: "ir", + source, + optimizer: { level: 3 }, + }); + if (!compiled.success) throw new Error("compile failed"); + const { blocks } = compiled.value.ir.main; + const continuations = [...blocks.values()].flatMap((block) => + block.terminator.kind === "call" + ? [block.terminator.continuation] + : [], + ); + expect(continuations).not.toHaveLength(0); + for (const continuation of continuations) { + expect(blocks.has(continuation)).toBe(true); + for (const block of blocks.values()) { + const { terminator } = block; + const targets = + terminator.kind === "jump" + ? [terminator.target] + : terminator.kind === "branch" + ? [terminator.trueTarget, terminator.falseTarget] + : []; + expect(targets).not.toContain(continuation); + } } - } - for (const [calldata, expected] of [ - ["", 0n], - ["0x01", 2n], - ] as const) { - const result = await executeProgram(source, { - calldata, - optimizationLevel: 3, - }); - expect(result.callSuccess).toBe(true); - expect(await result.getStorage(0n)).toBe(expected); - } - }); + for (const [calldata, expected] of [ + ["", 0n], + ["0x01", 2n], + ] as const) { + const result = await executeProgram(source, { + calldata, + optimizationLevel: 3, + }); + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(expected); + } + }); + } } });