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
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const CODAMA_ERROR__UNSUPPORTED_VERSION = 12;
export const CODAMA_ERROR__INVALID_BRANDED_STRING = 13;
export const CODAMA_ERROR__INJECTED_VALUE_NOT_PROVIDED = 14;
export const CODAMA_ERROR__CANNOT_RESOLVE_PATH = 15;
export const CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING = 16;

// Visitors-related errors.
// Reserve error codes in the range [1200000-1200999].
Expand Down Expand Up @@ -163,6 +164,7 @@ export type CodamaErrorCode =
| typeof CODAMA_ERROR__INVALID_BRANDED_STRING
| typeof CODAMA_ERROR__LINKED_NODE_NOT_FOUND
| typeof CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE
| typeof CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING
| typeof CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS
| typeof CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE
| typeof CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND
Expand Down
5 changes: 5 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
CODAMA_ERROR__INVALID_BRANDED_STRING,
CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING,
CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS,
CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE,
CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND,
Expand Down Expand Up @@ -259,6 +260,10 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
[CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]: {
fsFunction: string;
};
[CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING]: {
/** The path that should contain a program node, e.g. to resolve links from. */
path: readonly Node[];
};
[CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS]: {
dependencies: readonly string[];
message: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
CODAMA_ERROR__INVALID_BRANDED_STRING,
CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING,
CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS,
CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE,
CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND,
Expand Down Expand Up @@ -135,6 +136,8 @@ export const CodamaErrorMessages: Readonly<{
[CODAMA_ERROR__LINKED_NODE_NOT_FOUND]: 'Could not find linked node [$name] from [$kind].',
[CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]:
'Node.js filesystem function [$fsFunction] is not available in your environment.',
[CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING]:
'Expected a node path containing a program node, e.g. to resolve the links of its last node.',
[CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS]:
'No version specified for the following dependencies: [$dependencies]. $message',
[CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE]: 'Cannot render the encountered node of kind [$kind].',
Expand Down
41 changes: 41 additions & 0 deletions packages/visitors-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,47 @@ const size = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]),

By default, this visitor will keep track of its own `NodeStack` but you may provide your own via the `stack` option in order to share the same `NodeStack` across multiple visitors.

### `getDefinedTypeLinksVisitor`

The `getDefinedTypeLinksVisitor` lists the paths of the defined types a node links to, without following these links. Links that cannot be resolved are skipped, and so are the defined types of `EnumValueNodes`, which only reference them. It powers `isDefinedTypeCyclic`.

```ts
// node = struct { value: link(amount), next: option<link(node)> }
const stack = new NodeStack();
stack.visitPath([root, program, node], getDefinedTypeLinksVisitor(linkables, { stack }));
// ^ [[root, program, amount], [root, program, node]]
```

### `getHasFiniteValueVisitor`

The `getHasFiniteValueVisitor` tells whether a type has at least one finite value, following links. Options, enums with another variant and collections without a fixed positive count stop a recursion, whereas a struct field or a fixed-size array linking back to its own type never does. It powers `hasDefinedTypeFiniteValue`.

```ts
// list = struct { next: option<link(list)> }, loop = struct { next: link(loop) }
const stack = new NodeStack();
stack.visitPath([root, program, list], getHasFiniteValueVisitor(linkables, { stack })); // true
stack.visitPath([root, program, loop], getHasFiniteValueVisitor(linkables, { stack })); // false
```

### `isDefinedTypeCyclic`

The `isDefinedTypeCyclic` helper tells whether a defined type links back to itself, directly or through other defined types, following the links recorded in a `LinkableDictionary`. The path must contain the program defining the type, since links are resolved from it.

```ts
// node = struct { value: u8, next: option<link(node)> }
isDefinedTypeCyclic([root, program, node], linkables); // true
```

### `hasDefinedTypeFiniteValue`

The `hasDefinedTypeFiniteValue` helper tells whether a defined type has at least one finite value, using the `getHasFiniteValueVisitor`. Types lack one when they are cyclic without a way out, or when they contain an enum without variants. As above, the path must contain the program defining the type.

