Skip to content

Commit

Permalink
fix consecutive narrow inference, nested pipes (#971)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad committed May 24, 2024
1 parent f54b7d5 commit 79c2b27
Show file tree
Hide file tree
Showing 19 changed files with 285 additions and 62 deletions.
14 changes: 14 additions & 0 deletions .changeset/healthy-numbers-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@arktype/util": patch
---

Provide recommended tsconfig via `tsconfig.base.json`, e.g.:

`tsconfig.json`

```ts
{
"extends": "@arktype/util/tsconfig.base.json",
// your settings here
}
```
5 changes: 5 additions & 0 deletions .changeset/little-icons-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arktype/schema": patch
---

Pipe and narrow bug fixes (see [arktype CHANGELOG](../type/CHANGELOG.md))
4 changes: 2 additions & 2 deletions ark/attest/__tests__/snapPopulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ contextualize(() => {
fromHere("benchExpectedOutput.ts")
).replaceAll("\r\n", "\n")
equal(actual, expectedOutput)
}).timeout(10000)
}).timeout(20000)

it("snap populates file", () => {
const actual = runThenGetContents(fromHere("snapTemplate.ts"))
const expectedOutput = readFile(
fromHere("snapExpectedOutput.ts")
).replaceAll("\r\n", "\n")
equal(actual, expectedOutput)
}).timeout(10000)
}).timeout(20000)
})
11 changes: 11 additions & 0 deletions ark/schema/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ export type constrain<
t,
kind extends PrimitiveConstraintKind,
schema extends NodeSchema<kind>
> =
_constrain<t, kind, schema> extends infer constrained ?
[t, constrained] extends [constrained, t] ?
t
: constrained
: never

type _constrain<
t,
kind extends PrimitiveConstraintKind,
schema extends NodeSchema<kind>
> =
schemaToConstraint<kind, schema> extends infer constraint ?
t extends of<infer base, infer constraints> ?
Expand Down
11 changes: 9 additions & 2 deletions ark/schema/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
type TraverseAllows,
type TraverseApply
} from "./shared/traversal.js"
import type { arkKind } from "./shared/utils.js"

export type UnknownNode = BaseNode | Root

