diff --git a/.vscode/settings.json b/.vscode/settings.json index f438589b7d..a76a49d9b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,5 +48,8 @@ }, "debug.javascript.terminalOptions": { "skipFiles": ["/**", "**/node_modules/**"] + }, + "editor.quickSuggestions": { + "strings": "on" } } diff --git a/LICENSE b/LICENSE index bddd28836d..8341b27470 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 ArkType +Copyright 2024 ArkType Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/ark/attest/CHANGELOG.md b/ark/attest/CHANGELOG.md index 2e079e11e7..03ab3ccfdf 100644 --- a/ark/attest/CHANGELOG.md +++ b/ark/attest/CHANGELOG.md @@ -1,5 +1,9 @@ # @ark/attest +## 0.9.2 + +Fix a bug preventing consecutive benchmark runs from populating snapshots inline + ## 0.8.2 ### Patch Changes diff --git a/ark/attest/bench/bench.ts b/ark/attest/bench/bench.ts index 6d4b0c4357..273d57e365 100644 --- a/ark/attest/bench/bench.ts +++ b/ark/attest/bench/bench.ts @@ -1,4 +1,4 @@ -import { caller, type SourcePosition } from "@ark/fs" +import { caller, rmRf, type SourcePosition } from "@ark/fs" import { performance } from "node:perf_hooks" import { ensureCacheDirs, @@ -22,6 +22,8 @@ export type StatName = keyof typeof stats export type TimeAssertionName = StatName | "mark" +let benchHasRun = false + export const bench = ( name: string, fn: Fn, @@ -34,8 +36,15 @@ export const bench = ( fn.constructor.name === "AsyncFunction", options ) + + if (!benchHasRun) { + rmRf(ctx.cfg.cacheDir) + ensureCacheDirs() + benchHasRun = true + } + ctx.benchCallPosition = caller() - ensureCacheDirs() + if ( typeof ctx.cfg.filter === "string" && !qualifiedPath.includes(ctx.cfg.filter) diff --git a/ark/attest/config.ts b/ark/attest/config.ts index 9381269069..8431d44bc7 100644 --- a/ark/attest/config.ts +++ b/ark/attest/config.ts @@ -1,7 +1,7 @@ import { ensureDir, fromCwd } from "@ark/fs" import { - arrayFrom, isArray, + liftArray, tryParseNumber, type autocomplete } from "@ark/util" @@ -135,7 +135,7 @@ const parseTsVersions = (aliases: TsVersionAliases): TsVersionData[] => { const versions = findAttestTypeScriptVersions() if (aliases === "*") return versions - return arrayFrom(aliases).map(alias => { + return liftArray(aliases).map(alias => { const matching = versions.find(v => v.alias === alias) if (!matching) { throw new Error( diff --git a/ark/attest/package.json b/ark/attest/package.json index 9d41d46fd1..a3a9ca2248 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.9.1", + "version": "0.9.3", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/dark/package.json b/ark/dark/package.json index 4aeac08311..0148997bc8 100644 --- a/ark/dark/package.json +++ b/ark/dark/package.json @@ -57,6 +57,9 @@ } ], "configurationDefaults": { + "editor.quickSuggestions": { + "strings": "on" + }, "errorLens.followCursor": "closestProblem", "errorLens.delay": 0, "errorLens.editorHoverPartsEnabled": { diff --git a/ark/docs/src/content/docs/intro/setup.mdx b/ark/docs/src/content/docs/intro/setup.mdx index b6ec8a7d84..65de4128b0 100644 --- a/ark/docs/src/content/docs/intro/setup.mdx +++ b/ark/docs/src/content/docs/intro/setup.mdx @@ -32,8 +32,28 @@ You'll also need... - [`skipLibCheck`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) (strongly recommended, see [FAQ](/reference/faq#why-do-i-see-type-errors-in-an-arktype-package-in-node_modules)) - [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) (recommended) -## Extension (optional) +## VSCode -If you're using VSCode, we recommend installing [ArkDark](https://marketplace.visualstudio.com/items?itemName=arktypeio.arkdark), an extension we built to provide the embedded syntax highlighting you'll see throughout these docs. +### Settings + +To take advantage of all of ArkType's autocomplete capabilities, you'll need to add the following to your workspace settings at `.vscode/settings.json`: + +```jsonc +{ + // other settings... + // allow autocomplete for ArkType expressions like "string | num" + "editor.quickSuggestions": { + "strings": "on" + } +} +``` + +### Extension (optional) + +[ArkDark](https://marketplace.visualstudio.com/items?itemName=arktypeio.arkdark) provides the embedded syntax highlighting you'll see throughout the docs. + +Even without it, your definitions will feel like a natural extension of the language. With it, you'll forget the boundary altogether. + +## Other editors If you're using a different editor, we'd love [help adding support](https://github.com/arktypeio/arktype/issues/989). In the meantime, don't worry- ArkType still offers best-in-class DX anywhere TypeScript is supported. diff --git a/ark/docs/src/content/docs/reference/generics.md b/ark/docs/src/content/docs/reference/generics.md index afffe9f2ed..cff4cc5539 100644 --- a/ark/docs/src/content/docs/reference/generics.md +++ b/ark/docs/src/content/docs/reference/generics.md @@ -58,7 +58,60 @@ import { type } from "arktype" const stringRecord = type("Record") ``` -Other common utils like `Pick` and `Omit` to follow in the an upcoming release. +In addition to `Record`, the following generics from TS are now available in ArkType: + +- **Pick** +- **Omit** +- **Extract** +- **Exclude** + +These can be instantiated in one of three ways: + +### Syntactic Definition + +```ts +import { type } from "arktype" + +const one = type("Extract<0 | 1, 1>") +``` + +### Chained Definition + +```ts +import { type } from "arktype" + +const user = type({ + name: "string", + "age?": "number", + isAdmin: "boolean" +}) + +// hover me! +const basicUser = user.pick("name", "age") +``` + +### Invoked Definition + +```ts +import { ark } from "arktype" + +const unfalse = ark.Exclude("boolean", "false") +``` + +### Generic HKTs + +Our new generics have been built using a new method for integrating arbitrary external types as native ArkType generics! This opens up tons of possibilities for external integrations that would otherwise not be possible, but we're still finalizing the API. As a preview, here's what the implementation of `Exclude` looks like internally: + +```ts +// @noErrors +class ArkExclude extends generic("T", "U")(args => args.T.exclude(args.U)) { + declare hkt: ( + args: conform + ) => Exclude<(typeof args)[0], (typeof args)[1]> +} +``` + +More to come on this as the API is finalized! Recursive and cyclic generics are also currently unavailable and will be added soon. diff --git a/ark/fs/package.json b/ark/fs/package.json index 2395c5199d..37e21dc719 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index 755f23c50e..bf3e0c2e8d 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,16 +1,14 @@ -import { scope, type } from "arktype" +import { ark, scope, type } from "arktype" -const nonEmpty = type("", "arr > 0") +const t = type({ + foo: "string | number", + bar: ["number | string | boolean"] +}) -const m = nonEmpty("number[]") +const o = type("string") -const threeSixtyNoModule = scope({ - three: "3", - sixty: "60", - no: "'no'" -}).export() +ark.Record -const types = scope({ - ...threeSixtyNoModule, - threeSixtyNo: "three|sixty|no" -}).export() +// ("string", { +// bar: "number | string" +// }) diff --git a/ark/repo/scratch/hkt.ts b/ark/repo/scratch/hkt.ts deleted file mode 100644 index b1599959b8..0000000000 --- a/ark/repo/scratch/hkt.ts +++ /dev/null @@ -1,121 +0,0 @@ -// import type { Hkt } from "@ark/util" - -// // Custom user type -// interface Chunk { -// t: T -// isChunk: true -// } - -// // User defines the HKT signature -// interface ToChunk extends Hkt.Kind { -// f(x: this[Hkt.key]): Chunk -// } - -// Original HKT implementation: -// export type Module = { -// // just adding the nominal id this way and mapping it is cheaper than an intersection -// [k in exportedName | arkKind]: k extends string -// ? [r["exports"][k]] extends [never] -// ? Type> -// : isAny extends true -// ? Type> -// : r["exports"][k] extends Kind -// ? ( -// def: validateTypeRoot> -// ) => inferTypeRoot> extends infer t -// ? Apply -// : never -// : r["exports"][k] extends PreparsedResolution -// ? r["exports"][k] -// : Type> -// : // set the nominal symbol's value to something validation won't care about -// // since the inferred type will be omitted anyways -// CastTo<"module"> -// } - -///** These are legal as values of a scope but not as definitions in other contexts */ -// Note this will have to be updated to distinguish Kind from NodeKinds -// type PreparsedResolution = Module | GenericProps | Kind - -// export type parseUnenclosed< -// s extends StaticState, -// $, -// args -// > = Scanner.shiftUntilNextTerminator< -// s["unscanned"] -// > extends Scanner.shiftResult -// ? token extends "keyof" -// ? state.addPrefix -// : tryResolve extends infer result -// ? result extends error -// ? state.error -// : result extends keyof $ -// ? $[result] extends Kind -// ? parseKindInstantiation< -// token, -// $[result], -// state.scanTo, -// $, -// args -// > -// : $[result] extends GenericProps -// ? parseGenericInstantiation< -// token, -// $[result], -// state.scanTo, -// $, -// args -// > -// : state.setRoot -// : state.setRoot -// : never -// : never - -// export type parseKindInstantiation< -// name extends string, -// k extends Kind, -// s extends StaticState, -// $, -// args -// // have to skip whitespace here since TS allows instantiations like `Partial ` -// > = Scanner.skipWhitespace extends `<${infer unscanned}` -// ? parseGenericArgs extends infer result -// ? result extends ParsedArgs -// ? state.setRoot< -// s, -// CastTo>>, -// nextUnscanned -// > -// : // propagate error -// result -// : never -// : state.error> - -// export type inferTerminal = token extends keyof args | keyof $ -// ? resolve -// // Added this for HKTs, could be less hacky? -// : token extends CastTo -// ? t -// : token extends StringLiteral -// ? Text -// : token extends RegexLiteral -// ? string -// : token extends DateLiteral -// ? Date -// : token extends NumberLiteral -// ? value -// : token extends BigintLiteral -// ? value -// : never - -// // User can now reference the HKT in any ArkType syntax with autocompletion - -// const s = scope({ -// foo: "string", -// toChunk: {} as ToChunk, -// dateChunkArray: "Array>" -// }).export() - -// // Generics can also be instantiated after the scope is defined -// const t = s.toChunk("toChunk") -// // ^? diff --git a/ark/schema/__tests__/errors.test.ts b/ark/schema/__tests__/errors.test.ts index eb43065a1c..d9595636ec 100644 --- a/ark/schema/__tests__/errors.test.ts +++ b/ark/schema/__tests__/errors.test.ts @@ -1,5 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { configure, schema } from "@ark/schema" +import { $ark } from "@ark/util" import { schemaScope } from "../scope.js" contextualize(() => { diff --git a/ark/schema/__tests__/parse.test.ts b/ark/schema/__tests__/parse.test.ts index 8c84d20497..f4c6c7c161 100644 --- a/ark/schema/__tests__/parse.test.ts +++ b/ark/schema/__tests__/parse.test.ts @@ -1,10 +1,10 @@ import { attest, contextualize } from "@ark/attest" -import { type Root, schema } from "@ark/schema" +import { type SchemaRoot, schema } from "@ark/schema" contextualize(() => { it("single constraint", () => { const t = schema({ domain: "string", pattern: ".*" }) - attest>(t) + attest>(t) attest(t.json).snap({ domain: "string", pattern: [".*"] }) }) @@ -19,7 +19,7 @@ contextualize(() => { divisor: 5 }) const result = l.and(r) - attest>(result) + attest>(result) attest(result.json).snap({ domain: "number", divisor: 15, diff --git a/ark/schema/__tests__/unit.test.ts b/ark/schema/__tests__/unit.test.ts index c13108c596..889fc4cf6d 100644 --- a/ark/schema/__tests__/unit.test.ts +++ b/ark/schema/__tests__/unit.test.ts @@ -1,6 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { assertNodeKind, schema } from "@ark/schema" -import { registeredReference } from "@ark/util" +import { registeredReference } from "../shared/registry.js" contextualize(() => { it("string allows", () => { diff --git a/ark/schema/ast.ts b/ark/schema/ast.ts index 65b7026046..2a2d6376d0 100644 --- a/ark/schema/ast.ts +++ b/ark/schema/ast.ts @@ -7,9 +7,12 @@ import type { equals, leftIfEqual, Primitive, + propValueOf, show } from "@ark/util" import type { PrimitiveConstraintKind } from "./constraint.js" +import type { platformObjectExports } from "./keywords/platformObjects.js" +import type { typedArrayExports } from "./keywords/typedArray.js" import type { NodeSchema } from "./kinds.js" import type { constraintKindOf } from "./roots/intersection.js" import type { MorphAst, Out } from "./roots/morph.js" @@ -349,7 +352,7 @@ type _distill< distilledKind extends "base" ? _distill : of<_distill, constraints> - : t extends TerminallyInferredObjectKind | ArkEnv.preserve | Primitive ? t + : t extends TerminallyInferredObjectKind | Primitive ? t : unknown extends t ? unknown : t extends MorphAst ? io extends "in" ? @@ -419,3 +422,5 @@ type distillPostfix< type TerminallyInferredObjectKind = | ArkEnv.preserve | BuiltinObjects[Exclude] + | propValueOf + | propValueOf diff --git a/ark/schema/config.ts b/ark/schema/config.ts index d0e2669b5c..2eed106d01 100644 --- a/ark/schema/config.ts +++ b/ark/schema/config.ts @@ -1,6 +1,12 @@ -import type { array, mutable, requireKeys, show } from "@ark/util" +import { + $ark, + type array, + type mutable, + type requireKeys, + type show +} from "@ark/util" import type { Ark } from "./keywords/keywords.js" -import type { IntrinsicKeywords, RawRootScope } from "./scope.js" +import type { InternalBaseScope, IntrinsicKeywords } from "./scope.js" import type { ActualWriter, ArkErrorCode, @@ -20,7 +26,7 @@ declare global { meta(): {} preserve(): never registry(): { - ambient: RawRootScope + ambient: InternalBaseScope intrinsic: IntrinsicKeywords config: ArkConfig defaultConfig: ResolvedArkConfig diff --git a/ark/schema/constraint.ts b/ark/schema/constraint.ts index 22266a7c34..8182e070f4 100644 --- a/ark/schema/constraint.ts +++ b/ark/schema/constraint.ts @@ -1,4 +1,5 @@ import { + $ark, append, appendUnique, capitalize, @@ -24,9 +25,9 @@ import type { IntersectionInner, MutableIntersectionInner } from "./roots/intersection.js" -import type { BaseRoot, Root, UnknownRoot } from "./roots/root.js" +import type { BaseRoot, SchemaRoot, UnknownRoot } from "./roots/root.js" import type { NodeCompiler } from "./shared/compile.js" -import type { RawNodeDeclaration } from "./shared/declare.js" +import type { BaseNodeDeclaration } from "./shared/declare.js" import { Disjoint } from "./shared/disjoint.js" import { compileErrorContext, @@ -42,7 +43,7 @@ import { intersectNodes, intersectNodesRoot } from "./shared/intersections.js" import type { TraverseAllows, TraverseApply } from "./shared/traversal.js" import { arkKind } from "./shared/utils.js" -export interface BaseConstraintDeclaration extends RawNodeDeclaration { +export interface BaseConstraintDeclaration extends BaseNodeDeclaration { kind: ConstraintKind } @@ -67,7 +68,7 @@ export type ConstraintReductionResult = | Disjoint | MutableIntersectionInner -export abstract class RawPrimitiveConstraint< +export abstract class InternalPrimitiveConstraint< d extends BaseConstraintDeclaration > extends BaseConstraint { abstract traverseAllows: TraverseAllows @@ -237,20 +238,20 @@ export const throwInvalidOperandError = ( export const writeInvalidOperandMessage = < kind extends ConstraintKind, - expected extends Root, - actual extends Root + expected extends SchemaRoot, + actual extends SchemaRoot >( kind: kind, expected: expected, actual: actual -): writeInvalidOperandMessage => +) => `${capitalize(kind)} operand must be ${ expected.description } (was ${actual.exclude(expected).description})` as never export type writeInvalidOperandMessage< kind extends ConstraintKind, - actual extends Root + actual extends SchemaRoot > = `${Capitalize} operand must be ${describe< Prerequisite >} (was ${describe>>})` diff --git a/ark/schema/generic.ts b/ark/schema/generic.ts index 8ca0d90c7b..0023a21b6d 100644 --- a/ark/schema/generic.ts +++ b/ark/schema/generic.ts @@ -1,22 +1,24 @@ import { - Callable, + $ark, cached, + Callable, flatMorph, - isThunk, + snapshot, throwParseError, type array, - type thunkable + type Hkt, + type Json } from "@ark/util" import type { inferRoot } from "./inference.js" import type { RootSchema } from "./kinds.js" -import type { Root, UnknownRoot } from "./roots/root.js" -import type { RawRootScope, RootScope } from "./scope.js" +import type { SchemaRoot, UnknownRoot } from "./roots/root.js" +import type { BaseScope, InternalBaseScope } from "./scope.js" import { arkKind } from "./shared/utils.js" export type GenericParamAst< name extends string = string, constraint = unknown -> = readonly [name: name, constraint: constraint] +> = [name: name, constraint: constraint] export type GenericParamDef = | name @@ -28,21 +30,19 @@ export type ConstrainedGenericParamDef = export const parseGeneric = ( paramDefs: array, bodyDef: unknown, - $: thunkable + $: BaseScope ): GenericRoot => new GenericRoot(paramDefs, bodyDef, $, $) type genericParamSchemaToAst = schema extends string ? GenericParamAst : schema extends ConstrainedGenericParamDef ? - GenericParamAst> + [schema[0], inferRoot] : never export type genericParamSchemasToAst< schemas extends array, $ -> = readonly [ - ...{ [i in keyof schemas]: genericParamSchemaToAst } -] +> = [...{ [i in keyof schemas]: genericParamSchemaToAst }] export type genericParamAstToDefs> = { [i in keyof asts]: GenericParamDef @@ -64,16 +64,18 @@ type instantiateParams> = { : never } -export type GenericNodeSignature< +export type GenericRootInstantiator< params extends array, def, $ > = >( ...args: args -) => Root>> +) => SchemaRoot< + inferRoot> +> type instantiateConstraintsOf> = { - [i in keyof params]: Root + [i in keyof params]: SchemaRoot } export type GenericParam< @@ -99,45 +101,41 @@ export interface GenericProps< $ = any > { [arkKind]: "generic" + paramsAst: params params: instantiateParams names: genericParamNames constraints: instantiateConstraintsOf bodyDef: bodyDef - $: RootScope<$> + $: BaseScope<$> } export type GenericArgResolutions< params extends array = array > = { - [i in keyof params as params[i & `${number}`][0]]: UnknownRoot< + [i in keyof params as params[i & `${number}`][0]]: SchemaRoot< params[i & `${number}`][1] > } -export type LazyGenericSchema< +export class LazyGenericBody< params extends array = array, - returns extends RootSchema = RootSchema -> = (args: GenericArgResolutions) => returns - -export class LazyGenericRoot< - params extends array = array -> extends Callable> {} + returns = unknown +> extends Callable<(args: GenericArgResolutions) => returns> {} export class GenericRoot< - params extends array = array, - bodyDef = any, - $ = any - > - extends Callable> - implements GenericProps -{ + params extends array = array, + bodyDef = unknown, + $ = {}, + arg$ = $ +> extends Callable> { readonly [arkKind] = "generic" + declare readonly paramsAst: params constructor( public paramDefs: genericParamAstToDefs, public bodyDef: bodyDef, - private _$: thunkable>, - private _arg$: thunkable> + public $: BaseScope<$>, + public arg$: BaseScope ) { super((...args: any[]) => { const argNodes = flatMorph(this.names, (i, name) => { @@ -152,28 +150,25 @@ export class GenericRoot< ) } return [name, arg] - }) as GenericArgResolutions + }) as GenericArgResolutions + + if (this.defIsLazy()) { + const def = this.bodyDef(argNodes) - if (bodyDef instanceof LazyGenericRoot) - return this.$.parseRoot(bodyDef(argNodes)) as never + return this.$.parseRoot(def) as never + } - return this.$.parseRoot(bodyDef as never, { args: argNodes }) as never + return this.$.parseRoot(bodyDef, { args: argNodes }) as never }) - // if this is a standalone generic, validate its base constraints right away - if (!isThunk(this._$)) this.validateBaseInstantiation() - // if it's part of a scope, scope.export will be resposible for invoking - // validateBaseInstantiation on export() once everything is resolvable - } - get $() { - return isThunk(this._$) ? this._$() : this._$ + this.validateBaseInstantiation() } - get arg$() { - return isThunk(this._arg$) ? this._arg$() : this._arg$ + defIsLazy(): this is GenericRoot { + return this.bodyDef instanceof LazyGenericBody } - bindScope($: RawRootScope): this { + bindScope($: InternalBaseScope): this { if (this.arg$ === ($ as never)) return this return new GenericRoot( this.params as never, @@ -183,6 +178,16 @@ export class GenericRoot< ) as never } + @cached + get json(): Json { + return { + params: this.params.map(param => + param[1].isUnknown() ? param[0] : [param[0], param[1].json] + ), + body: snapshot(this.bodyDef) as never + } + } + @cached get params(): instantiateParams { return this.paramDefs.map( @@ -204,7 +209,7 @@ export class GenericRoot< } @cached - get baseInstantiation(): Root { + get baseInstantiation(): SchemaRoot { return this(...(this.constraints as never)) } @@ -222,6 +227,29 @@ export class GenericRoot< } } +export type GenericHktSchemaParser<$ = {}> = < + const paramsDef extends array +>( + ...params: paramsDef +) => ( + instantiateDef: LazyGenericBody< + genericParamSchemasToAst, + RootSchema + > +) => GenericHktRootSubclass, $> + +export type GenericHktRootSubclass< + params extends array, + $ +> = abstract new () => GenericHktRoot + +// convenient for AST display without including default params +interface Hkt extends Hkt.Kind {} + +export interface GenericHktRoot, $, args$> + extends GenericRoot, + Hkt.Kind {} + export const writeUnsatisfiedParameterConstraintMessage = < name extends string, constraint extends string, diff --git a/ark/schema/index.ts b/ark/schema/index.ts index 6c7ee8906d..414887f530 100644 --- a/ark/schema/index.ts +++ b/ark/schema/index.ts @@ -32,11 +32,13 @@ export * from "./roots/root.js" export * from "./roots/union.js" export * from "./roots/unit.js" export * from "./scope.js" +export * from "./shared/compile.js" export * from "./shared/declare.js" export * from "./shared/disjoint.js" export * from "./shared/errors.js" export * from "./shared/implement.js" export * from "./shared/intersections.js" +export * from "./shared/registry.js" export * from "./shared/utils.js" export * from "./structure/indexed.js" export * from "./structure/optional.js" diff --git a/ark/schema/inference.ts b/ark/schema/inference.ts index a970d30439..7dd1df9270 100644 --- a/ark/schema/inference.ts +++ b/ark/schema/inference.ts @@ -23,20 +23,15 @@ import type { import type { ProtoSchema } from "./roots/proto.js" import type { NormalizedUnionSchema, UnionSchema } from "./roots/union.js" import type { UnitSchema } from "./roots/unit.js" -import type { ArkErrors } from "./shared/errors.js" import type { BasisKind, ConstraintKind } from "./shared/implement.js" import type { inferred } from "./shared/utils.js" -export namespace type { - export type cast = { - [inferred]?: t - } - - export type errors = ArkErrors +export type InferredRoot = { + [inferred]?: t } export type validateRoot = - schema extends type.cast ? schema + schema extends InferredRoot ? schema : schema extends array ? { [i in keyof schema]: validateRootBranch } : schema extends NormalizedUnionSchema ? @@ -51,7 +46,7 @@ export type validateRoot = : validateRootBranch export type inferRoot = - schema extends type.cast ? to + schema extends InferredRoot ? to : schema extends UnionSchema ? branches["length"] extends 0 ? never : branches["length"] extends 1 ? inferRootBranch @@ -64,7 +59,7 @@ type validateRootBranch = : validateMorphChild type inferRootBranch = - schema extends type.cast ? to + schema extends InferredRoot ? to : schema extends MorphSchema ? ( In: schema["in"] extends {} ? inferMorphChild : unknown diff --git a/ark/schema/keywords/jsObjects.ts b/ark/schema/keywords/jsObjects.ts index 94b2890c0b..1f4d1d1edc 100644 --- a/ark/schema/keywords/jsObjects.ts +++ b/ark/schema/keywords/jsObjects.ts @@ -1,11 +1,14 @@ +import type { Constructor } from "@ark/util" import type { SchemaModule } from "../module.js" import { schemaScope } from "../scope.js" +// ECMAScript Objects +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects export interface jsObjectExports { Array: Array - Function: Function Date: Date Error: Error + Function: Function Map: Map RegExp: RegExp Set: Set @@ -19,15 +22,15 @@ export type jsObjects = SchemaModule export const jsObjects: jsObjects = schemaScope( { Array, - Function, Date, Error, + Function, Map, RegExp, Set, WeakMap, WeakSet, Promise - }, + } satisfies { [k in keyof jsObjectExports]: Constructor }, { prereducedAliases: true, intrinsic: true } ).export() diff --git a/ark/schema/keywords/keywords.ts b/ark/schema/keywords/keywords.ts index 81d33a6789..09688fbc88 100644 --- a/ark/schema/keywords/keywords.ts +++ b/ark/schema/keywords/keywords.ts @@ -1,25 +1,32 @@ import type { RootModule, SchemaModule } from "../module.js" -import { schemaScope, type RootScope } from "../scope.js" +import { schemaScope, type BaseScope } from "../scope.js" // the import ordering here is important so builtin keywords can be resolved // and used to bootstrap nodes with constraints import { tsKeywords, type tsKeywordExports } from "./tsKeywords.js" +import { $ark } from "@ark/util" import { formatting, type formattingExports } from "./format.js" import { internal, type internalExports } from "./internal.js" import { jsObjects, type jsObjectExports } from "./jsObjects.js" import { parsing, type parsingExports } from "./parsing.js" +import { + platformObjects, + type platformObjectExports +} from "./platformObjects.js" import { tsGenerics, type tsGenericsExports } from "./tsGenerics.js" +import { typedArray, type typedArrayExports } from "./typedArray.js" import { validation, type validationExports } from "./validation.js" -export const ambientRootScope: RootScope = schemaScope({ +export const ambientRootScope: BaseScope = schemaScope({ ...tsKeywords, ...jsObjects, + ...platformObjects, ...validation, ...internal, ...tsGenerics, + TypedArray: typedArray, parse: parsing, format: formatting - // TODO: remove cast }) as never $ark.ambient = ambientRootScope.internal @@ -31,9 +38,11 @@ export const keywordNodes: SchemaModule = ambientRootScope.export() export interface Ark extends tsKeywordExports, jsObjectExports, + platformObjectExports, validationExports, tsGenericsExports, internalExports { + TypedArray: RootModule parse: RootModule format: RootModule } diff --git a/ark/schema/keywords/parsing.ts b/ark/schema/keywords/parsing.ts index 6ac06195b2..1c3970b10b 100644 --- a/ark/schema/keywords/parsing.ts +++ b/ark/schema/keywords/parsing.ts @@ -60,12 +60,49 @@ const date = defineRoot({ } }) +export type FormDataValue = string | File + +export type ParsedFormData = Record + +export const parse = (data: FormData): ParsedFormData => { + const result: ParsedFormData = {} + for (const [k, v] of data) { + if (k in result) { + const existing = result[k] + if (typeof existing === "string" || existing instanceof File) + result[k] = [existing, v] + else existing.push(v) + } else result[k] = v + } + return result +} + +// support Node18 +const File = globalThis.File ?? Blob + +const formData = defineRoot({ + in: FormData, + morphs: (data: FormData): ParsedFormData => { + const result: ParsedFormData = {} + for (const [k, v] of data) { + if (k in result) { + const existing = result[k] + if (typeof existing === "string" || existing instanceof File) + result[k] = [existing, v] + else existing.push(v) + } else result[k] = v + } + return result + } +}) + export type parsingExports = { url: (In: string) => Out number: (In: string) => Out integer: (In: string) => Out> date: (In: string) => Out json: (In: string) => Out + formData: (In: FormData) => Out } export type parsing = SchemaModule @@ -75,5 +112,6 @@ export const parsing: parsing = schemaScope({ number, integer, date, - json + json, + formData }).export() diff --git a/ark/schema/keywords/platformObjects.ts b/ark/schema/keywords/platformObjects.ts new file mode 100644 index 0000000000..5a4b29afae --- /dev/null +++ b/ark/schema/keywords/platformObjects.ts @@ -0,0 +1,36 @@ +import type { Constructor } from "@ark/util" +import type { SchemaModule } from "../module.js" +import { schemaScope } from "../scope.js" + +// Platform APIs +// See https://developer.mozilla.org/en-US/docs/Web/API +// Must be implemented in Node etc. as well as the browser to include here +export interface platformObjectExports { + ArrayBuffer: ArrayBuffer + Blob: Blob + File: File + FormData: FormData + Headers: Headers + Request: Request + Response: Response + URL: URL +} + +export type platformObjects = SchemaModule + +export const platformObjects: platformObjects = schemaScope( + { + ArrayBuffer, + Blob, + // support Node18 + File: globalThis.File ?? Blob, + FormData, + Headers, + Request, + Response, + URL + } satisfies { + [k in keyof platformObjectExports]: Constructor + }, + { prereducedAliases: true } +).export() diff --git a/ark/schema/keywords/tsGenerics.ts b/ark/schema/keywords/tsGenerics.ts index 888452b44f..810e74e202 100644 --- a/ark/schema/keywords/tsGenerics.ts +++ b/ark/schema/keywords/tsGenerics.ts @@ -1,30 +1,83 @@ -import type { Key } from "@ark/util" -import type { GenericRoot } from "../generic.js" +import { + $ark, + liftArray, + type conform, + type Hkt, + type Key, + type show +} from "@ark/util" import type { SchemaModule } from "../module.js" -import { generic, schemaScope, type RootScope } from "../scope.js" - -export interface tsGenericsExports<$ = {}> { - Record: GenericRoot< - [["K", Key], ["V", unknown]], - { - "[K]": "V" - }, - // as long as the generics in the root scope don't reference one - // another, they shouldn't need a bound local scope - $ - > +import type { Out } from "../roots/morph.js" +import { generic, schemaScope } from "../scope.js" + +class ArkRecord extends generic( + ["K", $ark.intrinsic.propertyKey], + "V" +)(args => ({ + domain: "object", + index: { + signature: args.K, + value: args.V + } +})) { + declare hkt: ( + args: conform + ) => Record<(typeof args)[0], (typeof args)[1]> +} + +class ArkPick extends generic( + ["T", $ark.intrinsic.object], + ["K", $ark.intrinsic.propertyKey] +)(args => args.T.pick(args.K as never)) { + declare hkt: ( + args: conform + ) => show> +} + +class ArkOmit extends generic( + ["T", $ark.intrinsic.object], + ["K", $ark.intrinsic.propertyKey] +)(args => args.T.omit(args.K as never)) { + declare hkt: ( + args: conform + ) => show> +} + +class ArkExclude extends generic("T", "U")(args => args.T.exclude(args.U)) { + declare hkt: ( + args: conform + ) => Exclude<(typeof args)[0], (typeof args)[1]> } +class ArkExtract extends generic("T", "U")(args => args.T.extract(args.U)) { + declare hkt: ( + args: conform + ) => Extract<(typeof args)[0], (typeof args)[1]> +} + +class ArkLiftArray extends generic("T")(args => + args.T.or(args.T.array()).pipe(liftArray) +) { + declare hkt: ( + args: conform + ) => liftArray<(typeof args)[0]> extends infer lifted ? + (In: (typeof args)[0] | lifted) => Out + : never +} + +const tsGenericsExports = { + Record: new ArkRecord(), + Pick: new ArkPick(), + Omit: new ArkOmit(), + Exclude: new ArkExclude(), + Extract: new ArkExtract(), + liftArray: new ArkLiftArray() +} + +export type tsGenericsExports = typeof tsGenericsExports + export type tsGenerics = SchemaModule -const $: RootScope = schemaScope({ - Record: generic([["K", $ark.intrinsic.propertyKey], "V"])(args => ({ - domain: "object", - index: { - signature: args.K, - value: args.V - } - })) -}) +const $ = schemaScope(tsGenericsExports) export const tsGenerics: tsGenerics = $.export() diff --git a/ark/schema/keywords/tsKeywords.ts b/ark/schema/keywords/tsKeywords.ts index ced50374dd..0ba9ec90f3 100644 --- a/ark/schema/keywords/tsKeywords.ts +++ b/ark/schema/keywords/tsKeywords.ts @@ -1,4 +1,4 @@ -import type { type } from "../inference.js" +import type { InferredRoot } from "../inference.js" import type { SchemaModule } from "../module.js" import { schemaScope } from "../scope.js" @@ -15,7 +15,6 @@ export interface tsKeywordExports { symbol: symbol true: true unknown: unknown - void: void undefined: undefined } @@ -23,10 +22,10 @@ export type tsKeywords = SchemaModule export const tsKeywords: tsKeywords = schemaScope( { - any: {} as type.cast, + any: {} as InferredRoot, bigint: "bigint", // since we know this won't be reduced, it can be safely cast to a union - boolean: [{ unit: false }, { unit: true }] as type.cast, + boolean: [{ unit: false }, { unit: true }] as InferredRoot, false: { unit: false }, never: [], null: { unit: null }, @@ -36,8 +35,9 @@ export const tsKeywords: tsKeywords = schemaScope( symbol: "symbol", true: { unit: true }, unknown: {}, - void: { unit: undefined } as type.cast, undefined: { unit: undefined } + // void is not included because it doesn't have a well-defined meaning + // as a standalone type }, { prereducedAliases: true, intrinsic: true } ).export() diff --git a/ark/schema/keywords/typedArray.ts b/ark/schema/keywords/typedArray.ts new file mode 100644 index 0000000000..a065d51247 --- /dev/null +++ b/ark/schema/keywords/typedArray.ts @@ -0,0 +1,39 @@ +import type { Constructor } from "@ark/util" +import type { SchemaModule } from "../module.js" +import { schemaScope } from "../scope.js" + +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray +export interface typedArrayExports { + Int8: Int8Array + Uint8: Uint8Array + Uint8Clamped: Uint8ClampedArray + Int16: Int16Array + Uint16: Uint16Array + Int32: Int32Array + Uint32: Uint32Array + Float32: Float32Array + Float64: Float64Array + BigInt64: BigInt64Array + BigUint64: BigUint64Array +} + +export type typedArray = SchemaModule + +export const typedArray: typedArray = schemaScope( + { + Int8: Int8Array, + Uint8: Uint8Array, + Uint8Clamped: Uint8ClampedArray, + Int16: Int16Array, + Uint16: Uint16Array, + Int32: Int32Array, + Uint32: Uint32Array, + Float32: Float32Array, + Float64: Float64Array, + BigInt64: BigInt64Array, + BigUint64: BigUint64Array + } satisfies { + [k in keyof typedArrayExports]: Constructor + }, + { prereducedAliases: true } +).export() diff --git a/ark/schema/kinds.ts b/ark/schema/kinds.ts index 514cb12451..8b264a0c43 100644 --- a/ark/schema/kinds.ts +++ b/ark/schema/kinds.ts @@ -1,4 +1,10 @@ -import { envHasCsp, flatMorph, type array, type listable } from "@ark/util" +import { + $ark, + envHasCsp, + flatMorph, + type array, + type listable +} from "@ark/util" import type { ResolvedArkConfig } from "./config.js" import type { BaseNode } from "./node.js" import { @@ -57,7 +63,7 @@ import { unitImplementation, type UnitDeclaration } from "./roots/unit.js" -import type { RawRootScope } from "./scope.js" +import type { InternalBaseScope } from "./scope.js" import type { ConstraintKind, NodeKind, @@ -147,7 +153,7 @@ $ark.defaultConfig = Object.assign( export const nodeClassesByKind: Record< NodeKind, - new (attachments: UnknownAttachments, $: RawRootScope) => BaseNode + new (attachments: UnknownAttachments, $: InternalBaseScope) => BaseNode > = { ...boundClassesByKind, alias: AliasNode, diff --git a/ark/schema/module.ts b/ark/schema/module.ts index c3dc26cadd..5cb68f827e 100644 --- a/ark/schema/module.ts +++ b/ark/schema/module.ts @@ -1,5 +1,5 @@ import { DynamicBase, type anyOrNever } from "@ark/util" -import type { Root } from "./roots/root.js" +import type { SchemaRoot } from "./roots/root.js" import { arkKind } from "./shared/utils.js" export type PreparsedNodeResolution = { @@ -18,9 +18,9 @@ export class RootModule< type exportSchemaScope<$> = { [k in keyof $]: $[k] extends PreparsedNodeResolution ? [$[k]] extends [anyOrNever] ? - Root<$[k], $> + SchemaRoot<$[k], $> : $[k] - : Root<$[k], $> + : SchemaRoot<$[k], $> } export const SchemaModule: new <$ = {}>( diff --git a/ark/schema/node.ts b/ark/schema/node.ts index 5ae1161ddb..e1cf656c62 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -1,4 +1,5 @@ import { + $ark, Callable, appendUnique, cached, @@ -22,11 +23,11 @@ import type { NodeParseOptions } from "./parse.js" import type { MorphNode } from "./roots/morph.js" import type { BaseRoot, Root } from "./roots/root.js" import type { UnitNode } from "./roots/unit.js" -import type { RawRootScope } from "./scope.js" +import type { InternalBaseScope } from "./scope.js" import type { NodeCompiler } from "./shared/compile.js" import type { BaseMeta, - RawNodeDeclaration, + BaseNodeDeclaration, attachmentsOf } from "./shared/declare.js" import { @@ -55,11 +56,11 @@ export type UnknownNode = BaseNode | Root export abstract class BaseNode< /** uses -ignore rather than -expect-error because this is not an error in .d.ts * @ts-ignore allow instantiation assignment to the base type */ - out d extends RawNodeDeclaration = RawNodeDeclaration + out d extends BaseNodeDeclaration = BaseNodeDeclaration > extends Callable<(data: d["prerequisite"]) => unknown, attachmentsOf> { constructor( public attachments: UnknownAttachments, - public $: RawRootScope + public $: InternalBaseScope ) { super( // pipedFromCtx allows us internally to reuse TraversalContext @@ -85,7 +86,7 @@ export abstract class BaseNode< ) } - bindScope($: RawRootScope): this { + bindScope($: InternalBaseScope): this { if (this.$ === $) return this as never return new (this.constructor as any)(this.attachments, $) } @@ -395,21 +396,21 @@ export abstract class BaseNode< /** a literal key (named property) or a node (index signatures) representing part of a type structure */ export type TypeKey = Key | BaseRoot -export type TypePath = array +export type TypeIndexer = TypeKey | number export type FlatRef = { - path: TypePath + path: array node: root propString: string } -export const typePathToPropString = (path: Readonly) => +export const typePathToPropString = (path: array) => pathToPropString(path, { stringifyNonKey: node => node.expression }) export const flatRef = ( - path: TypePath, + path: array, node: node ): FlatRef => ({ path, @@ -438,7 +439,7 @@ export const appendUniqueNodes = ( export type DeepNodeTransformOptions = { shouldTransform?: ShouldTransformFn - bindScope?: RawRootScope + bindScope?: InternalBaseScope prereduced?: boolean } @@ -448,7 +449,7 @@ export type ShouldTransformFn = ( ) => boolean export interface DeepNodeTransformContext extends DeepNodeTransformOptions { - path: mutable + path: mutable> seen: { [originalId: string]: (() => BaseNode | undefined) | undefined } parseOptions: NodeParseOptions } diff --git a/ark/schema/package.json b/ark/schema/package.json index 132dfa0d1c..4946cfba84 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/parse.ts b/ark/schema/parse.ts index 9fc8ed5868..d525bfeb7d 100644 --- a/ark/schema/parse.ts +++ b/ark/schema/parse.ts @@ -19,7 +19,7 @@ import { type NormalizedSchema } from "./kinds.js" import type { BaseNode } from "./node.js" -import type { RawRootScope } from "./scope.js" +import type { InternalBaseScope } from "./scope.js" import { Disjoint } from "./shared/disjoint.js" import { constraintKeys, @@ -46,7 +46,7 @@ export type NodeParseOptions = { export interface NodeParseContext extends NodeParseOptions { - $: RawRootScope + $: InternalBaseScope args: GenericArgResolutions schema: NormalizedSchema id: string @@ -113,7 +113,7 @@ export const parseNode = ( id: string, kind: kind, schema: NormalizedSchema, - $: RawRootScope, + $: InternalBaseScope, opts: NodeParseOptions ): BaseNode => { const ctx: NodeParseContext = { diff --git a/ark/schema/predicate.ts b/ark/schema/predicate.ts index ca3ea7bc57..18ff547ecc 100644 --- a/ark/schema/predicate.ts +++ b/ark/schema/predicate.ts @@ -1,4 +1,3 @@ -import { registeredReference, type RegisteredReference } from "@ark/util" import type { constrain, of } from "./ast.js" import { BaseConstraint } from "./constraint.js" import type { errorContext } from "./kinds.js" @@ -9,6 +8,10 @@ import { implementNode, type nodeImplementationOf } from "./shared/implement.js" +import { + type RegisteredReference, + registeredReference +} from "./shared/registry.js" import type { TraversalContext, TraverseAllows, diff --git a/ark/schema/refinements/after.ts b/ark/schema/refinements/after.ts index 5265ac7ef2..bf0e6c4fe9 100644 --- a/ark/schema/refinements/after.ts +++ b/ark/schema/refinements/after.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { @@ -67,7 +68,7 @@ export const afterImplementation: nodeImplementationOf = }) export class AfterNode extends BaseRange { - impliedBasis: BaseRoot = $ark.intrinsic.Date + impliedBasis: BaseRoot = $ark.intrinsic.Date.internal traverseAllows: TraverseAllows = this.exclusive ? data => data > this.rule : data => data >= this.rule diff --git a/ark/schema/refinements/before.ts b/ark/schema/refinements/before.ts index dc640c1bb4..5e2bd336e9 100644 --- a/ark/schema/refinements/before.ts +++ b/ark/schema/refinements/before.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -77,5 +78,5 @@ export class BeforeNode extends BaseRange { traverseAllows: TraverseAllows = this.exclusive ? data => data < this.rule : data => data <= this.rule - impliedBasis: BaseRoot = $ark.intrinsic.Date + impliedBasis: BaseRoot = $ark.intrinsic.Date.internal } diff --git a/ark/schema/refinements/divisor.ts b/ark/schema/refinements/divisor.ts index 3755a7e67c..b44c833b45 100644 --- a/ark/schema/refinements/divisor.ts +++ b/ark/schema/refinements/divisor.ts @@ -1,8 +1,9 @@ +import { $ark } from "@ark/util" import { - RawPrimitiveConstraint, + InternalPrimitiveConstraint, writeInvalidOperandMessage } from "../constraint.js" -import type { BaseRoot, Root } from "../roots/root.js" +import type { BaseRoot, SchemaRoot } from "../roots/root.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { implementNode, @@ -50,21 +51,21 @@ export const divisorImplementation: nodeImplementationOf = } }) -export class DivisorNode extends RawPrimitiveConstraint { +export class DivisorNode extends InternalPrimitiveConstraint { traverseAllows: TraverseAllows = data => data % this.rule === 0 readonly compiledCondition: string = `data % ${this.rule} === 0` readonly compiledNegation: string = `data % ${this.rule} !== 0` - readonly impliedBasis: BaseRoot = $ark.intrinsic.number + readonly impliedBasis: BaseRoot = $ark.intrinsic.number.internal readonly expression: string = `% ${this.rule}` } -export const writeIndivisibleMessage = ( +export const writeIndivisibleMessage = ( t: node ): writeIndivisibleMessage => writeInvalidOperandMessage("divisor", $ark.intrinsic.number as never, t) -export type writeIndivisibleMessage = +export type writeIndivisibleMessage = writeInvalidOperandMessage<"divisor", node> // https://en.wikipedia.org/wiki/Euclidean_algorithm diff --git a/ark/schema/refinements/exactLength.ts b/ark/schema/refinements/exactLength.ts index 4ba10600f5..7f94990f23 100644 --- a/ark/schema/refinements/exactLength.ts +++ b/ark/schema/refinements/exactLength.ts @@ -1,4 +1,5 @@ -import { RawPrimitiveConstraint } from "../constraint.js" +import { $ark } from "@ark/util" +import { InternalPrimitiveConstraint } from "../constraint.js" import type { BaseRoot } from "../roots/root.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -67,12 +68,12 @@ export const exactLengthImplementation: nodeImplementationOf { +export class ExactLengthNode extends InternalPrimitiveConstraint { traverseAllows: TraverseAllows = data => data.length === this.rule readonly compiledCondition: string = `data.length === ${this.rule}` readonly compiledNegation: string = `data.length !== ${this.rule}` - readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable + readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable.internal readonly expression: string = `{ length: ${this.rule} }` } diff --git a/ark/schema/refinements/max.ts b/ark/schema/refinements/max.ts index 0543f4c953..40117bcf4c 100644 --- a/ark/schema/refinements/max.ts +++ b/ark/schema/refinements/max.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -60,7 +61,7 @@ export const maxImplementation: nodeImplementationOf = }) export class MaxNode extends BaseRange { - impliedBasis: BaseRoot = $ark.intrinsic.number + impliedBasis: BaseRoot = $ark.intrinsic.number.internal traverseAllows: TraverseAllows = this.exclusive ? data => data < this.rule : data => data <= this.rule diff --git a/ark/schema/refinements/maxLength.ts b/ark/schema/refinements/maxLength.ts index fb7e343c94..e21ae0ef00 100644 --- a/ark/schema/refinements/maxLength.ts +++ b/ark/schema/refinements/maxLength.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -65,7 +66,7 @@ export const maxLengthImplementation: nodeImplementationOf }) export class MaxLengthNode extends BaseRange { - readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable + readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable.internal traverseAllows: TraverseAllows = this.exclusive ? diff --git a/ark/schema/refinements/min.ts b/ark/schema/refinements/min.ts index a4c26ba53d..a296f1bd34 100644 --- a/ark/schema/refinements/min.ts +++ b/ark/schema/refinements/min.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { @@ -53,7 +54,7 @@ export const minImplementation: nodeImplementationOf = }) export class MinNode extends BaseRange { - readonly impliedBasis: BaseRoot = $ark.intrinsic.number + readonly impliedBasis: BaseRoot = $ark.intrinsic.number.internal traverseAllows: TraverseAllows = this.exclusive ? data => data > this.rule : data => data >= this.rule diff --git a/ark/schema/refinements/minLength.ts b/ark/schema/refinements/minLength.ts index 474a4fc0e8..3ef845f1d8 100644 --- a/ark/schema/refinements/minLength.ts +++ b/ark/schema/refinements/minLength.ts @@ -1,3 +1,4 @@ +import { $ark } from "@ark/util" import type { BaseRoot } from "../roots/root.js" import type { declareNode } from "../shared/declare.js" import { @@ -62,7 +63,7 @@ export const minLengthImplementation: nodeImplementationOf }) export class MinLengthNode extends BaseRange { - readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable + readonly impliedBasis: BaseRoot = $ark.intrinsic.lengthBoundable.internal traverseAllows: TraverseAllows = this.exclusive ? diff --git a/ark/schema/refinements/pattern.ts b/ark/schema/refinements/pattern.ts index d411c831a6..8e8d4136ee 100644 --- a/ark/schema/refinements/pattern.ts +++ b/ark/schema/refinements/pattern.ts @@ -1,4 +1,5 @@ -import { RawPrimitiveConstraint } from "../constraint.js" +import { $ark } from "@ark/util" +import { InternalPrimitiveConstraint } from "../constraint.js" import type { BaseRoot } from "../roots/root.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { @@ -53,7 +54,7 @@ export const patternImplementation: nodeImplementationOf = } }) -export class PatternNode extends RawPrimitiveConstraint { +export class PatternNode extends InternalPrimitiveConstraint { readonly instance: RegExp = new RegExp(this.rule, this.flags) readonly expression: string = `${this.instance}` traverseAllows: (string: string) => boolean = this.instance.test.bind( @@ -62,5 +63,5 @@ export class PatternNode extends RawPrimitiveConstraint { readonly compiledCondition: string = `${this.expression}.test(data)` readonly compiledNegation: string = `!${this.compiledCondition}` - readonly impliedBasis: BaseRoot = $ark.intrinsic.string + readonly impliedBasis: BaseRoot = $ark.intrinsic.string.internal } diff --git a/ark/schema/refinements/range.ts b/ark/schema/refinements/range.ts index bbb507266a..919fee3657 100644 --- a/ark/schema/refinements/range.ts +++ b/ark/schema/refinements/range.ts @@ -1,10 +1,10 @@ import { type array, isKeyOf, type propValueOf, type satisfy } from "@ark/util" -import { RawPrimitiveConstraint } from "../constraint.js" +import { InternalPrimitiveConstraint } from "../constraint.js" import type { Node } from "../kinds.js" -import type { BaseMeta, RawNodeDeclaration } from "../shared/declare.js" +import type { BaseMeta, BaseNodeDeclaration } from "../shared/declare.js" import type { KeySchemaDefinitions, RangeKind } from "../shared/implement.js" -export interface BaseRangeDeclaration extends RawNodeDeclaration { +export interface BaseRangeDeclaration extends BaseNodeDeclaration { kind: RangeKind inner: BaseRangeInner normalizedSchema: UnknownNormalizedRangeSchema @@ -12,7 +12,7 @@ export interface BaseRangeDeclaration extends RawNodeDeclaration { export abstract class BaseRange< d extends BaseRangeDeclaration -> extends RawPrimitiveConstraint { +> extends InternalPrimitiveConstraint { readonly boundOperandKind: OperandKindsByBoundKind[d["kind"]] = operandKindsByBoundKind[this.kind] readonly compiledActual: string = diff --git a/ark/schema/roots/alias.ts b/ark/schema/roots/alias.ts index 52811106ac..ac8909a6bc 100644 --- a/ark/schema/roots/alias.ts +++ b/ark/schema/roots/alias.ts @@ -1,4 +1,4 @@ -import { append, cached, domainDescriptions } from "@ark/util" +import { $ark, append, cached, domainDescriptions } from "@ark/util" import type { NodeCompiler } from "../shared/compile.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -8,7 +8,7 @@ import { } from "../shared/implement.js" import { intersectNodes } from "../shared/intersections.js" import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" -import { BaseRoot, type RawRootDeclaration } from "./root.js" +import { BaseRoot, type InternalRootDeclaration } from "./root.js" import { defineRightwardIntersections } from "./utils.js" export interface AliasInner extends BaseMeta { @@ -37,7 +37,7 @@ export class AliasNode extends BaseRoot { return this.resolve?.() ?? this.$.resolveRoot(this.alias) } - rawKeyOf(): BaseRoot { + rawKeyOf(): BaseRoot { return this.resolution.keyof() } @@ -101,4 +101,4 @@ export const aliasImplementation: nodeImplementationOf = }) const neverIfDisjoint = (result: BaseRoot | Disjoint): BaseRoot => - result instanceof Disjoint ? $ark.intrinsic.never : result + result instanceof Disjoint ? $ark.intrinsic.never.internal : result diff --git a/ark/schema/roots/basis.ts b/ark/schema/roots/basis.ts index 1f9e1f1cc5..5fffa72214 100644 --- a/ark/schema/roots/basis.ts +++ b/ark/schema/roots/basis.ts @@ -2,10 +2,10 @@ import type { array, Key } from "@ark/util" import type { NodeCompiler } from "../shared/compile.js" import { compileErrorContext } from "../shared/implement.js" import type { TraverseApply } from "../shared/traversal.js" -import { BaseRoot, type RawRootDeclaration } from "./root.js" +import { BaseRoot, type InternalRootDeclaration } from "./root.js" -export abstract class RawBasis< - d extends RawRootDeclaration = RawRootDeclaration +export abstract class InternalBasis< + d extends InternalRootDeclaration = InternalRootDeclaration > extends BaseRoot { abstract compiledCondition: string abstract compiledNegation: string diff --git a/ark/schema/roots/domain.ts b/ark/schema/roots/domain.ts index cf745bb3fc..6009a6fd76 100644 --- a/ark/schema/roots/domain.ts +++ b/ark/schema/roots/domain.ts @@ -13,7 +13,7 @@ import { type nodeImplementationOf } from "../shared/implement.js" import type { TraverseAllows } from "../shared/traversal.js" -import { RawBasis } from "./basis.js" +import { InternalBasis } from "./basis.js" export interface DomainInner< domain extends NonEnumerableDomain = NonEnumerableDomain @@ -35,7 +35,7 @@ export interface DomainDeclaration errorContext: DomainInner }> {} -export class DomainNode extends RawBasis { +export class DomainNode extends InternalBasis { traverseAllows: TraverseAllows = data => domainOf(data) === this.domain readonly compiledCondition: string = diff --git a/ark/schema/roots/intersection.ts b/ark/schema/roots/intersection.ts index bfda46d401..d229aa0cad 100644 --- a/ark/schema/roots/intersection.ts +++ b/ark/schema/roots/intersection.ts @@ -1,4 +1,5 @@ import { + $ark, flatMorph, hasDomain, isEmptyObject, @@ -182,7 +183,7 @@ export class IntersectionNode extends BaseRoot { this.structure ? this.basis.rawKeyOf().or(this.structure.keyof()) : this.basis.rawKeyOf() - : this.structure?.keyof() ?? $ark.intrinsic.never + : this.structure?.keyof() ?? $ark.intrinsic.never.internal ) } } diff --git a/ark/schema/roots/morph.ts b/ark/schema/roots/morph.ts index c51e204f97..a300cef59b 100644 --- a/ark/schema/roots/morph.ts +++ b/ark/schema/roots/morph.ts @@ -1,13 +1,13 @@ import { + $ark, arrayEquals, - arrayFrom, - registeredReference, + liftArray, throwParseError, type array, type listable } from "@ark/util" import type { distillConstrainableIn } from "../ast.js" -import type { type } from "../inference.js" +import type { InferredRoot } from "../inference.js" import type { Node, NodeSchema } from "../kinds.js" import type { NodeCompiler } from "../shared/compile.js" import type { BaseMeta, declareNode } from "../shared/declare.js" @@ -18,6 +18,7 @@ import { type nodeImplementationOf } from "../shared/implement.js" import { intersectNodes, type inferPipe } from "../shared/intersections.js" +import { registeredReference } from "../shared/registry.js" import type { TraversalContext, TraverseAllows, @@ -76,7 +77,7 @@ export const morphImplementation: nodeImplementationOf = parse: (schema, ctx) => ctx.$.node(morphChildKinds, schema) }, morphs: { - parse: arrayFrom, + parse: liftArray, serialize: morphs => morphs.map(m => hasArkKind(m, "root") ? m.json : registeredReference(m) @@ -163,7 +164,7 @@ export class MorphNode extends BaseRoot { } override get out(): BaseRoot { - return this.validatedOut ?? $ark.intrinsic.unknown + return this.validatedOut ?? $ark.intrinsic.unknown.internal } /** Check if the morphs of r are equal to those of this node */ @@ -200,7 +201,7 @@ Right: ${rDescription}` export type inferPipes = pipes extends [infer head extends Morph, ...infer tail extends Morph[]] ? inferPipes< - pipes[0] extends type.cast ? inferPipe + pipes[0] extends InferredRoot ? inferPipe : inferMorphOut extends infer out ? (In: distillConstrainableIn) => Out : never, diff --git a/ark/schema/roots/proto.ts b/ark/schema/roots/proto.ts index 016c31656a..282e147938 100644 --- a/ark/schema/roots/proto.ts +++ b/ark/schema/roots/proto.ts @@ -1,4 +1,5 @@ import { + $ark, builtinConstructors, constructorExtends, getExactBuiltinConstructorName, @@ -18,7 +19,7 @@ import { type nodeImplementationOf } from "../shared/implement.js" import type { TraverseAllows } from "../shared/traversal.js" -import { RawBasis } from "./basis.js" +import { InternalBasis } from "./basis.js" import type { DomainNode } from "./domain.js" export interface ProtoInner @@ -90,7 +91,7 @@ export const protoImplementation: nodeImplementationOf = } }) -export class ProtoNode extends RawBasis { +export class ProtoNode extends InternalBasis { builtinName: BuiltinObjectKind | null = getExactBuiltinConstructorName( this.proto ) diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 42779251d5..e24c7aadb1 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -1,4 +1,5 @@ import type { + ConstraintKind, DivisorSchema, ExactLengthSchema, ExclusiveDateRangeSchema, @@ -6,23 +7,29 @@ import type { FlatRef, InclusiveDateRangeSchema, InclusiveNumericRangeSchema, + InferredRoot, LimitSchemaValue, PatternSchema, + TypeIndexer, TypeKey, - TypePath, - UnknownRangeSchema + UnknownRangeSchema, + writeInvalidOperandMessage } from "@ark/schema" import { + $ark, cached, includes, omit, - printable, throwParseError, type Callable, + type ErrorMessage, type Json, - type Key, + type NonEmptyList, + type anyOrNever, type array, - type conform + type conform, + type typeToString, + type unset } from "@ark/util" import type { constrain, @@ -35,11 +42,16 @@ import { throwInvalidOperandError, type PrimitiveConstraintKind } from "../constraint.js" -import type { Node, NodeSchema, reducibleKindOf } from "../kinds.js" +import type { + Node, + NodeSchema, + Prerequisite, + reducibleKindOf +} from "../kinds.js" import { BaseNode, appendUniqueFlatRefs } from "../node.js" import type { Predicate } from "../predicate.js" -import type { RootScope } from "../scope.js" -import type { BaseMeta, RawNodeDeclaration } from "../shared/declare.js" +import type { BaseScope } from "../scope.js" +import type { BaseMeta, BaseNodeDeclaration } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" import { ArkErrors } from "../shared/errors.js" import { @@ -56,25 +68,27 @@ import { import { arkKind, hasArkKind, - type inferred, + inferred, type internalImplementationOf } from "../shared/utils.js" import type { StructureInner, - UndeclaredKeyBehavior + UndeclaredKeyBehavior, + arkKeyOf, + getArkKey } from "../structure/structure.js" import type { constraintKindOf } from "./intersection.js" import type { Morph, MorphNode, inferMorphOut, inferPipes } from "./morph.js" import type { UnionChildKind, UnionChildNode } from "./union.js" -export interface RawRootDeclaration extends RawNodeDeclaration { +export interface InternalRootDeclaration extends BaseNodeDeclaration { kind: RootKind } -export type UnknownRoot = Root | BaseRoot +export type UnknownRoot = SchemaRoot | BaseRoot export type TypeOnlyRootKey = - | (keyof Root & symbol) + | (keyof SchemaRoot & symbol) | "infer" | "inferIn" | "t" @@ -84,12 +98,14 @@ export type TypeOnlyRootKey = export abstract class BaseRoot< /** uses -ignore rather than -expect-error because this is not an error in .d.ts * @ts-ignore allow instantiation assignment to the base type */ - out d extends RawRootDeclaration = RawRootDeclaration + out d extends InternalRootDeclaration = InternalRootDeclaration > extends BaseNode // don't require intersect so we can make it protected to ensure it is not called internally - implements internalImplementationOf + implements + internalImplementationOf { + [inferred]?: unknown readonly branches: readonly Node[] = this.hasKind("union") ? this.inner.branches : [this as never] @@ -99,6 +115,10 @@ export abstract class BaseRoot< return this } + as(): this { + return this + } + abstract rawKeyOf(): BaseRoot abstract get shortDescription(): string @@ -118,6 +138,14 @@ export abstract class BaseRoot< return intersectNodesRoot(this, rNode, this.$) as never } + isUnknown(): boolean { + return this.hasKind("intersection") && this.children.length === 0 + } + + isNever(): boolean { + return this.hasKind("union") && this.children.length === 0 + } + and(r: unknown): BaseRoot { const result = this.intersect(r) return result instanceof Disjoint ? result.throw() : (result as never) @@ -134,21 +162,70 @@ export abstract class BaseRoot< return result instanceof ArkErrors ? result.throw() : result } - get(...[key, ...tail]: TypePath): BaseRoot { - if (key === undefined) return this - if (hasArkKind(key, "root") && key.hasKind("unit")) key = key.unit as Key - if (typeof key === "number") key = `${key}` + pick(...keys: array): BaseRoot { + return this.filterKeys("pick", keys) + } + + omit(...keys: array): BaseRoot { + return this.filterKeys("omit", keys) + } + + private filterKeys( + operation: "pick" | "omit", + keys: array + ): BaseRoot { + if (this.hasKind("union")) { + return this.$.schema( + this.branches.map(branch => branch[operation](...keys)) + ) + } + + if (this.hasKind("morph")) { + return this.$.node("morph", { + ...this.inner, + in: this.in[operation](...keys) + }) + } + + if (this.hasKind("intersection")) { + if (!this.inner.structure) { + throwParseError( + writeNonStructuralOperandMessage(operation, this.expression) + ) + } + + return this.$.node("intersection", { + ...this.inner, + structure: this.inner.structure[operation](...keys) + }) + } + + if (this.isBasis() && this.domain === "object") + // if it's an object but has no Structure node, return an empty object + return $ark.intrinsic.object.internal.bindScope(this.$) + + return throwParseError( + writeNonStructuralOperandMessage(operation, this.expression) + ) + } + + get(...path: array): BaseRoot { + if (path[0] === undefined) return this if (this.hasKind("union")) { return this.branches.reduce( - (acc, b) => acc.or(b.get(key, ...tail)), - $ark.intrinsic.never + (acc, b) => acc.or(b.get(...path)), + $ark.intrinsic.never.internal ) } + const branch = this as {} as UnionChildNode + return ( - (this as {} as UnionChildNode).structure?.get(key, ...tail) ?? - throwParseError(writeNonStructuralIndexAccessMessage(key)) + branch.structure?.get(...(path as NonEmptyList)) ?? + throwParseError( + writeNonStructuralOperandMessage("index access", this.expression) + ) ) } @@ -192,10 +269,6 @@ export abstract class BaseRoot< return r.extends(this as never) } - includes(r: unknown): boolean { - return hasArkKind(r, "root") ? r.extends(this) : this.allows(r) - } - configure(configOrDescription: BaseMeta | string): this { return this.configureShallowDescendants(configOrDescription) } @@ -402,7 +475,7 @@ export const exclusivizeRangeSchema = ( export type exclusivizeRangeSchema = schema extends LimitSchemaValue ? { rule: schema; exclusive: true } : schema -export declare abstract class InnerRoot extends Callable< +export declare abstract class Root extends Callable< (data: unknown) => distillOut | ArkErrors > { t: t @@ -417,9 +490,10 @@ export declare abstract class InnerRoot extends Callable< expression: string internal: BaseRoot - abstract $: RootScope<$>; + abstract $: BaseScope<$>; abstract get in(): unknown abstract get out(): unknown + abstract as(): unknown abstract keyof(): unknown abstract intersect(r: never): unknown | Disjoint abstract and(r: never): unknown @@ -430,9 +504,15 @@ export declare abstract class InnerRoot extends Callable< abstract exclude(r: never): unknown abstract extends(r: never): this is unknown abstract overlaps(r: never): boolean + abstract pick(...keys: never): unknown + abstract omit(...keys: never): unknown abstract array(): unknown abstract pipe(morph: Morph): unknown + isUnknown(): boolean + + isNever(): boolean + assert(data: unknown): this["infer"] allows(data: unknown): data is this["inferIn"] @@ -451,22 +531,53 @@ export declare abstract class InnerRoot extends Callable< // this is declared as a class internally so we can ensure all "abstract" // methods of BaseRoot are overridden, but we end up exporting it as an interface // to ensure it is not accessed as a runtime value -declare class _Root extends InnerRoot { - $: RootScope<$>; - - get in(): Root - - get out(): Root - - keyof(): Root +declare class _SchemaRoot extends Root { + $: BaseScope<$> + + as(...args: validateChainedAsArgs): SchemaRoot + + get in(): SchemaRoot + + get out(): SchemaRoot + + keyof(): SchemaRoot + + pick = never>( + this: validateStructuralOperand<"pick", this>, + ...keys: array> + ): SchemaRoot<{ [k in key]: getArkKey }, $> + + omit = never>( + this: validateStructuralOperand<"omit", this>, + ...keys: array> + ): SchemaRoot<{ [k in key]: getArkKey }, $> + + get>( + k1: k1 | InferredRoot + ): SchemaRoot, $> + get, k2 extends arkKeyOf>>( + k1: k1 | InferredRoot, + k2: k2 | InferredRoot + ): SchemaRoot, k2>, $> + get< + k1 extends arkKeyOf, + k2 extends arkKeyOf>, + k3 extends arkKeyOf, k2>> + >( + k1: k1 | InferredRoot, + k2: k2 | InferredRoot, + k3: k3 | InferredRoot + ): SchemaRoot, k2>, k3>, $> - intersect(r: r): Root> | Disjoint + intersect( + r: r + ): SchemaRoot> | Disjoint - and(r: r): Root> + and(r: r): SchemaRoot> - or(r: r): Root + or(r: r): SchemaRoot - array(): Root + array(): SchemaRoot constrain< kind extends PrimitiveConstraintKind, @@ -474,41 +585,41 @@ declare class _Root extends InnerRoot { >( kind: conform>, schema: schema - ): Root, $> + ): SchemaRoot, $> - equals(r: Root): this is Root + equals(r: SchemaRoot): this is SchemaRoot // TODO: i/o - extract(r: Root): Root - exclude(r: Root): Root + extract(r: SchemaRoot): SchemaRoot + exclude(r: SchemaRoot): SchemaRoot // add the extra inferred intersection so that a variable of Type // can be narrowed without other branches becoming never - extends(other: Root): this is Root & { [inferred]?: r } + extends(other: SchemaRoot): this is SchemaRoot & { [inferred]?: r } - pipe>(a: a): Root, $> + pipe>(a: a): SchemaRoot, $> pipe, b extends Morph>>( a: a, b: b - ): Root, $> + ): SchemaRoot, $> pipe< a extends Morph, b extends Morph>, c extends Morph> - >(a: a, b: b, c: c): Root, $> + >(a: a, b: b, c: c): SchemaRoot, $> pipe< a extends Morph, b extends Morph>, c extends Morph>, d extends Morph> - >(a: a, b: b, c: c, d: d): Root, $> + >(a: a, b: b, c: c, d: d): SchemaRoot, $> pipe< a extends Morph, b extends Morph>, c extends Morph>, d extends Morph>, e extends Morph> - >(a: a, b: b, c: c, d: d, e: e): Root, $> + >(a: a, b: b, c: c, d: d, e: e): SchemaRoot, $> pipe< a extends Morph, b extends Morph>, @@ -523,7 +634,7 @@ declare class _Root extends InnerRoot { d: d, e: e, f: f - ): Root, $> + ): SchemaRoot, $> pipe< a extends Morph, b extends Morph>, @@ -540,19 +651,32 @@ declare class _Root extends InnerRoot { e: e, f: f, g: g - ): Root, $> + ): SchemaRoot, $> - overlaps(r: Root): boolean + overlaps(r: SchemaRoot): boolean } -export const writeNonStructuralIndexAccessMessage = (key: TypeKey) => - `${printable(key)} cannot be accessed on ${this}, which has no structural keys` +export const typeOrTermExtends = (t: unknown, base: unknown) => + hasArkKind(base, "root") ? + hasArkKind(t, "root") ? t.extends(base) + : base.allows(t) + : hasArkKind(t, "root") ? t.hasUnit(base) + : base === t -export interface Root< +export type validateChainedAsArgs = + [t] extends [unset] ? + [t] extends [anyOrNever] ? + [] + : [ + ErrorMessage<"as requires an explicit type parameter like myType.as()"> + ] + : [] + +export interface SchemaRoot< /** @ts-expect-error allow instantiation assignment to the base type */ out t = unknown, $ = any -> extends _Root {} +> extends _SchemaRoot {} export type intersectRoot = [l, r] extends [r, l] ? l @@ -573,3 +697,35 @@ export type schemaKindRightOf = Extract< export type schemaKindOrRightOf = | kind | schemaKindRightOf + +export type validateStructuralOperand< + name extends StructuralOperationName, + t extends { inferIn: unknown } +> = + t["inferIn"] extends object ? t + : ErrorMessage< + writeNonStructuralOperandMessage> + > + +export type validateChainedConstraint< + kind extends ConstraintKind, + t extends { inferIn: unknown } +> = + t["inferIn"] extends Prerequisite ? t + : ErrorMessage>> + +export type StructuralOperationName = "pick" | "omit" | "index access" + +export type writeNonStructuralOperandMessage< + operation extends StructuralOperationName, + operand extends string +> = `${operation} operand must be an object (was ${operand})` + +export const writeNonStructuralOperandMessage = < + operation extends StructuralOperationName, + operand extends string +>( + operation: operation, + operand: operand +): writeNonStructuralOperandMessage => + `${operation} operand must be an object (was ${operand})` diff --git a/ark/schema/roots/union.ts b/ark/schema/roots/union.ts index 76ff0ed88a..80a587bf46 100644 --- a/ark/schema/roots/union.ts +++ b/ark/schema/roots/union.ts @@ -1,16 +1,14 @@ import { + $ark, appendUnique, arrayEquals, cached, - compileLiteralPropAccess, - compileSerializedValue, domainDescriptions, flatMorph, groupBy, isArray, isKeyOf, printable, - registeredReference, throwInternalError, throwParseError, type Domain, @@ -23,7 +21,11 @@ import { } from "@ark/util" import type { Node, NodeSchema } from "../kinds.js" import { typePathToPropString } from "../node.js" -import type { NodeCompiler } from "../shared/compile.js" +import { + compileLiteralPropAccess, + compileSerializedValue, + type NodeCompiler +} from "../shared/compile.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" import type { ArkError } from "../shared/errors.js" @@ -35,6 +37,7 @@ import { type nodeImplementationOf } from "../shared/implement.js" import { intersectNodes, intersectNodesRoot } from "../shared/intersections.js" +import { registeredReference } from "../shared/registry.js" import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" import { pathToPropString } from "../shared/utils.js" import type { DomainInner, DomainNode } from "./domain.js" @@ -196,7 +199,6 @@ export const unionImplementation: nodeImplementationOf = }) export class UnionNode extends BaseRoot { - isNever: boolean = this.branches.length === 0 isBoolean: boolean = this.branches.length === 2 && this.branches[0].hasUnit(false) && @@ -311,7 +313,7 @@ export class UnionNode extends BaseRoot { rawKeyOf(): BaseRoot { return this.branches.reduce( (result, branch) => result.and(branch.rawKeyOf()), - $ark.intrinsic.unknown + $ark.intrinsic.unknown.internal ) } diff --git a/ark/schema/roots/unit.ts b/ark/schema/roots/unit.ts index 1ace86090e..8fa01732f5 100644 --- a/ark/schema/roots/unit.ts +++ b/ark/schema/roots/unit.ts @@ -16,7 +16,7 @@ import { type nodeImplementationOf } from "../shared/implement.js" import type { TraverseAllows } from "../shared/traversal.js" -import { RawBasis } from "./basis.js" +import { InternalBasis } from "./basis.js" import { defineRightwardIntersections } from "./utils.js" export type UnitSchema = UnitInner @@ -71,7 +71,7 @@ export const unitImplementation: nodeImplementationOf = } }) -export class UnitNode extends RawBasis { +export class UnitNode extends InternalBasis { compiledValue: JsonPrimitive = (this.json as any).unit serializedValue: JsonPrimitive = typeof this.unit === "string" || this.unit instanceof Date ? diff --git a/ark/schema/scope.ts b/ark/schema/scope.ts index cb06df3156..6893ac99d7 100644 --- a/ark/schema/scope.ts +++ b/ark/schema/scope.ts @@ -1,5 +1,5 @@ import { - CompiledFunction, + $ark, DynamicBase, ParseError, bound, @@ -11,6 +11,7 @@ import { throwParseError, type Dict, type Json, + type anyOrNever, type array, type flattenListable, type show @@ -21,12 +22,10 @@ import { type ResolvedArkConfig } from "./config.js" import { - LazyGenericRoot, - parseGeneric, - type GenericParamDef, - type GenericRoot, - type LazyGenericSchema, - type genericParamSchemasToAst + GenericRoot, + LazyGenericBody, + type GenericHktSchemaParser, + type GenericParamDef } from "./generic.js" import type { inferRoot, validateRoot } from "./inference.js" import type { internal } from "./keywords/internal.js" @@ -52,8 +51,8 @@ import { type NodeParseOptions } from "./parse.js" import { normalizeAliasSchema, type AliasNode } from "./roots/alias.js" -import type { BaseRoot, Root } from "./roots/root.js" -import { NodeCompiler } from "./shared/compile.js" +import type { BaseRoot, SchemaRoot } from "./roots/root.js" +import { CompiledFunction, NodeCompiler } from "./shared/compile.js" import type { NodeKind, RootKind } from "./shared/implement.js" import type { TraverseAllows, TraverseApply } from "./shared/traversal.js" import { @@ -67,7 +66,7 @@ export type nodeResolutions = { [k in keyof keywords]: BaseRoot } export type BaseResolutions = Record -export type RawRootResolutions = Record +export type InternalResolutions = Record export type exportedNameOf<$> = Exclude @@ -83,23 +82,22 @@ export type resolveReference, $> = export type PrivateDeclaration = `#${key}` -type toRawScope<$> = RawRootScope<{ +type toInternalScope<$> = InternalBaseScope<{ [k in keyof $]: $[k] extends { [arkKind]: infer kind } ? - kind extends "generic" ? GenericRoot - : kind extends "module" ? RawRootModule + [$[k]] extends [anyOrNever] ? BaseRoot + : kind extends "generic" ? GenericRoot + : kind extends "module" ? InternalRootModule : never : BaseRoot }> // these allow builtin types to be accessed during parsing without cyclic imports // they are populated as each scope is parsed with `intrinsic` in its config -export type IntrinsicKeywords = { - [alias in keyof tsKeywords | keyof jsObjects | keyof internal]: BaseRoot -} +export interface IntrinsicKeywords extends tsKeywords, jsObjects, internal {} -export type RawResolution = BaseRoot | GenericRoot | RawRootModule +export type InternalResolution = BaseRoot | GenericRoot | InternalRootModule -type CachedResolution = string | RawResolution +type CachedResolution = string | InternalResolution const schemaBranchesOf = (schema: object) => isArray(schema) ? schema @@ -121,12 +119,13 @@ export type writeDuplicateAliasError = export type AliasDefEntry = [name: string, defValue: unknown] -const scopesById: Record = {} +const scopesById: Record = {} $ark.intrinsic = {} as never -export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> - implements internalImplementationOf +export abstract class InternalBaseScope< + $ extends InternalResolutions = InternalResolutions +> implements internalImplementationOf { readonly config: ArkConfig readonly resolvedConfig: ResolvedArkConfig @@ -182,6 +181,11 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> return this } + @bound + defineSchema(def: RootSchema): RootSchema { + return def + } + @bound schema(def: RootSchema, opts?: NodeParseOptions): BaseRoot { return this.node(schemaKindOf(def), def, opts) @@ -189,19 +193,20 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> @bound defineRoot(def: RootSchema): RootSchema { - return def + return this.defineSchema(def) } @bound generic( - params: array, - def?: unknown - ): GenericRoot | ((def: LazyGenericSchema) => GenericRoot) { - if (def === undefined) { - return (def: LazyGenericSchema) => - this.generic(params, new LazyGenericRoot(def)) as never - } - return parseGeneric(params, def, this as never) + ...params: array + ): ReturnType { + const $: BaseScope = this as never + return (instantiateDef): any => + class GenericHktSubclass extends GenericRoot { + constructor() { + super(params, new LazyGenericBody(instantiateDef), $, $) + } + } } @bound @@ -309,14 +314,6 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> return opts } - parseRoot(def: unknown, opts: NodeParseOptions = {}): BaseRoot { - const node = this.schema( - def as never, - this.finalizeRootArgs(opts, () => node) - ) - return node - } - resolveRoot(name: string): BaseRoot { return ( this.maybeResolveRoot(name) ?? @@ -345,7 +342,7 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> return [k, v] } - maybeResolve(name: string): RawResolution | undefined { + maybeResolve(name: string): InternalResolution | undefined { const resolution = this.maybeShallowResolve(name) return typeof resolution === "string" ? this.node("alias", { alias: resolution }, { prereduced: true }) @@ -360,11 +357,8 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> if (!def) return this.maybeResolveSubalias(name) const preparsed = this.preparseRoot(def) - if (hasArkKind(preparsed, "generic")) { - return (this.resolutions[name] = preparsed - .validateBaseInstantiation() - ?.bindScope(this)) - } + if (hasArkKind(preparsed, "generic")) + return (this.resolutions[name] = preparsed.bindScope(this)) if (hasArkKind(preparsed, "module")) { return (this.resolutions[name] = new RootModule( @@ -399,7 +393,7 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> ) as never } - private _exportedResolutions: RawRootResolutions | undefined + private _exportedResolutions: InternalResolutions | undefined private _exports: RootExportCache | undefined export[]>( ...names: names @@ -428,13 +422,8 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> this.lazyResolutions.forEach(node => node.resolution) this._exportedResolutions = resolutionsOfModule(this, this._exports) - // TODO: add generic json - Object.assign( - this.json, - flatMorph(this._exportedResolutions as Dict, (k, v) => - hasArkKind(v, "root") ? [k, v.json] : [] - ) - ) + + Object.assign(this.json, resolutionsToJson(this._exportedResolutions)) Object.assign(this.resolutions, this._exportedResolutions) if (this.config.intrinsic) Object.assign($ark.intrinsic, this._exportedResolutions) @@ -456,8 +445,18 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> ): destructuredExportContext<$, []>[name] { return this.export()[name] as never } + + abstract parseRoot(schema: any, opts?: NodeParseOptions): BaseRoot } +const resolutionsToJson = (resolutions: InternalResolutions): Json => + flatMorph(resolutions, (k, v) => [ + k, + hasArkKind(v, "root") || hasArkKind(v, "generic") ? + v.json + : resolutionsToJson(v) + ]) + const maybeResolveSubalias = ( base: Dict, name: string @@ -501,9 +500,9 @@ export type instantiateAliases = { export const schemaScope = ( aliases: validateAliases, config?: ArkConfig -): RootScope> => new RootScope(aliases, config) +): SchemaScope> => new SchemaScope(aliases, config) -export interface RootScope<$ = any> { +export interface BaseScope<$ = any> { t: $ [arkKind]: "scope" config: ArkConfig @@ -514,19 +513,19 @@ export interface RootScope<$ = any> { /** The set of names defined at the root-level of the scope mapped to their * corresponding definitions.**/ aliases: Record - internal: toRawScope<$> + internal: toInternalScope<$> + + defineSchema(schema: def): def schema( schema: def, opts?: NodeParseOptions - ): Root, $> - - defineRoot(schema: def): def + ): SchemaRoot, $> units( values: branches, opts?: NodeParseOptions - ): Root + ): SchemaRoot node>( kinds: kinds, @@ -534,26 +533,6 @@ export interface RootScope<$ = any> { opts?: NodeParseOptions ): Node>> - generic< - const paramsDef extends array, - bodyDef extends LazyGenericSchema< - genericParamSchemasToAst - > = never - >( - params: paramsDef, - def?: bodyDef - ): [bodyDef] extends [never] ? - >>( - lazyDef: lazyDef - ) => GenericRoot< - genericParamSchemasToAst, - ReturnType, - $ - > - : GenericRoot, bodyDef, $> - - parseRoot(schema: unknown, opts?: NodeParseOptions): BaseRoot - import[]>( ...names: names ): SchemaModule>> @@ -564,31 +543,52 @@ export interface RootScope<$ = any> { resolve>( name: name - ): $[name] extends PreparsedNodeResolution ? $[name] : Root<$[name], $> + ): $[name] extends PreparsedNodeResolution ? $[name] : SchemaRoot<$[name], $> + + parseRoot(def: any, opts?: NodeParseOptions): SchemaRoot +} + +export class InternalSchemaScope< + $ extends InternalResolutions = InternalResolutions +> extends InternalBaseScope<$> { + parseRoot(def: unknown, opts: NodeParseOptions = {}): BaseRoot { + const node = this.schema( + def as never, + this.finalizeRootArgs(opts, () => node) + ) + return node + } +} + +export interface SchemaScope<$ = {}> extends BaseScope<$> { + defineRoot: this["defineSchema"] + parseRoot: this["schema"] + generic: GenericHktSchemaParser<$> } -export const RootScope: new <$ = any>( - ...args: ConstructorParameters -) => RootScope<$> = RawRootScope as never +export const SchemaScope: new <$ = {}>( + ...args: ConstructorParameters +) => SchemaScope<$> = InternalSchemaScope as never -export const root: RootScope<{}> = new RootScope({}) +export const root: SchemaScope = new SchemaScope({}) -export const schema: RootScope["schema"] = root.schema -export const node: RootScope["node"] = root.node -export const defineRoot: RootScope["defineRoot"] = root.defineRoot -export const units: RootScope["units"] = root.units -export const generic: RootScope["generic"] = root.generic -export const internalSchema: RawRootScope["schema"] = root.internal.schema -export const internalNode: RawRootScope["node"] = root.internal.node -export const defineInternalRoot: RawRootScope["defineRoot"] = +export const schema: SchemaScope["schema"] = root.schema +export const node: SchemaScope["node"] = root.node +export const defineRoot: SchemaScope["defineRoot"] = root.defineRoot +export const units: SchemaScope["units"] = root.units +export const generic: SchemaScope["generic"] = root.generic +export const internalSchema: InternalBaseScope["schema"] = root.internal.schema +export const internalNode: InternalBaseScope["node"] = root.internal.node +export const defineInternalRoot: InternalBaseScope["defineRoot"] = root.internal.defineRoot -export const internalUnits: RawRootScope["units"] = root.internal.units -export const internalGeneric: RawRootScope["generic"] = root.internal.generic +export const internalUnits: InternalBaseScope["units"] = root.internal.units +export const internalGeneric: InternalBaseScope["generic"] = + root.internal.generic export const parseAsSchema = ( def: unknown, opts?: NodeParseOptions -): Root | ParseError => { +): SchemaRoot | ParseError => { try { return schema(def as RootSchema, opts) as never } catch (e) { @@ -597,10 +597,9 @@ export const parseAsSchema = ( } } -export class RawRootModule< - resolutions extends RawRootResolutions = RawRootResolutions +export class InternalRootModule< + resolutions extends InternalResolutions = InternalResolutions > extends DynamicBase { - // TODO: kind? declare readonly [arkKind]: "module" } @@ -615,11 +614,14 @@ export type destructuredImportContext<$, names extends exportedNameOf<$>[]> = { export type RootExportCache = Record< string, - BaseRoot | GenericRoot | RawRootModule | undefined + BaseRoot | GenericRoot | InternalRootModule | undefined > -const resolutionsOfModule = ($: RawRootScope, typeSet: RootExportCache) => { - const result: RawRootResolutions = {} +const resolutionsOfModule = ( + $: InternalBaseScope, + typeSet: RootExportCache +) => { + const result: InternalResolutions = {} for (const k in typeSet) { const v = typeSet[k] if (hasArkKind(v, "module")) { diff --git a/ark/schema/shared/compile.ts b/ark/schema/shared/compile.ts index 606c27b29e..ffcd65712b 100644 --- a/ark/schema/shared/compile.ts +++ b/ark/schema/shared/compile.ts @@ -1,8 +1,145 @@ -import { CompiledFunction } from "@ark/util" +import { + CastableBase, + DynamicFunction, + hasDomain, + isDotAccessible, + serializePrimitive +} from "@ark/util" import type { BaseNode } from "../node.js" import type { Discriminant } from "../roots/union.js" +import { registeredReference } from "./registry.js" import type { TraversalKind } from "./traversal.js" +export type CoercibleValue = string | number | boolean | null | undefined + +export class CompiledFunction< + args extends readonly string[] +> extends CastableBase<{ + [k in args[number]]: k +}> { + readonly argNames: args + readonly body = "" + + constructor(...args: args) { + super() + this.argNames = args + for (const arg of args) { + if (arg in this) { + throw new Error( + `Arg name '${arg}' would overwrite an existing property on FunctionBody` + ) + } + ;(this as any)[arg] = arg + } + } + + indentation = 0 + indent(): this { + this.indentation += 4 + return this + } + + dedent(): this { + this.indentation -= 4 + return this + } + + prop(key: PropertyKey, optional = false): string { + return compileLiteralPropAccess(key, optional) + } + + index(key: string | number, optional = false): string { + return indexPropAccess(`${key}`, optional) + } + + line(statement: string): this { + ;(this.body as any) += `${" ".repeat(this.indentation)}${statement}\n` + return this + } + + const(identifier: string, expression: CoercibleValue): this { + this.line(`const ${identifier} = ${expression}`) + return this + } + + let(identifier: string, expression: CoercibleValue): this { + return this.line(`let ${identifier} = ${expression}`) + } + + set(identifier: string, expression: CoercibleValue): this { + return this.line(`${identifier} = ${expression}`) + } + + if(condition: string, then: (self: this) => this): this { + return this.block(`if (${condition})`, then) + } + + elseIf(condition: string, then: (self: this) => this): this { + return this.block(`else if (${condition})`, then) + } + + else(then: (self: this) => this): this { + return this.block("else", then) + } + + /** Current index is "i" */ + for( + until: string, + body: (self: this) => this, + initialValue: CoercibleValue = 0 + ): this { + return this.block(`for (let i = ${initialValue}; ${until}; i++)`, body) + } + + /** Current key is "k" */ + forIn(object: string, body: (self: this) => this): this { + return this.block(`for (const k in ${object})`, body) + } + + block(prefix: string, contents: (self: this) => this, suffix = ""): this { + this.line(`${prefix} {`) + this.indent() + contents(this) + this.dedent() + return this.line(`}${suffix}`) + } + + return(expression: CoercibleValue = ""): this { + return this.line(`return ${expression}`) + } + + compile< + f extends ( + ...args: { + [i in keyof args]: never + } + ) => unknown + >(): f { + return new DynamicFunction(...this.argNames, this.body) + } +} + +export const compileSerializedValue = (value: unknown): string => + hasDomain(value, "object") || typeof value === "symbol" ? + registeredReference(value) + : serializePrimitive(value as never) + +export const compileLiteralPropAccess = ( + key: PropertyKey, + optional = false +): string => { + if (typeof key === "string" && isDotAccessible(key)) + return `${optional ? "?" : ""}.${key}` + + return indexPropAccess(serializeLiteralKey(key), optional) +} + +export const serializeLiteralKey = (key: PropertyKey): string => + typeof key === "symbol" ? registeredReference(key) : JSON.stringify(key) + +export const indexPropAccess = (key: string, optional = false): string => + `${optional ? "?." : ""}[${key}]` + export interface InvokeOptions extends ReferenceOptions { arg?: string } diff --git a/ark/schema/shared/declare.ts b/ark/schema/shared/declare.ts index 9abe7a85e3..d9c08c2298 100644 --- a/ark/schema/shared/declare.ts +++ b/ark/schema/shared/declare.ts @@ -50,10 +50,10 @@ export type declareNode< type prerequisiteOf = "prerequisite" extends keyof d ? d["prerequisite"] : unknown -export type attachmentsOf = +export type attachmentsOf = NarrowedAttachments & d["inner"] -export interface RawNodeDeclaration { +export interface BaseNodeDeclaration { kind: NodeKind schema: unknown normalizedSchema: BaseMeta @@ -65,6 +65,6 @@ export interface RawNodeDeclaration { errorContext: BaseErrorContext | null } -export type ownIntersectionResult = +export type ownIntersectionResult = | Node> | Disjoint diff --git a/ark/schema/shared/implement.ts b/ark/schema/shared/implement.ts index ac11daff17..b9eadd1f4c 100644 --- a/ark/schema/shared/implement.ts +++ b/ark/schema/shared/implement.ts @@ -1,5 +1,4 @@ import { - compileSerializedValue, flatMorph, printable, throwParseError, @@ -7,8 +6,8 @@ import { type Json, type JsonData, type PartialRecord, + type arrayIndexOf, type entryOf, - type indexOf, type keySet, type keySetOf, type listable, @@ -25,12 +24,13 @@ import type { schemaKindOrRightOf, schemaKindRightOf } from "../roots/root.js" -import type { RawRootScope } from "../scope.js" +import type { InternalBaseScope } from "../scope.js" import type { StructureInner } from "../structure/structure.js" +import { compileSerializedValue } from "./compile.js" import type { BaseErrorContext, BaseMeta, - RawNodeDeclaration + BaseNodeDeclaration } from "./declare.js" import type { Disjoint } from "./disjoint.js" import { isNode } from "./utils.js" @@ -145,7 +145,7 @@ export interface InternalIntersectionOptions { } export interface IntersectionContext extends InternalIntersectionOptions { - $: RawRootScope + $: InternalBaseScope invert: boolean } @@ -194,7 +194,7 @@ export type UnknownIntersectionMap = { export type UnknownIntersectionResult = BaseNode | Disjoint | null type PrecedenceByKind = { - [i in indexOf as OrderedNodeKinds[i]]: i + [i in arrayIndexOf as OrderedNodeKinds[i]]: i } export const precedenceByKind: PrecedenceByKind = flatMorph( @@ -232,11 +232,11 @@ export const schemaKindsRightOf = ( ): schemaKindRightOf[] => rootKinds.slice(precedenceOfKind(kind) + 1) as never -export type KeySchemaDefinitions = { +export type KeySchemaDefinitions = { [k in keyRequiringSchemaDefinition]: NodeKeyImplementation } -type keyRequiringSchemaDefinition = Exclude< +type keyRequiringSchemaDefinition = Exclude< keyof d["normalizedSchema"], keyof BaseMeta > @@ -254,7 +254,7 @@ export const defaultValueSerializer = (v: unknown): JsonData => { } export type NodeKeyImplementation< - d extends RawNodeDeclaration, + d extends BaseNodeDeclaration, k extends keyof d["normalizedSchema"], instantiated = k extends keyof d["inner"] ? Exclude : never @@ -276,7 +276,7 @@ export type NodeKeyImplementation< | ([instantiated] extends [listable] ? "child" : never) > -interface CommonNodeImplementationInput { +interface CommonNodeImplementationInput { kind: d["kind"] keys: KeySchemaDefinitions normalize: (schema: d["schema"]) => d["normalizedSchema"] @@ -285,12 +285,12 @@ interface CommonNodeImplementationInput { collapsibleKey?: keyof d["inner"] reduce?: ( inner: d["inner"], - $: RawRootScope + $: InternalBaseScope ) => Node | Disjoint | undefined } export interface UnknownNodeImplementation - extends CommonNodeImplementationInput { + extends CommonNodeImplementationInput { defaults: ResolvedUnknownNodeConfig intersectionIsOpen: boolean intersections: UnknownIntersectionMap @@ -305,14 +305,14 @@ export const compileErrorContext = (ctx: object): string => { return result + " }" } -export type nodeImplementationOf = +export type nodeImplementationOf = nodeImplementationInputOf & { intersections: IntersectionMap intersectionIsOpen: d["intersectionIsOpen"] defaults: Required> } -export type nodeImplementationInputOf = +export type nodeImplementationInputOf = CommonNodeImplementationInput & { intersections: IntersectionMap defaults: nodeSchemaaultsImplementationInputFor @@ -354,7 +354,7 @@ export interface UnknownAttachments { readonly typeHash: string } -export interface NarrowedAttachments +export interface NarrowedAttachments extends UnknownAttachments { kind: d["kind"] inner: d["inner"] @@ -369,9 +369,9 @@ export const baseKeys: PartialRecord< propValueOf> > = { description: { meta: true } -} satisfies KeySchemaDefinitions as never +} satisfies KeySchemaDefinitions as never -export const implementNode = ( +export const implementNode = ( _: nodeImplementationInputOf ): nodeImplementationOf => { const implementation: UnknownNodeImplementation = _ as never diff --git a/ark/schema/shared/intersections.ts b/ark/schema/shared/intersections.ts index 04933d632c..c9abe256d4 100644 --- a/ark/schema/shared/intersections.ts +++ b/ark/schema/shared/intersections.ts @@ -11,7 +11,7 @@ import type { Constraints, of, parseConstraints } from "../ast.js" import type { BaseNode } from "../node.js" import type { MorphAst, MorphNode, Out } from "../roots/morph.js" import type { BaseRoot } from "../roots/root.js" -import type { RawRootScope } from "../scope.js" +import type { InternalBaseScope } from "../scope.js" import { Disjoint } from "./disjoint.js" import type { IntersectionContext, @@ -90,7 +90,7 @@ type InternalNodeIntersection = ( ) => l["kind"] | r["kind"] extends RootKind ? BaseRoot | Disjoint : BaseNode | Disjoint | null -export const intersectNodesRoot: InternalNodeIntersection = ( +export const intersectNodesRoot: InternalNodeIntersection = ( l, r, $ @@ -101,7 +101,7 @@ export const intersectNodesRoot: InternalNodeIntersection = ( pipe: false }) -export const pipeNodesRoot: InternalNodeIntersection = ( +export const pipeNodesRoot: InternalNodeIntersection = ( l, r, $ diff --git a/ark/schema/shared/registry.ts b/ark/schema/shared/registry.ts new file mode 100644 index 0000000000..544d70d1ec --- /dev/null +++ b/ark/schema/shared/registry.ts @@ -0,0 +1,58 @@ +import { + $ark, + groupBy, + register, + type InitialRegistryContents +} from "@ark/util" +import type { NonNegativeIntegerString } from "../keywords/internal.js" + +let _registryName = "$ark" +let suffix = 2 + +while (_registryName in globalThis) _registryName = `$ark${suffix++}` + +export const registryName = _registryName +;(globalThis as any)[registryName] = $ark + +if (suffix !== 2) { + const g: any = globalThis + const registries: InitialRegistryContents[] = [g.$ark] + for (let i = 2; i < suffix; i++) + if (g[`$ark${i}`]) registries.push(g[`$ark${i}`]) + + console.warn( + `Multiple @ark registries detected. This can lead to unexpected behavior.` + ) + const byPath = groupBy(registries, "filename") + + const paths = Object.keys(byPath) + + for (const path of paths) { + if (byPath[path]!.length > 1) { + console.warn( + `File ${path} was initialized multiple times, likely due to being imported from both CJS and ESM contexts.` + ) + } + } + + if (paths.length > 1) { + console.warn( + `Registries were initialized at the following paths:` + + paths + .map( + path => ` ${path} (@ark/util version ${byPath[path]![0].version})` + ) + .join("\n") + ) + } +} + +export const reference = (name: string): RegisteredReference => + `${registryName}.${name}` as never + +export const registeredReference = ( + value: object | symbol +): RegisteredReference => reference(register(value)) + +export type RegisteredReference = + `$ark${"" | NonNegativeIntegerString}.${to}` diff --git a/ark/schema/shared/utils.ts b/ark/schema/shared/utils.ts index 0b7f764974..ebb6e4df19 100644 --- a/ark/schema/shared/utils.ts +++ b/ark/schema/shared/utils.ts @@ -13,7 +13,7 @@ import type { BaseConstraint } from "../constraint.js" import type { GenericRoot } from "../generic.js" import type { BaseNode } from "../node.js" import type { BaseRoot } from "../roots/root.js" -import type { RawRootModule, RawRootScope } from "../scope.js" +import type { InternalBaseScope, InternalRootModule } from "../scope.js" import type { ArkError } from "./errors.js" export const makeRootAndArrayPropertiesMutable = ( @@ -84,9 +84,9 @@ export const arkKind: unique symbol = Symbol("ArkTypeInternalKind") export interface ArkKinds { constraint: BaseConstraint root: BaseRoot - scope: RawRootScope + scope: InternalBaseScope generic: GenericRoot - module: RawRootModule + module: InternalRootModule error: ArkError } diff --git a/ark/schema/structure/indexed.ts b/ark/schema/structure/indexed.ts index aae52f5a0d..500712fec7 100644 --- a/ark/schema/structure/indexed.ts +++ b/ark/schema/structure/indexed.ts @@ -1,4 +1,5 @@ import { + $ark, append, printable, stringAndSymbolicEntriesOf, @@ -108,7 +109,7 @@ export const indexImplementation: nodeImplementationOf = }) export class IndexNode extends BaseConstraint { - impliedBasis: BaseRoot = $ark.intrinsic.object + impliedBasis: BaseRoot = $ark.intrinsic.object.internal expression = `[${this.signature.expression}]: ${this.value.expression}` traverseAllows: TraverseAllows = (data, ctx) => diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index a0f8963bbc..4ce461083f 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -1,8 +1,7 @@ import { + $ark, append, - compileSerializedValue, printable, - registeredReference, throwParseError, unset, type Key @@ -17,11 +16,12 @@ import { } from "../node.js" import type { Morph } from "../roots/morph.js" import type { BaseRoot } from "../roots/root.js" -import type { NodeCompiler } from "../shared/compile.js" +import { compileSerializedValue, type NodeCompiler } from "../shared/compile.js" import type { BaseMeta } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" import type { IntersectionContext, RootKind } from "../shared/implement.js" import { intersectNodes } from "../shared/intersections.js" +import { registeredReference } from "../shared/registry.js" import type { TraverseAllows, TraverseApply } from "../shared/traversal.js" import type { OptionalDeclaration, OptionalNode } from "./optional.js" import type { RequiredDeclaration } from "./required.js" @@ -101,7 +101,7 @@ export abstract class BaseProp< > { required: boolean = this.kind === "required" optional: boolean = this.kind === "optional" - impliedBasis: BaseRoot = $ark.intrinsic.object + impliedBasis: BaseRoot = $ark.intrinsic.object.internal serializedKey: string = compileSerializedValue(this.key) compiledKey: string = typeof this.key === "string" ? this.key : this.serializedKey diff --git a/ark/schema/structure/sequence.ts b/ark/schema/structure/sequence.ts index f63dec3210..13eb306c02 100644 --- a/ark/schema/structure/sequence.ts +++ b/ark/schema/structure/sequence.ts @@ -1,4 +1,5 @@ import { + $ark, append, cached, throwInternalError, @@ -233,7 +234,7 @@ export const sequenceImplementation: nodeImplementationOf = }) export class SequenceNode extends BaseConstraint { - impliedBasis: BaseRoot = $ark.intrinsic.Array + impliedBasis: BaseRoot = $ark.intrinsic.Array.internal prefix: array = this.inner.prefix ?? [] optionals: array = this.inner.optionals ?? [] prevariadic: array = [...this.prefix, ...this.optionals] @@ -308,11 +309,11 @@ export class SequenceNode extends BaseConstraint { append( element.flatRefs.map(ref => flatRef( - [$ark.intrinsic.nonNegativeIntegerString, ...ref.path], + [$ark.intrinsic.nonNegativeIntegerString.internal, ...ref.path], ref.node ) ), - flatRef([$ark.intrinsic.nonNegativeIntegerString], element) + flatRef([$ark.intrinsic.nonNegativeIntegerString.internal], element) ) ) ) @@ -361,7 +362,7 @@ export class SequenceNode extends BaseConstraint { mapper: DeepNodeTransformation, ctx: DeepNodeTransformContext ) { - ctx.path.push($ark.intrinsic.nonNegativeIntegerString) + ctx.path.push($ark.intrinsic.nonNegativeIntegerString.internal) const result = super._transform(mapper, ctx) ctx.path.pop() return result diff --git a/ark/schema/structure/shared.ts b/ark/schema/structure/shared.ts index 293e404c4a..215a757608 100644 --- a/ark/schema/structure/shared.ts +++ b/ark/schema/structure/shared.ts @@ -1,8 +1,11 @@ -import { registeredReference } from "@ark/util" +import { + registeredReference, + type RegisteredReference +} from "../shared/registry.js" export const arrayIndexSource = `^(?:0|[1-9]\\d*)$` export const arrayIndexMatcher = new RegExp(arrayIndexSource) -export const arrayIndexMatcherReference: `$ark.${string}` = +export const arrayIndexMatcherReference: RegisteredReference = registeredReference(arrayIndexMatcher) diff --git a/ark/schema/structure/structure.ts b/ark/schema/structure/structure.ts index 5c738b01a6..705b211f2a 100644 --- a/ark/schema/structure/structure.ts +++ b/ark/schema/structure/structure.ts @@ -1,14 +1,15 @@ import { + $ark, append, cached, flatMorph, printable, - registeredReference, spliterate, throwParseError, type array, + type join, type Key, - type RegisteredReference + type typeToString } from "@ark/util" import { BaseConstraint, @@ -16,11 +17,12 @@ import { flattenConstraints, intersectConstraints } from "../constraint.js" +import type { InferredRoot } from "../inference.js" import type { NonNegativeIntegerString } from "../keywords/internal.js" import type { MutableInner } from "../kinds.js" -import type { TypeKey, TypePath } from "../node.js" -import type { BaseRoot } from "../roots/root.js" -import type { RawRootScope } from "../scope.js" +import type { TypeIndexer, TypeKey } from "../node.js" +import { typeOrTermExtends, type BaseRoot } from "../roots/root.js" +import type { InternalBaseScope } from "../scope.js" import type { NodeCompiler } from "../shared/compile.js" import type { BaseMeta, declareNode } from "../shared/declare.js" import { Disjoint } from "../shared/disjoint.js" @@ -30,6 +32,10 @@ import { type StructuralKind } from "../shared/implement.js" import { intersectNodesRoot } from "../shared/intersections.js" +import { + registeredReference, + type RegisteredReference +} from "../shared/registry.js" import type { TraversalContext, TraversalKind, @@ -78,7 +84,7 @@ export interface StructureDeclaration }> {} export class StructureNode extends BaseConstraint { - impliedBasis: BaseRoot = $ark.intrinsic.object + impliedBasis: BaseRoot = $ark.intrinsic.object.internal impliedSiblings = this.children.flatMap( n => (n.impliedSiblings as BaseConstraint[]) ?? [] ) @@ -119,11 +125,21 @@ export class StructureNode extends BaseConstraint { return this.$.node("union", branches) } - get(key: TypeKey, ...tail: TypePath): BaseRoot { + assertHasKeys(keys: array) { + const invalidKeys = keys.filter(k => !typeOrTermExtends(k, this.keyof())) + + if (invalidKeys.length) { + return throwParseError( + writeInvalidKeysMessage(this.expression, invalidKeys) + ) + } + } + + get(indexer: TypeIndexer, ...path: array): BaseRoot { let value: BaseRoot | undefined let required = false - if (hasArkKind(key, "root") && key.hasKind("unit")) key = key.unit as never + const key = indexerToKey(indexer) if ( (typeof key === "string" || typeof key === "symbol") && @@ -134,12 +150,13 @@ export class StructureNode extends BaseConstraint { } this.index?.forEach(n => { - if (n.signature.includes(key)) value = value?.and(n.value) ?? n.value + if (typeOrTermExtends(key, n.signature)) + value = value?.and(n.value) ?? n.value }) if ( this.sequence && - $ark.intrinsic.nonNegativeIntegerString.includes(key) + typeOrTermExtends(key, $ark.intrinsic.nonNegativeIntegerString) ) { if (hasArkKind(key, "root")) { if (this.sequence.variadic) @@ -172,26 +189,32 @@ export class StructureNode extends BaseConstraint { key.extends($ark.intrinsic.number) ) { return throwParseError( - writeRawNumberIndexMessage(key.expression, this.sequence.expression) + writeNumberIndexMessage(key.expression, this.sequence.expression) ) } - return throwParseError(writeBadKeyAccessMessage(key, this.expression)) + return throwParseError(writeInvalidKeysMessage(this.expression, [key])) } - const result = value.get(...tail) + const result = value.get(...path) return required ? result : result.or($ark.intrinsic.undefined) } readonly exhaustive: boolean = this.undeclared !== undefined || this.index !== undefined + pick(...keys: array): StructureNode { + this.assertHasKeys(keys) + return this.$.node("structure", this.filterKeys("pick", keys)) + } + omit(...keys: array): StructureNode { - return this.$.node("structure", omitFromInner(this.inner, keys)) + this.assertHasKeys(keys) + return this.$.node("structure", this.filterKeys("omit", keys)) } merge(r: StructureNode): StructureNode { const inner = makeRootAndArrayPropertiesMutable( - omitFromInner(this.inner, [r.keyof()]) + this.filterKeys("omit", [r.keyof()]) ) if (r.required) inner.required = append(inner.required, r.required) if (r.optional) inner.optional = append(inner.optional, r.optional) @@ -202,6 +225,29 @@ export class StructureNode extends BaseConstraint { return this.$.node("structure", inner) } + private filterKeys( + operation: "pick" | "omit", + keys: array + ): StructureInner { + const result = { ...this.inner } + + const includeKey = (key: TypeKey) => { + const matchesKey = keys.some(k => typeOrTermExtends(key, k)) + return operation === "pick" ? matchesKey : !matchesKey + } + + if (result.required) + result.required = result.required.filter(prop => includeKey(prop.key)) + + if (result.optional) + result.optional = result.optional.filter(prop => includeKey(prop.key)) + + if (result.index) + result.index = result.index.filter(index => includeKey(index.signature)) + + return result + } + traverseAllows: TraverseAllows = (data, ctx) => this._traverse("Allows", data, ctx) @@ -356,29 +402,11 @@ export class StructureNode extends BaseConstraint { } } -const omitFromInner = ( - inner: StructureInner, - keys: array -): StructureInner => { - const result = { ...inner } - keys.forEach(k => { - if (result.required) { - result.required = result.required.filter(b => - hasArkKind(k, "root") ? !k.allows(b.key) : k !== b.key - ) - } - if (result.optional) { - result.optional = result.optional.filter(b => - hasArkKind(k, "root") ? !k.allows(b.key) : k !== b.key - ) - } - if (result.index && hasArkKind(k, "root")) { - // we only have to filter index nodes if the input was a node, as - // literal keys should never subsume an index - result.index = result.index.filter(n => !n.signature.extends(k)) - } - }) - return result +const indexerToKey = (indexable: TypeIndexer): TypeKey => { + if (hasArkKind(indexable, "root") && indexable.hasKind("unit")) + indexable = indexable.unit as Key + if (typeof indexable === "number") indexable = `${indexable}` + return indexable } const createStructuralWriter = @@ -525,17 +553,12 @@ export const structureImplementation: nodeImplementationOf } }) -export const writeRawNumberIndexMessage = ( +export const writeNumberIndexMessage = ( indexExpression: string, sequenceExpression: string ) => `${indexExpression} is not allowed as an array index on ${sequenceExpression}. Use the 'nonNegativeIntegerString' keyword instead.` -export const writeBadKeyAccessMessage = ( - key: TypeKey, - structuralExpression: string -) => `${printable(key)} does not exist on ${structuralExpression}` - export type NormalizedIndex = { index?: IndexNode required?: RequiredNode[] @@ -545,7 +568,7 @@ export type NormalizedIndex = { export const normalizeIndex = ( signature: BaseRoot, value: BaseRoot, - $: RawRootScope + $: InternalBaseScope ): NormalizedIndex => { const [enumerableBranches, nonEnumerableBranches] = spliterate( signature.branches, @@ -570,7 +593,14 @@ export const normalizeIndex = ( return normalized } -export type indexOf = +export type toArkKey = + k extends number ? + [o, number] extends [array, k] ? + NonNegativeIntegerString + : `${k}` + : k + +export type arkKeyOf = o extends array ? | (number extends o["length"] ? NonNegativeIntegerString : never) | { @@ -581,7 +611,31 @@ export type indexOf = [k in keyof o]: k extends number ? k | `${k}` : k }[keyof o] -export type indexInto> = o[Extract< +export type getArkKey> = o[Extract< k extends NonNegativeIntegerString ? number : k, keyof o >] + +export const typeKeyToString = (k: TypeKey) => + hasArkKind(k, "root") ? k.expression : printable(k) + +export type typeKeyToString = typeToString< + k extends InferredRoot ? t : k +> + +export const writeInvalidKeysMessage = < + o extends string, + keys extends array +>( + o: o, + keys: keys +) => + `Key${keys.length === 1 ? "" : "s"} ${keys.map(typeKeyToString).join(", ")} ${keys.length === 1 ? "does" : "do"} not exist on ${o}` as writeInvalidKeysMessage< + o, + keys + > + +export type writeInvalidKeysMessage< + o extends string, + keys extends array +> = `Key${keys["length"] extends 1 ? "" : "s"} ${join<{ [i in keyof keys]: typeKeyToString }, ", ">} ${keys["length"] extends 1 ? "does" : "do"} not exist on ${o}` diff --git a/ark/type/CHANGELOG.md b/ark/type/CHANGELOG.md index 763d501147..f2c4c22676 100644 --- a/ark/type/CHANGELOG.md +++ b/ark/type/CHANGELOG.md @@ -1,5 +1,113 @@ # arktype +## 2.0.0-beta.1 + +### Generic Builtins + +In addition to `Record`, the following generics from TS are now available in ArkType: + +- **Pick** +- **Omit** +- **Extract** +- **Exclude** + +These can be instantiated in one of three ways: + +### Syntactic Definition + +```ts +// Type<1> +const one = type("Extract<0 | 1, 1>") +``` + +### Chained Definition + +```ts +const user = type({ + name: "string", + "age?": "number", + isAdmin: "boolean" +}) + +// Type<{ +// name: string; +// age?: number; +// }> +const basicUser = user.pick("name", "age") +``` + +### Invoked Definition + +```ts +import { ark } from "arktype" + +// Type +const unfalse = ark.Exclude("boolean", "false") +``` + +### New Keywords + +#### BuiltinObjects + +We've added many new keywords for builtin JavaScript objects: + +- `ArrayBuffer` +- `Blob` +- `File` +- `FormData` +- `Headers` +- `Request` +- `Response` +- `URL` +- `TypedArray.Int8` +- `TypedArray.Uint8` +- `TypedArray.Uint8Clamped` +- `TypedArray.Int16` +- `TypedArray.Uint16` +- `TypedArray.Int32` +- `TypedArray.Uint32` +- `TypedArray.Float32` +- `TypedArray.Float64` +- `TypedArray.BigInt64` +- `TypedArray.BigUint64` + +#### `pasre.formData` and `liftArray` + +We've also added a new builtin parse keyword, `parse.formData`. It validates an input is an instance of `FormData`, then converts it to a `Record`. The first entry for a given key will have a `string | File` value. If subsequent entries with the same key are encountered, the value will be an array listing them. + +This is especially useful when combined with a new builtin generic, `liftArray`. This generic accepts a single parameter, accepts inputs of that type or arrays of that type, and converts the input to an array if it is not one already. + +Here's an example of how they can be used together: + +```ts +const user = type({ + email: "email", + file: "File", + tags: "liftArray" +}) + +// Type<(In: FormData) => Out<{ +// email: string.matching<"?">; +// file: File; +// tags: (In: string | string[]) => Out; +// }>> +const parseUserForm = type("parse.formData").pipe(user) +``` + +### Generic HKTs + +Our new generics have been built using a new method for integrating arbitrary external types as native ArkType generics! This opens up tons of possibilities for external integrations that would otherwise not be possible, but we're still finalizing the API. As a preview, here's what the implementation of `Exclude` looks like internally: + +```ts +class ArkExclude extends generic("T", "U")(args => args.T.exclude(args.U)) { + declare hkt: ( + args: conform + ) => Exclude<(typeof args)[0], (typeof args)[1]> +} +``` + +More to come on this as the API is finalized! + ## 2.0.0-beta.0 ### Generics diff --git a/ark/type/README.md b/ark/type/README.md new file mode 120000 index 0000000000..fe84005413 --- /dev/null +++ b/ark/type/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/ark/type/__tests__/completions.test.ts b/ark/type/__tests__/completions.test.ts index d570df2424..26f5ccd682 100644 --- a/ark/type/__tests__/completions.test.ts +++ b/ark/type/__tests__/completions.test.ts @@ -27,6 +27,13 @@ contextualize(() => { }) }) + it("completes within expressions in objects", () => { + // @ts-expect-error + attest(() => type({ key: "number | b" })).completions({ + "number | b": ["number | bigint", "number | boolean"] + }) + }) + it("completes user-defined aliases", () => { const $ = scope({ over9000: "number>9000", diff --git a/ark/type/__tests__/declared.test.ts b/ark/type/__tests__/declared.test.ts index 16249035cb..df17be05e3 100644 --- a/ark/type/__tests__/declared.test.ts +++ b/ark/type/__tests__/declared.test.ts @@ -95,7 +95,7 @@ contextualize(() => { a: "string" }) ).type.errors( - `Property 'b' is missing in type '{ a: "string"; }' but required in type '{ a: "string"; b: number; }'.` + `Property 'b' is missing in type '{ a: "string"; }' but required in type '{ b: number; a: "string"; }'.` ) }) diff --git a/ark/type/__tests__/discrimination.test.ts b/ark/type/__tests__/discrimination.test.ts index d9cb859514..76b9aefc82 100644 --- a/ark/type/__tests__/discrimination.test.ts +++ b/ark/type/__tests__/discrimination.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@ark/attest" -import { registeredReference } from "@ark/util" +import { registeredReference } from "@ark/schema" import { scope, type } from "arktype" contextualize(() => { diff --git a/ark/type/__tests__/expressions.test.ts b/ark/type/__tests__/expressions.test.ts index f6c0111bde..d78fff99be 100644 --- a/ark/type/__tests__/expressions.test.ts +++ b/ark/type/__tests__/expressions.test.ts @@ -17,14 +17,27 @@ contextualize(() => { "...", "===", "Array", + "ArrayBuffer", + "Blob", "Date", "Error", + "Exclude", + "Extract", + "File", + "FormData", "Function", + "Headers", "Map", + "Omit", + "Pick", "Promise", "Record", "RegExp", + "Request", + "Response", "Set", + "TypedArray", + "URL", "WeakMap", "WeakSet", "alpha", @@ -42,6 +55,7 @@ contextualize(() => { "ip", "keyof", "lengthBoundable", + "liftArray", "lowercase", "never", "nonNegativeIntegerString", @@ -59,8 +73,7 @@ contextualize(() => { "unknown", "uppercase", "url", - "uuid", - "void" + "uuid" ] }) // @ts-expect-error @@ -73,14 +86,27 @@ contextualize(() => { "?", "@", "Array", + "ArrayBuffer", + "Blob", "Date", "Error", + "Exclude", + "Extract", + "File", + "FormData", "Function", + "Headers", "Map", + "Omit", + "Pick", "Promise", "Record", "RegExp", + "Request", + "Response", "Set", + "TypedArray", + "URL", "WeakMap", "WeakSet", "[]", @@ -98,6 +124,7 @@ contextualize(() => { "ip", "keyof", "lengthBoundable", + "liftArray", "lowercase", "never", "nonNegativeIntegerString", @@ -116,7 +143,6 @@ contextualize(() => { "uppercase", "url", "uuid", - "void", "|" ] }) diff --git a/ark/type/__tests__/generic.test.ts b/ark/type/__tests__/generic.test.ts index 34af791ce6..e3d765f156 100644 --- a/ark/type/__tests__/generic.test.ts +++ b/ark/type/__tests__/generic.test.ts @@ -6,7 +6,8 @@ import { writeUnresolvableMessage, writeUnsatisfiedParameterConstraintMessage } from "@ark/schema" -import { scope, type } from "arktype" +import type { conform, Hkt } from "@ark/util" +import { generic, scope, type } from "arktype" import { emptyGenericParameterMessage, type Generic } from "../generic.js" import { writeUnclosedGroupMessage } from "../parser/string/reduce/shared.js" import { writeInvalidGenericArgCountMessage } from "../parser/string/shift/operand/genericArgs.js" @@ -164,7 +165,6 @@ contextualize(() => { attest(t.t) attest(t.expression).equals(expected.expression) - // @ts-expect-error attest(() => positiveToInteger("number")) .throws( @@ -175,7 +175,7 @@ contextualize(() => { ) ) .type.errors( - "Argument of type 'string' is not assignable to parameter of type 'Root, any>'" + "Argument of type 'string' is not assignable to parameter of type 'Type, {}>'" ) }) @@ -387,37 +387,58 @@ contextualize(() => { }).export() ).throwsAndHasTypeError(emptyGenericParameterMessage) }) - - // it("self-reference", () => { - // const types = scope({ - // "alternate": { - // // ensures old generic params aren't intersected with - // // updated values (would be never) - // swap: "alternate", - // order: ["a", "b"] - // }, - // reference: "alternate<0, 1>" - // }).export() - - // attest<[0, 1]>(types.reference.infer.swap.swap.order) - // attest<[1, 0]>(types.reference.infer.swap.swap.swap.order) - // const fromCall = types.alternate("'off'", "'on'") - // attest<["off", "on"]>(fromCall.infer.swap.swap.order) - // attest<["on", "off"]>(fromCall.infer.swap.swap.swap.order) - // }) - - // it("self-reference no params", () => { - // attest(() => - // scope({ - // "nest": { - // // @ts-expect-error - // nest: "nest" - // } - // }).export() - // ).throwsAndHasTypeError( - // writeInvalidGenericArgsMessage("nest", ["t"], []) - // ) - // }) } ) + describe("hkt", () => { + it("can infer a generic from an hkt", () => { + // class MyExternalClass { + // constructor(public data: T) {} + // } + // class ValidatedExternalGeneric extends generic("T")(args => + // type("instanceof", MyExternalClass).and({ + // data: args.T + // }) + // ) { + // declare hkt: ( + // args: conform + // ) => MyExternalClass<(typeof args)[0]> + // } + // const myExternalClass = new ValidatedExternalGeneric() + // const myType = myExternalClass({ + // name: "string", + // age: "number" + // }) + }) + }) + + describe("cyclic", () => { + // it("self-reference", () => { + // const types = scope({ + // "alternate": { + // // ensures old generic params aren't intersected with + // // updated values (would be never) + // swap: "alternate", + // order: ["a", "b"] + // }, + // reference: "alternate<0, 1>" + // }).export() + // attest<[0, 1]>(types.reference.infer.swap.swap.order) + // attest<[1, 0]>(types.reference.infer.swap.swap.swap.order) + // const fromCall = types.alternate("'off'", "'on'") + // attest<["off", "on"]>(fromCall.infer.swap.swap.order) + // attest<["on", "off"]>(fromCall.infer.swap.swap.swap.order) + // }) + // it("self-reference no params", () => { + // attest(() => + // scope({ + // "nest": { + // // @ts-expect-error + // nest: "nest" + // } + // }).export() + // ).throwsAndHasTypeError( + // writeInvalidGenericArgsMessage("nest", ["t"], []) + // ) + // }) + }) }) diff --git a/ark/type/__tests__/get.test.ts b/ark/type/__tests__/get.test.ts index 14bc7751e7..d3839002e3 100644 --- a/ark/type/__tests__/get.test.ts +++ b/ark/type/__tests__/get.test.ts @@ -1,7 +1,7 @@ import { attest, contextualize } from "@ark/attest" import { - writeBadKeyAccessMessage, - writeRawNumberIndexMessage, + writeInvalidKeysMessage, + writeNumberIndexMessage, type Matching, type of, type string @@ -72,7 +72,7 @@ contextualize(() => { // @ts-expect-error attest(() => t.get("bar")).throws( - writeBadKeyAccessMessage("bar", t.expression) + writeInvalidKeysMessage(t.expression, ["bar"]) ) }) @@ -114,7 +114,7 @@ contextualize(() => { // @ts-expect-error attest(() => t.get("goog").expression).throws( - writeBadKeyAccessMessage("goog", t.expression) + writeInvalidKeysMessage(t.expression, ["goog"]) ) }) @@ -137,11 +137,11 @@ contextualize(() => { // @ts-expect-error attest(() => t.get("-1")).throws( - writeBadKeyAccessMessage("-1", t.expression) + writeInvalidKeysMessage(t.expression, ["-1"]) ) // @ts-expect-error attest(() => t.get("5.5")).throws( - writeBadKeyAccessMessage("5.5", t.expression) + writeInvalidKeysMessage(t.expression, ["5.5"]) ) attest(t.get(ark.nonNegativeIntegerString).expression).snap( @@ -154,13 +154,13 @@ contextualize(() => { // @ts-expect-error attest(() => t.get(ark.number)).throws( - writeRawNumberIndexMessage("number", t.expression) + writeNumberIndexMessage("number", t.expression) ) // number subtype // @ts-expect-error attest(() => t.get(ark.integer)).throws( - writeRawNumberIndexMessage("number % 1", t.expression) + writeNumberIndexMessage("number % 1", t.expression) ) }) @@ -177,7 +177,7 @@ contextualize(() => { // out of bounds // @ts-expect-error - attest(() => t.get(2)).throws(writeBadKeyAccessMessage("2", t.expression)) + attest(() => t.get(2)).throws(writeInvalidKeysMessage(t.expression, ["2"])) }) it("variadic tuple", () => { diff --git a/ark/type/__tests__/keywords.test.ts b/ark/type/__tests__/keywords.test.ts index cf2128cc7f..953809d66f 100644 --- a/ark/type/__tests__/keywords.test.ts +++ b/ark/type/__tests__/keywords.test.ts @@ -1,6 +1,15 @@ import { attest, contextualize } from "@ark/attest" -import { internalSchema } from "@ark/schema" -import { ark, type } from "arktype" +import { + internalSchema, + keywordNodes, + writeIndivisibleMessage, + writeInvalidKeysMessage, + writeNonStructuralOperandMessage, + writeUnsatisfiedParameterConstraintMessage, + type Out, + type string +} from "@ark/schema" +import { ark, scope, type } from "arktype" contextualize(() => { describe("jsObjects", () => { @@ -71,14 +80,6 @@ contextualize(() => { // should be equivalent to an unconstrained predicate attest(type("unknown").json).equals(expected.json) }) - - it("void", () => { - const t = type("void") - attest(t.infer) - const expected = type("undefined") - //should be treated as undefined at runtime - attest(t.json).equals(expected.json) - }) }) describe("validation", () => { @@ -224,6 +225,51 @@ contextualize(() => { ) attest(parseDate(5).toString()).snap("must be a string (was number)") }) + + it("formData", () => { + const user = type({ + email: "email", + file: "File", + tags: "liftArray" + }) + + const parseUserForm = type("parse.formData").pipe(user) + + attest< + (In: FormData) => Out<{ + email: string.matching<"?"> + file: File + tags: (In: string | string[]) => Out + }> + >(parseUserForm.t) + + // support Node18 + if (!globalThis.File) return + + const data = new FormData() + const file = new File([], "") + + data.append("email", "david@arktype.io") + data.append("file", file) + data.append("tags", "typescript") + data.append("tags", "arktype") + + const out = parseUserForm(data) + attest(out).equals({ + email: "david@arktype.io", + file, + tags: ["typescript", "arktype"] + }) + + data.set("email", "david") + data.set("file", null) + data.append("tags", file) + + attest(parseUserForm(data).toString()) + .snap(`email must be a valid email (was "david") +file must be an instance of File (was string) +tags[2] must be a string (was object)`) + }) }) describe("format", () => { @@ -245,12 +291,212 @@ contextualize(() => { }) describe("generics", () => { - it("record", () => { - const expected = type({ "[string]": "number" }) + describe("record", () => { + it("parsed", () => { + const expected = type({ "[string]": "number" }) + + const expression = type("Record") + attest(expression.json).equals(expected.json) + attest(expression.t) + }) + + it("invoked", () => { + const expected = type({ "[string]": "number" }) + + const t = ark.Record("string", "number") + attest(t.json).equals(expected.json) + attest(t.t) + }) + + it("invoked validation error", () => { + // @ts-expect-error + attest(() => ark.Record("string", "string % 2")).throwsAndHasTypeError( + writeIndivisibleMessage(keywordNodes.string) + ) + }) + + it("invoked constraint error", () => { + // @ts-expect-error + attest(() => ark.Record("boolean", "number")) + .throws( + writeUnsatisfiedParameterConstraintMessage( + "K", + "string | symbol", + "boolean" + ) + ) + .type.errors( + `'string' is not assignable to parameter of type 'Type'` + ) + }) + }) + + describe("pick", () => { + it("parsed", () => { + const types = scope({ + from: { + foo: "1", + "bar?": "1", + baz: "1", + "quux?": "1" + }, + actual: "Pick", + expected: { + foo: "1", + "bar?": "1" + } + }).export() + + attest(types.actual.t) + attest(types.actual.expression).equals(types.expected.expression) + }) + + it("chained", () => { + const user = type({ + name: "string", + "age?": "number", + isAdmin: "boolean" + }) + + const basicUser = user.pick("name", "age") + + const expected = type({ + name: "string", + "age?": "number" + }) + + attest(basicUser.t) + + attest(basicUser.expression).equals(expected.expression) + }) + + it("invalid key", () => { + const user = type({ + name: "string" + }) + + // @ts-expect-error + attest(() => user.pick("length")) + .throws(writeInvalidKeysMessage(user.expression, ["length"])) + .type.errors.snap( + 'Argument of type \'"length"\' is not assignable to parameter of type \'"name" | cast<"name">\'.' + ) + }) + + it("non-structure", () => { + // @ts-expect-error + attest(() => type("string").pick("length")).throwsAndHasTypeError( + writeNonStructuralOperandMessage("pick", "string") + ) + }) + }) + + describe("omit", () => { + it("parsed", () => { + const types = scope({ + from: { + foo: "1", + "bar?": "1", + baz: "1", + "quux?": "1" + }, + actual: "Omit", + expected: { + baz: "1", + "quux?": "1" + } + }).export() + + attest(types.actual.t) + attest(types.actual.expression).equals(types.expected.expression) + }) + + it("chained", () => { + const user = type({ + name: "string", + "age?": "number", + isAdmin: "boolean", + "isActive?": "boolean" + }) + + const extras = user.omit("name", "age") + + const expected = type({ + isAdmin: "boolean", + "isActive?": "boolean" + }) + + attest(extras.t) + + attest(extras.expression).equals(expected.expression) + }) + }) + + describe("extract", () => { + it("parsed", () => { + const types = scope({ + from: "0 | 1", + actual: "Extract", + expected: "1" + }).export() + + attest(types.actual.t) + attest(types.actual.expression).equals(types.expected.expression) + }) + + it("chained", () => { + const extracted = type("true | 0 | 'foo'").extract("boolean | number") + + const expected = type("true | 0") + + attest(extracted.t) + + attest(extracted.expression).equals(expected.expression) + }) + }) + + describe("exclude", () => { + it("parsed", () => { + const types = scope({ + from: "0 | 1", + actual: "Exclude", + expected: "0" + }).export() + + attest(types.actual.t) + attest(types.actual.expression).equals(types.expected.expression) + }) + + it("chained", () => { + const extracted = type("true | 0 | 'foo'").exclude("string") + + const expected = type("true | 0") + + attest(extracted.t) + + attest(extracted.expression).equals(expected.expression) + }) + }) + + describe("liftArray", () => { + it("parsed", () => { + const liftNumberArray = type("liftArray") + + attest<(In: number | number[]) => Out>(liftNumberArray.t) + + attest(liftNumberArray(5)).equals([5]) + attest(liftNumberArray([5])).equals([5]) + attest(liftNumberArray("five").toString()).snap( + "must be a number or an array (was string)" + ) + attest(liftNumberArray(["five"]).toString()).snap( + "must be a number (was object) or [0] must be a number (was string)" + ) + }) - const expression = type("Record") - attest(expression.json).equals(expected.json) - attest(expression.t) + it("invoked", () => { + ark.liftArray({ data: "number" }) + }) }) }) }) diff --git a/ark/type/__tests__/literal.test.ts b/ark/type/__tests__/literal.test.ts index aeacfe3693..385012c1a3 100644 --- a/ark/type/__tests__/literal.test.ts +++ b/ark/type/__tests__/literal.test.ts @@ -1,5 +1,6 @@ import { attest, contextualize } from "@ark/attest" -import { printable, registeredReference } from "@ark/util" +import { registeredReference } from "@ark/schema" +import { printable } from "@ark/util" import { type } from "arktype" contextualize(() => { diff --git a/ark/type/__tests__/match.bench.ts b/ark/type/__tests__/match.bench.ts index 888153f7aa..570b9e6122 100644 --- a/ark/type/__tests__/match.bench.ts +++ b/ark/type/__tests__/match.bench.ts @@ -1,19 +1,36 @@ import { bench } from "@ark/attest" import { match } from "arktype" -bench("general matchers", () => { +bench("when(3)", () => { const matcher = match() - .when("string", s => s) - .when("number", n => n) - .when("boolean", b => b) + .when("0", n => `${n}` as const) + .when("1", n => `${n}` as const) + .when("2", n => `${n}` as const) .orThrow() - const a = matcher("abc") - const b = matcher(4) - const c = matcher(true) -}) - .median() - .types() + const zero = matcher(0) + const one = matcher(1) + const two = matcher(2) +}).types([34953, "instantiations"]) + +bench("when(10)", () => { + const matcher = match() + .when("0", n => `${n}` as const) + .when("1", n => `${n}` as const) + .when("2", n => `${n}` as const) + .when("3", n => `${n}` as const) + .when("4", n => `${n}` as const) + .when("5", n => `${n}` as const) + .when("6", n => `${n}` as const) + .when("7", n => `${n}` as const) + .when("8", n => `${n}` as const) + .when("9", n => `${n}` as const) + .orThrow() + + const zero = matcher(0) + const one = matcher(1) + const two = matcher(2) +}).types([98902, "instantiations"]) bench("match.only", () => { const matcher = match @@ -26,6 +43,4 @@ bench("match.only", () => { const a = matcher("abc") const b = matcher(4) const c = matcher(true) -}) - .median() - .types() +}).types([37333, "instantiations"]) diff --git a/ark/type/__tests__/narrow.test.ts b/ark/type/__tests__/narrow.test.ts index 790174f866..acf848be19 100644 --- a/ark/type/__tests__/narrow.test.ts +++ b/ark/type/__tests__/narrow.test.ts @@ -1,6 +1,13 @@ import { attest, contextualize } from "@ark/attest" -import type { Narrowed, Out, number, of, string } from "@ark/schema" -import { registeredReference, type equals } from "@ark/util" +import { + registeredReference, + type Narrowed, + type Out, + type number, + type of, + type string +} from "@ark/schema" +import type { equals } from "@ark/util" import { type, type Type } from "arktype" contextualize(() => { diff --git a/ark/type/__tests__/objectLiteral.test.ts b/ark/type/__tests__/objectLiteral.test.ts index cd742fd0e3..744d0c05cc 100644 --- a/ark/type/__tests__/objectLiteral.test.ts +++ b/ark/type/__tests__/objectLiteral.test.ts @@ -1,10 +1,11 @@ import { attest, contextualize } from "@ark/attest" import { + registeredReference, writeInvalidPropertyKeyMessage, writeUnboundableMessage, writeUnresolvableMessage } from "@ark/schema" -import { printable, registeredReference } from "@ark/util" +import { printable } from "@ark/util" import { scope, type } from "arktype" import { writeInvalidSpreadTypeMessage, diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 452d35adbf..d39435ac0a 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -1,14 +1,14 @@ import { attest, contextualize } from "@ark/attest" -import type { - AtLeastLength, - AtMostLength, - Narrowed, - number, - of, - Out, - string +import { + type AtLeastLength, + type AtMostLength, + type Narrowed, + type number, + type of, + type Out, + registeredReference, + type string } from "@ark/schema" -import { registeredReference } from "@ark/util" import { scope, type, type Type } from "arktype" import type { Module } from "../module.js" diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index a6906b6949..ed9d6c4dcf 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -234,7 +234,7 @@ contextualize(() => { b: { a: "a|true" } }).export() attest(types).type.toString.snap( - "Module<{ a: { b: false | { a: true | ...; }; }; b: { a: true | { b: false | ...; }; }; }>" + "Module<{ b: { a: true | { b: false | ...; }; }; a: { b: false | { a: true | ...; }; }; }>" ) }) @@ -244,7 +244,7 @@ contextualize(() => { b: { a: "a&b" } }).export() attest(types).type.toString.snap( - "Module<{ a: { b: { a: { b: ...; a: ...; }; b: ...; }; }; b: { a: { b: { a: ...; b: ...; }; a: ...; }; }; }>" + "Module<{ b: { a: { b: { a: ...; b: ...; }; a: ...; }; }; a: { b: { a: { b: ...; a: ...; }; b: ...; }; }; }>" ) }) diff --git a/ark/type/__tests__/type.test.ts b/ark/type/__tests__/type.test.ts index a3ffe5a0db..c85256046e 100644 --- a/ark/type/__tests__/type.test.ts +++ b/ark/type/__tests__/type.test.ts @@ -41,4 +41,38 @@ contextualize(() => { "AggregateError: a must be a string (was number)" ) }) + + describe("as", () => { + it("valid cast", () => { + const from = type("/^foo.*$/") + const t = from.as<`foo${string}`>() + + attest<`foo${string}`>(t.t) + attest(t === from).equals(true) + }) + + it("cast to any", () => { + const t = type("unknown").as() + attest(t.t) + }) + + it("cast to never", () => { + const t = type("unknown").as() + attest(t.t) + }) + + it("missing type param", () => { + // @ts-expect-error + attest(() => type("string").as()).type.errors.snap( + "Expected 1 arguments, but got 0." + ) + }) + + it("missing type param with arg", () => { + // @ts-expect-error + attest(() => type("string").as("foo")).type.errors( + "as requires an explicit type parameter like myType.as() " + ) + }) + }) }) diff --git a/ark/type/ark.ts b/ark/type/ark.ts index fae44279b7..ed5c27460a 100644 --- a/ark/type/ark.ts +++ b/ark/type/ark.ts @@ -4,16 +4,11 @@ import { type ArkErrors, type inferred } from "@ark/schema" -import type { CastableBase } from "@ark/util" +import type { GenericHktParser } from "./generic.js" import type { MatchParser } from "./match.js" import type { Module } from "./module.js" import { scope, type Scope } from "./scope.js" -import type { - DeclarationParser, - DefinitionParser, - Type, - TypeParser -} from "./type.js" +import type { DeclarationParser, DefinitionParser, TypeParser } from "./type.js" export const ambient: Scope = scope(keywordNodes) as never @@ -27,16 +22,10 @@ export namespace type { } export type errors = ArkErrors - - export interface of extends Type {} - - export interface infer> - extends CastableBase {} - - export interface inferIn> - extends CastableBase {} } +export const generic: GenericHktParser<{}> = ambient.generic as never + export const match: MatchParser<{}> = ambient.match as never export const define: DefinitionParser<{}> = ambient.define as never diff --git a/ark/type/generic.ts b/ark/type/generic.ts index 5e285d77b8..bacde9b27a 100644 --- a/ark/type/generic.ts +++ b/ark/type/generic.ts @@ -1,8 +1,10 @@ import type { GenericParamAst, GenericParamDef, + genericParamSchemasToAst, + GenericProps, GenericRoot, - writeUnsatisfiedParameterConstraintMessage + LazyGenericBody } from "@ark/schema" import { throwParseError, @@ -11,8 +13,8 @@ import { type Callable, type conform, type ErrorMessage, + type Hkt, type keyError, - type typeToString, type WhiteSpaceToken } from "@ark/util" import type { inferDefinition } from "./parser/definition.js" @@ -35,37 +37,34 @@ export type validateParameterString = ErrorMessage : s -export type validateGenericArg = - validateTypeRoot extends infer result ? - result extends ErrorMessage ? result - : inferTypeRoot extends param[1] ? def - : ErrorMessage< - writeUnsatisfiedParameterConstraintMessage< - param[0], - typeToString, - "" - > - > - : never - -export type GenericInstantiation< - params extends array = array, - def = any, - $ = any -> = ( - ...args: conform< - args, - { - [i in keyof params]: validateGenericArg< - params[i], - args[i & keyof args], - $ - > - } - > -) => Type>, $> +export type validateGenericArg = + inferTypeRoot extends param[1] ? arg : Type + +export type GenericInstantiator< + params extends array, + def, + $, + args$ +> = < + const args extends { + [i in keyof params]: validateTypeRoot + } +>( + /** @ts-expect-error treat as array */ + ...args: { + [i in keyof args]: validateGenericArg< + args[i], + params[i & keyof params & `${number}`], + args$ + > + } +) => Type< + def extends Hkt.Kind ? + Hkt.apply }> + : inferDefinition>, + $ +> -// TODO: Fix external reference (i.e. if this is attached to a scope, then args are defined using it) type bindGenericArgs, $, args> = { [i in keyof params & `${number}` as params[i][0]]: inferTypeRoot< args[i & keyof args], @@ -80,9 +79,12 @@ export type baseGenericArgs> = { export interface Generic< params extends array = array, bodyDef = unknown, - $ = {} -> extends Callable>, - GenericRoot {} + $ = {}, + args$ = $ +> extends Callable>, + GenericProps { + internal: GenericRoot +} export type GenericDeclaration< name extends string = string, @@ -218,3 +220,29 @@ type _parseOptionalConstraint< [...result, [name, unknown]], $ > + +export type GenericHktParser<$ = {}> = < + const paramsDef extends array +>( + ...params: paramsDef +) => ( + instantiateDef: LazyGenericBody> +) => GenericHktSubclass, $> + +export type GenericHktSubclass< + params extends array, + $ +> = abstract new () => GenericHkt< + genericParamSchemasToAst, + Hkt.Kind, + $, + $ +> + +export interface GenericHkt< + params extends array, + hkt extends Hkt.Kind, + $, + args$ +> extends Generic, + Hkt.Kind {} diff --git a/ark/type/index.ts b/ark/type/index.ts index 009b77af58..d1f8a1182c 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -1,6 +1,6 @@ export { ArkError, ArkErrors as ArkErrors } from "@ark/schema" export type { Ark, ArkConfig, Out, constrained, inferred } from "@ark/schema" -export { ambient, ark, declare, define, match, type } from "./ark.js" +export { ambient, ark, declare, define, generic, match, type } from "./ark.js" export { Module } from "./module.js" export { scope, diff --git a/ark/type/match.ts b/ark/type/match.ts index ec941cd2c0..4e55d1f672 100644 --- a/ark/type/match.ts +++ b/ark/type/match.ts @@ -144,7 +144,7 @@ type MatchInvocationContext = { } export type MatchInvocation = < - data extends ctx["initialInputs"] + const data extends ctx["initialInputs"] >( data: data ) => { diff --git a/ark/type/module.ts b/ark/type/module.ts index 5755855819..42cd036588 100644 --- a/ark/type/module.ts +++ b/ark/type/module.ts @@ -1,12 +1,17 @@ -import { RootModule, type PreparsedNodeResolution } from "@ark/schema" -import type { anyOrNever } from "@ark/util" +import { RootModule, type arkKind, type GenericProps } from "@ark/schema" +import type { anyOrNever, Hkt } from "@ark/util" +import type { Generic, GenericHkt } from "./generic.js" import type { Type } from "./type.js" type exportScope<$> = { - [k in keyof $]: $[k] extends PreparsedNodeResolution ? + [k in keyof $]: $[k] extends { [arkKind]: "module" } ? [$[k]] extends [anyOrNever] ? Type<$[k], $> : $[k] + : $[k] extends GenericProps ? + $[k] extends Hkt.Kind ? + GenericHkt + : Generic : Type<$[k], $> } diff --git a/ark/type/package.json b/ark/type/package.json index 74a00597cd..2f2ecec82e 100644 --- a/ark/type/package.json +++ b/ark/type/package.json @@ -1,7 +1,7 @@ { "name": "arktype", "description": "TypeScript's 1:1 validator, optimized from editor to runtime", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/type/parser/semantic/divisor.ts b/ark/type/parser/semantic/divisor.ts index 86e42dfd08..57386b0598 100644 --- a/ark/type/parser/semantic/divisor.ts +++ b/ark/type/parser/semantic/divisor.ts @@ -1,4 +1,4 @@ -import type { Root, writeIndivisibleMessage } from "@ark/schema" +import type { SchemaRoot, writeIndivisibleMessage } from "@ark/schema" import type { ErrorMessage } from "@ark/util" import type { inferAstIn } from "./infer.js" import type { validateAst } from "./validate.js" @@ -7,5 +7,5 @@ export type validateDivisor = inferAstIn extends infer data ? [data] extends [number] ? validateAst - : ErrorMessage>> + : ErrorMessage>> : never diff --git a/ark/type/parser/semantic/infer.ts b/ark/type/parser/semantic/infer.ts index a6eef1fe89..d2a43c43ff 100644 --- a/ark/type/parser/semantic/infer.ts +++ b/ark/type/parser/semantic/infer.ts @@ -12,7 +12,7 @@ import type { normalizeLimit, string } from "@ark/schema" -import type { BigintLiteral, array } from "@ark/util" +import type { BigintLiteral, Hkt, array } from "@ark/util" import type { UnparsedScope, resolve, @@ -40,25 +40,29 @@ export type GenericInstantiationAst< export type inferExpression = ast extends GenericInstantiationAst ? - inferDefinition< - generic["bodyDef"], - generic["$"]["t"] extends UnparsedScope ? - // If the generic was defined in the current scope, its definition can be - // resolved using the same scope as that of the input args. - $ - : // Otherwise, use the scope that was explicitly associated with it. - generic["$"]["t"], - { - // Using keyof g["params"] & number here results in the element types - // being mixed- another reason TS should not have separate `${number}` and number keys! - [i in keyof generic["params"] & `${number}` as generic["names"][i & - keyof generic["names"]]]: inferConstrainableAst< - argAsts[i & keyof argAsts], - $, - args - > - } - > + generic extends Hkt.Kind ? + Hkt.apply< + generic, + { [i in keyof argAsts]: inferConstrainableAst } + > + : inferDefinition< + generic["bodyDef"], + generic["$"]["t"] extends UnparsedScope ? + // If the generic was defined in the current scope, its definition can be + // resolved using the same scope as that of the input args. + $ + : // Otherwise, use the scope that was explicitly associated with it. + generic["$"]["t"], + { + // Using keyof g["params"] & number here results in the element types being mixed + [i in keyof generic["params"] & `${number}` as generic["names"][i & + keyof generic["names"]]]: inferConstrainableAst< + argAsts[i & keyof argAsts], + $, + args + > + } + > : ast[1] extends "[]" ? inferConstrainableAst[] : ast[1] extends "|" ? | inferConstrainableAst diff --git a/ark/type/scope.ts b/ark/type/scope.ts index f2236a354c..59e242dc44 100644 --- a/ark/type/scope.ts +++ b/ark/type/scope.ts @@ -1,18 +1,18 @@ import { - RawRootScope, + InternalBaseScope, hasArkKind, parseGeneric, type AliasDefEntry, type ArkConfig, type BaseRoot, + type BaseScope, type GenericArgResolutions, type GenericParamAst, type GenericParamDef, type GenericProps, + type InternalResolutions, type PreparsedNodeResolution, type PrivateDeclaration, - type RawRootResolutions, - type RootScope, type arkKind, type destructuredExportContext, type destructuredImportContext, @@ -36,6 +36,7 @@ import { parseGenericParams, type Generic, type GenericDeclaration, + type GenericHktParser, type ParameterString, type baseGenericArgs, type parseValidGenericParams @@ -57,7 +58,7 @@ import { type StringParseResult } from "./parser/string/string.js" import { - RawTypeParser, + InternalTypeParser, type DeclarationParser, type DefinitionParser, type Type, @@ -174,7 +175,7 @@ export type tryInferSubmoduleReference<$, token> = : never export interface ParseContext extends TypeParseOptions { - $: RawScope + $: InternalScope } export interface TypeParseOptions { @@ -182,40 +183,22 @@ export interface TypeParseOptions { } export const scope: ScopeParser = ((def: Dict, config: ArkConfig = {}) => - new RawScope(def, config)) as never + new InternalScope(def, config)) as never -export interface Scope<$ = any> extends RootScope<$> { - type: TypeParser<$> - - match: MatchParser<$> - - declare: DeclarationParser<$> - - define: DefinitionParser<$> - - import[]>( - ...names: names - ): Module>> - - export[]>( - ...names: names - ): Module>> -} - -export class RawScope< - $ extends RawRootResolutions = RawRootResolutions -> extends RawRootScope<$> { +export class InternalScope< + $ extends InternalResolutions = InternalResolutions +> extends InternalBaseScope<$> { private parseCache: Record = {} constructor(def: Record, config?: ArkConfig) { super(def, config) } - type: RawTypeParser = new RawTypeParser(this as never) + type: InternalTypeParser = new InternalTypeParser(this as never) match: MatchParser<$> = createMatchParser(this as never) as never - declare: () => { type: RawTypeParser } = (() => ({ + declare: () => { type: InternalTypeParser } = (() => ({ type: this.type })).bind(this) @@ -258,7 +241,7 @@ export class RawScope< } @bound - override parseRoot(def: unknown, opts: TypeParseOptions = {}): BaseRoot { + parseRoot(def: unknown, opts: TypeParseOptions = {}): BaseRoot { const node: BaseRoot = this.parse( def, Object.assign( @@ -318,6 +301,28 @@ export class RawScope< } } +export interface Scope<$ = any> extends BaseScope<$> { + type: TypeParser<$> + + match: MatchParser<$> + + declare: DeclarationParser<$> + + define: DefinitionParser<$> + + generic: GenericHktParser<$> + + import[]>( + ...names: names + ): Module>> + + export[]>( + ...names: names + ): Module>> +} + +export const Scope: new <$ = any>() => Scope<$> = InternalScope as never + export const writeShallowCycleErrorMessage = ( name: string, seen: string[] diff --git a/ark/type/type.ts b/ark/type/type.ts index 12b9e205f4..83116be290 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -1,8 +1,8 @@ import { ArkErrors, BaseRoot, + GenericRoot, type BaseMeta, - type ConstraintKind, type Disjoint, type DivisorSchema, type ExactLengthSchema, @@ -10,35 +10,36 @@ import { type ExclusiveNumericRangeSchema, type InclusiveDateRangeSchema, type InclusiveNumericRangeSchema, - type InnerRoot, type Morph, type MorphAst, type NodeSchema, type Out, type PatternSchema, type Predicate, - type Prerequisite, type PrimitiveConstraintKind, type Root, + type arkKeyOf, type constrain, type constraintKindOf, type distillIn, type distillOut, type exclusivizeRangeSchema, - type indexInto, - type indexOf, + type getArkKey, type inferIntersection, type inferMorphOut, type inferPipes, type inferPredicate, - type writeInvalidOperandMessage + type toArkKey, + type validateChainedAsArgs, + type validateChainedConstraint, + type validateStructuralOperand } from "@ark/schema" import { Callable, type Constructor, - type ErrorMessage, type array, - type conform + type conform, + type unset } from "@ark/util" import type { type } from "./ark.js" import { @@ -59,7 +60,7 @@ import type { IndexZeroOperator, TupleInfixOperator } from "./parser/tuple.js" -import type { RawScope, Scope, bindThis } from "./scope.js" +import type { InternalScope, Scope, bindThis } from "./scope.js" /** The convenience properties attached to `type` */ export type TypeParserAttachments = @@ -101,11 +102,11 @@ export interface TypeParser<$ = {}> { errors: typeof ArkErrors } -export class RawTypeParser extends Callable< +export class InternalTypeParser extends Callable< (...args: unknown[]) => BaseRoot | Generic, TypeParserAttachments > { - constructor($: RawScope) { + constructor($: InternalScope) { super( (...args) => { if (args.length === 1) { @@ -124,8 +125,13 @@ export class RawTypeParser extends Callable< $, args: {} }) - const def = args[1] - return $.generic(params, def) as never + + return new GenericRoot( + params, + args[1], + $ as never, + $ 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 @@ -150,18 +156,13 @@ export type DeclarationParser<$> = () => { ) => Type } -type validateChainedConstraint< - kind extends ConstraintKind, - t extends { inferIn: unknown } -> = - t["inferIn"] extends Prerequisite ? t - : ErrorMessage>> - // this is declared as a class internally so we can ensure all "abstract" // methods of BaseRoot are overridden, but we end up exporting it as an interface // to ensure it is not accessed as a runtime value -declare class _Type extends InnerRoot { - $: Scope<$>; +declare class _Type extends Root { + $: Scope<$> + + as(...args: validateChainedAsArgs): Type get in(): Type get out(): Type @@ -251,28 +252,53 @@ declare class _Type extends InnerRoot { def: validateTypeRoot ): this is Type, $> - // TODO: i/o - extract(r: validateTypeRoot): Type - exclude(r: validateTypeRoot): Type + extract( + r: validateTypeRoot + ): Type>, $> + exclude( + r: validateTypeRoot + ): Type>, $> + extends( other: validateTypeRoot ): this is Type, $> + overlaps(r: validateTypeRoot): boolean - get>(k1: k1 | type.cast): Type, $> - get, k2 extends indexOf>>( + omit = never>( + this: validateStructuralOperand<"omit", this>, + ...keys: array> + ): Type< + { + [k in keyof t as Exclude, key>]: t[k] + }, + $ + > + + pick = never>( + this: validateStructuralOperand<"pick", this>, + ...keys: array> + ): Type< + { + [k in keyof t as Extract, key>]: t[k] + }, + $ + > + + get>(k1: k1 | type.cast): Type, $> + get, k2 extends arkKeyOf>>( k1: k1 | type.cast, k2: k2 | type.cast - ): Type, k2>, $> + ): Type, k2>, $> get< - k1 extends indexOf, - k2 extends indexOf>, - k3 extends indexOf, k2>> + k1 extends arkKeyOf, + k2 extends arkKeyOf>, + k3 extends arkKeyOf, k2>> >( k1: k1 | type.cast, k2: k2 | type.cast, k3: k3 | type.cast - ): Type, k2>, k3>, $> + ): Type, k2>, k3>, $> constrain< kind extends PrimitiveConstraintKind, diff --git a/ark/util/__tests__/registry.test.ts b/ark/util/__tests__/registry.test.ts new file mode 100644 index 0000000000..8d081ce849 --- /dev/null +++ b/ark/util/__tests__/registry.test.ts @@ -0,0 +1,9 @@ +import { attest, contextualize } from "@ark/attest" +import { arkUtilVersion } from "@ark/util" +import { version } from "../package.json" with { type: "json" } + +contextualize(() => { + it("version matches package.json", () => { + attest(arkUtilVersion).equals(version) + }) +}) diff --git a/ark/util/__tests__/traits.test.ts b/ark/util/__tests__/traits.test.ts index 555fcffe93..8d123b0b1b 100644 --- a/ark/util/__tests__/traits.test.ts +++ b/ark/util/__tests__/traits.test.ts @@ -146,7 +146,7 @@ contextualize(() => { class B extends Trait<{ abstractMethods: { b(): number } }> {} // @ts-expect-error attest(class C extends implement(A, B, {}) {}).type.errors( - "Type '{}' is missing the following properties from type '{ a: () => number; b: () => number; }': a, b" + `Type '{}' is missing the following properties from type '{ b: () => number; a: () => number; }': b, a` ) }) }) diff --git a/ark/util/arrays.ts b/ark/util/arrays.ts index dfbab61b65..a5760c1641 100644 --- a/ark/util/arrays.ts +++ b/ark/util/arrays.ts @@ -1,3 +1,5 @@ +import type { Guardable } from "./functions.js" +import type { anyOrNever } from "./generics.js" import type { isDisjoint } from "./intersections.js" import type { parseNonNegativeInteger } from "./numericLiterals.js" @@ -98,28 +100,32 @@ export type initOf = export type numericStringKeyOf = Extract -export type indexOf = +export type arrayIndexOf = keyof a extends infer k ? parseNonNegativeInteger : never -export const arrayFrom = ( - data: t -): t extends array ? - [t] extends [null] ? - // check for any/never - t[] - : t -: t[] => (Array.isArray(data) ? data : [data]) as never +export type liftArray = + t extends array ? + [t] extends [anyOrNever] ? + t[] + : t + : t[] + +export const liftArray = (data: t): liftArray => + (Array.isArray(data) ? data : [data]) as never export const spliterate = ( list: readonly item[], - by: (item: item) => item is included -): [included: included[], excluded: Exclude[]] => { + by: Guardable +): [ + included: included[], + excluded: item extends included ? item[] : Exclude[] +] => { const result: [any[], any[]] = [[], []] for (const item of list) { if (by(item)) result[0].push(item) else result[1].push(item) } - return result + return result as never } export const ReadonlyArray = Array as unknown as new ( @@ -183,7 +189,7 @@ export const conflatenate = ( if (elementOrList === undefined || elementOrList === null) return to ?? ([] as never) - if (to === undefined || to === null) return arrayFrom(elementOrList) as never + if (to === undefined || to === null) return liftArray(elementOrList) as never return to.concat(elementOrList) as never } @@ -219,7 +225,7 @@ export const appendUnique = ( return Array.isArray(value) ? (value as never) : ([value] as never) const isEqual = opts?.isEqual ?? ((l, r) => l === r) - arrayFrom(value).forEach(v => { + liftArray(value).forEach(v => { if (!to.some(existing => isEqual(existing as never, v as never))) to.push(v) }) diff --git a/ark/util/compilation.ts b/ark/util/compilation.ts deleted file mode 100644 index b7504209e4..0000000000 --- a/ark/util/compilation.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { DynamicFunction } from "./functions.js" -import { CastableBase } from "./records.js" -import { isDotAccessible, registeredReference } from "./registry.js" - -export type CoercibleValue = string | number | boolean | null | undefined - -export class CompiledFunction< - args extends readonly string[] -> extends CastableBase<{ - [k in args[number]]: k -}> { - readonly argNames: args - readonly body = "" - - constructor(...args: args) { - super() - this.argNames = args - for (const arg of args) { - if (arg in this) { - throw new Error( - `Arg name '${arg}' would overwrite an existing property on FunctionBody` - ) - } - ;(this as any)[arg] = arg - } - } - - indentation = 0 - indent(): this { - this.indentation += 4 - return this - } - - dedent(): this { - this.indentation -= 4 - return this - } - - prop(key: PropertyKey, optional = false): string { - return compileLiteralPropAccess(key, optional) - } - - index(key: string | number, optional = false): string { - return indexPropAccess(`${key}`, optional) - } - - line(statement: string): this { - ;(this.body as any) += `${" ".repeat(this.indentation)}${statement}\n` - return this - } - - const(identifier: string, expression: CoercibleValue): this { - this.line(`const ${identifier} = ${expression}`) - return this - } - - let(identifier: string, expression: CoercibleValue): this { - return this.line(`let ${identifier} = ${expression}`) - } - - set(identifier: string, expression: CoercibleValue): this { - return this.line(`${identifier} = ${expression}`) - } - - if(condition: string, then: (self: this) => this): this { - return this.block(`if (${condition})`, then) - } - - elseIf(condition: string, then: (self: this) => this): this { - return this.block(`else if (${condition})`, then) - } - - else(then: (self: this) => this): this { - return this.block("else", then) - } - - /** Current index is "i" */ - for( - until: string, - body: (self: this) => this, - initialValue: CoercibleValue = 0 - ): this { - return this.block(`for (let i = ${initialValue}; ${until}; i++)`, body) - } - - /** Current key is "k" */ - forIn(object: string, body: (self: this) => this): this { - return this.block(`for (const k in ${object})`, body) - } - - block(prefix: string, contents: (self: this) => this, suffix = ""): this { - this.line(`${prefix} {`) - this.indent() - contents(this) - this.dedent() - return this.line(`}${suffix}`) - } - - return(expression: CoercibleValue = ""): this { - return this.line(`return ${expression}`) - } - - compile< - f extends ( - ...args: { - [i in keyof args]: never - } - ) => unknown - >(): f { - return new DynamicFunction(...this.argNames, this.body) - } -} - -export const compileLiteralPropAccess = ( - key: PropertyKey, - optional = false -): string => { - if (typeof key === "string" && isDotAccessible(key)) - return `${optional ? "?" : ""}.${key}` - - return indexPropAccess(serializeLiteralKey(key), optional) -} - -export const serializeLiteralKey = (key: PropertyKey): string => - typeof key === "symbol" ? registeredReference(key) : JSON.stringify(key) - -export const indexPropAccess = (key: string, optional = false): string => - `${optional ? "?." : ""}[${key}]` diff --git a/ark/util/functions.ts b/ark/util/functions.ts index a68f45b1c6..c606a2a347 100644 --- a/ark/util/functions.ts +++ b/ark/util/functions.ts @@ -92,7 +92,11 @@ export class Callable< f extends (...args: never[]) => unknown, attachments extends object = {} > extends NoopBase { - constructor(f: f, opts?: CallableOptions) { + constructor( + f: f, + ...[opts]: {} extends attachments ? [opts?: CallableOptions] + : [opts: CallableOptions] + ) { super() return Object.assign( Object.setPrototypeOf( diff --git a/ark/util/index.ts b/ark/util/index.ts index 1b93daacfe..e2e1b9ad8a 100644 --- a/ark/util/index.ts +++ b/ark/util/index.ts @@ -1,6 +1,5 @@ export * from "./arrays.js" export * from "./clone.js" -export * from "./compilation.js" export * from "./describe.js" export * from "./domain.js" export * from "./errors.js" diff --git a/ark/util/package.json b/ark/util/package.json index 594007b61c..c673602a58 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/util/registry.ts b/ark/util/registry.ts index 2590c427c4..eecc01226b 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -1,34 +1,36 @@ -import { domainOf, hasDomain } from "./domain.js" -import { throwError, throwInternalError } from "./errors.js" +import { domainOf } from "./domain.js" +import { throwInternalError } from "./errors.js" import { objectKindOf } from "./objectKinds.js" -import { serializePrimitive, type SerializablePrimitive } from "./primitive.js" import type { PartialRecord } from "./records.js" -declare global { - export const $ark: ArkEnv.registry +// Eventually we can just import from package.json in the source itself +// but for now, import assertions are too unstable and it wouldn't support +// recent node versions (https://nodejs.org/api/esm.html#json-modules). + +// For now, we assert this matches the package.json version via a unit test. +export const arkUtilVersion = "0.1.1" + +export const initialRegistryContents = { + version: arkUtilVersion, + filename: import.meta.filename +} +export type InitialRegistryContents = typeof initialRegistryContents + +export const $ark: ArkEnv.registry = initialRegistryContents as never + +declare global { export interface ArkEnv { registry(): {} } export namespace ArkEnv { export type registry = PartialRecord & + InitialRegistryContents & ReturnType } } -if ("$ark" in globalThis) { - throwError( - `Tried to initialize an $ark registry but one already existed. -This probably means you are either depending on multiple versions of an arktype package, -or importing the same package from both ESM and CJS. -Review package.json versions across your repo to ensure consistency.` - ) -} - -export const registry: Record = {} -;(globalThis as any).$ark = registry - const namesByResolution = new WeakMap() const nameCounts: Record = {} @@ -40,27 +42,14 @@ export const register = (value: object | symbol): string => { if (nameCounts[name]) name = `${name}${nameCounts[name]!++}` else nameCounts[name] = 1 - registry[name] = value + $ark[name] = value namesByResolution.set(value, name) return name } -export const reference = (name: string): RegisteredReference => `$ark.${name}` - -export const registeredReference = ( - value: object | symbol -): RegisteredReference => reference(register(value)) - -export type RegisteredReference = `$ark.${to}` - export const isDotAccessible = (keyName: string): boolean => /^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(keyName) -export const compileSerializedValue = (value: unknown): string => - hasDomain(value, "object") || typeof value === "symbol" ? - registeredReference(value) - : serializePrimitive(value as SerializablePrimitive) - const baseNameFor = (value: object | symbol) => { switch (typeof value) { case "object": { diff --git a/package.json b/package.json index 708ede5270..d0bac18498 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "type": "module", "private": true, "scripts": { - "prChecks": "pnpm lint && pnpm build && pnpm testRepo && pnpm bench && pnpm testTsVersions", + "prChecks": "pnpm lint && pnpm build && pnpm testRepo && pnpm testTsVersions", "build": "pnpm -r build", "buildCjs": "ARKTYPE_CJS=1 pnpm -r build", "test": "pnpm testTyped --skipTypes", @@ -92,5 +92,5 @@ "eslintConfig": { "extends": "./ark/repo/.eslintrc.cjs" }, - "packageManager": "pnpm@9.5.0" + "packageManager": "pnpm@9.6.0" } diff --git a/tsconfig.json b/tsconfig.json index 3359a41508..9b3444f70c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ // should be off by default, but here as a convenience to toggle // "noErrorTruncation": true, // "isolatedDeclarations": true, + "resolveJsonModule": true, "paths": { "arktype": ["./ark/type/index.ts"], "arktype/config": ["./ark/type/config.ts"],