```ts
// list = struct { next: option<link(list)> }, loop = struct { next: link(loop) }
hasDefinedTypeFiniteValue([root, program, list], linkables); // true
hasDefinedTypeFiniteValue([root, program, loop], linkables); // false
```

### `getResolvedInstructionInputsVisitor`

The `getResolvedInstructionInputsVisitor` visits `InstructionNodes` only and returns an array of instruction accounts and arguments in the order they should be rendered for their default values to be resolved.
Expand Down
13 changes: 13 additions & 0 deletions packages/visitors-core/src/NodePath.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING, CodamaError } from '@codama/errors';
import { assertIsNode, GetNodeFromKind, InstructionNode, isNode, Node, NodeKind, ProgramNode } from '@codama/nodes';

/**
Expand Down Expand Up @@ -48,6 +49,18 @@ export function findProgramNodeFromPath(path: NodePath): ProgramNode | undefined
return findLastNodeFromPath(path, 'programNode');
}

/**
* Assert that the path contains a program node, e.g. to resolve the links of
* its last node, which are resolved from the closest program of their path.
*
* @throws `CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING` when it does not.
*/
export function assertNodePathHasProgram(path: NodePath): void {
if (!findProgramNodeFromPath(path)) {
throw new CodamaError(CODAMA_ERROR__NODE_PATH_PROGRAM_MISSING, { path });
}
}

export function findInstructionNodeFromPath(path: NodePath): InstructionNode | undefined {
return findLastNodeFromPath(path, 'instructionNode');
}
Expand Down
29 changes: 17 additions & 12 deletions packages/visitors-core/src/getByteSizeVisitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CountNode, isNode, isScalarEnum, REGISTERED_TYPE_NODE_KINDS, RegisteredTypeNode } from '@codama/nodes';
import {
CountNode,
DefinedTypeNode,
isNode,
isScalarEnum,
REGISTERED_TYPE_NODE_KINDS,
RegisteredTypeNode,
} from '@codama/nodes';

