From b9cfe706fc04558b32231d84fd0328972548a845 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Mon, 13 Mar 2023 16:04:11 +0100 Subject: [PATCH 1/3] Experiment of creating a TypeScript plugin (WIP) --- package.json5 | 2 + plugins/.gitignore | 1 + plugins/infer.ts | 98 +++++++++++++++++++++++++++++++++ src/generic/arithmeticDirect.ts | 8 +++ src/generic/infer.ts | 4 ++ 5 files changed, 113 insertions(+) create mode 100644 plugins/.gitignore create mode 100644 plugins/infer.ts create mode 100644 src/generic/arithmeticDirect.ts create mode 100644 src/generic/infer.ts diff --git a/package.json5 b/package.json5 index 959a089..c6dd1cb 100644 --- a/package.json5 +++ b/package.json5 @@ -5,6 +5,8 @@ main: 'index.ts', scripts: { 'build-and-run': 'ttsc -b && node build', + 'experiment-infer': 'tsc plugins/infer.ts && node plugins/infer.js ./src/generic/arithmetic.ts', + 'experiment-infer-direct': 'tsc plugins/infer.ts && node plugins/infer.js ./src/generic/arithmeticDirect.ts', test: 'echo "Error: no test specified" && exit 1', }, keywords: [ diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/plugins/infer.ts b/plugins/infer.ts new file mode 100644 index 0000000..03b792d --- /dev/null +++ b/plugins/infer.ts @@ -0,0 +1,98 @@ +import { readFileSync } from "fs"; +import * as ts from "typescript"; +import { inspect } from 'util' + +/** + * # The idea + * + * Create a TypeScript plugin which can replace structures like: + * + * infer(factoryFunction) + * + * where `factoryFunction` is a mathjs factory function in TypeScript, with something like: + * + * infer({ signature: factoryFunction }) + * + * where `signature` is a string containing the type of the factory function and its dependencies. + * + * # How to run + * + * pnpm experiment-infer + * pnpm experiment-infer-direct + * + * # 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-Language-Service-API + */ + +export function infer(sourceFile: ts.SourceFile) { + recurse(sourceFile); + + function getType(kind: number) { + switch(kind) { + case ts.SyntaxKind.NumberKeyword: return 'number' + case ts.SyntaxKind.StringKeyword: return 'string' + case ts.SyntaxKind.BooleanKeyword: return 'boolean' + default: return String(ts.SyntaxKind[kind]) // TODO: work out all types + } + } + + function recurse(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Identifier) { + console.log('Identifier', node['escapedText'], ts.SyntaxKind[node.kind]) + } + + // recognize a structure like: + // + // export const square = infer((dep: { + // multiply: (a: number, b: number) => number + // }): (a: number) => number => + // z => dep.multiply(z, z) + // ) + if (node?.['name']?.kind === ts.SyntaxKind.Identifier && node?.['name']['escapedText'] === 'dep') { + console.log('dep', getType(node['type'].kind), node) + + node['type']?.members?.forEach(member => { + console.log('member', { + name: member.name.escapedText, + parameters: member.type.parameters.map(parameter => { + return parameter.name.escapedText + ': ' + getType(parameter.type.kind) + }), + returns: getType(member.type.type.kind) + }) + }) + } + + // recognize a structure like: + // + // export const square = + // (dep: Dependencies<'multiply' | 'unaryMinus', T>): Signature<'square', T> => + // z => dep.multiply(z, z) + if (node?.['name']?.kind === ts.SyntaxKind.Identifier && node?.['name']['escapedText'] === 'dep') { + // TODO + } + + ts.forEachChild(node, recurse); + } +} + +const fileNames = process.argv.slice(2); +console.log('infer files', fileNames) +fileNames.forEach(fileName => { + // Parse a file + const sourceFile = ts.createSourceFile( + fileName, + readFileSync(fileName).toString(), + ts.ScriptTarget.ES2022, + /*setParentNodes */ true + ); + + console.log('AST', fileName, inspect(sourceFile, { depth: null, colors: true })) + + console.log(sourceFile.text) + console.log() + + infer(sourceFile); +}); diff --git a/src/generic/arithmeticDirect.ts b/src/generic/arithmeticDirect.ts new file mode 100644 index 0000000..65a7782 --- /dev/null +++ b/src/generic/arithmeticDirect.ts @@ -0,0 +1,8 @@ +import {infer} from './infer' + +export const square = infer((dep: { + multiply: (a: number, b: number) => number, + unaryMinus: (x: number) => number, // just for the experiment +}): (a: number) => number => + z => dep.multiply(z, z) +) diff --git a/src/generic/infer.ts b/src/generic/infer.ts new file mode 100644 index 0000000..6e654cb --- /dev/null +++ b/src/generic/infer.ts @@ -0,0 +1,4 @@ +export function infer(arg: T) : T { + console.error('infer should be replace with runtime type information by a magic TypeScript plugin') + return arg +} From aa044a54e78f547516fa0f7ef497d91da27c7c3f Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Mon, 13 Mar 2023 16:31:41 +0100 Subject: [PATCH 2/3] Move the experiment into src/plugins and src/experiment --- package.json5 | 4 ++-- plugins/.gitignore | 1 - .../arithmeticDirect.ts => experiment/arithmeticInfer.ts} | 2 +- {plugins => src/plugins}/infer.ts | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 plugins/.gitignore rename src/{generic/arithmeticDirect.ts => experiment/arithmeticInfer.ts} (84%) rename {plugins => src/plugins}/infer.ts (97%) diff --git a/package.json5 b/package.json5 index c6dd1cb..2fcf926 100644 --- a/package.json5 +++ b/package.json5 @@ -5,8 +5,8 @@ main: 'index.ts', scripts: { 'build-and-run': 'ttsc -b && node build', - 'experiment-infer': 'tsc plugins/infer.ts && node plugins/infer.js ./src/generic/arithmetic.ts', - 'experiment-infer-direct': 'tsc plugins/infer.ts && node plugins/infer.js ./src/generic/arithmeticDirect.ts', + 'experiment-infer': 'ttsc -b && node build/plugins/infer.js ./src/generic/arithmetic.ts', + 'experiment-infer-direct': 'ttsc -b && node build/plugins/infer.js ./src/experiment/arithmeticInfer.ts', test: 'echo "Error: no test specified" && exit 1', }, keywords: [ diff --git a/plugins/.gitignore b/plugins/.gitignore deleted file mode 100644 index a6c7c28..0000000 --- a/plugins/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/src/generic/arithmeticDirect.ts b/src/experiment/arithmeticInfer.ts similarity index 84% rename from src/generic/arithmeticDirect.ts rename to src/experiment/arithmeticInfer.ts index 65a7782..92aeffd 100644 --- a/src/generic/arithmeticDirect.ts +++ b/src/experiment/arithmeticInfer.ts @@ -1,4 +1,4 @@ -import {infer} from './infer' +import {infer} from '../generic/infer' export const square = infer((dep: { multiply: (a: number, b: number) => number, diff --git a/plugins/infer.ts b/src/plugins/infer.ts similarity index 97% rename from plugins/infer.ts rename to src/plugins/infer.ts index 03b792d..8efdc22 100644 --- a/plugins/infer.ts +++ b/src/plugins/infer.ts @@ -24,6 +24,7 @@ import { inspect } from 'util' * * - 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 */ From f8553aa748fff0e843661fe9b71a09583a035aac Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Tue, 14 Mar 2023 09:51:28 +0100 Subject: [PATCH 3/3] Add another experiment infer2 (WIP) --- README.md | 50 +++++++++++++ package.json5 | 5 +- ...arithmeticInfer.ts => arithmeticInfer1.ts} | 0 src/experiment/arithmeticInfer2.ts | 15 ++++ src/plugins/{infer.ts => infer1.ts} | 26 ------- src/plugins/infer2.ts | 74 +++++++++++++++++++ 6 files changed, 142 insertions(+), 28 deletions(-) rename src/experiment/{arithmeticInfer.ts => arithmeticInfer1.ts} (100%) create mode 100644 src/experiment/arithmeticInfer2.ts rename src/plugins/{infer.ts => infer1.ts} (75%) create mode 100644 src/plugins/infer2.ts diff --git a/README.md b/README.md index 49e5266..44b4a46 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,53 @@ To build and run the prototype, run: pnpm install pnpm build-and-run ``` + +## experiment + +See: the section under `/src/experiment` and `/src/plugins`. + +### The idea + +Create a TypeScript plugin which can replace structures like: + + infer(factoryFunction) + +where `factoryFunction` is a mathjs factory function in TypeScript, with something like: + + infer({ signature: factoryFunction }) + +where `signature` is a string containing the type of the factory function and its dependencies. + +Relevant methods of the TypeScript compiler are: + +```ts +const program = ts.createProgram(fileNames, options) +const typeChecker = program.getTypeChecker() + +// relevant methods: +// +// typeChecker.getSymbolAtLocation +// typeChecker.getTypeOfSymbolAtLocation +// typeChecker.getResolvedSignature +// typeChecker.getSignaturesOfType +``` + +### Status + +None of the experiments (`infer1` and `infer2`) are outputting something useful yet. + + +### How to run + + pnpm experiment:infer1 + pnpm experiment:infer1-direct + pnpm experiment:infer2 + +### 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 \ No newline at end of file diff --git a/package.json5 b/package.json5 index 2fcf926..a140cb8 100644 --- a/package.json5 +++ b/package.json5 @@ -5,8 +5,9 @@ main: 'index.ts', scripts: { 'build-and-run': 'ttsc -b && node build', - 'experiment-infer': 'ttsc -b && node build/plugins/infer.js ./src/generic/arithmetic.ts', - 'experiment-infer-direct': 'ttsc -b && node build/plugins/infer.js ./src/experiment/arithmeticInfer.ts', + 'experiment:infer1': 'ttsc -b && node build/plugins/infer.js ./src/generic/arithmetic.ts', + 'experiment:infer1-direct': 'ttsc -b && node build/plugins/infer.js ./src/experiment/arithmeticInfer1.ts', + 'experiment:infer2': 'ttsc -b && node build/plugins/infer2.js ./src/experiment/arithmeticInfer2.ts', test: 'echo "Error: no test specified" && exit 1', }, keywords: [ diff --git a/src/experiment/arithmeticInfer.ts b/src/experiment/arithmeticInfer1.ts similarity index 100% rename from src/experiment/arithmeticInfer.ts rename to src/experiment/arithmeticInfer1.ts diff --git a/src/experiment/arithmeticInfer2.ts b/src/experiment/arithmeticInfer2.ts new file mode 100644 index 0000000..ab4eebf --- /dev/null +++ b/src/experiment/arithmeticInfer2.ts @@ -0,0 +1,15 @@ +import { infer } from '../generic/infer' +import { Dependencies, Signature } from '../interfaces/type' + +export type multiplyDep = Dependencies<'multiply', T> + +export const square1 = + (dep: Dependencies<'multiply', T>): Signature<'square', T> => + z => dep.multiply(z, z) + +export const square2 = infer((dep: { + multiply: (a: number, b: number) => number, + unaryMinus: (x: number) => number, // just for the experiment +}): (a: number) => number => + z => dep.multiply(z, z) +) diff --git a/src/plugins/infer.ts b/src/plugins/infer1.ts similarity index 75% rename from src/plugins/infer.ts rename to src/plugins/infer1.ts index 8efdc22..81fa0a6 100644 --- a/src/plugins/infer.ts +++ b/src/plugins/infer1.ts @@ -2,32 +2,6 @@ import { readFileSync } from "fs"; import * as ts from "typescript"; import { inspect } from 'util' -/** - * # The idea - * - * Create a TypeScript plugin which can replace structures like: - * - * infer(factoryFunction) - * - * where `factoryFunction` is a mathjs factory function in TypeScript, with something like: - * - * infer({ signature: factoryFunction }) - * - * where `signature` is a string containing the type of the factory function and its dependencies. - * - * # How to run - * - * pnpm experiment-infer - * pnpm experiment-infer-direct - * - * # 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 - */ - export function infer(sourceFile: ts.SourceFile) { recurse(sourceFile); diff --git a/src/plugins/infer2.ts b/src/plugins/infer2.ts new file mode 100644 index 0000000..43fc64b --- /dev/null +++ b/src/plugins/infer2.ts @@ -0,0 +1,74 @@ +import * as ts from "typescript" + +// based on: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker + +infer2(process.argv.slice(2), { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS +}) + +function infer2( + fileNames: string[], + options: ts.CompilerOptions +): void { + const program = ts.createProgram(fileNames, options) + const typeChecker = program.getTypeChecker() + + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + ts.forEachChild(sourceFile, visit) + } + } + + return + + function visit(node: ts.Node) { + // // Only consider exported nodes + // if (!isNodeExported(node)) { + // return; + // } + + // console.log('Node', node.kind, node?.['name']?.escapedText) + + if (ts.isModuleDeclaration(node)) { + // This is a namespace, visit its children + console.log('check') + ts.forEachChild(node, visit); + } else if (ts.isTypeAliasDeclaration(node)) { + console.log('isTypeAliasDeclaration', node.name.escapedText) + + let symbol = typeChecker.getSymbolAtLocation(node.name); + if (symbol) { + const symbolType = typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration) + const symbolSignature = typeChecker.getSignaturesOfType(symbolType, ts.SignatureKind.Call) + + // checker.getResolvedSignature(symbol) + console.log('symbol', symbol.getName(), symbolSignature) + + // getTypeOfSymbolAtLocation + // getResolvedSignature + } + } else if (ts.isCallExpression(node)) { + console.log('isCallExpression', node.expression) + } else if (ts.isFunctionDeclaration(node)) { + console.log('isFunctionDeclaration', node.name.escapedText, { typeParameter0: node.typeParameters[0] }) + + if (node.name.escapedText === 'infer') { + const param0 = node.typeParameters[0] + if (ts.isPropertyDeclaration(param0)) { + const symbol = typeChecker.getSymbolAtLocation(param0) + + // TODO: get resolving + + // console.log('getResolvedSignature', typeChecker.getResolvedSignature(node) ) + + // const symbolType = typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration) + // const symbolSignature = typeChecker.getSignaturesOfType(symbolType, ts.SignatureKind.Call) + // console.log('symbol', symbol.getName(), symbolSignature) + + // console.log('getSignaturesOfType', typeChecker.getSignaturesOfType(param0) + } + } + } + } +}