Skip to content

Commit 7b662d9

Browse files
committed
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
1 parent 174513d commit 7b662d9

12 files changed

Lines changed: 340 additions & 47 deletions

File tree

packages/format/src/types/pointer/pointer.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,17 @@ export namespace Pointer {
130130
| Collection.List
131131
| Collection.Conditional
132132
| Collection.Scope
133-
| Collection.Reference;
133+
| Collection.Reference
134+
| Collection.Templates;
134135

135136
export const isCollection = (value: unknown): value is Collection =>
136137
[
137138
Collection.isGroup,
138139
Collection.isList,
139140
Collection.isConditional,
140141
Collection.isScope,
141-
Collection.isReference
142+
Collection.isReference,
143+
Collection.isTemplates
142144
].some(guard => guard(value));
143145

144146
export namespace Collection {
@@ -223,6 +225,23 @@ export namespace Pointer {
223225
([k, v]) => isIdentifier(k) && isIdentifier(v)
224226
)
225227
))
228+
229+
export interface Templates {
230+
templates: {
231+
[identifier: string]: Pointer.Template;
232+
};
233+
in: Pointer;
234+
}
235+
236+
export const isTemplates = (value: unknown): value is Templates =>
237+
!!value &&
238+
typeof value === "object" &&
239+
"templates" in value &&
240+
typeof value.templates === "object" && !!value.templates &&
241+
Object.keys(value.templates).every(isIdentifier) &&
242+
Object.values(value.templates).every(isTemplate) &&
243+
"in" in value &&
244+
isPointer(value.in);
226245
}
227246

228247
export type Expression =

