Skip to content

Commit

Permalink
feat: more precise string default parsing, improved cyclic union reso…
Browse files Browse the repository at this point in the history
…lutions (#1028)
  • Loading branch information
ssalbdivad authored Jun 24, 2024
1 parent c5569dd commit 5fe79c6
Show file tree
Hide file tree
Showing 32 changed files with 487 additions and 363 deletions.
7 changes: 7 additions & 0 deletions .changeset/wet-suns-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@arktype/attest": patch
"@arktype/schema": patch
"@arktype/util": patch
---

Bump version
2 changes: 1 addition & 1 deletion ark/attest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"bunTest": "bun test --preload ../repo/bunTestSetup.ts"
},
"dependencies": {
"arktype": "latest",
"arktype": "*",
"@arktype/fs": "workspace:*",
"@arktype/util": "workspace:*",
"@typescript/vfs": "1.5.3",
Expand Down
3 changes: 1 addition & 2 deletions ark/schema/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export type GenericNodeInstantiation<
...args: conform<args, repeat<[RootSchema], params["length"]>>
) => Root<inferRoot<def, $ & bindGenericNodeInstantiation<params, $, args>>>

// TODO: ????
export type bindGenericNodeInstantiation<params extends string[], $, args> = {
[i in keyof params & `${number}` as params[i]]: inferRoot<
args[i & keyof args],
Expand Down Expand Up @@ -63,7 +62,7 @@ export class GenericRoot<params extends string[] = string[], def = any, $ = any>
})
}

bindScope($: RawRootScope) {
bindScope($: RawRootScope): never {
throw new Error(`Unimplemented generic bind ${$}`)
}

Expand Down
1 change: 1 addition & 0 deletions ark/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from "./shared/utils.js"
export * from "./structure/indexed.js"
export * from "./structure/optional.js"
export * from "./structure/prop.js"
export * from "./structure/required.js"
export * from "./structure/sequence.js"
export * from "./structure/structure.js"

Expand Down
4 changes: 2 additions & 2 deletions ark/schema/roots/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ export class AliasNode extends BaseRoot<AliasDeclaration> {

traverseAllows: TraverseAllows = (data, ctx) => {
const seen = ctx.seen[this.id]
if (seen?.includes(data as object)) return true
if (seen?.includes(data)) return true
ctx.seen[this.id] = append(seen, data)
return this.resolution.traverseAllows(data, ctx)
}

traverseApply: TraverseApply = (data, ctx) => {
const seen = ctx.seen[this.id]
if (seen?.includes(data as object)) return
if (seen?.includes(data)) return
ctx.seen[this.id] = append(seen, data)
this.resolution.traverseApply(data, ctx)
}
Expand Down
42 changes: 20 additions & 22 deletions ark/schema/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,22 +150,7 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions>
// ensure exportedResolutions is populated
$ark.ambient.export()
// TODO: generics and modules
this.resolutions = flatMorph(
$ark.ambient.resolutions,
(alias, resolution) =>
// an alias defined in this scope should override an ambient alias of the same name
alias in this.aliases ?
[]
: [
alias,
(
hasArkKind(resolution, "root") ||
hasArkKind(resolution, "generic")
) ?
resolution.internal
: resolution
]
)
this.resolutions = {}
}
scopesById[this.id] = this
}
Expand Down Expand Up @@ -310,15 +295,28 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions>
maybeShallowResolve(name: string): CachedResolution | undefined {
const cached = this.resolutions[name]
if (cached) return cached
let def = this.aliases[name]
const def = this.aliases[name] ?? $ark.ambient.resolutions[name]

if (!def) return this.maybeResolveSubalias(name)
def = this.preparseRoot(def)
if (hasArkKind(def, "generic"))
return (this.resolutions[name] = def.validateBaseInstantiation())

if (hasArkKind(def, "module")) return (this.resolutions[name] = def)
const preparsed = this.preparseRoot(def)
if (hasArkKind(preparsed, "generic")) {
return (this.resolutions[name] = preparsed
.validateBaseInstantiation()
?.bindScope(this))
}

if (hasArkKind(preparsed, "module")) {
return (this.resolutions[name] = new RootModule(
flatMorph(preparsed, (alias, node) => [
alias,
(node as BaseRoot | GenericRoot).bindScope(this)
])
))
}

this.resolutions[name] = name
return (this.resolutions[name] = this.parseRoot(def))
return (this.resolutions[name] = this.parseRoot(preparsed).bindScope(this))
}

