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
16 changes: 10 additions & 6 deletions packages/renderers-js/src/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { RenderMap } from '@codama/renderers-core';
import {
extendVisitor,
findProgramNodeFromPath,
getResolvedInstructionInputsVisitor,
LinkableDictionary,
NodeStack,
Expand Down Expand Up @@ -141,13 +142,14 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
v =>
extendVisitor(v, {
visitAccount(node) {
if (!stack.getProgram()) {
const accountPath = stack.getPath('accountNode');
if (!findProgramNodeFromPath(accountPath)) {
throw new Error('Account must be visited inside a program.');
}

const scope = {
...globalScope,
accountPath: stack.getPath('accountNode'),
accountPath,
typeManifest: visit(node, typeManifestVisitor),
};

Expand Down Expand Up @@ -221,7 +223,8 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
},

visitInstruction(node) {
if (!stack.getProgram()) {
const instructionPath = stack.getPath('instructionNode');
if (!findProgramNodeFromPath(instructionPath)) {
throw new Error('Instruction must be visited inside a program.');
}

Expand All @@ -236,7 +239,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
strict: nameApi.dataType(instructionExtraName),
}),
),
instructionPath: stack.getPath('instructionNode'),
instructionPath,
renamedArgs: getRenamedArgsMap(node),
resolvedInputs: visit(node, resolvedInstructionInputVisitor),
};
Expand Down Expand Up @@ -289,11 +292,12 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
},

visitPda(node) {
if (!stack.getProgram()) {
const pdaPath = stack.getPath('pdaNode');
if (!findProgramNodeFromPath(pdaPath)) {
throw new Error('Account must be visited inside a program.');
}

const scope = { ...globalScope, pdaPath: stack.getPath('pdaNode') };
const scope = { ...globalScope, pdaPath };
const pdaFunctionFragment = getPdaFunctionFragment(scope);
const imports = new ImportMap().mergeWith(pdaFunctionFragment);

Expand Down
6 changes: 4 additions & 2 deletions packages/renderers-js/src/getTypeManifestVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@codama/nodes';
import {
extendVisitor,
findLastNodeFromPath,
LinkableDictionary,
NodeStack,
pipe,
Expand Down Expand Up @@ -830,8 +831,9 @@ export function getTypeManifestVisitor(input: {
}

// Check if we are inside an instruction or account to use discriminator constants when available.
const instructionNode = stack.find('instructionNode');
const accountNode = stack.find('accountNode');
const parentPath = stack.getPath();
const instructionNode = findLastNodeFromPath(parentPath, 'instructionNode');
const accountNode = findLastNodeFromPath(parentPath, 'accountNode');
const discriminatorPrefix = instructionNode ? instructionNode.name : accountNode?.name;
const discriminators =
(instructionNode ? instructionNode.discriminators : accountNode?.discriminators) ?? [];
Expand Down
2 changes: 1 addition & 1 deletion packages/validators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type ValidationItem = {
// The node that the validation item is related to.
node: Node;
// The path of nodes that led to the node above (including the node itself).
path: readonly Node[];
path: NodePath;
};
```
Expand Down
8 changes: 4 additions & 4 deletions packages/validators/src/ValidationItem.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node } from '@codama/nodes';
import { NodeStack } from '@codama/visitors-core';
import { NodePath, NodeStack } from '@codama/visitors-core';

export const LOG_LEVELS = ['debug', 'trace', 'info', 'warn', 'error'] as const;
export type LogLevel = (typeof LOG_LEVELS)[number];
Expand All @@ -8,20 +8,20 @@ export type ValidationItem = {
level: LogLevel;
message: string;
node: Node;
path: readonly Node[];
path: NodePath;
};

export function validationItem(
level: LogLevel,
message: string,
node: Node,
stack: Node[] | NodeStack,
path: NodePath | NodeStack,
): ValidationItem {
return {
level,
message,
node,
path: Array.isArray(stack) ? [...stack] : stack.all(),
path: Array.isArray(path) ? path : (path as NodeStack).getPath(),
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/validators/src/getValidationItemsVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
const items = [] as ValidationItem[];
if (!node.name) {
items.push(validationItem('error', 'Pointing to a defined type with no name.', node, stack));
} else if (!linkables.has(stack.getPath())) {
} else if (!linkables.has(stack.getPath(node.kind))) {
items.push(
validationItem(
'error',
Expand Down
12 changes: 2 additions & 10 deletions packages/visitors-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,19 +442,11 @@ const lastNode = nodeStack.pop();
// Peek at the last node in the stack.
const lastNode = nodeStack.peek();
// Get all the nodes in the stack as an array.
const nodes = nodeStack.all();
// Get the closest node in the stack matching one or several node kinds.
const nodes = nodeStack.find('accountNode');
// Get the closest program node in the stack.
const nodes = nodeStack.getProgram();
// Get the closest instruction node in the stack.
const nodes = nodeStack.getInstruction();
const path = nodeStack.getPath();
// Check if the stack is empty.
const isEmpty = nodeStack.isEmpty();
// Clone the stack.
const clonedStack = nodeStack.clone();
// Get a string representation of the stack.
const stackString = nodeStack.toString();
```

### `recordNodeStackVisitor`
Expand All @@ -470,7 +462,7 @@ const visitor = pipe(
v => recordNodeStackVisitor(v, stack),
v =>
interceptVisitor(v, (node, next) => {
console.log(stack.clone().toString());
console.log(nodePathToString(stack.getPath()));
return next(node);
}),
);
Expand Down
22 changes: 19 additions & 3 deletions packages/visitors-core/src/NodePath.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { assertIsNode, GetNodeFromKind, InstructionNode, isNode, Node, NodeKind, ProgramNode } from '@codama/nodes';

export type NodePath<TNode extends Node = Node> = readonly [...Node[], TNode];
export type NodePath<TNode extends Node | undefined = undefined> = TNode extends undefined
? readonly Node[]
: readonly [...Node[], TNode];

export function getLastNodeFromPath<TNode extends Node>(path: NodePath<TNode>): TNode {
return path[path.length - 1] as TNode;
Expand Down Expand Up @@ -47,16 +49,30 @@ export function getNodePathUntilLastNode<TKind extends NodeKind>(
return path.slice(0, lastIndex + 1) as unknown as NodePath<GetNodeFromKind<TKind>>;
}

function isNotEmptyNodePath(path: NodePath | null | undefined): path is NodePath<Node> {
return !!path && path.length > 0;
}

export function isNodePath<TKind extends NodeKind>(
path: NodePath | null | undefined,
kind: TKind | TKind[],
): path is NodePath<GetNodeFromKind<TKind>> {
return isNode(path ? getLastNodeFromPath(path) : null, kind);
return isNode(isNotEmptyNodePath(path) ? getLastNodeFromPath<Node>(path) : null, kind);
}

export function assertIsNodePath<TKind extends NodeKind>(
path: NodePath | null | undefined,
kind: TKind | TKind[],
): asserts path is NodePath<GetNodeFromKind<TKind>> {
assertIsNode(path ? getLastNodeFromPath(path) : null, kind);
assertIsNode(isNotEmptyNodePath(path) ? getLastNodeFromPath<Node>(path) : null, kind);
}

export function nodePathToStringArray(path: NodePath): string[] {
return path.map((node): string => {
return 'name' in node ? `[${node.kind}]${node.name}` : `[${node.kind}]`;
});
}

export function nodePathToString(path: NodePath): string {
return nodePathToStringArray(path).join(' > ');
}
3 changes: 2 additions & 1 deletion packages/visitors-core/src/NodeSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export const getNodeSelectorFunction = (selector: NodeSelector): NodeSelectorFun
const nodeSelectors = selector.split('.');
const lastNodeSelector = nodeSelectors.pop() as string;

return (node, stack) => checkNode(node, lastNodeSelector) && checkStack(stack.all() as Node[], [...nodeSelectors]);
return (node, stack) =>
checkNode(node, lastNodeSelector) && checkStack(stack.getPath() as Node[], [...nodeSelectors]);
};

export const getConjunctiveNodeSelectorFunction = (selector: NodeSelector | NodeSelector[]): NodeSelectorFunction => {
Expand Down
98 changes: 36 additions & 62 deletions packages/visitors-core/src/NodeStack.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,72 @@
import {
assertIsNode,
GetNodeFromKind,
InstructionNode,
Node,
NodeKind,
ProgramNode,
REGISTERED_NODE_KINDS,
} from '@codama/nodes';
import { GetNodeFromKind, Node, NodeKind } from '@codama/nodes';

import { findLastNodeFromPath, NodePath } from './NodePath';
import { assertIsNodePath, NodePath } from './NodePath';

type MutableNodePath = Node[];

export class NodeStack {
/**
* Contains all the node stacks saved during the traversal.
* Contains all the node paths saved during the traversal.
*
* - The very last stack is the current stack which is being
* - The very last path is the current path which is being
* used during the traversal.
* - The other stacks can be used to save and restore the
* current stack when jumping to different parts of the tree.
* - The other paths can be used to save and restore the
* current path when jumping to different parts of the tree.
*
* There must at least be one stack in the heap at all times.
* There must at least be one path in the stack at all times.
*/
private readonly heap: [...Node[][], Node[]];
private readonly stack: [...MutableNodePath[], MutableNodePath];

constructor(...heap: readonly [...(readonly (readonly Node[])[]), readonly Node[]] | readonly []) {
this.heap = heap.length === 0 ? [[]] : ([...heap.map(nodes => [...nodes])] as [...Node[][], Node[]]);
constructor(...stack: readonly [...(readonly NodePath[]), NodePath] | readonly []) {
this.stack =
stack.length === 0
? [[]]
: ([...stack.map(nodes => [...nodes])] as [...MutableNodePath[], MutableNodePath]);
}

public get stack(): Node[] {
return this.heap[this.heap.length - 1];
private get currentPath(): MutableNodePath {
return this.stack[this.stack.length - 1];
}

public push(node: Node): void {
this.stack.push(node);
this.currentPath.push(node);
}

public pop(): Node | undefined {
return this.stack.pop();
return this.currentPath.pop();
}

public peek(): Node | undefined {
return this.isEmpty() ? undefined : this.stack[this.stack.length - 1];
return this.isEmpty() ? undefined : this.currentPath[this.currentPath.length - 1];
}

public pushStack(newStack: readonly Node[] = []): void {
this.heap.push([...newStack]);
public pushPath(newPath: NodePath = []): void {
this.stack.push([...newPath]);
}

public popStack(): readonly Node[] {
const oldStack = this.heap.pop() as Node[];
if (this.heap.length === 0) {
public popPath(): NodePath {
if (this.stack.length === 0) {
// TODO: Coded error
throw new Error('The heap of stacks can never be empty.');
throw new Error('The stack of paths can never be empty.');
}
return [...oldStack] as readonly Node[];
}

public find<TKind extends NodeKind>(kind: TKind | TKind[]): GetNodeFromKind<TKind> | undefined {
return findLastNodeFromPath([...this.stack] as unknown as NodePath<GetNodeFromKind<TKind>>, kind);
}

public getProgram(): ProgramNode | undefined {
return this.find('programNode');
}

public getInstruction(): InstructionNode | undefined {
return this.find('instructionNode');
return [...this.stack.pop()!];
}

public all(): readonly Node[] {
return [...this.stack];
}

public getPath<TKind extends NodeKind>(kind?: TKind | TKind[]): NodePath<GetNodeFromKind<TKind>> {
const node = this.peek();
assertIsNode(node, kind ?? REGISTERED_NODE_KINDS);
return [...this.stack] as unknown as NodePath<GetNodeFromKind<TKind>>;
public getPath(): NodePath;
public getPath<TKind extends NodeKind>(kind: TKind | TKind[]): NodePath<GetNodeFromKind<TKind>>;
public getPath<TKind extends NodeKind>(kind?: TKind | TKind[]): NodePath {
const path = [...this.currentPath];
if (kind) {
assertIsNodePath(path, kind);
}
return path;
}

public isEmpty(): boolean {
return this.stack.length === 0;
return this.currentPath.length === 0;
}

public clone(): NodeStack {
return new NodeStack(...this.heap);
}

public toString(): string {
return this.toStringArray().join(' > ');
}

public toStringArray(): string[] {
return this.stack.map((node): string => {
return 'name' in node ? `[${node.kind}]${node.name}` : `[${node.kind}]`;
});
return new NodeStack(...this.stack);
}
}
2 changes: 1 addition & 1 deletion packages/visitors-core/src/recordLinkablesVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function getRecordLinkablesVisitor<TNodeKind extends NodeKind>(
v =>
interceptVisitor(v, (node, next) => {
if (isNode(node, LINKABLE_NODES)) {
linkables.recordPath(stack.getPath());
linkables.recordPath(stack.getPath(LINKABLE_NODES));
}
return next(node);
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ test('it can transform nodes using multiple node selectors', () => {
// - the second one selects all nodes with more than one ancestor.
const visitor = bottomUpTransformerVisitor([
{
select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.all().length > 1],
select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.getPath().length > 1],
transform: () => stringTypeNode('utf8'),
},
]);
Expand Down
4 changes: 2 additions & 2 deletions packages/visitors-core/test/recordNodeStackVisitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test('it records the current node stack of a visit', () => {

// Then we expect the number stacks to have been recorded.
expect(numberStacks.length).toBe(1);
expect(numberStacks[0].all()).toEqual([node, node.type]);
expect(numberStacks[0].getPath()).toEqual([node, node.type]);

// And the current node stack to be empty.
expect(stack.isEmpty()).toBe(true);
Expand Down Expand Up @@ -52,5 +52,5 @@ test('it includes the current node when applied last', () => {
// Then we expect the number stacks to have been recorded
// such that the number node themselves are included in the stack.
expect(numberStacks.length).toBe(1);
expect(numberStacks[0].all()).toEqual([node, node.type, (node.type as TupleTypeNode).items[0]]);
expect(numberStacks[0].getPath()).toEqual([node, node.type, (node.type as TupleTypeNode).items[0]]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test('it can transform nodes using multiple node selectors', () => {
// - the second one selects all nodes with more than one ancestor.
const visitor = topDownTransformerVisitor([
{
select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.all().length > 1],
select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.getPath().length > 1],
transform: node => numberTypeNode('u64') as typeof node,
},
]);
Expand Down
Loading