diff --git a/CHANGELOG.md b/CHANGELOG.md index 7435bcfcbd..74e6a49dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. 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 @@ -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 diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index dfb09a7cd7..d886076eea 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -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 @@ -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 @@ -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 diff --git a/packages/bugc/examples/intermediate/mappings.bug b/packages/bugc/examples/intermediate/mappings.bug index 9ff5a84d70..6d94561fda 100644 --- a/packages/bugc/examples/intermediate/mappings.bug +++ b/packages/bugc/examples/intermediate/mappings.bug @@ -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 } */ diff --git a/packages/bugc/src/evmgen/debug/local-variables.test.ts b/packages/bugc/src/evmgen/debug/local-variables.test.ts index 9d7dba1605..df4d73755f 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.test.ts @@ -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; @@ -64,7 +64,7 @@ function isFrameRelative(pointer: unknown): boolean { const data = group[1] as Record; if (frame.name !== "-frame" || frame.location !== "memory") return false; const offset = data.offset as Record | undefined; - return !!offset && Array.isArray(offset.$sum); + return !!offset && Array.isArray(offset["~sum"]); } describe("local-variable debug emission (memory-homed, O0)", () => { diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts index 5b8f612021..2b7eca419b 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.ts @@ -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"; @@ -91,7 +91,7 @@ function memoryPointer( { name: identifier, location: "memory", - offset: { $sum: [{ $read: FRAME }, offset] }, + offset: { "~sum": [{ "~read": FRAME }, offset] }, length: size, }, ], @@ -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` }, }, ]; } @@ -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; @@ -176,7 +176,7 @@ function dataPointers( }; } else { const nested = dataPointers( - { $read: element }, + { "~read": element }, type.element, element, depth + 1, @@ -197,7 +197,7 @@ function dataPointers( return [ length, - { list: { count: { $read: `${name}-length` }, each: index, is } }, + { list: { count: { "~read": `${name}-length` }, each: index, is } }, ]; } @@ -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] }, }; } @@ -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]; diff --git a/packages/bugc/src/irgen/debug/pointers.test.ts b/packages/bugc/src/irgen/debug/pointers.test.ts index e600e00fe9..d2bde9abe3 100644 --- a/packages/bugc/src/irgen/debug/pointers.test.ts +++ b/packages/bugc/src/irgen/debug/pointers.test.ts @@ -3,27 +3,30 @@ 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], }); }); }); @@ -31,13 +34,13 @@ describe("mappingAccess", () => { 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"], }); }); }); diff --git a/packages/bugc/src/irgen/debug/pointers.ts b/packages/bugc/src/irgen/debug/pointers.ts index b6644f7387..20ca0cdd5d 100644 --- a/packages/bugc/src/irgen/debug/pointers.ts +++ b/packages/bugc/src/irgen/debug/pointers.ts @@ -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. @@ -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 @@ -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 }; } /** @@ -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)], }; } @@ -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], }; } } @@ -249,6 +249,6 @@ export function structFieldAccess( } return { - $sum: [baseSlot, fieldOffset], + "~sum": [baseSlot, fieldOffset], }; } diff --git a/packages/bugc/src/irgen/debug/storage-analysis.test.ts b/packages/bugc/src/irgen/debug/storage-analysis.test.ts index 15de5ee705..b36ad8a483 100644 --- a/packages/bugc/src/irgen/debug/storage-analysis.test.ts +++ b/packages/bugc/src/irgen/debug/storage-analysis.test.ts @@ -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 }], }); }); @@ -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 }], }, ], }); @@ -477,7 +477,7 @@ describe("storage-analysis", () => { // Should be: keccak256(wordsized(2)) expect(pointer).toEqual({ - $keccak256: [{ $wordsized: 2 }], + "~keccak256": [{ "~wordsized": 2 }], }); }); @@ -496,7 +496,7 @@ describe("storage-analysis", () => { // Should be: sum(3, 1) expect(pointer).toEqual({ - $sum: [3, 1], + "~sum": [3, 1], }); }); @@ -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, ], diff --git a/packages/bugc/src/irgen/debug/variables.ts b/packages/bugc/src/irgen/debug/variables.ts index 502f2a3582..08e5d26153 100644 --- a/packages/bugc/src/irgen/debug/variables.ts +++ b/packages/bugc/src/irgen/debug/variables.ts @@ -56,7 +56,7 @@ function plus( slots: number, ): Format.Pointer.Expression { if (slots === 0) return slot; - return typeof slot === "number" ? slot + slots : { $sum: [slot, slots] }; + return typeof slot === "number" ? slot + slots : { "~sum": [slot, slots] }; } /** @@ -138,7 +138,7 @@ export function generateStoragePointer( const index = depth === 0 ? "i" : `i${depth}`; const first: Format.Pointer.Expression = bugType.size === undefined - ? { $keccak256: [{ $wordsized: baseSlot }] } + ? { "~keccak256": [{ "~wordsized": baseSlot }] } : baseSlot; let elementPointer: Format.Pointer | undefined; @@ -150,11 +150,11 @@ export function generateStoragePointer( elementPointer = { name: `${prefix}element`, location: "storage", - slot: { $sum: [first, { $quotient: [index, perSlot] }] }, + slot: { "~sum": [first, { "~quotient": [index, perSlot] }] }, offset: { - $difference: [ + "~difference": [ 32 - size, - { $product: [{ $remainder: [index, perSlot] }, size] }, + { "~product": [{ "~remainder": [index, perSlot] }, size] }, ], }, length: size, @@ -163,7 +163,10 @@ export function generateStoragePointer( const stride = Storage.slots(element); elementPointer = generateStoragePointer( { - $sum: [first, stride === 1 ? index : { $product: [index, stride] }], + "~sum": [ + first, + stride === 1 ? index : { "~product": [index, stride] }, + ], }, element, 0, @@ -181,7 +184,7 @@ export function generateStoragePointer( list: { count: bugType.size === undefined - ? { $read: `${prefix}array-length` } + ? { "~read": `${prefix}array-length` } : bugType.size, each: index, is: elementPointer, @@ -272,7 +275,9 @@ function generateMappingPointer( expect: ["slot", "key"], for: { define: { - slot: { $keccak256: [{ $wordsized: "key" }, { $wordsized: "slot" }] }, + slot: { + "~keccak256": [{ "~wordsized": "key" }, { "~wordsized": "slot" }], + }, }, in: Format.Pointer.isRegion(value) ? { ...value, name: "value" } @@ -304,13 +309,13 @@ function generateStorageBytesPointer( name: flag, location: "storage", slot, - offset: { $difference: ["$wordsize", 1] }, + offset: { "~difference": ["~wordsize", 1] }, length: 1, }, { - if: { $remainder: [{ $sum: [{ $read: flag }, 1] }, 2] }, + if: { "~remainder": [{ "~sum": [{ "~read": flag }, 1] }, 2] }, then: { - define: { length: { $quotient: [{ $read: flag }, 2] } }, + define: { length: { "~quotient": [{ "~read": flag }, 2] } }, in: { name: data, location: "storage", slot, length: "length" }, }, else: { @@ -319,9 +324,12 @@ function generateStorageBytesPointer( { define: { length: { - $quotient: [{ $difference: [{ $read: longLength }, 1] }, 2], + "~quotient": [ + { "~difference": [{ "~read": longLength }, 1] }, + 2, + ], }, - start: { $keccak256: [{ $wordsized: slot }] }, + start: { "~keccak256": [{ "~wordsized": slot }] }, }, in: { name: data, diff --git a/packages/conformance/test/conformance.test.ts b/packages/conformance/test/conformance.test.ts index 7cbbc7add0..cf584f150b 100644 --- a/packages/conformance/test/conformance.test.ts +++ b/packages/conformance/test/conformance.test.ts @@ -103,7 +103,7 @@ function validProgram() { function validResources() { return { ethdebug: { - schema: "ethdebug/format/info/resources", + schema: "ethdebug/format/info/resources" as const, version: "0.1.0-draft.0", }, compilation: validCompilation(), @@ -169,6 +169,65 @@ function withSolcResourcesStamp(artifact: EthdebugArtifact): EthdebugArtifact { }; } +// Expected failure (#324): solc still writes the format's pointer +// vocabulary with the old `$` sigil (`$sum`, `$wordsize`, `$this`, ...), +// and the schemas now require `~`. Until solc emits `~`, the solc tests +// translate a copy of solc's programs and resources before they validate +// them, so the rest of the output is still checked. SolDB reads solc's +// original output and does not see the copy. The check below fails once +// solc emits a `~` term; then remove this function and validate the +// artifact as it is. +const termOperators = + "sum|difference|product|quotient|remainder|read|keccak256|concat|" + + "wordsized|sized[1-9][0-9]*"; +const termConstants = "wordsize|this"; +const solcDollarOperator = new RegExp(`^\\$(${termOperators})$`); +const solcDollarConstant = new RegExp(`^\\$(${termConstants})$`); +const tildeTerm = new RegExp(`^~(${termOperators}|${termConstants})$`); + +function withSolcTildeVocabulary(artifact: EthdebugArtifact): EthdebugArtifact { + const translate = (value: unknown): unknown => { + if (typeof value === "string") { + expect( + tildeTerm.test(value), + `solc now emits the ~ term ${value}; remove withSolcTildeVocabulary`, + ).toBe(false); + return solcDollarConstant.test(value) ? `~${value.slice(1)}` : value; + } + if (Array.isArray(value)) { + return value.map(translate); + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => { + expect( + tildeTerm.test(key), + `solc now emits the ~ term ${key}; remove withSolcTildeVocabulary`, + ).toBe(false); + const translated = solcDollarOperator.test(key) + ? `~${key.slice(1)}` + : key; + return [translated, translate(entry)]; + }), + ); + } + return value; + }; + + return { + ...artifact, + programs: artifact.programs.map((program) => ({ + ...program, + program: translate( + program.program, + ) as EthdebugArtifact["programs"][0]["program"], + })), + ...(artifact.resources && { + resources: translate(artifact.resources) as EthdebugArtifact["resources"], + }), + }; +} + function validArtifact( overrides: Partial = {}, ): EthdebugArtifact { @@ -215,7 +274,7 @@ describe("@ethdebug/conformance", () => { }); const result = await validateStaticConformance( - withSolcResourcesStamp(artifact), + withSolcTildeVocabulary(withSolcResourcesStamp(artifact)), ); expect(result.issues).toEqual([]); expect(result.ok).toBe(true); @@ -268,7 +327,7 @@ describe("@ethdebug/conformance", () => { ], }); const result = await validateStaticConformance( - withSolcResourcesStamp(artifact), + withSolcTildeVocabulary(withSolcResourcesStamp(artifact)), ); expect(result.issues).toEqual([]); expect(result.ok).toBe(true); @@ -359,6 +418,42 @@ describe("@ethdebug/conformance", () => { }, ); + it("translates a copy of solc's $ vocabulary to ~", () => { + const pointer = { + location: "memory", + offset: { $sum: [{ ".offset": "$this" }, "$wordsize"] }, + length: { $sized2: { $read: "$x" } }, + }; + const artifact = validArtifact({ + compilation: undefined, + resources: { ...validResources(), pointers: { P: { for: pointer } } }, + }); + + const translated = withSolcTildeVocabulary(artifact); + + expect(translated.resources?.pointers.P).toHaveProperty("for", { + location: "memory", + offset: { "~sum": [{ ".offset": "~this" }, "~wordsize"] }, + length: { "~sized2": { "~read": "$x" } }, + }); + expect(artifact.resources?.pointers.P).toHaveProperty("for", pointer); + expect(pointer.offset).toHaveProperty("$sum"); + }); + + it("stops translating once solc emits ~", () => { + const artifact = validArtifact({ + compilation: undefined, + resources: { + ...validResources(), + pointers: { P: { for: { location: "stack", slot: { "~sum": [] } } } }, + }, + }); + + expect(() => withSolcTildeVocabulary(artifact)).toThrow( + /remove withSolcTildeVocabulary/, + ); + }); + it("rejects malformed ETHDebug programs through JSON-Schema validation", async () => { const artifact = validArtifact({ programs: [ diff --git a/packages/format/CHANGELOG.md b/packages/format/CHANGELOG.md index 42282f20ce..42757606ef 100644 --- a/packages/format/CHANGELOG.md +++ b/packages/format/CHANGELOG.md @@ -6,6 +6,15 @@ tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). ## Unreleased +### Breaking + +- The bundled schemas write the pointer vocabulary with `~` instead of `$` + (`~wordsize`, `~this`, `~sum`, `~read`, `~keccak256`, `~sized`, and so + on). The `Pointer.Expression` types and guards follow: an operation such as + `Pointer.Expression.Arithmetic.Sum` has the key `"~sum"`, `Constant` is + `"~wordsize"`, and `Reference` admits `"~this"`. See the root + [`CHANGELOG.md`](../../CHANGELOG.md) ([#323]). + ## 0.1.0-draft.1 — 2026-10-03 ### Added @@ -129,6 +138,7 @@ First publication. [#300]: https://github.com/ethdebug/format/pull/300 [#303]: https://github.com/ethdebug/format/pull/303 [#305]: https://github.com/ethdebug/format/pull/305 +[#323]: https://github.com/ethdebug/format/pull/323 [`0ef2f37`]: https://github.com/ethdebug/format/commit/0ef2f37 [`10ab103`]: https://github.com/ethdebug/format/commit/10ab103 [`21e532e`]: https://github.com/ethdebug/format/commit/21e532e diff --git a/packages/format/src/types/pointer/pointer.test.ts b/packages/format/src/types/pointer/pointer.test.ts index 583178acdb..5e93280bc1 100644 --- a/packages/format/src/types/pointer/pointer.test.ts +++ b/packages/format/src/types/pointer/pointer.test.ts @@ -14,6 +14,8 @@ describe("Pointer.isIdentifier", () => { "", // empty "0abc", // leading digit "$x", // leading $ + "~x", // leading ~, reserved for the format's own terms + "~wordsize", // the constant, not an identifier "a b", // space "foo\\bar", // embedded backslash — accepted before the fix "\\", // lone backslash @@ -46,6 +48,21 @@ describe("Pointer.isIdentifier", () => { }); }); +describe("the ~ sigil", () => { + it("reads ~-prefixed terms as the format's own", () => { + expect(Pointer.Expression.isConstant("~wordsize")).toBe(true); + expect(Pointer.Expression.isReference("~this")).toBe(true); + expect(Pointer.Expression.Arithmetic.isSum({ "~sum": [1, 2] })).toBe(true); + }); + + it("does not read the old $-prefixed terms", () => { + expect(Pointer.Expression.isConstant("$wordsize")).toBe(false); + expect(Pointer.isExpression("$wordsize")).toBe(false); + expect(Pointer.Expression.isReference("$this")).toBe(false); + expect(Pointer.isExpression({ $sum: [1, 2] })).toBe(false); + }); +}); + const expressionSchema = "schema:ethdebug/format/pointer/expression"; testSchemaGuards("ethdebug/format/pointer", [ { diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index 11f38de8b4..5271de7fd2 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -252,9 +252,9 @@ export namespace Pointer { typeof value === "number" || (typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value)); - export type Constant = "$wordsize"; + export type Constant = "~wordsize"; export const isConstant = (value: unknown): value is Constant => - typeof value === "string" && ["$wordsize"].includes(value); + typeof value === "string" && ["~wordsize"].includes(value); export type Variable = string; export const isVariable = (value: unknown): value is Variable => @@ -306,46 +306,46 @@ export namespace Pointer { isOperands(value) && value.length === 2; export interface Sum { - $sum: Expression[]; + "~sum": Expression[]; } - export const isSum = makeIsOperation<"$sum", Sum>("$sum", isOperands); + export const isSum = makeIsOperation<"~sum", Sum>("~sum", isOperands); export interface Difference { - $difference: [Expression, Expression]; + "~difference": [Expression, Expression]; } - export const isDifference = makeIsOperation<"$difference", Difference>( - "$difference", + export const isDifference = makeIsOperation<"~difference", Difference>( + "~difference", isTwoOperands, ); export interface Product { - $product: Expression[]; + "~product": Expression[]; } - export const isProduct = makeIsOperation<"$product", Product>( - "$product", + export const isProduct = makeIsOperation<"~product", Product>( + "~product", isOperands, ); export interface Quotient { - $quotient: [Expression, Expression]; + "~quotient": [Expression, Expression]; } - export const isQuotient = makeIsOperation<"$quotient", Quotient>( - "$quotient", + export const isQuotient = makeIsOperation<"~quotient", Quotient>( + "~quotient", isTwoOperands, ); export interface Remainder { - $remainder: [Expression, Expression]; + "~remainder": [Expression, Expression]; } - export const isRemainder = makeIsOperation<"$remainder", Remainder>( - "$remainder", + export const isRemainder = makeIsOperation<"~remainder", Remainder>( + "~remainder", isTwoOperands, ); } - export type Reference = Identifier | "$this"; + export type Reference = Identifier | "~this"; export const isReference = (value: unknown): value is Reference => - isIdentifier(value) || value === "$this"; + isIdentifier(value) || value === "~this"; export type Lookup = Lookup.Offset | Lookup.Length | Lookup.Slot; export const isLookup = (value: unknown): value is Lookup => @@ -392,23 +392,23 @@ export namespace Pointer { } export interface Read { - $read: Reference; + "~read": Reference; } - export const isRead = makeIsOperation<"$read", Read>("$read", isReference); + export const isRead = makeIsOperation<"~read", Read>("~read", isReference); export interface Keccak256 { - $keccak256: Expression[]; + "~keccak256": Expression[]; } - export const isKeccak256 = makeIsOperation<"$keccak256", Keccak256>( - "$keccak256", + export const isKeccak256 = makeIsOperation<"~keccak256", Keccak256>( + "~keccak256", isOperands, ); export interface Concat { - $concat: Expression[]; + "~concat": Expression[]; } - export const isConcat = makeIsOperation<"$concat", Concat>( - "$concat", + export const isConcat = makeIsOperation<"~concat", Concat>( + "~concat", isOperands, ); @@ -422,7 +422,7 @@ export namespace Pointer { export namespace Resize { export type ToNumber = { - [K in `$sized${N}`]: Expression; + [K in `~sized${N}`]: Expression; }; export const isToNumber = ( value: unknown, @@ -436,19 +436,19 @@ export namespace Pointer { } const [key] = Object.keys(value); - return typeof key === "string" && /^\$sized([1-9]+[0-9]*)$/.test(key); + return typeof key === "string" && /^~sized([1-9]+[0-9]*)$/.test(key); }; export type ToWordsize = { - $wordsized: Expression; + "~wordsized": Expression; }; export const isToWordsize = (value: unknown): value is ToWordsize => !!value && typeof value === "object" && Object.keys(value).length === 1 && - "$wordsized" in value && - typeof value.$wordsized !== "undefined" && - isExpression(value.$wordsized); + "~wordsized" in value && + typeof value["~wordsized"] !== "undefined" && + isExpression(value["~wordsized"]); } } diff --git a/packages/pointers-react/CHANGELOG.md b/packages/pointers-react/CHANGELOG.md index aa58890ba7..db3fa603c8 100644 --- a/packages/pointers-react/CHANGELOG.md +++ b/packages/pointers-react/CHANGELOG.md @@ -7,6 +7,10 @@ root [`CHANGELOG.md`](../../CHANGELOG.md). ## Unreleased +### Breaking + +- Follows the `~` vocabulary of `@ethdebug/pointers` ([#323]). + ## 0.1.0-preview.1 — 2026-10-03 Updated `@ethdebug/format` to `0.1.0-draft.1` and `@ethdebug/pointers` to @@ -39,3 +43,4 @@ First publication. [#298]: https://github.com/ethdebug/format/pull/298 [#299]: https://github.com/ethdebug/format/pull/299 [#300]: https://github.com/ethdebug/format/pull/300 +[#323]: https://github.com/ethdebug/format/pull/323 diff --git a/packages/pointers/CHANGELOG.md b/packages/pointers/CHANGELOG.md index 12f60b4811..6a651fe827 100644 --- a/packages/pointers/CHANGELOG.md +++ b/packages/pointers/CHANGELOG.md @@ -14,6 +14,13 @@ itself are tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). template for a struct with a string member read the members after it one slot too far ([#317]). +### Breaking + +- `evaluate` and `dereference` read the pointer vocabulary with `~` instead of + `$` (`~wordsize`, `~this`, `~sum`, `~read`, `~keccak256`, `~sized`, and so + on). The `$` spellings are no longer format terms; the error for one + suggests its `~` spelling ([#323]). + ## 0.1.0-preview.1 — 2026-10-03 ### Fixed @@ -121,3 +128,4 @@ First publication. [#315]: https://github.com/ethdebug/format/pull/315 [#316]: https://github.com/ethdebug/format/pull/316 [#317]: https://github.com/ethdebug/format/pull/317 +[#323]: https://github.com/ethdebug/format/pull/323 diff --git a/packages/pointers/src/dereference/index.test.ts b/packages/pointers/src/dereference/index.test.ts index c57d7eaab2..05419e5a8f 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -52,7 +52,7 @@ describe("dereference", () => { const pointer: Pointer = { location: "memory", offset: { - $sum: [0x40, 0x20], + "~sum": [0x40, 0x20], }, length: 0x20, }; @@ -76,14 +76,14 @@ describe("dereference", () => { name: "a", location: "memory", offset: { - $sum: [0x40, 0x20], + "~sum": [0x40, 0x20], }, length: 0x20, }, { location: "memory", offset: { - $sum: [{ ".offset": "a" }, { ".length": "a" }], + "~sum": [{ ".offset": "a" }, { ".length": "a" }], }, length: { ".length": "a" }, }, @@ -117,7 +117,7 @@ describe("dereference", () => { name: "item", location: "memory", offset: { - $product: ["i", 32], + "~product": ["i", 32], }, length: 32, }, @@ -141,11 +141,11 @@ describe("dereference", () => { } }); - it("allows referencing previous fields by way of $this", async () => { + it("allows referencing previous fields by way of ~this", async () => { const pointer: Pointer = { location: "memory", offset: 32, - length: { ".offset": "$this" }, + length: { ".offset": "~this" }, }; const cursor = await dereference(pointer); @@ -160,10 +160,10 @@ describe("dereference", () => { }); }); - it("allows referencing later fields by way of $this", async () => { + it("allows referencing later fields by way of ~this", async () => { const pointer: Pointer = { location: "memory", - offset: { ".length": "$this" }, + offset: { ".length": "~this" }, length: 32, }; @@ -182,8 +182,8 @@ describe("dereference", () => { it("allows referencing fields that reference other fields", async () => { const pointer: Pointer = { location: "storage", - slot: { ".offset": "$this" }, - offset: { ".length": "$this" }, + slot: { ".offset": "~this" }, + offset: { ".length": "~this" }, length: 32, }; @@ -200,7 +200,7 @@ describe("dereference", () => { }); }); - it("resolves .length lookups of $this inside list items", async () => { + it("resolves .length lookups of ~this inside list items", async () => { // mirrors the shape of the `struct Record[] memory` schema example, // where each item's offset is computed from its own length const pointer: Pointer = { @@ -211,7 +211,10 @@ describe("dereference", () => { name: "item", location: "memory", offset: { - $sum: [64, { $product: ["item-index", { ".length": "$this" }] }], + "~sum": [ + 64, + { "~product": ["item-index", { ".length": "~this" }] }, + ], }, length: 32, }, @@ -229,38 +232,38 @@ describe("dereference", () => { ]); }); - it("throws an error on a self-referential $this lookup", async () => { + it("throws an error on a self-referential ~this lookup", async () => { const pointer: Pointer = { location: "memory", offset: 0, - length: { ".length": "$this" }, + length: { ".length": "~this" }, }; const cursor = await dereference(pointer); await expect(cursor.view(state)).rejects.toThrow( - "Circular reference detected: $this.length", + "Circular reference detected: ~this.length", ); }); it("throws an error on circular reference", async () => { const pointer: Pointer = { location: "memory", - offset: { ".length": "$this" }, - length: { ".offset": "$this" }, + offset: { ".length": "~this" }, + length: { ".offset": "~this" }, }; const cursor = await dereference(pointer); await expect(cursor.view(state)).rejects.toThrow( - "Circular reference detected: $this.offset", + "Circular reference detected: ~this.offset", ); }); it("works for conditionals", async () => { const pointer: Pointer = { if: { - $difference: [5, 5], + "~difference": [5, 5], }, then: { name: "a", @@ -312,12 +315,12 @@ describe("dereference", () => { in: { group: [ { - define: { slot: { $sum: ["slot", 1] } }, + define: { slot: { "~sum": ["slot", 1] } }, in: { location: "storage", slot: "slot", name: "a" }, }, { location: "storage", - slot: { $sum: ["slot", 2] }, + slot: { "~sum": ["slot", 2] }, name: "b", }, ], @@ -341,7 +344,7 @@ describe("dereference", () => { shifted: { expect: ["slot"], for: { - define: { slot: { $sum: ["slot", 1] } }, + define: { slot: { "~sum": ["slot", 1] } }, in: { location: "storage", slot: "slot", name: "a" }, }, }, @@ -351,7 +354,7 @@ describe("dereference", () => { { template: "shifted" }, { location: "storage", - slot: { $sum: ["slot", 2] }, + slot: { "~sum": ["slot", 2] }, name: "b", }, ], @@ -479,7 +482,7 @@ describe("dereference", () => { for: { group: [ { name: "a", location: "storage", slot: "slot" }, - { name: "b", location: "storage", slot: { $sum: ["slot", 1] } }, + { name: "b", location: "storage", slot: { "~sum": ["slot", 1] } }, ], }, }, @@ -519,7 +522,7 @@ describe("dereference", () => { name: "second", location: "memory", offset: { - $sum: [{ ".offset": "first" }, { ".length": "first" }], + "~sum": [{ ".offset": "first" }, { ".length": "first" }], }, length: { ".length": "first" }, }, @@ -572,7 +575,7 @@ describe("dereference", () => { in: { template: "inner" }, }, { - define: { slot: { $sum: ["base-slot", 1] } }, + define: { slot: { "~sum": ["base-slot", 1] } }, in: { template: "inner" }, }, ], @@ -812,4 +815,18 @@ describe("dereference", () => { expect(regions[0].name).toEqual("my-slot-value"); expect(regions.lookup["my-slot-value"]).toBeDefined(); }); + + it("rejects a $ operator", async () => { + const pointer = { + location: "memory", + offset: { $sum: [1, 2] }, + length: 0x20, + } as unknown as Pointer; + + const cursor = await dereference(pointer); + + await expect(cursor.view(state)).rejects.toThrow( + "Unexpected unknown kind of pointer", + ); + }); }); diff --git a/packages/pointers/src/dereference/region.ts b/packages/pointers/src/dereference/region.ts index a492c9d479..8c855a37c2 100644 --- a/packages/pointers/src/dereference/region.ts +++ b/packages/pointers/src/dereference/region.ts @@ -6,13 +6,13 @@ import { evaluate, Value, type EvaluateOptions } from "#evaluate"; /** * Evaluate all Pointer.Expression-value properties on a given region * - * Due to the availability of `$this` as a builtin allowable by the schema, + * Due to the availability of `~this` as a builtin allowable by the schema, * this function evaluates each property as part of a queue. If a property's * expression fails to evaluate due to a missing reference, the property is * added to the end of the queue. * * Circular dependencies are detected naïvely by counting evaluation attempts - * for each property, since the maximum length of a chain of $this references + * for each property, since the maximum length of a chain of ~this references * within a single region is one less than the number of properties that * require evaluation). Exceeding this many attempts indicates circularity. */ @@ -35,7 +35,7 @@ export async function evaluateRegion( return evaluatedProperties[property as keyof R]; } throw new Error( - `Property not evaluated yet: $this.${property.toString()}`, + `Property not evaluated yet: ~this.${property.toString()}`, ); }, }, @@ -59,7 +59,7 @@ export async function evaluateRegion( ...options, regions: { ...options.regions, - $this: partialRegion, + "~this": partialRegion, }, }); @@ -67,14 +67,14 @@ export async function evaluateRegion( } catch (error) { if ( error instanceof Error && - error.message.startsWith("Property not evaluated yet: $this.") + error.message.startsWith("Property not evaluated yet: ~this.") ) { const attempts = propertyAttempts[property] || 0; // fields may reference each other, but the chain of references // should not exceed the number of fields minus 1 if (attempts > propertiesRequiringEvaluation.length - 1) { throw new Error( - `Circular reference detected: $this.${property.toString()}`, + `Circular reference detected: ~this.${property.toString()}`, ); } @@ -106,9 +106,9 @@ export function adjustStackLength( stackLengthChange === 0n ? region.slot : stackLengthChange > 0n - ? { $sum: [region.slot, `0x${stackLengthChange.toString(16)}`] } + ? { "~sum": [region.slot, `0x${stackLengthChange.toString(16)}`] } : { - $difference: [ + "~difference": [ region.slot, `0x${-stackLengthChange.toString(16)}`, ], diff --git a/packages/pointers/src/evaluate.examples.test.ts b/packages/pointers/src/evaluate.examples.test.ts index 98e3486532..e257b0a949 100644 --- a/packages/pointers/src/evaluate.examples.test.ts +++ b/packages/pointers/src/evaluate.examples.test.ts @@ -8,7 +8,7 @@ import type { Cursor } from "#cursor"; import { evaluate, Value, type EvaluateOptions } from "./evaluate.js"; /** - * Every `$keccak256` / `$concat` expression appearing in the examples of + * Every `~keccak256` / `~concat` expression appearing in the examples of * the pointer schemas, along with the `define`d variable names seen along * the way (so that a variable can be stubbed with the sort of its defining * expression). diff --git a/packages/pointers/src/evaluate.test.ts b/packages/pointers/src/evaluate.test.ts index 9216dae711..8b8f502f32 100644 --- a/packages/pointers/src/evaluate.test.ts +++ b/packages/pointers/src/evaluate.test.ts @@ -88,8 +88,8 @@ describe("evaluate", () => { }); }); - it("evaluates $wordsize to the integer 32", async () => { - expect(await evaluate("$wordsize", options)).toEqual(Value.integer(32n)); + it("evaluates ~wordsize to the integer 32", async () => { + expect(await evaluate("~wordsize", options)).toEqual(Value.integer(32n)); }); it("evaluates variables to their values, preserving sort", async () => { @@ -109,7 +109,7 @@ describe("evaluate", () => { describe("arithmetic", () => { it("evaluates sums to an integer, coercing bytes operands", async () => { const expression: Pointer.Expression = { - $sum: [42, "0x1f", "foo", "bar"], + "~sum": [42, "0x1f", "foo", "bar"], }; expect(await evaluate(expression, options)).toEqual( @@ -118,20 +118,20 @@ describe("evaluate", () => { }); it("evaluates differences", async () => { - expect(await evaluate({ $difference: ["foo", "bar"] }, options)).toEqual( - Value.integer(42n - 0x1fn), - ); + expect( + await evaluate({ "~difference": ["foo", "bar"] }, options), + ).toEqual(Value.integer(42n - 0x1fn)); }); it("clamps differences at zero", async () => { - expect(await evaluate({ $difference: ["bar", "foo"] }, options)).toEqual( - Value.integer(0n), - ); + expect( + await evaluate({ "~difference": ["bar", "foo"] }, options), + ).toEqual(Value.integer(0n)); }); it("evaluates products", async () => { const expression: Pointer.Expression = { - $product: [42, "0x1f", "foo", "bar"], + "~product": [42, "0x1f", "foo", "bar"], }; expect(await evaluate(expression, options)).toEqual( @@ -140,26 +140,26 @@ describe("evaluate", () => { }); it("evaluates quotients", async () => { - expect(await evaluate({ $quotient: ["foo", "bar"] }, options)).toEqual( + expect(await evaluate({ "~quotient": ["foo", "bar"] }, options)).toEqual( Value.integer(42n / 0x1fn), ); }); it("evaluates remainders", async () => { - expect(await evaluate({ $remainder: ["foo", "bar"] }, options)).toEqual( + expect(await evaluate({ "~remainder": ["foo", "bar"] }, options)).toEqual( Value.integer(42n % 0x1fn), ); }); it("reads bytes operands as big-endian integers", async () => { - expect(await evaluate({ $sum: ["0x0100", "0x00"] }, options)).toEqual( + expect(await evaluate({ "~sum": ["0x0100", "0x00"] }, options)).toEqual( Value.integer(256n), ); }); it("produces integers with no width, even from wide operands", async () => { expect( - await evaluate({ $difference: ["0x0000", "0x0000"] }, options), + await evaluate({ "~difference": ["0x0000", "0x0000"] }, options), ).toEqual(Value.integer(0n)); }); }); @@ -171,9 +171,9 @@ describe("evaluate", () => { ); }); - it("evaluates offset lookups with $this", async () => { - const $this = { - name: "$this", + it("evaluates offset lookups with ~this", async () => { + const thisRegion = { + name: "~this", location: "memory", offset: Data.fromNumber(0x120), length: Data.fromNumber(0x40), @@ -181,12 +181,12 @@ describe("evaluate", () => { expect( await evaluate( - { ".offset": "$this" }, + { ".offset": "~this" }, { ...options, regions: { ...regions, - $this, + "~this": thisRegion, }, }, ), @@ -212,8 +212,8 @@ describe("evaluate", () => { }); }); - it("evaluates $read to bytes of the region's length", async () => { - const result = await evaluate({ $read: "memory" }, options); + it("evaluates ~read to bytes of the region's length", async () => { + const result = await evaluate({ "~read": "memory" }, options); expect(result).toEqual( Value.bytes(Data.fromBytes(new Uint8Array(11).fill(0xee))), @@ -222,80 +222,87 @@ describe("evaluate", () => { describe("resize", () => { it("gives an integer a width", async () => { - expect(await evaluate({ $sized1: 0 }, options)).toEqual( + expect(await evaluate({ "~sized1": 0 }, options)).toEqual( Value.bytes(Data.fromHex("0x00")), ); - expect(await evaluate({ $sized2: 42 }, options)).toEqual( + expect(await evaluate({ "~sized2": 42 }, options)).toEqual( Value.bytes(Data.fromHex("0x002a")), ); - expect(await evaluate({ $wordsized: 0xabcd }, options)).toEqual( + expect(await evaluate({ "~wordsized": 0xabcd }, options)).toEqual( Value.bytes(Data.fromNumber(0xabcd).resizeTo(32)), ); }); it("resizes bytes, padding or truncating on the left", async () => { - expect(await evaluate({ $sized1: "0xabcd" }, options)).toEqual( + expect(await evaluate({ "~sized1": "0xabcd" }, options)).toEqual( Value.bytes(Data.fromHex("0xcd")), ); - expect(await evaluate({ $sized4: "0xabcd" }, options)).toEqual( + expect(await evaluate({ "~sized4": "0xabcd" }, options)).toEqual( Value.bytes(Data.fromHex("0x0000abcd")), ); - expect(await evaluate({ $wordsized: "0xabcd" }, options)).toEqual( + expect(await evaluate({ "~wordsized": "0xabcd" }, options)).toEqual( Value.bytes(Data.fromHex("0xabcd").resizeTo(32)), ); }); it("truncates an integer too large for the requested width", async () => { - expect(await evaluate({ $sized1: 0x1234 }, options)).toEqual( + expect(await evaluate({ "~sized1": 0x1234 }, options)).toEqual( Value.bytes(Data.fromHex("0x34")), ); }); it("gives arithmetic results a width", async () => { - expect(await evaluate({ $sized2: { $sum: [1, 2] } }, options)).toEqual( - Value.bytes(Data.fromHex("0x0003")), - ); + expect( + await evaluate({ "~sized2": { "~sum": [1, 2] } }, options), + ).toEqual(Value.bytes(Data.fromHex("0x0003"))); }); }); - describe("$concat", () => { + describe("~concat", () => { it("concatenates hex literals", async () => { - expect(await evaluate({ $concat: ["0x00", "0x00"] }, options)).toEqual( + expect(await evaluate({ "~concat": ["0x00", "0x00"] }, options)).toEqual( Value.bytes(Data.fromHex("0x0000")), ); }); it("concatenates multiple values preserving byte widths", async () => { expect( - await evaluate({ $concat: ["0xdead", "0xbeef"] }, options), + await evaluate({ "~concat": ["0xdead", "0xbeef"] }, options), ).toEqual(Value.bytes(Data.fromHex("0xdeadbeef"))); }); it("returns empty bytes for an empty operand list", async () => { - expect(await evaluate({ $concat: [] }, options)).toEqual( + expect(await evaluate({ "~concat": [] }, options)).toEqual( Value.bytes(Data.zero()), ); }); it("preserves a single operand unchanged", async () => { - expect(await evaluate({ $concat: ["0xabcdef"] }, options)).toEqual( + expect(await evaluate({ "~concat": ["0xabcdef"] }, options)).toEqual( Value.bytes(Data.fromHex("0xabcdef")), ); }); it("preserves leading zeros in hex literals", async () => { - const result = await evaluate({ $concat: ["0x0001", "0x0002"] }, options); + const result = await evaluate( + { "~concat": ["0x0001", "0x0002"] }, + options, + ); expect(result).toEqual(Value.bytes(Data.fromHex("0x00010002"))); }); it("concatenates bytes-valued variables and resized integers", async () => { const expression: Pointer.Expression = { - $concat: [{ $sized2: "foo" }, "bar", { $sized1: { $sum: [1, 2] } }], + "~concat": [ + { "~sized2": "foo" }, + "bar", + { "~sized1": { "~sum": [1, 2] } }, + ], }; expect(await evaluate(expression, options)).toEqual( @@ -305,48 +312,53 @@ describe("evaluate", () => { it("rejects a JSON number operand", async () => { await expect( - evaluate({ $concat: ["0xdead", 0] }, options), + evaluate({ "~concat": ["0xdead", 0] }, options), ).rejects.toThrow( - "Operand 1 of $concat (0) evaluates to the integer 0, which has no " + - "byte width; give it a width with $wordsized or $sizedN", + "Operand 1 of ~concat (0) evaluates to the integer 0, which has no " + + "byte width; give it a width with ~wordsized or ~sizedN", ); }); it("rejects an integer-valued variable operand", async () => { await expect( - evaluate({ $concat: ["foo", "bar"] }, options), + evaluate({ "~concat": ["foo", "bar"] }, options), ).rejects.toThrow( - 'Operand 0 of $concat ("foo") evaluates to the integer 42', + 'Operand 0 of ~concat ("foo") evaluates to the integer 42', ); }); it("rejects an arithmetic result operand", async () => { await expect( - evaluate({ $concat: [{ $sum: [1, 2] }, "0xff"] }, options), + evaluate({ "~concat": [{ "~sum": [1, 2] }, "0xff"] }, options), ).rejects.toThrow("evaluates to the integer 3"); }); it("rejects an odd-digit hex literal operand", async () => { - await expect(evaluate({ $concat: ["0x1"] }, options)).rejects.toThrow( + await expect(evaluate({ "~concat": ["0x1"] }, options)).rejects.toThrow( "evaluates to the integer 1", ); }); - it("rejects $wordsize and lookups as operands", async () => { + it("rejects ~wordsize and lookups as operands", async () => { await expect( - evaluate({ $concat: ["$wordsize"] }, options), + evaluate({ "~concat": ["~wordsize"] }, options), ).rejects.toThrow("evaluates to the integer 32"); await expect( - evaluate({ $concat: [{ ".slot": "stack" }] }, options), + evaluate({ "~concat": [{ ".slot": "stack" }] }, options), ).rejects.toThrow("evaluates to the integer 42"); }); }); - describe("$keccak256", () => { + describe("~keccak256", () => { it("hashes the concatenation of bytes operands", async () => { const expression: Pointer.Expression = { - $keccak256: [{ $wordsized: "foo" }, "bar", { $sized1: 42 }, "0x1f"], + "~keccak256": [ + { "~wordsized": "foo" }, + "bar", + { "~sized1": 42 }, + "0x1f", + ], }; const preimage = Data.fromNumber(42) @@ -363,7 +375,7 @@ describe("evaluate", () => { }); it("produces 32 bytes", async () => { - const result = await evaluate({ $keccak256: [] }, options); + const result = await evaluate({ "~keccak256": [] }, options); expect(Value.isBytes(result) && result.data.length).toBe(32); expect(result).toEqual( @@ -373,7 +385,7 @@ describe("evaluate", () => { it("hashes a word-sized key and slot over 64 bytes", async () => { const expression: Pointer.Expression = { - $keccak256: [{ $wordsized: "0x1234" }, { $wordsized: 0 }], + "~keccak256": [{ "~wordsized": "0x1234" }, { "~wordsized": 0 }], }; const preimage = Data.fromHex("0x1234").resizeTo(32).concat(word(0)); @@ -385,24 +397,51 @@ describe("evaluate", () => { }); it("rejects a bare integer slot operand", async () => { - // the shape `{ $keccak256: [{ $wordsized: key }, slot] }` with a bare + // the shape `{ "~keccak256": [{ "~wordsized": key }, slot] }` with a bare // integer slot would hash 32 bytes instead of 64 await expect( - evaluate({ $keccak256: [{ $wordsized: "0x1234" }, 0] }, options), + evaluate({ "~keccak256": [{ "~wordsized": "0x1234" }, 0] }, options), ).rejects.toThrow( - "Operand 1 of $keccak256 (0) evaluates to the integer 0, which has " + - "no byte width; give it a width with $wordsized or $sizedN", + "Operand 1 of ~keccak256 (0) evaluates to the integer 0, which has " + + "no byte width; give it a width with ~wordsized or ~sizedN", ); }); it("rejects an integer-valued variable operand", async () => { - await expect(evaluate({ $keccak256: ["foo"] }, options)).rejects.toThrow( - 'Operand 0 of $keccak256 ("foo")', - ); + await expect( + evaluate({ "~keccak256": ["foo"] }, options), + ).rejects.toThrow('Operand 0 of ~keccak256 ("foo")'); }); }); }); +describe("evaluate, with the old $ sigil", () => { + const options: EvaluateOptions = { state, variables: {}, regions: {} }; + + it("rejects a $ operator, with a hint", async () => { + const expression = { $sum: [1, 2] } as unknown as Pointer.Expression; + + await expect(evaluate(expression, options)).rejects.toThrow( + "did you mean `~sum`? (the expression sigil changed from `$` to `~`)", + ); + }); + + it("hints at ~ for a $ constant or reference", async () => { + await expect(evaluate("$wordsize", options)).rejects.toThrow( + 'expression: "$wordsize"; did you mean `~wordsize`?', + ); + await expect(evaluate({ ".offset": "$this" }, options)).rejects.toThrow( + '{".offset":"$this"}; did you mean `~this`?', + ); + }); + + it("gives no hint for other names", async () => { + await expect(evaluate("$balance", options)).rejects.toThrow( + /^Unexpected runtime failure to recognize kind of expression: "\$balance"$/, + ); + }); +}); + describe("Value", () => { it("coerces bytes to a big-endian integer", () => { expect(Value.toInteger(Value.bytes(Data.fromHex("0x0100")))).toBe(256n); diff --git a/packages/pointers/src/evaluate.ts b/packages/pointers/src/evaluate.ts index 310e249f58..6750c13cf9 100644 --- a/packages/pointers/src/evaluate.ts +++ b/packages/pointers/src/evaluate.ts @@ -9,11 +9,11 @@ import { keccak256 } from "ethereum-cryptography/keccak"; * The result of evaluating an expression: one of two sorts of value. * * An **integer** is an unbounded non-negative integer with no width; it - * is produced by JSON-number literals, `$wordsize`, lookups, arithmetic, + * is produced by JSON-number literals, `~wordsize`, lookups, arithmetic, * and odd-digit hex literals. * * **Bytes** are a byte sequence with a definite width; they are produced - * by even-digit hex literals, `$read`, and the resize forms. + * by even-digit hex literals, `~read`, and the resize forms. * * Variables carry the sort of the expression that defined them. */ @@ -124,10 +124,48 @@ export async function evaluate( throw new Error( `Unexpected runtime failure to recognize kind of expression: ${JSON.stringify( expression, - )}`, + )}${sigilHint(...namesIn(expression))}`, ); } +const formerTerm = new RegExp( + "^\\$(wordsize|this|sum|difference|product|quotient|remainder|read|" + + "keccak256|concat|wordsized|sized\\d+)$", +); + +/** + * The strings in an unrecognized expression that may be old `$` terms: the + * expression itself, or an object's keys and string values (as in + * `{ ".offset": "$this" }`) + */ +function namesIn(expression: unknown): string[] { + if (typeof expression === "string") { + return [expression]; + } + if (expression && typeof expression === "object") { + return [ + ...Object.keys(expression), + ...Object.values(expression).filter( + (value): value is string => typeof value === "string", + ), + ]; + } + return []; +} + +/** + * For an error message: if one of `names` is a format term written with + * the old `$` sigil, suggest its `~` spelling. The value still fails; + * `$` is not an alias. + */ +function sigilHint(...names: string[]): string { + const name = names.find((name) => formerTerm.test(name)); + return name + ? `; did you mean \`~${name.slice(1)}\`? ` + + "(the expression sigil changed from `$` to `~`)" + : ""; +} + /** * Evaluate an expression where an integer is expected, coercing bytes */ @@ -139,11 +177,11 @@ async function evaluateInteger( } /** - * Evaluate the operands of a width-sensitive operation (`$concat`, - * `$keccak256`), each of which must evaluate to bytes + * Evaluate the operands of a width-sensitive operation (`~concat`, + * `~keccak256`), each of which must evaluate to bytes */ async function evaluateBytesOperands( - operation: "$concat" | "$keccak256", + operation: "~concat" | "~keccak256", operands: Pointer.Expression[], options: EvaluateOptions, ): Promise { @@ -156,7 +194,7 @@ async function evaluateBytesOperands( [ `Operand ${index} of ${operation} (${JSON.stringify(operand)}) `, `evaluates to the integer ${value.value}, which has no byte `, - `width; give it a width with $wordsized or $sizedN`, + `width; give it a width with ~wordsized or ~sizedN`, ].join(""), ); } @@ -189,7 +227,7 @@ async function evaluateConstant( constant: Pointer.Expression.Constant, ): Promise { switch (constant) { - case "$wordsize": + case "~wordsize": return Value.integer(32n); } } @@ -220,21 +258,21 @@ async function evaluateArithmetic( ); switch (operation) { - case "$sum": + case "~sum": return Value.integer(operands.reduce((sum, value) => sum + value, 0n)); - case "$difference": { + case "~difference": { const [a, b] = operands; return Value.integer(a > b ? a - b : 0n); } - case "$product": + case "~product": return Value.integer( operands.reduce((product, value) => product * value, 1n), ); - case "$quotient": { + case "~quotient": { const [a, b] = operands; return Value.integer(a / b); } - case "$remainder": { + case "~remainder": { const [a, b] = operands; return Value.integer(a % b); } @@ -248,8 +286,8 @@ async function evaluateKeccak256( options: EvaluateOptions, ): Promise { const operands = await evaluateBytesOperands( - "$keccak256", - expression.$keccak256, + "~keccak256", + expression["~keccak256"], options, ); @@ -263,8 +301,8 @@ async function evaluateConcat( options: EvaluateOptions, ): Promise { const operands = await evaluateBytesOperands( - "$concat", - expression.$concat, + "~concat", + expression["~concat"], options, ); @@ -278,7 +316,7 @@ async function evaluateResize( const [[operation, subexpression]] = Object.entries(expression); const newLength = Pointer.Expression.Resize.isToNumber(expression) - ? Number(operation.match(/^\$sized([1-9]+[0-9]*)$/)![1]) + ? Number(operation.match(/^~sized([1-9]+[0-9]*)$/)![1]) : 32; const value = await evaluate(subexpression, options); @@ -318,7 +356,7 @@ async function evaluateRead( ): Promise { const { state: _state, regions } = options; - const identifier = expression.$read; + const identifier = expression["~read"]; const region = regions[identifier]; if (!region) { throw new Error(`Region not found: ${identifier}`); diff --git a/packages/pointers/src/read.test.ts b/packages/pointers/src/read.test.ts index 8cee2f9c28..5824ed3911 100644 --- a/packages/pointers/src/read.test.ts +++ b/packages/pointers/src/read.test.ts @@ -288,7 +288,7 @@ describe("read (segment carry)", () => { ), ); - it("treats offset $wordsize as byte 0 of the next slot", async () => { + it("treats offset ~wordsize as byte 0 of the next slot", async () => { const region: Cursor.Region = { location: "storage", slot: Data.fromNumber(0), @@ -306,7 +306,7 @@ describe("read (segment carry)", () => { expect(result).toEqual(bytes([0xa1, 4])); }); - it("carries an offset beyond $wordsize into a later slot", async () => { + it("carries an offset beyond ~wordsize into a later slot", async () => { const region: Cursor.Region = { location: "storage", slot: Data.fromNumber(5), diff --git a/packages/pointers/src/read.ts b/packages/pointers/src/read.ts index 2dfed67a6e..05784be47b 100644 --- a/packages/pointers/src/read.ts +++ b/packages/pointers/src/read.ts @@ -95,8 +95,8 @@ const wordsize = 32n; * words) and `length` may run across word boundaries (concatenating * sequentially-addressed words). * - * Given offset `n`, the segment begins at byte `n mod $wordsize` of the - * word `floor(n / $wordsize)` words after the one specified. When `length` + * Given offset `n`, the segment begins at byte `n mod ~wordsize` of the + * word `floor(n / ~wordsize)` words after the one specified. When `length` * is omitted, the segment ends at the end of the word in which it begins. * * `readWord(carry, slice)` reads bytes from the word `carry` words after diff --git a/packages/pointers/src/segment.examples.test.ts b/packages/pointers/src/segment.examples.test.ts index ea2dc77947..834b5f6a75 100644 --- a/packages/pointers/src/segment.examples.test.ts +++ b/packages/pointers/src/segment.examples.test.ts @@ -60,10 +60,10 @@ describe("segment schema examples (packed)", () => { 12, 8, { - $difference: ["$wordsize", { ".length": "$this" }], + "~difference": ["~wordsize", { ".length": "~this" }], }, { - $difference: ["$wordsize", { $sum: [20, { ".length": "$this" }] }], + "~difference": ["~wordsize", { "~sum": [20, { ".length": "~this" }] }], }, ]); }); diff --git a/packages/programs-react/CHANGELOG.md b/packages/programs-react/CHANGELOG.md index d647bac580..c6d1810a91 100644 --- a/packages/programs-react/CHANGELOG.md +++ b/packages/programs-react/CHANGELOG.md @@ -31,6 +31,10 @@ root [`CHANGELOG.md`](../../CHANGELOG.md). `frame`) compose into one; an entry without a declaration stays apart ([#342]). +### Breaking + +- Follows the `~` vocabulary of `@ethdebug/pointers` ([#323]). + ## 0.1.0-preview.1 — 2026-10-03 Updated `@ethdebug/format` to `0.1.0-draft.1` and `@ethdebug/pointers` to @@ -62,5 +66,6 @@ First publication. [#298]: https://github.com/ethdebug/format/pull/298 [#299]: https://github.com/ethdebug/format/pull/299 [#300]: https://github.com/ethdebug/format/pull/300 +[#323]: https://github.com/ethdebug/format/pull/323 [#342]: https://github.com/ethdebug/format/pull/342 [#349]: https://github.com/ethdebug/format/pull/349 diff --git a/packages/web/docs/concepts/pointers.mdx b/packages/web/docs/concepts/pointers.mdx index 8b0025b9ad..0bab12659c 100644 --- a/packages/web/docs/concepts/pointers.mdx +++ b/packages/web/docs/concepts/pointers.mdx @@ -43,7 +43,7 @@ Complex pointers describe dynamic locations that depend on runtime values: {`{ "location": "storage", "slot": { - "$keccak256": [{ "$read": "key-value" }, { "$wordsized": 3 }] + "~keccak256": [{ "~read": "key-value" }, { "~wordsized": 3 }] } }`} @@ -227,18 +227,18 @@ mapping values, and many other locations depend on runtime values. {`{ "location": "storage", "slot": { - "$sum": [{ "$keccak256": [{ "$wordsized": 5 }] }, "element-index"] + "~sum": [{ "~keccak256": [{ "~wordsized": 5 }] }, "element-index"] } }`} Expressions support: -- **Arithmetic**: `$sum`, `$difference`, `$product`, `$quotient`, `$remainder` -- **Reading values**: `$read` retrieves bytes from a named region +- **Arithmetic**: `~sum`, `~difference`, `~product`, `~quotient`, `~remainder` +- **Reading values**: `~read` retrieves bytes from a named region - **Region properties**: `.offset`, `.length`, `.slot` reference region fields -- **Hashing**: `$keccak256` computes storage slots for dynamic data -- **Data manipulation**: `$concat`, `$sized`, `$wordsized` +- **Hashing**: `~keccak256` computes storage slots for dynamic data +- **Data manipulation**: `~concat`, `~sized`, `~wordsized` Variables in expressions come from list iteration (`each`) or scope definitions (`define`). @@ -276,7 +276,7 @@ Giving a region a `name` lets you reference it elsewhere: }, { "location": "memory", - "offset": { "$read": "base-pointer" }, + "offset": { "~read": "base-pointer" }, "length": 32 } ] diff --git a/packages/web/docs/core-schemas/pointers/collections.mdx b/packages/web/docs/core-schemas/pointers/collections.mdx index 689e4bbdf3..f68688a9fa 100644 --- a/packages/web/docs/core-schemas/pointers/collections.mdx +++ b/packages/web/docs/core-schemas/pointers/collections.mdx @@ -58,7 +58,7 @@ identifiers with types and pointers). See the The list evaluates `count` to determine how many pointers to generate, then for each index (bound to the variable named by `each`), it evaluates the `is` -pointer template. The `count` can itself come from storage, read via `$read`, +pointer template. The `count` can itself come from storage, read via `~read`, so the number of elements is determined at runtime. ### List properties diff --git a/packages/web/docs/core-schemas/pointers/expressions.mdx b/packages/web/docs/core-schemas/pointers/expressions.mdx index 8ce19fb53b..b1a0a10f19 100644 --- a/packages/web/docs/core-schemas/pointers/expressions.mdx +++ b/packages/web/docs/core-schemas/pointers/expressions.mdx @@ -34,19 +34,19 @@ base and the index. ## Integers and bytes -Expressions produce two kinds of value. Arithmetic (`$sum`, `$product`, and +Expressions produce two kinds of value. Arithmetic (`~sum`, `~product`, and so on) is unbounded integer math: the result is a number with no byte width. -`$read`, hexadecimal literals with an even number of digits, and the resize +`~read`, hexadecimal literals with an even number of digits, and the resize operations produce **bytes** of a definite width. -The distinction matters for hashing and concatenation. `$keccak256` and -`$concat` work on bytes, and their results depend on how wide each operand +The distinction matters for hashing and concatenation. `~keccak256` and +`~concat` work on bytes, and their results depend on how wide each operand is, so every operand must already be bytes. A bare number like `5` or an -arithmetic result is not; wrap it in `$wordsized` (or `$sized`) first. +arithmetic result is not; wrap it in `~wordsized` (or `~sized`) first. That applies to mapping keys _and_ to slot numbers before hashing: - {`{ "$keccak256": [{ "$wordsized": "key" }, { "$wordsized": "slot" }] }`} + {`{ "~keccak256": [{ "~wordsized": "key" }, { "~wordsized": "slot" }] }`} Resizes are the only way to turn an integer into bytes; nothing pads @@ -59,33 +59,33 @@ a slot or added to. Basic math operations for computing addresses: -### `$sum` — Addition +### `~sum` — Addition Adds all values in an array. -### `$difference` — Subtraction +### `~difference` — Subtraction Subtracts the second value from the first (saturates at zero). -### `$product` — Multiplication +### `~product` — Multiplication Multiplies all values in an array. -### `$quotient` — Division +### `~quotient` — Division Integer division of the first value by the second. -### `$remainder` — Modulo +### `~remainder` — Modulo Remainder after division. ## Reading values -### `$read` — Read from a named region +### `~read` — Read from a named region Reads the bytes from a previously defined region; the result is bytes as wide as the region. For example, a group can name an `array-length-slot` -region and then use `{ $read: "array-length-slot" }` to retrieve the array's +region and then use `{ "~read": "array-length-slot" }` to retrieve the array's length at runtime and use it in a later computation. ## Region property lookups @@ -122,7 +122,7 @@ Property lookups can compute the next element's position from the previous one. For example, `element-1`'s offset can be computed as the sum of `element-0`'s `.offset` and its `.length`. -## Computing storage slots with `$keccak256` +## Computing storage slots with `~keccak256` Solidity uses keccak256 hashing to compute storage locations for dynamic data. @@ -130,12 +130,12 @@ Solidity uses keccak256 hashing to compute storage locations for dynamic data. For a dynamic array at slot `n`, elements start at `keccak256(n)`, so element `i` lives at `keccak256(n) + i`. The slot is word-sized before hashing, and -the 32-byte hash is then read as an integer by `$sum`: +the 32-byte hash is then read as an integer by `~sum`: {`{ - "$sum": [ - { "$keccak256": [{ "$wordsized": 5 }] }, + "~sum": [ + { "~keccak256": [{ "~wordsized": 5 }] }, "element-index" ] }`} @@ -147,7 +147,7 @@ For a mapping at slot `n`, the value for key `k` is at `keccak256(k, n)`, with both the key and the slot word-sized: - {`{ "$keccak256": [{ "$wordsized": "key" }, { "$wordsized": 3 }] }`} + {`{ "~keccak256": [{ "~wordsized": "key" }, { "~wordsized": 3 }] }`} ### Nested mappings @@ -158,28 +158,28 @@ bytes and needs no resize; the keys and the literal slot do: {`{ - "$keccak256": [ - { "$wordsized": "inner-key" }, - { "$keccak256": [{ "$wordsized": "outer-key" }, { "$wordsized": 2 }] } + "~keccak256": [ + { "~wordsized": "inner-key" }, + { "~keccak256": [{ "~wordsized": "outer-key" }, { "~wordsized": 2 }] } ] }`} ## Data manipulation -### `$concat` — Concatenate bytes +### `~concat` — Concatenate bytes Joins byte sequences without padding. Every operand must be bytes. Useful for building hash inputs from multiple values. -### `$sized` — resize to N bytes +### `~sized` — resize to N bytes Truncates or pads to exactly N bytes. Pads with zeros on the left; truncates from the left if too long. -### `$wordsized` — Resize to word size +### `~wordsized` — Resize to word size -Equivalent to `$sized32` on the EVM (pads or truncates to 32 bytes). +Equivalent to `~sized32` on the EVM (pads or truncates to 32 bytes). ## Variables in expressions diff --git a/packages/web/docs/core-schemas/pointers/regions.mdx b/packages/web/docs/core-schemas/pointers/regions.mdx index c1e9d95fb9..a0cc121671 100644 --- a/packages/web/docs/core-schemas/pointers/regions.mdx +++ b/packages/web/docs/core-schemas/pointers/regions.mdx @@ -35,7 +35,7 @@ slots. Regions use `slot`, and may narrow or extend the byte range with `offset` and `length`, both measured in bytes relative to the start of `slot`. A segment may begin at any non-negative `offset`; an offset at or past -`$wordsize` carries into later slots, so byte `$wordsize` of slot `p` is byte +`~wordsize` carries into later slots, so byte `~wordsize` of slot `p` is byte `0` of slot `p + 1`. Its `length` may likewise run past the end of the slot, in which case the segment is the concatenation of consecutive slots. When `length` is omitted, the segment ends at the end of the slot in which it @@ -88,7 +88,7 @@ same slot-based addressing as regular storage. Any region can have a `name` that other parts of the pointer reference. Names enable: -- Reading the region's value with `{ "$read": "token-balance" }` +- Reading the region's value with `{ "~read": "token-balance" }` - Referencing properties with `{ ".slot": "token-balance" }` - Building self-documenting pointer structures @@ -98,7 +98,7 @@ Region fields like `offset`, `slot`, and `length` can use expressions to compute values at runtime. This enables pointers for dynamic data like arrays and mappings. -For the full expression language including arithmetic, `$keccak256`, and value +For the full expression language including arithmetic, `~keccak256`, and value reading, see [expressions](./expressions). ## Learn more diff --git a/packages/web/docs/explore/pointer-playground.mdx b/packages/web/docs/explore/pointer-playground.mdx index 079bc7d2e4..e1c1eb23b5 100644 --- a/packages/web/docs/explore/pointer-playground.mdx +++ b/packages/web/docs/explore/pointer-playground.mdx @@ -19,7 +19,9 @@ export const arrayElementPointer = { in: { name: "element", location: "storage", - slot: { $sum: [{ $keccak256: [{ $wordsized: 5 }] }, "element-index"] }, + slot: { + "~sum": [{ "~keccak256": [{ "~wordsized": 5 }] }, "element-index"], + }, }, }; @@ -29,7 +31,7 @@ export const mappingValuePointer = { { name: "value", location: "storage", - slot: { $keccak256: [{ $read: "key-region" }, { $wordsized: 3 }] }, + slot: { "~keccak256": [{ "~read": "key-region" }, { "~wordsized": 3 }] }, }, ], }; @@ -46,10 +48,10 @@ export const solidityStringPointer = { length: 1, }, { - if: { $remainder: [{ $sum: [{ $read: "length-flag" }, 1] }, 2] }, + if: { "~remainder": [{ "~sum": [{ "~read": "length-flag" }, 1] }, 2] }, then: { define: { - "string-length": { $quotient: [{ $read: "length-flag" }, 2] }, + "string-length": { "~quotient": [{ "~read": "length-flag" }, 2] }, }, in: { name: "string", @@ -71,15 +73,20 @@ export const solidityStringPointer = { { define: { "string-length": { - $quotient: [ - { $difference: [{ $read: "long-string-length-data" }, 1] }, + "~quotient": [ + { + "~difference": [ + { "~read": "long-string-length-data" }, + 1, + ], + }, 2, ], }, - "start-slot": { $keccak256: [{ $wordsized: "slot" }] }, + "start-slot": { "~keccak256": [{ "~wordsized": "slot" }] }, "total-slots": { - $quotient: [ - { $sum: ["string-length", { $difference: [32, 1] }] }, + "~quotient": [ + { "~sum": ["string-length", { "~difference": [32, 1] }] }, 32, ], }, @@ -90,14 +97,14 @@ export const solidityStringPointer = { each: "i", is: { define: { - "current-slot": { $sum: ["start-slot", "i"] }, - "previous-length": { $product: ["i", 32] }, + "current-slot": { "~sum": ["start-slot", "i"] }, + "previous-length": { "~product": ["i", 32] }, }, in: { if: { - $difference: [ + "~difference": [ "string-length", - { $sum: ["previous-length", 32] }, + { "~sum": ["previous-length", 32] }, ], }, then: { @@ -111,7 +118,7 @@ export const solidityStringPointer = { slot: "current-slot", offset: 0, length: { - $difference: ["string-length", "previous-length"], + "~difference": ["string-length", "previous-length"], }, }, }, @@ -319,6 +326,6 @@ across that slot and the next. - [Collections](/docs/core-schemas/pointers/collections) for groups, lists, and conditionals - [Expressions](/docs/core-schemas/pointers/expressions) for arithmetic, - reads, and `$keccak256` + reads, and `~keccak256` diff --git a/packages/web/docs/getting-started/for-compiler-authors.mdx b/packages/web/docs/getting-started/for-compiler-authors.mdx index 545ddeaaaa..e82e5fd9de 100644 --- a/packages/web/docs/getting-started/for-compiler-authors.mdx +++ b/packages/web/docs/getting-started/for-compiler-authors.mdx @@ -84,12 +84,12 @@ For a dynamic array where the length is at slot 2 and elements start at { "name": "array-length", "location": "storage", "slot": "0x2" }, { "list": { - "count": { "$read": "array-length" }, + "count": { "~read": "array-length" }, "each": "i", "is": { "location": "storage", "slot": { - "$sum": [{ "$keccak256": [{ "$wordsized": "0x2" }] }, "i"] + "~sum": [{ "~keccak256": [{ "~wordsized": "0x2" }] }, "i"] } } } @@ -98,6 +98,10 @@ For a dynamic array where the length is at slot 2 and elements start at }`} +Operators and constants such as `~read`, `~sum` and `~wordsize` start with +`~`. Names that you choose, such as `array-length` and `i` here, never start +with `~`. + ## Integration approach Most compilers can add **ethdebug/format** support incrementally: diff --git a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx index d25d5cca85..b5b4eeb67a 100644 --- a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx +++ b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx @@ -178,14 +178,14 @@ and length are computed while the program runs: "name": "text-data", "location": "calldata", "offset": { - "$quotient": [ - { "$read": "text" }, + "~quotient": [ + { "~read": "text" }, "0x100000000000000000000000000000000" ] }, "length": { - "$remainder": [ - { "$read": "text" }, + "~remainder": [ + { "~read": "text" }, "0x100000000000000000000000000000000" ] } @@ -321,9 +321,9 @@ a slot of its own. "is": { "name": "element", "location": "storage", - "slot": { "$sum": [0, { "$quotient": ["i", 32] }] }, + "slot": { "~sum": [0, { "~quotient": ["i", 32] }] }, "offset": { - "$difference": [31, { "$product": [{ "$remainder": ["i", 32] }, 1] }] + "~difference": [31, { "~product": [{ "~remainder": ["i", 32] }, 1] }] }, "length": 1 } @@ -345,12 +345,12 @@ For dynamic arrays, BUG generates pointers with keccak256 expressions: { "name": "array-length", "location": "storage", "slot": 0 }, { "list": { - "count": { "$read": "array-length" }, + "count": { "~read": "array-length" }, "each": "i", "is": { "name": "element", "location": "storage", - "slot": { "$sum": [{ "$keccak256": [{ "$wordsized": 0 }] }, "i"] } + "slot": { "~sum": [{ "~keccak256": [{ "~wordsized": 0 }] }, "i"] } } } } diff --git a/packages/web/docs/implementation-guides/pointers/dereference-logic/making-regions-concrete.mdx b/packages/web/docs/implementation-guides/pointers/dereference-logic/making-regions-concrete.mdx index 36fac29961..e12a2e677c 100644 --- a/packages/web/docs/implementation-guides/pointers/dereference-logic/making-regions-concrete.mdx +++ b/packages/web/docs/implementation-guides/pointers/dereference-logic/making-regions-concrete.mdx @@ -34,7 +34,7 @@ expression properties and converts them into their `Data` values. This process would be very straightforward, except that pointer expressions may reference the region in which they are specified by use of the special -region identifier `"$this"`. +region identifier `"~this"`. Fortunately, the schema does not allow any kind of circular reference, so a more robust implementation could pre-process a region's properties to detect @@ -45,14 +45,14 @@ might take this pointer: {`{ "location": "memory", "offset": { - "$sum": [0x60, { ".length": "$this" }] + "~sum": [0x60, { ".length": "~this" }] }, - "length": "$wordsize" + "length": "~wordsize" }`} diff --git a/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx b/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx index acbe87d586..cd639362c0 100644 --- a/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx +++ b/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx @@ -15,11 +15,11 @@ if variable and region references are pre-evaluated. The schema defines every expression to evaluate to one of two sorts of value: an **integer**, which has a numeric value but no width, or **bytes**, -which have a definite width. The distinction matters because `$keccak256` -and `$concat` produce results that depend on the widths of their operands, +which have a definite width. The distinction matters because `~keccak256` +and `~concat` produce results that depend on the widths of their operands, so the schema requires those operands to be bytes. A bare integer (a JSON -number, `$wordsize`, an arithmetic result, or a lookup) must first be given -a width with `$sized` or `$wordsized`; the resize forms are the only +number, `~wordsize`, an arithmetic result, or a lookup) must first be given +a width with `~sized` or `~wordsized`; the resize forms are the only bridge from an integer to bytes. This reference implementation represents the two sorts as a tagged union: @@ -74,7 +74,7 @@ specific logic based on the kind of expression: ## Evaluating constants, literals, and variables -Evaluating constant expressions is quite straightforward; `$wordsize` is the +Evaluating constant expressions is quite straightforward; `~wordsize` is the integer 32: sourceFile.getFunction("evaluateArithmetic")} /> -**Note** how `$difference` operates on unsigned values only by bounding the -result below at 0, and how `$quotient` uses integer division only. +**Note** how `~difference` operates on unsigned values only by bounding the +result below at 0, and how `~quotient` uses integer division only. ## Evaluating resize expressions -This schema provides the `{ "$sized": }` and -`{ "$wordsized": }` constructs to allow explicitly resizing a +This schema provides the `{ "~sized": }` and +`{ "~wordsized": }` constructs to allow explicitly resizing a subexpression. A resize always produces bytes of the requested width, and so is the way to give an integer a width; this implementation encodes an integer as its minimal big-endian bytes and then uses the @@ -238,18 +238,18 @@ this function presents no surprises: extract={(sourceFile) => sourceFile.getFunction("evaluateRead")} /> -## Note on `"$this"` region lookups +## Note on `"~this"` region lookups Astute readers might notice that these docs contain no mention until now about how to implement support for expressions that reference the region in which they are defined, a mechanism the schema permits via the special region -name identifier `"$this"`. +name identifier `"~this"`. -Performing _read_ operations against `"$this"` region is meaningless since +Performing _read_ operations against `"~this"` region is meaningless since this schema does not afford any mechanism for defining regions recursively down to a base case (or similar composition). Thus, the only syntactic construct for self-referential reads resembles, e.g., defining a storage region -whose `slot` is `{ $read: "$this" }`. Evaluating this `slot` would require +whose `slot` is `{ "~read": "~this" }`. Evaluating this `slot` would require knowing the slot before knowing where to read, and knowing the slow requires knowing the machine value, ad nauseum. @@ -261,9 +261,9 @@ Since the `evaluate<.*>()` functions here are written to accept only one expression at a time, this reference implementation relegates this concern to a higher-level module; proper use of `evaluate()` here requires its `options.regions` map to include a pre-evaluated (albeit partial) -`"$this"` region. +`"~this"` region. -The logic for creating `"$this"` regions and calling `evaluate()` correctly +The logic for creating `"~this"` regions and calling `evaluate()` correctly is described in the section pertaining to that area of the code. Be forewarned that this reference implementation takes a naïve trial-and-error approach for determining property evaluation order; implementations requiring diff --git a/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx b/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx index 69131bf0ae..8742b10dc2 100644 --- a/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx +++ b/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx @@ -46,7 +46,7 @@ See these quick examples to get a sense for this part of the type hierarchy: import { Pointer } from "@ethdebug/format"; const expression: Pointer.Expression = { - $sum: [0, 1], + "~sum": [0, 1], }; Pointer.Expression.isKeccak256(expression); // false diff --git a/packages/web/docs/reference/glossary.mdx b/packages/web/docs/reference/glossary.mdx index 2715d75145..fe5880412f 100644 --- a/packages/web/docs/reference/glossary.mdx +++ b/packages/web/docs/reference/glossary.mdx @@ -133,10 +133,10 @@ A computation that produces a bytes value when evaluated against machine state. Expressions enable pointers to describe dynamic data locations. Common expression types include: -- **Arithmetic**: `$sum`, `$difference`, `$product`, `$quotient`, `$remainder` -- **Hashing**: `$keccak256` (for computing storage slots) +- **Arithmetic**: `~sum`, `~difference`, `~product`, `~quotient`, `~remainder` +- **Hashing**: `~keccak256` (for computing storage slots) - **Lookups**: `.offset`, `.length`, `.slot` (reading from defined regions) -- **References**: `$this`, variables by name +- **References**: `~this`, variables by name ### Variable (in pointers) @@ -151,7 +151,7 @@ allow pointers to name and reuse intermediate computations: "define": { "baseSlot": 3 }, "in": { "location": "storage", - "slot": { "$sum": ["baseSlot", 1] } + "slot": { "~sum": ["baseSlot", 1] } } }`} diff --git a/packages/web/spec/pointer/concepts.mdx b/packages/web/spec/pointer/concepts.mdx index 4e41e1c702..a3cc05030e 100644 --- a/packages/web/spec/pointer/concepts.mdx +++ b/packages/web/spec/pointer/concepts.mdx @@ -121,7 +121,7 @@ explicit EVM lookup, and a few builtin operations. { "location": "memory", "offset": { - "$sum": [1, 1] + "~sum": [1, 1] }, "length": 1 } @@ -153,7 +153,7 @@ known at some point in execution to be the value at the top of the stack. "name": "storage-slot", "location": "storage", "slot": { - "$read": "pointer-to-storage-slot" + "~read": "pointer-to-storage-slot" } } ] @@ -185,7 +185,7 @@ and length. "name": "record-x", "location": "memory", "offset": { - "$read": "record-pointer" + "~read": "record-pointer" }, "length": 32 }, @@ -193,7 +193,7 @@ and length. "name": "record-y", "location": "memory", "offset": { - "$sum": [{ ".offset": "record-x" }, { ".length": "record-x" }] + "~sum": [{ ".offset": "record-x" }, { ".length": "record-x" }] }, "length": 32 } @@ -240,16 +240,16 @@ item's index in the list, and the underlying pointer for the item itself. }, { "list": { - "count": { "$read": "array-count" }, + "count": { "~read": "array-count" }, "each": "item-index", "is": { "name": "array-item", "location": "memory", "offset": { - "$sum": [ + "~sum": [ { ".offset": "array-count" }, { ".length": "array-count" }, - "$product": [ + "~product": [ "item-index", { ".length": "array-item" } ] diff --git a/packages/web/spec/pointer/expression.mdx b/packages/web/spec/pointer/expression.mdx index 88c253e8e9..c64a2add7a 100644 --- a/packages/web/spec/pointer/expression.mdx +++ b/packages/web/spec/pointer/expression.mdx @@ -15,19 +15,19 @@ hashed to derive them. ## Integers and bytes Every expression evaluates to a value of one of two sorts. An **integer** is -an unbounded, non-negative number with no width: JSON numbers, `$wordsize`, +an unbounded, non-negative number with no width: JSON numbers, `~wordsize`, variables, lookups, arithmetic results, and odd-digit hexadecimal literals are integers. **Bytes** are a finite sequence with a definite width: -even-digit hexadecimal literals, `$read`, `$keccak256`, `$concat`, and the +even-digit hexadecimal literals, `~read`, `~keccak256`, `~concat`, and the resize forms produce bytes. Where an integer is expected (arithmetic operands, a list `count`, a segment's `slot`, `offset`, or `length`), a bytes value is read as the non-negative integer its bytes encode, big-endian. Where bytes are expected -(the operands of `$concat` and `$keccak256`, whose results depend on operand +(the operands of `~concat` and `~keccak256`, whose results depend on operand widths), the operand **must** be width-bearing; a bare integer there is -invalid and **must** first be given a width with `$sized` or -`$wordsized`. There is no implicit widening: the resize forms are the only +invalid and **must** first be given a width with `~sized` or +`~wordsized`. There is no implicit widening: the resize forms are the only bridge from an integer to bytes. ## Literal values @@ -56,6 +56,10 @@ resize form. An expression can be a string value equal to the identifier for a known scalar variable introduced by some pointer representation. +The format's own terms start with `~` (`~wordsize`, `~this`, `~sum`, and so +on). Names of variables, regions and templates never start with `~`, so a +name can never be confused with one of these terms. + For an example where scalar variables may appear, see the [List collection schema](/spec/pointer/collection/list). @@ -89,7 +93,7 @@ that value is an **integer**. ## Reading from the EVM -An expression can be an object of the form `{ "$read": "" }`, where +An expression can be an object of the form `{ "~read": "" }`, where `` references a particular region defined in some root pointer. The value of such an expression is the concatenation of bytes present in the @@ -103,7 +107,7 @@ region: **bytes** whose width is the length of that region. ## Keccak256 hashes -An expression can be an object of form `{ "$keccak256": [...] }`, indicating +An expression can be an object of form `{ "~keccak256": [...] }`, indicating that the value of the expression is a Solidity-style, tightly-packed keccak256 hash of the concatenation of bytes specified by the list. Each operand **must** be bytes, and the result is **bytes** of width 32. @@ -115,7 +119,7 @@ hash of the concatenation of bytes specified by the list. Each operand ## Bytes concatenation -An expression can be an object of form `{ "$concat": [...] }`, indicating +An expression can be an object of form `{ "~concat": [...] }`, indicating that the value of the expression is the concatenation of bytes from each value in the list. Each operand **must** be bytes; the byte width of each operand is preserved, no padding is added or removed between operands, and @@ -144,11 +148,11 @@ bytes. ## Region references Regions can be referenced either by name (which **must** be a defined region), -or by use of the literal string value `"$this"` (which indicates that the +or by use of the literal string value `"~this"` (which indicates that the referenced region is the region containing the expression itself). In cases where an expression is used outside the context of a particular -region definition, the use of `"$this"` is **prohibited**. +region definition, the use of `"~this"` is **prohibited**. Individual properties **may not** be defined with any reference to themselves. Properties also **may not** be defined in terms of mutual reference to each diff --git a/schemas/info.schema.yaml b/schemas/info.schema.yaml index f3b532b86f..b49dd47c12 100644 --- a/schemas/info.schema.yaml +++ b/schemas/info.schema.yaml @@ -175,16 +175,16 @@ examples: length: 1 - name: "beneficiary" location: "storage" - slot: { "$sum": ["slot", 1] } + slot: { "~sum": ["slot", 1] } - name: "asset" location: "storage" - slot: { "$sum": ["slot", 2] } + slot: { "~sum": ["slot", 2] } - name: "amount" location: "storage" - slot: { "$sum": ["slot", 3] } + slot: { "~sum": ["slot", 3] } - name: "canRemit" location: "storage" - slot: { "$sum": ["slot", 4] } + slot: { "~sum": ["slot", 4] } programs: - contract: diff --git a/schemas/info/resources.schema.yaml b/schemas/info/resources.schema.yaml index 686f9742db..868fa2efad 100644 --- a/schemas/info/resources.schema.yaml +++ b/schemas/info/resources.schema.yaml @@ -95,7 +95,7 @@ examples: location: storage slot: contract_variable_slot__struct__Coordinate__storage offset: - $sum: + ~sum: - .offset: member__x__struct__Coordinate__storage - .length: member__x__struct__Coordinate__storage length: 16 diff --git a/schemas/pointer.schema.yaml b/schemas/pointer.schema.yaml index fbf8c0f9de..c87aefb2bc 100644 --- a/schemas/pointer.schema.yaml +++ b/schemas/pointer.schema.yaml @@ -40,15 +40,15 @@ examples: - name: "array-count" location: memory offset: - $read: "array-start" - length: $wordsize + ~read: "array-start" + length: ~wordsize # thirdly, declare a sub-pointer that is a dynamic list whose size is # indicated by the value at "array-count", where each "item-index" # corresponds to a discrete "array-item" region - list: count: - $read: "array-count" + ~read: "array-count" each: "item-index" is: name: "array-item" @@ -57,13 +57,13 @@ examples: # array items are positioned so that the item with index 0 # immediately follows "array-count", and each subsequent item # immediately follows the previous. - $sum: + ~sum: - .offset: "array-count" - .length: "array-count" - - $product: + - ~product: - "item-index" - - .length: $this - length: $wordsize + - .length: ~this + length: ~wordsize - # example `struct Record { uint8 x; uint8 y; bytes4 salt; }` in storage # @@ -81,7 +81,7 @@ examples: location: storage slot: "struct-storage-contract-variable-slot" offset: - $difference: ["previous", "size"] + ~difference: ["previous", "size"] length: "size" in: define: @@ -92,7 +92,7 @@ examples: - name: "packing-begin" location: storage slot: "struct-storage-contract-variable-slot" - offset: $wordsize + offset: ~wordsize length: 0 - define: { previous: { .offset: "packing-begin" }, size: 1 } @@ -123,15 +123,15 @@ examples: - name: "array-count" location: memory offset: - $read: "array-start" - length: $wordsize + ~read: "array-start" + length: ~wordsize # declare this to include a list of pointers of size indicated by the # value at "array-count", where each "item-index" corresponds to a # group of pointers - list: count: - $read: "array-count" + ~read: "array-count" each: "item-index" is: group: @@ -141,30 +141,30 @@ examples: - name: "struct-pointer" location: memory offset: - $sum: + ~sum: - .offset: "array-count" - .length: "array-count" - - $product: + - ~product: - "item-index" - - .length: $this - length: $wordsize + - .length: ~this + length: ~wordsize # following that pointer leads to the region corresponding to # the first member of the struct - name: "struct-member-0" location: memory offset: - $read: "struct-pointer" - length: $wordsize + ~read: "struct-pointer" + length: ~wordsize # the second struct member immediately follows the first - name: "struct-member-1" location: memory offset: - $sum: + ~sum: - .offset: "struct-member-0" - .length: "struct-member-0" - length: $wordsize + length: ~wordsize - # example `string storage` allocation define: @@ -176,15 +176,15 @@ examples: location: storage slot: "string-storage-contract-variable-slot" offset: - $difference: [$wordsize, 1] + ~difference: [~wordsize, 1] length: 1 # define the region representing the string data itself conditionally # based on odd or even length data - if: - $remainder: - - $sum: - - $read: "length-flag" + ~remainder: + - ~sum: + - ~read: "length-flag" - 1 - 2 @@ -192,7 +192,7 @@ examples: then: define: "string-length": - $quotient: [{ $read: "length-flag" }, 2] + ~quotient: [{ ~read: "length-flag" }, 2] in: name: "string" location: storage @@ -208,26 +208,26 @@ examples: location: storage slot: "string-storage-contract-variable-slot" offset: 0 - length: $wordsize + length: ~wordsize - define: "string-length": - $quotient: - - $difference: - - $read: "long-string-length-data" + ~quotient: + - ~difference: + - ~read: "long-string-length-data" - 1 - 2 "start-slot": - $keccak256: - - $wordsized: "string-storage-contract-variable-slot" + ~keccak256: + - ~wordsized: "string-storage-contract-variable-slot" "total-slots": # account for both zero and nonzero slot remainders by adding - # $wordsize-1 to the length before dividing - $quotient: - - $sum: ["string-length", { $difference: [$wordsize, 1] }] - - $wordsize + # ~wordsize-1 to the length before dividing + ~quotient: + - ~sum: ["string-length", { ~difference: [~wordsize, 1] }] + - ~wordsize in: list: count: "total-slots" @@ -235,17 +235,17 @@ examples: is: define: "current-slot": - $sum: ["start-slot", "i"] + ~sum: ["start-slot", "i"] "previous-length": - $product: ["i", $wordsize] + ~product: ["i", ~wordsize] in: # conditional based on whether this is the last slot: # is the string length longer than the previous length # plus this whole slot? if: - $difference: + ~difference: - "string-length" - - $sum: ["previous-length", "$wordsize"] + - ~sum: ["previous-length", "~wordsize"] then: # include the whole slot name: "string" @@ -258,7 +258,7 @@ examples: slot: "current-slot" offset: 0 length: - $difference: ["string-length", "previous-length"] + ~difference: ["string-length", "previous-length"] - # example `string storage` (long form) as a single multi-slot region # @@ -276,19 +276,19 @@ examples: location: storage slot: "string-storage-slot" offset: 0 - length: $wordsize + length: ~wordsize - define: "string-length": - $quotient: - - $difference: - - $read: "long-string-length-data" + ~quotient: + - ~difference: + - ~read: "long-string-length-data" - 1 - 2 "start-slot": - $keccak256: - - $wordsized: "string-storage-slot" + ~keccak256: + - ~wordsized: "string-storage-slot" in: name: "string" location: storage diff --git a/schemas/pointer/collection/group.schema.yaml b/schemas/pointer/collection/group.schema.yaml index 5be6972419..27ef8e2e3a 100644 --- a/schemas/pointer/collection/group.schema.yaml +++ b/schemas/pointer/collection/group.schema.yaml @@ -22,5 +22,5 @@ examples: slot: 0 - location: memory offset: - $read: "data-pointer" + ~read: "data-pointer" length: 32 diff --git a/schemas/pointer/collection/scope.schema.yaml b/schemas/pointer/collection/scope.schema.yaml index f5622647fa..8217653c3b 100644 --- a/schemas/pointer/collection/scope.schema.yaml +++ b/schemas/pointer/collection/scope.schema.yaml @@ -37,9 +37,9 @@ additionalProperties: false examples: - define: example-offset: - $sum: [1, 2] + ~sum: [1, 2] example-length: - $product: [2, $wordsize] + ~product: [2, ~wordsize] in: name: example location: memory diff --git a/schemas/pointer/expression.schema.yaml b/schemas/pointer/expression.schema.yaml index 15f05bb04a..4c60f717e3 100644 --- a/schemas/pointer/expression.schema.yaml +++ b/schemas/pointer/expression.schema.yaml @@ -16,17 +16,17 @@ description: | The two sorts are produced by different forms: - - **Integers** are produced by a JSON-number literal, the `$wordsize` + - **Integers** are produced by a JSON-number literal, the `~wordsize` constant, a variable or lookup (`.offset` / `.length` / `.slot`) that - denotes an index or count, an arithmetic operation (`$sum`, - `$difference`, `$product`, `$quotient`, `$remainder`), and a + denotes an index or count, an arithmetic operation (`~sum`, + `~difference`, `~product`, `~quotient`, `~remainder`), and a hexadecimal literal that has an **odd** number of digits (which has no whole-byte width — see `Literal`). - **Bytes** are produced by a hexadecimal literal with an **even** number - of digits (its width is the number of bytes written), `$read` (its + of digits (its width is the number of bytes written), `~read` (its width is the length of the region read), the resize forms - `$sizedN` / `$wordsized` (whose width is `N` / the word size), - `$keccak256` (width 32), and `$concat` (width the sum of its operands'). + `~sizedN` / `~wordsized` (whose width is `N` / the word size), + `~keccak256` (width 32), and `~concat` (width the sum of its operands'). ## Coercion and the width-bearing requirement @@ -34,11 +34,11 @@ description: | segment `slot` / `offset` / `length` — a bytes value is accepted and read as the non-negative integer its bytes encode (big-endian). - Where **bytes** are expected — the operands of `$concat` and `$keccak256`, + Where **bytes** are expected — the operands of `~concat` and `~keccak256`, whose results depend on operand widths — the operand **must** be width-bearing. A bare integer (a JSON number, an odd-digit hex literal, - `$wordsize`, an arithmetic result, or a lookup) is **not** valid there: - give it a width first with `$sizedN` or `$wordsized`. There is no + `~wordsize`, an arithmetic result, or a lookup) is **not** valid there: + give it a width first with `~sizedN` or `~wordsized`. There is no implicit widening; the resize forms are the only bridge from an integer to bytes. @@ -80,7 +80,7 @@ $defs: title: Constant value type: string enum: - - $wordsize + - ~wordsize Variable: title: Variable identifier @@ -96,15 +96,15 @@ $defs: Ordinary integer arithmetic. Each operand is taken as an **integer** (a bytes operand is read as the non-negative integer its bytes encode), and the result is an **integer** with no width. To use an arithmetic - result where bytes are required, give it a width with `$sizedN` or - `$wordsized`. + result where bytes are required, give it a width with `~sizedN` or + `~wordsized`. type: object properties: - "$sum": + "~sum": description: | A list of expressions to be added together. $ref: "#/$defs/Operands" - "$difference": + "~difference": description: | A tuple of two expressions where the second is to be subtracted from the first. @@ -112,31 +112,31 @@ $defs: If the second operand is larger than the first, the result of this arithmetic operation is defined to equal zero (`0`). - (i.e., `{ "$difference": [a, b] }` equals `a` minus `b`.) + (i.e., `{ "~difference": [a, b] }` equals `a` minus `b`.) $ref: "#/$defs/Operands" minItems: 2 maxItems: 2 - "$product": + "~product": description: | A list of expressions to be multiplied. $ref: "#/$defs/Operands" - "$quotient": + "~quotient": description: | A tuple of two expressions where the first corresponds to the dividend and the second corresponds to the divisor, for the purposes of doing integer division. - (i.e., `{ "$quotient": [a, b] }` equals `a` divided by `b`.) + (i.e., `{ "~quotient": [a, b] }` equals `a` divided by `b`.) $ref: "#/$defs/Operands" minItems: 2 maxItems: 2 - "$remainder": + "~remainder": description: | A tuple of two expressions where the first corresponds to the dividend and the second corresponds to the divisor, for the purposes of computing the modular-arithmetic remainder. - (i.e., `{ "$remainder": [a, b] }` equals `a` mod `b`.) + (i.e., `{ "~remainder": [a, b] }` equals `a` mod `b`.) $ref: "#/$defs/Operands" minItems: 2 maxItems: 2 @@ -144,12 +144,12 @@ $defs: minProperties: 1 maxProperties: 1 examples: - - "$sum": [5, 3, 4] - - "$difference": [5, 3] - - "$product": [5, 3, 0] - - "$quotient": [5, 3] - - "$remainder": - - "$product": + - "~sum": [5, 3, 4] + - "~difference": [5, 3] + - "~product": [5, 3, 0] + - "~quotient": [5, 3] + - "~remainder": + - "~product": - 2 - 2 - 2 @@ -184,31 +184,31 @@ $defs: examples: - .offset: "array-count" - .length: "array-item" - - .offset: $this + - .offset: ~this Read: title: Read region bytes description: | - An object of the form `{ "$read": "" }`. The value of this + An object of the form `{ "~read": "" }`. The value of this expression equals the raw bytes present in the running machine state in the referenced region. The result is **bytes** whose width is the length of the region read. type: object properties: - $read: + ~read: $ref: "#/$defs/Reference" required: - - $read + - ~read additionalProperties: false examples: - - $read: "struct-start" + - ~read: "struct-start" Reference: title: Region reference description: | A string value that **must** either be the `"name"` of at least one region declared with `{ "name": "" }` previously in some root - pointer representation, or it **must** be the literal value `"$this"`, + pointer representation, or it **must** be the literal value `"~this"`, which indicates a reference to the region containing this expression. If more than one region is defined with the same name, resolution is @@ -218,71 +218,71 @@ $defs: type: string oneOf: - $ref: "schema:ethdebug/format/pointer/identifier" - - const: "$this" + - const: "~this" description: | Indicates a reference to the region containing this expression. - A property lookup via `$this` (e.g. `{ ".length": "$this" }`) must + A property lookup via `~this` (e.g. `{ ".length": "~this" }`) must not be circular: the referenced property must be resolvable without depending on the value currently being defined. Keccak256: title: Keccak256 hash description: | - An object of the form `{ "$keccak256": [...values] }`, indicating + An object of the form `{ "~keccak256": [...values] }`, indicating that this expression evaluates to the Solidity-style keccak256 hash of the tightly-packed bytes encoded by `values`. The result is **bytes** of width 32. Because the hash is taken over the concatenation of the operands' bytes, each operand **must** be width-bearing (bytes): a bare integer - is not valid here and must be given a width first with `$sizedN` or - `$wordsized`. This is why a mapping-slot computation word-sizes its key + is not valid here and must be given a width first with `~sizedN` or + `~wordsized`. This is why a mapping-slot computation word-sizes its key and slot before hashing. type: object properties: - $keccak256: + ~keccak256: title: Array of hashed values type: array items: $ref: "schema:ethdebug/format/pointer/expression" additionalProperties: false required: - - $keccak256 + - ~keccak256 examples: - - $keccak256: - - $wordsized: 0 + - ~keccak256: + - ~wordsized: 0 - "0x00" Concat: title: Concatenate values description: | - An object of the form `{ "$concat": [...values] }`, indicating that this + An object of the form `{ "~concat": [...values] }`, indicating that this expression evaluates to the concatenation of bytes from each value. The byte width of each operand is preserved; no padding is added or removed between operands. The result is **bytes** whose width is the sum of the operand widths. Each operand **must** be width-bearing (bytes): a bare integer is not - valid here and must be given a width first with `$sizedN` or - `$wordsized`. + valid here and must be given a width first with `~sizedN` or + `~wordsized`. type: object properties: - $concat: + ~concat: title: Array of values to concatenate type: array items: $ref: "schema:ethdebug/format/pointer/expression" additionalProperties: false required: - - $concat + - ~concat examples: - - $concat: + - ~concat: - "0x00" - "0x00" - - $concat: + - ~concat: - "0xdead" - "0xbeef" - - $concat: [] + - ~concat: [] Resize: title: Resize data @@ -292,14 +292,14 @@ $defs: yields bytes of the requested width. A resize operation expression is either an object of the form - `{ "$sized": }` or an object of the form - `{ "$wordsized": }`, where `` is an expression + `{ "~sized": }` or an object of the form + `{ "~wordsized": }`, where `` is an expression whose value is to be resized, and, if applicable, where `` is the smallest decimal representation of an unsigned integer. This object's value is evaluated as follows, based on the bytes width of the value `` evaluates to and based on `` (using the - value of `"$wordsize"` for `` in the case of the latter form above): + value of `"~wordsize"` for `` in the case of the latter form above): - If the width equals ``, this object evaluates to the same value as `` (equivalent to the identity function or no-op). - If the width is less than ``, this object evaluates to the same value @@ -317,29 +317,29 @@ $defs: - title: Resize to literal number of bytes type: object patternProperties: - "^\\$sized([1-9]+[0-9]*)$": + "^~sized([1-9]+[0-9]*)$": $ref: "schema:ethdebug/format/pointer/expression" additionalProperties: false - title: Resize to word-size type: object patternProperties: - "^\\$wordsized$": + "^~wordsized$": $ref: "schema:ethdebug/format/pointer/expression" additionalProperties: false minProperties: 1 maxProperties: 1 examples: - - $sized2: "0x00" # 0x0000 - - $sized2: "0xffffff" # 0xffff - - $wordsized: "0x00" # 0x0000000000000000000000000000000000000000000000000000000000000000 + - ~sized2: "0x00" # 0x0000 + - ~sized2: "0xffffff" # 0xffff + - ~wordsized: "0x00" # 0x0000000000000000000000000000000000000000000000000000000000000000 examples: - 0 - - $sum: + - ~sum: - .offset: "array-start" - .length: "array-start" - 1 - - $keccak256: - - $wordsized: 5 - - $wordsized: + - ~keccak256: + - ~wordsized: 5 + - ~wordsized: .offset: "array-start" diff --git a/schemas/pointer/identifier.schema.yaml b/schemas/pointer/identifier.schema.yaml index f5e6b9e773..57eacbfa70 100644 --- a/schemas/pointer/identifier.schema.yaml +++ b/schemas/pointer/identifier.schema.yaml @@ -4,6 +4,10 @@ $id: "schema:ethdebug/format/pointer/identifier" title: ethdebug/format/pointer/identifier description: | An identifier for use within the context of a root pointer + + An identifier **must not** start with `~`: that prefix is reserved for the + format's own terms in pointer expressions (e.g., `~wordsize`, `~this`, + `~sum`), so a name can never be confused with one of them. type: string pattern: "^[a-zA-Z_\\-]+[a-zA-Z0-9$_\\-]*$" diff --git a/schemas/pointer/region/stack.schema.yaml b/schemas/pointer/region/stack.schema.yaml index 64a3303d01..dac2ff1bd4 100644 --- a/schemas/pointer/region/stack.schema.yaml +++ b/schemas/pointer/region/stack.schema.yaml @@ -32,6 +32,6 @@ examples: - location: stack slot: 1 length: - $product: - - $wordsize + ~product: + - ~wordsize - 2 diff --git a/schemas/pointer/region/storage.schema.yaml b/schemas/pointer/region/storage.schema.yaml index 6b1138fb9e..f9d4acec07 100644 --- a/schemas/pointer/region/storage.schema.yaml +++ b/schemas/pointer/region/storage.schema.yaml @@ -28,16 +28,16 @@ examples: - location: storage slot: "0x06" length: - $product: - - $wordsize + ~product: + - ~wordsize - 2 - location: storage slot: "0x08" offset: - $quotient: - - $wordsize + ~quotient: + - ~wordsize - 2 length: - $quotient: - - $wordsize + ~quotient: + - ~wordsize - 2 diff --git a/schemas/pointer/region/transient.schema.yaml b/schemas/pointer/region/transient.schema.yaml index 0a74376c5e..04fe3d9052 100644 --- a/schemas/pointer/region/transient.schema.yaml +++ b/schemas/pointer/region/transient.schema.yaml @@ -28,16 +28,16 @@ examples: - location: transient slot: "0x06" length: - $product: - - $wordsize + ~product: + - ~wordsize - 2 - location: transient slot: "0x08" offset: - $quotient: - - $wordsize + ~quotient: + - ~wordsize - 2 length: - $quotient: - - $wordsize + ~quotient: + - ~wordsize - 2 diff --git a/schemas/pointer/scheme/segment.schema.yaml b/schemas/pointer/scheme/segment.schema.yaml index 4404a431d7..0d742644f0 100644 --- a/schemas/pointer/scheme/segment.schema.yaml +++ b/schemas/pointer/scheme/segment.schema.yaml @@ -12,7 +12,7 @@ description: | In such cases, this schema defines the range as the concatenation of bytes across slots such that the address of the first byte after the end of slot - `p` (i.e., `{ "offset": "$wordsize" }`) is interpreted as the first byte of + `p` (i.e., `{ "offset": "~wordsize" }`) is interpreted as the first byte of slot `p + 1`. type: object @@ -25,10 +25,10 @@ properties: The starting byte index within the slot. Bytes within a slot are numbered from the most significant byte. A - slot's value is its `$wordsize`-byte big-endian word, and byte `0` is + slot's value is its `~wordsize`-byte big-endian word, and byte `0` is the first byte of that word, as if the word were written to memory. An `offset` of `0` therefore addresses the most significant byte of the - slot, and an `offset` of `$wordsize - 1` addresses the least + slot, and an `offset` of `~wordsize - 1` addresses the least significant byte. This field is **optional**. If unspecified, it has the default value of @@ -37,13 +37,13 @@ properties: A data layout that counts bytes from the low-order end of a slot must convert: a value of `n` bytes that sits `o` bytes from the low-order end - is at `offset` `$wordsize - o - n`. An emitter may write that number as + is at `offset` `~wordsize - o - n`. An emitter may write that number as a literal, which is the simplest form to read and resolve. It may also write the conversion as an expression, such as ```json { - "$difference": ["$wordsize", { "$sum": [o, { ".length": "$this" }] }] + "~difference": ["~wordsize", { "~sum": [o, { ".length": "~this" }] }] } ``` @@ -53,15 +53,15 @@ properties: This field's expression must resolve to a non-negative value. It is **not** bounded by the word size: an offset that meets or exceeds - `$wordsize` carries into subsequent slots. Given a `slot` value `p` + `~wordsize` carries into subsequent slots. Given a `slot` value `p` and an `offset` value `n`, the segment begins at byte - `n mod $wordsize` of slot `p + floor(n / $wordsize)`. (Equivalently, - byte `{ "offset": "$wordsize" }` of slot `p` is byte `0` of slot + `n mod ~wordsize` of slot `p + floor(n / ~wordsize)`. (Equivalently, + byte `{ "offset": "~wordsize" }` of slot `p` is byte `0` of slot `p + 1`, consistent with the multi-slot note above.) Emitters may therefore chain byte sums across a slot boundary without decomposing into slot and byte components themselves; a resolver recovers the effective slot and byte by division and remainder against - `$wordsize`. + `~wordsize`. $ref: "schema:ethdebug/format/pointer/expression" default: 0 length: @@ -78,11 +78,11 @@ properties: following the slot specified. $ref: "schema:ethdebug/format/pointer/expression" default: - $difference: - - $wordsize - - $remainder: - - .offset: $this - - $wordsize + ~difference: + - ~wordsize + - ~remainder: + - .offset: ~this + - ~wordsize required: - slot @@ -91,14 +91,14 @@ examples: - slot: 0 - slot: 1 length: - $product: - - $wordsize + ~product: + - ~wordsize - 3 - # a carry example: an offset at or beyond `$wordsize` addresses a later - # slot. Here `offset: $wordsize` is byte 0 of slot 1, so this segment is + # a carry example: an offset at or beyond `~wordsize` addresses a later + # slot. Here `offset: ~wordsize` is byte 0 of slot 1, so this segment is # the 4 bytes beginning there. - slot: 0 - offset: $wordsize + offset: ~wordsize length: 4 # packed values: an `address` (20 bytes) at the low-order end of slot 2, # and a `uint32` (4 bytes) just above it, written with literal offsets @@ -108,19 +108,19 @@ examples: - slot: 2 offset: 8 length: 4 - # the same two values with the conversion `$wordsize - (o + n)` written as + # the same two values with the conversion `~wordsize - (o + n)` written as # an expression that takes `n` from the region's own length - slot: 2 offset: - $difference: - - $wordsize - - .length: $this + ~difference: + - ~wordsize + - .length: ~this length: 20 - slot: 2 offset: - $difference: - - $wordsize - - $sum: + ~difference: + - ~wordsize + - ~sum: - 20 - - .length: $this + - .length: ~this length: 4