/** If name is a valid reference to a submodule alias, return its resolution */
Expand Down
16 changes: 14 additions & 2 deletions ark/schema/shared/intersections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,24 @@ export const intersectNodes: InternalNodeIntersection<IntersectionContext> = (

let result: UnknownIntersectionResult

if (ctx.pipe && l.hasKind("morph")) {
if (ctx.pipe && l.includesMorph) {
if (!l.hasKind("morph")) {
return ctx.$.node("morph", {
morphs: [r],
in: l
})
}
result =
ctx.invert ?
pipeToMorph(r as never, l, ctx)
: pipeFromMorph(l, r as never, ctx)
} else if (ctx.pipe && r.hasKind("morph")) {
} else if (ctx.pipe && r.includesMorph) {
if (!r.hasKind("morph")) {
return ctx.$.node("morph", {
morphs: [r],
in: l
})
}
result =
ctx.invert ?
pipeFromMorph(r, l as never, ctx)
Expand Down
2 changes: 1 addition & 1 deletion ark/schema/shared/traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class TraversalContext {
errors: ArkErrors = new ArkErrors(this)
branches: BranchTraversalContext[] = []

seen: { [id in string]?: object[] } = {}
seen: { [id in string]?: unknown[] } = {}

constructor(
public root: unknown,
Expand Down
19 changes: 19 additions & 0 deletions ark/type/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# arktype

## 2.0.0-dev.26

### Improved string default parsing

String defaults are now parsed more efficiently by the core string parser. They can include arbitrary whitespace and give more specific errors.

### Fix a resolution issue on certain cyclic unions

```ts
// Now resolves correctly
const types = scope({
TypeWithKeywords: "ArraySchema",
Schema: "number|ArraySchema",
ArraySchema: {
"additionalItems?": "Schema"
}
}).export()
```

## 2.0.0-dev.25

### String defaults
Expand Down
11 changes: 2 additions & 9 deletions ark/type/__tests__/bounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import {
writeOpenRangeMessage,
writeUnpairableComparatorMessage
} from "../parser/string/reduce/shared.js"
import {
singleEqualsMessage,
writeInvalidLimitMessage
} from "../parser/string/shift/operator/bounds.js"
import { writeInvalidLimitMessage } from "../parser/string/shift/operator/bounds.js"
import { shallowDefaultMessage } from "../parser/string/shift/operator/default.js"

contextualize(
"string expressions",
Expand Down Expand Up @@ -155,11 +153,6 @@ contextualize(
attest(t.allows(new Date(now.valueOf() + 1000))).equals(false)
})

it("single equals", () => {
// @ts-expect-error
attest(() => type("string=5")).throwsAndHasTypeError(singleEqualsMessage)
})

it("invalid left comparator", () => {
// @ts-expect-error
attest(() => type("3>number<5")).throwsAndHasTypeError(
Expand Down
39 changes: 29 additions & 10 deletions ark/type/__tests__/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { attest, contextualize } from "@arktype/attest"
import type { Date, Default } from "@arktype/schema"
import { scope, type } from "arktype"
import { invalidDefaultKeyKindMessage } from "../parser/objectLiteral.js"
import { singleEqualsMessage } from "../parser/string/shift/operator/bounds.js"
import {
shallowDefaultMessage,
writeNonLiteralDefaultMessage
} from "../parser/string/shift/operator/default.js"

contextualize(
"parsing and traversal",
Expand Down Expand Up @@ -41,7 +44,7 @@ contextualize(
.throws.snap(
'ParseError: Default value at "bar" must be a number (was string)'
)
.type.errors("Type 'string' is not assignable to type 'number'")
.type.errors()
})

it("optional with default", () => {
Expand Down Expand Up @@ -155,16 +158,31 @@ contextualize(
attest(() =>
// @ts-expect-error
type({ foo: "string", bar: "unknown = number" })
).throwsAndHasTypeError(singleEqualsMessage)
).throwsAndHasTypeError(writeNonLiteralDefaultMessage("number"))
})

// https://github.com/arktypeio/arktype/issues/1017
// it("validated default in scope", () => {
// const $ = scope({
// specialNumber: "number",
// obj: { foo: "string", bar: "specialNumber =5" }
// })
// })
it("validated default in scope", () => {
const $ = scope({
specialNumber: "number",
obj: { foo: "string", bar: "specialNumber = 5" }
})

$.export()

attest($.json).snap({
specialNumber: { domain: "number" },
obj: {
required: [{ key: "foo", value: "string" }],
optional: [{ default: 5, key: "bar", value: "number" }],
domain: "object"
}
})
})

it("shallow default", () => {
// would be ideal if this was a type error as well
attest(() => type("string='foo'")).throws(shallowDefaultMessage)
})

it("optional with default", () => {
attest(() =>
Expand All @@ -185,6 +203,7 @@ contextualize(
domain: "object"
})
})

it("same default", () => {
const l = type({ bar: ["number", "=", 5] })
const r = type({ bar: ["5", "=", 5] })
Expand Down
5 changes: 4 additions & 1 deletion ark/type/__tests__/objectLiteral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
writeInvalidSpreadTypeMessage,
writeInvalidUndeclaredBehaviorMessage
} from "../parser/objectLiteral.js"
import { writeUnexpectedCharacterMessage } from "../parser/string/shift/operator/operator.js"

contextualize(
"named",
Expand Down Expand Up @@ -412,7 +413,9 @@ other must be a string (was bigint)`)
// @ts-expect-error
"[unresolvable]": "'unresolvable' is unresolvable"
})
).throwsAndHasTypeError(writeUnresolvableMessage("unresolvable"))
)
.throws(writeUnexpectedCharacterMessage("i"))
.type.errors(writeUnresolvableMessage("unresolvable"))
})

it("semantic error in index definition", () => {
Expand Down
52 changes: 26 additions & 26 deletions ark/type/__tests__/operand.bench.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { bench } from "@arktype/attest"
import { scope, type } from "arktype"
import { type } from "arktype"

bench("single-quoted", () => {
const _ = type("'nineteen characters'")
})
.median([3.05, "us"])
.types([502, "instantiations"])
}).types([2697, "instantiations"])

bench("double-quoted", () => {
const _ = type('"nineteen characters"')
})
.median([3.13, "us"])
.types([502, "instantiations"])
}).types([2697, "instantiations"])

bench("regex literal", () => {
const _ = type("/nineteen characters/")
})
.median([4.18, "us"])
.types([502, "instantiations"])
}).types([2741, "instantiations"])

bench("keyword", () => {
const _ = type("string")
})
.median([1.44, "us"])
.types([84, "instantiations"])

const $ = scope({ strung: "string" })
bench("alias", () => {
const _ = $.type("strung")
})
.median([1.54, "us"])
.types([725, "instantiations"])
}).types([2507, "instantiations"])

bench("number", () => {
const _ = type("-98765.4321")
})
.median([4.41, "us"])
.types([415, "instantiations"])
}).types([2589, "instantiations"])

bench("bigint", () => {
const _ = type("-987654321n")
})
.median()
.types()
}).types([2611, "instantiations"])

bench("instantiations", () => {
const t = type({ foo: "string" })
}).types([3522, "instantiations"])

bench("union", () => {
// Union is automatically discriminated using shallow or deep keys
const user = type({
kind: "'admin'",
"powers?": "string[]"
})
.or({
kind: "'superadmin'",
"superpowers?": "string[]"
})
.or({
kind: "'pleb'"
})
}).types([8430, "instantiations"])
Loading

0 comments on commit 5fe79c6

Please sign in to comment.