From b646959236e1e6e04d7dd4d4dcb06c0fa64e6983 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 10 Mar 2026 23:58:29 -0400 Subject: [PATCH 1/3] bugc: Add behavioral test helper and initial test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add executeProgram() helper that compiles BUG source, deploys bytecode via @ethdebug/evm Executor, optionally calls with calldata, and returns results for direct assertion on storage and execution state — no pointer dereferencing needed. Tests cover: constructor storage init, runtime storage modification, multiple calls, for loops, conditionals, bare return (STOP), and compilation failure. Internal function calls are skipped (known stack underflow issue). --- packages/bugc/src/evmgen/behavioral.test.ts | 250 ++++++++++++++++++++ packages/bugc/test/evm/behavioral.ts | 111 +++++++++ 2 files changed, 361 insertions(+) create mode 100644 packages/bugc/src/evmgen/behavioral.test.ts create mode 100644 packages/bugc/test/evm/behavioral.ts diff --git a/packages/bugc/src/evmgen/behavioral.test.ts b/packages/bugc/src/evmgen/behavioral.test.ts new file mode 100644 index 000000000..a337aed88 --- /dev/null +++ b/packages/bugc/src/evmgen/behavioral.test.ts @@ -0,0 +1,250 @@ +import { describe, it, expect } from "vitest"; + +import { executeProgram } from "#test/evm/behavioral"; + +describe("behavioral tests", () => { + describe("deploy + check storage", () => { + it("should initialize storage in constructor", async () => { + const source = ` + name InitStorage; + + storage { + [0] value: uint256; + [1] flag: uint256; + } + + create { + value = 42; + flag = 1; + } + + code {} + `; + + const result = await executeProgram(source); + expect(result.deployed).toBe(true); + expect(await result.getStorage(0n)).toBe(42n); + expect(await result.getStorage(1n)).toBe(1n); + }); + + it("should handle multiple storage slots", async () => { + const source = ` + name MultiSlot; + + storage { + [0] a: uint256; + [1] b: uint256; + [2] c: uint256; + } + + create { + a = 100; + b = 200; + c = a + b; + } + + code {} + `; + + const result = await executeProgram(source); + expect(await result.getStorage(0n)).toBe(100n); + expect(await result.getStorage(1n)).toBe(200n); + expect(await result.getStorage(2n)).toBe(300n); + }); + }); + + describe("deploy + call + check storage", () => { + it("should modify storage on call", async () => { + const source = ` + name Counter; + + storage { + [0] count: uint256; + } + + create { + count = 0; + } + + code { + count = count + 1; + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(1n); + }); + + it("should support multiple calls", async () => { + const source = ` + name MultiCall; + + storage { + [0] count: uint256; + } + + create { + count = 0; + } + + code { + count = count + 1; + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + expect(await result.getStorage(0n)).toBe(1n); + + // Call again using the same executor + const execResult = await result.executor.execute({ + data: "", + }); + expect(execResult.success).toBe(true); + expect(await result.getStorage(0n)).toBe(2n); + }); + }); + + describe("internal functions", () => { + // Internal function calls currently fail at runtime + // (stack underflow). Tracked as known issue. + it.skip("should call defined functions and store result", async () => { + const source = ` + name FuncCall; + + define { + function add( + a: uint256, b: uint256 + ) -> uint256 { + return a + b; + }; + } + + storage { + [0] result: uint256; + } + + create { + result = 0; + } + + code { + result = add(10, 20); + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(30n); + }); + }); + + describe("loops", () => { + it("should execute a for loop", async () => { + const source = ` + name Loop; + + storage { + [0] total: uint256; + } + + create { + total = 0; + } + + code { + for (let i = 0; i < 5; i = i + 1) { + total = total + 1; + } + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(5n); + }); + }); + + describe("conditional behavior", () => { + it("should execute conditional branches", async () => { + const source = ` + name Conditional; + + storage { + [0] value: uint256; + [1] flag: uint256; + } + + create { + value = 0; + flag = 1; + } + + code { + if (flag == 1) { + value = 100; + } else { + value = 200; + } + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(100n); + }); + }); + + describe("error paths", () => { + it("should report compilation failure", async () => { + const source = `this is not valid BUG code`; + + await expect(executeProgram(source)).rejects.toThrow( + /Compilation failed/, + ); + }); + + it("should handle bare return (STOP) gracefully", async () => { + const source = ` + name EarlyReturn; + + storage { + [0] reached: uint256; + } + + create { + reached = 0; + } + + code { + reached = 1; + return; + } + `; + + const result = await executeProgram(source, { + calldata: "", + }); + + // STOP is not an error — it's a clean halt + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(1n); + // No return data from bare return + expect(result.returnValue.length).toBe(0); + }); + }); +}); diff --git a/packages/bugc/test/evm/behavioral.ts b/packages/bugc/test/evm/behavioral.ts new file mode 100644 index 000000000..1b237c58f --- /dev/null +++ b/packages/bugc/test/evm/behavioral.ts @@ -0,0 +1,111 @@ +/** + * Behavioral test helper for bugc. + * + * Compiles BUG source, deploys bytecode, optionally calls + * with calldata, and returns execution results for assertion. + * No pointer dereferencing — just raw EVM state. + */ + +import { compile } from "#compiler"; +import { Executor } from "@ethdebug/evm"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +export interface ExecuteProgramOptions { + /** Calldata hex string (without 0x) to send after deploy */ + calldata?: string; + /** ETH value (wei) to send with the call */ + value?: bigint; + /** Optimization level (default: 0) */ + optimizationLevel?: 0 | 1 | 2 | 3; +} + +export interface ExecuteProgramResult { + /** Whether deployment succeeded */ + deployed: boolean; + /** Whether the call succeeded (undefined if no call) */ + callSuccess?: boolean; + /** Return data from the call (empty if no call) */ + returnValue: Uint8Array; + /** Read a storage slot as bigint */ + getStorage: (slot: bigint) => Promise; + /** The executor instance for advanced queries */ + executor: Executor; +} + +/** + * Compile BUG source, deploy, optionally call, and return + * results for behavioral assertions. + * + * Throws if compilation fails. + */ +export async function executeProgram( + source: string, + options: ExecuteProgramOptions = {}, +): Promise { + const { calldata, value, optimizationLevel = 0 } = options; + + // Compile + const result = await compile({ + to: "bytecode", + source, + optimizer: { level: optimizationLevel }, + }); + + if (!result.success) { + const errors = result.messages.error ?? []; + const msgs = errors + .map((e: { message?: string }) => e.message ?? String(e)) + .join("\n"); + throw new Error(`Compilation failed:\n${msgs}`); + } + + const { bytecode } = result.value; + const executor = new Executor(); + + // Deploy + const hasCreate = bytecode.create && bytecode.create.length > 0; + const createCode = hasCreate + ? bytesToHex(bytecode.create!) + : bytesToHex(bytecode.runtime); + + await executor.deploy(createCode); + + let callSuccess: boolean | undefined; + let returnValue = new Uint8Array(); + + // Call if calldata provided (even empty string means "call") + if (calldata !== undefined) { + const execResult = await executor.execute({ + data: calldata, + value, + }); + callSuccess = execResult.success; + returnValue = execResult.returnValue; + } + + return { + deployed: true, + callSuccess, + returnValue, + getStorage: (slot: bigint) => executor.getStorage(slot), + executor, + }; +} + +/** + * Read a uint256 from a return value at the given word offset. + */ +export function readUint256( + returnValue: Uint8Array, + wordOffset: number = 0, +): bigint { + const start = wordOffset * 32; + const end = start + 32; + + if (returnValue.length < end) { + return 0n; + } + + const slice = returnValue.slice(start, end); + return BigInt("0x" + bytesToHex(slice)); +} From c85e1e9b370e643e894e48d14ce4eabe930d93d1 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 00:01:19 -0400 Subject: [PATCH 2/3] Fix Uint8Array type compatibility for strict TS Copy returnValue via `new Uint8Array()` to avoid Uint8Array vs Uint8Array mismatch in TS 5.7+ strict mode. --- packages/bugc/test/evm/behavioral.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bugc/test/evm/behavioral.ts b/packages/bugc/test/evm/behavioral.ts index 1b237c58f..5c5b31cd8 100644 --- a/packages/bugc/test/evm/behavioral.ts +++ b/packages/bugc/test/evm/behavioral.ts @@ -71,7 +71,7 @@ export async function executeProgram( await executor.deploy(createCode); let callSuccess: boolean | undefined; - let returnValue = new Uint8Array(); + let returnValue: Uint8Array = new Uint8Array(); // Call if calldata provided (even empty string means "call") if (calldata !== undefined) { @@ -80,7 +80,7 @@ export async function executeProgram( value, }); callSuccess = execResult.success; - returnValue = execResult.returnValue; + returnValue = new Uint8Array(execResult.returnValue); } return { From f846e116968179ee6c7f7cbacce5af99ace0e980 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 00:02:27 -0400 Subject: [PATCH 3/3] Address style review: flush-left sources, trim JSDoc, remove unused export --- packages/bugc/src/evmgen/behavioral.test.ts | 231 +++++++++----------- packages/bugc/test/evm/behavioral.ts | 43 ---- 2 files changed, 105 insertions(+), 169 deletions(-) diff --git a/packages/bugc/src/evmgen/behavioral.test.ts b/packages/bugc/src/evmgen/behavioral.test.ts index a337aed88..18c10db35 100644 --- a/packages/bugc/src/evmgen/behavioral.test.ts +++ b/packages/bugc/src/evmgen/behavioral.test.ts @@ -5,21 +5,19 @@ import { executeProgram } from "#test/evm/behavioral"; describe("behavioral tests", () => { describe("deploy + check storage", () => { it("should initialize storage in constructor", async () => { - const source = ` - name InitStorage; + const source = `name InitStorage; - storage { - [0] value: uint256; - [1] flag: uint256; - } +storage { + [0] value: uint256; + [1] flag: uint256; +} - create { - value = 42; - flag = 1; - } +create { + value = 42; + flag = 1; +} - code {} - `; +code {}`; const result = await executeProgram(source); expect(result.deployed).toBe(true); @@ -28,23 +26,21 @@ describe("behavioral tests", () => { }); it("should handle multiple storage slots", async () => { - const source = ` - name MultiSlot; + const source = `name MultiSlot; - storage { - [0] a: uint256; - [1] b: uint256; - [2] c: uint256; - } +storage { + [0] a: uint256; + [1] b: uint256; + [2] c: uint256; +} - create { - a = 100; - b = 200; - c = a + b; - } +create { + a = 100; + b = 200; + c = a + b; +} - code {} - `; +code {}`; const result = await executeProgram(source); expect(await result.getStorage(0n)).toBe(100n); @@ -55,21 +51,19 @@ describe("behavioral tests", () => { describe("deploy + call + check storage", () => { it("should modify storage on call", async () => { - const source = ` - name Counter; + const source = `name Counter; - storage { - [0] count: uint256; - } +storage { + [0] count: uint256; +} - create { - count = 0; - } +create { + count = 0; +} - code { - count = count + 1; - } - `; +code { + count = count + 1; +}`; const result = await executeProgram(source, { calldata: "", @@ -80,28 +74,25 @@ describe("behavioral tests", () => { }); it("should support multiple calls", async () => { - const source = ` - name MultiCall; + const source = `name MultiCall; - storage { - [0] count: uint256; - } +storage { + [0] count: uint256; +} - create { - count = 0; - } +create { + count = 0; +} - code { - count = count + 1; - } - `; +code { + count = count + 1; +}`; const result = await executeProgram(source, { calldata: "", }); expect(await result.getStorage(0n)).toBe(1n); - // Call again using the same executor const execResult = await result.executor.execute({ data: "", }); @@ -113,30 +104,26 @@ describe("behavioral tests", () => { describe("internal functions", () => { // Internal function calls currently fail at runtime // (stack underflow). Tracked as known issue. - it.skip("should call defined functions and store result", async () => { - const source = ` - name FuncCall; - - define { - function add( - a: uint256, b: uint256 - ) -> uint256 { - return a + b; - }; - } - - storage { - [0] result: uint256; - } - - create { - result = 0; - } - - code { - result = add(10, 20); - } - `; + it.skip("should call defined functions", async () => { + const source = `name FuncCall; + +define { + function add(a: uint256, b: uint256) -> uint256 { + return a + b; + }; +} + +storage { + [0] result: uint256; +} + +create { + result = 0; +} + +code { + result = add(10, 20); +}`; const result = await executeProgram(source, { calldata: "", @@ -149,23 +136,21 @@ describe("behavioral tests", () => { describe("loops", () => { it("should execute a for loop", async () => { - const source = ` - name Loop; + const source = `name Loop; - storage { - [0] total: uint256; - } +storage { + [0] total: uint256; +} - create { - total = 0; - } +create { + total = 0; +} - code { - for (let i = 0; i < 5; i = i + 1) { - total = total + 1; - } - } - `; +code { + for (let i = 0; i < 5; i = i + 1) { + total = total + 1; + } +}`; const result = await executeProgram(source, { calldata: "", @@ -178,27 +163,25 @@ describe("behavioral tests", () => { describe("conditional behavior", () => { it("should execute conditional branches", async () => { - const source = ` - name Conditional; - - storage { - [0] value: uint256; - [1] flag: uint256; - } - - create { - value = 0; - flag = 1; - } - - code { - if (flag == 1) { - value = 100; - } else { - value = 200; - } - } - `; + const source = `name Conditional; + +storage { + [0] value: uint256; + [1] flag: uint256; +} + +create { + value = 0; + flag = 1; +} + +code { + if (flag == 1) { + value = 100; + } else { + value = 200; + } +}`; const result = await executeProgram(source, { calldata: "", @@ -218,32 +201,28 @@ describe("behavioral tests", () => { ); }); - it("should handle bare return (STOP) gracefully", async () => { - const source = ` - name EarlyReturn; + it("should handle bare return (STOP)", async () => { + const source = `name EarlyReturn; - storage { - [0] reached: uint256; - } +storage { + [0] reached: uint256; +} - create { - reached = 0; - } +create { + reached = 0; +} - code { - reached = 1; - return; - } - `; +code { + reached = 1; + return; +}`; const result = await executeProgram(source, { calldata: "", }); - // STOP is not an error — it's a clean halt expect(result.callSuccess).toBe(true); expect(await result.getStorage(0n)).toBe(1n); - // No return data from bare return expect(result.returnValue.length).toBe(0); }); }); diff --git a/packages/bugc/test/evm/behavioral.ts b/packages/bugc/test/evm/behavioral.ts index 5c5b31cd8..33371e29f 100644 --- a/packages/bugc/test/evm/behavioral.ts +++ b/packages/bugc/test/evm/behavioral.ts @@ -1,50 +1,27 @@ -/** - * Behavioral test helper for bugc. - * - * Compiles BUG source, deploys bytecode, optionally calls - * with calldata, and returns execution results for assertion. - * No pointer dereferencing — just raw EVM state. - */ - import { compile } from "#compiler"; import { Executor } from "@ethdebug/evm"; import { bytesToHex } from "ethereum-cryptography/utils"; export interface ExecuteProgramOptions { - /** Calldata hex string (without 0x) to send after deploy */ calldata?: string; - /** ETH value (wei) to send with the call */ value?: bigint; - /** Optimization level (default: 0) */ optimizationLevel?: 0 | 1 | 2 | 3; } export interface ExecuteProgramResult { - /** Whether deployment succeeded */ deployed: boolean; - /** Whether the call succeeded (undefined if no call) */ callSuccess?: boolean; - /** Return data from the call (empty if no call) */ returnValue: Uint8Array; - /** Read a storage slot as bigint */ getStorage: (slot: bigint) => Promise; - /** The executor instance for advanced queries */ executor: Executor; } -/** - * Compile BUG source, deploy, optionally call, and return - * results for behavioral assertions. - * - * Throws if compilation fails. - */ export async function executeProgram( source: string, options: ExecuteProgramOptions = {}, ): Promise { const { calldata, value, optimizationLevel = 0 } = options; - // Compile const result = await compile({ to: "bytecode", source, @@ -62,7 +39,6 @@ export async function executeProgram( const { bytecode } = result.value; const executor = new Executor(); - // Deploy const hasCreate = bytecode.create && bytecode.create.length > 0; const createCode = hasCreate ? bytesToHex(bytecode.create!) @@ -73,7 +49,6 @@ export async function executeProgram( let callSuccess: boolean | undefined; let returnValue: Uint8Array = new Uint8Array(); - // Call if calldata provided (even empty string means "call") if (calldata !== undefined) { const execResult = await executor.execute({ data: calldata, @@ -91,21 +66,3 @@ export async function executeProgram( executor, }; } - -/** - * Read a uint256 from a return value at the given word offset. - */ -export function readUint256( - returnValue: Uint8Array, - wordOffset: number = 0, -): bigint { - const start = wordOffset * 32; - const end = start + 32; - - if (returnValue.length < end) { - return 0n; - } - - const slice = returnValue.slice(start, end); - return BigInt("0x" + bytesToHex(slice)); -}