import { applyByteSizeTransforms, nodeHasTransforms } from './applyByteSizeTransforms';
import { extendVisitor } from './extendVisitor';
Expand All @@ -25,8 +32,9 @@ export function getByteSizeVisitor(
): Visitor<number | null, ByteSizeVisitorKeys> {
const stack = options.stack ?? new NodeStack();

const visitedDefinedTypes = new Map<string, number | null>();
const definedTypeStack: string[] = [];
// Keyed by node rather than identifier: same-named types of different programs are distinct.
const visitedDefinedTypes = new Map<DefinedTypeNode, number | null>();
const visitingDefinedTypes = new Set<DefinedTypeNode>();

const sumSizes = (values: (number | null)[]): number | null =>
values.reduce((all, one) => (all === null || one === null ? null : all + one), 0 as number | null);
Expand Down Expand Up @@ -81,24 +89,21 @@ export function getByteSizeVisitor(
},

visitDefinedType(node, { self }) {
if (visitedDefinedTypes.has(node.identifier)) {
return visitedDefinedTypes.get(node.identifier)!;
}
definedTypeStack.push(node.identifier);
if (visitedDefinedTypes.has(node)) return visitedDefinedTypes.get(node)!;
visitingDefinedTypes.add(node);
const child = visit(node.type, self);
definedTypeStack.pop();
visitedDefinedTypes.set(node.identifier, child);
visitingDefinedTypes.delete(node);
visitedDefinedTypes.set(node, child);
return child;
},

visitDefinedTypeLink(node, { self }) {
// Fetch the linked type and return null if not found.
const linkedDefinedPath = linkables.getPath(stack.getPath(node.kind));
if (!linkedDefinedPath) return null;
const linkedDefinedType = getLastNodeFromPath(linkedDefinedPath);

// This prevents infinite recursion by assuming cyclic types don't have a fixed size.
if (definedTypeStack.includes(linkedDefinedType.identifier)) return null;
// Cyclic types have no bounded size, and following them would never end.
if (visitingDefinedTypes.has(getLastNodeFromPath(linkedDefinedPath))) return null;

return stack.visitPath(linkedDefinedPath, self);
},
Expand Down
53 changes: 53 additions & 0 deletions packages/visitors-core/src/getDefinedTypeLinksVisitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { DefinedTypeNode } from '@codama/nodes';

import { extendVisitor } from './extendVisitor';
import { mergeVisitor } from './generated/mergeVisitor';
import type { LinkableDictionary } from './LinkableDictionary';
import { NodePath } from './NodePath';
import { NodeStack } from './NodeStack';
import { pipe } from './pipe';
import { recordNodeStackVisitor } from './recordNodeStackVisitor';
import { visit, Visitor } from './visitor';

/**
* List the paths of the defined types the visited node links to, without
* following these links. Links that cannot be resolved are skipped.
*
* Only links shaping a type are listed: the defined type of an
* `enumValueNode` merely references it, so it is left out.
*
* Links are resolved from the closest program of the current path, so visit
* nodes with their full path, e.g. using `stack.visitPath(path, visitor)` with
* the visitor's own stack. Links visited without a program are not resolved.
*
* @example
* ```ts
* // node = struct { value: link(amount), next: option<link(node)> }
* stack.visitPath([root, program, node], getDefinedTypeLinksVisitor(linkables, { stack }));
* // [[root, program, amount], [root, program, node]]
* ```
*/
export function getDefinedTypeLinksVisitor(
linkables: LinkableDictionary,
options: { stack?: NodeStack } = {},
): Visitor<NodePath<DefinedTypeNode>[]> {
const stack = options.stack ?? new NodeStack();

return pipe(
mergeVisitor(
() => [] as NodePath<DefinedTypeNode>[],
(_, values) => values.flat(),
),
v =>
extendVisitor(v, {
visitDefinedTypeLink(node, { next }) {
const linkedPath = linkables.getPath(stack.getPath(node.kind));
return [...(linkedPath ? [linkedPath] : []), ...next(node)];
},
visitEnumValue(node, { self }) {
return node.value ? visit(node.value, self) : [];
},
}),
v => recordNodeStackVisitor(v, stack),
);
}
109 changes: 109 additions & 0 deletions packages/visitors-core/src/getHasFiniteValueVisitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { CountNode, DefinedTypeNode, isNode } from '@codama/nodes';

import { extendVisitor } from './extendVisitor';
import { mergeVisitor } from './generated/mergeVisitor';
import type { LinkableDictionary } from './LinkableDictionary';
import { getLastNodeFromPath } from './NodePath';
import { NodeStack } from './NodeStack';
import { pipe } from './pipe';
import { recordNodeStackVisitor } from './recordNodeStackVisitor';
import { visit, Visitor } from './visitor';

/**
* Whether the visited type has at least one finite value, following links.
*
* Only the shape of values matters: options may be `None`, enums may pick any
* variant and collections without a fixed positive count may be empty, so all
* of them stop a recursion. Transforms, prefixes and sizes only prevent a
* value from existing when their constants link back to a type being visited,
* since those constants could then never be created. Links that cannot be
* resolved count as finite.
*
* A link back to a defined type being visited has no finite value on its own:
* a finite value never needs to nest a type within itself, so another branch
* must provide one. Enums without variants have no value at all.
*
* Links are resolved from the closest program of the current path, so visit
* nodes with their full path, e.g. using `stack.visitPath(path, visitor)` with
* the visitor's own stack. Links visited without a program are not resolved.
*
* @example
* ```ts
* // list = struct { next: option<link(list)> }, loop = struct { next: link(loop) }
* stack.visitPath([root, program, list], getHasFiniteValueVisitor(linkables, { stack })); // true
* stack.visitPath([root, program, loop], getHasFiniteValueVisitor(linkables, { stack })); // false
* ```
*/
export function getHasFiniteValueVisitor(
linkables: LinkableDictionary,
options: { stack?: NodeStack } = {},
): Visitor<boolean> {
const stack = options.stack ?? new NodeStack();
const visitingDefinedTypes = new Set<DefinedTypeNode>();
// Only `true` is cached: a `false` may only hold because of the defined types being
// visited, whereas a `true` holds whichever are, so it never needs computing again.
const finiteDefinedTypes = new Set<DefinedTypeNode>();

return pipe(
mergeVisitor(
() => true,
(_, values) => values.every(Boolean),
),
v =>
extendVisitor(v, {
visitArrayType(node, { self }) {
return !hasFixedPositiveCount(node.count) || visit(node.item, self);
},

visitDefinedType(node, { next }) {
if (finiteDefinedTypes.has(node)) return true;
visitingDefinedTypes.add(node);
const hasFiniteValue = next(node);
visitingDefinedTypes.delete(node);
if (hasFiniteValue) finiteDefinedTypes.add(node);
return hasFiniteValue;
},

visitDefinedTypeLink(node, { self }) {
const linkedPath = linkables.getPath(stack.getPath(node.kind));
if (!linkedPath) return true;
if (visitingDefinedTypes.has(getLastNodeFromPath(linkedPath))) return false;
return stack.visitPath(linkedPath, self);
},
Comment thread
lorisleiva marked this conversation as resolved.

visitEnumType(node, { self }) {
return (node.variants ?? []).some(variant => visit(variant, self));
},
Comment thread
lorisleiva marked this conversation as resolved.

// The defined type of an enum value only references it.
visitEnumValue() {
return true;
},

visitMapType(node, { self }) {
return !hasFixedPositiveCount(node.count) || (visit(node.key, self) && visit(node.value, self));
},

visitOptionType() {
return true;
},

visitRemainderOptionType() {
return true;
},

visitSetType(node, { self }) {
return !hasFixedPositiveCount(node.count) || visit(node.item, self);
},

visitZeroableOptionType() {
return true;
},
}),
v => recordNodeStackVisitor(v, stack),
);
}

function hasFixedPositiveCount(count: CountNode): boolean {
return isNode(count, 'fixedCountNode') && count.value > 0;
}
13 changes: 8 additions & 5 deletions packages/visitors-core/src/getInstructionDataFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export type InstructionDataField = {
*
* Fields are only addressable where the data type resolves to a struct, so
* nested fields are listed for struct-typed (or struct-linked) fields only.
* Each defined type is followed at most once, which guards against cycles.
* Each defined type is followed at most once per branch, which guards against
* cycles while still expanding sibling fields linking the same type.
*
* @param instructionPath - The path to the instruction. It must contain the
* instruction's `programNode` for linked data to be followed; otherwise a
Expand All @@ -37,18 +38,20 @@ export function getInstructionDataFields(
): InstructionDataField[] {
const fields: InstructionDataField[] = [];
const stack = new NodeStack(instructionPath);
// Keyed by node rather than identifier: same-named types of different programs are distinct.
const walkedDefinedTypes = new Set<DefinedTypeNode>();
// The defined types followed on the current branch, keyed by node rather than
// identifier: same-named types of different programs are distinct.
const followedDefinedTypes = new Set<DefinedTypeNode>();

const walk = (type: TypeNode | undefined, prefix: string): void => {
if (!type) return;
if (isNode(type, 'definedTypeLinkNode')) {
const linkedPath = linkables.getPath([...stack.getPath(), type]);
if (!linkedPath) return;
const definedType = getLastNodeFromPath(linkedPath);
if (walkedDefinedTypes.has(definedType)) return;
walkedDefinedTypes.add(definedType);
if (followedDefinedTypes.has(definedType)) return;
followedDefinedTypes.add(definedType);
stack.withPath(linkedPath, () => walk(definedType.type, prefix));
followedDefinedTypes.delete(definedType);
return;
}
if (!isNode(type, 'structTypeNode')) return;
Expand Down
Loading
Loading