diff --git a/.gitignore b/.gitignore index d0c9d9e..aeea740 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *~ # Typescript # emitted code -obj +build # ---> Node # Logs diff --git a/README.md b/README.md index d15fcc4..968cf51 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,47 @@ A final (?) prototype for a refactor of mathjs, culminating the picomath, pocoma To build and run the prototype, run: ``` -npx tsc -node obj +pnpm install +pnpm build-and-run ``` + +## experiment + +Have a look at the section under `/src/experiment` and `/src/plugins`: + +- `src/plugins/typeInferPlugin.ts` is the actual plugin +- in `tsconfig.json` we configure TypeScript to run the plugin +- `src/experiment/arithmeticInfer.ts` with an example where we define `__infer__` +- after running TypeScript: look at `build/experiment/arithmeticInfer.ts` where the `__infer__` string literal is replaced with the actual types + +### The idea + +Create a TypeScript plugin which can replace a string literal like `__infer__` in a typed-function definition: + + typed('square', '__infer__', (dep: { ... } => { ... }) + +with the actual types, something like: + + typed('square', '{ deps: { multiply: (a: T, b: T) => T; }; return: (a: T) => T }', (dep: { ... } => { ... }) + +(We can discuss what syntax we like most, this is just a POC) + +### How to run + + pnpm build-and-run + +### Read more + +- https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin +- https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API +- https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker +- https://github.com/Microsoft/TypeScript/wiki/Using-the-Language-Service-API +- https://stackoverflow.com/questions/63944135/typescript-compiler-api-how-to-get-type-with-resolved-type-arguments +- https://stackoverflow.com/questions/48886508/typechecker-api-how-do-i-find-inferred-type-arguments-to-a-function +- https://blog.logrocket.com/using-typescript-transforms-to-enrich-runtime-code-3fd2863221ed/ +- https://github.com/itsdouges/typescript-transformer-handbook#transforms + +### Interesting libraries + +- https://github.com/GoogleFeud/ts-macros/ +- https://ts-morph.com diff --git a/package.json5 b/package.json5 index 874d5e3..6c2691d 100644 --- a/package.json5 +++ b/package.json5 @@ -4,7 +4,7 @@ description: 'A hopeful final typescipt-pragmatic mathjs proof-of-concept', main: 'index.ts', scripts: { - test: 'echo "Error: no test specified" && exit 1', + 'go': 'ttsc && echo {"type":"module"} > build/package.json && node build/index.js', }, keywords: [ 'math', @@ -17,7 +17,12 @@ type: 'git', url: 'https://code.studioinfinity.org/glen/typocomath.git', }, + dependencies: { + '@types/node': '20.5.7', + }, devDependencies: { - typescript: '^4.9.3', + 'ts-node': '10.9.1', + ttypescript: '1.5.15', + typescript: '4.7.4', }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 095704b..e931f27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,15 +1,186 @@ -lockfileVersion: 5.4 +lockfileVersion: '6.0' -specifiers: - typescript: ^4.9.3 +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@types/node': + specifier: 20.5.7 + version: 20.5.7 devDependencies: - typescript: 4.9.3 + ts-node: + specifier: 10.9.1 + version: 10.9.1(@types/node@20.5.7)(typescript@4.7.4) + ttypescript: + specifier: 1.5.15 + version: 1.5.15(ts-node@10.9.1)(typescript@4.7.4) + typescript: + specifier: 4.7.4 + version: 4.7.4 packages: - /typescript/4.9.3: - resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/node@20.5.7: + resolution: {integrity: sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==} + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /ts-node@10.9.1(@types/node@20.5.7)(typescript@4.7.4): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.5.7 + acorn: 8.10.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.7.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /ttypescript@1.5.15(ts-node@10.9.1)(typescript@4.7.4): + resolution: {integrity: sha512-48ykDNHzFnPMnv4hYX1P8Q84TvCZyL1QlFxeuxsuZ48X2+ameBgPenvmCkHJtoOSxpoWTWi8NcgNrRnVDOmfSg==} + hasBin: true + peerDependencies: + ts-node: '>=8.0.2' + typescript: '>=3.2.2' + dependencies: + resolve: 1.22.4 + ts-node: 10.9.1(@types/node@20.5.7)(typescript@4.7.4) + typescript: 4.7.4 + dev: true + + /typescript@4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} hasBin: true dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true diff --git a/src/Complex/all.ts b/src/Complex/all.ts index 3ff6311..ea13e82 100644 --- a/src/Complex/all.ts +++ b/src/Complex/all.ts @@ -1,6 +1,2 @@ -import * as Complex from './native.js' -import * as complex from './arithmetic.js' - -export { complex } - -export {Complex} +export * as Complex from './native.js' +export * as complex from './arithmetic.js' diff --git a/src/core/$reflect.ts b/src/core/$reflect.ts new file mode 100644 index 0000000..ca4f920 --- /dev/null +++ b/src/core/$reflect.ts @@ -0,0 +1,11 @@ +export function $reflect(arg: T, types?: string) : T { + // TODO: implement typed-function for real + if (!types) { + console.error('types should be resolved with runtime type information by the TypeScript plugin') + } + + console.log(`INFER: Creating function with types ${types}`) + + // TODO: here we can now turn the inputs into a real typed-function with a signature + return arg +} diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index def1bd3..4ba58fa 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -46,13 +46,17 @@ export class Dispatcher { // that's really possible, though. ) { console.log('Pretending to install', name, signature, '=>', returns) + + // @ts-ignore + // console.log(name, 'signature', reflect(signature)) + // console.log(name, 'dependencies', reflect(dependencies)) //TODO: implement me } installType(name: TypeName, typespec: TypeSpecification) { console.log('Pretending to install type', name, typespec) //TODO: implement me } - constructor(collection: SpecificationsGroup) { + constructor(collection: SpecificationsGroup) { for (const key in collection) { console.log('Working on', key) for (const identifier in collection[key]) { diff --git a/src/experiment/arithmeticInfer.ts b/src/experiment/arithmeticInfer.ts new file mode 100644 index 0000000..771d37c --- /dev/null +++ b/src/experiment/arithmeticInfer.ts @@ -0,0 +1,19 @@ +import { $reflect } from '../core/$reflect.js' +import { Dependencies } from '../interfaces/type.js' + +// unaryMinus dep is just for the experiment +// FIXME: the typescript plugin should resolve Dependencies<'multiply' | 'unaryMinus', T> +export const square = $reflect((dep: Dependencies<'multiply' | 'unaryMinus', T>): (a: T) => T => + z => dep.multiply(z, z) +) + +// export const square2 = function $reflect(dep: Dependencies<'multiply' | 'unaryMinus', T>) { +// return (z: T) => dep.multiply(z, z) +// } + +// export const square = $reflect((dep: { +// multiply: (a: T, b: T) => T, +// unaryMinus: (x: T) => T, // just for the experiment +// }): (a: T) => T => +// z => dep.multiply(z, z) +// ) diff --git a/src/index.ts b/src/index.ts index 297b271..9fd1e68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,28 @@ -import {Dispatcher} from './core/Dispatcher.js' -import * as Specifications from './all.js' +import { Complex } from './Complex/type.js' +import { absquare as absquare_complex } from './Complex/arithmetic.js' +import { square } from './experiment/arithmeticInfer.js' -export default new Dispatcher(Specifications) - -import {Complex} from './Complex/type.js' -import {absquare as absquare_complex} from './Complex/arithmetic.js' - -const mockRealAdd = (a: number, b: number) => a+b -const mockComplexAbsquare = (z: Complex) => z.re*z.re + z.im*z.im +const add = (a: number, b: number) => a + b +const multiply = (a: number, b: number) => a * b +const unaryMinus = (a: number) => -a +const absquare = (z: Complex) => z.re * z.re + z.im * z.im const quatAbsquare = absquare_complex({ - add: mockRealAdd, - absquare: mockComplexAbsquare + add, + absquare }) -const myabs = quatAbsquare({re: {re: 0, im: 1}, im: {re:2, im: 3}}) -const typeTest: typeof myabs = 7 // check myabs is just a number +const result = quatAbsquare({re: {re: 0, im: 1}, im: {re:2, im: 3}}) +const typeTest: typeof result = 7 // check myabs is just a number -console.log('Result is', myabs) +console.log() +console.log('Result is', result) + + +const mySquare = square({ + multiply, + unaryMinus +}) + +console.log() +console.log('mySquare(4)=', mySquare(4)) diff --git a/src/numbers/all.ts b/src/numbers/all.ts index deb4a8e..b71b4c3 100644 --- a/src/numbers/all.ts +++ b/src/numbers/all.ts @@ -1,3 +1 @@ -import * as numbers from './native.js' - -export {numbers} +export * as numbers from './native.js' diff --git a/src/numbers/arithmetic.ts b/src/numbers/arithmetic.ts index 11da5c2..ef0af93 100644 --- a/src/numbers/arithmetic.ts +++ b/src/numbers/arithmetic.ts @@ -10,7 +10,7 @@ export const absquare: Signature<'absquare', number> = a => a * a export const reciprocal: Signature<'reciprocal', number> = a => 1 / a export const divide: Signature<'divide', number> = (a, b) => a / b -const basicSqrt = a => isNaN(a) ? NaN : Math.sqrt(a) +const basicSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a) export const conservativeSqrt: Signature<'conservativeSqrt', number> = basicSqrt export const sqrt = diff --git a/src/plugins/typeInferPlugin.ts b/src/plugins/typeInferPlugin.ts new file mode 100644 index 0000000..d4fbe94 --- /dev/null +++ b/src/plugins/typeInferPlugin.ts @@ -0,0 +1,84 @@ +import ts from 'typescript' + +const transformer: ts.TransformerFactory = context => { + // TODO: get a reference to the program instance that the plugin is running in instead of creating a new program? + const program = ts.createProgram([], {}) + const checker = program.getTypeChecker() + + return sourceFile => { + const visitor = (node: ts.Node): ts.Node => { + // we're looking for a function call like $reflect(deps => ...) + // @ts-ignore + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.escapedText === '$reflect') { + console.log('PLUGIN: FOUND AN OCCURRENCE OF $reflect') + // console.log('PARENT') + // console.log(node) + + // TODO: validate that argNode is an ArrowFunction + // @ts-ignore + const argNode = node.arguments[0] + // @ts-ignore + const returnType = argNode.type.getText(sourceFile) + console.log('PLUGIN: RETURN TYPE') + console.log(returnType) + // (a: number) => number + + // @ts-ignore + const paramNode = argNode.parameters[0] + const paramTypeSrc = paramNode.type.getText(sourceFile) + console.log('PLUGIN: PARAM TYPE SRC', paramTypeSrc) + // { + // multiply: (a: number, b: number) => number, + // unaryMinus: (x: number) => number, // just for the experiment + // } + + // WIP + // @ts-ignore + const type = checker.getTypeAtLocation(paramNode) + const paramType = checker.typeToString(type, undefined, ts.TypeFormatFlags.InTypeAlias) + // const paramType = checker.typeToString(type) + // TDOO: get checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration) + console.log('PLUGIN: PARAM TYPE STRING', paramType) + // { multiply: (a: number, b: number) => number; unaryMinus: (x: number) => number; } + + + // WIP + // For a function definition + // const signature = checker.getResolvedSignature(node); + // if (signature != null) { + // // outputs -- (Ctor: Ctor): void + // console.log(signature) + // console.log('TEST 1', checker.signatureToString(signature)); + // // @ts-ignore + // // console.log('TEST 2', checker.getResolvedSignatureForStringLiteralCompletions(node)); + // const params = signature.getParameters(); + // for (const param of params) { + // const type = checker.getTypeOfSymbolAtLocation(param, node); + // // outputs -- Ctor + // console.log('TEST 3', checker.typeToString(type)); + // } + // } + + // WIP + const type1 = checker.getTypeAtLocation(paramNode) + const type2 = checker.getApparentType(type1) + const typeStr = checker.typeToString(type2, undefined, ts.TypeFormatFlags.InTypeAlias) + console.log('PLUGIN: RESOLVED TYPE ARGUMENT', typeStr) // TODO: not yet working + + const depsAndReturnType = `{ deps: ${paramType}; return: ${returnType} }` + + // Now we insert a second argument to the $reflect function call: a string with the types + // @ts-ignore + node.arguments.push(ts.factory.createStringLiteral(depsAndReturnType)) + + return node + } + + return ts.visitEachChild(node, visitor, context) + } + + return ts.visitNode(sourceFile, visitor) + } +} + +export default transformer diff --git a/tsconfig.json b/tsconfig.json index aae3a94..eed8f17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,14 @@ "compilerOptions": { "target": "ES2022", "rootDir": "./src", - "outDir": "./obj" + "outDir": "./build", + "esModuleInterop": true, + "allowJs": false, + "noImplicitAny": false, + "moduleResolution": "Node", + "plugins": [{ + "transform": "./src/plugins/typeInferPlugin.ts", + "type": "raw" + }] } }