Skip to content
Draft
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions packages/format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions packages/format/src/types/pointer/pointer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
});
});
49 changes: 29 additions & 20 deletions packages/format/src/types/pointer/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,29 +140,42 @@ 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;
is: 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" &&
Expand All @@ -174,45 +187,42 @@ 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;
};
in: Pointer;
}

export const isScope = (value: unknown): value is Scope =>
!!value &&
typeof value === "object" &&
isBase(value) &&
"define" in value &&
typeof value.define === "object" &&
!!value.define &&
Object.keys(value.define).every((key) => isIdentifier(key)) &&
"in" in value &&
isPointer(value.in);

export interface Reference {
export interface Reference extends Base {
template: string;
yields?: Record<string, string>;
}

export const isReference = (value: unknown): value is Reference =>
!!value &&
typeof value === "object" &&
isBase(value) &&
"template" in value &&
typeof value.template === "string" &&
!!value.template &&
Expand All @@ -223,16 +233,15 @@ export namespace Pointer {
([k, v]) => isIdentifier(k) && isIdentifier(v),
)));

export interface Templates {
export interface Templates extends Base {
templates: {
[identifier: string]: Pointer.Template;
};
in: Pointer;
}

export const isTemplates = (value: unknown): value is Templates =>
!!value &&
typeof value === "object" &&
isBase(value) &&
"templates" in value &&
typeof value.templates === "object" &&
!!value.templates &&
Expand Down
6 changes: 6 additions & 0 deletions packages/pointers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions packages/pointers/src/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
15 changes: 15 additions & 0 deletions packages/pointers/src/dereference/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export async function* generateRegions(
// Stack of template definitions for inline templates
const templatesStack: Array<Pointer.Templates> = [];

// 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;
Expand Down Expand Up @@ -72,6 +75,10 @@ export async function* generateRegions(
}
}

if (path.length > 0) {
region = { ...region, path: [...path] };
}

yield region;
result = await process.next();
}
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading