diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index 2f9b3a9360..5cdaa2b151 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -69,6 +69,25 @@ support. Changes to the specification itself are tracked in the root Before, the pointer named the whole word `x`, and `x-data` took its offset and length as `~quotient` and `~remainder` of that word by 2^128. The bytes the pointer reads are the same ([#398]). +- A pointer shape that repeats is now a pointer template, defined + once, and a variable's pointer refers to it by name: + `{ "define": { ... }, "in": { "template": ..., "yields": { ... } } }`. + The bytecode output has a new field, `resources`: an + `ethdebug/format/info/resources` object whose `pointers` holds the + templates. Dereference a pointer with them: + `dereference(pointer, { state, templates: resources.pointers })`. + The templates are `frame-local` (a local in the call frame); + `frame-calldata-local`, `memory-calldata-local` and + `stack-calldata-local` (bytes or a string in calldata, by where the + local's word is); `memory-bytes` and `memory-` (the data of a + string, bytes, array or struct in memory); `storage-bytes` (a string + or bytes in storage); and `storage-` (an array, struct or + mapping in storage, with a mapping's entry templates in it). Region + names do not change, and each pointer reads the same bytes as + before. A storage variable of a value type, and a local in a stack + slot or at a static memory offset, keep their inline pointers. The + programs are about half as large. The new example + `intermediate/pointer-shapes.bug` has these shapes ([#399]). ### Fixed @@ -606,4 +625,5 @@ First publication. [#395]: https://github.com/ethdebug/format/pull/395 [#397]: https://github.com/ethdebug/format/pull/397 [#398]: https://github.com/ethdebug/format/pull/398 +[#399]: https://github.com/ethdebug/format/pull/399 [#400]: https://github.com/ethdebug/format/pull/400 diff --git a/packages/bugc/examples/intermediate/mappings.bug b/packages/bugc/examples/intermediate/mappings.bug index 6d94561fda..869bded243 100644 --- a/packages/bugc/examples/intermediate/mappings.bug +++ b/packages/bugc/examples/intermediate/mappings.bug @@ -23,15 +23,8 @@ code { variables: balances: pointer: - templates: - entry: - expect: [slot, key] - for: - define: - slot: - ~keccak256: [{ ~wordsized: key }, { ~wordsized: slot }] - in: { location: storage, slot: slot, name: value } - in: { location: storage, slot: 0 } + define: { slot: 0 } + in: { template: storage-mapping-address-uint256 } */ // Read from mapping diff --git a/packages/bugc/examples/intermediate/pointer-shapes.bug b/packages/bugc/examples/intermediate/pointer-shapes.bug new file mode 100644 index 0000000000..79ba887338 --- /dev/null +++ b/packages/bugc/examples/intermediate/pointer-shapes.bug @@ -0,0 +1,61 @@ +name PointerShapes; + +// Pointer shapes that refer to shared templates: a dynamic storage +// array of structs, a mapping inside a struct, struct fields named +// like a template's own regions (`length`, `data`), and locals in a +// function's frame that are a string in memory and bytes in calldata + +define { + struct Blob { + length: uint64; + data: bytes; + }; + + struct Entry { + length: uint256; + data: string; + tags: mapping; + }; + + function record(note: string calldata, raw: bytes calldata) -> uint256 { + let copy: string = note; + entries.push(); + blobs.push(); + entries[0].length = note.length; + entries[0].data = copy; + entries[0].tags[7] = 3; + blobs[0].length = raw.length as uint64; + blobs[0].data = raw; + let blob: Blob = blobs[0]; + return blob.length as uint256; + }; +} + +storage { + [0] entries: array; + [1] blobs: array; + [2] byOwner: mapping; + [3] total: uint256; +} + +code { + let len = msg.data[0:32] as bytes32 as uint256; + let note: string calldata = msg.data[32:32 + len] as string calldata; + let raw: bytes calldata = msg.data[32:32 + len] as bytes calldata; + total = record(note, raw); + /*@test recorded + call-data: "000000000000000000000000000000000000000000000000000000000000002d61206e6f7465206c6f6e676572207468616e207468697274792d74776f2062797465732c20746f207370696c6c" + variables: + total: + value: 45 + entries: + values: + array-length: 1 + length: 45 + data-data: "0x61206e6f7465206c6f6e676572207468616e207468697274792d74776f2062797465732c20746f207370696c6c" + blobs: + values: + length: 45 + data-data: "0x61206e6f7465206c6f6e676572207468616e207468697274792d74776f2062797465732c20746f207370696c6c" + */ +} diff --git a/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap b/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap index 8195b37074..b63d07009e 100644 --- a/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap +++ b/packages/bugc/src/evmgen/__snapshots__/activation-pairing.test.ts.snap @@ -142,6 +142,10 @@ exports[`the code does not depend on the markers > is the same for every example "intermediate/phi-nodes.bug at O1": "6d4c6ea207ad3a5c967c8ca80d9a7a594c17ab9fd582856f0f977e766d316b58", "intermediate/phi-nodes.bug at O2": "6d4c6ea207ad3a5c967c8ca80d9a7a594c17ab9fd582856f0f977e766d316b58", "intermediate/phi-nodes.bug at O3": "6d4c6ea207ad3a5c967c8ca80d9a7a594c17ab9fd582856f0f977e766d316b58", + "intermediate/pointer-shapes.bug at O0": "0ea8fcf89707f2868284d965ddd743ad96df4091b78642e5cd328fcfac42b6a9", + "intermediate/pointer-shapes.bug at O1": "c1a7b25b6c663855114141be67df907a165c269099f1e2af7666372c62e3830b", + "intermediate/pointer-shapes.bug at O2": "369bc9d3794d4400613f92b5d614080a844c4e22968d149475142354e5df3861", + "intermediate/pointer-shapes.bug at O3": "024072c4c363eee5bd0488cf46d168691234fb6926f2771bfcf483f68e0b5c37", "intermediate/scopes.bug at O0": "6f7fab968d0fdf458d2536c83abbb3e36a2291d4d3c32b6c4874831a7d28e9bc", "intermediate/scopes.bug at O1": "9e9e0d1d5e3b39e7ddb512bb5de2ca8d3d6d32fd12a211a68ed0c27ac273b0dc", "intermediate/scopes.bug at O2": "990ea851a89f3cf194506deb42a9f0640fda9b4da8f6984219622b2f6f7538e3", diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts index 92742164a4..71ead6bba0 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts @@ -1684,13 +1684,23 @@ describe.each(levels)("a calldata reference's word at O%i", (level) => { const context = run.instructionAt(run.steps[k])?.context; for (const entry of localsOf(context)) { const pointer = entry.pointer as Format.Pointer | undefined; - const json = JSON.stringify(pointer); - if (!pointer || !json.includes('"calldata"')) continue; - expect(json).not.toMatch(/~quotient|~remainder/); - const name = entry.identifier as string; + if (!pointer) continue; const state = run.stateAt(k + 1); - const view = await (await dereference(pointer, { state })).view(state); + const { templates } = run; + const view = await ( + await dereference(pointer, { state, templates }) + ).view(state); + const data = view.regions.named(`${name}-data`); + if (data[0]?.location !== "calldata") continue; + // (the pointer, and the templates it refers to) + const referred = [ + ...JSON.stringify(pointer).matchAll(/"template":"([^"]+)"/g), + ].map(([, template]) => templates[template]); + expect(JSON.stringify([pointer, referred])).not.toMatch( + /~quotient|~remainder/, + ); + const [offset, length] = ["offset", "length"].map((part) => { const regions = view.regions.named(`${name}-${part}`); expect(regions).toHaveLength(1); diff --git a/packages/bugc/src/evmgen/debug/local-variables.test.ts b/packages/bugc/src/evmgen/debug/local-variables.test.ts index df4d73755f..5e6600b431 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.test.ts @@ -55,16 +55,19 @@ function localEntries( return byId; } -/** True if a pointer is a frame-relative group (name "-frame" + ~read). */ +/** + * True if a pointer is frame-relative: a reference to a frame template + * (whose regions read the frame pointer, "-frame"), at an offset. + */ function isFrameRelative(pointer: unknown): boolean { if (!pointer || typeof pointer !== "object") return false; - const group = (pointer as { group?: unknown[] }).group; - if (!Array.isArray(group) || group.length < 2) return false; - const frame = group[0] as Record; - 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"]); + const { define, in: in_ } = pointer as { + define?: { offset?: unknown }; + in?: { template?: string }; + }; + return ( + typeof define?.offset === "number" && !!in_?.template?.startsWith("frame-") + ); } 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 5d0e42ba1d..4d14b82000 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.ts @@ -30,9 +30,13 @@ * * A reference-typed local (memory array, string, dynamic bytes) is * held as an address word; its pointer names that word, then the data - * it refers to (`dataPointers`). A local of bytes in calldata is held + * it refers to (`memoryData`). A local of bytes in calldata is held * as a word with their offset and length; its pointer names that - * word, then those bytes (`calldataPointer`). + * word, then those bytes (`wordPointer`). + * + * Pointer shapes that repeat are templates (`wordTemplates`, + * `memoryData`), defined once in the module's `pointerTemplates`; a + * local's pointer refers to them. * * The memory pointer encodes bugc's frame convention: the frame base * lives in memory at `FRAME_POINTER` (0x80); a frame-homed local is at @@ -49,247 +53,377 @@ import { Type as BugType } from "#types"; import { Memory } from "#evmgen/analysis"; import { convertToEthDebugType } from "../../irgen/debug/types.js"; import { fromBugType } from "../../irgen/type.js"; +import { + type Templates, + prefixed, + define, + referTo, + regionNames, + typeName, +} from "../../irgen/debug/templates.js"; type VariableEntry = Format.Program.Context.Variables["variables"][number]; -/** The frame pointer's region; not a BUG identifier, so no local's */ -const FRAME = "-frame"; type StackLocal = NonNullable[number]; +/** What a local's word holds (see `wordPointer`) */ +type Word = StackLocal["word"]; + +/** Where a local's word is: at a static memory offset, at an offset in + * the function's frame, or in a stack slot */ +type Home = { memory: number } | { frame: number } | { stack: number }; + type DefSite = Ir.Utils.DefSite; -/** A region in a local's word (see `memoryPointer`) */ -type Part = StackLocal["parts"][number]; +/** The frame pointer's region; not a BUG identifier, so no local's */ +const FRAME = "-frame"; + +const frame: Format.Pointer = { + name: FRAME, + location: "memory", + offset: Memory.regions.FRAME_POINTER, + length: 32, +}; -/** A scalar of `size` bytes, or a whole word, as one part */ -function wholePart(name: string, size = 32): Part[] { - return [{ name, offset: 32 - size, length: size }]; +/** `mem[FRAME_POINTER] + offset + within`, in a frame template */ +function inFrame(within: number): Format.Pointer.Expression { + return { + "~sum": [{ "~read": FRAME }, "offset", ...(within > 0 ? [within] : [])], + }; } /** - * A value is homed by a full-word `MSTORE` of its stack word, which - * writes the word big-endian. bugc keeps every scalar right-aligned on - * the stack (`bytesN` included), so a scalar of `size` bytes is the - * word's low-order `size` bytes: `32 - size` bytes into the word, - * `size` bytes long. The same holds for a stack slot (segment offsets - * count from the word's most significant byte). - * - * A word may hold more than one part (bytes in calldata: their offset - * and length); each part is a region of its own. + * The regions of a word that refers to bytes in calldata, each part + * placed by `at` (given its offset in the word, and its length): the + * bytes' offset in the word's high 16 bytes (`offset`), their length + * in its low 16 bytes (`length`), then the bytes themselves (`data`). */ -function memoryPointer( - parts: Part[], - allocation: Memory.Allocation, - frameSize: number | undefined, -): Format.Pointer { - const word = Number(allocation.offset); +function calldataWord( + at: (within: number, length: number) => Format.Pointer.Region, +): Format.Pointer[] { + return [ + { ...at(0, 16), name: "offset" }, + { ...at(16, 16), name: "length" }, + { + name: "data", + location: "calldata", + offset: { "~read": "offset" }, + length: { "~read": "length" }, + }, + ]; +} - if (frameSize === undefined) { - // No call frame (main/create): static memory offset. - const regions = parts.map( - ({ name, offset, length }): Format.Pointer => ({ - name, +/** + * The templates for a local's word, by where it is (see `wordPointer`). + * The memory pointer encodes bugc's frame convention (see above). + */ +const wordTemplates: Record = { + "frame-local": { + expect: ["offset", "length"], + for: { + group: [ + frame, + { + name: "local", + location: "memory", + offset: inFrame(0), + length: "length", + }, + ], + }, + }, + "frame-calldata-local": { + expect: ["offset"], + for: { + group: [ + frame, + ...calldataWord((within, length) => ({ + location: "memory", + offset: inFrame(within), + length, + })), + ], + }, + }, + "memory-calldata-local": { + expect: ["offset"], + for: { + group: calldataWord((within, length) => ({ location: "memory", - offset: word + offset, + offset: within > 0 ? { "~sum": ["offset", within] } : "offset", length, - }), - ); - return regions.length === 1 ? regions[0] : { group: regions }; - } + })), + }, + }, + "stack-calldata-local": { + expect: ["slot"], + for: { + group: calldataWord((within, length) => ({ + location: "stack", + slot: "slot", + offset: within, + length, + })), + }, + }, +}; - // Frame-relative: mem[ mem[FRAME_POINTER] + delta ]. - return { - group: [ - { - name: FRAME, - location: "memory", - offset: Memory.regions.FRAME_POINTER, - length: 32, - }, - ...parts.map( - ({ name, offset, length }): Format.Pointer => ({ - name, - location: "memory", - offset: { "~sum": [{ "~read": FRAME }, word + offset] }, - length, - }), - ), - ], - }; +/** A reference to the word template `name`, defined in `templates` */ +function word( + templates: Templates | undefined, + name: string, + bindings: Record, + yields: Record, +): Format.Pointer { + if (templates) define(templates, name, () => wordTemplates[name]); + return referTo(name, bindings, yields); } -function stackPointer(parts: Part[], slot: number): Format.Pointer { - const regions = parts.map( - ({ name, offset, length }): Format.Pointer => ({ +/** + * The pointer to a local's word, named after the local. A scalar's + * word holds its value: a value is homed by a full-word `MSTORE` of + * its stack word, which writes the word big-endian, and bugc keeps + * every scalar right-aligned on the stack (`bytesN` included), so a + * scalar of `size` bytes is the word's low-order `size` bytes: `32 - + * size` bytes into the word. The same holds for a stack slot (segment + * offsets count from the word's most significant byte). A reference + * to bytes in calldata holds their offset and length, each a region + * of its own, `x-offset` and `x-length`, and the pointer gives the + * bytes, `x-data`. + * + * A word in the frame, and a reference to calldata anywhere, is a + * reference to a template defined in `templates` (if given); any other + * word is a single region. + */ +function wordPointer( + name: string, + { kind, size }: Word, + home: Home, + templates?: Templates, +): Format.Pointer { + if (kind === "calldata") { + const yields = prefixed(["offset", "length", "data"], `${name}-`); + if ("stack" in home) { + return word( + templates, + "stack-calldata-local", + { slot: home.stack }, + yields, + ); + } + return "frame" in home + ? word(templates, "frame-calldata-local", { offset: home.frame }, yields) + : word( + templates, + "memory-calldata-local", + { offset: home.memory }, + yields, + ); + } + + const offset = 32 - size; + if ("stack" in home) { + return { name, location: "stack", - slot, - ...(length < 32 ? { offset, length } : {}), - }), + slot: home.stack, + ...(size < 32 ? { offset, length: size } : {}), + }; + } + if ("memory" in home) { + return { + name, + location: "memory", + offset: home.memory + offset, + length: size, + }; + } + return word( + templates, + "frame-local", + { offset: home.frame + offset, length: size }, + { local: name }, ); - return regions.length === 1 ? regions[0] : { group: regions }; +} + +/** The word templates whose regions read the frame pointer */ +const frameTemplates = new Set(["frame-local", "frame-calldata-local"]); + +/** True for a pointer to a word in the frame (see `wordPointer`) */ +function isFrameRelative(pointer: Format.Pointer): boolean { + if ("group" in pointer) return isFrameRelative(pointer.group[0]); + if ("define" in pointer) return isFrameRelative(pointer.in); + return "template" in pointer && frameTemplates.has(pointer.template); } /** - * Pointers to the data a reference at `base` refers to, by bugc's - * memory layout, or undefined if the layout is not described here. - * - * - `string` and dynamic `bytes`: a length word at `base`, then that - * many bytes of data. - * - dynamic array: a length word at `base`, then one word per element - * (`base + 32 + 32 * i`). A scalar element is right-aligned in its - * word; a reference element's word holds the address of its data, - * described the same way. + * The template for the data a reference in memory refers to, by bugc's + * memory layout, defined in `templates`; or undefined if the layout is + * not described here. It expects `base`, the data's address. * - * - struct: one word per field, in order, from `base`. A scalar field - * is right-aligned in its word; a reference field's word holds the + * - `string` and dynamic `bytes` (`memory-bytes`): a length word at + * `base`, then that many bytes of data. + * - dynamic array (`memory-array-`): a length word at `base`, + * then one word per element (`base + 32 + 32 * i`). A scalar element + * is right-aligned in its word; a reference element's word holds the * address of its data, described the same way. + * - struct (`memory-`): one word per field, in order, from + * `base`. A scalar field is right-aligned in its word; a reference + * field's word holds the address of its data, described the same + * way. * - * Regions are named after the local: `x-length`, `x-data`, - * `x-element`, `x-`, and for a nested reference - * `x-element-length`, `x--data` and so on. Fixed-size arrays - * are left out: bugc cannot build one in memory yet (array literals - * are dynamic). + * Regions are named `length`, `data`, `element`, ``, and for a + * nested reference `element-length`, `-data` and so on. + * Fixed-size arrays are left out: bugc cannot build one in memory yet + * (array literals are dynamic). */ -function dataPointers( - base: Format.Pointer.Expression, +function memoryData( type: Ir.Type.Origin, - name: string, - depth: number = 0, -): Format.Pointer[] | undefined { + templates: Templates, +): string | undefined { if (type === "synthetic") return undefined; - const length: Format.Pointer = { - name: `${name}-length`, - location: "memory", - offset: base, - length: 32, - }; - if ( BugType.isElementary(type) && (BugType.Elementary.isString(type) || (BugType.Elementary.isBytes(type) && type.size === undefined)) ) { - return [ - length, - { - name: `${name}-data`, - location: "memory", - offset: { "~sum": [base, 32] }, - length: { "~read": `${name}-length` }, + return define(templates, "memory-bytes", () => ({ + expect: ["base"], + for: { + group: [ + lengthWord, + { + name: "data", + location: "memory", + offset: { "~sum": ["base", 32] }, + length: { "~read": "length" }, + }, + ], }, - ]; + })); } + if ( + !BugType.isStruct(type) && + !(BugType.isArray(type) && type.size === undefined) + ) { + return undefined; + } + return define( + templates, + `memory-${typeName(type)}`, + () => { + const pointer = BugType.isStruct(type) + ? structLayout(type, templates) + : arrayLayout(type, templates); + return pointer && { expect: ["base"], for: pointer }; + }, + BugType.format(type), + ); +} - if (BugType.isStruct(type)) { - const fields: Format.Pointer[] = []; - let index = 0; - for (const [field, fieldType] of type.fields) { - const region = `${name}-${field}`; - const word = 32 * index++; - const offset = (within: number): Format.Pointer.Expression => - word + within === 0 ? base : { "~sum": [base, word + within] }; - const irType = fromBugType(fieldType); - if (Ir.Type.isScalar(irType)) { - fields.push({ - name: region, - location: "memory", - offset: offset(32 - irType.size), - length: irType.size, - }); - continue; - } - const nested = dataPointers( - { "~read": region }, - fieldType, - region, - depth, - ); - if (!nested) return undefined; - fields.push( - { name: region, location: "memory", offset: offset(0), length: 32 }, - ...nested, - ); +/** The length word at `base` (of bytes, a string, or an array) */ +const lengthWord: Format.Pointer = { + name: "length", + location: "memory", + offset: "base", + length: 32, +}; + +/** A struct in memory at `base` (see `memoryData`) */ +function structLayout( + type: BugType.Struct, + templates: Templates, +): Format.Pointer | undefined { + const fields: Format.Pointer[] = []; + let index = 0; + for (const [field, fieldType] of type.fields) { + const start = 32 * index++; + const offset = (within: number): Format.Pointer.Expression => + start + within === 0 ? "base" : { "~sum": ["base", start + within] }; + const irType = fromBugType(fieldType); + if (Ir.Type.isScalar(irType)) { + fields.push({ + name: field, + location: "memory", + offset: offset(32 - irType.size), + length: irType.size, + }); + continue; } - return fields; + const nested = memoryData(fieldType, templates); + if (!nested) return undefined; + fields.push( + { name: field, location: "memory", offset: offset(0), length: 32 }, + dataPointer(field, nested, templates), + ); } + return { group: fields }; +} - if (!BugType.isArray(type) || type.size !== undefined) return undefined; - - const index = `i${depth}`; - const element = `${name}-element`; +/** A dynamic array in memory at `base` (see `memoryData`) */ +function arrayLayout( + type: BugType.Array, + templates: Templates, +): Format.Pointer | undefined { const wordOffset = (within: number): Format.Pointer.Expression => ({ - "~sum": [base, 32 + within, { "~product": [index, 32] }], + "~sum": ["base", 32 + within, { "~product": ["i", 32] }], }); const elementType = fromBugType(type.element); let is: Format.Pointer; if (Ir.Type.isScalar(elementType)) { const size = elementType.size; is = { - name: element, + name: "element", location: "memory", offset: wordOffset(32 - size), length: size, }; } else { - const nested = dataPointers( - { "~read": element }, - type.element, - element, - depth + 1, - ); + const nested = memoryData(type.element, templates); if (!nested) return undefined; is = { group: [ { - name: element, + name: "element", location: "memory", offset: wordOffset(0), length: 32, }, - ...nested, + dataPointer("element", nested, templates), ], }; } - - return [ - length, - { list: { count: { "~read": `${name}-length` }, each: index, is } }, - ]; + return { + group: [ + lengthWord, + { list: { count: { "~read": "length" }, each: "i", is } }, + ], + }; } /** - * The parts of a local's word that refers to bytes in calldata: their - * offset in its high 16 bytes, their length in its low 16 bytes. The - * regions are named `x-offset` and `x-length`. + * A pointer to the data that the reference in region `name` refers to: + * a reference to its template (see `memoryData`), with its regions + * named after the reference (`x-length`, `x-data`, `x-element`, ...) */ -function calldataParts(name: string): Part[] { - return [ - { name: `${name}-offset`, offset: 0, length: 16 }, - { name: `${name}-length`, offset: 16, length: 16 }, - ]; -} - -/** A pointer to the bytes in calldata, named `x-data`, that the parts - * of a local's word (`calldataParts`) give */ -function calldataPointer(name: string): Format.Pointer { - return { - name: `${name}-data`, - location: "calldata", - offset: { "~read": `${name}-offset` }, - length: { "~read": `${name}-length` }, - }; +function dataPointer( + name: string, + template: string, + templates: Templates, +): Format.Pointer { + return referTo( + template, + { base: { "~read": name } }, + prefixed(regionNames(templates[template].for, templates), `${name}-`), + ); } /** A pointer to a local's word plus, for a reference, its data. */ function withData( word: Format.Pointer, - data: Format.Pointer[] | undefined, + data: Format.Pointer | undefined, ): Format.Pointer { - if (!data) return word; - return "group" in word - ? { group: [...word.group, ...data] } - : { group: [word, ...data] }; + return data ? { group: [word, data] } : word; } /** @@ -463,7 +597,10 @@ export function withStackLocals( if (slot < 0) return entry; return { ...entry, - pointer: withData(stackPointer(local.parts, slot), local.data), + pointer: withData( + wordPointer(local.identifier, local.word, { stack: slot }), + local.data, + ), }; }); return { @@ -519,9 +656,7 @@ export function withoutFrameLocals( if (!context || !Array.isArray(context.variables)) return debug; const isLocal = (entry: VariableEntry) => - !entry.pointer || - ("group" in entry.pointer && - (entry.pointer.group[0] as { name?: string }).name === FRAME); + !entry.pointer || isFrameRelative(entry.pointer); const variables = (context.variables as VariableEntry[]).filter( (entry) => !isLocal(entry), ); @@ -615,6 +750,8 @@ interface FunctionInfo { allocations: Record; frameSize: number | undefined; sourceId: string; + /** Where the templates its pointers refer to are defined */ + templates: Templates; } /** A position, after optimization and (if known) before it */ @@ -715,29 +852,35 @@ function snapshotAt( // A scalar's word holds its value; a reference's word holds the // address of its data. const { name } = declaration; - let parts = wholePart(name); - let data: Format.Pointer[] | undefined; + let word: Word = { kind: "value", size: 32 }; + let data: Format.Pointer | undefined; if (Ir.Type.isScalar(type)) { - parts = wholePart(name, type.size); + word = { kind: "value", size: type.size }; } else if (type.kind === "ref" && type.location === "calldata") { - parts = calldataParts(name); - data = [calldataPointer(name)]; + word = { kind: "calldata", size: 32 }; } else { if (type.kind !== "ref" || type.location !== "memory") continue; - data = dataPointers({ "~read": name }, type.origin, name); - if (!data) continue; + const template = memoryData(type.origin, info.templates); + if (!template) continue; + data = dataPointer(name, template, info.templates); } const allocation = info.allocations[located.temp]; if (allocation) { + const offset = Number(allocation.offset); + const home = + info.frameSize === undefined ? { memory: offset } : { frame: offset }; entry.pointer = withData( - memoryPointer(parts, allocation, info.frameSize), + wordPointer(name, word, home, info.templates), data, ); } else { + // Its pointer is given during code generation (`withStackLocals`), + // and refers to the templates defined here + wordPointer(name, word, { stack: 0 }, info.templates); stackLocals.push({ identifier: name, temp: located.temp, - parts, + word, ...(data ? { data } : {}), }); } @@ -754,6 +897,7 @@ function enrichFunction( module: Ir.Module, memory: Memory.Function.Info, sourceId: string, + templates: Templates, ): void { if (!func.ssaVariables || func.ssaVariables.size === 0) return; @@ -774,6 +918,7 @@ function enrichFunction( allocations: memory.allocations, frameSize: memory.frameSize, sourceId, + templates, }; const homed = new Set( [...tempOf.values()].filter((t) => info.allocations[t] !== undefined), @@ -908,13 +1053,16 @@ function enrichFunction( /** * Enrich a module's instructions with per-instruction local-variable - * snapshots. Mutates in place; safe before generation. + * snapshots, and add the templates their pointers refer to to its + * `pointerTemplates`. Mutates in place; safe before generation. */ export function enrich( module: Ir.Module, memory: Memory.Module.Info, ): Ir.Module { const sourceId = module.sourceId; + const templates = { ...module.pointerTemplates }; + module.pointerTemplates = templates; const targets: { func: Ir.Function; mem?: Memory.Function.Info }[] = [ { func: module.main, mem: memory.main }, @@ -927,7 +1075,7 @@ export function enrich( for (const { func, mem } of targets) { if (!mem) continue; - enrichFunction(func, module, mem, sourceId); + enrichFunction(func, module, mem, sourceId, templates); } return module; diff --git a/packages/bugc/src/evmgen/pass.ts b/packages/bugc/src/evmgen/pass.ts index fba894757d..d11756ee7e 100644 --- a/packages/bugc/src/evmgen/pass.ts +++ b/packages/bugc/src/evmgen/pass.ts @@ -29,6 +29,19 @@ export interface EvmGenerationOutput { runtimeProgram: Format.Program; /** Create program with debug info (ethdebug/format, optional) */ createProgram?: Format.Program; + /** + * The compilation's info resources (ethdebug/format/info/resources): + * the pointer templates that the programs' pointers refer to, by + * name. A debugger dereferences those pointers with these templates. + */ + resources: Resources; +} + +/** An ethdebug/format/info/resources object, as bugc gives it */ +export interface Resources { + ethdebug: Format.Data.Stamp & { schema: "ethdebug/format/info/resources" }; + types: Record; + pointers: Format.Pointer.Templates; } /** @@ -106,6 +119,10 @@ const pass: Pass<{ createInstructions: result.createInstructions, runtimeProgram, createProgram, + resources: Format.Data.stamp("ethdebug/format/info/resources", { + types: {}, + pointers: ir.pointerTemplates ?? {}, + }), }, }, result.warnings, diff --git a/packages/bugc/src/evmgen/storage-array-layout.test.ts b/packages/bugc/src/evmgen/storage-array-layout.test.ts index ebabab67ad..16c4c2b8da 100644 --- a/packages/bugc/src/evmgen/storage-array-layout.test.ts +++ b/packages/bugc/src/evmgen/storage-array-layout.test.ts @@ -155,7 +155,7 @@ describe("storage array pointers", () => { for (const [name, { source, pointers }] of Object.entries(cases)) { if (!pointers) continue; it(name, async () => { - const { program, executor } = await traceLocals( + const { program, templates, executor } = await traceLocals( `name Layout;\n${source}`, ); // the storage after the transaction @@ -164,7 +164,10 @@ describe("storage array pointers", () => { const variable = ( program.context as Format.Program.Context.Variables ).variables.find((v) => v.identifier === identifier)!; - const cursor = await dereference(variable.pointer!, { state }); + const cursor = await dereference(variable.pointer!, { + state, + templates, + }); const view = await cursor.view(state); for (const [regionName, values] of Object.entries(regions)) { const read = await Promise.all( diff --git a/packages/bugc/src/evmgen/storage-array-push.test.ts b/packages/bugc/src/evmgen/storage-array-push.test.ts index 4196905571..cf7f9d86ad 100644 --- a/packages/bugc/src/evmgen/storage-array-push.test.ts +++ b/packages/bugc/src/evmgen/storage-array-push.test.ts @@ -129,7 +129,7 @@ describe("push on a storage array", () => { } it("gives the pushed elements to the array's pointer", async () => { - const { program, executor } = await traceLocals( + const { program, templates, executor } = await traceLocals( `name Push;\nstorage { [0] h: array; } code { h.push(5); h.push(7); h.push(9); }`, ); @@ -138,7 +138,7 @@ code { h.push(5); h.push(7); h.push(9); }`, const variable = ( program.context as Format.Program.Context.Variables ).variables.find((v) => v.identifier === "h")!; - const cursor = await dereference(variable.pointer!, { state }); + const cursor = await dereference(variable.pointer!, { state, templates }); const view = await cursor.view(state); const read = await Promise.all( view.regions diff --git a/packages/bugc/src/ir/spec/instruction.ts b/packages/bugc/src/ir/spec/instruction.ts index e15a3dcd72..ede889f19a 100644 --- a/packages/bugc/src/ir/spec/instruction.ts +++ b/packages/bugc/src/ir/spec/instruction.ts @@ -76,11 +76,12 @@ export namespace Instruction { stackLocals?: Array<{ identifier: string; temp: string; - /** The regions in the temp's word: each `length` bytes, `offset` - * bytes from its most significant byte */ - parts: Array<{ name: string; offset: number; length: number }>; - /** For a reference, the pointers to the data it refers to */ - data?: Format.Pointer[]; + /** What the temp's word holds: a value of `size` bytes, + * right-aligned, or the offset and length of bytes in calldata */ + word: { kind: "value" | "calldata"; size: number }; + /** For a reference in memory, the pointer to the data it refers + * to */ + data?: Format.Pointer; }>; } diff --git a/packages/bugc/src/ir/spec/module.ts b/packages/bugc/src/ir/spec/module.ts index 9d28a63261..2b119f4b67 100644 --- a/packages/bugc/src/ir/spec/module.ts +++ b/packages/bugc/src/ir/spec/module.ts @@ -21,4 +21,6 @@ export interface Module { loc?: Ast.SourceLocation; /** Program-level debug context (storage variables, etc.) */ debugContext?: Format.Program.Context; + /** The pointer templates its debug contexts' pointers refer to */ + pointerTemplates?: Format.Pointer.Templates; } diff --git a/packages/bugc/src/irgen/debug/templates.golden.json b/packages/bugc/src/irgen/debug/templates.golden.json new file mode 100644 index 0000000000..cbe7c925ed --- /dev/null +++ b/packages/bugc/src/irgen/debug/templates.golden.json @@ -0,0 +1,66 @@ +{ + "advanced/nested-arrays.bug at O0": { + "reads": 571, + "digest": "4ec3e3a37c971656218b53321691f84d963075cf6b503053932c425250c14c27" + }, + "advanced/nested-arrays.bug at O2": { + "reads": 392, + "digest": "9e36b17023c5f85a534188d78fcfe7e57e137b5f2e26ffabf6a59d881c7840a9" + }, + "advanced/nested-structs.bug at O0": { + "reads": 61, + "digest": "a04f810aeb08ab9e3f9e5f0056ebc6a0bcd50ed52d99bf7281ca63b60f7c53dc" + }, + "advanced/nested-structs.bug at O2": { + "reads": 38, + "digest": "c1bc27083ae0ff87be015c5afbc67c9ebfb38ec6691301a3828097504d1449d7" + }, + "advanced/token-registry.bug at O0": { + "reads": 150, + "digest": "98d06f5fe6dfbb9d58cde4c0d0539ad383819672b291bfaea0f2802bb6963f1c" + }, + "advanced/token-registry.bug at O2": { + "reads": 156, + "digest": "4c5bc026e29fcc42f34df492846a242f9f46d91846660a38e9b883ba56108793" + }, + "basic/functions.bug at O0": { + "reads": 821, + "digest": "336b3b9a140a08e9314630a3cd17b91cc8e086cfc9738575628c829e715453cc" + }, + "basic/functions.bug at O2": { + "reads": 146, + "digest": "f7677cbb9aa7cf6119a6f015056b1b903114706c85be6f4b7321ad78603c2f63" + }, + "intermediate/mappings.bug at O0": { + "reads": 439, + "digest": "93dc2f8d15d7499b832253cec263149836b5b17985b7197c1361bd8ad9c6974b" + }, + "intermediate/mappings.bug at O2": { + "reads": 188, + "digest": "c538ebd3daf832a1200e5861d0d856324c1e1325f585d7a46595f5967d187bd0" + }, + "intermediate/memory-arrays.bug at O0": { + "reads": 23, + "digest": "a47973739f3e06057e87b87287e94c5bb80b0af2fb118681364a6e43eee5d679" + }, + "intermediate/memory-arrays.bug at O2": { + "reads": 11, + "digest": "a65147f06d247180df9b07f7d6091f309c49e9bafb542b41b0e9760b3dc65aa8" + }, + "intermediate/pointer-shapes.bug at O0": { + "reads": 9450, + "digest": "ad31f8a041360eca8424cd6ce211be103360102eb1f471c047f84f834ff5f3f4" + }, + "intermediate/pointer-shapes.bug at O2": { + "reads": 6331, + "digest": "7477d2810c3df1dead6397d6ffcb29be4f47af6c25d9a29c9180ea2cf404cc53" + }, + "intermediate/strings.bug at O0": { + "reads": 74, + "digest": "567a1663aa4c23d39c491a51e822be394e4aa6d52a12653c85642ecbee48d19a" + }, + "intermediate/strings.bug at O2": { + "reads": 68, + "digest": "afe096adad34620894c92cdb7dcfc2579506552eaa05fe8fac6c09a4739ce518" + } +} diff --git a/packages/bugc/src/irgen/debug/templates.test.ts b/packages/bugc/src/irgen/debug/templates.test.ts new file mode 100644 index 0000000000..bf8974a281 --- /dev/null +++ b/packages/bugc/src/irgen/debug/templates.test.ts @@ -0,0 +1,262 @@ +/** + * A pointer that refers to shared templates must read what bugc's + * pointers read before they referred to templates. Every listed + * variable's pointer is read at every step of a run of an example: + * + * - For every example, at every level, both ways: with the + * compilation's templates, and inline (each reference replaced by its + * template's pointer, regions renamed as the reference's `yields` + * says). The two must give the same regions with the same bytes. + * - For some examples, at O0 and O2, the reads must be those of bugc + * before templates (origin/main at 9c8d7ccb1, all inline pointers): + * `templates.golden.json` holds the number of reads and a digest of + * them. `UPDATE_TEMPLATES_GOLDEN=1` writes it again from the + * compiler at hand, so it is written with a compiler from before. + */ +import { describe, it, expect, afterAll, afterEach } from "vitest"; +import { createHash } from "crypto"; +import { promises as fs } from "fs"; +import path from "path"; +import { glob } from "glob"; +import * as Format from "@ethdebug/format"; +import { createMachineState } from "@ethdebug/evm"; +import { dereference, type Data, type Machine } from "@ethdebug/pointers"; + +import { localsOf, traceLocals } from "#test/evm/locals"; +import { parseTestBlocks } from "#test/examples/annotations"; + +import type { Templates } from "./templates.js"; + +const EXAMPLES_DIR = path.resolve(__dirname, "../../../examples"); +const levels = [0, 1, 2, 3] as const; + +/** Keys whose (string) value names a region */ +const regionKeys = new Set(["name", "~read", ".offset", ".length", ".slot"]); + +/** A pointer with its regions, and references to them, renamed */ +function rename(value: unknown, renames: Record): unknown { + if (Array.isArray(value)) return value.map((v) => rename(v, renames)); + if (!value || typeof value !== "object") return value; + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + regionKeys.has(key) && typeof v === "string" && v in renames + ? renames[v] + : rename(v, renames), + ]), + ); +} + +/** + * A pointer with each reference to a template in `templates` replaced + * by the template's pointer, renamed by the reference's `yields`. + * References to templates a pointer defines inline (`local`) stay. + */ +function inline( + pointer: Format.Pointer, + templates: Templates, + local: Set = new Set(), +): Format.Pointer { + const within = (p: Format.Pointer) => inline(p, templates, local); + if (Format.Pointer.isRegion(pointer)) return pointer; + if ("group" in pointer) return { group: pointer.group.map(within) }; + if ("list" in pointer) { + return { list: { ...pointer.list, is: within(pointer.list.is) } }; + } + if ("if" in pointer) { + return { + ...pointer, + then: within(pointer.then), + ...(pointer.else ? { else: within(pointer.else) } : {}), + }; + } + if ("define" in pointer) { + return { define: pointer.define, in: within(pointer.in) }; + } + if ("templates" in pointer) { + const names = new Set([...local, ...Object.keys(pointer.templates)]); + return { + templates: Object.fromEntries( + Object.entries(pointer.templates).map(([name, template]) => [ + name, + { ...template, for: inline(template.for, templates, names) }, + ]), + ), + in: inline(pointer.in, templates, names), + }; + } + if (local.has(pointer.template)) return pointer; + return rename( + within(templates[pointer.template].for), + pointer.yields ?? {}, + ) as Format.Pointer; +} + +/** A pointer's regions at a state, with their bytes */ +async function read( + pointer: Format.Pointer, + state: Machine.State, + templates: Templates, +): Promise { + try { + const view = await ( + await dereference(pointer, { state, templates }) + ).view(state); + const regions = []; + for (const region of view.regions) { + const at = (["slot", "offset", "length"] as const).map((key) => [ + key, + (region as Partial>)[key]?.toHex(), + ]); + regions.push({ + name: region.name, + location: region.location, + ...Object.fromEntries(at), + bytes: (await view.read(region)).toHex(), + }); + } + return regions; + } catch (error) { + // (a trace holds only the storage it touched) + return { error: (error as Error).message }; + } +} + +/** The calldata to run an example with: none, and its tests' */ +function calldataOf(source: string): string[] { + return [ + ...new Set([ + "", + ...parseTestBlocks(source).flatMap(({ parsed }) => + parsed.callData ? [parsed.callData] : [], + ), + ]), + ]; +} + +/** + * Visit each listed variable's pointer, at the state after each step of + * a run of `source` with each of its calldata (contexts are + * postconditions), then each storage variable's, with the storage after + * the call. Visits are in order, labeled with the run and step. + */ +async function eachPointer( + source: string, + level: (typeof levels)[number], + visit: ( + label: string, + pointer: Format.Pointer, + state: Machine.State, + templates: Templates, + ) => Promise, +): Promise { + for (const calldata of calldataOf(source)) { + const run = await traceLocals(source, level, calldata); + // (bugc gave no templates before) + const templates = run.templates ?? {}; + for (let k = 0; k + 1 < run.steps.length; k++) { + const context = run.instructionAt(run.steps[k])?.context; + const entries = localsOf(context).filter(({ pointer }) => pointer); + if (entries.length === 0) continue; + const state = run.stateAt(k + 1); + // (give the event loop a turn; see `afterEach` below) + await new Promise((resolve) => setImmediate(resolve)); + for (const { identifier, pointer } of entries) { + const label = `${calldata.slice(0, 8)} ${k} ${identifier}`; + await visit(label, pointer as Format.Pointer, state, templates); + } + } + const state = createMachineState(await run.executor.currentState()); + for (const { identifier, pointer } of localsOf(run.program.context)) { + if (!pointer) continue; + const label = `${calldata.slice(0, 8)} end ${identifier}`; + await visit(label, pointer as Format.Pointer, state, templates); + } + } +} + +// A long run of reads never turns the event loop, so the worker cannot +// read vitest's replies to its progress reports, and once that lasts a +// minute, vitest fails the run with 'Timeout calling "onTaskUpdate"' +// (as in test/examples/examples.test.ts). +afterEach(() => new Promise((resolve) => setImmediate(resolve))); + +async function examples(): Promise> { + const files = await glob("**/*.bug", { cwd: EXAMPLES_DIR }); + const sources: Array<[string, string]> = []; + for (const file of files.sort()) { + const source = await fs.readFile(path.join(EXAMPLES_DIR, file), "utf-8"); + if (!/\/\/ @(wip|skip|expect-)/.test(source)) sources.push([file, source]); + } + return sources; +} + +describe("pointers that refer to templates read as written out", async () => { + for (const [file, source] of await examples()) { + for (const level of levels) { + it(`${file} at O${level}`, async () => { + let pointers = 0; + let reads = 0; + await eachPointer(source, level, async (_, pointer, state, t) => { + const read_ = await read(pointer, state, t); + expect(read_).toEqual(await read(inline(pointer, t), state, {})); + pointers++; + if (Array.isArray(read_)) reads++; + }); + // (not all failing alike) + if (pointers > 0) expect(reads).toBeGreaterThan(0); + }, 60_000); + } + } +}); + +const GOLDEN = path.join(__dirname, "templates.golden.json"); +const goldenExamples = [ + "advanced/nested-arrays.bug", + "advanced/nested-structs.bug", + "advanced/token-registry.bug", + "basic/functions.bug", + "intermediate/mappings.bug", + "intermediate/memory-arrays.bug", + "intermediate/pointer-shapes.bug", + "intermediate/strings.bug", +]; + +describe("pointers read as they did before templates", async () => { + const update = !!process.env.UPDATE_TEMPLATES_GOLDEN; + const golden: Record = update + ? {} + : JSON.parse(await fs.readFile(GOLDEN, "utf-8")); + if (update) { + afterAll(() => + fs.writeFile(GOLDEN, JSON.stringify(golden, null, 2) + "\n"), + ); + } + + for (const file of goldenExamples) { + for (const level of [0, 2] as const) { + const key = `${file} at O${level}`; + it( + key, + async () => { + const source = await fs.readFile( + path.join(EXAMPLES_DIR, file), + "utf-8", + ); + const hash = createHash("sha256"); + let reads = 0; + await eachPointer(source, level, async (label, pointer, state, t) => { + const read_ = await read(pointer, state, t); + hash.update(JSON.stringify([label, read_]) + "\n"); + if (Array.isArray(read_)) reads++; + }); + const actual = { reads, digest: hash.digest("hex") }; + expect(reads).toBeGreaterThan(0); + if (update) golden[key] = actual; + else expect(actual).toEqual(golden[key]); + }, + 60_000, + ); + } + } +}); diff --git a/packages/bugc/src/irgen/debug/templates.ts b/packages/bugc/src/irgen/debug/templates.ts new file mode 100644 index 0000000000..9d15dc7f95 --- /dev/null +++ b/packages/bugc/src/irgen/debug/templates.ts @@ -0,0 +1,145 @@ +/** + * Pointer templates shared by a compilation's pointers + * + * A pointer shape that repeats (a local in the frame, a string in + * storage, a struct in memory, ...) is defined once, by name, as a + * pointer template in the compilation's info resources (`pointers`), + * and each pointer of that shape refers to it: the template's expected + * variables are bound by a scope (`define`) around the reference, and + * its regions are renamed by the reference's `yields`, so they keep the + * names they would have in an inline pointer. + */ +import * as Format from "@ethdebug/format"; + +import { Type } from "#types"; + +export type Templates = Format.Pointer.Templates; + +/** + * Template `name` in `templates`, for `what` (a type, as BUG writes + * it, or the name itself): defined, if it is not there yet, as `make` + * gives it (undefined: none). Its name, if defined. A name defined for + * something else is an error: a reference to it would read another + * layout. + */ +export function define( + templates: Templates, + name: string, + make: () => Format.Pointer.Template | undefined, + what: string = name, +): string | undefined { + const defined = definedFor.get(templates) ?? new Map(); + definedFor.set(templates, defined); + const other = defined.get(name); + if (other !== undefined && other !== what) { + throw new Error( + `Pointer template ${name} is defined for ${other}, not ${what}`, + ); + } + if (!(name in templates)) { + const template = make(); + if (!template) return undefined; + templates[name] = template; + } + defined.set(name, what); + return name; +} + +/** What each template in a collection is defined for (see `define`) */ +const definedFor = new WeakMap>(); + +/** + * A reference to template `name`: with `define` binding its variables + * (a variable bound to itself is left out), and `yields` renaming its + * regions (a name kept as it is is left out). + */ +export function referTo( + name: string, + define: Record = {}, + yields: Record = {}, +): Format.Pointer { + const renames = Object.entries(yields).filter(([from, to]) => from !== to); + const ref: Format.Pointer = { + template: name, + ...(renames.length > 0 ? { yields: Object.fromEntries(renames) } : {}), + }; + const bindings = Object.entries(define).filter(([name, e]) => e !== name); + return bindings.length > 0 + ? { define: Object.fromEntries(bindings), in: ref } + : ref; +} + +/** + * The names of the regions a pointer gives, in order, each once: its + * regions', and those of the templates it refers to, as renamed. Region + * names in templates a pointer defines inline (a mapping's entry) are + * not its own. + */ +export function regionNames( + pointer: Format.Pointer, + templates: Templates, +): string[] { + const known = namesOf.get(pointer); + if (known) return known; + const names = new Set(); + const visit = (pointer: Format.Pointer): void => { + if (Format.Pointer.isRegion(pointer)) { + if (pointer.name !== undefined) names.add(pointer.name); + return; + } + if ("group" in pointer) return pointer.group.forEach(visit); + if ("list" in pointer) return visit(pointer.list.is); + if ("if" in pointer) { + visit(pointer.then); + if (pointer.else) visit(pointer.else); + return; + } + if ("in" in pointer) return visit(pointer.in); + if ("template" in pointer) { + const yields: Record = pointer.yields ?? {}; + const { for: body } = templates[pointer.template]; + for (const name of regionNames(body, templates)) { + names.add(yields[name] ?? name); + } + } + }; + visit(pointer); + namesOf.set(pointer, [...names]); + return [...names]; +} + +/** The region names of pointers already visited (templates' pointers + * are visited once for each reference to them) */ +const namesOf = new WeakMap(); + +/** Renames of `names` with `prefix` (for `yields`) */ +export function prefixed( + names: string[], + prefix: string, +): Record { + return Object.fromEntries(names.map((name) => [name, `${prefix}${name}`])); +} + +/** + * A type's name as part of a template name, distinct for each type: + * its words as BUG writes them, in order, joined with dashes, without + * the brackets. `array` and `mapping` always take the same number of + * types (an array's size, a number, is a word of its own), so the + * words say how the type nests: `array, 3>` is + * `array-3-array-uint256`, `array>` is + * `array-array-3-uint256`, and `mapping` is + * `mapping-address-Player`. + */ +export function typeName(type: Type): string { + if (Type.isArray(type)) { + return [ + "array", + ...(type.size === undefined ? [] : [type.size]), + typeName(type.element), + ].join("-"); + } + if (Type.isMapping(type)) { + return `mapping-${typeName(type.key)}-${typeName(type.value)}`; + } + return Type.format(type).replace(/[^a-zA-Z0-9$_-]+/g, "-"); +} diff --git a/packages/bugc/src/irgen/debug/variables.test.ts b/packages/bugc/src/irgen/debug/variables.test.ts index 6be4b92597..6ab1ca9033 100644 --- a/packages/bugc/src/irgen/debug/variables.test.ts +++ b/packages/bugc/src/irgen/debug/variables.test.ts @@ -53,18 +53,27 @@ code { /** * The pointer to an entry of a mapping, from the entry template that a - * pointer defines (with the mapping's slot and the key) + * pointer defines (with the mapping's slot and the key). A mapping + * variable's pointer refers to its type's template, which defines it. */ function entryOf( pointer: Format.Pointer, + templates: Format.Pointer.Templates, template: string, define: Record, ): Format.Pointer { - if (!Format.Pointer.Collection.isTemplates(pointer)) { + const reference = Format.Pointer.Collection.isScope(pointer) + ? pointer.in + : pointer; + if (!Format.Pointer.Collection.isReference(reference)) { + throw new Error("pointer refers to no template"); + } + const { for: body } = templates[reference.template]; + if (!Format.Pointer.Collection.isTemplates(body)) { throw new Error("pointer defines no templates"); } return { - templates: pointer.templates, + templates: body.templates, in: { define, in: { template } }, }; } @@ -73,9 +82,10 @@ function entryOf( async function readRegions( pointer: Format.Pointer, state: ReturnType, + templates: Format.Pointer.Templates, name: string, ): Promise { - const cursor = await dereference(pointer, { state }); + const cursor = await dereference(pointer, { state, templates }); const view = await cursor.view(state); return Promise.all( view.regions @@ -99,37 +109,39 @@ describe("storage string and bytes pointers", () => { const string = bytesToHex(new TextEncoder().encode(text(length))); const bytes = bytesToHex(hexToBytes(hex(length))); - expect(await readRegions(pointerOf("s"), state, "data")).toEqual([ - string, - ]); - expect(await readRegions(pointerOf("b"), state, "data")).toEqual([ - bytes, - ]); - expect(await readRegions(pointerOf("p"), state, "id")).toEqual([ - "0".repeat(63) + "1", - ]); - expect(await readRegions(pointerOf("p"), state, "name-data")).toEqual([ - string, - ]); - expect(await readRegions(pointerOf("p"), state, "tag-data")).toEqual([ - bytes, - ]); - expect(await readRegions(pointerOf("a"), state, "data")).toEqual([ - "", - string, - ]); + expect( + await readRegions(pointerOf("s"), state, run.templates, "data"), + ).toEqual([string]); + expect( + await readRegions(pointerOf("b"), state, run.templates, "data"), + ).toEqual([bytes]); + expect( + await readRegions(pointerOf("p"), state, run.templates, "id"), + ).toEqual(["0".repeat(63) + "1"]); + expect( + await readRegions(pointerOf("p"), state, run.templates, "name-data"), + ).toEqual([string]); + expect( + await readRegions(pointerOf("p"), state, run.templates, "tag-data"), + ).toEqual([bytes]); + expect( + await readRegions(pointerOf("a"), state, run.templates, "data"), + ).toEqual(["", string]); // A mapping's entry `u[7]` comes from its entry template - const entry = entryOf(pointerOf("u"), "entry", { slot: 7, key: 7 }); - expect(await readRegions(entry, state, "value-id")).toEqual([ - "0".repeat(63) + "2", - ]); - expect(await readRegions(entry, state, "value-name-data")).toEqual([ - string, - ]); - expect(await readRegions(entry, state, "value-tag-data")).toEqual([ - bytes, - ]); + const entry = entryOf(pointerOf("u"), run.templates, "entry", { + slot: 7, + key: 7, + }); + expect( + await readRegions(entry, state, run.templates, "value-id"), + ).toEqual(["0".repeat(63) + "2"]); + expect( + await readRegions(entry, state, run.templates, "value-name-data"), + ).toEqual([string]); + expect( + await readRegions(entry, state, run.templates, "value-tag-data"), + ).toEqual([bytes]); }); } } @@ -180,6 +192,7 @@ code { }); if (!result.success) throw new Error("compile failed"); const { bytecode } = result.value; + const templates = bytecode.resources.pointers; const executor = new Executor(); await executor.deploy(bytesToHex(bytecode.create ?? bytecode.runtime)); for (const { key, name } of players) { @@ -196,36 +209,81 @@ code { for (const { key, name } of players) { const text = new TextEncoder().encode(name); - const player = entryOf(pointerOf("players"), "entry", { + const player = entryOf(pointerOf("players"), templates, "entry", { slot: 4, key, }); - expect(await readRegions(player, state, "value-score")).toEqual([ - word(text.length).slice(-16), - ]); - expect(await readRegions(player, state, "value-combo")).toEqual([ - "00000003", - ]); - expect(await readRegions(player, state, "value-name-data")).toEqual([ - bytesToHex(text), - ]); + expect( + await readRegions(player, state, templates, "value-score"), + ).toEqual([word(text.length).slice(-16)]); + expect( + await readRegions(player, state, templates, "value-combo"), + ).toEqual(["00000003"]); + expect( + await readRegions(player, state, templates, "value-name-data"), + ).toEqual([bytesToHex(text)]); - const plays = entryOf(pointerOf("plays"), "entry", { slot: 5, key }); - expect(await readRegions(plays, state, "value")).toEqual([word(1)]); + const plays = entryOf(pointerOf("plays"), templates, "entry", { + slot: 5, + key, + }); + expect(await readRegions(plays, state, templates, "value")).toEqual([ + word(1), + ]); // `grid[key]` is a mapping: its slot is the inner mapping's, // and the inner template gives `grid[key][len]` - const grid = entryOf(pointerOf("grid"), "entry", { slot: 6, key }); - const cursor = await dereference(grid, { state }); + const grid = entryOf(pointerOf("grid"), templates, "entry", { + slot: 6, + key, + }); + const cursor = await dereference(grid, { state, templates }); const [inner] = (await cursor.view(state)).regions; expect(inner.name).toEqual("value"); if (!("slot" in inner)) throw new Error("not a storage region"); - const cell = entryOf(pointerOf("grid"), "value-entry", { + const cell = entryOf(pointerOf("grid"), templates, "value-entry", { slot: inner.slot.toHex(), key: text.length, }); - expect(await readRegions(cell, state, "value")).toEqual(["07"]); + expect(await readRegions(cell, state, templates, "value")).toEqual([ + "07", + ]); } }); } }); + +/** + * Each type has its own template: two array types whose words are the + * same, in another order, must not share one (the second would read the + * first's layout). + */ +describe("storage templates of nested array types", () => { + const program = `name Nested; +storage { [0] a: array, 3>; [3] b: array>; } +code { a[1].push(5); a[1].push(6); }`; + + for (const level of levels) { + it(`reads each by its own template (level ${level})`, async () => { + const run = await traceLocals(program, level); + const state = createMachineState(await run.executor.currentState()); + const pointerOf = (identifier: string) => + ( + run.program.context as Format.Program.Context.Variables + ).variables.find((v) => v.identifier === identifier)!.pointer!; + const templateOf = (pointer: Format.Pointer) => + JSON.stringify(pointer).match(/"template":"([^"]+)"/)?.[1]; + + expect(templateOf(pointerOf("a"))).not.toEqual( + templateOf(pointerOf("b")), + ); + const word = (value: number) => value.toString(16).padStart(64, "0"); + const read = (identifier: string, name: string) => + readRegions(pointerOf(identifier), state, run.templates, name); + expect(await read("a", "array-length")).toEqual([0, 2, 0].map(word)); + expect(await read("a", "element")).toEqual([5, 6].map(word)); + expect(await read("b", "array-length")).toEqual([word(0)]); + expect(await read("b", "element")).toEqual([]); + }); + } +}); diff --git a/packages/bugc/src/irgen/debug/variables.ts b/packages/bugc/src/irgen/debug/variables.ts index 08e5d26153..e7704362d2 100644 --- a/packages/bugc/src/irgen/debug/variables.ts +++ b/packages/bugc/src/irgen/debug/variables.ts @@ -9,6 +9,13 @@ import type { State } from "../generate/state.js"; import { generatePointer, type VariableLocation } from "./pointers.js"; import { Storage, Type } from "#types"; import { convertBugType } from "./types.js"; +import { + type Templates, + define, + prefixed, + referTo, + typeName, +} from "./templates.js"; /** * Information about a variable available for debug contexts @@ -70,10 +77,13 @@ function plus( * A mapping's entries depend on a key, so each mapping in the pointer * gets a template for its entry (see `generateMappingPointer`). The * templates go in one `templates` collection around the whole pointer. + * A string's pointer refers to a shared template, defined in `shared` + * (see `generateStorageBytesPointer`). */ export function generateStoragePointer( baseSlot: Format.Pointer.Expression, bugType: Type, + shared: Templates, byteOffset: number = 0, prefix: string = "", depth: number = 0, @@ -84,6 +94,7 @@ export function generateStoragePointer( const pointer = generateStoragePointer( baseSlot, bugType, + shared, byteOffset, prefix, depth, @@ -108,6 +119,7 @@ export function generateStoragePointer( const fieldPointer = generateStoragePointer( fieldSlot, fieldType, + shared, fieldOffset, `${prefix}${fieldName}-`, depth, @@ -169,6 +181,7 @@ export function generateStoragePointer( ], }, element, + shared, 0, prefix, depth + 1, @@ -206,7 +219,7 @@ export function generateStoragePointer( } if (Type.isMapping(bugType)) { - return generateMappingPointer(baseSlot, bugType, prefix, entries); + return generateMappingPointer(baseSlot, bugType, prefix, shared, entries); } if ( @@ -214,7 +227,7 @@ export function generateStoragePointer( (Type.Elementary.isBytes(bugType) && Type.Elementary.Bytes.isDynamic(bugType)) ) { - return generateStorageBytesPointer(baseSlot, prefix); + return generateStorageBytesPointer(baseSlot, prefix, shared); } // For elementary types, generate pointer with offset and length @@ -264,12 +277,18 @@ function generateMappingPointer( slot: Format.Pointer.Expression, mapping: Type.Mapping, prefix: string, + shared: Templates, { templates, scope }: Entries, ): Format.Pointer { - const value = generateStoragePointer("slot", mapping.value, 0, "value-", 0, { - templates, - scope: `${scope}${prefix}`, - }); + const value = generateStoragePointer( + "slot", + mapping.value, + shared, + 0, + "value-", + 0, + { templates, scope: `${scope}${prefix}` }, + ); if (value) { templates[`${scope}${prefix}entry`] = { expect: ["slot", "key"], @@ -289,50 +308,50 @@ function generateMappingPointer( } /** - * A pointer for a storage string or dynamic `bytes`, by Solidity's + * The template for a storage string or dynamic `bytes`, by Solidity's * encoding, in the shape solc gives its `t_string_storage` template. - * The slot's low byte is even for a short value (up to 31 bytes): the - * slot holds the data, left-aligned, and length * 2 in that byte. Else - * the slot holds length * 2 + 1, and the data starts at - * keccak256(slot), in one region across as many slots as it needs. + * It expects `slot`. The slot's low byte is even for a short value (up + * to 31 bytes): the slot holds the data, left-aligned, and length * 2 + * in that byte. Else the slot holds length * 2 + 1, and the data starts + * at keccak256(slot), in one region across as many slots as it needs. */ -function generateStorageBytesPointer( - slot: Format.Pointer.Expression, - prefix: string, -): Format.Pointer { - const flag = `${prefix}length-flag`; - const longLength = `${prefix}long-length`; - const data = `${prefix}data`; - return { +const storageBytes: Format.Pointer.Template = { + expect: ["slot"], + for: { group: [ { - name: flag, + name: "length-flag", location: "storage", - slot, + slot: "slot", offset: { "~difference": ["~wordsize", 1] }, length: 1, }, { - if: { "~remainder": [{ "~sum": [{ "~read": flag }, 1] }, 2] }, + if: { "~remainder": [{ "~sum": [{ "~read": "length-flag" }, 1] }, 2] }, then: { - define: { length: { "~quotient": [{ "~read": flag }, 2] } }, - in: { name: data, location: "storage", slot, length: "length" }, + define: { length: { "~quotient": [{ "~read": "length-flag" }, 2] } }, + in: { + name: "data", + location: "storage", + slot: "slot", + length: "length", + }, }, else: { group: [ - { name: longLength, location: "storage", slot }, + { name: "long-length", location: "storage", slot: "slot" }, { define: { length: { "~quotient": [ - { "~difference": [{ "~read": longLength }, 1] }, + { "~difference": [{ "~read": "long-length" }, 1] }, 2, ], }, - start: { "~keccak256": [{ "~wordsized": slot }] }, + start: { "~keccak256": [{ "~wordsized": "slot" }] }, }, in: { - name: data, + name: "data", location: "storage", slot: "start", length: "length", @@ -342,7 +361,52 @@ function generateStorageBytesPointer( }, }, ], - }; + }, +}; + +/** + * A pointer for a storage string or dynamic `bytes` at `slot`: a + * reference to the `storage-bytes` template, its regions named + * `length-flag`, `long-length` and `data`. + */ +function generateStorageBytesPointer( + slot: Format.Pointer.Expression, + prefix: string, + shared: Templates, +): Format.Pointer { + define(shared, "storage-bytes", () => storageBytes); + return referTo( + "storage-bytes", + { slot }, + prefixed(["length-flag", "long-length", "data"], prefix), + ); +} + +/** + * The pointer for a storage variable of type `bugType` at `slot`. A + * value type's (a single region, a packed field) is given inline, a + * string's or dynamic `bytes`'s refers to the `storage-bytes` + * template. Any other type's refers to a template for the type, + * `storage-`, which expects `slot` and is defined in `shared`. + */ +export function generateStorageVariablePointer( + slot: Format.Pointer.Expression, + bugType: Type, + shared: Templates, +): Format.Pointer | undefined { + if (Type.isElementary(bugType)) { + return generateStoragePointer(slot, bugType, shared); + } + const name = define( + shared, + `storage-${typeName(bugType)}`, + () => { + const pointer = generateStoragePointer("slot", bugType, shared); + return pointer && { expect: ["slot"], for: pointer }; + }, + Type.format(bugType), + ); + return name === undefined ? undefined : referTo(name, { slot }); } /** @@ -355,10 +419,13 @@ function generateStorageBytesPointer( * * SSA temps are NOT included because they don't have concrete runtime * locations until EVM code generation. + * + * The templates the pointers refer to are defined in `templates`. */ export function collectVariablesWithLocations( state: State, sourceId: string, + templates: Templates = {}, ): VariableInfo[] { const variables: VariableInfo[] = []; @@ -389,7 +456,11 @@ export function collectVariablesWithLocations( } // Generate sophisticated pointer based on type - const pointer = generateStoragePointer(storageDecl.slot, bugType); + const pointer = generateStorageVariablePointer( + storageDecl.slot, + bugType, + templates, + ); if (!pointer) continue; // Convert Bug type to ethdebug format type diff --git a/packages/bugc/src/irgen/generate/module.ts b/packages/bugc/src/irgen/generate/module.ts index caa8bb4471..d16ec77285 100644 --- a/packages/bugc/src/irgen/generate/module.ts +++ b/packages/bugc/src/irgen/generate/module.ts @@ -92,7 +92,12 @@ export function* buildModule( // Build program-level debug context with storage variables const state: State = yield { type: "peek" }; - const variables = collectVariablesWithLocations(state, module_.sourceId); + const pointerTemplates = {}; + const variables = collectVariablesWithLocations( + state, + module_.sourceId, + pointerTemplates, + ); const debugContext = variables.length > 0 ? { variables: variables.map(toVariableContextEntry) } @@ -105,6 +110,7 @@ export function* buildModule( functions: module_.functions, main: module_.main || createEmptyFunction("main"), debugContext, + pointerTemplates, }; if (module_.create) { diff --git a/packages/bugc/src/irgen/generate/storage.test.ts b/packages/bugc/src/irgen/generate/storage.test.ts index e2fccf7e17..daa61aa571 100644 --- a/packages/bugc/src/irgen/generate/storage.test.ts +++ b/packages/bugc/src/irgen/generate/storage.test.ts @@ -130,7 +130,10 @@ describe("packed storage struct field pointers", () => { expect(variable?.pointer).toBeDefined(); const state = createMachineState(await executor.currentState()); - const cursor = await dereference(variable!.pointer!, { state }); + const cursor = await dereference(variable!.pointer!, { + state, + templates: compiled.value.bytecode.resources.pointers, + }); const view = await cursor.view(state); const read = async (name: string) => (await view.read(view.regions.lookup[name])).asUint(); diff --git a/packages/bugc/src/optimizer/optimizer.ts b/packages/bugc/src/optimizer/optimizer.ts index 95b19ebd6a..15d307b0d1 100644 --- a/packages/bugc/src/optimizer/optimizer.ts +++ b/packages/bugc/src/optimizer/optimizer.ts @@ -210,6 +210,9 @@ export abstract class BaseOptimizationStep implements OptimizationStep { // Keep the program-level context (storage variables); dropping it // here erased those variables from optimization level 1 upward. ...(module.debugContext ? { debugContext: module.debugContext } : {}), + ...(module.pointerTemplates + ? { pointerTemplates: module.pointerTemplates } + : {}), }; } diff --git a/packages/bugc/test/evm/locals.ts b/packages/bugc/test/evm/locals.ts index 2e8d876706..067a1cfc0f 100644 --- a/packages/bugc/test/evm/locals.ts +++ b/packages/bugc/test/evm/locals.ts @@ -21,6 +21,8 @@ export type Level = 0 | 1 | 2 | 3; export interface LocalsTrace { program: Format.Program; + /** The pointer templates the program's pointers refer to */ + templates: Format.Pointer.Templates; executor: Executor; trace: Trace; steps: readonly TraceStep[]; @@ -61,6 +63,7 @@ export async function traceLocals( ); return { program, + templates: bytecode.resources.pointers, executor, trace, steps: trace.steps, @@ -228,8 +231,9 @@ export async function check( for (const entry of localsOf(context)) { const pointer = entry.pointer as Record | undefined; - // A storage variable (a local's pointer is never into storage) - if (!pointer || JSON.stringify(pointer).includes('"storage"')) continue; + // A storage variable (a local's pointer is never into storage, nor + // refers to a storage template) + if (!pointer || /"storage(-|")/.test(JSON.stringify(pointer))) continue; const name = entry.identifier as string; const key = keyOf(entry); const where = `after ${steps[k].opcode} at pc ${steps[k].pc}`; @@ -319,7 +323,10 @@ function stringify(value: unknown): string { async function regionsAt(pointer: unknown, run: LocalsTrace, index: number) { const state = run.stateAt(index); const { memory, calldata } = run.trace.stateAt(index); - const cursor = await dereference(pointer as Format.Pointer, { state }); + const cursor = await dereference(pointer as Format.Pointer, { + state, + templates: run.templates, + }); const view = await cursor.view(state); const regions = [...view.regions]; let next = 0; diff --git a/packages/bugc/test/examples/examples.test.ts b/packages/bugc/test/examples/examples.test.ts index e6dcea09c8..57f899e13b 100644 --- a/packages/bugc/test/examples/examples.test.ts +++ b/packages/bugc/test/examples/examples.test.ts @@ -61,6 +61,7 @@ interface CompiledBytecode { createInstructions?: Instruction[]; runtimeProgram: Format.Program; createProgram?: Format.Program; + resources: { pointers: Format.Pointer.Templates }; } interface ExampleInfo { @@ -280,7 +281,15 @@ describe("Example Files", async () => { expect.fail(`Compilation failed at O${level}`); } - const { runtimeProgram, createProgram } = result.value.bytecode; + const { runtimeProgram, createProgram, resources } = + result.value.bytecode; + // (as written out, as JSON) + expect( + await invalidLocations( + "schema:ethdebug/format/info/resources", + JSON.parse(JSON.stringify(resources)), + ), + ).toEqual([]); const storageNames = (result.value.ast.storage ?? []).map( ({ name }) => name, ); diff --git a/packages/bugc/test/examples/runners.ts b/packages/bugc/test/examples/runners.ts index 547b99347d..66629055cb 100644 --- a/packages/bugc/test/examples/runners.ts +++ b/packages/bugc/test/examples/runners.ts @@ -16,6 +16,13 @@ import type { VariablesTest } from "./annotations.js"; import type { SourceMapping } from "./source-map.js"; import { findInstructionsAtLine } from "./source-map.js"; +/** Compiled bytecode, and the templates its pointers refer to */ +interface Bytecode { + runtime: Uint8Array; + create?: Uint8Array; + resources: { pointers: Format.Pointer.Templates }; +} + export interface TestResult { passed: boolean; message?: string; @@ -31,7 +38,7 @@ function toHex(bytes: Uint8Array): string { * Run a variables test: check pointer structure and/or dereferenced values. */ export async function runVariablesTest( - bytecode: { runtime: Uint8Array; create?: Uint8Array }, + bytecode: Bytecode, instructions: Evm.Instruction[], sourceMapping: SourceMapping, test: VariablesTest, @@ -133,7 +140,7 @@ export async function runVariablesTest( * Deploy contract and check a scalar value (first region). */ async function checkScalarValue( - bytecode: { runtime: Uint8Array; create?: Uint8Array }, + bytecode: Bytecode, pointer: Format.Pointer, name: string, expectedValue: string | number | bigint, @@ -163,7 +170,10 @@ async function checkScalarValue( // Dereference the pointer const state = createMachineState(await executor.currentState()); - const cursor = await dereference(pointer, { state }); + const cursor = await dereference(pointer, { + state, + templates: bytecode.resources.pointers, + }); const view = await cursor.view(state); if (view.regions.length === 0) { @@ -202,7 +212,7 @@ async function checkScalarValue( * values is an object like { length: 3, element: [100, 200, 300] } */ async function checkRegionValues( - bytecode: { runtime: Uint8Array; create?: Uint8Array }, + bytecode: Bytecode, pointer: Format.Pointer, varName: string, expectedValues: Record< @@ -235,7 +245,10 @@ async function checkRegionValues( // Dereference the pointer const state = createMachineState(await executor.currentState()); - const cursor = await dereference(pointer, { state }); + const cursor = await dereference(pointer, { + state, + templates: bytecode.resources.pointers, + }); const view = await cursor.view(state); // Check each expected region diff --git a/packages/conformance/src/adapters/bugc.ts b/packages/conformance/src/adapters/bugc.ts index 84f444e899..155c52a34d 100644 --- a/packages/conformance/src/adapters/bugc.ts +++ b/packages/conformance/src/adapters/bugc.ts @@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import type { Materials } from "@ethdebug/format"; -import { Data } from "@ethdebug/format"; import { VERSION, compile } from "@ethdebug/bugc"; import type { BugcCompileOptions, EthdebugArtifact } from "../types.js"; @@ -113,11 +112,7 @@ export async function compileBugc( sources, programs, compilation, - resources: Data.stamp("ethdebug/format/info/resources", { - compilation, - types: {}, - pointers: {}, - }), + resources: { ...bytecode.resources, compilation }, raw: result.value, }; } diff --git a/packages/web/docs/implementation-guides/building-a-trace-viewer.mdx b/packages/web/docs/implementation-guides/building-a-trace-viewer.mdx index c9ef15b7e8..5359c0292b 100644 --- a/packages/web/docs/implementation-guides/building-a-trace-viewer.mdx +++ b/packages/web/docs/implementation-guides/building-a-trace-viewer.mdx @@ -28,6 +28,8 @@ Load compiled program data containing: - Instruction list with contexts - Source materials - Type definitions +- Pointer templates (the `pointers` of the compilation's info resources), + which pointers can refer to by name ## 3. Pointer resolver @@ -37,7 +39,10 @@ Use `@ethdebug/pointers` to resolve variable locations: import { dereference } from "@ethdebug/pointers"; // For each variable in scope -const cursor = await dereference(variable.pointer, { state: machineState }); +const cursor = await dereference(variable.pointer, { + state: machineState, + templates: resources.pointers, +}); const view = await cursor.view(machineState); const value = await view.read(view.regions[0]); ``` diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 402f21e403..5b537b2b76 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -28,6 +28,7 @@ import { createTrace, type TraceStep as EvmStep, } from "@ethdebug/evm"; +import type { Pointer } from "@ethdebug/format"; import { dereference, Data, type Machine } from "@ethdebug/pointers"; import { storageByStep } from "./storageByStep"; import { @@ -65,6 +66,8 @@ interface CompileResult { success: boolean; error?: string; bytecode?: BytecodeOutput; + /** The pointer templates the debug info's pointers refer to */ + templates?: Pointer.Templates; } /** bugc optimizer levels the tracer can compile at. */ @@ -322,6 +325,7 @@ function TraceDrawerContent(): JSX.Element { // stepIndex is the step whose observed state its argument // pointers describe (the callee entry's postcondition). const argCacheRef = useRef>(new Map()); + const templates = compileResult?.templates; const [resolvedArgs, setResolvedArgs] = useState>( new Map(), @@ -364,7 +368,7 @@ function TraceDrawerContent(): JSX.Element { const resolvePromises = ptrs.map(async (ptr, i) => { try { - const value = await resolvePointer(ptr, state); + const value = await resolvePointer(ptr, state, templates); args[i] = { ...args[i], value }; } catch (err) { args[i] = { @@ -391,7 +395,7 @@ function TraceDrawerContent(): JSX.Element { return () => { cancelled = true; }; - }, [callStack, trace, storages]); + }, [callStack, trace, storages, templates]); // Resolve the current instruction's variable values by // dereferencing each variable's pointer against the step @@ -415,7 +419,10 @@ function TraceDrawerContent(): JSX.Element { currentVariables.map(async (v) => { if (!v.pointer) return; try { - next.set(v.identifier, await resolvePointer(v.pointer, state)); + next.set( + v.identifier, + await resolvePointer(v.pointer, state, templates), + ); } catch { // leave unresolved } @@ -427,7 +434,7 @@ function TraceDrawerContent(): JSX.Element { return () => { cancelled = true; }; - }, [currentVariables, currentStep, trace, storage]); + }, [currentVariables, currentStep, trace, storage, templates]); // Gas remaining at the current step plus the delta consumed // reaching it (when the executor reports gas). @@ -479,7 +486,11 @@ function TraceDrawerContent(): JSX.Element { createInstructions: result.value.bytecode.createInstructions, }; - setCompileResult({ success: true, bytecode }); + setCompileResult({ + success: true, + bytecode, + templates: result.value.bytecode.resources.pointers, + }); } catch (e) { setCompileResult({ success: false, @@ -1435,10 +1446,11 @@ function traceStepToState( async function resolvePointer( pointer: unknown, state: Machine.State, + templates: Pointer.Templates = {}, ): Promise { const cursor = await dereference( pointer as Parameters[0], - { state, templates: {} }, + { state, templates }, ); const view = await cursor.view(state); diff --git a/packages/web/src/theme/ProgramExample/TraceViewer.tsx b/packages/web/src/theme/ProgramExample/TraceViewer.tsx index c2348beb7b..f73dd697b2 100644 --- a/packages/web/src/theme/ProgramExample/TraceViewer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceViewer.tsx @@ -5,7 +5,7 @@ */ import React from "react"; -import type { Program, Materials } from "@ethdebug/format"; +import type { Program, Materials, Pointer } from "@ethdebug/format"; import { TraceProvider, TraceControls, @@ -30,6 +30,9 @@ export interface TraceViewerProps { trace: TraceStep[]; /** The program definition */ program: Program; + /** The pointer templates its pointers refer to (the `pointers` of + * the compilation's info resources) */ + templates?: Pointer.Templates; /** Source materials for highlighting */ sources?: Materials.Source[]; /** Initial step index */ @@ -48,6 +51,7 @@ export interface TraceViewerProps { export function TraceViewer({ trace, program, + templates, sources = [], initialStepIndex = 0, showVariables = true, @@ -59,6 +63,7 @@ export function TraceViewer({