import type { AbstractableConstructor, Constructor, Domain, evaluate, inferDomain, isUnknown, List, listable } from "../../../dev/utils/src/main.js" import { domainOf, isArray, throwInternalError, throwParseError } from "../../../dev/utils/src/main.js" import { writeUnboundableMessage } from "../../parse/ast/bound.js" import { writeIndivisibleMessage } from "../../parse/ast/divisor.js" import type { inferMorphOut, Morph, Narrow, NarrowCast, Out } from "../../parse/tuple.js" import { Disjoint } from "../disjoint.js" import type { NodeKinds } from "../kinds.js" import { createNodeOfKind, precedenceByKind } from "../kinds.js" import type { BaseNode } from "../node.js" import { defineNodeKind } from "../node.js" import type { BasisInput, BasisNode, inferBasis } from "../primitive/basis/basis.js" import type { ClassNode } from "../primitive/basis/class.js" import { classNode } from "../primitive/basis/class.js" import type { DomainNode } from "../primitive/basis/domain.js" import { domainNode } from "../primitive/basis/domain.js" import type { ValueNode } from "../primitive/basis/value.js" import { valueNode } from "../primitive/basis/value.js" import type { Range } from "../primitive/range.js" import type { SerializedRegexLiteral } from "../primitive/regex.js" import type { inferPropsInput } from "./infer.js" import type { PropsInput } from "./props.js" export interface PredicateNode extends BaseNode { basis: BasisNode | undefined constraints: ConstraintNode[] getConstraint: (k: k) => ConstraintKinds[k] valueNode: ValueNode | undefined constrain( kind: kind, input: ConstraintsInput[kind] ): PredicateNode } export const predicateNode = defineNodeKind( { kind: "predicate", parse: (input) => { let children: PredicateChildren if (isArray(input)) { children = input } else { const basis = input.basis && basisNodeFrom(input.basis) children = basis ? [basis] : [] for (const kind of constraintKindNames) { if (input[kind]) { assertAllowsConstraint(basis, kind) children.push( createNodeOfKind(kind, input[kind] as never) ) } } } // sort by precedence, and then alphabetically by kind return children.sort((l, r) => precedenceByKind[l.kind] > precedenceByKind[r.kind] ? 1 : precedenceByKind[l.kind] < precedenceByKind[r.kind] ? -1 : l.kind > r.kind ? 1 : -1 ) }, compile: (children, state) => { let result = "" const initialChild = children.at(0) const basis = initialChild?.isBasis() ? initialChild : undefined if (basis) { state.bases.push(basis) } for (const child of children) { const childResult = child.compile(state) if (childResult) { result = result ? `${result}\n${childResult}` : childResult } } if (basis) { state.bases.pop() } return result }, intersect: (l, r): PredicateNode | Disjoint => { // if ( // // s.lastOperator === "&" && // rules.morphs?.some( // (morph, i) => morph !== branch.tree.morphs?.[i] // ) // ) { // throwParseError( // writeImplicitNeverMessage(s.path, "Intersection", "of morphs") // ) // } // TODO: can props imply object basis for compilation? const basis = l.basis ? r.basis ? l.basis.intersect(r.basis) : l.basis : r.basis if (basis instanceof Disjoint) { return basis } if (l.valueNode) { return r.allows(l.valueNode.rule) ? l : Disjoint.from("assignability", l.valueNode, r) } if (r.valueNode) { return l.allows(r.valueNode.rule) ? r : Disjoint.from("assignability", l, r.valueNode) } const rules: PredicateChildren = basis ? [basis] : [] for (const kind of constraintKindNames) { const lNode = l.getConstraint(kind) const rNode = r.getConstraint(kind) if (lNode) { if (rNode) { const result = lNode.intersect(rNode as never) // TODO: don't return here if (result instanceof Disjoint) { return result } rules.push(result) } else { rules.push(lNode) } } else if (rNode) { rules.push(rNode) } } return predicateNode(rules) } }, (base) => { const initialRule = base.rule.at(0) const basis = initialRule?.isBasis() ? initialRule : undefined const constraints = ( basis ? base.rule.slice(1) : base.rule ) as ConstraintNode[] const description = base.rule.length === 0 ? "unknown" : constraints.length ? constraints.map((rule) => rule.toString()).join(" and ") : `${basis}` return { description, basis, constraints, getConstraint: (k) => constraints.find( (constraint) => constraint.kind === k ) as never, valueNode: basis?.hasKind("value") ? basis : undefined, constrain(kind, input): PredicateNode { assertAllowsConstraint(this.basis, kind) const constraint = createNodeOfKind(kind, input as never) const result = this.intersect(predicateNode([constraint])) if (result instanceof Disjoint) { return result.throw() } return result } } } ) // keyof() { // if (!this.basis) { // return neverTypeNode // } // const basisKey = this.basis.keyof() // const propsKey = this.getConstraint("props")?.keyof() // return propsKey?.or(basisKey) ?? basisKey // } export const assertAllowsConstraint = ( basis: BasisNode | undefined, kind: ConstraintKind ) => { if (basis?.hasKind("value")) { if (kind !== "morph") { throwInvalidConstraintError( kind, "a non-literal type", basis.toString() ) } return } const domain = basis?.domain ?? "unknown" switch (kind) { case "divisor": if (domain !== "number") { throwParseError(writeIndivisibleMessage(domain)) } return case "range": if (domain !== "string" && domain !== "number") { const hasSizedClassBasis = basis?.hasKind("class") && basis.extendsOneOf(Array, Date) if (!hasSizedClassBasis) { throwParseError(writeUnboundableMessage(domain)) } } return case "regex": if (domain !== "string") { throwInvalidConstraintError("regex", "a string", domain) } return case "props": if (domain !== "object") { throwInvalidConstraintError("props", "an object", domain) } return case "narrow": return case "morph": return default: throwInternalError(`Unexpxected rule kind '${kind}'`) } } export const writeInvalidConstraintMessage = ( kind: ConstraintKind, typeMustBe: string, typeWas: string ) => { return `${kind} constraint may only be applied to ${typeMustBe} (was ${typeWas})` } export const throwInvalidConstraintError = ( ...args: Parameters ) => throwParseError(writeInvalidConstraintMessage(...args)) const constraintKindNames = [ "divisor", "range", "regex", "props", "narrow", "morph" ] as const satisfies List export type ListableInputKind = "regex" | "narrow" | "morph" export type PredicateChildren = | [BasisNode, ...ConstraintNode[]] | ConstraintNode[] export type ConstraintNode = ConstraintKinds[ConstraintKind] type ConstraintKinds = Pick< NodeKinds, "range" | "divisor" | "regex" | "props" | "narrow" | "morph" > export type PredicateChildKind = "basis" | ConstraintKind export type ConstraintKind = keyof ConstraintKinds export type PredicateInput< basis extends BasisInput | undefined = BasisInput | undefined > = | Record | evaluate< { basis: basis } & ConstraintsInput > export type ConstraintsInput< basis extends BasisInput | undefined = BasisInput | undefined > = BasisInput extends basis ? { [k in ConstraintKind]?: unknownConstraintInput } : basis extends BasisInput ? constraintsOf : functionalConstraints type unknownConstraintInput = kind extends "props" ? PropsInput : | ConstraintKinds[kind]["rule"] // Add the unlisted version as a valid input for these kinds | (kind extends ListableInputKind ? ConstraintKinds[kind]["rule"][number] : never) export type inferPredicateDefinition = input["morph"] extends Morph ? (In: inferPredicateInput) => Out> : input["morph"] extends readonly [...any[], Morph] ? (In: inferPredicateInput) => Out> : inferPredicateInput type inferPredicateInput = input["narrow"] extends NarrowCast ? narrowed : input["narrow"] extends List ? inferNarrowArray extends infer result ? isUnknown extends true ? inferNonFunctionalConstraints : result : never : inferNonFunctionalConstraints type inferNarrowArray< filters extends List, result = unknown > = filters extends readonly [infer head, ...infer tail] ? inferNarrowArray< tail, result & (head extends NarrowCast ? narrowed : unknown) > : evaluate type inferNonFunctionalConstraints = input["basis"] extends BasisInput ? input["props"] extends PropsInput ? inferPropsInput : inferBasis : unknown type constraintsOf = basis extends Domain ? functionalConstraints> & domainConstraints : basis extends Constructor ? functionalConstraints> & classConstraints : basis extends readonly ["===", infer value] ? // Exact values cannot be filtered, but can be morphed Pick, "morph"> : never type domainConstraints = basis extends "object" ? { props?: PropsInput } : basis extends "string" ? { regex?: listable range?: Range } : basis extends "number" ? { divisor?: number range?: Range } : {} type functionalConstraints = { narrow?: listable> morph?: listable> } type classConstraints = base extends typeof Array ? { props?: PropsInput range?: Range } : { props?: PropsInput } export type basisNodeFrom = input extends Domain ? DomainNode : input extends AbstractableConstructor ? ClassNode : ValueNode export const basisNodeFrom = ( input: BasisInput ): DomainNode | ClassNode | ValueNode => { switch (typeof input) { case "string": return domainNode(input) case "object": return valueNode(input[1]) case "function": return classNode(input) default: return throwInternalError( `Unexpectedly got a basis input of type ${domainOf(input)}` ) } }