Skip to content

Commit 7501bfe

Browse files
committed
bugc: copy all of a slice's bytes
irgen built a slice's data as a read and a write, and a read or write moves at most one word, so only the first 32 bytes were copied. Add a copy IR instruction (CALLDATACOPY from calldata, MCOPY from memory) and emit it for the slice's data.
1 parent 6c81c10 commit 7501bfe

18 files changed

Lines changed: 231 additions & 35 deletions

File tree

‎packages/bugc-react/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ specification itself are tracked in the root
77

88
## Unreleased
99

10+
### Added
11+
12+
- The IR view shows bugc's new `copy` instruction ([#PR]).
13+
1014
## 0.1.0-preview.1 — 2026-10-03
1115

1216
Updated `@ethdebug/bugc` to `0.1.0-preview.1`.

‎packages/bugc-react/src/components/IrView.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ function InstructionRenderer({
336336
break;
337337
}
338338

339+
case "copy":
340+
add("memory[offset: ");
341+
addOperand("offset", formatValue(instruction.offset));
342+
add(", length: ");
343+
addOperand("length", formatValue(instruction.length));
344+
add(`] = copy ${instruction.location}[`);
345+
addOperand("source", formatValue(instruction.source));
346+
add("]");
347+
break;
348+
339349
default:
340350
add(`; unknown instruction: ${(instruction as { kind?: string }).kind}`);
341351
}

‎packages/bugc/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ support. Changes to the specification itself are tracked in the root
7979

8080
### Fixed
8181

