feat: add type reflection via ts-macros

This commit is contained in:
Glen Whitney 2024-09-29 22:10:09 -07:00
parent f575582879
commit a0324e5f2b
11 changed files with 215 additions and 14 deletions

View File

@ -28,8 +28,8 @@ were actually implementations of the operation "add" but with different
signatures. I found that mechanism confusing. signatures. I found that mechanism confusing.
You can verify that the new code compiles and generates implementation You can verify that the new code compiles and generates implementation
information by cloning the repository and then running `pnpm install` and information by cloning the repository and then running `pnpm install`,
`pnpm go`. `npx ts-patch install`, and then `pnpm go`.
Outcomes of the refactor, corresponding to the motivations: 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 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 this absquare case is essentially a one-off, it doesn't really matter
if it is a bit elaborate.) 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.

View File

@ -5,6 +5,7 @@
scripts: { scripts: {
build: 'tsc && echo "{\\"type\\": \\"module\\"}" > build/package.json', build: 'tsc && echo "{\\"type\\": \\"module\\"}" > build/package.json',
go: 'pnpm build && pnpm start', go: 'pnpm build && pnpm start',
prepare: 'ts-patch install -s',
start: 'node --experimental-loader tsc-module-loader build', start: 'node --experimental-loader tsc-module-loader build',
test: 'echo Error no test specified && exit 1', test: 'echo Error no test specified && exit 1',
}, },
@ -22,6 +23,8 @@
}, },
devDependencies: { devDependencies: {
'@types/node': '^22.7.4', '@types/node': '^22.7.4',
'ts-macros': '^2.6.2',
'ts-patch': '^3.2.1',
typescript: '^5.6.2', typescript: '^5.6.2',
'undici-types': '^6.19.8', 'undici-types': '^6.19.8',
}, },

View File

@ -15,6 +15,12 @@ importers:
'@types/node': '@types/node':
specifier: ^22.7.4 specifier: ^22.7.4
version: 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: typescript:
specifier: ^5.6.2 specifier: ^5.6.2
version: 5.6.2 version: 5.6.2
@ -27,20 +33,60 @@ packages:
'@types/node@22.7.4': '@types/node@22.7.4':
resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} 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: commonjs-extension-resolution-loader@0.1.0:
resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==} resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==}
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 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: hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
is-core-module@2.15.1: is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'} 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: path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@ -48,10 +94,33 @@ packages:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true 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: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} 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: tsc-module-loader@0.0.1:
resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==} resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==}
@ -63,26 +132,67 @@ packages:
undici-types@6.19.8: undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 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: snapshots:
'@types/node@22.7.4': '@types/node@22.7.4':
dependencies: dependencies:
undici-types: 6.19.8 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: commonjs-extension-resolution-loader@0.1.0:
dependencies: dependencies:
resolve: 1.22.8 resolve: 1.22.8
function-bind@1.1.2: {} 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: hasown@2.0.2:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
ini@1.3.8: {}
is-core-module@2.15.1: is-core-module@2.15.1:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
isexe@2.0.0: {}
kind-of@6.0.3: {}
minimist@1.2.8: {}
path-parse@1.0.7: {} path-parse@1.0.7: {}
resolve@1.22.8: resolve@1.22.8:
@ -91,8 +201,32 @@ snapshots:
path-parse: 1.0.7 path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0 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: {} 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: tsc-module-loader@0.0.1:
dependencies: dependencies:
commonjs-extension-resolution-loader: 0.1.0 commonjs-extension-resolution-loader: 0.1.0
@ -101,3 +235,9 @@ snapshots:
typescript@5.6.2: {} typescript@5.6.2: {}
undici-types@6.19.8: {} undici-types@6.19.8: {}
which@1.3.1:
dependencies:
isexe: 2.0.0
yargs-parser@21.1.1: {}

View File

@ -1,2 +1,2 @@
export * as type_data from './type' export * as type from './type'
export * as arithmetic_functions from './arithmetic' export * as arithmetic from './arithmetic'

View File

