From a0324e5f2b71eaaeccd56bf603db1bdc3ef66961 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 29 Sep 2024 22:10:09 -0700 Subject: [PATCH] feat: add type reflection via ts-macros --- README.md | 39 ++++++++++- package.json5 | 3 + pnpm-lock.yaml | 140 ++++++++++++++++++++++++++++++++++++++ src/Complex/all.ts | 4 +- src/Complex/arithmetic.ts | 6 +- src/Complex/type.ts | 5 +- src/interfaces/type.ts | 10 +++ src/numbers/all.ts | 4 +- src/numbers/arithmetic.ts | 5 +- src/numbers/type.ts | 5 +- tsconfig.json | 8 ++- 11 files changed, 215 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 35dd1ed..991f645 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ were actually implementations of the operation "add" but with different signatures. I found that mechanism confusing. You can verify that the new code compiles and generates implementation -information by cloning the repository and then running `pnpm install` and -`pnpm go`. +information by cloning the repository and then running `pnpm install`, +`npx ts-patch install`, and then `pnpm go`. Outcomes of the refactor, corresponding to the motivations: @@ -107,3 +107,38 @@ expressing desired signatures could be devised; I'd want to wait until we had collected a larger number of use cases before trying to design them. (If this absquare case is essentially a one-off, it doesn't really matter if it is a bit elaborate.) + +### Looking ahead + +If this format is pursued, the next step would be to extract type information +that's needed to assemble the factories into working operations by injecting +the proper dependencies. There are two possible sources for this information: +(1) parsing the `.d.ts` files generated by tsc during the build, or +(2) generating strings encoding the types using the `$$typeToString` facility +of the `ts-macros` package. + +To help pick between the two, this version is instrumented with $$typeToString +to record the type of all of the exported implementation objects, so that +one can simply compare the output of `pnpm go` with the `.d.ts` files. + +At a first look, some features relevant to the choice are: + +A) With ts-macros (method 2), the type information it generates is +available immediately upon importing the JavaScript files generated by +the `tsc` build step. With method 1, we would need to insert an additional +build step after `tsc` that parses the `.d.ts` files and produces one or more +small JavaScript modules (or possibly JSON files) that contain the type +information in a usable format. + +B) On the other hand, the type specifications in the `d.ts` files appear +to have many more type definitions resolved and expanded out for us, making +them easier to read, parse, and use in the operations-assembly process. + +C) With ts-macros we have a couple of additional package dependencies and an +additional installation step (`npx ts-patch install`). For either method, we +will have a TypeScript type parser module that we will need to write and +maintain. + +Given these points, on balance at the moment I would lean ever so slightly +toward just parsing the `.d.ts` files -- it seems like less trouble overall +despite the additional build step -- but I could totally go either way. diff --git a/package.json5 b/package.json5 index 8637870..61b3f24 100644 --- a/package.json5 +++ b/package.json5 @@ -5,6 +5,7 @@ scripts: { build: 'tsc && echo "{\\"type\\": \\"module\\"}" > build/package.json', go: 'pnpm build && pnpm start', + prepare: 'ts-patch install -s', start: 'node --experimental-loader tsc-module-loader build', test: 'echo Error no test specified && exit 1', }, @@ -22,6 +23,8 @@ }, devDependencies: { '@types/node': '^22.7.4', + 'ts-macros': '^2.6.2', + 'ts-patch': '^3.2.1', typescript: '^5.6.2', 'undici-types': '^6.19.8', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 459ca7e..d2673f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,12 @@ importers: '@types/node': specifier: ^22.7.4 version: 22.7.4 + ts-macros: + specifier: ^2.6.2 + version: 2.6.2(typescript@5.6.2) + ts-patch: + specifier: ^3.2.1 + version: 3.2.1 typescript: specifier: ^5.6.2 version: 5.6.2 @@ -27,20 +33,60 @@ packages: '@types/node@22.7.4': resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commonjs-extension-resolution-loader@0.1.0: resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==} function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -48,10 +94,33 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + ts-macros@2.6.2: + resolution: {integrity: sha512-ZzEn268Td/efdvgFptYS2Hh4k8fEihF9P2QFqwX9OzEwAhdWq0oyhD0nUH6xh+mXklPKQiGQySS2NyW79tG5eA==} + hasBin: true + peerDependencies: + typescript: 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + + ts-patch@3.2.1: + resolution: {integrity: sha512-hlR43v+GUIUy8/ZGFP1DquEqPh7PFKQdDMTAmYt671kCCA6AkDQMoeFaFmZ7ObPLYOmpMgyKUqL1C+coFMf30w==} + hasBin: true + tsc-module-loader@0.0.1: resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==} @@ -63,26 +132,67 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + snapshots: '@types/node@22.7.4': dependencies: undici-types: 6.19.8 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + commonjs-extension-resolution-loader@0.1.0: dependencies: resolve: 1.22.8 function-bind@1.1.2: {} + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + has-flag@4.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 + ini@1.3.8: {} + is-core-module@2.15.1: dependencies: hasown: 2.0.2 + isexe@2.0.0: {} + + kind-of@6.0.3: {} + + minimist@1.2.8: {} + path-parse@1.0.7: {} resolve@1.22.8: @@ -91,8 +201,32 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + semver@7.6.3: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + ts-macros@2.6.2(typescript@5.6.2): + dependencies: + typescript: 5.6.2 + yargs-parser: 21.1.1 + + ts-patch@3.2.1: + dependencies: + chalk: 4.1.2 + global-prefix: 3.0.0 + minimist: 1.2.8 + resolve: 1.22.8 + semver: 7.6.3 + strip-ansi: 6.0.1 + tsc-module-loader@0.0.1: dependencies: commonjs-extension-resolution-loader: 0.1.0 @@ -101,3 +235,9 @@ snapshots: typescript@5.6.2: {} undici-types@6.19.8: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + yargs-parser@21.1.1: {} diff --git a/src/Complex/all.ts b/src/Complex/all.ts index b0c3344..cd1accd 100644 --- a/src/Complex/all.ts +++ b/src/Complex/all.ts @@ -1,2 +1,2 @@ -export * as type_data from './type' -export * as arithmetic_functions from './arithmetic' +export * as type from './type' +export * as arithmetic from './arithmetic' diff --git a/src/Complex/arithmetic.ts b/src/Complex/arithmetic.ts index f823b7e..f1b3e0e 100644 --- a/src/Complex/arithmetic.ts +++ b/src/Complex/arithmetic.ts @@ -2,7 +2,7 @@ import {Complex} from './type.js' import {implementations, commonSpecs} from '@/core/Dispatcher' import type {RawDependencies} from '@/core/Dispatcher' -import {commonSignature, RealType} from '@/interfaces/type' +import {commonSignature, RealType, $reflect} from '@/interfaces/type' import type {CommonSignature, CommonReturn} from '@/interfaces/type' // Narrowly typed signature selectors, for the operations we need to use @@ -10,7 +10,7 @@ import type {CommonSignature, CommonReturn} from '@/interfaces/type' const add = 'add' as const const divide = 'divide' as const -export default function () { +export function common() { const baseSignature = commonSpecs() const withComplex = (rd: RD) => baseSignature({ @@ -150,3 +150,5 @@ export function mixed() { }) .ship() } + +$reflect!([common, mixed]) diff --git a/src/Complex/type.ts b/src/Complex/type.ts index e12d275..25744fc 100644 --- a/src/Complex/type.ts +++ b/src/Complex/type.ts @@ -1,5 +1,6 @@ import {implementations, commonSpecs} from '@/core/Dispatcher' import {joinTypes} from '@/core/type' +import {$reflect} from '@/interfaces/type' import type { CommonInterface, CommonSignature, NaNType, OneType, ZeroType @@ -50,7 +51,7 @@ export function lift() { }).ship() } -export default function () { +export function common() { const baseSignature = commonSpecs() return implementations>>() @@ -69,3 +70,5 @@ export default function () { }) .ship() } + +$reflect!([common, lift]) diff --git a/src/interfaces/type.ts b/src/interfaces/type.ts index a49da98..52bbb14 100644 --- a/src/interfaces/type.ts +++ b/src/interfaces/type.ts @@ -1,3 +1,5 @@ +import {$$typeToString} from 'ts-macros' + import {Configuration} from '@/core/Configuration' /* First some type utilities: */ @@ -114,3 +116,11 @@ export type Dependency = { export type Dependencies, Aux = T> = Pick, Names> + +// Macro for type reflection: +export function $reflect(tup: ImplTuple) { + +[[tup], (elt: T) => { + (elt as {reflectedType: string}).reflectedType + = $$typeToString!(true, false, true); + }] +} diff --git a/src/numbers/all.ts b/src/numbers/all.ts index b0c3344..cd1accd 100644 --- a/src/numbers/all.ts +++ b/src/numbers/all.ts @@ -1,2 +1,2 @@ -export * as type_data from './type' -export * as arithmetic_functions from './arithmetic' +export * as type from './type' +export * as arithmetic from './arithmetic' diff --git a/src/numbers/arithmetic.ts b/src/numbers/arithmetic.ts index 6f430d6..0c714b0 100644 --- a/src/numbers/arithmetic.ts +++ b/src/numbers/arithmetic.ts @@ -1,9 +1,10 @@ import {implementations} from '@/core/Dispatcher' +import {$reflect} from '@/interfaces/type' import type {CommonSignature} from '@/interfaces/type' const conservativeSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a) -export default implementations>() +export const common = implementations>() .independent({ add: (a, b) => a + b, unaryMinus: a => -a, @@ -24,3 +25,5 @@ export default implementations>() } }}) .ship() + +$reflect!([common]) diff --git a/src/numbers/type.ts b/src/numbers/type.ts index c0d0841..87554b3 100644 --- a/src/numbers/type.ts +++ b/src/numbers/type.ts @@ -1,5 +1,6 @@ import type {Complex} from '@/Complex/type' import {implementations} from '@/core/Dispatcher' +import {$reflect} from '@/interfaces/type' import type {CommonSignature} from '@/interfaces/type' export const number_type = { @@ -22,10 +23,12 @@ declare module "@/interfaces/type" { } } -export default implementations>() +export const common = implementations>() .independent({ zero: a => 0, one: a => 1, nan: a => NaN, re: a => a }).ship() + +$reflect!([common]) diff --git a/tsconfig.json b/tsconfig.json index 6218fdd..8e4baa2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,12 @@ "outDir": "./build", "paths": { "@/*": ["./src/*"], - "undici-types": [ - "./node_modules/undici-types/index.d.ts" - ] + "ts-macros": ["./node_modules/ts-macros/dist/index.js"], + "undici-types": ["./node_modules/undici-types/index.d.ts"] }, + "plugins": [ + { "transform": "ts-macros" } + ], "rootDir": "./src", "target": "esnext", "types": ["node"]