Skip to content

Commit 8b5f1e8

Browse files
committed
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.
1 parent 718b019 commit 8b5f1e8

4 files changed

Lines changed: 85 additions & 100 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ support. Changes to the specification itself are tracked in the root
202202
- `.length` of an array literal, as in `[1, 2, 3].length`, is now the
203203
number of elements. Before, at every optimization level, it was 32
204204
([#344]).
205+
- A local initialized with a `bytes` literal
206+
(`let data: bytes = 0x0001…;`) now holds the literal's bytes. Before, at
207+
every optimization level, its data was the memory address of a copy of
208+
the literal, so its debug pointer read zeros and then that address
209+
([#345]).
205210

206211
## 0.1.0-preview.1 — 2026-10-03
207212

@@ -256,3 +261,4 @@ First publication.
256261
[#341]: https://github.com/ethdebug/format/pull/341
257262
[#343]: https://github.com/ethdebug/format/pull/343
258263
[#344]: https://github.com/ethdebug/format/pull/344
264+
[#345]: https://github.com/ethdebug/format/pull/345
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import { executeProgram } from "#test/evm/behavioral";
4+
5+
// 40 bytes, 0x01 to 0x28: too long for a bytesN, so `bytes`
6+
const literal = Array.from({ length: 40 }, (_, i) =>
7+
(i + 1).toString(16).padStart(2, "0"),
8+
).join("");
9+
10+
describe("a bytes literal", () => {
11+
for (const level of [0, 1, 2, 3] as const) {
12+
it(`should hold its bytes and length (level ${level})`, async () => {
13+
const source = `name BytesLiteral;
14+
storage { [0] n: uint256; [1] head: bytes32; [2] tail: bytes32; }
15+
code {
16+
let data: bytes = 0x${literal};
17+
n = data.length;
18+
head = data[0:32] as bytes32;
19+
tail = data[8:40] as bytes32;
20+
}`;
21+
22+
const result = await executeProgram(source, {
23+
calldata: "",
24+
optimizationLevel: level,
25+
});
26+
27+
expect(result.callSuccess).toBe(true);
28+
expect(await result.getStorage(0n)).toBe(40n);
29+
expect(await result.getStorage(1n)).toBe(
30+
BigInt("0x" + literal.slice(0, 64)),
31+
);
32+
expect(await result.getStorage(2n)).toBe(
33+
BigInt("0x" + literal.slice(16, 80)),
34+
);
35+
});
36+
}
37+
});

‎packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,25 @@ code {
960960
},
961961
},
962962
},
963+
{
964+
name: "bytes literal",
965+
source: `name BytesLit;
966+
storage { [0] r: uint256; [1] s: uint256; }
967+
create { s = 1; }
968+
code {
969+
let data: bytes = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223;
970+
if (s > 0) { r = data.length; }
971+
r = r + 1;
972+
}`,
973+
locals: {
974+
data: {
975+
shape: { kind: "bytes" },
976+
values: [
977+
"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
978+
],
979+
},
980+
},
981+
},
963982
{
964983
name: "bytes slice",
965984
source: `name Slice;

‎packages/bugc/src/irgen/generate/statements/declare.ts‎

Lines changed: 23 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -58,34 +58,9 @@ function* buildVariableDeclaration(
5858
if (needsMemoryAllocation) {
5959
// For types that need memory allocation
6060

61-
// For reference types, calculate size needed
62-
let sizeValue: Ir.Value;
63-
64-
// Check if we have an initializer to determine size
65-
if (decl.initializer) {
66-
// For hex literals, calculate actual size needed
67-
if (
68-
Ast.Expression.isLiteral(decl.initializer) &&
69-
Ast.Expression.Literal.isHex(decl.initializer)
70-
) {
71-
const hexLiteral = decl.initializer as Ast.Expression.Literal;
72-
const hexValue = hexLiteral.value.startsWith("0x")
73-
? hexLiteral.value.slice(2)
74-
: hexLiteral.value;
75-
const byteSize = hexValue.length / 2;
76-
// Add 32 bytes for length prefix
77-
sizeValue = Ir.Value.constant(
78-
BigInt(byteSize + 32),
79-
Ir.Type.Scalar.uint256,
80-
);
81-
} else {
82-
// Default size for other initializers
83-
sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256);
84-
}
85-
} else {
86-
// Default size when no initializer
87-
sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256);
88-
}
61+
// A placeholder allocation, for a local declared without an
62+
// initializer; with one, the local's value is the initializer's
63+
const sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256);
8964

9065
// Allocate memory
9166
const allocTemp = yield* Process.Variables.newTemp();
@@ -98,14 +73,9 @@ function* buildVariableDeclaration(
9873
} as Ir.Instruction);
9974

10075
// Declare the SSA variable and directly use the allocTemp as its value
101-
// With an initializer other than a hex literal, the local's value
102-
// is the initializer's (below), not this allocation.
103-
const builtLater =
104-
!!decl.initializer &&
105-
!(
106-
Ast.Expression.isLiteral(decl.initializer) &&
107-
Ast.Expression.Literal.isHex(decl.initializer)
108-
);
76+
// With an initializer, the local's value is the initializer's
77+
// (below), not this allocation.
78+
const builtLater = !!decl.initializer;
10979
yield* Process.Variables.declareWithExistingTemp(
11080
decl.name,
11181
irType,
@@ -120,71 +90,24 @@ function* buildVariableDeclaration(
12090
kind: "rvalue",
12191
});
12292

123-
// For reference types, we need to handle initialization
124-
// Check the initializer type to determine how to store it
125-
if (!builtLater) {
126-
const hexLiteral = decl.initializer as Ast.Expression.Literal;
127-
if (Ast.Expression.Literal.isHex(hexLiteral)) {
128-
const hexValue = hexLiteral.value.startsWith("0x")
129-
? hexLiteral.value.slice(2)
130-
: hexLiteral.value;
131-
const byteSize = hexValue.length / 2;
132-
133-
// Store length at the beginning
134-
yield* Process.Instructions.emit({
135-
kind: "write",
136-
location: "memory",
137-
offset: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256),
138-
length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256),
139-
value: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256),
140-
operationDebug: yield* Process.Debug.forAstNode(decl),
141-
} as Ir.Instruction.Write);
142-
143-
// Store the actual bytes data after the length
144-
const dataOffsetTemp = yield* Process.Variables.newTemp();
145-
yield* Process.Instructions.emit({
146-
kind: "binary",
147-
op: "add",
148-
left: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256),
149-
right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256),
150-
dest: dataOffsetTemp,
151-
operationDebug: yield* Process.Debug.forAstNode(decl),
152-
} as Ir.Instruction.BinaryOp);
153-
154-
yield* Process.Instructions.emit({
155-
kind: "write",
156-
location: "memory",
157-
offset: Ir.Value.temp(dataOffsetTemp, Ir.Type.Scalar.uint256),
158-
length: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256),
159-
value: value,
160-
operationDebug: yield* Process.Debug.forAstNode(decl),
161-
} as Ir.Instruction.Write);
162-
}
93+
// The value is already a reference to memory (a literal, an
94+
// array or a slice): the local refers to it
95+
if (value.kind === "temp") {
96+
yield* Process.Variables.updateSsaToExistingTemp(
97+
decl.name,
98+
value.id,
99+
irType,
100+
);
163101
} else {
164-
// For string literals, slice expressions and other bytes operations,
165-
// the value is already a reference to memory
166-
// We need to copy the slice result to the new allocation
167-
// This is a simplified version - a full implementation would need to
168-
// handle different cases more carefully
169-
170-
// If the value is a temp, update the SSA variable to point to it
171-
if (value.kind === "temp") {
172-
yield* Process.Variables.updateSsaToExistingTemp(
173-
decl.name,
174-
value.id,
175-
irType,
176-
);
177-
} else {
178-
// For non-temp values, we need to copy it
179-
yield* Process.Instructions.emit({
180-
kind: "binary",
181-
op: "add",
182-
left: value,
183-
right: Ir.Value.constant(0n, Ir.Type.Scalar.uint256),
184-
dest: allocTemp,
185-
operationDebug: yield* Process.Debug.forAstNode(decl),
186-
} as Ir.Instruction.BinaryOp);
187-
}
102+
// For non-temp values, we need to copy it
103+
yield* Process.Instructions.emit({
104+
kind: "binary",
105+
op: "add",
106+
left: value,
107+
right: Ir.Value.constant(0n, Ir.Type.Scalar.uint256),
108+
dest: allocTemp,
109+
operationDebug: yield* Process.Debug.forAstNode(decl),
110+
} as Ir.Instruction.BinaryOp);
188111
}
189112
}
190113
} else {

0 commit comments

Comments
 (0)