82+
- A slice now copies all its bytes, as in `msg.data[4:]` or `m[1:40]`.
83+
The IR has a `copy` instruction, which compiles to `CALLDATACOPY` from
84+
calldata and `MCOPY` from memory. Before, a slice copied one word, so a
85+
slice longer than 32 bytes held zeros after its first 32 ([#PR]).
8286
- A call to a function with no return type now compiles as a statement,
8387
as in `bump();`, at every optimization level. The function's `return`
8488
context has no `data`. Before, IR generation failed with "Cannot convert

‎packages/bugc/src/evmgen/analysis/liveness.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ function getUsedValues(inst: Ir.Instruction): Set<string> {
303303
addValue(inst.length);
304304
addValue(inst.value);
305305
break;
306+
case "copy":
307+
addValue(inst.source);
308+
addValue(inst.offset);
309+
addValue(inst.length);
310+
break;
306311
// NEW: unified compute offset
307312
case "compute_offset":
308313
addValue(inst.base);
@@ -341,6 +346,7 @@ function getDefinedValue(inst: Ir.Instruction): string | undefined {
341346
return inst.dest;
342347
// These instructions don't define values
343348
case "write": // NEW: unified write
349+
case "copy":
344350
case "assert":
345351
return undefined;
346352
}

‎packages/bugc/src/evmgen/analysis/memory.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ function simulateInstruction(stack: string[], inst: Ir.Instruction): string[] {
281281
if (inst.length) newStack.pop();
282282
newStack.pop(); // value
283283
break;
284+
case "copy":
285+
newStack.pop(); // source
286+
newStack.pop(); // offset
287+
newStack.pop(); // length
288+
break;
284289
// NEW: compute offset
285290
case "compute_offset":
286291
newStack.pop(); // base
@@ -357,6 +362,11 @@ function getUsedValues(inst: Ir.Instruction): Set<string> {
357362
case "hash":
358363
addValue(inst.value);
359364
break;
365+
case "copy":
366+
addValue(inst.source);
367+
addValue(inst.offset);
368+
addValue(inst.length);
369+
break;
360370
case "assert":
361371
addValue(inst.condition);
362372
break;
@@ -416,6 +426,7 @@ function definedType(inst: Ir.Instruction): Ir.Type | undefined {
416426
case "compute_offset":
417427
return Ir.Type.Scalar.word;
418428
case "write":
429+
case "copy":
419430
case "assert":
420431
return undefined;
421432
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,33 @@ code {
10141014
// Called with no calldata, so a longer slice would revert
10151015
locals: { b: { shape: { kind: "bytes" }, values: ["0x"] } },
10161016
},
1017+
{
1018+
// A slice longer than a word: its pointer reads every byte
1019+
name: "a long bytes slice",
1020+
source: `name LongSlice;
1021+
storage { [0] r: uint256; [1] s: uint256; }
1022+
create { s = 1; }
1023+
code {
1024+
let data: bytes = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223;
1025+
let b = data[1:35];
1026+
if (s > 0) { r = b.length; }
1027+
r = r + b.length;
1028+
}`,
1029+
locals: {
1030+
data: {
1031+
shape: { kind: "bytes" },
1032+
values: [
1033+
"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
1034+
],
1035+
},
1036+
b: {
1037+
shape: { kind: "bytes" },
1038+
values: [
1039+
"0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122",
1040+
],
1041+
},
1042+
},
1043+
},
10171044
{
10181045
name: "a uint8 cast",
10191046
source: `name U8;

‎packages/bugc/src/evmgen/generation/instruction.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
generateAllocate,
2222
generateComputeOffset,
2323
generateAssert,
24+
generateCopy,
2425
} from "./instructions/index.js";
2526

2627
/**
@@ -41,6 +42,8 @@ export function generate<S extends Stack>(
4142
return generateRead(inst);
4243
case "write":
4344
return generateWrite(inst);
45+
case "copy":
46+
return generateCopy(inst);
4447
case "env":
4548
return generateEnvOp(inst);
4649
case "hash":
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type * as Ir from "#ir";
2+
import type { Stack } from "#evm";
3+
4+
import { type Transition, pipe, operations } from "#evmgen/operations";
5+
6+
import { loadValue } from "../values/index.js";
7+
8+
const { MCOPY, CALLDATACOPY } = operations;
9+
10+
/**
11+
* Generate code for a copy to memory: MCOPY from memory, CALLDATACOPY
12+
* from calldata
13+
*/
14+
export function generateCopy<S extends Stack>(
15+
inst: Ir.Instruction.Copy,
16+
): Transition<S, S> {
17+
const debug = inst.operationDebug;
18+
const copy = inst.location === "calldata" ? CALLDATACOPY : MCOPY;
19+
20+
return pipe<S>()
21+
.then(loadValue(inst.length, { debug }), { as: "size" })
22+
.then(loadValue(inst.source, { debug }), { as: "offset" })
23+
.then(loadValue(inst.offset, { debug }), { as: "destOffset" })
24+
.then(copy({ debug }))
25+
.done();
26+
}

‎packages/bugc/src/evmgen/generation/instructions/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { generateComputeSlot } from "./compute-slot.js";
1010
export { generateAllocate } from "./allocate.js";
1111
export { generateComputeOffset } from "./compute-offset.js";
1212
export { generateAssert } from "./assert.js";
13+
export { generateCopy } from "./copy.js";
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect } from "vitest";
2+
import { keccak256 } from "ethereum-cryptography/keccak";
3+
import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils";
4+
5+
import {
6+
executeProgram,
7+
type ExecuteProgramResult,
8+
} from "#test/evm/behavioral";
9+
10+
const lengths = [0, 31, 32, 33, 100];
11+
const levels = [0, 1, 2, 3] as const;
12+
13+
// 101 distinct bytes, 0x01 to 0x65
14+
const source = Array.from({ length: 101 }, (_, i) =>
15+
(i + 1).toString(16).padStart(2, "0"),
16+
).join("");
17+
18+
const word = (n: bigint) => hexToBytes(n.toString(16).padStart(64, "0"));
19+
20+
/** Storage `[1] out: mapping<uint256, uint256>` at key `i` */
21+
const outSlot = (i: number) =>
22+
BigInt(
23+
"0x" +
24+
bytesToHex(keccak256(new Uint8Array([...word(BigInt(i)), ...word(1n)]))),
25+
);
26+
27+
/**
28+
* The program copies a slice's length to slot 0, and its bytes, one by
29+
* one, to `out`
30+
*/
31+
const copyOut = (slice: string) => `
32+
let x = ${slice};
33+
n = x.length;
34+
for (let i = 0; i < x.length; i = i + 1) {
35+
out[i] = x[i];
36+
}`;
37+
38+
async function expectBytes(result: ExecuteProgramResult, expected: string) {
39+
expect(result.callSuccess).toBe(true);
40+
const bytes = hexToBytes(expected);
41+
expect(await result.getStorage(0n)).toBe(BigInt(bytes.length));
42+
for (let i = 0; i < bytes.length; i++) {
43+
expect(await result.getStorage(outSlot(i)), `byte ${i}`).toBe(
44+
BigInt(bytes[i]),
45+
);
46+
}
47+
}
48+
49+
describe("a slice copies all its bytes", () => {
50+
for (const level of levels) {
51+
for (const length of lengths) {
52+
it(`of msg.data, ${length} bytes (level ${level})`, async () => {
53+
const result = await executeProgram(
54+
`name CalldataSlice;
55+
storage { [0] n: uint256; [1] out: mapping<uint256, uint256>; }
56+
code {${copyOut("msg.data[1:msg.data.length]")}
57+
}`,
58+
{
59+
calldata: source.slice(0, (length + 1) * 2),
60+
optimizationLevel: level,
61+
},
62+
);
63+
64+
await expectBytes(result, source.slice(2, (length + 1) * 2));
65+
});
66+
67+
it(`of memory bytes, ${length} bytes (level ${level})`, async () => {
68+
const result = await executeProgram(
69+
`name MemorySlice;
70+
storage { [0] n: uint256; [1] out: mapping<uint256, uint256>; }
71+
code {
72+
let m: bytes = 0x${source};${copyOut(`m[1:${length + 1}]`)}
73+
}`,
74+
{ calldata: "", optimizationLevel: level },
75+
);
76+
77+
await expectBytes(result, source.slice(2, (length + 1) * 2));
78+
});
79+
}
80+
}
81+
});

0 commit comments

Comments
 (0)