packages/pointers/src/dereference/generate.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,27 @@ export async function* generateRegions(
3838
// Stack of rename mappings for template references with yields
3939
const renameStack: Array<Record<string, string>> = [];
4040

41+
// Stack of template definitions for inline templates
42+
const templatesStack: Array<Pointer.Templates> = [];
43+
4144
const stack: Memo[] = [Memo.dereferencePointer(pointer)];
4245
while (stack.length > 0) {
4346
const memo: Memo = stack.pop() as Memo;
4447

4548
let memos: Memo[] = [];
4649
switch (memo.kind) {
4750
case "dereference-pointer": {
51+
// Merge inline templates with base templates (inline takes precedence)
52+
const currentTemplates = templatesStack.reduce(
53+
(acc, templates) => ({ ...acc, ...templates }),
54+
options.templates
55+
);
56+
4857
// Process the pointer, intercepting yielded regions to apply renames
49-
const process = processPointer(memo.pointer, options);
58+
const process = processPointer(memo.pointer, {
59+
...options,
60+
templates: currentTemplates
61+
});
5062
let result = await process.next();
5163
while (!result.done) {
5264
let region = result.value;
@@ -95,6 +107,14 @@ export async function* generateRegions(
95107
renameStack.pop();
96108
break;
97109
}
110+
case "push-templates": {
111+
templatesStack.push(memo.templates);
112+
break;
113+
}
114+
case "pop-templates": {
115+
templatesStack.pop();
116+
break;
117+
}
98118
}
99119

100120
// add new memos to the stack in reverse order

packages/pointers/src/dereference/index.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,4 +479,144 @@ describe("dereference", () => {
479479
expect(regions[1].name).toEqual("outer-data");
480480
expect(regions.named("outer-data")).toHaveLength(2);
481481
});
482+
483+
it("works for inline template definitions", async () => {
484+
const pointer: Pointer = {
485+
templates: {
486+
"memory-range": {
487+
expect: ["offset", "length"],
488+
for: {
489+
location: "memory",
490+
offset: "offset",
491+
length: "length"
492+
}
493+
}
494+
},
495+
in: {
496+
define: {
497+
"offset": 0,
498+
"length": 32
499+
},
500+
in: {
501+
template: "memory-range"
502+
}
503+
}
504+
};
505+
506+
const cursor = await dereference(pointer);
507+
const { regions } = await cursor.view(state);
508+
509+
expect(regions).toHaveLength(1);
510+
expect(regions[0].offset).toEqual(Data.fromNumber(0));
511+
expect(regions[0].length).toEqual(Data.fromNumber(32));
512+
});
513+
514+
it("inline templates take precedence over external templates", async () => {
515+
// External template defines a slot-based region
516+
const externalTemplates: Pointer.Templates = {
517+
"my-region": {
518+
expect: ["value"],
519+
for: {
520+
name: "external",
521+
location: "storage",
522+
slot: "value"
523+
}
524+
}
525+
};
526+
527+
// Inline template overrides with memory-based region
528+
const pointer: Pointer = {
529+
templates: {
530+
"my-region": {
531+
expect: ["value"],
532+
for: {
533+
name: "inline",
534+
location: "memory",
535+
offset: "value",
536+
length: 32
537+
}
538+
}
539+
},
540+
in: {
541+
define: { "value": 64 },
542+
in: {
543+
template: "my-region"
544+
}
545+
}
546+
};
547+
548+
const cursor = await dereference(pointer, { templates: externalTemplates });
549+
const { regions } = await cursor.view(state);
550+
551+
expect(regions).toHaveLength(1);
552+
expect(regions[0].name).toEqual("inline");
553+
expect(regions[0].location).toEqual("memory");
554+
});
555+
556+
it("works for nested inline templates", async () => {
557+
const pointer: Pointer = {
558+
templates: {
559+
"outer-template": {
560+
expect: ["base"],
561+
for: {
562+
templates: {
563+
"inner-template": {
564+
expect: ["offset"],
565+
for: {
566+
name: "data",
567+
location: "memory",
568+
offset: "offset",
569+
length: 32
570+
}
571+
}
572+
},
573+
in: {
574+
define: { "offset": "base" },
575+
in: { template: "inner-template" }
576+
}
577+
}
578+
}
579+
},
580+
in: {
581+
define: { "base": 128 },
582+
in: { template: "outer-template" }
583+
}
584+
};
585+
586+
const cursor = await dereference(pointer);
587+
const { regions } = await cursor.view(state);
588+
589+
expect(regions).toHaveLength(1);
590+
expect(regions[0].name).toEqual("data");
591+
expect(regions[0].offset).toEqual(Data.fromNumber(128));
592+
});
593+
594+
it("inline templates with yields work correctly", async () => {
595+
const pointer: Pointer = {
596+
templates: {
597+
"named-slot": {
598+
expect: ["slot"],
599+
for: {
600+
name: "value",
601+
location: "storage",
602+
slot: "slot"
603+
}
604+
}
605+
},
606+
in: {
607+
define: { "slot": 5 },
608+
in: {
609+
template: "named-slot",
610+
yields: { "value": "my-slot-value" }
611+
}
612+
}
613+
};
614+
615+
const cursor = await dereference(pointer);
616+
const { regions } = await cursor.view(state);
617+
618+
expect(regions).toHaveLength(1);
619+
expect(regions[0].name).toEqual("my-slot-value");
620+
expect(regions.lookup["my-slot-value"]).toBeDefined();
621+
});
482622
});

packages/pointers/src/dereference/memo.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export type Memo =
1010
| Memo.SaveRegions
1111
| Memo.SaveVariables
1212
| Memo.PushRegionRenames
13-
| Memo.PopRegionRenames;
13+
| Memo.PopRegionRenames
14+
| Memo.PushTemplates
15+
| Memo.PopTemplates;
1416

1517
export namespace Memo {
1618
/**
@@ -105,4 +107,37 @@ export namespace Memo {
105107
export const popRegionRenames = (): PopRegionRenames => ({
106108
kind: "pop-region-renames"
107109
});
110+
111+
/**
112+
* A request to push template definitions onto the context stack.
113+
* While active, these templates are available for use by reference
114+
* collections.
115+
*/
116+
export interface PushTemplates {
117+
kind: "push-templates";
118+
templates: Pointer.Templates;
119+
}
120+
121+
/**
122+
* Initialize a PushTemplates memo
123+
*/
124+
export const pushTemplates =
125+
(templates: Pointer.Templates): PushTemplates => ({
126+
kind: "push-templates",
127+
templates
128+
});
129+
130+
/**
131+
* A request to pop the current template definitions from the context stack.
132+
*/
133+
export interface PopTemplates {
134+
kind: "pop-templates";
135+
}
136+
137+
/**
138+
* Initialize a PopTemplates memo
139+
*/
140+
export const popTemplates = (): PopTemplates => ({
141+
kind: "pop-templates"
142+
});
108143
}

packages/pointers/src/dereference/process.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export async function* processPointer(
6161
return yield* processReference(collection, options);
6262
}
6363

64+
if (Pointer.Collection.isTemplates(collection)) {
65+
return yield* processTemplates(collection, options);
66+
}
67+
6468
console.error("%s", JSON.stringify(pointer, undefined, 2));
6569
throw new Error("Unexpected unknown kind of pointer");
6670
}
@@ -203,3 +207,16 @@ async function* processReference(
203207
Memo.dereferencePointer(pointer)
204208
];
205209
}
210+
211+
async function* processTemplates(
212+
collection: Pointer.Collection.Templates,
213+
options: ProcessOptions
214+
): Process {
215+
const { templates, in: in_ } = collection;
216+
217+
return [
218+
Memo.pushTemplates(templates),
219+
Memo.dereferencePointer(in_),
220+
Memo.popTemplates()
221+
];
222+
}

