Skip to content

Commit c058c33

Browse files
committed
bugc: copy a struct read from storage into memory
1 parent 6c81c10 commit c058c33

7 files changed

Lines changed: 300 additions & 9 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

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

8080
### Fixed
8181

82+
- A struct read from storage into a local, as in
83+
`let p = players[who];` or `let s = stored;`, is now a copy in memory,
84+
as the examples' README describes: reading `p.score` reads the copy,
85+
and a write to `p.score` changes the copy, not storage. A field that is
86+
a struct is copied too; a struct with an array, mapping, `string` or
87+
dynamic `bytes` field is an error. Before, at every optimization level,
88+
the local held the struct's first storage word as if it were a memory
89+
address, and a write to one of its fields failed code generation
90+
([#PR]).
91+
- A value whose only later uses are as the address, slot or value of a
92+
read or write, or as the base of an offset, now gets a home in memory
93+
when code generation cannot keep it on the stack. Before, such a value
94+
could fail code generation with "Cannot load value" ([#PR]).
8295
- A call to a function with no return type now compiles as a statement,
8396
as in `bump();`, at every optimization level. The function's `return`
8497
context has no `data`. Before, IR generation failed with "Cannot convert

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,25 @@ function getUsedValues(inst: Ir.Instruction): Set<string> {
360360
case "assert":
361361
addValue(inst.condition);
362362
break;
363+
case "read":
364+
addValue(inst.slot);
365+
addValue(inst.offset);
366+
addValue(inst.length);
367+
break;
368+
case "write":
369+
addValue(inst.slot);
370+
addValue(inst.offset);
371+
addValue(inst.length);
372+
addValue(inst.value);
373+
break;
374+
case "compute_offset":
375+
addValue(inst.base);
376+
if (Ir.Instruction.ComputeOffset.isArray(inst)) {
377+
addValue(inst.index);
378+
} else if (Ir.Instruction.ComputeOffset.isByte(inst)) {
379+
addValue(inst.offset);
380+
}
381+
break;
363382
// Call instruction removed - calls are now block terminators
364383
}
365384

‎packages/bugc/src/evmgen/behavioral.test.ts‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,105 @@ code {
12891289
}
12901290
});
12911291

1292+
describe("struct locals copied from storage", () => {
1293+
const program = (body: string) => `name StructCopy;
1294+
1295+
define {
1296+
struct Inner {
1297+
a: uint8;
1298+
b: uint256;
1299+
};
1300+
struct Player {
1301+
score: uint64;
1302+
combo: int32;
1303+
active: bool;
1304+
total: uint256;
1305+
inner: Inner;
1306+
};
1307+
}
1308+
1309+
storage {
1310+
[0] players: mapping<address, Player>;
1311+
[1] out: uint256;
1312+
[2] one: Player;
1313+
}
1314+
1315+
create {
1316+
players[0x0000000000000000000000000000000000000000].score = 10;
1317+
players[0x0000000000000000000000000000000000000000].combo = (0 as int32) - 3;
1318+
players[0x0000000000000000000000000000000000000000].active = true;
1319+
players[0x0000000000000000000000000000000000000000].total = 99;
1320+
players[0x0000000000000000000000000000000000000000].inner.a = 7;
1321+
players[0x0000000000000000000000000000000000000000].inner.b = 8;
1322+
one.score = 20;
1323+
one.total = 21;
1324+
}
1325+
1326+
code {
1327+
let who = 0x0000000000000000000000000000000000000000;
1328+
${body}
1329+
}`;
1330+
1331+
const cases: Record<string, [string, bigint]> = {
1332+
"a field in the first slot": [
1333+
`let p = players[who];
1334+
out = p.score;`,
1335+
10n,
1336+
],
1337+
"a signed field": [
1338+
`let p = players[who];
1339+
if (p.combo == (0 as int32) - 3) { out = 1; }`,
1340+
1n,
1341+
],
1342+
"a bool field": [
1343+
`let p = players[who];
1344+
if (p.active) { out = 1; }`,
1345+
1n,
1346+
],
1347+
"a field in the second slot": [
1348+
`let p = players[who];
1349+
out = p.total;`,
1350+
99n,
1351+
],
1352+
"a nested struct's fields": [
1353+
`let p = players[who];
1354+
out = (p.inner.a as uint256) * 1000 + p.inner.b;`,
1355+
7008n,
1356+
],
1357+
"a storage struct variable": [
1358+
`let p = one;
1359+
out = p.score + p.total;`,
1360+
41n,
1361+
],
1362+
"a write to the copy, which storage does not see": [
1363+
`let p = players[who];
1364+
p.score = 50;
1365+
out = p.score * 1000 + players[who].score;`,
1366+
50010n,
1367+
],
1368+
"a write to storage, which the copy does not see": [
1369+
`let p = players[who];
1370+
players[who].score = 50;
1371+
out = p.score * 1000 + players[who].score;`,
1372+
10050n,
1373+
],
1374+
};
1375+
1376+
for (const [name, [body, expected]] of Object.entries(cases)) {
1377+
for (const level of [0, 1, 2, 3] as const) {
1378+
it(`should copy ${name} (level ${level})`, async () => {
1379+
const result = await executeProgram(program(body), {
1380+
calldata: "",
1381+
optimizationLevel: level,
1382+
});
1383+
1384+
expect(result.callSuccess).toBe(true);
1385+
expect(await result.getStorage(1n)).toBe(expected);
1386+
});
1387+
}
1388+
}
1389+
});
1390+
12921391
describe("modulo", () => {
12931392
const program = (expr: string) => `name Modulo;
12941393

‎packages/bugc/src/irgen/generate/expressions/identifier.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { Severity } from "#result";
55
import { Error as IrgenError, ErrorMessages } from "#irgen/errors";
66
import { fromBugType } from "#irgen/type";
77

8+
import { Type } from "#types";
9+
810
import { Process } from "../process.js";
11+
import { emitStorageStructCopy } from "../storage.js";
912

1013
/**
1114
* Build an identifier expression
@@ -30,6 +33,15 @@ export function* buildIdentifier(
3033
? fromBugType(storageType)
3134
: Ir.Type.Scalar.uint256;
3235

36+
// A struct is a copy in memory, not the slot's word
37+
if (storageType && Type.isStruct(storageType)) {
38+
return yield* emitStorageStructCopy(
39+
Ir.Value.constant(BigInt(storageSlot.slot), Ir.Type.Scalar.uint256),
40+
storageType,
41+
expr,
42+
);
43+
}
44+
3345
// Build storage load using new unified read instruction
3446
const tempId = yield* Process.Variables.newTemp();
3547
yield* Process.Instructions.emit({

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,16 @@ function* assignToTarget(node: Ast.Expression, value: Ir.Value): Process<void> {
173173
const offsetTemp = yield* Process.Variables.newTemp();
174174
// Calculate field offset - assuming 32 bytes per field for now
175175
const fieldOffset = fieldIndex * 32;
176-
yield* Process.Instructions.emit({
177-
kind: "compute_offset",
178-
location: "memory",
179-
base: object,
180-
field: fieldName,
181-
fieldOffset,
182-
dest: offsetTemp,
183-
operationDebug: yield* Process.Debug.forAstNode(accessNode),
184-
} as Ir.Instruction.ComputeOffset);
176+
yield* Process.Instructions.emit(
177+
Ir.Instruction.ComputeOffset.field(
178+
"memory",
179+
object,
180+
fieldName,
181+
fieldOffset,
182+
offsetTemp,
183+
yield* Process.Debug.forAstNode(accessNode),
184+
),
185+
);
185186

186187
// Then write to that offset
187188
yield* Process.Instructions.emit({

‎packages/bugc/src/irgen/generate/storage.ts‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ export function* emitStorageChainLoad(
271271
}
272272
}
273273

274+
// A struct is a copy in memory, not the slot's word
275+
if (currentOrigin && Type.isStruct(currentOrigin)) {
276+
return yield* emitStorageStructCopy(currentSlot, currentOrigin, node);
277+
}
278+
274279
// Check if the last access was a struct field to get packed field info
275280
let byteOffset = 0;
276281
let fieldSize = 32; // Default to full slot
@@ -305,6 +310,117 @@ export function* emitStorageChainLoad(
305310
return Ir.Value.temp(loadTempId, valueType);
306311
}
307312

313+
/**
314+
* Copy a struct from storage, starting at `slot`, into new memory, and
315+
* return its address. A struct in memory has one word per field, in
316+
* order; a struct field is the address of its own copy.
317+
*/
318+
export function* emitStorageStructCopy(
319+
slot: Ir.Value,
320+
struct: Type.Struct,
321+
node: Ast.Node | undefined,
322+
): Process<Ir.Value> {
323+
const debug = node ? yield* Process.Debug.forAstNode(node) : {};
324+
const uint256 = Ir.Type.Scalar.uint256;
325+
const constant = (value: bigint) => Ir.Value.constant(value, uint256);
326+
327+
const address = yield* Process.Variables.newTemp();
328+
yield* Process.Instructions.emit({
329+
kind: "allocate",
330+
location: "memory",
331+
size: constant(BigInt(struct.fields.size * 32)),
332+
dest: address,
333+
operationDebug: debug,
334+
} as Ir.Instruction);
335+
336+
let index = 0;
337+
for (const [name, fieldType] of struct.fields) {
338+
const layout = struct.layout.get(name);
339+
if (!layout) {
340+
throw new Error(`Field ${name} not found in struct ${struct.name}`);
341+
}
342+
343+
// The field's slot
344+
let fieldSlot = slot;
345+
if (layout.byteOffset >= 32) {
346+
const temp = yield* Process.Variables.newTemp();
347+
yield* Process.Instructions.emit({
348+
kind: "compute_slot",
349+
slotKind: "field",
350+
base: slot,
351+
fieldOffset: layout.byteOffset,
352+
dest: temp,
353+
operationDebug: debug,
354+
} as Ir.Instruction.ComputeSlot);
355+
fieldSlot = Ir.Value.temp(temp, uint256);
356+
}
357+
358+
let value: Ir.Value;
359+
if (Type.isStruct(fieldType)) {
360+
value = yield* emitStorageStructCopy(fieldSlot, fieldType, node);
361+
} else if (
362+
Type.isElementary(fieldType) &&
363+
!(Type.Elementary.isBytes(fieldType) && fieldType.size === undefined) &&
364+
!Type.Elementary.isString(fieldType)
365+
) {
366+
const irType = fromBugType(fieldType);
367+
const temp = yield* Process.Variables.newTemp();
368+
yield* Process.Instructions.emit({
369+
kind: "read",
370+
location: "storage",
371+
slot: fieldSlot,
372+
offset: constant(BigInt(layout.byteOffset % 32)),
373+
length: constant(BigInt(getFieldSize(irType))),
374+
type: irType,
375+
dest: temp,
376+
operationDebug: debug,
377+
} as Ir.Instruction.Read);
378+
value = Ir.Value.temp(temp, irType);
379+
} else {
380+
yield* Process.Errors.report(
381+
new IrgenError(
382+
`Cannot copy struct ${struct.name} from storage: its field ` +
383+
`${name} is a ${Type.format(fieldType)}, and only value ` +
384+
`and struct fields can be copied to memory`,
385+
node?.loc ?? undefined,
386+
Severity.Error,
387+
),
388+
);
389+
value = constant(0n);
390+
}
391+
392+
// The field's word in memory
393+
let offset = Ir.Value.temp(address, uint256);
394+
if (index > 0) {
395+
const temp = yield* Process.Variables.newTemp();
396+
yield* Process.Instructions.emit(
397+
Ir.Instruction.ComputeOffset.field(
398+
"memory",
399+
offset,
400+
name,
401+
index * 32,
402+
temp,
403+
debug,
404+
),
405+
);
406+
offset = Ir.Value.temp(temp, uint256);
407+
}
408+
409+
yield* Process.Instructions.emit({
410+
kind: "write",
411+
location: "memory",
412+
offset,
413+
length: constant(32n),
414+
value,
415+
operationDebug: debug,
416+
} as Ir.Instruction.Write);
417+
418+
index++;
419+
}
420+
421+
return Ir.Value.temp(address, Ir.Type.Ref.memory());
422+
}
423+
308424
/**
309425
* Emit a storage write for an access chain
310426
*/

‎packages/bugc/src/irgen/generator.error.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,35 @@ describe("generateModule error handling", () => {
107107
);
108108
expect(defaultingWarnings).toHaveLength(0);
109109
});
110+
it("should reject a struct copy from storage with an array field", async () => {
111+
const source = `
112+
name Test;
113+
define {
114+
struct Bag {
115+
count: uint256;
116+
items: array<uint256>;
117+
};
118+
}
119+
storage {
120+
[0] bag: Bag;
121+
[1] out: uint256;
122+
}
123+
code {
124+
let b = bag;
125+
out = b.count;
126+
}
127+
`;
128+
129+
const result = await compileTest(source);
130+
131+
expect(result.success).toBe(false);
132+
const messages = Result.findMessages(result, {
133+
severity: Severity.Error,
134+
}).map((d) => d.message);
135+
expect(messages).toContainEqual(
136+
expect.stringContaining(
137+
"Cannot copy struct Bag from storage: its field items",
138+
),
139+
);
140+
});
110141
});

0 commit comments

Comments
 (0)