Skip to content

Commit 1d45fea

Browse files
authored
bugc: combine thirteen fixes and additions (#368)
* bugc: compile `!` as a logical not * bugc: link the changelog entry to #353 * bugc: end each source range at its last token * bugc: link the changelog entry to #362 * bugc: add block.prevrandao * bugc: link the changelog entries to #363 * bugc: hash several value-type words with keccak256 * bugc: link the changelog entries to #365 * bugc: slice a fixed-size bytes value by its bytes * bugc: link the changelog entry to #357 * bugc: hash the data of dynamic bytes and strings * bugc: link the changelog entry to #360 * bugc: give a caller's call JUMP the invoke's arguments * bugc: list variables before a function's first statement * bugc: store a memory string's bytes when assigning it to storage A storage write took its value as a word, so a memory string or bytes reference wrote its memory address into the slot. Code generation now encodes the referenced bytes as Solidity does: inline with length * 2 up to 31 bytes, else length * 2 + 1 in the slot and the data from keccak256(slot), with the last word's trailing bytes cleared. A slice now carries the bytes type, so its value is recognized too. * bugc: link the changelog entry to #355 * bugc: say why the bytes test slices without naming the copy width * 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. * bugc: link the changelog entries to #359 * bugc: copy a struct read from storage into memory * bugc: link the changelog entries to #361 * bugc: lay storage arrays out as Solidity does * bugc: add push to dynamic arrays in storage
1 parent 46585fd commit 1d45fea

62 files changed

Lines changed: 3121 additions & 602 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/bugc-react/CHANGELOG.md‎

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

88
## Unreleased
99

10+
### Added
11+
12+
- The CFG view shows a `block.prevrandao` read as `block.prevrandao`
13+
([#363]).
14+
- The IR view shows bugc's new `copy` instruction ([#359]).
15+
16+
### Changed
17+
18+
- The IR and CFG views show each operand of a `hash` instruction, which
19+
now has a list of `values` ([#365]).
20+
1021
## 0.1.0-preview.1 — 2026-10-03
1122

1223
Updated `@ethdebug/bugc` to `0.1.0-preview.1`.
@@ -34,3 +45,6 @@ First publication.
3445

3546
[#299]: https://github.com/ethdebug/format/pull/299
3647
[#300]: https://github.com/ethdebug/format/pull/300
48+
[#359]: https://github.com/ethdebug/format/pull/359
49+
[#363]: https://github.com/ethdebug/format/pull/363
50+
[#365]: https://github.com/ethdebug/format/pull/365

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,14 @@ function CfgViewContent({ ir }: CfgViewProps): JSX.Element {
349349
return `${envInst.dest} = block.timestamp`;
350350
case "block_number":
351351
return `${envInst.dest} = block.number`;
352+
case "block_prevrandao":
353+
return `${envInst.dest} = block.prevrandao`;
352354
default:
353355
return `${envInst.dest} = ${envInst.op}`;
354356
}
355357
}
356358
case "hash":
357-
return `${inst.dest} = keccak256(${formatValue(inst.value)})`;
359+
return `${inst.dest} = keccak256(${inst.values.map(formatValue).join(", ")})`;
358360
case "cast":
359361
return `${inst.dest} = cast ${formatValue(inst.value)} to ${inst.targetType.kind}`;
360362
case "compute_slot": {

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ function InstructionRenderer({
160160

161161
case "hash":
162162
add(`${formatDest(instruction.dest)} = hash `);
163-
addOperand("value", formatValue(instruction.value));
163+
instruction.values.forEach((value, index) => {
164+
if (index > 0) add(", ");
165+
addOperand("value", formatValue(value));
166+
});
164167
break;
165168

166169
case "cast":
@@ -336,6 +339,16 @@ function InstructionRenderer({
336339
break;
337340
}
338341

342+
case "copy":
343+
add("memory[offset: ");
344+
addOperand("offset", formatValue(instruction.offset));
345+
add(", length: ");
346+
addOperand("length", formatValue(instruction.length));
347+
add(`] = copy ${instruction.location}[`);
348+
addOperand("source", formatValue(instruction.source));
349+
add("]");
350+
break;
351+
339352
default:
340353
add(`; unknown instruction: ${(instruction as { kind?: string }).kind}`);
341354
}

‎packages/bugc-react/src/utils/irDebugUtils.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ export function extractInstructionDebug(
9090
operands.push({ label: "operand", debug: instruction.operandDebug });
9191
break;
9292

93-
case "hash":
94-
operands.push({ label: "value", debug: instruction.valueDebug });
95-
break;
96-
9793
case "cast":
9894
operands.push({ label: "value", debug: instruction.valueDebug });
9995
break;

‎packages/bugc/CHANGELOG.md‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ support. Changes to the specification itself are tracked in the root
99

1010
### Added
1111

12+
- `block.prevrandao`, the previous block's RANDAO value (EIP-4399), as a
13+
`uint256`. It compiles to the EVM's `PREVRANDAO` (`0x44`) ([#363]).
14+
- `keccak256` over value types: `keccak256(a, b, ...)` hashes the 32-byte
15+
words of one or more integers, `address`, `bool` or `bytesN` values, in
16+
order. For integers, `address` and `bool` this equals Solidity's
17+
`keccak256(abi.encode(a, b, ...))`. A `bytesN` narrower than 32 bytes
18+
hashes its word with its bytes at the right end, unlike `abi.encode`;
19+
cast it to `bytes32` to match Solidity. A single dynamic `bytes` or
20+
`string` argument still hashes its data, and must be the only argument.
21+
The optimizer folds a hash of constant words at levels 1 to 3 ([#365]).
22+
- `push` on a dynamic array in storage, as in Solidity: `a.push(v)`
23+
writes `v` at index `a.length` and adds one to the length, and
24+
`a.push()` adds a zero element, as for an array of arrays. `a` may be
25+
in a mapping or a struct. A push has no value. On an array in memory
26+
or a fixed-size array, it is a type error ([#366]).
1227
- The `%` operator, with the precedence of `*` and `/`. It compiles to
1328
the EVM's `MOD` (`SMOD` for signed operands), so `x % 0` is `0`, as
1429
`x / 0` is ([#321]).
@@ -28,6 +43,24 @@ support. Changes to the specification itself are tracked in the root
2843

2944
### Changed
3045

46+
- The IR `hash` instruction now has `values`, a list, in place of
47+
`value` and `valueDebug` ([#365]).
48+
- An index into a dynamic array in storage now reverts with Solidity's
49+
`Panic(0x32)` unless it is less than the array's length, for reads and
50+
writes, as an index into an array in memory does. Before, a write past
51+
the end wrote there and left the length as it was, so the array's
52+
pointer did not show the element ([#366]).
53+
- Storage arrays now have Solidity's layout. A fixed-size array is
54+
inline: its elements start at its own slot, so `[0] a: array<uint256,
55+
3>` takes slots 0 to 2, and a struct member that is a fixed-size array
56+
takes all its slots. A dynamic array still keeps its length in its
57+
slot and its elements from `keccak256(slot)`. In either, elements
58+
narrower than 16 bytes now share a slot, as many as fit, from its
59+
low-order end: an `array<uint8, 40>` takes two slots. Each storage
60+
variable's pointer describes the same layout. Before, every array's
61+
elements started at `keccak256(slot)`, one element to a slot, while
62+
the pointer of a fixed-size array described it inline, and the pointer
63+
of an array of narrow elements described them packed ([#364]).
3164
- An integer literal operand of an arithmetic or comparison operator now
3265
takes the type of the other operand when its value fits, so with
3366
`x: int8`, `x < 0`, `x == 1` and `-1 < x` compare as `int8`. A literal
@@ -79,6 +112,8 @@ support. Changes to the specification itself are tracked in the root
79112

80113
### Fixed
81114

115+
- `a.length` on a dynamic array in storage now reads the length in the
116+
array's slot. Before, it did not read that slot ([#366]).
82117
- At optimization levels 2 and 3, an inlined body's control flow now has
83118
a `transform: ["inline"]` context: the branch of an `if`, the jumps
84119
between the body's blocks, each block's `JUMPDEST`, and the jump that
@@ -89,6 +124,61 @@ support. Changes to the specification itself are tracked in the root
89124
has the branch's context: its source range, its variables (the storage
90125
variables too) and its transforms. Before, it had no context, so a
91126
debugger listed no variables while it ran ([#356]).
127+
- `!` is now a logical not: `!true` is `false`. Before, at every
128+
optimization level, it compiled to the EVM's bitwise `NOT`, so `!x` was
129+
a non-zero word, and so `true`, for every `x` ([#353]).
130+
- A source range now ends at its node's last token. Before, a node
131+
followed by whitespace or a comment included them, so most `code`
132+
ranges had trailing whitespace: `block.number ` in `block.number + 1`,
133+
the target `x ` in `x = 1`, a statement up to the next statement, and
134+
the `create` and `code` blocks up to the next block ([#362]).
135+
- A slice of a fixed-size `bytesN` value, as in
136+
`keccak256("transfer(address,uint256)")[0:4]`, now gives those bytes as
137+
dynamic `bytes`. Before, at every optimization level, the compiler read
138+
the value as the memory address of dynamic `bytes`, so the slice
139+
reverted with `Panic(0x32)` or held other memory ([#357]).
140+
- `keccak256` of dynamic `bytes` or a `string` now hashes its data, and
141+
`keccak256(msg.data)` hashes the calldata. Before, code generation
142+
hashed the word that refers to the data (its memory address, or 0 for
143+
`msg.data`). So at optimization level 0, `keccak256("transfer()")` was
144+
wrong, while levels 1 to 3 folded it to the right constant; at every
145+
level, the hash of a slice or of `msg.data` was wrong ([#360]).
146+
- The `invoke` context on a caller's `JUMP` into a function now has the
147+
call's `arguments`, as the callee's entry `JUMPDEST` does: the `JUMP`
148+
leaves them on the stack, and the `JUMPDEST` does not change it.
149+
Before, only the `JUMPDEST`'s `invoke` had them, so a debugger that
150+
opens the frame on the step after the `JUMP` showed it for one step
151+
without its arguments ([#354]).
152+
- The instructions that run before a function's first statement now have
153+
a `variables` context: the memory setup at the start of the code (and
154+
`create`) blocks, and a function's entry `JUMPDEST` and prologue. They
155+
list the storage variables and the locals in scope, a local without a
156+
pointer: a parameter is not yet in its home there, and the `invoke`'s
157+
`arguments` point at it on the stack. Before, they listed no variables,
158+
so a debugger showed none for those steps ([#358]).
159+
- Assigning a memory `string` or `bytes` to storage, as in `s = m;`, now
160+
stores its bytes as Solidity does: up to 31 bytes in the slot, with
161+
twice the length in the low byte; else twice the length plus one in the
162+
slot, and the data from `keccak256(slot)`. A slice's bytes past its
163+
length are stored as zero. Before, the slot held the value's memory
164+
address ([#355]).
165+
- A slice now copies all its bytes, as in `msg.data[4:]` or `m[1:40]`.
166+
The IR has a `copy` instruction, which compiles to `CALLDATACOPY` from
167+
calldata and `MCOPY` from memory. Before, a slice copied one word, so a
168+
slice longer than 32 bytes held zeros after its first 32 ([#359]).
169+
- A struct read from storage into a local, as in
170+
`let p = players[who];` or `let s = stored;`, is now a copy in memory,
171+
as the examples' README describes: reading `p.score` reads the copy,
172+
and a write to `p.score` changes the copy, not storage. A field that is
173+
a struct is copied too; a struct with an array, mapping, `string` or
174+
dynamic `bytes` field is an error. Before, at every optimization level,
175+
the local held the struct's first storage word as if it were a memory
176+
address, and a write to one of its fields failed code generation
177+
([#361]).
178+
- A value whose only later uses are as the address, slot or value of a
179+
read or write, or as the base of an offset, now gets a home in memory
180+
when code generation cannot keep it on the stack. Before, such a value
181+
could fail code generation with "Cannot load value" ([#361]).
92182
- A call to a function with no return type now compiles as a statement,
93183
as in `bump();`, at every optimization level. The function's `return`
94184
context has no `data`. Before, IR generation failed with "Cannot convert
@@ -300,4 +390,17 @@ First publication.
300390
[#349]: https://github.com/ethdebug/format/pull/349
301391
[#351]: https://github.com/ethdebug/format/pull/351
302392
[#352]: https://github.com/ethdebug/format/pull/352
393+
[#353]: https://github.com/ethdebug/format/pull/353
394+
[#354]: https://github.com/ethdebug/format/pull/354
395+
[#355]: https://github.com/ethdebug/format/pull/355
303396
[#356]: https://github.com/ethdebug/format/pull/356
397+
[#357]: https://github.com/ethdebug/format/pull/357
398+
[#358]: https://github.com/ethdebug/format/pull/358
399+
[#359]: https://github.com/ethdebug/format/pull/359
400+
[#360]: https://github.com/ethdebug/format/pull/360
401+
[#361]: https://github.com/ethdebug/format/pull/361
402+
[#362]: https://github.com/ethdebug/format/pull/362
403+
[#363]: https://github.com/ethdebug/format/pull/363
404+
[#364]: https://github.com/ethdebug/format/pull/364
405+
[#365]: https://github.com/ethdebug/format/pull/365
406+
[#366]: https://github.com/ethdebug/format/pull/366

‎packages/bugc/examples/README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,28 @@ account.isActive = true;
143143
accounts[user].balance = accounts[user].balance + 100;
144144
accounts[user].isActive = true;
145145
```
146+
147+
## Hashing
148+
149+
`keccak256` has two forms. The compiler tells them apart by the type of
150+
the argument:
151+
152+
- **One dynamic `bytes` or `string`** hashes its data:
153+
`keccak256("transfer(address,uint256)")`.
154+
- **One or more value types** (integers, `address`, `bool`, `bytesN`)
155+
hashes their 32-byte words, concatenated in order. This equals
156+
Solidity's `keccak256(abi.encode(a, b, ...))` for integers, `address`
157+
and `bool`:
158+
159+
```bug
160+
// a toy roll: 2 in 3 is a hit
161+
let hit = (keccak256(block.number, msg.sender) as uint256) % 3 != 0;
162+
```
163+
164+
A `bytes` or `string` argument must be the only one.
165+
166+
A `bytesN` narrower than 32 bytes hashes its full word, with its bytes
167+
at the low-order (right) end, as BUG holds it. Solidity's `abi.encode`
168+
puts a `bytesN` value's bytes at the high-order (left) end, so for
169+
`bytes4` and the like the hashes differ from Solidity's. Cast to
170+
`bytes32` first (which puts the bytes at the left end) to match it.

‎packages/bugc/examples/advanced/nested-arrays.bug‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name NestedArrays;
33
// Example 7: Nested Arrays
44
//
55
// Expected IR:
6-
// // First dimension: matrix[i]
6+
// // First dimension: matrix[i], checked against matrix's length
77
// %outer_base = compute_slot kind="array", base=15
88
// %inner_array_slot = add %outer_base, %i
99
//
@@ -23,8 +23,15 @@ storage {
2323
}
2424

2525
code {
26-
let i = 2;
27-
let j = 5;
26+
// An index must be below the length, so push the rows and elements
27+
// first: matrix[1] gets two elements
28+
matrix.push();
29+
matrix.push();
30+
matrix[1].push(0);
31+
matrix[1].push(0);
32+
33+
let i = 1;
34+
let j = 1;
2835
let value = 999;
2936
matrix[i][j] = value;
3037
let elem = matrix[i][j];

‎packages/bugc/examples/intermediate/arrays.bug‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ name ArraysAndLoops;
22

33
storage {
44
[0] numbers: array<uint256, 10>;
5-
[1] sum: uint256;
6-
[2] max: uint256;
7-
[3] count: uint256;
8-
[4] found: bool;
9-
[5] searchValue: uint256;
5+
[10] sum: uint256;
6+
[11] max: uint256;
7+
[12] count: uint256;
8+
[13] found: bool;
9+
[14] searchValue: uint256;
1010
}
1111

1212
code {
@@ -21,7 +21,7 @@ code {
2121
after: deploy
2222
variables:
2323
sum:
24-
pointer: { location: storage, slot: 1 }
24+
pointer: { location: storage, slot: 10 }
2525
value: 0
2626
*/
2727
for (let i = 0; i < 10; i = i + 1) {
@@ -78,21 +78,21 @@ code {
7878
fails: stack overflow during call - needs investigation
7979
variables:
8080
sum:
81-
pointer: { location: storage, slot: 1 }
81+
pointer: { location: storage, slot: 10 }
8282
value: 285
8383
*/
8484
/*@test final-max
8585
fails: stack overflow during call - needs investigation
8686
variables:
8787
max:
88-
pointer: { location: storage, slot: 2 }
88+
pointer: { location: storage, slot: 11 }
8989
value: 81
9090
*/
9191
/*@test final-count
9292
fails: stack overflow during call - needs investigation
9393
variables:
9494
count:
95-
pointer: { location: storage, slot: 3 }
95+
pointer: { location: storage, slot: 12 }
9696
value: 8
9797
*/
9898

‎packages/bugc/src/ast/spec.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1439,7 +1439,8 @@ export namespace Expression {
14391439
| Expression.Special.MsgValue
14401440
| Expression.Special.MsgData
14411441
| Expression.Special.BlockTimestamp
1442-
| Expression.Special.BlockNumber;
1442+
| Expression.Special.BlockNumber
1443+
| Expression.Special.BlockPrevrandao;
14431444

14441445
export const isSpecial = (
14451446
expression: Expression.Base,
@@ -1451,6 +1452,7 @@ export namespace Expression {
14511452
Expression.Special.isMsgSender,
14521453
Expression.Special.isBlockNumber,
14531454
Expression.Special.isBlockTimestamp,
1455+
Expression.Special.isBlockPrevrandao,
14541456
].some((guard) => guard(expression));
14551457

14561458
export namespace Special {
@@ -1532,6 +1534,26 @@ export namespace Expression {
15321534
export function blockNumber(id: Id, loc?: SourceLocation): BlockNumber {
15331535
return { id, kind: "expression:special:block.number", loc: loc ?? null };
15341536
}
1537+
1538+
export interface BlockPrevrandao extends Base {
1539+
kind: "expression:special:block.prevrandao";
1540+
}
1541+
1542+
export const isBlockPrevrandao = (
1543+
expression: Expression.Base,
1544+
): expression is BlockPrevrandao =>
1545+
expression.kind === "expression:special:block.prevrandao";
1546+
1547+
export function blockPrevrandao(
1548+
id: Id,
1549+
loc?: SourceLocation,
1550+
): BlockPrevrandao {
1551+
return {
1552+
id,
1553+
kind: "expression:special:block.prevrandao",
1554+
loc: loc ?? null,
1555+
};
1556+
}
15351557
}
15361558
}
15371559

‎packages/bugc/src/ast/visitor.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export function visitDetailed<N extends Ast.Node, T, C = never>(
201201
case "expression:special:msg.data":
202202
case "expression:special:block.timestamp":
203203
case "expression:special:block.number":
204+
case "expression:special:block.prevrandao":
204205
return visitor.expressionSpecial(node as Ast.Expression.Special, context);
205206

206207
default:

0 commit comments

Comments
 (0)