Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ed2eb9b
bugc: compile `!` as a logical not
gnidan Oct 7, 2026
a02f0a5
bugc: link the changelog entry to #353
gnidan Oct 7, 2026
7ecc295
bugc: end each source range at its last token
gnidan Oct 7, 2026
e2eaa55
bugc: link the changelog entry to #362
gnidan Oct 7, 2026
095bc0b
bugc: add block.prevrandao
gnidan Oct 7, 2026
f056c3c
bugc: link the changelog entries to #363
gnidan Oct 7, 2026
b748500
bugc: hash several value-type words with keccak256
gnidan Oct 7, 2026
316db96
bugc: link the changelog entries to #365
gnidan Oct 7, 2026
b8fbef0
bugc: slice a fixed-size bytes value by its bytes
gnidan Oct 7, 2026
8df3007
bugc: link the changelog entry to #357
gnidan Oct 7, 2026
eb17761
bugc: hash the data of dynamic bytes and strings
gnidan Oct 7, 2026
b2c2631
bugc: link the changelog entry to #360
gnidan Oct 7, 2026
b8f84a2
bugc: give a caller's call JUMP the invoke's arguments
gnidan Oct 7, 2026
bfe04d4
bugc: list variables before a function's first statement
gnidan Oct 7, 2026
f3018cd
bugc: store a memory string's bytes when assigning it to storage
gnidan Oct 7, 2026
ad29e59
bugc: link the changelog entry to #355
gnidan Oct 7, 2026
154ee51
bugc: say why the bytes test slices without naming the copy width
gnidan Oct 7, 2026
40ea7a5
bugc: copy all of a slice's bytes
gnidan Oct 7, 2026
691f5fd
bugc: link the changelog entries to #359
gnidan Oct 7, 2026
63fa54e
bugc: copy a struct read from storage into memory
gnidan Oct 7, 2026
781740e
bugc: link the changelog entries to #361
gnidan Oct 7, 2026
61acac4
bugc: lay storage arrays out as Solidity does
gnidan Oct 7, 2026
0880a4f
bugc: add push to dynamic arrays in storage
gnidan Oct 7, 2026
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
14 changes: 14 additions & 0 deletions packages/bugc-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ specification itself are tracked in the root

## Unreleased

### Added

