diff --git a/packages/pointers/src/dereference/generate.ts b/packages/pointers/src/dereference/generate.ts index 6b39bec5..9cc97ff2 100644 --- a/packages/pointers/src/dereference/generate.ts +++ b/packages/pointers/src/dereference/generate.ts @@ -11,6 +11,7 @@ import { processPointer, type ProcessOptions } from "./process.js"; * for a particular pointer at runtime. */ export interface GenerateRegionsOptions { + templates: Pointer.Templates; state: Machine.State; initialStackLength: bigint; } @@ -62,6 +63,7 @@ export async function* generateRegions( } async function initializeProcessOptions({ + templates, state, initialStackLength }: GenerateRegionsOptions): Promise { @@ -72,6 +74,7 @@ async function initializeProcessOptions({ const variables: Record = {}; return { + templates, state, stackLengthChange, regions, diff --git a/packages/pointers/src/dereference/index.test.ts b/packages/pointers/src/dereference/index.test.ts index b5983e47..894c40ae 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -250,4 +250,35 @@ describe("dereference", () => { expect(regions[0].offset).toEqual(Data.fromNumber(0)); expect(regions[0].length).toEqual(Data.fromNumber(32)); }); + + it("works for templates", async () => { + const templates: Pointer.Templates = { + "memory-range": { + expect: ["offset", "length"], + for: { + location: "memory", + offset: "offset", + length: "length" + } + } + }; + + const pointer: Pointer = { + define: { + "offset": 0, + "length": 32 + }, + in: { + template: "memory-range" + } + }; + + const cursor = await dereference(pointer, { templates }); + + 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)); + }); }); diff --git a/packages/pointers/src/dereference/index.ts b/packages/pointers/src/dereference/index.ts index 949dc3b8..15172245 100644 --- a/packages/pointers/src/dereference/index.ts +++ b/packages/pointers/src/dereference/index.ts @@ -11,6 +11,7 @@ export interface DereferenceOptions { * Required for any pointers that reference the stack. */ state?: Machine.State; + templates?: Pointer.Templates } /** @@ -43,11 +44,15 @@ export async function dereference( * `generateRegions()` will potentially need. */ async function initializeGenerateRegionsOptions({ + templates = {}, state: initialState }: DereferenceOptions): Promise> { const initialStackLength = initialState ? await initialState.stack.length : 0n; - return { initialStackLength }; + return { + templates, + initialStackLength + }; } diff --git a/packages/pointers/src/dereference/process.ts b/packages/pointers/src/dereference/process.ts index 24e7465c..61a866cb 100644 --- a/packages/pointers/src/dereference/process.ts +++ b/packages/pointers/src/dereference/process.ts @@ -12,6 +12,7 @@ import { adjustStackLength, evaluateRegion } from "./region.js"; * Contextual information for use within a pointer dereference process */ export interface ProcessOptions { + templates: Pointer.Templates; state: Machine.State; stackLengthChange: bigint; regions: Record; @@ -56,6 +57,10 @@ export async function* processPointer( return yield* processScope(collection, options); } + if (Pointer.Collection.isReference(collection)) { + return yield* processReference(collection, options); + } + console.error("%s", JSON.stringify(pointer, undefined, 2)); throw new Error("Unexpected unknown kind of pointer"); } @@ -150,3 +155,41 @@ async function* processScope( Memo.dereferencePointer(in_) ]; } + +async function* processReference( + collection: Pointer.Collection.Reference, + options: ProcessOptions +): Process { + const { template: templateName } = collection; + + const { templates, variables } = options; + + const template = templates[templateName]; + + if (!template) { + throw new Error( + `Unknown pointer template named ${templateName}` + ); + } + + const { + expect: expectedVariables, + for: pointer + } = template; + + const definedVariables = new Set(Object.keys(variables)); + const missingVariables = expectedVariables + .filter(identifier => !definedVariables.has(identifier)); + + if (missingVariables.length > 0) { + throw new Error([ + `Invalid reference to template named ${templateName}; missing expected `, + `variables with identifiers: ${missingVariables.join(", ")}. `, + `Please ensure these variables are defined prior to this reference.` + ].join("")); + } + + return [ + Memo.dereferencePointer(pointer) + ]; +} diff --git a/packages/pointers/src/pointer.test.ts b/packages/pointers/src/pointer.test.ts index 9b18421a..0b12c7e3 100644 --- a/packages/pointers/src/pointer.test.ts +++ b/packages/pointers/src/pointer.test.ts @@ -176,6 +176,12 @@ describe("type guards", () => { }, guard: isPointer }, + { + schema: { + id: "schema:ethdebug/format/pointer/template" + }, + guard: Pointer.isTemplate + }, ] as const; it.each(schemaGuards)("matches its examples", ({ diff --git a/packages/pointers/src/pointer.ts b/packages/pointers/src/pointer.ts index 8d37bd53..594f8319 100644 --- a/packages/pointers/src/pointer.ts +++ b/packages/pointers/src/pointer.ts @@ -129,13 +129,16 @@ export namespace Pointer { | Collection.Group | Collection.List | Collection.Conditional - | Collection.Scope; + | Collection.Scope + | Collection.Reference; + export const isCollection = (value: unknown): value is Collection => [ Collection.isGroup, Collection.isList, Collection.isConditional, - Collection.isScope + Collection.isScope, + Collection.isReference ].some(guard => guard(value)); export namespace Collection { @@ -202,6 +205,16 @@ export namespace Pointer { Object.keys(value.define).every(key => isIdentifier(key)) && "in" in value && isPointer(value.in); + + export interface Reference { + template: string; + } + + export const isReference = (value: unknown): value is Reference => + !!value && + typeof value === "object" && + "template" in value && + typeof value.template === "string" && !!value.template } export type Expression = @@ -421,8 +434,32 @@ export namespace Pointer { "$wordsized" in value && typeof value.$wordsized !== "undefined" && isExpression(value.$wordsized); + } + } + export interface Templates { + [identifier: string]: Pointer.Template; + } - } + export const isTemplates = (value: unknown): value is Templates => + !!value && + typeof value === "object" && + Object.keys(value).every(isIdentifier) && + Object.values(value).every(isTemplate); + + export interface Template { + expect: string[]; + for: Pointer; } + + export const isTemplate = (value: unknown): value is Template => + !!value && + typeof value === "object" && + Object.keys(value).length === 2 && + "expect" in value && + value.expect instanceof Array && + value.expect.every(isIdentifier) && + "for" in value && + isPointer(value.for); + } diff --git a/packages/pointers/test/observe.ts b/packages/pointers/test/observe.ts index f9e36039..2b6b5277 100644 --- a/packages/pointers/test/observe.ts +++ b/packages/pointers/test/observe.ts @@ -11,6 +11,11 @@ export interface ObserveTraceOptions { */ pointer: Pointer; + /** + * Pointer templates that may be referenced by the given pointer + */ + templates?: Pointer.Templates; + /** * The necessary metadata and the Solidity source code for a contract whose * `constructor()` manages the lifecycle of the variable that the specified @@ -58,6 +63,7 @@ export interface ObserveTraceOptions { */ export async function observeTrace({ pointer, + templates = {}, compileOptions, observe, equals = (a, b) => a === b, @@ -89,7 +95,7 @@ export async function observeTrace({ } if (!cursor) { - cursor = await dereference(pointer, { state }); + cursor = await dereference(pointer, { state, templates }); } const { regions, read } = await cursor.view(state); diff --git a/packages/web/spec/pointer/template.mdx b/packages/web/spec/pointer/template.mdx new file mode 100644 index 00000000..6f7109e9 --- /dev/null +++ b/packages/web/spec/pointer/template.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 7 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Pointer templates + +This format provides the concept of a **pointer template** to allow +deduplicating representations. Pointer templates are defined to specify the +variables they expect in scope and the pointer definition that uses those +variables. + + diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index a7b8f60a..65a2a87b 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -116,6 +116,10 @@ export const schemaIndex: SchemaIndex = { href: "/spec/pointer/expression" }, + "schema:ethdebug/format/pointer/template": { + href: "/spec/pointer/template" + }, + ...Object.entries({ Literal: { title: "Literal values schema", diff --git a/schemas/pointer/collection.schema.yaml b/schemas/pointer/collection.schema.yaml index 556e830c..145dc9a2 100644 --- a/schemas/pointer/collection.schema.yaml +++ b/schemas/pointer/collection.schema.yaml @@ -8,34 +8,33 @@ type: object allOf: - oneOf: - - required: - - group - - required: - - list - - required: - - if - - required: - - define + - required: [group] + - required: [list] + - required: [if] + - required: [define] + - required: [template] + - if: - required: - - group + required: [group] then: $ref: "schema:ethdebug/format/pointer/collection/group" - if: - required: - - list + required: [list] then: $ref: "schema:ethdebug/format/pointer/collection/list" - if: - required: - - if + required: [if] then: $ref: "schema:ethdebug/format/pointer/collection/conditional" - if: - required: - - define + required: [define] then: $ref: "schema:ethdebug/format/pointer/collection/scope" + + - if: + required: [template] + then: + $ref: "schema:ethdebug/format/pointer/collection/reference" diff --git a/schemas/pointer/collection/reference.schema.yaml b/schemas/pointer/collection/reference.schema.yaml new file mode 100644 index 00000000..6889231c --- /dev/null +++ b/schemas/pointer/collection/reference.schema.yaml @@ -0,0 +1,21 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/pointer/collection/reference" + +title: ethdebug/format/pointer/collection/reference +description: | + A pointer by named reference to a pointer template (defined elsewhere). + +type: object + +properties: + template: + title: Template identifier + $ref: "schema:ethdebug/format/pointer/identifier" + +required: + - template + +additionalProperties: false + +examples: + - template: "string-storage-pointer" diff --git a/schemas/pointer/template.schema.yaml b/schemas/pointer/template.schema.yaml new file mode 100644 index 00000000..5324685a --- /dev/null +++ b/schemas/pointer/template.schema.yaml @@ -0,0 +1,34 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/pointer/template" + +title: ethdebug/format/pointer/template +description: | + A schema for representing a pointer defined in terms of some variables whose + values are to be provided when invoking the template. + +type: object +properties: + expect: + title: Template variables + description: | + An array of variable identifiers used in the definition of the + pointer template. + type: array + items: + $ref: "schema:ethdebug/format/pointer/identifier" + additionalItems: false + + for: + $ref: "schema:ethdebug/format/pointer" + +required: + - expect + - for + +additionalProperties: false + +examples: + - expect: ["slot"] + for: + location: storage + slot: "slot"