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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ The sections do not signal obligations; the prefixes do.

### Changed

- The format's own vocabulary inside pointer expressions uses the sigil `~`
instead of `$`, which leaves `$` free for source-language names (Solidity
allows a name to start with `$`). The terms are the constants and
references `~wordsize` and `~this`, and the operators `~sum`,
`~difference`, `~product`, `~quotient`, `~remainder`, `~read`, `~keccak256`,
`~concat`, `~wordsized`, and `~sized<N>`. An identifier still must not
start with `~` ([#323]).
- Schemas: **ethdebug/format/pointer/expression**,
**ethdebug/format/pointer/identifier**, **ethdebug/format/pointer**,
**ethdebug/format/pointer/collection/group**,
**ethdebug/format/pointer/collection/scope**,
**ethdebug/format/pointer/region/stack**,
**ethdebug/format/pointer/region/storage**,
**ethdebug/format/pointer/region/transient**,
**ethdebug/format/pointer/scheme/segment**, **ethdebug/format/info**,
**ethdebug/format/info/resources**
- Producers: required: emit `~` for the operators, constants and the
`~this` reference; the `$` forms no longer validate.
**ethdebug/format/pointer/expression** names the operators in
`properties` (`~sum`, `~read`, `~keccak256`, and so on) and
`patternProperties` (`^~sized([1-9]+[0-9]*)$`, `^~wordsized$`), the
constant in `enum` (`~wordsize`), and the reference in `const`
(`~this`).
- Consumers: required: read the `~` operators, constants and reference
named by **ethdebug/format/pointer/expression**'s `properties`,
`patternProperties`, `enum` and `const`. The `$` spellings, such as
`"$wordsize"` and `{ "$sum": [...] }`, are no longer format terms.

- The variables of a scope (`define`/`in`) are visible only within its `in`.
Pointers outside the scope, such as later members of an enclosing group, do
not see them, and a variable defined with an outer variable's identifier
Expand Down Expand Up @@ -675,3 +703,4 @@ First published version of the specification.
[#309]: https://github.com/ethdebug/format/pull/309
[#317]: https://github.com/ethdebug/format/pull/317
[#318]: https://github.com/ethdebug/format/pull/318
[#323]: https://github.com/ethdebug/format/pull/323
11 changes: 9 additions & 2 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ support. Changes to the specification itself are tracked in the root
assignment to a `bytes` local or to storage, a cast to `bytes` or
`string`, a function argument or return value, and the argument of
`keccak256` copy the bytes to memory. A local's pointer names its word
and then a `calldata` region whose offset and length are `$quotient`
and `$remainder` of that word by 2^128, so it reads exactly the local's
and then a `calldata` region whose offset and length are `~quotient`
and `~remainder` of that word by 2^128, so it reads exactly the local's
bytes, even when its bounds are computed. Writing to bytes in
calldata, or assigning or casting memory bytes to `bytes calldata`, is
a type error. Before, every slice was copied to memory, and a local's
Expand Down Expand Up @@ -375,6 +375,12 @@ support. Changes to the specification itself are tracked in the root
the literal, so its debug pointer read zeros and then that address
([#345]).

### Breaking

- Emitted pointer expressions use the `~` vocabulary (`~sum`, `~product`,
`~quotient`, `~remainder`, `~difference`, `~read`, `~keccak256`,
`~wordsized`, `~wordsize`) instead of `$` ([#323]).

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

### Changed
Expand Down Expand Up @@ -414,6 +420,7 @@ First publication.
[#305]: https://github.com/ethdebug/format/pull/305
[#320]: https://github.com/ethdebug/format/pull/320
[#321]: https://github.com/ethdebug/format/pull/321
[#323]: https://github.com/ethdebug/format/pull/323
[#327]: https://github.com/ethdebug/format/pull/327
[#328]: https://github.com/ethdebug/format/pull/328
[#330]: https://github.com/ethdebug/format/pull/330
Expand Down
2 changes: 1 addition & 1 deletion packages/bugc/examples/intermediate/mappings.bug
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ code {
for:
define:
slot:
$keccak256: [{ $wordsized: key }, { $wordsized: slot }]
~keccak256: [{ ~wordsized: key }, { ~wordsized: slot }]
in: { location: storage, slot: slot, name: value }
in: { location: storage, slot: 0 }
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/bugc/src/evmgen/debug/local-variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function localEntries(
return byId;
}

/** True if a pointer is a frame-relative group (name "-frame" + $read). */
/** True if a pointer is a frame-relative group (name "-frame" + ~read). */
function isFrameRelative(pointer: unknown): boolean {
if (!pointer || typeof pointer !== "object") return false;
const group = (pointer as { group?: unknown[] }).group;
Expand All @@ -64,7 +64,7 @@ function isFrameRelative(pointer: unknown): boolean {
const data = group[1] as Record<string, unknown>;
if (frame.name !== "-frame" || frame.location !== "memory") return false;
const offset = data.offset as Record<string, unknown> | undefined;
return !!offset && Array.isArray(offset.$sum);
return !!offset && Array.isArray(offset["~sum"]);
}

describe("local-variable debug emission (memory-homed, O0)", () => {
Expand Down
22 changes: 11 additions & 11 deletions packages/bugc/src/evmgen/debug/local-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
* lives in memory at `FRAME_POINTER` (0x80); a frame-homed local is at
* `mem[ mem[FRAME_POINTER] + delta ]`, expressed as a `group` naming
* the frame region `-frame` (no BUG identifier starts with `-`) and a
* data region whose offset reads it (`$read`) and adds the static
* delta (`$sum`). Functions without a frame (main/create) home locals
* data region whose offset reads it (`~read`) and adds the static
* delta (`~sum`). Functions without a frame (main/create) home locals
* at a static memory offset.
*/
import type * as Format from "@ethdebug/format";
Expand Down Expand Up @@ -91,7 +91,7 @@ function memoryPointer(
{
name: identifier,
location: "memory",
offset: { $sum: [{ $read: FRAME }, offset] },
offset: { "~sum": [{ "~read": FRAME }, offset] },
length: size,
},
],
Expand Down Expand Up @@ -151,8 +151,8 @@ function dataPointers(
{
name: `${name}-data`,
location: "memory",
offset: { $sum: [base, 32] },
length: { $read: `${name}-length` },
offset: { "~sum": [base, 32] },
length: { "~read": `${name}-length` },
},
];
}
Expand All @@ -162,7 +162,7 @@ function dataPointers(
const index = `i${depth}`;
const element = `${name}-element`;
const wordOffset = (within: number): Format.Pointer.Expression => ({
$sum: [base, 32 + within, { $product: [index, 32] }],
"~sum": [base, 32 + within, { "~product": [index, 32] }],
});
const elementType = fromBugType(type.element);
let is: Format.Pointer;
Expand All @@ -176,7 +176,7 @@ function dataPointers(
};
} else {
const nested = dataPointers(
{ $read: element },
{ "~read": element },
type.element,
element,
depth + 1,
Expand All @@ -197,7 +197,7 @@ function dataPointers(

return [
length,
{ list: { count: { $read: `${name}-length` }, each: index, is } },
{ list: { count: { "~read": `${name}-length` }, each: index, is } },
];
}

Expand All @@ -211,8 +211,8 @@ function calldataPointer(name: string): Format.Pointer {
return {
name: `${name}-data`,
location: "calldata",
offset: { $quotient: [{ $read: name }, half] },
length: { $remainder: [{ $read: name }, half] },
offset: { "~quotient": [{ "~read": name }, half] },
length: { "~remainder": [{ "~read": name }, half] },
};
}

Expand Down Expand Up @@ -661,7 +661,7 @@ function snapshotAt(
data = [calldataPointer(name)];
} else {
if (type.kind !== "ref" || type.location !== "memory") continue;
data = dataPointers({ $read: name }, type.origin, name);
data = dataPointers({ "~read": name }, type.origin, name);
if (!data) continue;
}
const allocation = info.allocations[located.temp];
Expand Down
17 changes: 10 additions & 7 deletions packages/bugc/src/irgen/debug/pointers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,44 @@ import { describe, it, expect } from "vitest";
import { mappingAccess, arrayElementAccess } from "./pointers.js";

/**
* `$keccak256` operands must be width-bearing bytes: the EVM hashes
* `~keccak256` operands must be width-bearing bytes: the EVM hashes
* key‖slot as two 32-byte words, so a bare integer slot operand would
* hash the wrong number of bytes.
*/
describe("mappingAccess", () => {
it("wordsizes a literal slot operand", () => {
expect(mappingAccess(0, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
"~keccak256": [{ "~wordsized": 0x1234 }, { "~wordsized": 0 }],
});
});

it("wordsizes an arithmetic slot operand", () => {
expect(mappingAccess({ $sum: [3, 1] }, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: { $sum: [3, 1] } }],
expect(mappingAccess({ "~sum": [3, 1] }, 0x1234)).toEqual({
"~keccak256": [
{ "~wordsized": 0x1234 },
{ "~wordsized": { "~sum": [3, 1] } },
],
});
});

it("does not re-wrap a nested keccak256 slot operand", () => {
const inner = mappingAccess(1, 0xaaaa);
expect(mappingAccess(inner, 0xbbbb)).toEqual({
$keccak256: [{ $wordsized: 0xbbbb }, inner],
"~keccak256": [{ "~wordsized": 0xbbbb }, inner],
});
});
});

describe("arrayElementAccess", () => {
it("wordsizes the base slot of a dynamic array", () => {
expect(arrayElementAccess(2, "i", true)).toEqual({
$sum: [{ $keccak256: [{ $wordsized: 2 }] }, "i"],
"~sum": [{ "~keccak256": [{ "~wordsized": 2 }] }, "i"],
});
});

it("does not hash the base slot of a fixed array", () => {
expect(arrayElementAccess(2, "i", false)).toEqual({
$sum: [2, "i"],
"~sum": [2, "i"],
});
});
});
24 changes: 12 additions & 12 deletions packages/bugc/src/irgen/debug/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function generatePointer(
* Translate a compute_slot chain to an ethdebug/format pointer expression
*
* Takes a chain of compute_slot instructions and generates the corresponding
* pointer expression using $keccak256, $sum, and other operations.
* pointer expression using ~keccak256, ~sum, and other operations.
*
* SAFETY: Handles unknown patterns gracefully by returning simple expressions.
* Never crashes - just returns best-effort pointer.
Expand Down Expand Up @@ -133,14 +133,14 @@ export function translateComputeSlotChain(
// Note: actual element access is done with binary.add afterward
// which we don't see in the compute_slot chain
expr = {
$keccak256: [wordsized(expr)],
"~keccak256": [wordsized(expr)],
};
} else if (inst.slotKind === "field") {
// Struct field: slot + fieldSlotOffset
const slotOffset = step.fieldSlotOffset ?? 0;
if (slotOffset > 0) {
expr = {
$sum: [expr, slotOffset],
"~sum": [expr, slotOffset],
};
}
// If offset is 0, no change needed
Expand Down Expand Up @@ -181,19 +181,19 @@ function valueToExpression(
}

/**
* Give an expression a 32-byte width for use as a `$keccak256` operand.
* Give an expression a 32-byte width for use as a `~keccak256` operand.
*
* `$keccak256` operands must be width-bearing bytes; a bare integer
* (literal, `$sum`, ...) is invalid there. A `$keccak256` result is
* `~keccak256` operands must be width-bearing bytes; a bare integer
* (literal, `~sum`, ...) is invalid there. A `~keccak256` result is
* already 32 bytes wide, so it is passed through unwrapped.
*/
function wordsized(
expression: Format.Pointer.Expression,
): Format.Pointer.Expression {
if (typeof expression === "object" && "$keccak256" in expression) {
if (typeof expression === "object" && "~keccak256" in expression) {
return expression;
}
return { $wordsized: expression };
return { "~wordsized": expression };
}

/**
Expand All @@ -207,7 +207,7 @@ export function mappingAccess(
key: Format.Pointer.Expression,
): Format.Pointer.Expression {
return {
$keccak256: [{ $wordsized: key }, wordsized(slot)],
"~keccak256": [{ "~wordsized": key }, wordsized(slot)],
};
}

Expand All @@ -225,12 +225,12 @@ export function arrayElementAccess(
if (isDynamic) {
// Dynamic array: keccak256(wordsized(slot)) + index
return {
$sum: [{ $keccak256: [wordsized(baseSlot)] }, index],
"~sum": [{ "~keccak256": [wordsized(baseSlot)] }, index],
};
} else {
// Fixed array: slot + index
return {
$sum: [baseSlot, index],
"~sum": [baseSlot, index],
};
}
}
Expand All @@ -249,6 +249,6 @@ export function structFieldAccess(
}

return {
$sum: [baseSlot, fieldOffset],
"~sum": [baseSlot, fieldOffset],
};
}
16 changes: 8 additions & 8 deletions packages/bugc/src/irgen/debug/storage-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ describe("storage-analysis", () => {

// Should be: keccak256(wordsized(0x1234), wordsized(0))
expect(pointer).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
"~keccak256": [{ "~wordsized": 0x1234 }, { "~wordsized": 0 }],
});
});

Expand All @@ -453,10 +453,10 @@ describe("storage-analysis", () => {
// )
// The inner keccak256 is already 32-byte bytes, so it is not wrapped.
expect(pointer).toEqual({
$keccak256: [
{ $wordsized: 0xbbbb },
"~keccak256": [
{ "~wordsized": 0xbbbb },
{
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 1 }],
"~keccak256": [{ "~wordsized": 0xaaaa }, { "~wordsized": 1 }],
},
],
});
Expand All @@ -477,7 +477,7 @@ describe("storage-analysis", () => {

// Should be: keccak256(wordsized(2))
expect(pointer).toEqual({
$keccak256: [{ $wordsized: 2 }],
"~keccak256": [{ "~wordsized": 2 }],
});
});

Expand All @@ -496,7 +496,7 @@ describe("storage-analysis", () => {

// Should be: sum(3, 1)
expect(pointer).toEqual({
$sum: [3, 1],
"~sum": [3, 1],
});
});

Expand All @@ -516,9 +516,9 @@ describe("storage-analysis", () => {

// Should be: sum(keccak256(wordsized(0xaaaa), wordsized(4)), 2)
expect(pointer).toEqual({
$sum: [
"~sum": [
{
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 4 }],
"~keccak256": [{ "~wordsized": 0xaaaa }, { "~wordsized": 4 }],
},
2,
],
Expand Down
Loading
Loading