capnpc-ts
Version:
Cap'n Proto schema compiler for TypeScript.
721 lines (583 loc) • 24 kB
text/typescript
import * as capnp from "capnp-ts";
import * as s from "capnp-ts/src/std/schema.capnp.js";
import { format } from "capnp-ts/src/util";
import initTrace from "debug";
import ts from "typescript";
import {
createClassExtends,
createConcreteListProperty,
createConstProperty,
createMethod,
createNestedNodeProperty,
createUnionConstProperty,
createValueExpression,
} from "./ast-creators";
import { CodeGeneratorFileContext } from "./code-generator-file-context";
import {
__,
BOOLEAN_TYPE,
CAPNP,
ConcreteListType,
EXPORT,
LENGTH,
NUMBER_TYPE,
Primitive,
READONLY,
STATIC,
STRING_TYPE,
STRUCT,
THIS,
TS_FILE_ID,
VALUE,
VOID_TYPE,
OBJECT_SIZE,
} from "./constants";
import * as E from "./errors";
import {
compareCodeOrder,
getConcreteListType,
getDisplayNamePrefix,
getFullClassName,
getJsType,
getUnnamedUnionFields,
hasNode,
lookupNode,
needsConcreteListClass,
} from "./file";
import * as util from "./util";
const trace = initTrace("capnpc:generators");
trace("load");
export function generateCapnpImport(ctx: CodeGeneratorFileContext): void {
// Look for the special importPath annotation on the file to see if we need a different import path for capnp-ts.
const fileNode = lookupNode(ctx, ctx.file);
const tsFileId = capnp.Uint64.fromHexString(TS_FILE_ID);
// This may be undefined if ts.capnp is not imported; fine, we'll just use the default.
const tsAnnotationFile = ctx.nodes.find((n) => n.getId().equals(tsFileId));
// We might not find the importPath annotation; that's definitely a bug but let's move on.
const tsImportPathAnnotation =
tsAnnotationFile && tsAnnotationFile.getNestedNodes().find((n) => n.getName() === "importPath");
// There may not necessarily be an import path annotation on the file node. That's fine.
const importAnnotation =
tsImportPathAnnotation && fileNode.getAnnotations().find((a) => a.getId().equals(tsImportPathAnnotation.getId()));
const importPath = importAnnotation === undefined ? "capnp-ts" : importAnnotation.getValue().getText();
let u: ts.Identifier | undefined;
// import * as capnp from '${importPath}';
ctx.statements.push(
ts.createImportDeclaration(
__,
__,
ts.createImportClause(u as ts.Identifier, ts.createNamespaceImport(CAPNP)),
ts.createLiteral(importPath)
)
);
// import { ObjectSize as __O, Struct as __S } from '${importPath}';
ctx.statements.push(
ts.createStatement(ts.createIdentifier(`import { ObjectSize as __O, Struct as __S } from '${importPath}'`))
);
}
export function generateNestedImports(ctx: CodeGeneratorFileContext): void {
ctx.imports.forEach((i) => {
const name = i.getName();
let importPath: string;
if (name.substr(0, 7) === "/capnp/") {
importPath = `capnp-ts/src/std/${name.substr(7)}.js`;
} else {
importPath = name[0] === "." ? `${name}.js` : `./${name}.js`;
}
const imports = getImportNodes(ctx, lookupNode(ctx, i)).map(getFullClassName).join(", ");
if (imports.length < 1) return;
const importStatement = `import { ${imports} } from "${importPath}"`;
trace("emitting import statement:", importStatement);
ctx.statements.push(ts.createStatement(ts.createIdentifier(importStatement)));
});
}
export function generateConcreteListInitializer(
ctx: CodeGeneratorFileContext,
fullClassName: string,
field: s.Field
): void {
const left = ts.createPropertyAccess(ts.createIdentifier(fullClassName), `_${util.c2t(field.getName())}`);
const right = ts.createIdentifier(getConcreteListType(ctx, field.getSlot().getType()));
ctx.statements.push(ts.createStatement(ts.createAssignment(left, right)));
}
export function generateDefaultValue(field: s.Field): ts.PropertyAssignment {
const name = field.getName();
const slot = field.getSlot();
const whichSlotType = slot.getType().which();
const p = Primitive[whichSlotType];
let initializer;
switch (whichSlotType) {
case s.Type_Which.ANY_POINTER:
case s.Type_Which.DATA:
case s.Type_Which.LIST:
case s.Type_Which.STRUCT:
initializer = createValueExpression(slot.getDefaultValue());
break;
case s.Type_Which.TEXT:
initializer = ts.createLiteral(slot.getDefaultValue().getText());
break;
case s.Type_Which.BOOL:
initializer = ts.createCall(ts.createPropertyAccess(CAPNP, p.mask), __, [
createValueExpression(slot.getDefaultValue()),
ts.createNumericLiteral((slot.getOffset() % 8).toString()),
]);
break;
case s.Type_Which.ENUM:
case s.Type_Which.FLOAT32:
case s.Type_Which.FLOAT64:
case s.Type_Which.INT16:
case s.Type_Which.INT32:
case s.Type_Which.INT64:
case s.Type_Which.INT8:
case s.Type_Which.UINT16:
case s.Type_Which.UINT32:
case s.Type_Which.UINT64:
case s.Type_Which.UINT8:
initializer = ts.createCall(ts.createPropertyAccess(CAPNP, p.mask), __, [
createValueExpression(slot.getDefaultValue()),
]);
break;
default:
throw new Error(format(E.GEN_UNKNOWN_DEFAULT, s.Type_Which[whichSlotType]));
}
return ts.createPropertyAssignment(`default${util.c2t(name)}`, initializer);
}
export function generateEnumNode(ctx: CodeGeneratorFileContext, node: s.Node): void {
trace("generateEnumNode(%s) [%s]", node, node.getDisplayName());
const members = node
.getEnum()
.getEnumerants()
.toArray()
.sort(compareCodeOrder)
.map((e) => ts.createEnumMember(util.c2s(e.getName())));
const d = ts.createEnumDeclaration(__, [EXPORT], getFullClassName(node), members);
ctx.statements.push(d);
}
export function generateFileId(ctx: CodeGeneratorFileContext): void {
trace("generateFileId()");
// export const _capnpFileId = 'abcdef';
const fileId = ts.createLiteral(ctx.file.getId().toHexString());
ctx.statements.push(
ts.createVariableStatement(
[EXPORT],
ts.createVariableDeclarationList([ts.createVariableDeclaration("_capnpFileId", __, fileId)], ts.NodeFlags.Const)
)
);
}
export function generateInterfaceClasses(_ctx: CodeGeneratorFileContext, node: s.Node): void {
trace("Interface generation is not yet implemented.");
/* tslint:disable-next-line */
console.error(`CAPNP-TS: Warning! Interface generation (${node.getDisplayName()}) is not yet implemented.`);
}
export function generateNode(ctx: CodeGeneratorFileContext, node: s.Node): void {
trace("generateNode(%s, %s)", ctx, node.getId().toHexString());
const nodeId = node.getId();
const nodeIdHex = nodeId.toHexString();
if (ctx.generatedNodeIds.indexOf(nodeIdHex) > -1) return;
ctx.generatedNodeIds.push(nodeIdHex);
/** An array of group structs formed as children of this struct. They appear before the struct node in the file. */
const groupNodes = ctx.nodes.filter(
(n) => n.getScopeId().equals(nodeId) && n.isStruct() && n.getStruct().getIsGroup()
);
/**
* An array of nodes that are nested within this node; these must appear first since those symbols will be
* refernced in the node's class definition.
*/
const nestedNodes = node.getNestedNodes().map((n) => lookupNode(ctx, n));
nestedNodes.forEach((n) => generateNode(ctx, n));
groupNodes.forEach((n) => generateNode(ctx, n));
const whichNode = node.which();
switch (whichNode) {
case s.Node.STRUCT:
generateStructNode(ctx, node, false);
break;
case s.Node.CONST:
// Const nodes are generated along with the containing class, ignore these.
break;
case s.Node.ENUM:
generateEnumNode(ctx, node);
break;
case s.Node.INTERFACE:
generateStructNode(ctx, node, true);
break;
case s.Node.ANNOTATION:
trace("ignoring unsupported annotation node: %s", node.getDisplayName());
break;
case s.Node.FILE:
default:
throw new Error(format(E.GEN_NODE_UNKNOWN_TYPE, s.Node_Which[whichNode]));
}
}
const listLengthParameterName = "length";
export function generateStructFieldMethods(
ctx: CodeGeneratorFileContext,
members: ts.ClassElement[],
node: s.Node,
field: s.Field
): void {
let jsType: string;
let whichType: s.Type_Which | string;
if (field.isSlot()) {
const slotType = field.getSlot().getType();
jsType = getJsType(ctx, slotType, false);
whichType = slotType.which();
} else if (field.isGroup()) {
jsType = getFullClassName(lookupNode(ctx, field.getGroup().getTypeId()));
whichType = "group";
} else {
throw new Error(format(E.GEN_UNKNOWN_STRUCT_FIELD, field.which()));
}
const jsTypeReference = ts.createTypeReferenceNode(jsType, __);
const discriminantOffset = node.getStruct().getDiscriminantOffset();
const name = field.getName();
const properName = util.c2t(name);
const hadExplicitDefault = field.isSlot() && field.getSlot().getHadExplicitDefault();
const discriminantValue = field.getDiscriminantValue();
const fullClassName = getFullClassName(node);
const union = discriminantValue !== s.Field.NO_DISCRIMINANT;
const offset = (field.isSlot() && field.getSlot().getOffset()) || 0;
const offsetLiteral = ts.createNumericLiteral(offset.toString());
/** __S.getPointer(0, this) */
const getPointer = ts.createCall(ts.createPropertyAccess(STRUCT, "getPointer"), __, [offsetLiteral, THIS]);
/** __S.copyFrom(value, __S.getPointer(0, this)) */
const copyFromValue = ts.createCall(ts.createPropertyAccess(STRUCT, "copyFrom"), __, [VALUE, getPointer]);
/** capnp.Orphan<Foo> */
const orphanType = ts.createTypeReferenceNode("capnp.Orphan", [jsTypeReference]);
const discriminantOffsetLiteral = ts.createNumericLiteral((discriminantOffset * 2).toString());
const discriminantValueLiteral = ts.createNumericLiteral(discriminantValue.toString());
/** __S.getUint16(0, this) */
const getDiscriminant = ts.createCall(ts.createPropertyAccess(STRUCT, "getUint16"), __, [
discriminantOffsetLiteral,
THIS,
]);
/** __S.setUint16(0, this) */
const setDiscriminant = ts.createCall(ts.createPropertyAccess(STRUCT, "setUint16"), __, [
discriminantOffsetLiteral,
discriminantValueLiteral,
THIS,
]);
const defaultValue = hadExplicitDefault
? ts.createIdentifier(`${fullClassName}._capnp.default${properName}`)
: undefined;
let adopt = false;
let disown = false;
let init;
let has = false;
let get;
let set;
let getArgs: ts.Expression[];
switch (whichType) {
case s.Type.ANY_POINTER:
getArgs = [offsetLiteral, THIS];
if (defaultValue) getArgs.push(defaultValue);
adopt = true;
disown = true;
/** __S.getPointer(0, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getPointer"), __, getArgs);
has = true;
/** __S.copyFrom(value, __S.getPointer(0, this)) */
set = ts.createCall(ts.createPropertyAccess(STRUCT, "copyFrom"), __, [VALUE, get]);
break;
case s.Type.BOOL:
case s.Type.ENUM:
case s.Type.FLOAT32:
case s.Type.FLOAT64:
case s.Type.INT16:
case s.Type.INT32:
case s.Type.INT64:
case s.Type.INT8:
case s.Type.UINT16:
case s.Type.UINT32:
case s.Type.UINT64:
case s.Type.UINT8: {
const { byteLength, getter, setter } = Primitive[whichType as number];
// NOTE: For a BOOL type this is actually a bit offset; `byteLength` will be `1` in that case.
const byteOffset = ts.createNumericLiteral((offset * byteLength).toString());
getArgs = [byteOffset, THIS];
if (defaultValue) getArgs.push(defaultValue);
/** __S.getXYZ(0, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, getter), __, getArgs);
/** __S.setXYZ(0, value, this) */
set = ts.createCall(ts.createPropertyAccess(STRUCT, setter), __, [byteOffset, VALUE, THIS]);
break;
}
case s.Type.DATA:
getArgs = [offsetLiteral, THIS];
if (defaultValue) getArgs.push(defaultValue);
adopt = true;
disown = true;
/** __S.getData(0, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getData"), __, getArgs);
has = true;
/** __S.initData(0, length, this) */
init = ts.createCall(ts.createPropertyAccess(STRUCT, "initData"), __, [offsetLiteral, LENGTH, THIS]);
set = copyFromValue;
break;
case s.Type.INTERFACE:
if (hadExplicitDefault) {
throw new Error(format(E.GEN_EXPLICIT_DEFAULT_NON_PRIMITIVE, "INTERFACE"));
}
/** __S.getPointerAs(0, Foo, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getPointerAs"), __, [
offsetLiteral,
ts.createIdentifier(jsType),
THIS,
]);
set = copyFromValue;
break;
case s.Type.LIST: {
const whichElementType = field.getSlot().getType().getList().getElementType().which();
let listClass = ConcreteListType[whichElementType];
if (whichElementType === s.Type.LIST || whichElementType === s.Type.STRUCT) {
listClass = `${fullClassName}._${properName}`;
} else if (listClass === void 0) {
/* istanbul ignore next */
throw new Error(format(E.GEN_UNSUPPORTED_LIST_ELEMENT_TYPE, whichElementType));
}
const listClassIdentifier = ts.createIdentifier(listClass);
getArgs = [offsetLiteral, listClassIdentifier, THIS];
if (defaultValue) getArgs.push(defaultValue);
adopt = true;
disown = true;
/** __S.getList(0, MyStruct._Foo, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getList"), __, getArgs);
has = true;
/** __S.initList(0, MyStruct._Foo, length, this) */
init = ts.createCall(ts.createPropertyAccess(STRUCT, "initList"), __, [
offsetLiteral,
listClassIdentifier,
ts.createIdentifier(listLengthParameterName),
THIS,
]);
set = copyFromValue;
break;
}
case s.Type.STRUCT: {
const structType = ts.createIdentifier(getJsType(ctx, field.getSlot().getType(), false));
getArgs = [offsetLiteral, structType, THIS];
if (defaultValue) getArgs.push(defaultValue);
adopt = true;
disown = true;
/** __S.getStruct(0, Foo, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getStruct"), __, getArgs);
has = true;
/** __S.initStruct(0, Foo, this) */
init = ts.createCall(ts.createPropertyAccess(STRUCT, "initStructAt"), __, [offsetLiteral, structType, THIS]);
set = copyFromValue;
break;
}
case s.Type.TEXT:
getArgs = [offsetLiteral, THIS];
if (defaultValue) getArgs.push(defaultValue);
/** __S.getText(0, this) */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getText"), __, getArgs);
/** __S.setText(0, value, this) */
set = ts.createCall(ts.createPropertyAccess(STRUCT, "setText"), __, [offsetLiteral, VALUE, THIS]);
break;
case s.Type.VOID:
break;
case "group": {
if (hadExplicitDefault) {
throw new Error(format(E.GEN_EXPLICIT_DEFAULT_NON_PRIMITIVE, "group"));
}
const groupType = ts.createIdentifier(jsType);
/** __S.getAs(Foo, this); */
get = ts.createCall(ts.createPropertyAccess(STRUCT, "getAs"), __, [groupType, THIS]);
init = get;
break;
}
default:
// TODO Maybe this should be an error?
break;
}
// adoptFoo(value: capnp.Orphan<Foo>): void { __S.adopt(value, this._getPointer(3)); }}
if (adopt) {
const parameters = [ts.createParameter(__, __, __, VALUE, __, orphanType, __)];
const expressions = [ts.createCall(ts.createPropertyAccess(STRUCT, "adopt"), __, [VALUE, getPointer])];
if (union) expressions.unshift(setDiscriminant);
members.push(createMethod(`adopt${properName}`, parameters, VOID_TYPE, expressions));
}
// disownFoo(): capnp.Orphan<Foo> { return __S.disown(this.getFoo()); }
if (disown) {
const getter = ts.createCall(ts.createPropertyAccess(THIS, `get${properName}`), __, []);
const expressions = [ts.createCall(ts.createPropertyAccess(STRUCT, "disown"), __, [getter])];
members.push(createMethod(`disown${properName}`, [], orphanType, expressions));
}
// getFoo(): FooType { ... }
if (get) {
const expressions = [get];
if (union) {
expressions.unshift(
ts.createCall(ts.createPropertyAccess(STRUCT, "testWhich"), __, [
ts.createLiteral(name),
getDiscriminant,
discriminantValueLiteral,
THIS,
])
);
}
members.push(createMethod(`get${properName}`, [], jsTypeReference, expressions));
}
// hasFoo(): boolean { ... }
if (has) {
// !__S.isNull(this._getPointer(8));
const expressions = [
ts.createLogicalNot(ts.createCall(ts.createPropertyAccess(STRUCT, "isNull"), __, [getPointer])),
];
members.push(createMethod(`has${properName}`, [], BOOLEAN_TYPE, expressions));
}
// initFoo(): FooType { ... } / initFoo(length: number): capnp.List<FooElementType> { ... }
if (init) {
const parameters =
whichType === s.Type.DATA || whichType === s.Type.LIST
? [ts.createParameter(__, __, __, listLengthParameterName, __, NUMBER_TYPE, __)]
: [];
const expressions = [init];
if (union) expressions.unshift(setDiscriminant);
members.push(createMethod(`init${properName}`, parameters, jsTypeReference, expressions));
}
// isFoo(): boolean { ... }
if (union) {
const left = ts.createCall(ts.createPropertyAccess(STRUCT, "getUint16"), __, [discriminantOffsetLiteral, THIS]);
const right = discriminantValueLiteral;
const expressions = [ts.createBinary(left, ts.SyntaxKind.EqualsEqualsEqualsToken, right)];
members.push(createMethod(`is${properName}`, [], BOOLEAN_TYPE, expressions));
}
// setFoo(value: FooType): void { ... }
if (set || union) {
const expressions = [];
const parameters = [];
if (set) {
expressions.unshift(set);
parameters.unshift(ts.createParameter(__, __, __, VALUE, __, jsTypeReference, __));
}
if (union) {
expressions.unshift(setDiscriminant);
}
members.push(createMethod(`set${properName}`, parameters, VOID_TYPE, expressions));
}
}
export function generateStructNode(ctx: CodeGeneratorFileContext, node: s.Node, interfaceNode: boolean): void {
trace("generateStructNode(%s) [%s]", node, node.getDisplayName());
const displayNamePrefix = getDisplayNamePrefix(node);
const fullClassName = getFullClassName(node);
const nestedNodes = node
.getNestedNodes()
.map((n) => lookupNode(ctx, n))
.filter((n) => !n.isConst() && !n.isAnnotation());
const nodeId = node.getId();
const nodeIdHex = nodeId.toHexString();
const struct = node.which() === s.Node.STRUCT ? node.getStruct() : undefined;
const unionFields = getUnnamedUnionFields(node).sort(compareCodeOrder);
const dataWordCount = struct ? struct.getDataWordCount() : 0;
const dataByteLength = struct ? dataWordCount * 8 : 0;
const discriminantCount = struct ? struct.getDiscriminantCount() : 0;
const discriminantOffset = struct ? struct.getDiscriminantOffset() : 0;
const fields = struct ? struct.getFields().toArray().sort(compareCodeOrder) : [];
const pointerCount = struct ? struct.getPointerCount() : 0;
const concreteLists = fields.filter(needsConcreteListClass).sort(compareCodeOrder);
const consts = ctx.nodes.filter((n) => n.getScopeId().equals(nodeId) && n.isConst());
// const groups = ctx.nodes.filter(
// (n) => n.getScopeId().equals(nodeId) && n.isStruct() && n.getStruct().getIsGroup());
const hasUnnamedUnion = discriminantCount !== 0;
if (hasUnnamedUnion) {
generateUnnamedUnionEnum(ctx, fullClassName, unionFields);
}
const members: ts.ClassElement[] = [];
// static readonly CONSTANT = 'foo';
members.push(...consts.map(createConstProperty));
// static readonly WHICH = MyStruct_Which.WHICH;
members.push(...unionFields.map((f) => createUnionConstProperty(fullClassName, f)));
// static readonly NestedStruct = MyStruct_NestedStruct;
members.push(...nestedNodes.map(createNestedNodeProperty));
// static readonly Client = MyInterface_Client;
// static readonly Server = MyInterface_Server;
// if (interfaceNode) {
// members.push(
// ts.createProperty(__, [STATIC, READONLY], 'Client', __, __, ts.createLiteral(`${fullClassName}_Client`)));
// members.push(
// ts.createProperty(__, [STATIC, READONLY], 'Server', __, __, ts.createLiteral(`${fullClassName}_Server`)));
// }
const defaultValues = fields.reduce(
(acc, f) =>
f.isSlot() && f.getSlot().getHadExplicitDefault() && f.getSlot().getType().which() !== s.Type.VOID
? acc.concat(generateDefaultValue(f))
: acc,
[] as ts.PropertyAssignment[]
);
// static reaodnly _capnp = { displayName: 'MyStruct', id: '4732bab4310f81', size = new __O(8, 8) };
members.push(
ts.createProperty(
__,
[STATIC, READONLY],
"_capnp",
__,
__,
ts.createObjectLiteral(
[
ts.createPropertyAssignment("displayName", ts.createLiteral(displayNamePrefix)),
ts.createPropertyAssignment("id", ts.createLiteral(nodeIdHex)),
ts.createPropertyAssignment(
"size",
ts.createNew(OBJECT_SIZE, __, [
ts.createNumericLiteral(dataByteLength.toString()),
ts.createNumericLiteral(pointerCount.toString()),
])
),
].concat(defaultValues)
)
)
);
// private static _ConcreteListClass: MyStruct_ConcreteListClass;
members.push(...concreteLists.map((f) => createConcreteListProperty(ctx, f)));
// getFoo() { ... } initFoo() { ... } setFoo() { ... }
fields.forEach((f) => generateStructFieldMethods(ctx, members, node, f));
// toString(): string { return 'MyStruct_' + super.toString(); }
const toStringExpression = ts.createBinary(
ts.createLiteral(`${fullClassName}_`),
ts.SyntaxKind.PlusToken,
ts.createCall(ts.createIdentifier("super.toString"), __, [])
);
members.push(createMethod("toString", [], STRING_TYPE, [toStringExpression], true));
if (hasUnnamedUnion) {
// which(): MyStruct_Which { return __S.getUint16(12, this); }
const whichExpression = ts.createCall(ts.createPropertyAccess(STRUCT, "getUint16"), __, [
ts.createNumericLiteral((discriminantOffset * 2).toString()),
THIS,
]);
members.push(
createMethod("which", [], ts.createTypeReferenceNode(`${fullClassName}_Which`, __), [whichExpression], true)
);
}
const c = ts.createClassDeclaration(__, [EXPORT], fullClassName, __, [createClassExtends("__S")], members);
// Make sure the interface classes are generated first.
if (interfaceNode) {
generateInterfaceClasses(ctx, node);
}
ctx.statements.push(c);
// Write out the concrete list type initializer after all the class definitions. It can't be initialized within the
// class's static initializer because the nested type might not be defined yet.
// FIXME: This might be solvable with topological sorting?
ctx.concreteLists.push(...concreteLists.map<[string, s.Field]>((f) => [fullClassName, f]));
}
export function generateUnnamedUnionEnum(
ctx: CodeGeneratorFileContext,
fullClassName: string,
unionFields: s.Field[]
): void {
const members = unionFields
.sort(compareCodeOrder)
.map((f) =>
ts.createEnumMember(util.c2s(f.getName()), ts.createNumericLiteral(f.getDiscriminantValue().toString()))
);
const d = ts.createEnumDeclaration(__, [EXPORT], `${fullClassName}_Which`, members);
ctx.statements.push(d);
}
export function getImportNodes(ctx: CodeGeneratorFileContext, node: s.Node): s.Node[] {
return lookupNode(ctx, node)
.getNestedNodes()
.filter((n) => hasNode(ctx, n))
.map((n) => lookupNode(ctx, n))
.reduce((a, n) => a.concat([n], getImportNodes(ctx, n)), new Array<s.Node>())
.filter((n) => lookupNode(ctx, n).isStruct() || lookupNode(ctx, n).isEnum());
}