Skip to content

Commit

Permalink
fix a rare jitless crash, experimental string matching (#1347)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad authored Mar 6, 2025
1 parent f6643f4 commit 908d7ec
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 86 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ tmp
*.log
*.tsbuildinfo
.DS_Store
.docusaurus
.astro
.next
.source
.cache-loader
.attest
tsconfig.build.json
coverage
# we avoid committing the root pnpm-lock in order to keep the root of the repo as clean as possible.
# we can get away with this to since we're only installing devDependencies and they're all pinned.
Expand Down
2 changes: 1 addition & 1 deletion ark/attest/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/attest",
"version": "0.44.1",
"version": "0.44.2",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
2 changes: 1 addition & 1 deletion ark/fs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/fs",
"version": "0.44.1",
"version": "0.44.2",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
6 changes: 0 additions & 6 deletions ark/repo/scratch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
import { type } from "arktype"

export const urDOOMed = type({
grouping: "(0 | (1 | (2 | (3 | (4 | 5)[])[])[])[])[]",
nestedGenerics: "Exclude<0n | unknown[] | Record<string, unknown>, object>",
"escapes\\?": "'a | b' | 'c | d'"
})
6 changes: 3 additions & 3 deletions ark/repo/scratch/fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type FunctionParser<$> = {
// implementation: implementation
// ) => implementation

<arg0, ret = unknown>(
<const arg0, ret = unknown>(
arg0: type.validate<arg0, $>,
_?: ":",
ret?: type.validate<ret, $>
Expand All @@ -22,7 +22,7 @@ export type FunctionParser<$> = {
implementation: implementation
) => implementation

<arg0, arg1, ret = unknown>(
<const arg0, const arg1, ret = unknown>(
arg0: type.validate<arg0, $>,
arg1: type.validate<arg1, $>,
_?: ":",
Expand All @@ -36,7 +36,7 @@ export type FunctionParser<$> = {
implementation: implementation
) => implementation

<arg0, arg1, arg2, ret = unknown>(
<const arg0, const arg1, const arg2, ret = unknown>(
arg0: type.validate<arg0, $>,
arg1: type.validate<arg1, $>,
arg2: type.validate<arg2, $>,
Expand Down
2 changes: 1 addition & 1 deletion ark/schema/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ark/schema",
"version": "0.44.1",
"version": "0.44.2",
"license": "MIT",
"author": {
"name": "David Blass",
Expand Down
7 changes: 0 additions & 7 deletions ark/schema/shared/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,6 @@ export class ArkErrors
return this.toString()
}

/**
* Alias of `summary` for StandardSchema compatibility.
*/
get message(): string {
return this.toString()
}

/**
* Alias of this ArkErrors instance for StandardSchema compatibility.
*/
Expand Down
8 changes: 6 additions & 2 deletions ark/schema/structure/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,9 @@ export class StructureNode extends BaseConstraint<Structure.Declaration> {
}
}

if (this.structuralMorph && !ctx.hasError())
// added additional ctx check here to address
// https://github.com/arktypeio/arktype/issues/1346
if (this.structuralMorph && ctx && !ctx.hasError())
ctx.queueMorphs([this.structuralMorph])

return true
Expand Down Expand Up @@ -653,7 +655,9 @@ export class StructureNode extends BaseConstraint<Structure.Declaration> {

// always queue deleteUndeclared on valid traversal for "delete"
if (this.structuralMorphRef) {
js.if("!ctx.hasError()", () =>
// added additional ctx check here to address
// https://github.com/arktypeio/arktype/issues/1346
js.if("ctx && !ctx.hasError()", () =>
js.line(`ctx.queueMorphs([${this.structuralMorphRef}])`)
)
}
Expand Down
10 changes: 10 additions & 0 deletions ark/type/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# arktype

## 2.1.7

Address a rare crash on an invalid ctx reference in some jitless cases

Closes #1346

## 2.1.6

Improve some type-level parse errors on expressions with invalid finalizers

## 2.1.5

#### Fix JSDoc and go-to definition for unparsed keys
Expand Down
129 changes: 118 additions & 11 deletions ark/type/__tests__/match.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { attest, contextualize } from "@ark/attest"
import { registeredReference, writeUnboundableMessage } from "@ark/schema"
import {
registeredReference,
writeUnboundableMessage,
type ArkErrors
} from "@ark/schema"
import { match, scope, type } from "arktype"
import type { Out } from "arktype/internal/attributes.ts"
import { doubleAtMessage, throwOnDefault } from "arktype/internal/match.ts"
Expand Down Expand Up @@ -211,7 +215,7 @@ contextualize(() => {

// @ts-expect-error
attest(() => matcher(true))
.throws.snap("AggregateError: must be a string or a number (was boolean)")
.throws.snap("AggregateError: must be a number or a string (was boolean)")
.type.errors(
"Argument of type 'boolean' is not assignable to parameter of type 'string | number'"
)
Expand Down Expand Up @@ -704,15 +708,6 @@ contextualize(() => {
default: "reject"
})

attest(m).type.toString.snap(`Match<
unknown,
[
(In: number) => number,
(In: string) => number,
(In: unknown) => ArkErrors
]
>`)

attest(m("foo")).equals(3)
attest(m(3)).equals(3)
// can access directly since it has no overlap with input
Expand All @@ -735,4 +730,116 @@ contextualize(() => {
"AggregateError: must be a string, a number, a bigint or an object (was null)"
)
})

it("validates in", () => {
const exclaimFoo = match.in({ foo: "string" }).at("foo", {
default: o => `${o.foo}!` as const
})

attest(exclaimFoo).type.toString.snap(`Match<
{ foo: string },
[
(In: unknown) => ArkErrors,
(o: { foo: string }) => \`\${string}!\`
]
>`)

const out = exclaimFoo({ foo: "foo" })

// ensure ArkErrors is added as a possible outcome
// since input is validated without assertion
attest<ArkErrors | `${string}!`>(out).equals("foo!")

// @ts-expect-error
attest(exclaimFoo({ foo: 5 }).toString())
.snap("foo must be a string (was a number)")
.type.errors("Type 'number' is not assignable to type 'string'")
})

it("asserts in", () => {
const fooToLength = match.in({ foo: "string" }).at("foo", {
"string > 0": o => o.foo.length,
default: "assert"
})

attest(fooToLength).type.toString.snap(`Match<
{ foo: string },
[(In: { foo: string }) => number]
>`)

const out = fooToLength({ foo: "foo" })

// ensure ArkErrors is not added to output
// since result is asserted
attest<number>(out).equals(3)

// @ts-expect-error
attest(() => fooToLength({ foo: 5 }))
.throws("foo must be a string (was a number)")
.type.errors("Type 'number' is not assignable to type 'string'")
})

type Discriminated =
| {
kind: "a"
value: "a"
}
| {
kind: "b"
value: "b"
}
| {
kind: "c"
value: "c"
}

it("string literal matcher", () => {
const discriminate = match
.in<Discriminated>()
.at("kind")
.strings({
a: o => o.value,
b: o => o.value,
c: o => o.value,
default: "assert"
})

const a = discriminate({ kind: "a", value: "a" })
const b = discriminate({ kind: "b", value: "b" })
const c = discriminate({ kind: "c", value: "c" })

attest<["a", "b", "c"]>([a, b, c]).snap(["a", "b", "c"])

// @ts-expect-error
attest(() => discriminate({ kind: "d", value: "d" }))
.throws.snap('AggregateError: kind must be "a", "b" or "c" (was "d")')
.type.errors(`Type '"d"' is not assignable`)
})

it("invalid string key", () => {
attest(() =>
match
.in<Discriminated>()
.at("kind")
.strings({
// @ts-expect-error
d: o => o.value,
default: "assert"
})
).type.errors(`ErrorType<"d must be a possible string value", {}>`)
})

it("lone invalid string key", () => {
attest(() =>
match
.in<Discriminated>()
.at("kind")
.strings({
// @ts-expect-error
d: o => o.value
})
).type.errors(
`Object literal may only specify known properties, and 'd' does not exist`
)
})
})
35 changes: 35 additions & 0 deletions ark/type/__tests__/realWorld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,4 +1259,39 @@ Right: { x: number, y: number, + (undeclared): delete }`)
attest(tupleArrayType.assert([[1, 2]])).equals([[1, 2]])
attest(unionType.assert([[1, 2]])).equals([[1, 2]])
})

it("doomed shirt example", () => {
const urDOOMed = type({
grouping: "(0 | (1 | (2 | (3 | (4 | 5)[])[])[])[])[]",
nestedGenerics:
"Exclude<0n | unknown[] | Record<string, unknown>, object>",
"escapes\\?": "'a | b' | 'c | d'"
})

attest<{
grouping: (0 | (1 | (2 | (3 | (4 | 5)[])[])[])[])[]
nestedGenerics: 0n
"escapes?": "a | b" | "c | d"
}>(urDOOMed.t)

attest(urDOOMed.expression).snap(
'{ escapes?: "a | b" | "c | d", grouping: (((((4 | 5)[] | 3)[] | 2)[] | 1)[] | 0)[], nestedGenerics: 0n }'
)
})

it("ArkErrors not assignable to ArkErrorInput", () => {
attest(() =>
type({
type: "string"
}).narrow((_, ctx) => {
const result = type.number("foo")
// @ts-expect-error
if (result instanceof type.errors) return ctx.reject(result)

return true
})
).type.errors(
"Argument of type 'ArkErrors' is not assignable to parameter of type 'ArkErrorInput'"
)
})
})
Loading

0 comments on commit 908d7ec

Please sign in to comment.