Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ support. Changes to the specification itself are tracked in the root

## Unreleased

### Added

- A struct copied from storage to memory may have `string` and `bytes`
fields: each field's word holds the address of its own copy. A struct
in memory assigned to storage (`players[msg.sender] = player;`) is
copied field by field, each to its bytes in its slot, and a `string`
or `bytes` field as Solidity encodes it, clearing the data words the
old value used past the new one's ([#377]).
- A struct local's pointer is a group of its fields: each field's bytes
in its word in memory and, for a `string` or `bytes` field, the
field's word, then the length and data it refers to. Regions are
named after the local and the field (`player-score`, `player-name`,
`player-name-length`, `player-name-data`) ([#377]).

### Changed

- A struct member of a struct in storage now takes all the slots its
struct needs, as in Solidity, and the member after it starts at the
next free slot. Before, it took one slot, so the members after a
struct of two or more slots shared slots with it. This moves those
members, and data stored by a contract that an earlier bugc compiled
is not where the new build reads it. For example, with
`struct Inner { a: uint256; b: uint8; }` and
`struct Outer { x: uint8; inner: Inner; y: uint16; }`, `y` is now in
slot 3 of an `Outer`, after `inner` in slots 1 and 2. Before, `y` was
in slot 2 ([#377]).

### Fixed

- Reading a `string` or `bytes` from storage as a whole, as in
`let m: string = motd;`, now copies it to memory, decoded from its
storage encoding. This also corrects an index (`data[3]`), a slice
(`data[2:40]`), `.length` of a struct member (`p.s.length`), and
`keccak256(data)` of a `string` or `bytes` in storage. Before, the
value was the slot's word, used as if it were a memory address, so
these reverted or read the wrong bytes ([#377]).
- Assigning a struct in memory to storage now writes each of its
fields. Before, it wrote the struct's memory address to the first
slot ([#377]).
- Copying an array to or from storage, which bugc does not support, is
now an error: an array read from storage as a whole, as in
`let m: array<uint256> = items;`, an array in memory assigned to a
struct member or mapping value in storage, as in
`bag.items = [1, 2];`, and a struct with an array field copied either
way. Before, these compiled to a copy of the slot's word, or wrote
the memory address to the slot. An array literal assigned to a storage array variable
(`items = [1, 2];`) still compiles ([#377]).

## 0.1.0-preview.2 — 2026-10-07

### Added
Expand Down Expand Up @@ -460,3 +508,4 @@ First publication.
[#369]: https://github.com/ethdebug/format/pull/369
[#370]: https://github.com/ethdebug/format/pull/370
[#371]: https://github.com/ethdebug/format/pull/371
[#377]: https://github.com/ethdebug/format/pull/377
8 changes: 4 additions & 4 deletions packages/bugc/examples/advanced/nested-structs.bug
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name NestedStructsStorage;
// Example 9: Nested Structs in Storage
//
// Expected IR:
// // CEO salary is at slot 4 (base slot 2 + struct layout)
// write location="storage", slot=4, offset=0, length=32, value=1000000
// // CEO salary is at slot 5 (base slot 2 + struct layout)
// write location="storage", slot=5, offset=0, length=32, value=1000000
//
// // CEO address is at slot 3, first 20 bytes
// %ceo_addr = read location="storage", slot=3, offset=0, length=20
// // CEO address is at slot 4, first 20 bytes
// %ceo_addr = read location="storage", slot=4, offset=0, length=20
//
// EVM Strategy: Calculate nested struct slot offsets based on layout
//
Expand Down
59 changes: 59 additions & 0 deletions packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,24 @@ code { let v = calls + 5; total = outer(v); total = total + v; }`;

const words: Shape = { kind: "array", element: { kind: "scalar", size: 32 } };

/** The layout of the struct programs' `Player` local */
const player: Shape = {
kind: "struct",
fields: {
score: { kind: "scalar", size: 8 },
plays: { kind: "scalar", size: 4 },
name: { kind: "bytes" },
tag: { kind: "bytes" },
},
};
const longName = "a name longer than thirty-two bytes, in two words";
const playerValue = (plays: bigint, name: string) => ({
score: 100n,
plays,
name: textBytes(name),
tag: textBytes("hi"),
});

/** A scalar program for each sub-word kind, `ad` passed as `arg` */
const kinds = (
[
Expand Down Expand Up @@ -1118,6 +1136,47 @@ code {
},
},
},
{
// A struct copied from storage, changed, and written back: its
// pointer is a group of its fields' words, a string field's with
// the string's length and data
name: "a struct copied from storage",
source: `name StructLocal;
define {
struct Player { score: uint64; plays: uint32; name: string; tag: string; };
}
storage { [0] r: uint256; [1] players: mapping<address, Player>; }
create {
players[msg.sender].score = 100;
players[msg.sender].plays = 7;
players[msg.sender].name = "${longName}";
players[msg.sender].tag = "hi";
}
code {
let player: Player = players[msg.sender];
player.plays = player.plays + 1;
player.name = "renamed";
players[msg.sender] = player;
r = player.score + player.plays;
}`,
locals: {
player: {
shape: player,
everyLevel: true,
values: [
playerValue(7n, longName),
{
value: playerValue(8n, longName),
after: "player.plays = player.plays + 1",
},
{
value: playerValue(8n, "renamed"),
after: 'player.name = "renamed"',
},
],
},
},
},
{
name: "a uint8 cast",
source: `name U8;
Expand Down
44 changes: 41 additions & 3 deletions packages/bugc/src/evmgen/debug/local-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,15 @@ function stackPointer(
* word; a reference element's word holds the address of its data,
* described the same way.
*
* - struct: one word per field, in order, from `base`. A scalar field
* is right-aligned in its word; a reference field's word holds the
* address of its data, described the same way.
*
* Regions are named after the local: `x-length`, `x-data`,
* `x-element`, and for a nested reference `x-element-length` and so
* on. Fixed-size arrays and structs are left out: bugc cannot build
* one in memory yet (no struct literal; array literals are dynamic).
* `x-element`, `x-<field>`, and for a nested reference
* `x-element-length`, `x-<field>-data` and so on. Fixed-size arrays
* are left out: bugc cannot build one in memory yet (array literals
* are dynamic).
*/
function dataPointers(
base: Format.Pointer.Expression,
Expand Down Expand Up @@ -157,6 +162,39 @@ function dataPointers(
];
}

if (BugType.isStruct(type)) {
const fields: Format.Pointer[] = [];
let index = 0;
for (const [field, fieldType] of type.fields) {
const region = `${name}-${field}`;
const word = 32 * index++;
const offset = (within: number): Format.Pointer.Expression =>
word + within === 0 ? base : { "~sum": [base, word + within] };
const irType = fromBugType(fieldType);
if (Ir.Type.isScalar(irType)) {
fields.push({
name: region,
location: "memory",
offset: offset(32 - irType.size),
length: irType.size,
});
continue;
}
const nested = dataPointers(
{ "~read": region },
fieldType,
region,
depth,
);
if (!nested) return undefined;
fields.push(
{ name: region, location: "memory", offset: offset(0), length: 32 },
...nested,
);
}
return fields;
}

if (!BugType.isArray(type) || type.size !== undefined) return undefined;

const index = `i${depth}`;
Expand Down
Loading
Loading