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);