Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/pointers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ itself are tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md).
- `dereference` composes the `yields` renames of nested template references.
A region now takes the rename of each enclosing reference, innermost first,
where only the innermost rename used to apply.
- `dereference` accepts regions named like object properties, such as
`length` or `constructor`. A region named `length` made `cursor.view` throw
a `TypeError`; this is how solc's pointer templates name the length of a
top-level dynamic array. Such a region is reachable by `regions.lookup` and
`regions.named`, but not as a property of `regions` itself.

## 0.1.0-preview.0 — 2026-09-21

Expand Down
32 changes: 19 additions & 13 deletions packages/pointers/src/dereference/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export function createCursor(
list.push(region);
}

const named: { [name: string]: Cursor.Region[] } = {};
const current: { [name: string]: Cursor.Region } = {};
// no prototype, so that names like `constructor` or `__proto__` are
// ordinary keys
const named: { [name: string]: Cursor.Region[] } = Object.create(null);
const current: { [name: string]: Cursor.Region } = Object.create(null);

const propertyFlags = {
writable: false,
Expand Down Expand Up @@ -44,26 +46,30 @@ export function createCursor(
}
}

for (const [name, region] of Object.entries(current)) {
Object.defineProperty(regions, name, {
value: region,
...propertyFlags,
});
}

Object.defineProperties(regions, {
named: {
value: (name: string) => named[name] || [],
value: (name: string) => (name in named ? named[name] : []),
...propertyFlags,
},
lookup: {
value: {
...current,
},
value: current,
...propertyFlags,
},
});

// Also expose each name as a property of the array, unless it would
// shadow a property the array already has (e.g. `length`); those
// regions remain reachable by `named` and `lookup`.
for (const [name, region] of Object.entries(current)) {
if (name in regions) {
continue;
}
Object.defineProperty(regions, name, {
value: region,
...propertyFlags,
});
}

return {
regions,
async read(region: Cursor.Region) {
Expand Down
11 changes: 8 additions & 3 deletions packages/pointers/src/dereference/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function* generateRegions(
// nested yields compose
if (region.name) {
const name = renameStack.reduceRight(
(name, mapping) => mapping[name] ?? name,
(name, mapping) => (hasOwn(mapping, name) ? mapping[name] : name),
region.name,
);
if (name !== region.name) {
Expand Down Expand Up @@ -123,6 +123,9 @@ export async function* generateRegions(
}
}

const hasOwn = (object: object, key: string) =>
Object.prototype.hasOwnProperty.call(object, key);

async function initializeProcessOptions({
templates,
state,
Expand All @@ -131,8 +134,10 @@ async function initializeProcessOptions({
const currentStackLength = await state.stack.length;
const stackLengthChange = currentStackLength - initialStackLength;

const regions: Record<string, Cursor.Region> = {};
const variables: Record<string, Value> = {};
// no prototype, so that names like `constructor` or `__proto__` are
// ordinary keys
const regions: Record<string, Cursor.Region> = Object.create(null);
const variables: Record<string, Value> = Object.create(null);

return {
templates,
Expand Down
45 changes: 45 additions & 0 deletions packages/pointers/src/dereference/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,51 @@ describe("dereference", () => {
expect(regions.named("outer-data")).toHaveLength(2);
});

describe.each(["length", "constructor", "__proto__", "toString"])(
"regions named like object properties (%s)",
(name) => {
it("allows referencing the region by name", async () => {
const pointer: Pointer = {
group: [
{ name, location: "memory", offset: 32, length: 4 },
{
name: "data",
location: "memory",
offset: { ".offset": name },
length: { ".length": name },
},
],
};

const cursor = await dereference(pointer);
const { regions } = await cursor.view(state);

expect(regions).toHaveLength(2);
expect(regions[1].offset).toEqual(Data.fromNumber(32));
expect(regions[1].length).toEqual(Data.fromNumber(4));
});

it("allows renaming the region with yields", async () => {
const templates: Pointer.Templates = {
leaf: {
expect: [],
for: { name, location: "memory", offset: 0, length: 1 },
},
};
const pointer: Pointer = {
template: "leaf",
yields: { [name]: "renamed" },
};

const cursor = await dereference(pointer, { templates });
const { regions } = await cursor.view(state);

expect(regions).toHaveLength(1);
expect(regions[0].name).toEqual("renamed");
});
},
);

it("composes yields renames across nested template references", async () => {
const templates: Pointer.Templates = {
leaf: {
Expand Down
7 changes: 4 additions & 3 deletions packages/pointers/src/dereference/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ async function* processScope(
): Process {
const { define: variableExpressions, in: in_ } = collection;

const allVariables = {
...options.variables,
};
const allVariables: Record<string, Value> = Object.assign(
Object.create(null),
options.variables,
);
const newVariables: { [identifier: string]: Value } = {};
for (const [identifier, expression] of Object.entries(variableExpressions)) {
const value = await evaluate(expression, {
Expand Down
Loading