From 569079e9081a9b29b40d3921955df5b68a0966ea Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Oct 2023 23:10:58 -0700 Subject: [PATCH 1/2] refactor: change Dispatcher class to a closure Also starts work on typing that closure. Gets the property names right, but currently has the "unfilled" method types, rather than the returned "filled-in" function types. Not sure how to fix this. --- src/core/Dispatcher.ts | 136 ++++++++++++++++++++++++++++------------- src/index.ts | 8 ++- src/interfaces/type.ts | 2 +- tsconfig.json | 1 + 4 files changed, 103 insertions(+), 44 deletions(-) diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index 4ede7cd..1485160 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -6,6 +6,7 @@ */ import {parseReflectedType, ImplementationDef} from './parseReflectedType.js' +import type {ValueIntersectionByKeyUnion} from '../interfaces/type.js' // First helper types and functions for the Dispatcher @@ -35,50 +36,103 @@ type TypeSpecification = { infer?: (d: DependenciesType) => (z: unknown) => TypeName } -type SpecObject = Record +type Callable = (...args: any) => any +type SpecObject = Record type SpecificationsGroup = Record -export class Dispatcher { - installSpecification( - name: string, - defn: ImplementationDef, - behavior: Function // possible todo: constrain this type based - // on the signature, return type, and dependencies. Not sure if - // that's really possible, though. - ) { - console.log('Pretending to install', name, 'with signatures') - for (const signature of defn.fn.signatures) { - console.log(' ', signature.args, '=>', signature.returns) - } - if (defn.fn.aliasOf) { - console.log(' As an alias of', defn.fn.aliasOf) - } - //TODO: implement me - } - installType(name: TypeName, typespec: TypeSpecification) { - console.log('Pretending to install type', name, typespec) - //TODO: implement me - } - constructor(collection: SpecificationsGroup) { - const implementations = [] - for (const key in collection) { - console.log('Working on', key) - for (const identifier in collection[key]) { - const item = collection[key][identifier] - if (typeof item === 'function') { - implementations.push([key, identifier, item]) - } else { - console.log('Handling type', key, ':', identifier) - this.installType( - item.name, collection[key][identifier] as TypeSpecification) - } +// Find all of the keys of an object that are functions but not aliases +// to another operation +type BaseOperations = + {[K in keyof Obj]: + Obj[K] extends Callable + ? ReturnType['aliasOf'] extends (string | undefined) + ? never + : K extends string ? K : never + : never + }[keyof Obj] +// Gather all of the operations specified in a SpecificationsGroup +type DispatcherOperations = + {[K in keyof G]: BaseOperations}[keyof G] +// Get the type of a given operation in a SpecObject: +type BaseOperationSignature< + Obj extends SpecObject, + Name extends BaseOperations +> = ValueIntersectionByKeyUnion< + {[K in keyof Obj]: + K extends Name + ? Obj[K] + : Obj[K] extends Callable + ? ReturnType['aliasOf'] extends (Name | undefined) + ? Obj[K] : unknown + : unknown + }, + keyof Obj> +// Get the type of a given operation in a SpecificationsGroup +type OperationSignature< + G extends SpecificationsGroup, + Name extends DispatcherOperations +> = ValueIntersectionByKeyUnion< + {[K in keyof G]: + Name extends BaseOperations + ? BaseOperationSignature + : unknown}, + keyof G> +// Put it all together into the typing of the dispatcher +type DispatcherInterface = + {[Op in DispatcherOperations]: OperationSignature} + +export function createDispatcher( + collection: G): DispatcherInterface +{ + const implementations = [] + const types = {} // who knows what the type of this will be + const behaviors = {} // ditto + for (const key in collection) { + console.log('Working on', key) + for (const identifier in collection[key]) { + const item = collection[key][identifier] + if (typeof item === 'function') { + implementations.push([key, identifier, item]) + } else { + console.log('Handling type', key, ':', identifier) + installType( // In this design, installType would modify + // types by side effect; maybe Jos prefers some other + // factoring of this? + types, item.name, + collection[key][identifier] as TypeSpecification) } } - for (const trio of implementations) { - const [k, id, imp] = trio - console.log('Handling implementation', id, 'from', k) - this.installSpecification( - id, parseReflectedType(id, imp.reflectedType), imp) - } } + for (const trio of implementations) { + const [k, id, imp] = trio + console.log('Handling implementation', id, 'from', k) + installSpecification( + behaviors, id, parseReflectedType(id, imp.reflectedType), imp) + } + return {} as DispatcherInterface +} + +function installType( + types: Object, name: TypeName, typespec: TypeSpecification) +{ + console.log('Pretending to install type', name, typespec) + //TODO: implement me +} + +function installSpecification( + behaviors: Object, // Same issue as mentioned above with side effects... + name: string, + defn: ImplementationDef, + specification: Function // possible todo: constrain this type based + // on the signature, return type, and dependencies. Not sure if + // that's really possible, though. +) { + console.log('Pretending to install', name, 'with signatures') + for (const signature of defn.fn.signatures) { + console.log(' ', signature.args, '=>', signature.returns) + } + if (defn.fn.aliasOf) { + console.log(' As an alias of', defn.fn.aliasOf) + } + //TODO: implement me } diff --git a/src/index.ts b/src/index.ts index e3377e5..cf46df1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,12 @@ import {inspect} from 'node:util' -import {Dispatcher} from './core/Dispatcher.js' +import {createDispatcher} from './core/Dispatcher.js' import * as Specifications from './all.js' -export default new Dispatcher(Specifications) +export Specifications +const math = createDispatcher(Specifications) +export default math + +console.log('Made', math.add} import {Complex} from './Complex/type.js' import {absquare as absquare_complex} from './Complex/arithmetic.js' diff --git a/src/interfaces/type.ts b/src/interfaces/type.ts index 3ab1b9a..55a36a9 100644 --- a/src/interfaces/type.ts +++ b/src/interfaces/type.ts @@ -15,7 +15,7 @@ import {$$typeToString} from 'ts-macros' * but that's OK, the generic parameter doesn't hurt in those cases. ****/ -type ValueIntersectionByKeyUnion = { +export type ValueIntersectionByKeyUnion = { [P in TKey]: (k: T[P])=>void } [TKey] extends ((k: infer I)=>void) ? I : never diff --git a/tsconfig.json b/tsconfig.json index 63c0b04..edf7502 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "rootDir": "./src", "outDir": "./build", "moduleResolution": "nodenext", + "declaration": true, "plugins": [ { "transform": "ts-macros/dist/type-resolve", "transformProgram": true From 8dcf74c5d129e2a33be315cc1009076acbda3d4c Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 20 Oct 2023 09:05:02 -0700 Subject: [PATCH 2/2] refactor: Demonstrate macro generating a generic implementations interface I swapped in the new macro in source files numbers/predicate.ts, Complex/arithmetic.ts, and Complex/predicate.ts. Note that you have to specify whether you are reflecting the generic or concrete implementations, which in some cases will mean two separate macro calls (as in Complex/predicate.ts). Also tried out the $$typeMetadata macro in Complex/all.ts; you can see the result by building and looking at build/Complex/all.js. It splits things up somewhat but we would still need to do a bunch of parsing, so probably not worth switching. --- src/Complex/all.ts | 3 +++ src/Complex/arithmetic.ts | 8 ++++++-- src/Complex/predicate.ts | 5 +++-- src/core/Dispatcher.ts | 12 ++++++------ src/interfaces/type.ts | 33 ++++++++++++++++++++++++++++++++- src/numbers/native.ts | 5 +++++ src/numbers/predicate.ts | 10 +++++++--- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/Complex/all.ts b/src/Complex/all.ts index 3ff6311..320bb2a 100644 --- a/src/Complex/all.ts +++ b/src/Complex/all.ts @@ -1,6 +1,9 @@ import * as Complex from './native.js' import * as complex from './arithmetic.js' +import {$$typeMetadata} from 'ts-macros' export { complex } export {Complex} + +const tryit = $$typeMetadata!(true, true) diff --git a/src/Complex/arithmetic.ts b/src/Complex/arithmetic.ts index 0f11f2e..11a2d7e 100644 --- a/src/Complex/arithmetic.ts +++ b/src/Complex/arithmetic.ts @@ -2,7 +2,7 @@ import {Complex} from './type.js' import type { Dependencies, Signature, Returns, RealType, AliasOf } from '../interfaces/type.js' -import {$reflect} from '../interfaces/type.js' +import {$reflectGen, GENERIC} from '../interfaces/type.js' declare module "../interfaces/type" { interface Signatures { @@ -98,7 +98,11 @@ export const sqrt = return dep.divideReal(num, denom) } -$reflect!([ +export interface GenericImplementations { + kilroy: "was here" +} + +$reflectGen!(GENERIC, [ add, addReal, unaryMinus, conj, subtract, multiply, absquare, divideReal, reciprocal, divide, sqrt ]) diff --git a/src/Complex/predicate.ts b/src/Complex/predicate.ts index fe26906..625b6d2 100644 --- a/src/Complex/predicate.ts +++ b/src/Complex/predicate.ts @@ -1,6 +1,6 @@ import {Complex} from './type.js' import type {Dependencies, Signature} from '../interfaces/type.js' -import {$reflect} from '../interfaces/type.js' +import {$reflectGen, GENERIC, CONCRETE} from '../interfaces/type.js' export const isReal = (dep: Dependencies<'add' | 'equal' | 'isReal', T>): @@ -10,4 +10,5 @@ export const isReal = export const isSquare = (): Signature<'isSquare', Complex> => z => true // FIXME: not correct for Complex once we get there -$reflect!([isReal, isSquare]) +$reflectGen!(GENERIC, [isReal]) +$reflectGen!(CONCRETE, [isSquare]) diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index 1485160..3dab510 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -59,12 +59,12 @@ type BaseOperationSignature< Name extends BaseOperations > = ValueIntersectionByKeyUnion< {[K in keyof Obj]: - K extends Name - ? Obj[K] - : Obj[K] extends Callable - ? ReturnType['aliasOf'] extends (Name | undefined) - ? Obj[K] : unknown - : unknown + Obj[K] extends Callable + ? K extends Name + ? ReturnType + : ReturnType['aliasOf'] extends (Name | undefined) + ? ReturnType : unknown + : unknown }, keyof Obj> // Get the type of a given operation in a SpecificationsGroup diff --git a/src/interfaces/type.ts b/src/interfaces/type.ts index 55a36a9..da776d8 100644 --- a/src/interfaces/type.ts +++ b/src/interfaces/type.ts @@ -77,7 +77,7 @@ type SignatureKey = keyof Signatures export type Signature, T> = Signatures[Name] export type Returns, T> = ReturnType[Name]> -type Deps = T extends unknown ? { [K in keyof T]: T[K] } : never; +export type Deps = T extends unknown ? { [K in keyof T]: T[K] } : never; export type Dependencies, T> = Deps<{[K in Name]: Signature}> @@ -88,3 +88,34 @@ export function $reflect(tup: ImplTuple) { +[[tup], (elt: T) => elt.reflectedType = $$typeToString!(true, false, true)] } +export function $genImps(tup: ImplTuple) { + +[[tup], (elt: U) => { + export interface GenericImplementations { + [$$text!(elt)]: typeof elt + } + }] +} + +export function $genImpsT(tup: ImplTuple) { + +[[tup], (elt: U) => { + export interface GenericImplementations { + [$$text!(elt)]: typeof elt + } + }] +} +export const GENERIC = true +export const CONCRETE = false +export function $reflectGen(generic: boolean, tup: ImplTuple) { + +[[tup], (elt: T) => { + elt.reflectedType = $$typeToString!(true, false, true) + if (generic) { + export interface GenericImplementations { + [$$text!(elt)]: typeof elt + } + } else { + export interface GenericImplementations { + [$$text!(elt)]: typeof elt + } + } + }] +} diff --git a/src/numbers/native.ts b/src/numbers/native.ts index ea2f66b..50c97f0 100644 --- a/src/numbers/native.ts +++ b/src/numbers/native.ts @@ -1,4 +1,9 @@ +import {GenericImplementations} from './predicate.js' +import {$$typeToString} from 'ts-macros' export * from './type.js' export * from './arithmetic.js' export * from './predicate.js' export * from './relational.js' + +const test: ReturnType['isReal']> = + (a: number) => true // = "oops" would fail as desired. diff --git a/src/numbers/predicate.ts b/src/numbers/predicate.ts index 0a51d65..e0a4907 100644 --- a/src/numbers/predicate.ts +++ b/src/numbers/predicate.ts @@ -1,7 +1,11 @@ import type {Signature} from '../interfaces/type.js' -import {$reflect} from '../interfaces/type.js' - +import {$reflectGen, CONCRETE} from '../interfaces/type.js' +import {$$typeToString} from 'ts-macros' export const isReal = (): Signature<'isReal', number> => (a) => true export const isSquare = (): Signature<'isSquare', number> => (a) => a >= 0 -$reflect!([isReal, isSquare]) +export interface GenericImplementations { + kilroy: "was here" +} + +$reflectGen!(CONCRETE, [isReal, isSquare])