import type { evaluate } from "../../dev/utils/src/main.js" import { CompiledFunction } from "../../dev/utils/src/main.js" import { arkKind } from "../compile/registry.js" import { CompilationState, InputParameterName } from "../compile/state.js" import type { inferred } from "../parse/definition.js" import type { NodeEntry } from "./composite/props.js" import { Disjoint } from "./disjoint.js" import type { NodeKind, NodeKinds } from "./kinds.js" import type { BasisKind } from "./primitive/basis/basis.js" type BaseNodeImplementation = { kind: node["kind"] /** Should convert any supported input formats to rule, * then ensure rule is normalized such that equivalent * inputs will compile to the same string. */ parse: (rule: node["rule"] | parsableFrom) => node["rule"] compile: (rule: node["rule"], s: CompilationState) => string intersect: ( l: Parameters[0], r: Parameters[0] ) => ReturnType } export type NodeChildren = BaseNode[] | NodeEntry[] type NodeExtension = ( base: basePropsOf ) => extendedPropsOf type basePropsOf = Pick type extendedPropsOf = Omit< node, // we don't actually need the inferred symbol at runtime BuiltinBaseKey | typeof inferred > & ThisType interface PreconstructedBase { [arkKind]: "node" kind: NodeKind rule: rule compile(state: CompilationState): string condition: string intersect(other: intersectsWith | this): intersectsWith | this | Disjoint intersectionCache: Record< string, this | intersectsWith | Disjoint | undefined > // TODO: can this work as is with late resolution? allows(data: unknown): boolean hasKind(kind: kind): this is NodeKinds[kind] isBasis(): this is NodeKinds[BasisKind] } type BuiltinBaseKey = evaluate> export type BaseNodeExtensionProps = { description: string } export type BaseNode< rule = unknown, intersectsWith = never > = PreconstructedBase & BaseNodeExtensionProps type IntersectionCache = Record export type NodeConstructor = ( input: node["rule"] | input ) => node export const alphabetizeByCondition = ( nodes: nodes ) => nodes.sort((l, r) => (l.condition > r.condition ? 1 : -1)) export const defineNodeKind = < node extends BaseNode, parsableFrom = never >( def: BaseNodeImplementation, addProps: NodeExtension ): NodeConstructor => { const nodeCache: { [condition: string]: node | undefined } = {} return (input) => { const rule = def.parse(input) const condition = def.compile(rule, new CompilationState("allows")) if (nodeCache[condition]) { return nodeCache[condition]! } const intersectionCache: IntersectionCache = {} const isBasis = def.kind === "domain" || def.kind === "class" || def.kind === "value" const base: PreconstructedBase & ThisType = { [arkKind]: "node", kind: def.kind, hasKind: (kind) => kind === def.kind, isBasis: () => isBasis, condition, rule, compile: (state: CompilationState) => def.compile(rule, state), allows: new CompiledFunction( InputParameterName, `${condition} return true` ), intersectionCache, intersect(other) { if (this === other) { return this } if (intersectionCache[other.condition]) { return intersectionCache[other.condition]! } const result: BaseNode | Disjoint = def.intersect(this, other) intersectionCache[other.condition] = result other.intersectionCache[condition] = result instanceof Disjoint ? result.invert() : result return result } } const instance = Object.assign(addProps(base as node), base, { toString(this: node) { return this.description } }) as node nodeCache[condition] = instance return instance } }