/* eslint-disable @typescript-eslint/no-unused-vars */ import { CompiledFunction, transform } from "../dev/utils/src/main.js" import type { AbstractableConstructor, BuiltinObjectKind, BuiltinObjects, conform, error, isAny, Literalable, Primitive } from "../dev/utils/src/main.js" import { arkKind, registry } from "./compile/registry.js" import { CompilationState, InputParameterName } from "./compile/state.js" import type { CheckResult } from "./compile/traverse.js" import { TraversalState } from "./compile/traverse.js" import { type } from "./main.js" import type { PredicateInput } from "./nodes/composite/predicate.js" import type { TypeNode } from "./nodes/composite/type.js" import { builtins, node } from "./nodes/composite/type.js" import type { inferIntersection } from "./parse/ast/intersections.js" import type { inferDefinition, validateDeclared, validateDefinition } from "./parse/definition.js" import { inferred } from "./parse/definition.js" import type { GenericParamsParseError } from "./parse/generic.js" import { parseGenericParams } from "./parse/generic.js" import type { IndexOneOperator, IndexZeroOperator, inferMorphOut, inferNarrow, Morph, MorphAst, Narrow, Out, TupleInfixOperator } from "./parse/tuple.js" import type { Scope } from "./scope.js" import { bindThis } from "./scope.js" export type TypeParser<$> = TypeOverloads<$> & TypeProps<$> type TypeOverloads<$> = { // Parse and check the definition, returning either the original input for a // valid definition or a string representing an error message. (def: validateTypeRoot): Type, $> // Spread version of a tuple expression ( _0: zero extends IndexZeroOperator ? zero : validateDefinition>, _1: zero extends "keyof" ? validateDefinition> : zero extends "instanceof" ? conform : zero extends "===" ? conform : conform, ..._2: one extends TupleInfixOperator ? [ one extends ":" ? Narrow< extractIn< inferDefinition< zero, $, bindThisToExpression > > > : one extends "=>" ? // TODO: centralize Morph< extractOut< inferDefinition< zero, $, bindThisToExpression > >, unknown > : validateDefinition< two, $, bindThisToExpression > ] : [] ): Type, $>, $> ( params: `<${validateParameterString}>`, def: validateDefinition< def, $, { [param in parseGenericParams[number]]: unknown } > ): Generic, def, $> } type bindThisToExpression = bindThis< tupleExpression > type tupleExpression = one extends TupleInfixOperator ? [zero, one, two] : [zero, one] type TypeProps<$> = { literal: ( ...possibleValues: branches ) => Type instanceof: ( ...possibleConstructors: branches ) => Type, $> } export type DeclarationParser<$> = () => { type: ( def: validateDeclared> ) => Type } export const createTypeParser = <$>(scope: Scope): TypeParser<$> => { const parser: TypeOverloads<$> = (...args: unknown[]) => { if (args.length === 1) { // treat as a simple definition return new Type(args[0], scope) as never } if ( args.length === 2 && typeof args[0] === "string" && args[0][0] === "<" && args[0].at(-1) === ">" ) { // if there are exactly two args, the first of which looks like <${string}>, // treat as a generic const params = parseGenericParams(args[0].slice(1, -1)) const def = args[1] return validateUninstantiatedGeneric( generic(params, def, scope) as never ) as never } // otherwise, treat as a tuple expression. technically, this also allows // non-expression tuple definitions to be parsed, but it's not a supported // part of the API as specified by the associated types return new Type(args, scope) as never } const props: TypeProps<$> = { literal: (...possibleValues) => new Type(node.literal(...possibleValues), scope), instanceof: (...possibleConstructors) => new Type( node( ...possibleConstructors.map( (ctor): PredicateInput => ({ basis: ctor }) ) ), scope ) } return Object.assign(parser, props) } export type DefinitionParser<$> = ( def: validateDefinition> ) => def registry().registerInternal("state", TraversalState) export class Type extends CompiledFunction< (data: unknown) => CheckResult> > { declare [inferred]: t declare infer: extractOut declare inferIn: extractIn config: TypeConfig root: TypeNode condition: string allows: this["root"]["allows"] constructor(public definition: unknown, public scope: Scope) { const root = parseTypeRoot(definition, scope) as TypeNode super( InputParameterName, `const state = new ${registry().reference("state")}(); ${root.compile(new CompilationState("traverse"))} return state.finalize(${InputParameterName});` ) this.root = root this.condition = root.condition this.allows = root.allows this.config = scope.config } configure(config: TypeConfig) { this.config = { ...this.config, ...config } return this } // TODO: should return out from(literal: this["infer"]) { return literal } fromIn(literal: this["inferIn"]) { return literal } // TODO: Morph intersections, ordering and( def: validateChainedExpression< def, $, inferIntersection> > ): Type>, $> { return new Type( this.root.and(parseTypeRoot(def, this.scope)), this.scope ) as never } or(def: validateTypeRoot): Type, $> { return new Type( this.root.or(parseTypeRoot(def, this.scope)), this.scope ) as never } morph>>( morph: morph ): Type<(In: this["inferIn"]) => Out>>, $> morph>, def>( morph: morph, outValidator: validateTypeRoot ): Type< (In: this["inferIn"]) => Out< // TODO: validate overlapping // inferMorphOut> & extractOut> >, $ > morph(morph: Morph, outValidator?: unknown) { // TODO: tuple expression for out validator outValidator return new Type( this.root.constrain("morph", morph), this.scope ) as never } // TODO: based on below, should maybe narrow morph output if used after narrow>>( def: def ): Type, def>, $> { return new Type(this.root.constrain("narrow", def), this.scope) as never } array(): Type { return new Type(this.root.array(), this.scope) as never } keyof(): Type { return new Type(this.root.keyof(), this.scope) as never } assert(data: unknown): extractOut { const result = this.call(null, data) return result.problems ? result.problems.throw() : result.data } equals(other: Type): this is Type { return this.root === (other.root as unknown) } extends(other: Type): this is Type { return this.root.extends(other.root) } } const parseTypeRoot = (def: unknown, scope: Scope, args?: BoundArgs) => scope.parseRoot(def, args ?? bindThis()) export type validateTypeRoot = validateDefinition> export type inferTypeRoot = inferDefinition> type validateChainedExpression = def extends validateTypeRoot< def, $ > ? inferred extends error ? inferred : def : validateTypeRoot type validateParameterString = parseGenericParams extends GenericParamsParseError ? message : params export const validateUninstantiatedGeneric = (g: Generic) => { // the unconstrained instantiation of the generic is not used for now // other than to eagerly validate that the def does not contain any errors g.scope.parseRoot( g.definition, Object.fromEntries( // once we support constraints on generic parameters, we'd use // the base type here: https://github.com/arktypeio/arktype/issues/796 g.parameters.map((name) => [name, builtins.unknown()]) ) ) return g } export const generic = ( parameters: string[], definition: unknown, scope: Scope ) => { return Object.assign( (...args: unknown[]) => { const argNodes = transform(parameters, ([i, param]) => [ param, parseTypeRoot(args[i], scope) ]) const root = parseTypeRoot(definition, scope, argNodes) return new Type(root, scope) }, { [arkKind]: "generic", // TODO: remove at runtime $: undefined, parameters, definition, scope } satisfies GenericProps ) } // Comparing to Generic directly doesn't work well, so we compare to only its props export type GenericProps< params extends string[] = string[], def = unknown, $ = any > = { [arkKind]: "generic" $: $ parameters: params definition: def scope: Scope } export type BoundArgs = Record // TODO: Fix external reference (i.e. if this is attached to a scope, then args are defined using it) export type Generic< params extends string[] = string[], def = unknown, $ = any > = (( ...args: conform< args, { [i in keyof params]: validateTypeRoot } > ) => Type< inferDefinition>, $ >) & GenericProps type bindGenericInstantiation = { [i in keyof params & `${number}` as params[i]]: inferTypeRoot< args[i & keyof args], $ > } export type KeyCheckKind = "loose" | "strict" | "distilled" export type TypeConfig = { keys?: KeyCheckKind mustBe?: string } export type extractIn = extractMorphs extends t ? t : extractMorphs export type extractOut = extractMorphs extends t ? t : extractMorphs type extractMorphs = t extends MorphAst< infer i, infer o > ? io extends "in" ? i : o : t extends TerminallyInferredObjectKind | Primitive ? t : { [k in keyof t]: extractMorphs } /** Objects we don't want to expand during inference like Date or Promise */ type TerminallyInferredObjectKind = BuiltinObjects[Exclude< BuiltinObjectKind, "Object" | "Array" >]