Skip to content

Commit

Permalink
refactor: remove tree type inferrence (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
pevisscher authored May 21, 2024
1 parent 0cddc49 commit c2a841c
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 120 deletions.
27 changes: 12 additions & 15 deletions src/engine/operator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { inspect } from 'node:util'
import type { JSONExpr } from '../json/jsonexpr.type.js'
import type { AsExpression, DefinitionType, Expression, ValueExpression } from './types.js'
import { fromLiteral } from './types.js'

import type { JSONExpr } from '../json/jsonexpr.type.js'

import { inspect } from 'node:util'
export type FactsFomExprs<T> = T extends { facts: infer U } ? U : never

// biome-ignore lint/suspicious/noExplicitAny: any is necessary for the operator function
export interface Operator<I extends any[], O, Op extends string> {
symbol: string
operator: Op
<Exprs extends { [K in keyof I]: Expression<I[K]> | I[K] }>(
...exprs: Exprs
): ValueExpression<
O,
{ [K in keyof I]: AsExpression<Exprs[K]> },
FactsFomExprs<Exprs[number]>,
// biome-ignore lint/suspicious/noExplicitAny: any is necessary for the operator function
): ValueExpression<O, { [K in keyof I]: AsExpression<Exprs[K]> }, Extract<JSONExpr, Record<Op, any[] | JSONExpr>>>
Extract<JSONExpr, Record<Op, any[] | JSONExpr>>
>
}

