From 1a8a752f9e372e1ea377686ec7559f684793f713 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 12 Jan 2026 18:25:01 -0500 Subject: [PATCH 1/2] Add `yields` property to pointer template references - Define optional `yields` property on reference collection schema to map template region names to new names - Implement rename context stack in dereference loop to apply mappings - Save regions under both original names (for internal references) and new names (for external access) - Update struct storage example to use `packed-field` template with `yields` for x, y, salt fields - Add reference collection documentation page - Remove resolved limitation from pointer status banner Closes #122 --- packages/format/src/types/pointer/pointer.ts | 10 +- packages/pointers/src/dereference/generate.ts | 45 +++- .../pointers/src/dereference/index.test.ts | 198 ++++++++++++++++++ packages/pointers/src/dereference/memo.ts | 38 +++- packages/pointers/src/dereference/process.ts | 12 +- packages/pointers/src/test-cases.ts | 19 +- .../web/spec/pointer/collection/reference.mdx | 14 ++ packages/web/spec/pointer/overview.mdx | 2 + packages/web/src/schemas.ts | 2 +- packages/web/src/status/status-config.ts | 2 - schemas/pointer.schema.yaml | 45 ++-- .../pointer/collection/reference.schema.yaml | 17 ++ 12 files changed, 373 insertions(+), 31 deletions(-) create mode 100644 packages/web/spec/pointer/collection/reference.mdx diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index 76661f584..ab31d922a 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -208,13 +208,21 @@ export namespace Pointer { export interface Reference { template: string; + yields?: Record; } export const isReference = (value: unknown): value is Reference => !!value && typeof value === "object" && "template" in value && - typeof value.template === "string" && !!value.template + typeof value.template === "string" && !!value.template && + (!("yields" in value) || ( + typeof value.yields === "object" && + value.yields !== null && + Object.entries(value.yields as Record).every( + ([k, v]) => isIdentifier(k) && isIdentifier(v) + ) + )) } export type Expression = diff --git a/packages/pointers/src/dereference/generate.ts b/packages/pointers/src/dereference/generate.ts index 06c25e700..a63f44fdb 100644 --- a/packages/pointers/src/dereference/generate.ts +++ b/packages/pointers/src/dereference/generate.ts @@ -35,6 +35,9 @@ export async function* generateRegions( variables } = options; + // Stack of rename mappings for template references with yields + const renameStack: Array> = []; + const stack: Memo[] = [Memo.dereferencePointer(pointer)]; while (stack.length > 0) { const memo: Memo = stack.pop() as Memo; @@ -42,17 +45,55 @@ export async function* generateRegions( let memos: Memo[] = []; switch (memo.kind) { case "dereference-pointer": { - memos = yield* processPointer(memo.pointer, options); + // Process the pointer, intercepting yielded regions to apply renames + const process = processPointer(memo.pointer, options); + let result = await process.next(); + while (!result.done) { + let region = result.value; + + // Apply rename if in context and region has a name in mapping + const currentMapping = renameStack[renameStack.length - 1]; + if (currentMapping && region.name) { + const newName = currentMapping[region.name]; + if (newName && newName !== region.name) { + region = { ...region, name: newName }; + } + } + + yield region; + result = await process.next(); + } + memos = result.value; break; } case "save-regions": { - Object.assign(regions, memo.regions); + for (const [name, region] of Object.entries(memo.regions)) { + // Save under original name for internal reference resolution + regions[name] = region; + } break; } case "save-variables": { Object.assign(variables, memo.variables); break; } + case "push-region-renames": { + renameStack.push(memo.mapping); + break; + } + case "pop-region-renames": { + // Apply renames when exiting the context: add mappings from + // original names to new names for external reference resolution + const mapping = renameStack.pop(); + if (mapping) { + for (const [originalName, newName] of Object.entries(mapping)) { + if (originalName in regions && newName !== originalName) { + regions[newName] = { ...regions[originalName], name: newName }; + } + } + } + break; + } } // add new memos to the stack in reverse order diff --git a/packages/pointers/src/dereference/index.test.ts b/packages/pointers/src/dereference/index.test.ts index 2d1131939..ccd8dc6d3 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -281,4 +281,202 @@ describe("dereference", () => { expect(regions[0].offset).toEqual(Data.fromNumber(0)); expect(regions[0].length).toEqual(Data.fromNumber(32)); }); + + it("works for template references with yields (basic rename)", async () => { + const templates: Pointer.Templates = { + "named-region": { + expect: ["slot"], + for: { + name: "data", + location: "storage", + slot: "slot" + } + } + }; + + const pointer: Pointer = { + define: { + "slot": 0 + }, + in: { + template: "named-region", + yields: { + "data": "my-data" + } + } + }; + + const cursor = await dereference(pointer, { templates }); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(1); + expect(regions[0].name).toEqual("my-data"); + expect(regions.lookup["my-data"]).toBeDefined(); + expect(regions.lookup["my-data"].name).toEqual("my-data"); + }); + + it("works for multiple template references with different yields", async () => { + const templates: Pointer.Templates = { + "slot-region": { + expect: ["slot"], + for: { + name: "value", + location: "storage", + slot: "slot" + } + } + }; + + const pointer: Pointer = { + group: [ + { + define: { "slot": 0 }, + in: { + template: "slot-region", + yields: { "value": "first-value" } + } + }, + { + define: { "slot": 1 }, + in: { + template: "slot-region", + yields: { "value": "second-value" } + } + } + ] + }; + + const cursor = await dereference(pointer, { templates }); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(2); + expect(regions[0].name).toEqual("first-value"); + expect(regions[1].name).toEqual("second-value"); + expect(regions.lookup["first-value"]).toBeDefined(); + expect(regions.lookup["second-value"]).toBeDefined(); + }); + + it("allows partial yields mapping (unmapped regions keep names)", async () => { + const templates: Pointer.Templates = { + "two-regions": { + expect: ["slot"], + for: { + group: [ + { name: "a", location: "storage", slot: "slot" }, + { name: "b", location: "storage", slot: { $sum: ["slot", 1] } } + ] + } + } + }; + + const pointer: Pointer = { + define: { "slot": 0 }, + in: { + template: "two-regions", + yields: { "a": "renamed-a" } // only rename "a", leave "b" as-is + } + }; + + const cursor = await dereference(pointer, { templates }); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(2); + expect(regions[0].name).toEqual("renamed-a"); + expect(regions[1].name).toEqual("b"); // unchanged + expect(regions.lookup["renamed-a"]).toBeDefined(); + expect(regions.lookup["b"]).toBeDefined(); + }); + + it("preserves internal references within template when using yields", async () => { + const templates: Pointer.Templates = { + "dependent-regions": { + expect: ["base"], + for: { + group: [ + { + name: "first", + location: "memory", + offset: "base", + length: 32 + }, + { + name: "second", + location: "memory", + offset: { $sum: [{ ".offset": "first" }, { ".length": "first" }] }, + length: { ".length": "first" } + } + ] + } + } + }; + + const pointer: Pointer = { + define: { "base": 64 }, + in: { + template: "dependent-regions", + yields: { + "first": "my-first", + "second": "my-second" + } + } + }; + + const cursor = await dereference(pointer, { templates }); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(2); + expect(regions[0].name).toEqual("my-first"); + expect(regions[0].offset).toEqual(Data.fromNumber(64)); + expect(regions[0].length).toEqual(Data.fromNumber(32)); + + // Second region depends on first via internal reference + expect(regions[1].name).toEqual("my-second"); + expect(regions[1].offset).toEqual(Data.fromNumber(96)); // 64 + 32 + expect(regions[1].length).toEqual(Data.fromNumber(32)); + }); + + it("works for nested template references with yields", async () => { + const templates: Pointer.Templates = { + "inner": { + expect: ["slot"], + for: { + name: "inner-data", + location: "storage", + slot: "slot" + } + }, + "outer": { + expect: ["base-slot"], + for: { + group: [ + { + define: { "slot": "base-slot" }, + in: { template: "inner" } + }, + { + define: { "slot": { $sum: ["base-slot", 1] } }, + in: { template: "inner" } + } + ] + } + } + }; + + const pointer: Pointer = { + define: { "base-slot": 10 }, + in: { + template: "outer", + yields: { "inner-data": "outer-data" } + } + }; + + const cursor = await dereference(pointer, { templates }); + const { regions } = await cursor.view(state); + + // Both inner regions should be renamed + expect(regions).toHaveLength(2); + expect(regions[0].name).toEqual("outer-data"); + expect(regions[1].name).toEqual("outer-data"); + expect(regions.named("outer-data")).toHaveLength(2); + }); }); diff --git a/packages/pointers/src/dereference/memo.ts b/packages/pointers/src/dereference/memo.ts index c3be3d67c..2f13b8b4b 100644 --- a/packages/pointers/src/dereference/memo.ts +++ b/packages/pointers/src/dereference/memo.ts @@ -8,7 +8,9 @@ import type { Data } from "../data.js"; export type Memo = | Memo.DereferencePointer | Memo.SaveRegions - | Memo.SaveVariables; + | Memo.SaveVariables + | Memo.PushRegionRenames + | Memo.PopRegionRenames; export namespace Memo { /** @@ -69,4 +71,38 @@ export namespace Memo { kind: "save-variables", variables }); + + /** + * A request to push a region rename mapping onto the context stack. + * While active, regions with names in the mapping will be saved under + * both their original name (for internal references) and their new name + * (for external references after the template). + */ + export interface PushRegionRenames { + kind: "push-region-renames"; + mapping: Record; + } + + /** + * Initialize a PushRegionRenames memo + */ + export const pushRegionRenames = + (mapping: Record): PushRegionRenames => ({ + kind: "push-region-renames", + mapping + }); + + /** + * A request to pop the current region rename mapping from the context stack. + */ + export interface PopRegionRenames { + kind: "pop-region-renames"; + } + + /** + * Initialize a PopRegionRenames memo + */ + export const popRegionRenames = (): PopRegionRenames => ({ + kind: "pop-region-renames" + }); } diff --git a/packages/pointers/src/dereference/process.ts b/packages/pointers/src/dereference/process.ts index 8e960cc84..224ad21de 100644 --- a/packages/pointers/src/dereference/process.ts +++ b/packages/pointers/src/dereference/process.ts @@ -160,7 +160,7 @@ async function* processReference( collection: Pointer.Collection.Reference, options: ProcessOptions ): Process { - const { template: templateName } = collection; + const { template: templateName, yields } = collection; const { templates, variables } = options; @@ -189,6 +189,16 @@ async function* processReference( ].join("")); } + // If yields is specified with mappings, wrap the dereference with + // push/pop region renames memos + if (yields && Object.keys(yields).length > 0) { + return [ + Memo.pushRegionRenames(yields), + Memo.dereferencePointer(pointer), + Memo.popRegionRenames() + ]; + } + return [ Memo.dereferencePointer(pointer) ]; diff --git a/packages/pointers/src/test-cases.ts b/packages/pointers/src/test-cases.ts index 91830a5bc..cd475a045 100644 --- a/packages/pointers/src/test-cases.ts +++ b/packages/pointers/src/test-cases.ts @@ -3,18 +3,35 @@ import { findExamplePointer, type ObserveTraceOptions } from "../test/index.js"; +import { Pointer } from "@ethdebug/format"; import { type Cursor, Data } from "./index.js"; export interface ObserveTraceTest extends ObserveTraceOptions { expectedValues: V[]; } +const packedFieldTemplate: Pointer.Template = { + expect: ["struct-storage-contract-variable-slot", "previous", "size"], + for: { + name: "field", + location: "storage", + slot: "struct-storage-contract-variable-slot", + offset: { + $difference: ["previous", "size"] + }, + length: "size" + } +}; + const structStorageTest: ObserveTraceTest<{ x: number; y: number; salt: string; }> = { - pointer: findExamplePointer("struct-storage-contract-variable-slot"), + pointer: findExamplePointer("packed-field"), + templates: { + "packed-field": packedFieldTemplate + }, compileOptions: singleSourceCompilation({ path: "StructStorage.sol", contractName: "StructStorage", diff --git a/packages/web/spec/pointer/collection/reference.mdx b/packages/web/spec/pointer/collection/reference.mdx new file mode 100644 index 000000000..d9bb24fd8 --- /dev/null +++ b/packages/web/spec/pointer/collection/reference.mdx @@ -0,0 +1,14 @@ +--- +sidebar_position: 6 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Reference + +A reference collection invokes a named pointer template, optionally renaming +the regions it produces via the `yields` property. + + diff --git a/packages/web/spec/pointer/overview.mdx b/packages/web/spec/pointer/overview.mdx index 2a67fd633..0edf1c826 100644 --- a/packages/web/spec/pointer/overview.mdx +++ b/packages/web/spec/pointer/overview.mdx @@ -75,5 +75,7 @@ see the navigation bar for complete contents. - [**ethdebug/format/pointer/collection/group**](/spec/pointer/collection/group) - [**ethdebug/format/pointer/collection/list**](/spec/pointer/collection/list) - [**ethdebug/format/pointer/collection/conditional**](/spec/pointer/collection/conditional) + - [**ethdebug/format/pointer/collection/scope**](/spec/pointer/collection/scope) + - [**ethdebug/format/pointer/collection/reference**](/spec/pointer/collection/reference) - [Expression syntax](/spec/pointer/expression) (**ethdebug/format/pointer/expression** schema listing) diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index 6ad81eba7..4be9cda99 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -105,7 +105,7 @@ const pointerSchemaIndex: SchemaIndex = { ...( [ - "group", "list", "conditional", "scope" + "group", "list", "conditional", "scope", "reference" ].map(collection => ({ [`schema:ethdebug/format/pointer/collection/${collection}`]: { href: `/spec/pointer/collection/${collection}` diff --git a/packages/web/src/status/status-config.ts b/packages/web/src/status/status-config.ts index 0900ffa40..18156eeef 100644 --- a/packages/web/src/status/status-config.ts +++ b/packages/web/src/status/status-config.ts @@ -64,8 +64,6 @@ export const schemaStatus: Record = { "Comprehensive schema for describing data locations in EVM state. " + "Debugger-side reference implementation available in @ethdebug/pointers.", caveats: [ - "Pointer templates cannot rename their output region names, limiting " + - "composability for complex scenarios.", "No compiler-side reference implementation yet.", ], detailsPath: "/spec/pointer/overview#status", diff --git a/schemas/pointer.schema.yaml b/schemas/pointer.schema.yaml index 82d367d4f..88ed6a1ef 100644 --- a/schemas/pointer.schema.yaml +++ b/schemas/pointer.schema.yaml @@ -66,34 +66,35 @@ examples: length: $wordsize - # example `struct Record { uint8 x; uint8 y; bytes4 salt; }` in storage + # + # this example uses the "packed-field" template (defined separately) to + # demonstrate how templates can be reused with `yields` to rename regions. + # each field is placed by packing right-to-left from the previous offset. define: "struct-storage-contract-variable-slot": 0 in: group: - - name: "x" + # sentinel region marking where packing begins (end of word) + - name: "packing-begin" location: storage slot: "struct-storage-contract-variable-slot" - offset: - $difference: - - $wordsize - - .length: $this - length: 1 # uint8 - - name: "y" - location: storage - slot: "struct-storage-contract-variable-slot" - offset: - $difference: - - .offset: "x" - - .length: $this - length: 1 # uint8 - - name: "salt" - location: storage - slot: "struct-storage-contract-variable-slot" - offset: - $difference: - - .offset: "y" - - .length: $this - length: 4 # bytes4 + offset: $wordsize + length: 0 + + - define: { previous: { .offset: "packing-begin" }, size: 1 } + in: + template: "packed-field" + yields: { "field": "x" } + + - define: { previous: { .offset: "x" }, size: 1 } + in: + template: "packed-field" + yields: { "field": "y" } + + - define: { previous: { .offset: "y" }, size: 4 } + in: + template: "packed-field" + yields: { "field": "salt" } - # example `(struct Record { uint256 x; uint256 y; })[] memory` group: diff --git a/schemas/pointer/collection/reference.schema.yaml b/schemas/pointer/collection/reference.schema.yaml index 6889231c8..816b0944b 100644 --- a/schemas/pointer/collection/reference.schema.yaml +++ b/schemas/pointer/collection/reference.schema.yaml @@ -12,6 +12,18 @@ properties: title: Template identifier $ref: "schema:ethdebug/format/pointer/identifier" + yields: + title: Region name mapping + description: | + Maps region names produced by the template to new names for use + outside the template. Unmapped region names pass through unchanged. + When omitted, all regions keep their original names. + type: object + propertyNames: + $ref: "schema:ethdebug/format/pointer/identifier" + additionalProperties: + $ref: "schema:ethdebug/format/pointer/identifier" + required: - template @@ -19,3 +31,8 @@ additionalProperties: false examples: - template: "string-storage-pointer" + + - template: "string-storage-pointer" + yields: + data: "name-data" + length: "name-length" From 4e9d0ac5396e3a588a17d8f8361e8bbaeb9d95a0 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 12 Jan 2026 18:57:58 -0500 Subject: [PATCH 2/2] Add inline template definitions to pointer schema - Define `templates`/`in` collection type for declaring templates within a pointer, following the `define`/`in` pattern from scope - Implement template context stack in dereference loop to merge inline templates with external templates (inline takes precedence) - Update struct storage example to define `packed-field` template inline, making the example fully self-contained - Add documentation page for templates collection - Add unit tests for inline template definitions --- packages/format/src/types/pointer/pointer.ts | 23 ++- packages/pointers/src/dereference/generate.ts | 22 ++- .../pointers/src/dereference/index.test.ts | 140 ++++++++++++++++++ packages/pointers/src/dereference/memo.ts | 37 ++++- packages/pointers/src/dereference/process.ts | 17 +++ packages/pointers/src/test-cases.ts | 17 --- .../web/spec/pointer/collection/templates.mdx | 17 +++ packages/web/spec/pointer/overview.mdx | 1 + packages/web/src/schemas.ts | 2 +- schemas/pointer.schema.yaml | 64 ++++---- schemas/pointer/collection.schema.yaml | 6 + .../pointer/collection/templates.schema.yaml | 41 +++++ 12 files changed, 340 insertions(+), 47 deletions(-) create mode 100644 packages/web/spec/pointer/collection/templates.mdx create mode 100644 schemas/pointer/collection/templates.schema.yaml diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index ab31d922a..94aba4108 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -130,7 +130,8 @@ export namespace Pointer { | Collection.List | Collection.Conditional | Collection.Scope - | Collection.Reference; + | Collection.Reference + | Collection.Templates; export const isCollection = (value: unknown): value is Collection => [ @@ -138,7 +139,8 @@ export namespace Pointer { Collection.isList, Collection.isConditional, Collection.isScope, - Collection.isReference + Collection.isReference, + Collection.isTemplates ].some(guard => guard(value)); export namespace Collection { @@ -223,6 +225,23 @@ export namespace Pointer { ([k, v]) => isIdentifier(k) && isIdentifier(v) ) )) + + export interface Templates { + templates: { + [identifier: string]: Pointer.Template; + }; + in: Pointer; + } + + export const isTemplates = (value: unknown): value is Templates => + !!value && + typeof value === "object" && + "templates" in value && + typeof value.templates === "object" && !!value.templates && + Object.keys(value.templates).every(isIdentifier) && + Object.values(value.templates).every(isTemplate) && + "in" in value && + isPointer(value.in); } export type Expression = diff --git a/packages/pointers/src/dereference/generate.ts b/packages/pointers/src/dereference/generate.ts index a63f44fdb..eccdf0cfc 100644 --- a/packages/pointers/src/dereference/generate.ts +++ b/packages/pointers/src/dereference/generate.ts @@ -38,6 +38,9 @@ export async function* generateRegions( // Stack of rename mappings for template references with yields const renameStack: Array> = []; + // Stack of template definitions for inline templates + const templatesStack: Array = []; + const stack: Memo[] = [Memo.dereferencePointer(pointer)]; while (stack.length > 0) { const memo: Memo = stack.pop() as Memo; @@ -45,8 +48,17 @@ export async function* generateRegions( let memos: Memo[] = []; switch (memo.kind) { case "dereference-pointer": { + // Merge inline templates with base templates (inline takes precedence) + const currentTemplates = templatesStack.reduce( + (acc, templates) => ({ ...acc, ...templates }), + options.templates + ); + // Process the pointer, intercepting yielded regions to apply renames - const process = processPointer(memo.pointer, options); + const process = processPointer(memo.pointer, { + ...options, + templates: currentTemplates + }); let result = await process.next(); while (!result.done) { let region = result.value; @@ -94,6 +106,14 @@ export async function* generateRegions( } break; } + case "push-templates": { + templatesStack.push(memo.templates); + break; + } + case "pop-templates": { + templatesStack.pop(); + break; + } } // add new memos to the stack in reverse order diff --git a/packages/pointers/src/dereference/index.test.ts b/packages/pointers/src/dereference/index.test.ts index ccd8dc6d3..dffcde333 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -479,4 +479,144 @@ describe("dereference", () => { expect(regions[1].name).toEqual("outer-data"); expect(regions.named("outer-data")).toHaveLength(2); }); + + it("works for inline template definitions", async () => { + const pointer: Pointer = { + templates: { + "memory-range": { + expect: ["offset", "length"], + for: { + location: "memory", + offset: "offset", + length: "length" + } + } + }, + in: { + define: { + "offset": 0, + "length": 32 + }, + in: { + template: "memory-range" + } + } + }; + + const cursor = await dereference(pointer); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(1); + expect(regions[0].offset).toEqual(Data.fromNumber(0)); + expect(regions[0].length).toEqual(Data.fromNumber(32)); + }); + + it("inline templates take precedence over external templates", async () => { + // External template defines a slot-based region + const externalTemplates: Pointer.Templates = { + "my-region": { + expect: ["value"], + for: { + name: "external", + location: "storage", + slot: "value" + } + } + }; + + // Inline template overrides with memory-based region + const pointer: Pointer = { + templates: { + "my-region": { + expect: ["value"], + for: { + name: "inline", + location: "memory", + offset: "value", + length: 32 + } + } + }, + in: { + define: { "value": 64 }, + in: { + template: "my-region" + } + } + }; + + const cursor = await dereference(pointer, { templates: externalTemplates }); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(1); + expect(regions[0].name).toEqual("inline"); + expect(regions[0].location).toEqual("memory"); + }); + + it("works for nested inline templates", async () => { + const pointer: Pointer = { + templates: { + "outer-template": { + expect: ["base"], + for: { + templates: { + "inner-template": { + expect: ["offset"], + for: { + name: "data", + location: "memory", + offset: "offset", + length: 32 + } + } + }, + in: { + define: { "offset": "base" }, + in: { template: "inner-template" } + } + } + } + }, + in: { + define: { "base": 128 }, + in: { template: "outer-template" } + } + }; + + const cursor = await dereference(pointer); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(1); + expect(regions[0].name).toEqual("data"); + expect(regions[0].offset).toEqual(Data.fromNumber(128)); + }); + + it("inline templates with yields work correctly", async () => { + const pointer: Pointer = { + templates: { + "named-slot": { + expect: ["slot"], + for: { + name: "value", + location: "storage", + slot: "slot" + } + } + }, + in: { + define: { "slot": 5 }, + in: { + template: "named-slot", + yields: { "value": "my-slot-value" } + } + } + }; + + const cursor = await dereference(pointer); + const { regions } = await cursor.view(state); + + expect(regions).toHaveLength(1); + expect(regions[0].name).toEqual("my-slot-value"); + expect(regions.lookup["my-slot-value"]).toBeDefined(); + }); }); diff --git a/packages/pointers/src/dereference/memo.ts b/packages/pointers/src/dereference/memo.ts index 2f13b8b4b..c65268357 100644 --- a/packages/pointers/src/dereference/memo.ts +++ b/packages/pointers/src/dereference/memo.ts @@ -10,7 +10,9 @@ export type Memo = | Memo.SaveRegions | Memo.SaveVariables | Memo.PushRegionRenames - | Memo.PopRegionRenames; + | Memo.PopRegionRenames + | Memo.PushTemplates + | Memo.PopTemplates; export namespace Memo { /** @@ -105,4 +107,37 @@ export namespace Memo { export const popRegionRenames = (): PopRegionRenames => ({ kind: "pop-region-renames" }); + + /** + * A request to push template definitions onto the context stack. + * While active, these templates are available for use by reference + * collections. + */ + export interface PushTemplates { + kind: "push-templates"; + templates: Pointer.Templates; + } + + /** + * Initialize a PushTemplates memo + */ + export const pushTemplates = + (templates: Pointer.Templates): PushTemplates => ({ + kind: "push-templates", + templates + }); + + /** + * A request to pop the current template definitions from the context stack. + */ + export interface PopTemplates { + kind: "pop-templates"; + } + + /** + * Initialize a PopTemplates memo + */ + export const popTemplates = (): PopTemplates => ({ + kind: "pop-templates" + }); } diff --git a/packages/pointers/src/dereference/process.ts b/packages/pointers/src/dereference/process.ts index 224ad21de..7e84d19dd 100644 --- a/packages/pointers/src/dereference/process.ts +++ b/packages/pointers/src/dereference/process.ts @@ -61,6 +61,10 @@ export async function* processPointer( return yield* processReference(collection, options); } + if (Pointer.Collection.isTemplates(collection)) { + return yield* processTemplates(collection, options); + } + console.error("%s", JSON.stringify(pointer, undefined, 2)); throw new Error("Unexpected unknown kind of pointer"); } @@ -203,3 +207,16 @@ async function* processReference( Memo.dereferencePointer(pointer) ]; } + +async function* processTemplates( + collection: Pointer.Collection.Templates, + options: ProcessOptions +): Process { + const { templates, in: in_ } = collection; + + return [ + Memo.pushTemplates(templates), + Memo.dereferencePointer(in_), + Memo.popTemplates() + ]; +} diff --git a/packages/pointers/src/test-cases.ts b/packages/pointers/src/test-cases.ts index cd475a045..3d15d49cd 100644 --- a/packages/pointers/src/test-cases.ts +++ b/packages/pointers/src/test-cases.ts @@ -3,35 +3,18 @@ import { findExamplePointer, type ObserveTraceOptions } from "../test/index.js"; -import { Pointer } from "@ethdebug/format"; import { type Cursor, Data } from "./index.js"; export interface ObserveTraceTest extends ObserveTraceOptions { expectedValues: V[]; } -const packedFieldTemplate: Pointer.Template = { - expect: ["struct-storage-contract-variable-slot", "previous", "size"], - for: { - name: "field", - location: "storage", - slot: "struct-storage-contract-variable-slot", - offset: { - $difference: ["previous", "size"] - }, - length: "size" - } -}; - const structStorageTest: ObserveTraceTest<{ x: number; y: number; salt: string; }> = { pointer: findExamplePointer("packed-field"), - templates: { - "packed-field": packedFieldTemplate - }, compileOptions: singleSourceCompilation({ path: "StructStorage.sol", contractName: "StructStorage", diff --git a/packages/web/spec/pointer/collection/templates.mdx b/packages/web/spec/pointer/collection/templates.mdx new file mode 100644 index 000000000..d77e6359c --- /dev/null +++ b/packages/web/spec/pointer/collection/templates.mdx @@ -0,0 +1,17 @@ +--- +sidebar_position: 7 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Templates + +A templates collection defines pointer templates inline, making them available +for use by reference collections within the `in` pointer. + +This follows the same pattern as scope (`define`/`in`) but for templates +instead of variables. + + diff --git a/packages/web/spec/pointer/overview.mdx b/packages/web/spec/pointer/overview.mdx index 0edf1c826..8b18e7825 100644 --- a/packages/web/spec/pointer/overview.mdx +++ b/packages/web/spec/pointer/overview.mdx @@ -77,5 +77,6 @@ see the navigation bar for complete contents. - [**ethdebug/format/pointer/collection/conditional**](/spec/pointer/collection/conditional) - [**ethdebug/format/pointer/collection/scope**](/spec/pointer/collection/scope) - [**ethdebug/format/pointer/collection/reference**](/spec/pointer/collection/reference) + - [**ethdebug/format/pointer/collection/templates**](/spec/pointer/collection/templates) - [Expression syntax](/spec/pointer/expression) (**ethdebug/format/pointer/expression** schema listing) diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index 4be9cda99..eedb1b33f 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -105,7 +105,7 @@ const pointerSchemaIndex: SchemaIndex = { ...( [ - "group", "list", "conditional", "scope", "reference" + "group", "list", "conditional", "scope", "reference", "templates" ].map(collection => ({ [`schema:ethdebug/format/pointer/collection/${collection}`]: { href: `/spec/pointer/collection/${collection}` diff --git a/schemas/pointer.schema.yaml b/schemas/pointer.schema.yaml index 88ed6a1ef..d6c276956 100644 --- a/schemas/pointer.schema.yaml +++ b/schemas/pointer.schema.yaml @@ -67,34 +67,48 @@ examples: - # example `struct Record { uint8 x; uint8 y; bytes4 salt; }` in storage # - # this example uses the "packed-field" template (defined separately) to - # demonstrate how templates can be reused with `yields` to rename regions. + # this example defines the "packed-field" template inline and demonstrates + # how templates can be reused with `yields` to rename regions. # each field is placed by packing right-to-left from the previous offset. - define: - "struct-storage-contract-variable-slot": 0 - in: - group: - # sentinel region marking where packing begins (end of word) - - name: "packing-begin" + templates: + packed-field: + expect: + - "struct-storage-contract-variable-slot" + - "previous" + - "size" + for: + name: "field" location: storage slot: "struct-storage-contract-variable-slot" - offset: $wordsize - length: 0 - - - define: { previous: { .offset: "packing-begin" }, size: 1 } - in: - template: "packed-field" - yields: { "field": "x" } - - - define: { previous: { .offset: "x" }, size: 1 } - in: - template: "packed-field" - yields: { "field": "y" } - - - define: { previous: { .offset: "y" }, size: 4 } - in: - template: "packed-field" - yields: { "field": "salt" } + offset: + $difference: ["previous", "size"] + length: "size" + in: + define: + "struct-storage-contract-variable-slot": 0 + in: + group: + # sentinel region marking where packing begins (end of word) + - name: "packing-begin" + location: storage + slot: "struct-storage-contract-variable-slot" + offset: $wordsize + length: 0 + + - define: { previous: { .offset: "packing-begin" }, size: 1 } + in: + template: "packed-field" + yields: { "field": "x" } + + - define: { previous: { .offset: "x" }, size: 1 } + in: + template: "packed-field" + yields: { "field": "y" } + + - define: { previous: { .offset: "y" }, size: 4 } + in: + template: "packed-field" + yields: { "field": "salt" } - # example `(struct Record { uint256 x; uint256 y; })[] memory` group: diff --git a/schemas/pointer/collection.schema.yaml b/schemas/pointer/collection.schema.yaml index 145dc9a27..47f4ec1b4 100644 --- a/schemas/pointer/collection.schema.yaml +++ b/schemas/pointer/collection.schema.yaml @@ -13,6 +13,7 @@ allOf: - required: [if] - required: [define] - required: [template] + - required: [templates] - if: required: [group] @@ -38,3 +39,8 @@ allOf: required: [template] then: $ref: "schema:ethdebug/format/pointer/collection/reference" + + - if: + required: [templates] + then: + $ref: "schema:ethdebug/format/pointer/collection/templates" diff --git a/schemas/pointer/collection/templates.schema.yaml b/schemas/pointer/collection/templates.schema.yaml new file mode 100644 index 000000000..f86f2640a --- /dev/null +++ b/schemas/pointer/collection/templates.schema.yaml @@ -0,0 +1,41 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/pointer/collection/templates" + +title: ethdebug/format/pointer/collection/templates +description: | + A pointer with locally-defined templates available for use within. + + Templates defined here are available by name for reference collections + inside the `in` pointer. + +type: object + +properties: + templates: + title: Mapping of template names to template definitions + type: object + propertyNames: + $ref: "schema:ethdebug/format/pointer/identifier" + additionalProperties: + $ref: "schema:ethdebug/format/pointer/template" + in: + $ref: "schema:ethdebug/format/pointer" + +required: + - templates + - in + +additionalProperties: false + +examples: + - templates: + simple-slot: + expect: ["slot"] + for: + location: storage + slot: "slot" + in: + define: + slot: 0 + in: + template: "simple-slot"