packages/pointers/src/test-cases.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,18 @@ import {
33
findExamplePointer,
44
type ObserveTraceOptions
55
} from "../test/index.js";
6-
import { Pointer } from "@ethdebug/format";
76
import { type Cursor, Data } from "./index.js";
87

98
export interface ObserveTraceTest<V> extends ObserveTraceOptions<V> {
109
expectedValues: V[];
1110
}
1211

13-
const packedFieldTemplate: Pointer.Template = {
14-
expect: ["struct-storage-contract-variable-slot", "previous", "size"],
15-
for: {
16-
name: "field",
17-
location: "storage",
18-
slot: "struct-storage-contract-variable-slot",
19-
offset: {
20-
$difference: ["previous", "size"]
21-
},
22-
length: "size"
23-
}
24-
};
25-
2612
const structStorageTest: ObserveTraceTest<{
2713
x: number;
2814
y: number;
2915
salt: string;
3016
}> = {
3117
pointer: findExamplePointer("packed-field"),
32-
templates: {
33-
"packed-field": packedFieldTemplate
34-
},
3518
compileOptions: singleSourceCompilation({
3619
path: "StructStorage.sol",
3720
contractName: "StructStorage",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
sidebar_position: 7
3+
---
4+
5+
import SchemaViewer from "@site/src/components/SchemaViewer";
6+
7+
# Templates
8+
9+
A templates collection defines pointer templates inline, making them available
10+
for use by reference collections within the `in` pointer.
11+
12+
This follows the same pattern as scope (`define`/`in`) but for templates
13+
instead of variables.
14+
15+
<SchemaViewer
16+
schema={{ id: "schema:ethdebug/format/pointer/collection/templates" }}
17+
/>

packages/web/spec/pointer/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,6 @@ see the navigation bar for complete contents.
7777
- [**ethdebug/format/pointer/collection/conditional**](/spec/pointer/collection/conditional)
7878
- [**ethdebug/format/pointer/collection/scope**](/spec/pointer/collection/scope)
7979
- [**ethdebug/format/pointer/collection/reference**](/spec/pointer/collection/reference)
80+
- [**ethdebug/format/pointer/collection/templates**](/spec/pointer/collection/templates)
8081

8182
- [Expression syntax](/spec/pointer/expression) (**ethdebug/format/pointer/expression** schema listing)

packages/web/src/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ const pointerSchemaIndex: SchemaIndex = {
105105

106106
...(
107107
[
108-
"group", "list", "conditional", "scope", "reference"
108+
"group", "list", "conditional", "scope", "reference", "templates"
109109
].map(collection => ({
110110
[`schema:ethdebug/format/pointer/collection/${collection}`]: {
111111
href: `/spec/pointer/collection/${collection}`

0 commit comments

Comments
 (0)