diff --git a/CHANGELOG.md b/CHANGELOG.md index be55167952..56f08693f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,25 @@ The sections do not signal obligations; the prefixes do. are the fragment of its `expression`'s value, which a consumer must evaluate. A fragment outside the value's bytes is invalid. +- Any pointer collection may have a `name`. A collection's name scopes the + regions inside it in the result of the pointer, and a named list also scopes + each element by its index. This tells apart regions with the same name, such + as those of two references to one template, without `yields`. Expressions do + not see collection names ([#412]). + - Schemas: **ethdebug/format/pointer/collection**, + **ethdebug/format/pointer/collection/group**, + **ethdebug/format/pointer/collection/list**, + **ethdebug/format/pointer/collection/conditional**, + **ethdebug/format/pointer/collection/scope**, + **ethdebug/format/pointer/collection/reference**, + **ethdebug/format/pointer/collection/templates** + - Producers: optional: a pointer may name its collections to scope their + regions. + - Consumers: required: accept a `name` on any collection. The collection + schemas are closed objects (`additionalProperties: false`), so the previous + version's schemas reject it. A consumer may ignore the name: the regions + and their names do not change. + ### Changed - The members of a `define` no longer depend on their order, since JSON @@ -779,3 +798,4 @@ First published version of the specification. [#404]: https://github.com/ethdebug/format/pull/404 [#408]: https://github.com/ethdebug/format/pull/408 [#411]: https://github.com/ethdebug/format/pull/411 +[#412]: https://github.com/ethdebug/format/pull/412 diff --git a/packages/format/CHANGELOG.md b/packages/format/CHANGELOG.md index 0241128a54..71aa3ac8fb 100644 --- a/packages/format/CHANGELOG.md +++ b/packages/format/CHANGELOG.md @@ -12,6 +12,9 @@ tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). **ethdebug/format/pointer/scheme/fragment** schemas, with the types `Pointer.Region.Virtual` and `Pointer.Scheme.Fragment` and their guards `Pointer.Region.isVirtual` and `Pointer.Scheme.isFragment` ([#411]). +- `Pointer.Collection.Base`, the optional `name` of every kind of + collection, with its guard `Pointer.Collection.isBase`. Each collection + guard accepts a `name` that is an identifier ([#412]). ### Changed @@ -164,6 +167,7 @@ First publication. [#325]: https://github.com/ethdebug/format/pull/325 [#373]: https://github.com/ethdebug/format/pull/373 [#411]: https://github.com/ethdebug/format/pull/411 +[#412]: https://github.com/ethdebug/format/pull/412 [`0ef2f37`]: https://github.com/ethdebug/format/commit/0ef2f37 [`10ab103`]: https://github.com/ethdebug/format/commit/10ab103 [`21e532e`]: https://github.com/ethdebug/format/commit/21e532e diff --git a/packages/format/src/types/pointer/pointer.test.ts b/packages/format/src/types/pointer/pointer.test.ts index eb124e4895..b25ac3ea2b 100644 --- a/packages/format/src/types/pointer/pointer.test.ts +++ b/packages/format/src/types/pointer/pointer.test.ts @@ -172,6 +172,22 @@ testSchemaGuards("ethdebug/format/pointer", [ schema: "schema:ethdebug/format/pointer/collection/list", guard: Pointer.Collection.isList, }, + { + schema: "schema:ethdebug/format/pointer/collection/conditional", + guard: Pointer.Collection.isConditional, + }, + { + schema: "schema:ethdebug/format/pointer/collection/scope", + guard: Pointer.Collection.isScope, + }, + { + schema: "schema:ethdebug/format/pointer/collection/reference", + guard: Pointer.Collection.isReference, + }, + { + schema: "schema:ethdebug/format/pointer/collection/templates", + guard: Pointer.Collection.isTemplates, + }, { schema: "schema:ethdebug/format/pointer", guard: isPointer, @@ -181,3 +197,22 @@ testSchemaGuards("ethdebug/format/pointer", [ guard: Pointer.isTemplate, }, ] as const); + +describe("collection names", () => { + const collections = [ + { group: [{ location: "stack", slot: 0 }] }, + { list: { count: 1, each: "i", is: { location: "stack", slot: "i" } } }, + { if: 1, then: { location: "stack", slot: 0 } }, + { define: { a: 0 }, in: { location: "stack", slot: "a" } }, + { template: "t" }, + { templates: {}, in: { location: "stack", slot: 0 } }, + ]; + + it.each(collections)("accepts an identifier name on %j", (collection) => { + expect(Pointer.isCollection({ name: "c", ...collection })).toBe(true); + }); + + it.each(collections)("rejects a non-identifier name on %j", (collection) => { + expect(Pointer.isCollection({ name: "0c", ...collection })).toBe(false); + }); +}); diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index baf15b387d..f4e15ef847 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -140,19 +140,33 @@ export namespace Pointer { ].some((guard) => guard(value)); export namespace Collection { - export interface Group { - group: Pointer[]; + /** + * Any collection may have a name, which scopes the regions inside it + * in the result of the pointer + */ + export interface Base { + name?: Identifier; } - export const isGroup = (value: unknown): value is Group => + export const isBase = (value: unknown): value is Base => !!value && typeof value === "object" && - Object.keys(value).length === 1 && + (!("name" in value) || isIdentifier(value.name)); + + const hasOnlyKeys = (value: object, keys: string[]) => + Object.keys(value).every((key) => key === "name" || keys.includes(key)); + + export interface Group extends Base { + group: Pointer[]; + } + export const isGroup = (value: unknown): value is Group => + isBase(value) && + hasOnlyKeys(value, ["group"]) && "group" in value && Array.isArray(value.group) && value.group.length >= 1 && value.group.every(isPointer); - export interface List { + export interface List extends Base { list: { count: Expression; each: Identifier; @@ -160,9 +174,8 @@ export namespace Pointer { }; } export const isList = (value: unknown): value is List => - !!value && - typeof value === "object" && - Object.keys(value).length === 1 && + isBase(value) && + hasOnlyKeys(value, ["list"]) && "list" in value && !!value.list && typeof value.list === "object" && @@ -174,21 +187,20 @@ export namespace Pointer { "is" in value.list && isPointer(value.list.is); - export interface Conditional { + export interface Conditional extends Base { if: Expression; then: Pointer; else?: Pointer; } export const isConditional = (value: unknown): value is Conditional => - !!value && - typeof value === "object" && + isBase(value) && "if" in value && isExpression(value.if) && "then" in value && isPointer(value.then) && (!("else" in value) || isPointer(value.else)); - export interface Scope { + export interface Scope extends Base { define: { [identifier: string]: Expression; }; @@ -196,8 +208,7 @@ export namespace Pointer { } export const isScope = (value: unknown): value is Scope => - !!value && - typeof value === "object" && + isBase(value) && "define" in value && typeof value.define === "object" && !!value.define && @@ -205,14 +216,13 @@ export namespace Pointer { "in" in value && isPointer(value.in); - export interface Reference { + export interface Reference extends Base { template: string; yields?: Record; } export const isReference = (value: unknown): value is Reference => - !!value && - typeof value === "object" && + isBase(value) && "template" in value && typeof value.template === "string" && !!value.template && @@ -223,7 +233,7 @@ export namespace Pointer { ([k, v]) => isIdentifier(k) && isIdentifier(v), ))); - export interface Templates { + export interface Templates extends Base { templates: { [identifier: string]: Pointer.Template; }; @@ -231,8 +241,7 @@ export namespace Pointer { } export const isTemplates = (value: unknown): value is Templates => - !!value && - typeof value === "object" && + isBase(value) && "templates" in value && typeof value.templates === "object" && !!value.templates && diff --git a/packages/pointers/CHANGELOG.md b/packages/pointers/CHANGELOG.md index ef4a7e46b5..906346784e 100644 --- a/packages/pointers/CHANGELOG.md +++ b/packages/pointers/CHANGELOG.md @@ -12,6 +12,11 @@ itself are tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). region keeps the bytes of its `expression` and has `offset` and `length` filled in with their defaults. An expression with no width, or a fragment outside the value, throws ([#411]). +- `Cursor.Region` has an optional `path`: the names of the named collections + that contain the region, outermost first. A named list adds its name and + then the element index. `dereference` omits `path` when no named + collection contains the region, so pointers without collection names give + the same regions as before ([#412]). ### Changed @@ -163,3 +168,4 @@ First publication. [#325]: https://github.com/ethdebug/format/pull/325 [#408]: https://github.com/ethdebug/format/pull/408 [#411]: https://github.com/ethdebug/format/pull/411 +[#412]: https://github.com/ethdebug/format/pull/412 diff --git a/packages/pointers/src/cursor.ts b/packages/pointers/src/cursor.ts index 53b6e925be..74e04599f1 100644 --- a/packages/pointers/src/cursor.ts +++ b/packages/pointers/src/cursor.ts @@ -41,8 +41,20 @@ export namespace Cursor { ? Data | undefined : R[K] : R[K]; + } & { + /** + * The names of the named collections that contain this region, + * outermost first. A named list adds its name and then the index of + * the element. Absent when no named collection contains the region. + */ + path?: Cursor.Path; }; + /** + * A list of collection names and named list element indexes + */ + export type Path = (string | number)[]; + /** * A collection of concrete regions. * diff --git a/packages/pointers/src/dereference/generate.ts b/packages/pointers/src/dereference/generate.ts index 80fea15093..7644a3ea0c 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 template definitions for inline templates const templatesStack: Array = []; + // Names of the enclosing named collections (and named list indexes) + const path: Cursor.Path = []; + const stack: Memo[] = [Memo.dereferencePointer(pointer)]; while (stack.length > 0) { const memo: Memo = stack.pop() as Memo; @@ -72,6 +75,10 @@ export async function* generateRegions( } } + if (path.length > 0) { + region = { ...region, path: [...path] }; + } + yield region; result = await process.next(); } @@ -121,6 +128,14 @@ export async function* generateRegions( templatesStack.pop(); break; } + case "push-path": { + path.push(memo.segment); + break; + } + case "pop-path": { + path.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 bb7b525585..42eb6e7421 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -1064,4 +1064,160 @@ describe("dereference", () => { ); }); }); + + describe("collection names", () => { + const slot = (value: Pointer.Expression) => + ({ location: "stack", slot: value }) as const; + + // the name and path of each output region + const pathsOf = async (pointer: Pointer) => { + const { regions } = await (await dereference(pointer)).view(state); + return regions.map(({ name, path }) => ({ name, path })); + }; + + it("adds the name of a group to the path of its regions", async () => { + expect( + await pathsOf({ + name: "g", + group: [ + { name: "a", ...slot(0) }, + { name: "b", ...slot(1) }, + ], + }), + ).toEqual([ + { name: "a", path: ["g"] }, + { name: "b", path: ["g"] }, + ]); + }); + + it("adds nested names outermost first", async () => { + expect( + await pathsOf({ + name: "outer", + group: [ + { name: "a", ...slot(0) }, + { name: "inner", group: [{ name: "b", ...slot(1) }] }, + ], + }), + ).toEqual([ + { name: "a", path: ["outer"] }, + { name: "b", path: ["outer", "inner"] }, + ]); + }); + + it("adds the element index after the name of a list", async () => { + expect( + await pathsOf({ + name: "items", + list: { + count: 2, + each: "i", + is: { name: "elem", group: [{ name: "a", ...slot("i") }] }, + }, + }), + ).toEqual([ + { name: "a", path: ["items", 0, "elem"] }, + { name: "a", path: ["items", 1, "elem"] }, + ]); + }); + + it("adds the element index when the list's `is` has no name", async () => { + expect( + await pathsOf({ + name: "items", + list: { count: 2, each: "i", is: { name: "a", ...slot("i") } }, + }), + ).toEqual([ + { name: "a", path: ["items", 0] }, + { name: "a", path: ["items", 1] }, + ]); + }); + + it("adds nothing for an unnamed list", async () => { + expect( + await pathsOf({ + list: { count: 2, each: "i", is: { name: "a", ...slot("i") } }, + }), + ).toEqual([ + { name: "a", path: undefined }, + { name: "a", path: undefined }, + ]); + }); + + it("adds the names of a conditional, a scope and templates", async () => { + expect( + await pathsOf({ + name: "t", + templates: {}, + in: { + name: "s", + define: { v: 0 }, + in: { name: "c", if: 1, then: { name: "a", ...slot("v") } }, + }, + }), + ).toEqual([{ name: "a", path: ["t", "s", "c"] }]); + }); + + it("tells apart two uses of one template by named references", async () => { + const { examples } = schemas["schema:ethdebug/format/pointer"] as { + examples: Pointer[]; + }; + const example = examples.find((pointer) => + JSON.stringify(pointer).includes("line-storage-contract-variable-slot"), + )!; + + const { regions } = await (await dereference(example)).view(state); + + expect([...regions]).toMatchObject([ + { name: "x", path: ["a"], slot: Data.fromNumber(0) }, + { name: "y", path: ["a"], slot: Data.fromNumber(0) }, + { name: "x", path: ["b"], slot: Data.fromNumber(1) }, + { name: "y", path: ["b"], slot: Data.fromNumber(1) }, + ]); + + // region-name lookup ignores paths: the last "x" is b's + expect(regions.named("x")).toHaveLength(2); + expect(regions.lookup["x"]).toMatchObject({ slot: Data.fromNumber(1) }); + }); + + it("does not rename collection names with yields", async () => { + const templates: Pointer.Templates = { + t: { expect: [], for: { name: "inner", group: [slot(0)] } }, + }; + const cursor = await dereference( + { name: "ref", template: "t", yields: { inner: "renamed" } }, + { templates }, + ); + const { regions } = await cursor.view(state); + expect(regions[0].path).toEqual(["ref", "inner"]); + }); + + it("hides collection names from expressions", async () => { + const cursor = await dereference({ + group: [ + { name: "g", group: [{ name: "a", ...slot(0) }] }, + { location: "memory", offset: { "~read": "g" }, length: 1 }, + ], + }); + await expect(cursor.view(state)).rejects.toThrow("Region not found: g"); + }); + + it("leaves region-name references within a named collection", async () => { + const { regions } = await ( + await dereference({ + name: "g", + group: [ + { name: "a", location: "memory", offset: 0x40, length: 0x20 }, + { + name: "b", + location: "memory", + offset: { ".offset": "a" }, + length: 1, + }, + ], + }) + ).view(state); + expect(regions.lookup["b"].offset).toEqual(Data.fromNumber(0x40)); + }); + }); }); diff --git a/packages/pointers/src/dereference/memo.ts b/packages/pointers/src/dereference/memo.ts index 03633b6868..00d80e7cc1 100644 --- a/packages/pointers/src/dereference/memo.ts +++ b/packages/pointers/src/dereference/memo.ts @@ -13,7 +13,9 @@ export type Memo = | Memo.PushRegionRenames | Memo.PopRegionRenames | Memo.PushTemplates - | Memo.PopTemplates; + | Memo.PopTemplates + | Memo.PushPath + | Memo.PopPath; export namespace Memo { /** @@ -163,4 +165,35 @@ export namespace Memo { export const popTemplates = (): PopTemplates => ({ kind: "pop-templates", }); + + /** + * A request to push a segment onto the path that output regions carry: + * a collection name, or the index of an element of a named list + */ + export interface PushPath { + kind: "push-path"; + segment: string | number; + } + + /** + * Initialize a PushPath memo + */ + export const pushPath = (segment: string | number): PushPath => ({ + kind: "push-path", + segment, + }); + + /** + * A request to pop the last segment from the path + */ + export interface PopPath { + kind: "pop-path"; + } + + /** + * Initialize a PopPath memo + */ + export const popPath = (): PopPath => ({ + kind: "pop-path", + }); } diff --git a/packages/pointers/src/dereference/process.ts b/packages/pointers/src/dereference/process.ts index fc9d6b7bd7..62f4200cc5 100644 --- a/packages/pointers/src/dereference/process.ts +++ b/packages/pointers/src/dereference/process.ts @@ -38,7 +38,20 @@ export async function* processPointer( } const collection = pointer; + const memos = yield* processCollection(collection, options); + // a collection's name scopes the regions inside it in the output + if (typeof collection.name === "string") { + return [Memo.pushPath(collection.name), ...memos, Memo.popPath()]; + } + + return memos; +} + +async function* processCollection( + collection: Pointer.Collection, + options: ProcessOptions, +): Process { if (Pointer.Collection.isGroup(collection)) { return yield* processGroup(collection, options); } @@ -63,7 +76,7 @@ export async function* processPointer( return yield* processTemplates(collection, options); } - console.error("%s", JSON.stringify(pointer, undefined, 2)); + console.error("%s", JSON.stringify(collection, undefined, 2)); throw new Error("Unexpected unknown kind of pointer"); } @@ -97,7 +110,7 @@ async function* processList( collection: Pointer.Collection.List, options: ProcessOptions, ): Process { - const { list } = collection; + const { name, list } = collection; const { count: countExpression, each, is } = list; const count = Value.toInteger(await evaluate(countExpression, options)); @@ -110,7 +123,16 @@ async function* processList( }), ); - memos.push(Memo.dereferencePointer(is)); + // a named list scopes each element apart by its index + if (typeof name === "string") { + memos.push( + Memo.pushPath(Number(index)), + Memo.dereferencePointer(is), + Memo.popPath(), + ); + } else { + memos.push(Memo.dereferencePointer(is)); + } } return memos; diff --git a/packages/pointers/src/dereference/region.ts b/packages/pointers/src/dereference/region.ts index a2d11c66d3..c28675b5d8 100644 --- a/packages/pointers/src/dereference/region.ts +++ b/packages/pointers/src/dereference/region.ts @@ -59,7 +59,7 @@ export async function evaluateRegion( ...options, regions: { ...options.regions, - "~this": partialRegion, + "~this": partialRegion as Cursor.Region, }, }); diff --git a/packages/pointers/src/read.ts b/packages/pointers/src/read.ts index e38bb028c5..a7c31cc8b7 100644 --- a/packages/pointers/src/read.ts +++ b/packages/pointers/src/read.ts @@ -1,4 +1,3 @@ -import { Pointer } from "@ethdebug/format"; import type { Machine } from "#machine"; import { Data } from "#data"; import type { Cursor } from "#cursor"; @@ -154,23 +153,18 @@ function slotAfter(slot: Data, carry: bigint): Data { return Data.fromUint(slot.asUint() + carry).padUntilAtLeast(slot.length); } -type DataProperties = { - [K in keyof Cursor.Region & - ("slot" | "offset" | "length")]: Cursor.Region[K]; -}; - -type PickDataPropertiesAsUints< - R extends Pointer.Region, - U extends keyof DataProperties, +type DataPropertiesAsUints< + T extends Cursor.Region, + U extends keyof T & ("slot" | "offset" | "length"), > = { - [K in U]: undefined extends Cursor.Region[K] ? bigint | undefined : bigint; + [K in U]: undefined extends T[K] ? bigint | undefined : bigint; }; function withPropertiesAsUints< - R extends Pointer.Region, - U extends keyof DataProperties, ->(uintKeys: U[], region: Cursor.Region): PickDataPropertiesAsUints { - const result: Partial> = {}; + T extends Cursor.Region, + U extends keyof T & ("slot" | "offset" | "length"), +>(uintKeys: U[], region: T): DataPropertiesAsUints { + const result: Partial> = {}; for (const key of uintKeys) { const data: Data | undefined = region[key] as Data | undefined; if (typeof data !== "undefined") { @@ -178,5 +172,5 @@ function withPropertiesAsUints< } } - return result as PickDataPropertiesAsUints; + return result as DataPropertiesAsUints; } diff --git a/packages/pointers/test/test-cases.ts b/packages/pointers/test/test-cases.ts index 32d439286a..91299de59c 100644 --- a/packages/pointers/test/test-cases.ts +++ b/packages/pointers/test/test-cases.ts @@ -274,6 +274,68 @@ const virtualRegionsTest: ObserveTraceTest<{ x: number; tag: string }> = { }, }; +/** + * the `struct Line { Point a; Point b; }` example uses one template for + * both members, so both give regions named "x" and "y"; each region's + * path tells which member it belongs to + */ +const namedReferencesTest: ObserveTraceTest<{ + a: { x: number; y: number }; + b: { x: number; y: number }; +}> = { + pointer: findExamplePointer("line-storage-contract-variable-slot"), + compileOptions: singleSourceCompilation({ + path: "LineStorage.sol", + contractName: "LineStorage", + content: `contract LineStorage { + Line line; + + constructor() { + line = Line({ a: Point({ x: 1, y: 2 }), b: Point({ x: 3, y: 4 }) }); + line.b.x = 5; + } + } + + struct Point { + uint8 x; + uint8 y; + } + + struct Line { + Point a; + Point b; + } + `, + }), + + expectedValues: [ + { a: { x: 0, y: 0 }, b: { x: 0, y: 0 } }, + { a: { x: 1, y: 2 }, b: { x: 3, y: 4 } }, + { a: { x: 1, y: 2 }, b: { x: 5, y: 4 } }, + ], + + async observe({ regions, read }) { + const field = async (member: string, name: string) => { + const region = regions.find( + (region) => + region.name === name && + region.path?.length === 1 && + region.path[0] === member, + )!; + return Number((await read(region)).asUint()); + }; + + return { + a: { x: await field("a", "x"), y: await field("a", "y") }, + b: { x: await field("b", "x"), y: await field("b", "y") }, + }; + }, + + equals(a, b) { + return JSON.stringify(a) === JSON.stringify(b); + }, +}; + /** * collection of descriptions of tests that compile+deploy Solidity code, * then step through the machine trace of that code's execution, watching @@ -289,4 +351,5 @@ export const observeTraceTests = { "string storage (multi-slot)": multiSlotStringStorageTest, "uint256[] memory": uint256ArrayMemoryTest, "virtual regions": virtualRegionsTest, + "struct storage (named references)": namedReferencesTest, }; diff --git a/packages/web/docs/core-schemas/pointers/collections.mdx b/packages/web/docs/core-schemas/pointers/collections.mdx index 24bc9abb12..86713ef102 100644 --- a/packages/web/docs/core-schemas/pointers/collections.mdx +++ b/packages/web/docs/core-schemas/pointers/collections.mdx @@ -136,6 +136,30 @@ Pointers within collections can include a `name` property. Named regions are tracked during resolution and can be referenced using the `.slot`, `.offset`, and `.length` syntax in expressions. +## Collection names + +Any collection can also have a `name`. A collection's name scopes the regions +inside it in the result of the pointer. This tells apart regions that have the +same name, such as the regions of two references to one template: + +```yaml +group: + - define: { "point-slot": 0 } + in: { name: "a", template: "point" } + - define: { "point-slot": 1 } + in: { name: "b", template: "point" } +``` + +Both references give regions named `x` and `y`. The regions of the first +reference are in the scope `a`, and the regions of the second are in the +scope `b`. A named list also scopes each element by its index, so the regions +of one element stay together. + +Collection names affect only the result. Expressions do not see them: `~read`, +`.offset`, `.length` and `.slot` refer to region names only. A consumer that +ignores collection names gets the same regions, with the same names. The +`yields` of a reference renames regions only, never collections. + ## Learn more - [Regions](./regions) for simple pointer definitions diff --git a/packages/web/docs/implementation-guides/pointers/dereference-logic/generating-regions.mdx b/packages/web/docs/implementation-guides/pointers/dereference-logic/generating-regions.mdx index 37cb80559a..7e5a009405 100644 --- a/packages/web/docs/implementation-guides/pointers/dereference-logic/generating-regions.mdx +++ b/packages/web/docs/implementation-guides/pointers/dereference-logic/generating-regions.mdx @@ -39,8 +39,8 @@ this function's `while()` loop operates on the stack, memos for saving new named regions or updating variable values may appear and then get handled appropriately. -For reference, see the `memo.ts` module for type definitions for each of the -three types of memo and their corresponding helper constructor functions. +For reference, see the `memo.ts` module for type definitions for each +kind of memo and its corresponding helper constructor function.
See the `memo.ts` module @@ -239,3 +239,13 @@ enclosing scope. If every remaining member fails in turn, they form a cycle. sourcePath="src/dereference/process.ts" extract={(sourceFile) => sourceFile.getFunction("processScope")} /> + +### Collection names + +`processPointer()` handles a collection's `name` for every kind of collection +at once: it surrounds the collection's memos with a `PushPath` memo for the +name and a `PopPath` memo after them. `generateRegions()` keeps the current +path and copies it onto each region it yields, when the path is not empty. +A named list also surrounds each element with a `PushPath` memo for the +element's index (see `processList()` above). Paths never reach the saved +regions or variables, so expressions do not see collection names. diff --git a/packages/web/docs/implementation-guides/pointers/types/cursors.mdx b/packages/web/docs/implementation-guides/pointers/types/cursors.mdx index f0d24bd66f..83315d13b5 100644 --- a/packages/web/docs/implementation-guides/pointers/types/cursors.mdx +++ b/packages/web/docs/implementation-guides/pointers/types/cursors.mdx @@ -46,6 +46,12 @@ can reference internal variables and other regions, a `Cursor.Region` represents the runtime result of evaluating all these expressions: a specific range of bytes in a specific data location. +A `Cursor.Region` also has a `path` when named collections contain it: the +names of those collections, outermost first. A named list adds its name and +then the index of the element, so a list named `items` whose `is` is a group +named `elem` gives the paths `["items", 0, "elem"]`, `["items", 1, "elem"]`, +and so on. Regions that no named collection contains have no `path`. + The full listing of `namespace Cursor` follows: