import { arrayEquals, liftArray, throwParseError } from "@ark/util"; import { Disjoint } from "../shared/disjoint.js"; import { implementNode } from "../shared/implement.js"; import { intersectOrPipeNodes } from "../shared/intersections.js"; import { $ark, registeredReference } from "../shared/registry.js"; import { hasArkKind } from "../shared/utils.js"; import { BaseRoot } from "./root.js"; import { defineRightwardIntersections } from "./utils.js"; const implementation = implementNode({ kind: "morph", hasAssociatedError: false, keys: { in: { child: true, parse: (schema, ctx) => ctx.$.parseSchema(schema) }, morphs: { parse: liftArray, serialize: morphs => morphs.map(m => hasArkKind(m, "root") ? m.json : registeredReference(m)) }, declaredIn: { child: false, serialize: node => node.json }, declaredOut: { child: false, serialize: node => node.json } }, normalize: schema => schema, defaults: { description: node => `a morph from ${node.rawIn.description} to ${node.rawOut?.description ?? "unknown"}` }, intersections: { morph: (l, r, ctx) => { if (!l.hasEqualMorphs(r)) { return throwParseError(writeMorphIntersectionMessage(l.expression, r.expression)); } const inTersection = intersectOrPipeNodes(l.rawIn, r.rawIn, ctx); if (inTersection instanceof Disjoint) return inTersection; const baseInner = { morphs: l.morphs }; if (l.declaredIn || r.declaredIn) { const declaredIn = intersectOrPipeNodes(l.rawIn, r.rawIn, ctx); // we can't treat this as a normal Disjoint since it's just declared // it should only happen if someone's essentially trying to create a broken type if (declaredIn instanceof Disjoint) return declaredIn.throw(); else baseInner.declaredIn = declaredIn; } if (l.declaredOut || r.declaredOut) { const declaredOut = intersectOrPipeNodes(l.rawOut, r.rawOut, ctx); if (declaredOut instanceof Disjoint) return declaredOut.throw(); else baseInner.declaredOut = declaredOut; } // in case from is a union, we need to distribute the branches // to can be a union as any schema is allowed return inTersection.distribute(inBranch => ctx.$.node("morph", { ...baseInner, in: inBranch }), ctx.$.parseSchema); }, ...defineRightwardIntersections("morph", (l, r, ctx) => { const inTersection = l.inner.in ? intersectOrPipeNodes(l.inner.in, r, ctx) : r; return (inTersection instanceof Disjoint ? inTersection : inTersection.equals(l.inner.in) ? l : ctx.$.node("morph", { ...l.inner, in: inTersection })); }) } }); export class MorphNode extends BaseRoot { serializedMorphs = this.morphs.map(registeredReference); compiledMorphs = `[${this.serializedMorphs}]`; lastMorph = this.inner.morphs[this.inner.morphs.length - 1]; lastMorphIfNode = hasArkKind(this.lastMorph, "root") ? this.lastMorph : undefined; introspectableIn = this.inner.in; introspectableOut = this.lastMorphIfNode ? Object.assign(this.referencesById, this.lastMorphIfNode.referencesById) && this.lastMorphIfNode.rawOut : undefined; get shallowMorphs() { // if the morph input is a union, it should not contain any other shallow morphs return Array.isArray(this.inner.in?.shallowMorphs) ? [...this.inner.in.shallowMorphs, ...this.morphs] : this.morphs; } get rawIn() { return (this.declaredIn ?? this.inner.in?.rawIn ?? $ark.intrinsic.unknown.internal); } get rawOut() { return (this.declaredOut ?? this.introspectableOut ?? $ark.intrinsic.unknown.internal); } declareIn(declaredIn) { return this.$.node("morph", { ...this.inner, declaredIn }); } declareOut(declaredOut) { return this.$.node("morph", { ...this.inner, declaredOut }); } expression = `(In: ${this.rawIn.expression}) => ${this.lastMorphIfNode ? "To" : "Out"}<${this.rawOut.expression}>`; get defaultShortDescription() { return this.rawIn.meta.description ?? this.rawIn.defaultShortDescription; } innerToJsonSchema(ctx) { return ctx.fallback.morph({ code: "morph", base: this.rawIn.toJsonSchemaRecurse(ctx), out: this.introspectableOut?.toJsonSchemaRecurse(ctx) ?? null }); } compile(js) { if (js.traversalKind === "Allows") { if (!this.introspectableIn) return; js.return(js.invoke(this.introspectableIn)); return; } if (this.introspectableIn) js.line(js.invoke(this.introspectableIn)); js.line(`ctx.queueMorphs(${this.compiledMorphs})`); } traverseAllows = (data, ctx) => !this.introspectableIn || this.introspectableIn.traverseAllows(data, ctx); traverseApply = (data, ctx) => { if (this.introspectableIn) this.introspectableIn.traverseApply(data, ctx); ctx.queueMorphs(this.morphs); }; /** Check if the morphs of r are equal to those of this node */ hasEqualMorphs(r) { return arrayEquals(this.morphs, r.morphs, { isEqual: (lMorph, rMorph) => lMorph === rMorph || (hasArkKind(lMorph, "root") && hasArkKind(rMorph, "root") && lMorph.equals(rMorph)) }); } } export const Morph = { implementation, Node: MorphNode }; export const writeMorphIntersectionMessage = (lDescription, rDescription) => `The intersection of distinct morphs at a single path is indeterminate: Left: ${lDescription} Right: ${rDescription}`;