export function operator<I extends unknown[], O, Op extends string = string>({
Expand All @@ -25,10 +30,7 @@ export function operator<I extends unknown[], O, Op extends string = string>({
symbol: string
}): Operator<I, O, Op> {
return Object.assign(
<Exprs extends { [K in keyof I]: Expression<I[K]> | I[K] }>(
...exprs: Exprs
// biome-ignore lint/suspicious/noExplicitAny: any is necessary for the operator function
): ValueExpression<O, { [K in keyof I]: AsExpression<Exprs[K]> }, Extract<JSONExpr, Record<Op, any[] | JSONExpr>>> => {
<Exprs extends { [K in keyof I]: Expression<I[K]> | I[K] }>(...exprs: Exprs) => {
const xs = exprs.map((x) => fromLiteral(x as I[number]))
return {
fn,
Expand All @@ -39,16 +41,11 @@ export function operator<I extends unknown[], O, Op extends string = string>({
[inspect.custom]() {
return `${symbol}(${xs.map((x) => x[inspect.custom]?.() ?? '').join(', ')})`
},
} as unknown as ValueExpression<
O,
{ [K in keyof I]: AsExpression<Exprs[K]> },
// biome-ignore lint/suspicious/noExplicitAny: any is necessary for the operator function
Extract<JSONExpr, Record<Op, any[] | JSONExpr>>
>
}
},
{
symbol,
operator,
},
)
) as unknown as Operator<I, O, Op>
}
24 changes: 9 additions & 15 deletions src/engine/policy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { collect, stack } from '@skyleague/axioms'
import type { Simplify, UnionToIntersection } from '@skyleague/axioms/types'
import type { IsEmptyObject, Simplify } from '@skyleague/axioms/types'
import { version } from '../../package.json'
import type { FactsFomExprs } from './operator.js'
import type { Expression, ExpressionReturnType } from './types.js'

export class EvaluationContext {
Expand Down Expand Up @@ -73,37 +74,31 @@ function* collapseExpression(root: Expression[], seen = new WeakSet()) {

type InferFactName<Expr, k> = Expr extends { name: string } ? Expr['name'] : k

type FilterFactExpressions<Facts extends Record<string, unknown>, k extends keyof Facts> = Facts[k] extends {
type FilterFactExpressions<Facts, K extends keyof Facts> = Facts[K] extends {
_type: 'fact'
}
? InferFactName<Facts[k], k>
? InferFactName<Facts[K], K>
: never

type CollapseTreeToArray<T> = T extends { dependsOn: (infer U)[] } ? T | CollapseTreeToArray<U> : T
type _InputFromExpressions<Facts extends Record<string, unknown>> = Simplify<{
[k in keyof Facts as FilterFactExpressions<Facts, k>]: ExpressionReturnType<Facts[k]>
type _InputFromExpressions<Facts> = Simplify<{
[K in keyof Facts as FilterFactExpressions<Facts, K>]: ExpressionReturnType<Facts[K]>
}>
type NamedUnionToRecord<T> = T extends { name: infer Name } ? (Name extends PropertyKey ? { [k in Name]: T } : never) : never

type _FactsFomExprs<Facts> = Facts extends unknown[] ? Facts[number] : Facts
export type InputFromExpressions<Facts extends Record<string, unknown>> = Simplify<
_InputFromExpressions<Facts> &
_InputFromExpressions<
UnionToIntersection<NamedUnionToRecord<Facts extends Record<string, infer E> ? CollapseTreeToArray<E> : never>>
>
_InputFromExpressions<Facts> & _InputFromExpressions<{ [K in keyof Facts]: _FactsFomExprs<FactsFomExprs<Facts[K]>> }>
>

export type OutputFromFacts<Facts extends Record<string, unknown>> = Simplify<{
[k in keyof Facts]: ExpressionReturnType<Facts[k]>
}>

export interface Policy<I, O> {
evaluate: [I] extends [never] ? () => { input: I; output: O } : (x: I) => { input: I; output: O }
evaluate: IsEmptyObject<I> extends true ? () => { input: I; output: O } : (x: I) => { input: I; output: O }
expr: () => unknown
}

export function $policy<Facts extends Record<string, Expression>>(
expressions: Facts,
// @ts-ignore
): Policy<InputFromExpressions<Facts>, OutputFromFacts<Facts>> {
const facts = Object.entries(expressions).map(([name, e]) => {
if (e.name === undefined) {
Expand All @@ -122,7 +117,6 @@ export function $policy<Facts extends Record<string, Expression>>(
const properties = Object.fromEntries(inputNodes.map((e) => [e.name, e.expr('definition')]))
const outputExpression = Object.fromEntries(outputNodes.map((f) => [f.name, f.expr('expression')]))
return {
// @ts-ignore
evaluate: ((input: Record<string, unknown>) => {
const ctx = new EvaluationContext(input)

Expand Down
4 changes: 2 additions & 2 deletions src/engine/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ describe('fromLiteral', () => {
const fact = $fact(Arithmetic, 'input')
const fa = fromLiteral($from(fact, '$.a'))
const _fa_: Expression<number> = fa
expectTypeOf(fa).toEqualTypeOf<From<Arithmetic, number, [Fact<Arithmetic, 'input'>]>>()
expectTypeOf(fa).toEqualTypeOf<From<Arithmetic, number, Fact<Arithmetic, 'input'>>>()
const fb = fromLiteral($from(fact, '$.b'))
const _fb_: Expression<number> = fb
expectTypeOf(fb).toEqualTypeOf<From<Arithmetic, number, [Fact<Arithmetic, 'input'>]>>()
expectTypeOf(fb).toEqualTypeOf<From<Arithmetic, number, Fact<Arithmetic, 'input'>>>()

type val = ['1', '2']
const _test_multiple = {} as { [K in keyof val]: AsExpression<val[K]> }
Expand Down
17 changes: 11 additions & 6 deletions src/engine/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EvaluationContext } from './policy.js'

import { $literal } from '../expressions/input.js'
import { $literal, type Fact } from '../expressions/input.js'
import type {
BooleanArrExpr,
BooleanExpr,
Expand Down Expand Up @@ -40,6 +40,7 @@ export interface Expression<O = any, I = any, Expr extends JSONExpr | ValueItemE
_type?: string
name?: PropertyKey
dependsOn: Expression[]
// facts?: Expression[]
fn: (x: I, ctx: EvaluationContext) => O
expr: (definition: DefinitionType) => Expr
[inspect.custom]?(): string
Expand All @@ -50,30 +51,34 @@ export type ExpressionReturnType<E> = E extends Pick<Expression, 'fn'> ? ReturnT
// biome-ignore lint/suspicious/noExplicitAny: this is needed for greedy matching
export interface LiteralExpression<O = any, Expr extends JSONExpr = InferExpressionType<O>> extends Expression<O, any, Expr> {
dependsOn: []
// facts: []
_type: 'literal'
}

export interface FactExpression<T, Expr extends JSONExpr = InferExpressionType<T>> extends Expression<T, unknown, Expr> {
// biome-ignore lint/suspicious/noExplicitAny: this is needed for greedy matching
export interface FactExpression<T = any, Expr extends JSONExpr = InferExpressionType<T>> extends Expression<T, unknown, Expr> {
_type: 'fact'
}

export interface InputExpression<
O,
I,
DependsOn extends Expression[],
F extends Fact<Expression, string>,
Expr extends JSONExpr | ValueItemExpr = InferExpressionType<O>,
> extends Expression<O, I, Expr> {
dependsOn: DependsOn
dependsOn: [F]
facts: [F]
_type: 'value' | 'literal'
}

export type InputFromExpressions<Expr extends Expression[]> = {
[k in keyof Expr]: ExpressionReturnType<Expr[k]>
}

export interface ValueExpression<O, DependsOn extends Expression[], Expr extends JSONExpr = InferExpressionType<O>>
export interface ValueExpression<O, DependsOn extends Expression[], Facts, Expr extends JSONExpr = InferExpressionType<O>>
extends Expression<O, InputFromExpressions<DependsOn>, Expr> {
dependsOn: DependsOn
// dependsOn: DependsOn
facts: Facts //extends FactExpression ? [Facts] : never
_type?: 'value'
}

Expand Down
57 changes: 42 additions & 15 deletions src/expressions/boolean.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,20 @@ describe('startsWith', () => {

const x1 = $startsWith('1', '2')
expectTypeOf(x1).toEqualTypeOf<
ValueExpression<boolean, [LiteralExpression<'1', StringExpr>, LiteralExpression<'2', StringExpr>], StartsWithExpr>
ValueExpression<
boolean,
[LiteralExpression<'1', StringExpr>, LiteralExpression<'2', StringExpr>],
never,
StartsWithExpr
>
>()

const x2 = $startsWith($from(fact, '$.a'), '2')
expectTypeOf(x2).toEqualTypeOf<
ValueExpression<
boolean,
[From<AbObj, string, [Fact<AbObj, 'input'>]>, LiteralExpression<'2', StringExpr>],
[str: From<AbObj, string, Fact<AbObj, 'input'>>, searchString: LiteralExpression<'2', StringExpr>],
[Fact<AbObj, 'input'>],
StartsWithExpr
>
>()
Expand All @@ -106,7 +112,8 @@ describe('startsWith', () => {
expectTypeOf(x3).toEqualTypeOf<
ValueExpression<
boolean,
[LiteralExpression<'2', StringExpr>, From<AbObj, string, [Fact<AbObj, 'input'>]>],
[str: LiteralExpression<'2', StringExpr>, searchString: From<AbObj, string, Fact<AbObj, 'input'>>],
[Fact<AbObj, 'input'>],
StartsWithExpr
>
>()
Expand Down Expand Up @@ -190,14 +197,20 @@ describe('endsWith', () => {

const x1 = $endsWith('1', '2')
expectTypeOf(x1).toEqualTypeOf<
ValueExpression<boolean, [LiteralExpression<'1', StringExpr>, LiteralExpression<'2', StringExpr>], EndsWithExpr>
ValueExpression<
boolean,
[str: LiteralExpression<'1', StringExpr>, searchString: LiteralExpression<'2', StringExpr>],
never,
EndsWithExpr
>
>()

const x2 = $endsWith($from(fact, '$.a'), '2')
expectTypeOf(x2).toEqualTypeOf<
ValueExpression<
boolean,
[From<AbObj, string, [Fact<AbObj, 'input'>]>, LiteralExpression<'2', StringExpr>],
[str: From<AbObj, string, Fact<AbObj, 'input'>>, searchString: LiteralExpression<'2', StringExpr>],
[Fact<AbObj, 'input'>],
EndsWithExpr
>
>()
Expand All @@ -206,7 +219,8 @@ describe('endsWith', () => {
expectTypeOf(x3).toEqualTypeOf<
ValueExpression<
boolean,
[LiteralExpression<'2', StringExpr>, From<AbObj, string, [Fact<AbObj, 'input'>]>],
[str: LiteralExpression<'2', StringExpr>, searchString: From<AbObj, string, Fact<AbObj, 'input'>>],
[Fact<AbObj, 'input'>],
EndsWithExpr
>
>()
Expand Down Expand Up @@ -290,14 +304,20 @@ describe('includes', () => {

const x1 = $includes('1', '2')
expectTypeOf(x1).toEqualTypeOf<
ValueExpression<boolean, [LiteralExpression<'1', StringExpr>, LiteralExpression<'2', StringExpr>], IncludesExpr>
ValueExpression<
boolean,
[LiteralExpression<'1', StringExpr>, LiteralExpression<'2', StringExpr>],
never,
IncludesExpr
>
>()

const x2 = $includes($from(fact, '$.a'), '2')
expectTypeOf(x2).toEqualTypeOf<
ValueExpression<
boolean,
[From<AbObj, string, [Fact<AbObj, 'input'>]>, LiteralExpression<'2', StringExpr>],
[str: From<AbObj, string, Fact<AbObj, 'input'>>, searchString: LiteralExpression<'2', StringExpr>],
[Fact<AbObj, 'input'>],
IncludesExpr
>
>()
Expand All @@ -306,7 +326,8 @@ describe('includes', () => {
expectTypeOf(x3).toEqualTypeOf<
ValueExpression<
boolean,
[LiteralExpression<'2', StringExpr>, From<AbObj, string, [Fact<AbObj, 'input'>]>],
[str: LiteralExpression<'2', StringExpr>, searchString: From<AbObj, string, Fact<AbObj, 'input'>>],
[Fact<AbObj, 'input'>],
IncludesExpr
>
>()
Expand Down Expand Up @@ -413,7 +434,9 @@ describe('all', () => {
const fact = $fact(LogicObj, 'input')

const x1 = $all([1, 2], (x) => $equal(x, 1))
expectTypeOf(x1).toEqualTypeOf<ValueExpression<boolean, [LiteralExpression<number[], NumberArrExpr>], BooleanExpr>>()
expectTypeOf(x1).toEqualTypeOf<
ValueExpression<boolean, [LiteralExpression<number[], NumberArrExpr>], never, BooleanExpr>
>()

const a = $from(fact, '$.d')
const x2 = $all(a, (x) => $equal(x, 1))
Expand All @@ -427,17 +450,18 @@ describe('all', () => {
a: boolean
b: boolean
}[],
[Fact<LogicObj, 'input'>]
Fact<LogicObj, 'input'>
>,
],
[Fact<LogicObj, 'input'>],
BooleanExpr
>
>()

const ba = $from(fact, '$.b..a')
const x3 = $all(ba, (x) => $equal(x, 1))
expectTypeOf(x3).toEqualTypeOf<
ValueExpression<boolean, [From<LogicObj, never[], [Fact<LogicObj, 'input'>]>], BooleanExpr>
ValueExpression<boolean, [From<LogicObj, never[], Fact<LogicObj, 'input'>>], [Fact<LogicObj, 'input'>], BooleanExpr>
>()
})
})
Expand Down Expand Up @@ -542,7 +566,9 @@ describe('any', () => {
const fact = $fact(LogicObj, 'input')

const x1 = $any([1, 2], (x) => $equal(x, 1))
expectTypeOf(x1).toEqualTypeOf<ValueExpression<boolean, [LiteralExpression<number[], NumberArrExpr>], BooleanExpr>>()
expectTypeOf(x1).toEqualTypeOf<
ValueExpression<boolean, [LiteralExpression<number[], NumberArrExpr>], never, BooleanExpr>
>()

const a = $from(fact, '$.d')
const x2 = $any(a, (x) => $equal(x, 1))
Expand All @@ -556,17 +582,18 @@ describe('any', () => {
a: boolean
b: boolean
}[],
[Fact<LogicObj, 'input'>]
Fact<LogicObj, 'input'>
>,
],
[Fact<LogicObj, 'input'>],
BooleanExpr
>
>()

const ba = $from(fact, '$.b..a')
const x3 = $any(ba, (x) => $equal(x, 1))
expectTypeOf(x3).toEqualTypeOf<
ValueExpression<boolean, [From<LogicObj, never[], [Fact<LogicObj, 'input'>]>], BooleanExpr>
ValueExpression<boolean, [From<LogicObj, never[], Fact<LogicObj, 'input'>>], [Fact<LogicObj, 'input'>], BooleanExpr>
>()
})
})
10 changes: 5 additions & 5 deletions src/expressions/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $value, type ValueItem } from './higher-order-fn.js'

import { operator } from '../engine/operator.js'
import { type FactsFomExprs, operator } from '../engine/operator.js'
import {
type AsExpression,
type Expression,
Expand Down Expand Up @@ -44,7 +44,7 @@ export const $all = Object.assign(
<Expr extends LiteralOr<any[]>>(
xs: Expr,
predicate: (value: ValueItem<ExpressionTypeOfLiteral<Expr>>) => Expression<boolean>,
): ValueExpression<boolean, [AsExpression<Expr>]> => {
): ValueExpression<boolean, [AsExpression<Expr>], FactsFomExprs<Expr>> => {
const _xs = fromLiteral(xs)
const _value = $value<ExpressionTypeOfLiteral<Expr>>()
const _transform = predicate(_value)
Expand All @@ -59,7 +59,7 @@ export const $all = Object.assign(
[inspect.custom]() {
return `$all(${_xs[inspect.custom]?.() ?? ''}, (x) => ${_transform[inspect.custom]?.() ?? ''})`
},
} as ValueExpression<boolean, [AsExpression<Expr>]>
} as ValueExpression<boolean, [AsExpression<Expr>], FactsFomExprs<Expr>>
},
// biome-ignore lint/suspicious/noExplicitAny: this is needed for greedy matching
{ operator: 'all', symbol: '$all', parse: (xs: any, predicate: any) => $all(xs, () => predicate) } as const,
Expand All @@ -70,7 +70,7 @@ export const $any = Object.assign(
<Expr extends LiteralOr<any[]>>(
xs: Expr,
predicate: (value: ValueItem<ExpressionTypeOfLiteral<Expr>>) => Expression<boolean>,
): ValueExpression<boolean, [AsExpression<Expr>]> => {
): ValueExpression<boolean, [AsExpression<Expr>], FactsFomExprs<Expr>> => {
const _xs = fromLiteral(xs)
const _value = $value<ExpressionTypeOfLiteral<Expr>>()
const _transform = predicate(_value)
Expand All @@ -85,7 +85,7 @@ export const $any = Object.assign(
[inspect.custom]() {
return `$any(${_xs[inspect.custom]?.() ?? ''}, (x) => ${_transform[inspect.custom]?.() ?? ''})`
},
} as ValueExpression<boolean, [AsExpression<Expr>]>
} as ValueExpression<boolean, [AsExpression<Expr>], FactsFomExprs<Expr>>
},
// biome-ignore lint/suspicious/noExplicitAny: this is needed for greedy matching
{ operator: 'any', symbol: '$any', parse: (xs: any, predicate: any) => $any(xs, () => predicate) } as const,
Expand Down
Loading

0 comments on commit c2a841c

Please sign in to comment.