@ -2,7 +2,7 @@ import {Complex} from './type.js'
import {implementations, commonSpecs} from '@/core/Dispatcher' import {implementations, commonSpecs} from '@/core/Dispatcher'
import type {RawDependencies} 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' import type {CommonSignature, CommonReturn} from '@/interfaces/type'
// Narrowly typed signature selectors, for the operations we need to use // 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 add = 'add' as const
const divide = 'divide' as const const divide = 'divide' as const
export default function <T>() { export function common<T>() {
const baseSignature = commonSpecs<T>() const baseSignature = commonSpecs<T>()
const withComplex const withComplex
= <RD extends RawDependencies>(rd: RD) => baseSignature({ = <RD extends RawDependencies>(rd: RD) => baseSignature({
@ -150,3 +150,5 @@ export function mixed<T>() {
}) })
.ship() .ship()
} }
$reflect!([common, mixed])

View File

@ -1,5 +1,6 @@
import {implementations, commonSpecs} from '@/core/Dispatcher' import {implementations, commonSpecs} from '@/core/Dispatcher'
import {joinTypes} from '@/core/type' import {joinTypes} from '@/core/type'
import {$reflect} from '@/interfaces/type'
import type { import type {
CommonInterface, CommonSignature, NaNType, OneType, ZeroType CommonInterface, CommonSignature, NaNType, OneType, ZeroType
@ -50,7 +51,7 @@ export function lift<T>() {
}).ship() }).ship()
} }
export default function <T>() { export function common<T>() {
const baseSignature = commonSpecs<T>() const baseSignature = commonSpecs<T>()
return implementations<CommonSignature<Complex<T>>>() return implementations<CommonSignature<Complex<T>>>()
@ -69,3 +70,5 @@ export default function <T>() {
}) })
.ship() .ship()
} }
$reflect!([common, lift])

View File

@ -1,3 +1,5 @@
import {$$typeToString} from 'ts-macros'
import {Configuration} from '@/core/Configuration' import {Configuration} from '@/core/Configuration'
/* First some type utilities: */ /* First some type utilities: */
@ -114,3 +116,11 @@ export type Dependency<T, Aux = T> = {
export type Dependencies<T, Names extends SignatureKey<T>, Aux = T> = export type Dependencies<T, Names extends SignatureKey<T>, Aux = T> =
Pick<CommonSignature<T, Aux>, Names> Pick<CommonSignature<T, Aux>, Names>
// Macro for type reflection:
export function $reflect<ImplTuple>(tup: ImplTuple) {
+[[tup], <T>(elt: T) => {
(elt as {reflectedType: string}).reflectedType
= $$typeToString!<T>(true, false, true);
}]
}

View File

@ -1,2 +1,2 @@
export * as type_data from './type' export * as type from './type'
export * as arithmetic_functions from './arithmetic' export * as arithmetic from './arithmetic'

View File

@ -1,9 +1,10 @@
import {implementations} from '@/core/Dispatcher' import {implementations} from '@/core/Dispatcher'
import {$reflect} from '@/interfaces/type'
import type {CommonSignature} from '@/interfaces/type' import type {CommonSignature} from '@/interfaces/type'
const conservativeSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a) const conservativeSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a)
export default implementations<CommonSignature<number>>() export const common = implementations<CommonSignature<number>>()
.independent({ .independent({
add: (a, b) => a + b, add: (a, b) => a + b,
unaryMinus: a => -a, unaryMinus: a => -a,
@ -24,3 +25,5 @@ export default implementations<CommonSignature<number>>()
} }
}}) }})
.ship() .ship()
$reflect!([common])

View File

@ -1,5 +1,6 @@
import type {Complex} from '@/Complex/type' import type {Complex} from '@/Complex/type'
import {implementations} from '@/core/Dispatcher' import {implementations} from '@/core/Dispatcher'
import {$reflect} from '@/interfaces/type'
import type {CommonSignature} from '@/interfaces/type' import type {CommonSignature} from '@/interfaces/type'
export const number_type = { export const number_type = {
@ -22,10 +23,12 @@ declare module "@/interfaces/type" {
} }
} }
export default implementations<CommonSignature<number>>() export const common = implementations<CommonSignature<number>>()
.independent({ .independent({
zero: a => 0, zero: a => 0,
one: a => 1, one: a => 1,
nan: a => NaN, nan: a => NaN,
re: a => a re: a => a
}).ship() }).ship()
$reflect!([common])

View File

@ -5,10 +5,12 @@
"outDir": "./build", "outDir": "./build",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"undici-types": [ "ts-macros": ["./node_modules/ts-macros/dist/index.js"],
"./node_modules/undici-types/index.d.ts" "undici-types": ["./node_modules/undici-types/index.d.ts"]
]
}, },
"plugins": [
{ "transform": "ts-macros" }
],
"rootDir": "./src", "rootDir": "./src",
"target": "esnext", "target": "esnext",
"types": ["node"] "types": ["node"]