Expand Down Expand Up @@ -117,13 +118,13 @@ export abstract class BaseNode<
// decorator from @arktype/util on these for now
// as they cause a deopt in V8
private _in?: BaseNode;
get in(): BaseNode {
get in(): this extends { [arkKind]: "root" } ? BaseRoot : BaseNode {
this._in ??= this.getIo("in")
return this._in as never
}

private _out?: BaseNode
get out(): BaseNode {
get out(): this extends { [arkKind]: "root" } ? BaseRoot : BaseNode {
this._out ??= this.getIo("out")
return this._out as never
}
Expand Down Expand Up @@ -168,6 +169,12 @@ export abstract class BaseNode<
return this.typeHash === other.typeHash
}

assertHasKind<kind extends NodeKind>(kind: kind): Node<kind> {
if (!this.kind === (kind as never))
throwError(`${this.kind} node was not of asserted kind ${kind}`)
return this as never
}

hasKind<kind extends NodeKind>(kind: kind): this is Node<kind> {
return this.kind === (kind as never)
}
Expand Down
6 changes: 3 additions & 3 deletions ark/schema/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ export type PredicateCast<input = never, narrowed extends input = input> = (
ctx: TraversalContext
) => input is narrowed

export type inferNarrow<In, predicate> =
export type inferNarrow<t, predicate> =
predicate extends (data: any, ...args: any[]) => data is infer narrowed ?
In extends of<unknown, infer constraints> ?
t extends of<unknown, infer constraints> ?
constrain<of<narrowed, constraints>, "predicate", any>
: constrain<narrowed, "predicate", any>
: constrain<In, "predicate", any>
: constrain<t, "predicate", any>
2 changes: 1 addition & 1 deletion ark/schema/roots/intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export const intersectionImplementation: nodeImplementationOf<IntersectionDeclar
node.children.map(child => child.description).join(" and "),
expected: source =>
` • ${source.errors.map(e => e.expected).join("\n • ")}`,
problem: ctx => `must be...\n${ctx.expected}`
problem: ctx => `${ctx.actual} must be...\n${ctx.expected}`
},
intersections: {
intersection: (l, r, ctx) => intersectIntersections(l, r, ctx),
Expand Down
40 changes: 27 additions & 13 deletions ark/schema/roots/morph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ export class MorphNode extends BaseRoot<MorphDeclaration> {
this.in.traverseAllows(data, ctx)

traverseApply: TraverseApply = (data, ctx) => {
ctx.queueMorphs(this.morphs)
this.in.traverseApply(data, ctx)
ctx.queueMorphs(this.morphs)
}

expression = `(In: ${this.in.expression}) => Out<${this.out?.expression ?? "unknown"}>`
Expand All @@ -152,20 +152,20 @@ export class MorphNode extends BaseRoot<MorphDeclaration> {
js.return(js.invoke(this.in))
return
}
js.line(`ctx.queueMorphs(${this.compiledMorphs})`)
js.line(js.invoke(this.in))
js.line(`ctx.queueMorphs(${this.compiledMorphs})`)
}

override get in(): BaseRoot {
return this.inner.in
}

get validatedOut(): BaseRoot | undefined {
const lastMorph = this.inner.morphs.at(-1)
return hasArkKind(lastMorph, "root") ?
(lastMorph?.out as BaseRoot)
: undefined
}
lastMorph = this.inner.morphs.at(-1)
validatedOut: BaseRoot | undefined =
hasArkKind(this.lastMorph, "root") ?
Object.assign(this.referencesById, this.lastMorph.out.referencesById) &&
this.lastMorph.out
: undefined

override get out(): BaseRoot {
return this.validatedOut ?? this.$.keywords.unknown.raw
Expand All @@ -191,24 +191,38 @@ export type inferMorphOut<morph extends Morph> = Exclude<
>

export type distillIn<t> =
includesMorphs<t> extends true ? _distill<t, "in", "base"> : t
includesMorphsOrConstraints<t> extends true ? _distill<t, "in", "base"> : t

export type distillOut<t> =
includesMorphs<t> extends true ? _distill<t, "out", "base"> : t
includesMorphsOrConstraints<t> extends true ? _distill<t, "out", "base"> : t

export type distillConstrainableIn<t> =
includesMorphs<t> extends true ? _distill<t, "in", "constrainable"> : t
includesMorphsOrConstraints<t> extends true ?
_distill<t, "in", "constrainable">
: t

export type distillConstrainableOut<t> =
includesMorphs<t> extends true ? _distill<t, "out", "constrainable"> : t
includesMorphsOrConstraints<t> extends true ?
_distill<t, "out", "constrainable">
: t

export type includesMorphs<t> =
export type includesMorphsOrConstraints<t> =
[t, _distill<t, "in", "base">, t, _distill<t, "out", "base">] extends (
[_distill<t, "in", "base">, t, _distill<t, "out", "base">, t]
) ?
false
: true

export type includesMorphs<t> =
[
_distill<t, "in", "constrainable">,
_distill<t, "out", "constrainable">
] extends (
[_distill<t, "out", "constrainable">, _distill<t, "in", "constrainable">]
) ?
false
: true

type _distill<
t,
io extends "in" | "out",
Expand Down
48 changes: 36 additions & 12 deletions ark/schema/roots/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export abstract class BaseRoot<
return this.configure(description)
}

create(input: unknown): unknown {
from(input: unknown): unknown {
// ideally we wouldn't validate here but for now we need to do determine
// which morphs to apply
return this.assert(input)
Expand All @@ -179,8 +179,11 @@ export abstract class BaseRoot<
}

private pipeOnce(morph: Morph): BaseRoot {
if (hasArkKind(morph, "root"))
return pipeNodesRoot(this, morph, this.$) as never
if (hasArkKind(morph, "root")) {
const result = pipeNodesRoot(this, morph, this.$)
if (result instanceof Disjoint) return result.throw()
return result as BaseRoot
}
if (this.hasKind("union")) {
const branches = this.branches.map(node => node.pipe(morph))
return this.$.node("union", { ...this.inner, branches })
Expand All @@ -198,28 +201,49 @@ export abstract class BaseRoot<
}

narrow(predicate: Predicate): BaseRoot {
return this.constrain("predicate", predicate)
return this.constrainOut("predicate", predicate)
}

constrain<kind extends PrimitiveConstraintKind>(
kind: kind,
schema: NodeSchema<kind>
): BaseRoot {
return this._constrain("in", kind, schema)
}

constrainOut<kind extends PrimitiveConstraintKind>(
kind: kind,
schema: NodeSchema<kind>
): BaseRoot {
return this._constrain("out", kind, schema)
}

private _constrain(
io: "in" | "out",
kind: PrimitiveConstraintKind,
schema: any
): BaseRoot {
const constraint = this.$.node(kind, schema)
if (constraint.impliedBasis && !this.extends(constraint.impliedBasis)) {
if (constraint.impliedBasis && !this[io].extends(constraint.impliedBasis)) {
return throwInvalidOperandError(
kind,
constraint.impliedBasis as never,
this as never
)
}

return this.and(
// TODO: not an intersection
this.$.node("intersection", {
[kind]: constraint
})
)
const partialIntersection = this.$.node("intersection", {
[kind]: constraint
})

const result =
io === "in" ?
intersectNodesRoot(this, partialIntersection, this.$)
: pipeNodesRoot(this, partialIntersection, this.$)

if (result instanceof Disjoint) result.throw()

return result as never
}

onUndeclaredKey(undeclared: UndeclaredKeyBehavior): BaseRoot {
Expand Down Expand Up @@ -331,7 +355,7 @@ export declare abstract class InnerRoot<t = unknown, $ = any> extends Callable<

onUndeclaredKey(behavior: UndeclaredKeyBehavior): this

create(literal: this["inferIn"]): this["infer"]
from(literal: this["inferIn"]): this["infer"]
}

// this is declared as a class internally so we can ensure all "abstract"
Expand Down
19 changes: 9 additions & 10 deletions ark/schema/roots/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ export class UnionNode extends BaseRoot<UnionDeclaration> {
const l = this.branches[lIndex]
for (let rIndex = lIndex + 1; rIndex < this.branches.length; rIndex++) {
const r = this.branches[rIndex]
const result = intersectNodesRoot(l, r, l.$)
const result = intersectNodesRoot(l.in, r.in, l.$)
if (!(result instanceof Disjoint)) continue

for (const { path, kind, disjoint } of result.flat) {
Expand Down Expand Up @@ -523,18 +523,17 @@ export const reduceBranches = ({
continue
}
const intersection = intersectNodesRoot(
branches[i],
branches[j],
branches[i].in,
branches[j].in,
branches[0].$
)
)!
if (intersection instanceof Disjoint) continue

if (intersection.equals(branches[i])) {
if (!ordered) {
// preserve ordered branches that are a subtype of a subsequent branch
uniquenessByIndex[i] = false
}
} else if (intersection.equals(branches[j])) uniquenessByIndex[j] = false
if (intersection.equals(branches[i].in)) {
// preserve ordered branches that are a subtype of a subsequent branch
uniquenessByIndex[i] = !!ordered
} else if (intersection.equals(branches[j].in))
uniquenessByIndex[j] = false
}
}
return branches.filter((_, i) => uniquenessByIndex[i])
Expand Down
12 changes: 7 additions & 5 deletions ark/schema/shared/traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export class TraversalContext {
finalize(): unknown {
if (this.hasError()) return this.errors

let out: any = this.root
if (this.queuedMorphs.length) {
for (let i = 0; i < this.queuedMorphs.length; i++) {
const { path, morphs } = this.queuedMorphs[i]
Expand All @@ -60,14 +59,17 @@ export class TraversalContext {

if (key !== undefined) {
// find the object on which the key to be morphed exists
parent = out
parent = this.root
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++)
parent = parent[path[pathIndex]]
}

this.path = path
for (const morph of morphs) {
const result = morph(parent === undefined ? out : parent[key!], this)
const result = morph(
parent === undefined ? this.root : parent[key!],
this
)
if (result instanceof ArkErrors) return result
if (this.hasError()) return this.errors
if (result instanceof ArkError) {
Expand All @@ -79,12 +81,12 @@ export class TraversalContext {

// apply the morph function and assign the result to the
// corresponding property, or to root if path is empty
if (parent === undefined) out = result
if (parent === undefined) this.root = result
else parent[key!] = result
}
}
}
return out
return this.root
}

get currentErrorCount(): number {
Expand Down
Loading

0 comments on commit 79c2b27

Please sign in to comment.