import { append, appendUnique, capitalize, isArray, throwInternalError, throwParseError } from "@ark/util"; import { BaseNode } from "./node.js"; import { Disjoint } from "./shared/disjoint.js"; import { compileObjectLiteral, constraintKeys } from "./shared/implement.js"; import { intersectNodesRoot, intersectOrPipeNodes } from "./shared/intersections.js"; import { $ark } from "./shared/registry.js"; import { arkKind } from "./shared/utils.js"; export class BaseConstraint extends BaseNode { constructor(attachments, $) { super(attachments, $); // define as a getter to avoid it being enumerable/spreadable Object.defineProperty(this, arkKind, { value: "constraint", enumerable: false }); } impliedSiblings; intersect(r) { return intersectNodesRoot(this, r, this.$); } } export class InternalPrimitiveConstraint extends BaseConstraint { traverseApply = (data, ctx) => { if (!this.traverseAllows(data, ctx)) ctx.errorFromNodeContext(this.errorContext); }; compile(js) { if (js.traversalKind === "Allows") js.return(this.compiledCondition); else { js.if(this.compiledNegation, () => js.line(`ctx.errorFromNodeContext(${this.compiledErrorContext})`)); } } get errorContext() { return { code: this.kind, description: this.description, meta: this.meta, ...this.inner }; } get compiledErrorContext() { return compileObjectLiteral(this.errorContext); } } export const constraintKeyParser = (kind) => (schema, ctx) => { if (isArray(schema)) { if (schema.length === 0) { // Omit empty lists as input return; } const nodes = schema.map(schema => ctx.$.node(kind, schema)); // predicate order must be preserved to ensure inputs are narrowed // and checked in the correct order if (kind === "predicate") return nodes; return nodes.sort((l, r) => (l.hash < r.hash ? -1 : 1)); } const child = ctx.$.node(kind, schema); return (child.hasOpenIntersection() ? [child] : child); }; export const intersectConstraints = (s) => { const head = s.r.shift(); if (!head) { let result = s.l.length === 0 && s.kind === "structure" ? $ark.intrinsic.unknown.internal : s.ctx.$.node(s.kind, Object.assign(s.baseInner, unflattenConstraints(s.l)), { prereduced: true }); for (const root of s.roots) { if (result instanceof Disjoint) return result; result = intersectOrPipeNodes(root, result, s.ctx); } return result; } let matched = false; for (let i = 0; i < s.l.length; i++) { const result = intersectOrPipeNodes(s.l[i], head, s.ctx); if (result === null) continue; if (result instanceof Disjoint) return result; if (result.isRoot()) { s.roots.push(result); s.l.splice(i); return intersectConstraints(s); } if (!matched) { s.l[i] = result; matched = true; } else if (!s.l.includes(result)) { return throwInternalError(`Unexpectedly encountered multiple distinct intersection results for refinement ${head}`); } } if (!matched) s.l.push(head); if (s.kind === "intersection") { if (head.impliedSiblings) for (const node of head.impliedSiblings) appendUnique(s.r, node); } return intersectConstraints(s); }; export const flattenConstraints = (inner) => { const result = Object.entries(inner) .flatMap(([k, v]) => k in constraintKeys ? v : []) .sort((l, r) => l.precedence < r.precedence ? -1 : l.precedence > r.precedence ? 1 // preserve order for predicates : l.kind === "predicate" && r.kind === "predicate" ? 0 : l.hash < r.hash ? -1 : 1); return result; }; export const unflattenConstraints = (constraints) => { const inner = {}; for (const constraint of constraints) { if (constraint.hasOpenIntersection()) { inner[constraint.kind] = append(inner[constraint.kind], constraint); } else { if (inner[constraint.kind]) { return throwInternalError(`Unexpected intersection of closed refinements of kind ${constraint.kind}`); } inner[constraint.kind] = constraint; } } return inner; }; export const throwInvalidOperandError = (...args) => throwParseError(writeInvalidOperandMessage(...args)); export const writeInvalidOperandMessage = (kind, expected, actual) => { const actualDescription = actual.hasKind("morph") ? "a morph" : actual.isUnknown() ? "unknown" : actual.exclude(expected).defaultShortDescription; return `${capitalize(kind)} operand must be ${expected.description} (was ${actualDescription})`; };