Skip to content

Commit 62fcd21

Browse files
authored
Update documentation (#287)
This PR updates the `visitors-core` documentation to reflect the changes of this PR stack.
1 parent 274af2e commit 62fcd21

File tree

1 file changed

+132
-29
lines changed

1 file changed

+132
-29
lines changed

packages/visitors-core/README.md

Lines changed: 132 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -96,28 +96,27 @@ Therefore, let's start by exploring the core visitors provided by this package.
9696

9797
### Filtering node kinds
9898

99-
Before we list each available core visitor, it is important to know that each of these functions optionally accepts a node kind or an array of node kinds **as their last argument**. This allows us to restrict the visitor to a specific set of nodes and will return a `Visitor<T, U>` instance where `U` is the union of the provided node kinds.
99+
Before we list each available core visitor, it is important to know that each of these functions optionally accepts a node kind or an array of node kinds **as a `keys` attribute in their `options`**. This allows us to restrict the visitor to a specific set of nodes and will return a `Visitor<T, U>` instance where `U` is the union of the provided node kinds.
100100

101101
Here are some examples:
102102

103103
```ts
104104
// This visitor only accepts `ProgramNodes`.
105-
const visitor: Visitor<number, 'programNode'> = myNumberVisitor('programNode');
105+
const visitor: Visitor<number, 'programNode'> = voidVisitor({ keys: 'programNode' });
106106

107107
// This visitor accepts both `NumberTypeNodes` and `StringTypeNodes`.
108-
const visitor: Visitor<number, 'numberTypeNode' | 'stringTypeNode'> = myNumberVisitor([
109-
'numberTypeNode',
110-
'stringTypeNode',
111-
]);
108+
const visitor: Visitor<number, 'numberTypeNode' | 'stringTypeNode'> = voidVisitor({
109+
keys: ['numberTypeNode', 'stringTypeNode'],
110+
});
112111

113112
// This visitor accepts all type nodes.
114-
const visitor: Visitor<number, TypeNode['kind']> = myNumberVisitor(TYPE_NODES);
113+
const visitor: Visitor<number, TypeNode['kind']> = voidVisitor({ keys: TYPE_NODES });
115114

116115
// This visitor accepts all nodes.
117-
const visitor: Visitor<number> = myNumberVisitor();
116+
const visitor: Visitor<number> = voidVisitor();
118117
```
119118

120-
In the following sections describing the core visitors, this exact pattern can be used to restrict the visitors to specific node kinds. We won't cover this for each visitor but know that you can achieve this via the last argument of each function.
119+
In the following sections describing the core visitors, this exact pattern can be used to restrict the visitors to specific node kinds. We won't cover this for each visitor but know that you can achieve this via the `keys` option of each function.
121120

122121
### `voidVisitor`
123122

@@ -408,11 +407,55 @@ const visitor = rootNodeVisitor((root: RootNode) => {
408407
});
409408
```
410409

411-
## Recording node stacks
410+
## Recording node paths
411+
412+
### `NodePath`
413+
414+
The `NodePath` type defines an immutable array of `Nodes` that represents any path of nodes in the Codama IDL. It accepts an optional type parameter that tells us the type of the last node in the path. For instance `NodePath<NumberTypeNode>` represents a path of node ending with a `NumberTypeNode`.
415+
416+
Additionally, there are several utility functions to use with `NodePath` instances:
417+
418+
```ts
419+
// An example of a node path.
420+
const path: NodePath<AccountNode> = [rootNode, programNode, accountNode];
421+
422+
// Access the last node in the path. I.e. given NodePath<T>, returns T.
423+
const lastNode: AccountNode = getLastNodeFromPath(path);
424+
425+
// Access the first/last node of a specific kind in the path.
426+
const firstProgramNode: ProgramNode | undefined = findFirstNodeFromPath(path, 'programNode');
427+
const lastProgramNode: ProgramNode | undefined = findLastNodeFromPath(path, 'programNode');
428+
429+
// Access the last program/instruction node in the path (sugar for `findLastNodeFromPath`).
430+
const programNode: ProgramNode | undefined = findProgramNodeFromPath(path);
431+
const instructionNode: InstructionNode | undefined = findInstructionNodeFromPath(path);
432+
433+
// Get the subpath of a path from the beginning to the last node matching a specific kind.
434+
const subpath: NodePath = getNodePathUntilLastNode(path, 'programNode');
435+
// ^ [rootNode, programNode]
436+
437+
// Check that a path is not empty.
438+
if (isFilledNodePath(path as NodePath)) {
439+
path satisfies NodePath<Node>;
440+
}
441+
442+
// Check that a path finishes with a node matching the provided kind or kinds.
443+
if (isNodePath(path as NodePath, ['AccountNode', 'InstructionNode'])) {
444+
path satisfies NodePath<AccountNode | InstructionNode>;
445+
}
446+
447+
// Assert that a path finishes with a node matching the provided kind or kinds.
448+
assertIsNodePath(path as NodePath, ['AccountNode', 'InstructionNode']);
449+
path satisfies NodePath<AccountNode | InstructionNode>;
450+
451+
// Display paths as strings or arrays of strings.
452+
nodePathToStringArray(path); // -> ['[rootNode]', '[programNode]token', '[accountNode]mint']
453+
nodePathToString(path); // -> "[rootNode] > [programNode]token > [accountNode]mint"
454+
```
412455

413456
### `NodeStack`
414457

415-
The `NodeStack` class is a utility that allows us to record the stack of nodes that led to a specific node.
458+
The `NodeStack` class is a utility that allows us to record the path of nodes that led to the node being currently visited. It is essentially a mutable version of `NodePath` that pushes and pops `Nodes` as we go down and up the tree of nodes.
416459

417460
For instance, consider the following node:
418461

@@ -432,49 +475,85 @@ const stack = new NodeStack()
432475
.push(node.type.items[0]); // -> numberTypeNode.
433476
```
434477

435-
Once you have access to a `NodeStack` instance — provided by various utility visitors — you may use the following methods:
478+
Once you have access to a `NodeStack` instance, you may use the following methods:
436479

437480
```ts
438481
// Push a node to the stack.
439482
nodeStack.push(node);
483+
440484
// Pop the last node out of the stack.
441485
const lastNode = nodeStack.pop();
486+
442487
// Peek at the last node in the stack.
443488
const lastNode = nodeStack.peek();
444-
// Get all the nodes in the stack as an array.
445-
const path = nodeStack.getPath();
489+
490+
// Get all the nodes in the stack as an immutable `NodePath` array.
491+
const path: NodePath = nodeStack.getPath();
492+
493+
// Get a `NodePath` whilst asserting on the last node kind.
494+
const path: NodePath<AccountNode> = nodeStack.getPath('accountNode');
495+
446496
// Check if the stack is empty.
447497
const isEmpty = nodeStack.isEmpty();
498+
448499
// Clone the stack.
449500
const clonedStack = nodeStack.clone();
450501
```
451502

503+
Additionally, it is possible to save and restore multiple node paths within a `NodeStack` by using the `pushPath` and `popPath` methods. This is for more advanced uses cases where you need to jump from one part of the tree, to a different part of the tree, and back — without loosing the context of the original path. An application of this is when we need to follow a node from a `LinkNode` (see ["Resolving link nodes"](#resolving-link-nodes) below for more details).
504+
505+
```ts
506+
// Save the current path and push a new path.
507+
stack.pushPath([rootNode, programNode, linkableNode]);
508+
509+
// Pop the current path and restore the previous path.
510+
const previousPath = stack.popPath();
511+
```
512+
452513
### `recordNodeStackVisitor`
453514

454515
The `recordNodeStackVisitor` function gives us a convenient way to record the stack of each node currently being visited. It accepts a base visitor and an empty `NodeStack` instance that will automatically be pushed and popped as the visitor traverses the nodes. This means that we can inject the `NodeStack` instance into another extension of the visitor to access the stack whilst visiting the nodes.
455516

517+
Note that the `recordNodeStackVisitor` **should be the last visitor** in the pipe to ensure that the stack is correctly recorded and that the current node visited is part of the stack.
518+
456519
For instance, here's how we can log the `NodeStack` of any base visitor as we visit the nodes.
457520

458521
```ts
459522
const stack = new NodeStack();
460523
const visitor = pipe(
461524
baseVisitor,
462-
v => recordNodeStackVisitor(v, stack),
463525
v =>
464526
interceptVisitor(v, (node, next) => {
465527
console.log(nodePathToString(stack.getPath()));
466528
return next(node);
467529
}),
530+
v => recordNodeStackVisitor(v, stack),
531+
);
532+
```
533+
534+
Also note that some core visitors such as the `bottomUpTransformerVisitor` or the `getByteSizeVisitor` use a `NodeStack` internally to keep track of the current path. If you use these visitor within another visitor, you may wish to provide your own `NodeStack` instance as an option so that the same `NodeStack` is used across all visitors throughout the traversal.
535+
536+
```ts
537+
const stack = new NodeStack();
538+
const byteSizeVisitor = getByteSizeVisitor(..., { stack });
539+
540+
const visitor = pipe(
541+
voidVisitor(),
542+
v => tapVisitor(v, 'definedTypeNode', node => {
543+
const byteSize = visit(node, byteSizeVisitor);
544+
console.log(`The byte size of ${node.name} is ${byteSize}`);
545+
}),
546+
v => recordNodeStackVisitor(v, stack),
468547
);
469548
```
470549

471550
## Selecting nodes
472551

473552
When visiting a tree of nodes, it is often useful to be explicit about the paths we want to select. For instance, I may want to delete all accounts from a program node named "token".
474553

475-
To take end, the `NodeSelector` type represents a node selection that can take two forms:
554+
To that end, the `NodeSelector` type represents a node selection that can take two forms:
476555

477-
- A `NodeSelectorFunction` of type `(node: Node, stack: NodeStack) => boolean`. In this case, the provided function is used to determine if the node should be selected.
556+
- A `NodeSelectorFunction` of type `(path: NodePath) => boolean`. In this case, the provided function is used to determine if the last node in the provided `NodePath` should be selected.
478557
- A `NodeSelectorPath` of type `string`. In this case, the provided string uses a simple syntax to select nodes.
479558

480559
The `NodeSelectorPath` syntax is as follows:
@@ -560,6 +639,8 @@ const visitor = bottomUpTransformerVisitor([
560639
]);
561640
```
562641

642+
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.
643+
563644
### `topDownTransformerVisitor`
564645

565646
The `topDownTransformerVisitor` works the same way as the `bottomUpTransformerVisitor` but intercepts the nodes on the way down. This means that when we reach a node, we have not yet visited its children.
@@ -580,6 +661,8 @@ const visitor = topDownTransformerVisitor([
580661
]);
581662
```
582663

664+
Here as well, you may use the `stack` option to provide your own `NodeStack` instance.
665+
583666
### `deleteNodesVisitor`
584667

585668
The `deleteNodesVisitor` accepts an array of `NodeSelectors` and deletes all the nodes that match any of the provided selectors. Therefore, it is equivalent to using a transformer visitor such that the `transform` function returns `null` for the selected nodes.
@@ -589,6 +672,8 @@ The `deleteNodesVisitor` accepts an array of `NodeSelectors` and deletes all the
589672
const visitor = deleteNodesVisitor(['[accountNode]mint', '[numberTypeNode]']);
590673
```
591674

675+
Here as well, you may use the `stack` option to provide your own `NodeStack` instance.
676+
592677
## String representations
593678

594679
This package also offers visitors that help render nodes as strings. These visitors can be useful for debugging purposes as well as getting a unique hash string representation of a node.
@@ -648,25 +733,43 @@ const linkables = new LinkableDictionary();
648733
// Record linkable nodes via their full path.
649734
linkables.recordPath([rootNode, programNode, accountNode]);
650735

651-
// Get a linkable node using a link node, or throw an error if it is not found.
652-
const programNode = linkables.getOrThrow(programLinkNode, stack);
736+
// Get a linkable node using the full path of a link node, or return undefined if it is not found.
737+
const programNode: ProgramNode | undefined = linkables.get([...somePath, programLinkNode]);
738+
739+
// Get a linkable node using the full path of a link node, or throw an error if it is not found.
740+
const programNode: ProgramNode = linkables.getOrThrow([...somePath, programLinkNode]);
741+
742+
// Get the path of a linkable node using the full path of a link node, or return undefined if it is not found.
743+
const accountPath: NodePath<AccountNode> | undefined = linkables.getPath([...somePath, accountLinkNode]);
653744

654-
// Get a linkable node using a link node, or return undefined if it is not found.
655-
const accountNode = linkables.get(accountLinkNode, stack);
745+
// Get the path of a linkable node using the full path of a link node, or throw an error if it is not found.
746+
const accountPath: NodePath<AccountNode> = linkables.getPathOrThrow([...somePath, accountLinkNode]);
656747
```
657748

658749
Note that:
659750

660-
- The stack of the recorded node must be provided when recording a linkable node.
661-
- The stack of the link node must be provided when getting a linkable node from it.
751+
- The path of the recorded node must be provided when recording a linkable node.
752+
- The path of the link node must be provided when getting a linkable node (or its path) from it.
662753

663-
This API may be used with the `recordLinkablesOnFirstVisitVisitor` to record the linkable nodes before the first node visit; as well as the `recordNodeStackVisitor` to keep track of the current node stack when accessing the linkable nodes.
754+
This API may be used with the `recordLinkablesOnFirstVisitVisitor` to record the linkable nodes before the first node visit; as well as the `recordNodeStackVisitor` to keep track of the current node path when accessing the linkable nodes.
755+
756+
### `getRecordLinkablesVisitor`
757+
758+
This visitor accepts a `LinkableDictionary` instance and records all linkable nodes it encounters when visiting the nodes.
759+
760+
```ts
761+
const linkables = new LinkableDictionary();
762+
visit(rootNode, getRecordLinkablesVisitor(linkables));
763+
// Now, all linkable nodes are recorded in the `linkables` dictionary.
764+
```
664765

665766
### `recordLinkablesOnFirstVisitVisitor`
666767

667-
Much like the `recordNodeStackVisitor`, the `recordLinkablesOnFirstVisitVisitor` allows us to record linkable nodes as we traverse the tree of nodes. It accepts a base visitor and `LinkableDictionary` instance; and records any linkable node it encounters.
768+
This visitor is a utility that combines `interceptFirstVisitVisitor` and `getRecordLinkablesVisitor` to record all linkable nodes before the first visit of any node.
668769

669-
This means that we can inject the `LinkableDictionary` instance into another extension of the base visitor to resolve any link node we encounter.
770+
It accepts a base visitor and a `LinkableDictionary` instance; and returns a new visitor that records all linkable nodes it encounters before the very first visit of the provided base visitor. This means that we can inject the `LinkableDictionary` instance into other extensions of the base visitor to resolve any link node we encounter.
771+
772+
Note that the `recordLinkablesOnFirstVisitVisitor` **should be the last visitor** in the pipe to ensure that all linkable nodes are recorded before being used.
670773

671774
Here's an example that records a `LinkableDictionary` and uses it to log the amount of seeds in each linked PDA node.
672775

@@ -677,16 +780,14 @@ const visitor = pipe(
677780
baseVisitor,
678781
v =>
679782
tapVisitor(v, 'pdaLinkNode', node => {
680-
const pdaNode = linkables.getOrThrow(node);
783+
const pdaNode = linkables.getOrThrow(stack.getPath(node.kind));
681784
console.log(`${pdaNode.seeds.length} seeds`);
682785
}),
683786
v => recordNodeStackVisitor(v, stack),
684787
v => recordLinkablesOnFirstVisitVisitor(v, linkables),
685788
);
686789
```
687790

688-
Note that the `recordLinkablesOnFirstVisitVisitor` should be the last visitor in the pipe to ensure that all linkable nodes are recorded before being used.
689-
690791
## Other useful visitors
691792

692793
This package provides a few other visitors that may help build more complex visitors.
@@ -701,6 +802,8 @@ const size = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]),
701802
// ^ 36 (4 bytes for the u32 number and 32 bytes for the public key)
702803
```
703804

805+
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.
806+
704807
### `getResolvedInstructionInputsVisitor`
705808

706809
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.

0 commit comments

Comments
 (0)