- The CFG view shows a `block.prevrandao` read as `block.prevrandao`
([#363]).
- The IR view shows bugc's new `copy` instruction ([#359]).

### Changed

- The IR and CFG views show each operand of a `hash` instruction, which
now has a list of `values` ([#365]).

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

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

[#299]: https://github.com/ethdebug/format/pull/299
[#300]: https://github.com/ethdebug/format/pull/300
[#359]: https://github.com/ethdebug/format/pull/359
[#363]: https://github.com/ethdebug/format/pull/363
[#365]: https://github.com/ethdebug/format/pull/365
4 changes: 3 additions & 1 deletion packages/bugc-react/src/components/CfgView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,14 @@ function CfgViewContent({ ir }: CfgViewProps): JSX.Element {
return `${envInst.dest} = block.timestamp`;
case "block_number":
return `${envInst.dest} = block.number`;
case "block_prevrandao":
return `${envInst.dest} = block.prevrandao`;
default:
return `${envInst.dest} = ${envInst.op}`;
}
}
case "hash":
return `${inst.dest} = keccak256(${formatValue(inst.value)})`;
return `${inst.dest} = keccak256(${inst.values.map(formatValue).join(", ")})`;
case "cast":
return `${inst.dest} = cast ${formatValue(inst.value)} to ${inst.targetType.kind}`;
case "compute_slot": {
Expand Down
15 changes: 14 additions & 1 deletion packages/bugc-react/src/components/IrView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ function InstructionRenderer({

case "hash":
add(`${formatDest(instruction.dest)} = hash `);
addOperand("value", formatValue(instruction.value));
instruction.values.forEach((value, index) => {
if (index > 0) add(", ");
addOperand("value", formatValue(value));
});
break;

case "cast":
Expand Down Expand Up @@ -336,6 +339,16 @@ function InstructionRenderer({
break;
}

case "copy":
add("memory[offset: ");
addOperand("offset", formatValue(instruction.offset));
add(", length: ");
addOperand("length", formatValue(instruction.length));
add(`] = copy ${instruction.location}[`);
addOperand("source", formatValue(instruction.source));
add("]");
break;

default:
add(`; unknown instruction: ${(instruction as { kind?: string }).kind}`);
}
Expand Down
4 changes: 0 additions & 4 deletions packages/bugc-react/src/utils/irDebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,6 @@ export function extractInstructionDebug(
operands.push({ label: "operand", debug: instruction.operandDebug });
break;

case "hash":
operands.push({ label: "value", debug: instruction.valueDebug });
break;

case "cast":
operands.push({ label: "value", debug: instruction.valueDebug });
break;
Expand Down
103 changes: 103 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ support. Changes to the specification itself are tracked in the root

### Added

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

### Changed

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

### Fixed

- `a.length` on a dynamic array in storage now reads the length in the
array's slot. Before, it did not read that slot ([#366]).
- At optimization levels 2 and 3, an inlined body's control flow now has
a `transform: ["inline"]` context: the branch of an `if`, the jumps
between the body's blocks, each block's `JUMPDEST`, and the jump that
Expand All @@ -89,6 +124,61 @@ support. Changes to the specification itself are tracked in the root
has the branch's context: its source range, its variables (the storage
variables too) and its transforms. Before, it had no context, so a
debugger listed no variables while it ran ([#356]).
- `!` is now a logical not: `!true` is `false`. Before, at every
optimization level, it compiled to the EVM's bitwise `NOT`, so `!x` was
a non-zero word, and so `true`, for every `x` ([#353]).
- A source range now ends at its node's last token. Before, a node
followed by whitespace or a comment included them, so most `code`
ranges had trailing whitespace: `block.number ` in `block.number + 1`,
the target `x ` in `x = 1`, a statement up to the next statement, and
the `create` and `code` blocks up to the next block ([#362]).
- A slice of a fixed-size `bytesN` value, as in
`keccak256("transfer(address,uint256)")[0:4]`, now gives those bytes as
dynamic `bytes`. Before, at every optimization level, the compiler read
the value as the memory address of dynamic `bytes`, so the slice
reverted with `Panic(0x32)` or held other memory ([#357]).
- `keccak256` of dynamic `bytes` or a `string` now hashes its data, and
`keccak256(msg.data)` hashes the calldata. Before, code generation
hashed the word that refers to the data (its memory address, or 0 for
`msg.data`). So at optimization level 0, `keccak256("transfer()")` was
wrong, while levels 1 to 3 folded it to the right constant; at every
level, the hash of a slice or of `msg.data` was wrong ([#360]).
- The `invoke` context on a caller's `JUMP` into a function now has the
call's `arguments`, as the callee's entry `JUMPDEST` does: the `JUMP`
leaves them on the stack, and the `JUMPDEST` does not change it.
Before, only the `JUMPDEST`'s `invoke` had them, so a debugger that
opens the frame on the step after the `JUMP` showed it for one step
without its arguments ([#354]).
- The instructions that run before a function's first statement now have
a `variables` context: the memory setup at the start of the code (and
`create`) blocks, and a function's entry `JUMPDEST` and prologue. They
list the storage variables and the locals in scope, a local without a
pointer: a parameter is not yet in its home there, and the `invoke`'s
`arguments` point at it on the stack. Before, they listed no variables,
so a debugger showed none for those steps ([#358]).
- Assigning a memory `string` or `bytes` to storage, as in `s = m;`, now
stores its bytes as Solidity does: up to 31 bytes in the slot, with
twice the length in the low byte; else twice the length plus one in the
slot, and the data from `keccak256(slot)`. A slice's bytes past its
length are stored as zero. Before, the slot held the value's memory
address ([#355]).
- A slice now copies all its bytes, as in `msg.data[4:]` or `m[1:40]`.
The IR has a `copy` instruction, which compiles to `CALLDATACOPY` from
calldata and `MCOPY` from memory. Before, a slice copied one word, so a
slice longer than 32 bytes held zeros after its first 32 ([#359]).
- A struct read from storage into a local, as in
`let p = players[who];` or `let s = stored;`, is now a copy in memory,
as the examples' README describes: reading `p.score` reads the copy,
and a write to `p.score` changes the copy, not storage. A field that is
a struct is copied too; a struct with an array, mapping, `string` or
dynamic `bytes` field is an error. Before, at every optimization level,
the local held the struct's first storage word as if it were a memory
address, and a write to one of its fields failed code generation
([#361]).
- A value whose only later uses are as the address, slot or value of a
read or write, or as the base of an offset, now gets a home in memory
when code generation cannot keep it on the stack. Before, such a value
could fail code generation with "Cannot load value" ([#361]).
- A call to a function with no return type now compiles as a statement,
as in `bump();`, at every optimization level. The function's `return`
context has no `data`. Before, IR generation failed with "Cannot convert
Expand Down Expand Up @@ -300,4 +390,17 @@ First publication.
[#349]: https://github.com/ethdebug/format/pull/349
[#351]: https://github.com/ethdebug/format/pull/351
[#352]: https://github.com/ethdebug/format/pull/352
[#353]: https://github.com/ethdebug/format/pull/353
[#354]: https://github.com/ethdebug/format/pull/354
[#355]: https://github.com/ethdebug/format/pull/355
[#356]: https://github.com/ethdebug/format/pull/356
[#357]: https://github.com/ethdebug/format/pull/357
[#358]: https://github.com/ethdebug/format/pull/358
[#359]: https://github.com/ethdebug/format/pull/359
[#360]: https://github.com/ethdebug/format/pull/360
[#361]: https://github.com/ethdebug/format/pull/361
[#362]: https://github.com/ethdebug/format/pull/362
[#363]: https://github.com/ethdebug/format/pull/363
[#364]: https://github.com/ethdebug/format/pull/364
[#365]: https://github.com/ethdebug/format/pull/365
[#366]: https://github.com/ethdebug/format/pull/366
25 changes: 25 additions & 0 deletions packages/bugc/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,28 @@ account.isActive = true;
accounts[user].balance = accounts[user].balance + 100;
accounts[user].isActive = true;
```

## Hashing

`keccak256` has two forms. The compiler tells them apart by the type of
the argument:

- **One dynamic `bytes` or `string`** hashes its data:
`keccak256("transfer(address,uint256)")`.
- **One or more value types** (integers, `address`, `bool`, `bytesN`)
hashes their 32-byte words, concatenated in order. This equals
Solidity's `keccak256(abi.encode(a, b, ...))` for integers, `address`
and `bool`:

```bug
// a toy roll: 2 in 3 is a hit
let hit = (keccak256(block.number, msg.sender) as uint256) % 3 != 0;
```

A `bytes` or `string` argument must be the only one.

A `bytesN` narrower than 32 bytes hashes its full word, with its bytes
at the low-order (right) end, as BUG holds it. Solidity's `abi.encode`
puts a `bytesN` value's bytes at the high-order (left) end, so for
`bytes4` and the like the hashes differ from Solidity's. Cast to
`bytes32` first (which puts the bytes at the left end) to match it.
13 changes: 10 additions & 3 deletions packages/bugc/examples/advanced/nested-arrays.bug
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name NestedArrays;
// Example 7: Nested Arrays
//
// Expected IR:
// // First dimension: matrix[i]
// // First dimension: matrix[i], checked against matrix's length
// %outer_base = compute_slot kind="array", base=15
// %inner_array_slot = add %outer_base, %i
//
Expand All @@ -23,8 +23,15 @@ storage {
}

code {
let i = 2;
let j = 5;
// An index must be below the length, so push the rows and elements
// first: matrix[1] gets two elements
matrix.push();
matrix.push();
matrix[1].push(0);
matrix[1].push(0);

let i = 1;
let j = 1;
let value = 999;
matrix[i][j] = value;
let elem = matrix[i][j];
Expand Down
18 changes: 9 additions & 9 deletions packages/bugc/examples/intermediate/arrays.bug
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name ArraysAndLoops;

storage {
[0] numbers: array<uint256, 10>;
[1] sum: uint256;
[2] max: uint256;
[3] count: uint256;
[4] found: bool;
[5] searchValue: uint256;
[10] sum: uint256;
[11] max: uint256;
[12] count: uint256;
[13] found: bool;
[14] searchValue: uint256;
}

code {
Expand All @@ -21,7 +21,7 @@ code {
after: deploy
variables:
sum:
pointer: { location: storage, slot: 1 }
pointer: { location: storage, slot: 10 }
value: 0
*/
for (let i = 0; i < 10; i = i + 1) {
Expand Down Expand Up @@ -78,21 +78,21 @@ code {
fails: stack overflow during call - needs investigation
variables:
sum:
pointer: { location: storage, slot: 1 }
pointer: { location: storage, slot: 10 }
value: 285
*/
/*@test final-max
fails: stack overflow during call - needs investigation
variables:
max:
pointer: { location: storage, slot: 2 }
pointer: { location: storage, slot: 11 }
value: 81
*/
/*@test final-count
fails: stack overflow during call - needs investigation
variables:
count:
pointer: { location: storage, slot: 3 }
pointer: { location: storage, slot: 12 }
value: 8
*/

Expand Down
24 changes: 23 additions & 1 deletion packages/bugc/src/ast/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,8 @@ export namespace Expression {
| Expression.Special.MsgValue
| Expression.Special.MsgData
| Expression.Special.BlockTimestamp
| Expression.Special.BlockNumber;
| Expression.Special.BlockNumber
| Expression.Special.BlockPrevrandao;

export const isSpecial = (
expression: Expression.Base,
Expand All @@ -1451,6 +1452,7 @@ export namespace Expression {
Expression.Special.isMsgSender,
Expression.Special.isBlockNumber,
Expression.Special.isBlockTimestamp,
Expression.Special.isBlockPrevrandao,
].some((guard) => guard(expression));

export namespace Special {
Expand Down Expand Up @@ -1532,6 +1534,26 @@ export namespace Expression {
export function blockNumber(id: Id, loc?: SourceLocation): BlockNumber {
return { id, kind: "expression:special:block.number", loc: loc ?? null };
}

export interface BlockPrevrandao extends Base {
kind: "expression:special:block.prevrandao";
}

export const isBlockPrevrandao = (
expression: Expression.Base,
): expression is BlockPrevrandao =>
expression.kind === "expression:special:block.prevrandao";

export function blockPrevrandao(
id: Id,
loc?: SourceLocation,
): BlockPrevrandao {
return {
id,
kind: "expression:special:block.prevrandao",
loc: loc ?? null,
};
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/bugc/src/ast/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export function visitDetailed<N extends Ast.Node, T, C = never>(
case "expression:special:msg.data":
case "expression:special:block.timestamp":
case "expression:special:block.number":
case "expression:special:block.prevrandao":
return visitor.expressionSpecial(node as Ast.Expression.Special, context);

default:
Expand Down
Loading
Loading