From 8b5f1e8c5d2326c3d519bbca063c9f905952feee Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 6 Oct 2026 08:32:40 -0400 Subject: [PATCH] bugc: make a bytes literal local hold the literal's bytes A `bytes` literal compiles to a constant that already lays the literal out in memory (its length word, then its data) and gives its address. A declaration initialized with one allocated a second copy anyway, wrote the length there, and then wrote the first copy's address as the data. So the local's data read as zeros followed by that address, both in the program (as in `data[0:32]`) and through its debug pointer. Let the local refer to the constant, as a string literal local does. --- packages/bugc/CHANGELOG.md | 6 + .../bugc/src/evmgen/bytes-literal.test.ts | 37 ++++++ .../debug/local-variables.soundness.test.ts | 19 +++ .../src/irgen/generate/statements/declare.ts | 123 ++++-------------- 4 files changed, 85 insertions(+), 100 deletions(-) create mode 100644 packages/bugc/src/evmgen/bytes-literal.test.ts diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index 9714910944..5eee6e0cc1 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -202,6 +202,11 @@ support. Changes to the specification itself are tracked in the root - `.length` of an array literal, as in `[1, 2, 3].length`, is now the number of elements. Before, at every optimization level, it was 32 ([#344]). +- A local initialized with a `bytes` literal + (`let data: bytes = 0x0001…;`) now holds the literal's bytes. Before, at + every optimization level, its data was the memory address of a copy of + the literal, so its debug pointer read zeros and then that address + ([#345]). ## 0.1.0-preview.1 — 2026-10-03 @@ -256,3 +261,4 @@ First publication. [#341]: https://github.com/ethdebug/format/pull/341 [#343]: https://github.com/ethdebug/format/pull/343 [#344]: https://github.com/ethdebug/format/pull/344 +[#345]: https://github.com/ethdebug/format/pull/345 diff --git a/packages/bugc/src/evmgen/bytes-literal.test.ts b/packages/bugc/src/evmgen/bytes-literal.test.ts new file mode 100644 index 0000000000..c8299ba1ca --- /dev/null +++ b/packages/bugc/src/evmgen/bytes-literal.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; + +import { executeProgram } from "#test/evm/behavioral"; + +// 40 bytes, 0x01 to 0x28: too long for a bytesN, so `bytes` +const literal = Array.from({ length: 40 }, (_, i) => + (i + 1).toString(16).padStart(2, "0"), +).join(""); + +describe("a bytes literal", () => { + for (const level of [0, 1, 2, 3] as const) { + it(`should hold its bytes and length (level ${level})`, async () => { + const source = `name BytesLiteral; +storage { [0] n: uint256; [1] head: bytes32; [2] tail: bytes32; } +code { + let data: bytes = 0x${literal}; + n = data.length; + head = data[0:32] as bytes32; + tail = data[8:40] as bytes32; +}`; + + const result = await executeProgram(source, { + calldata: "", + optimizationLevel: level, + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(0n)).toBe(40n); + expect(await result.getStorage(1n)).toBe( + BigInt("0x" + literal.slice(0, 64)), + ); + expect(await result.getStorage(2n)).toBe( + BigInt("0x" + literal.slice(16, 80)), + ); + }); + } +}); diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts index d3d6d750e3..951c0bf3a8 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts @@ -960,6 +960,25 @@ code { }, }, }, + { + name: "bytes literal", + source: `name BytesLit; +storage { [0] r: uint256; [1] s: uint256; } +create { s = 1; } +code { + let data: bytes = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223; + if (s > 0) { r = data.length; } + r = r + 1; +}`, + locals: { + data: { + shape: { kind: "bytes" }, + values: [ + "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223", + ], + }, + }, + }, { name: "bytes slice", source: `name Slice; diff --git a/packages/bugc/src/irgen/generate/statements/declare.ts b/packages/bugc/src/irgen/generate/statements/declare.ts index 9242c1e075..4680a16a4f 100644 --- a/packages/bugc/src/irgen/generate/statements/declare.ts +++ b/packages/bugc/src/irgen/generate/statements/declare.ts @@ -58,34 +58,9 @@ function* buildVariableDeclaration( if (needsMemoryAllocation) { // For types that need memory allocation - // For reference types, calculate size needed - let sizeValue: Ir.Value; - - // Check if we have an initializer to determine size - if (decl.initializer) { - // For hex literals, calculate actual size needed - if ( - Ast.Expression.isLiteral(decl.initializer) && - Ast.Expression.Literal.isHex(decl.initializer) - ) { - const hexLiteral = decl.initializer as Ast.Expression.Literal; - const hexValue = hexLiteral.value.startsWith("0x") - ? hexLiteral.value.slice(2) - : hexLiteral.value; - const byteSize = hexValue.length / 2; - // Add 32 bytes for length prefix - sizeValue = Ir.Value.constant( - BigInt(byteSize + 32), - Ir.Type.Scalar.uint256, - ); - } else { - // Default size for other initializers - sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256); - } - } else { - // Default size when no initializer - sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256); - } + // A placeholder allocation, for a local declared without an + // initializer; with one, the local's value is the initializer's + const sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256); // Allocate memory const allocTemp = yield* Process.Variables.newTemp(); @@ -98,14 +73,9 @@ function* buildVariableDeclaration( } as Ir.Instruction); // Declare the SSA variable and directly use the allocTemp as its value - // With an initializer other than a hex literal, the local's value - // is the initializer's (below), not this allocation. - const builtLater = - !!decl.initializer && - !( - Ast.Expression.isLiteral(decl.initializer) && - Ast.Expression.Literal.isHex(decl.initializer) - ); + // With an initializer, the local's value is the initializer's + // (below), not this allocation. + const builtLater = !!decl.initializer; yield* Process.Variables.declareWithExistingTemp( decl.name, irType, @@ -120,71 +90,24 @@ function* buildVariableDeclaration( kind: "rvalue", }); - // For reference types, we need to handle initialization - // Check the initializer type to determine how to store it - if (!builtLater) { - const hexLiteral = decl.initializer as Ast.Expression.Literal; - if (Ast.Expression.Literal.isHex(hexLiteral)) { - const hexValue = hexLiteral.value.startsWith("0x") - ? hexLiteral.value.slice(2) - : hexLiteral.value; - const byteSize = hexValue.length / 2; - - // Store length at the beginning - yield* Process.Instructions.emit({ - kind: "write", - location: "memory", - offset: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256), - length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), - value: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256), - operationDebug: yield* Process.Debug.forAstNode(decl), - } as Ir.Instruction.Write); - - // Store the actual bytes data after the length - const dataOffsetTemp = yield* Process.Variables.newTemp(); - yield* Process.Instructions.emit({ - kind: "binary", - op: "add", - left: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256), - right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), - dest: dataOffsetTemp, - operationDebug: yield* Process.Debug.forAstNode(decl), - } as Ir.Instruction.BinaryOp); - - yield* Process.Instructions.emit({ - kind: "write", - location: "memory", - offset: Ir.Value.temp(dataOffsetTemp, Ir.Type.Scalar.uint256), - length: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256), - value: value, - operationDebug: yield* Process.Debug.forAstNode(decl), - } as Ir.Instruction.Write); - } + // The value is already a reference to memory (a literal, an + // array or a slice): the local refers to it + if (value.kind === "temp") { + yield* Process.Variables.updateSsaToExistingTemp( + decl.name, + value.id, + irType, + ); } else { - // For string literals, slice expressions and other bytes operations, - // the value is already a reference to memory - // We need to copy the slice result to the new allocation - // This is a simplified version - a full implementation would need to - // handle different cases more carefully - - // If the value is a temp, update the SSA variable to point to it - if (value.kind === "temp") { - yield* Process.Variables.updateSsaToExistingTemp( - decl.name, - value.id, - irType, - ); - } else { - // For non-temp values, we need to copy it - yield* Process.Instructions.emit({ - kind: "binary", - op: "add", - left: value, - right: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), - dest: allocTemp, - operationDebug: yield* Process.Debug.forAstNode(decl), - } as Ir.Instruction.BinaryOp); - } + // For non-temp values, we need to copy it + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: value, + right: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + dest: allocTemp, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.BinaryOp); } } } else {