diff options
| author | Akshay Nair <phenax5@gmail.com> | 2023-01-08 22:53:09 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2023-01-08 22:53:09 +0530 |
| commit | a41d70afe7692e4ef344bc3c599115009f2dad15 (patch) | |
| tree | 64fb57effa636a60751ea27178cf9a63b3b434ab /src | |
| parent | c79c5bf7a44c59cd1e484a538e2dddf264012f57 (diff) | |
| download | ts-types-lang-a41d70afe7692e4ef344bc3c599115009f2dad15.tar.gz ts-types-lang-a41d70afe7692e4ef344bc3c599115009f2dad15.zip | |
refactor: splits runtime up to use context
Diffstat (limited to 'src')
| -rw-r--r-- | src/context.ts | 80 | ||||
| -rw-r--r-- | src/eval.ts | 140 | ||||
| -rw-r--r-- | src/index.ts | 218 | ||||
| -rw-r--r-- | src/types.ts | 17 |
4 files changed, 244 insertions, 211 deletions
diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..35fa1ae --- /dev/null +++ b/src/context.ts @@ -0,0 +1,80 @@ +import { Project, ScriptTarget, Type, Node, SyntaxKind } from 'ts-morph' +import path from 'path' +import { v4 as uuid } from 'uuid'; +import { Ctx } from './types'; + +const RESULT_TYPE_NAME = '__$result' + +export const createContext = (): Ctx => { + const project = new Project({ + compilerOptions: { + target: ScriptTarget.ES3, + }, + }) + + const typeChecker = project.getTypeChecker() + + const [filePath] = process.argv.slice(2) + + if (!filePath) { + throw new Error('Must specify ts file to run') + } + + const sourceFile = project.addSourceFileAtPath(path.resolve(filePath)) + + const entryPoint = sourceFile.getExportedDeclarations().get('main')?.[0] + + if (!entryPoint) { + throw new Error('No "main" entrypoint defined in source file') + } + + const typeToString = (ty: Type | undefined): string => + ty ? typeChecker.compilerObject.typeToString(ty.compilerType) : '' + + const [resultTypeNode] = sourceFile.addStatements(`type ${RESULT_TYPE_NAME} = {}`) + + const getResultExpr = (resultKey?: string) => `${RESULT_TYPE_NAME}[${JSON.stringify(resultKey)}` + + const addResult = (name: string, ty: string): Node | undefined => + resultTypeNode + ?.asKind(SyntaxKind.TypeAliasDeclaration) + ?.getChildAtIndexIfKind(3, SyntaxKind.TypeLiteral) + ?.addProperty({ + name: JSON.stringify(name), + type: `{ output: ${ty} }`, + }) + + const createResult = (ty: string): [string, Node | undefined] => { + const resultKey = uuid() + const node = addResult(resultKey, ty) + return [resultKey, node] + } + + const customEffects: Record<string, (...args: Type[]) => any> = {} + + return { + sourceFile, + typeChecker, + entryPoint, + typeToString, + + createResult, + getResultExpr, + printResultNode: () => console.log(resultTypeNode?.print()), + + addCustomEffect: (name, expr) => { + const func = eval(expr) + Object.assign(customEffects, { [name]: func }) + }, + runCustomEffect: async (name, args) => { + const output = await customEffects[name]?.(...args) + if (output) { + const [resultKey, _] = createResult(`${JSON.stringify(output)}`) + return [resultKey] + } + return [] + }, + hasCustomEffect: (name) => customEffects[name] !== undefined, + } +} + diff --git a/src/eval.ts b/src/eval.ts new file mode 100644 index 0000000..4ba7033 --- /dev/null +++ b/src/eval.ts @@ -0,0 +1,140 @@ +import { Type } from 'ts-morph' +import { promises as fs } from 'fs' +import readline from 'readline'; + +import { match } from './util'; +import { Ctx } from './types'; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false +}); + +const readLineFromStdin = (): Promise<string> => new Promise((res) => + rl.on('line', res)) + +export const evaluateType = async (ctx: Ctx, effTyp: Type): Promise<string[]> => { + const name = effTyp.getSymbol()?.getName() + + return match(name, { + DefineEffect: async () => { + const [nameTyp, exprTyp] = effTyp.getTypeArguments() + const name = nameTyp?.getLiteralValue() as string + const exprStr = exprTyp?.getLiteralValue() as string + + ctx.addCustomEffect(name, exprStr) + return [] + }, + + Print: async () => { + console.log(...effTyp.getTypeArguments().map(ctx.typeToString)); + return [] + }, + + PutString: async () => { + const [strinTyp] = effTyp.getTypeArguments() + const typString = ctx.typeToString(strinTyp) + const string = JSON.parse(!typString.startsWith('"') ? `"${typString}"` : typString) + process.stdout.write(string); + return [] + }, + + Debug: async () => { + const [labelTyp, valueTyp] = effTyp.getTypeArguments() + const label = JSON.parse(ctx.typeToString(labelTyp)) + const value = ctx.typeToString(valueTyp) + console.log(label, value) + const [resultKey, _] = ctx.createResult(JSON.stringify(value)) + return [resultKey] + }, + + ReadFile: async () => { + const [pathTyp] = effTyp.getTypeArguments() + const filePath = JSON.parse(ctx.typeToString(pathTyp)) + const contents = await fs.readFile(filePath, 'utf-8') + const [resultKey, _] = ctx.createResult(JSON.stringify(contents)) + return [resultKey] + }, + + WriteFile: async () => { + const [pathTyp, contentsTyp] = effTyp.getTypeArguments() + const filePath = JSON.parse(ctx.typeToString(pathTyp)) + const contents = JSON.parse(ctx.typeToString(contentsTyp)) + await fs.writeFile(filePath, contents) + return [] + }, + + Bind: async () => { + const [inputTyp, chainToKind] = effTyp.getTypeArguments() + const [resultKey] = inputTyp ? await evaluateType(ctx, inputTyp) : [] + + const [_, compNode] = ctx.createResult( + `(${ctx.typeToString(chainToKind)} & { input: (${ctx.getResultExpr(resultKey)})['output'] })['return']`) + // TODO: Avoid using getTypeAtLocation? + const compTyp = compNode?.getType().getProperty('output')?.getTypeAtLocation(ctx.entryPoint) + + return compTyp ? await evaluateType(ctx, compTyp) : [] + }, + + GetEnv: async () => { + const [envTyp] = effTyp.getTypeArguments() + const envName = JSON.parse(ctx.typeToString(envTyp)) + const [resultKey, _] = ctx.createResult(`${JSON.stringify(process.env[envName] ?? '')}`) + return [resultKey] + }, + + GetArgs: async () => { + const [resultKey, _] = ctx.createResult(`${JSON.stringify(process.argv.slice(2))}`) + return [resultKey] + }, + + ReadLine: async () => { + const line = await readLineFromStdin() + const [resultKey, _] = ctx.createResult(`${JSON.stringify(line)}`) + return [resultKey] + }, + + JsExpr: async () => { + const [exprTyp] = effTyp.getTypeArguments() + const exprStr = JSON.parse(ctx.typeToString(exprTyp)) + const result = eval(`JSON.stringify(${exprStr})`) + const [resultKey, _] = ctx.createResult(`${result}`) + return [resultKey] + }, + + Seq: async () => { + const [effectTyps] = effTyp.getTypeArguments() + const effectResults = await evalList(ctx, effectTyps?.getTupleElements() ?? []) + const [resultKey, _] = ctx.createResult(`[ + ${effectResults.map(ctx.getResultExpr).join(', ')} + ]`) + return [resultKey] + }, + + Do: async () => { + const [effectTyps] = effTyp.getTypeArguments() + const effectResults = await evalList(ctx, effectTyps?.getTupleElements() ?? []) + const lastResKey = effectResults[effectResults.length - 1] + const [resultKey, _] = ctx.createResult(`${ctx.getResultExpr(lastResKey)}['output']`) + return [resultKey] + }, + + _: async () => { + if (name && ctx.hasCustomEffect(name)) { + return ctx.runCustomEffect(name, effTyp.getTypeArguments()) + } + + console.log(`${name} result effect is unhandled`) + return [] + }, + }) +} + +export const evalList = async (ctx: Ctx, effectTyps: Type[]) => { + const effectResults: string[] = [] + for (const item of effectTyps ?? []) { + effectResults.push(...(await evaluateType(ctx, item))) + } + return effectResults +} diff --git a/src/index.ts b/src/index.ts index 34038bf..2fa1f96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,218 +1,14 @@ -import { Project, ScriptTarget, Type, Node, SyntaxKind } from 'ts-morph' -import path from 'path' -import { promises as fs } from 'fs' -import readline from 'readline'; -import { v4 as uuid } from 'uuid'; -import { match } from './util'; - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false -}); - -const readLineFromStdin = (): Promise<string> => new Promise((res) => { - rl.on('line', res) -}) - -const project = new Project({ - compilerOptions: { - target: ScriptTarget.ES3, - }, -}) - -const typeChecker = project.getTypeChecker() - -const [filePath] = process.argv.slice(2) - -if (!filePath) { - throw new Error('Must specify runtime file') -} - -const sourceFile = project.addSourceFileAtPath(path.resolve(filePath)) - -const entryPoint = sourceFile.getExportedDeclarations().get('main')?.[0] - -const typeToString = (ty: Type | undefined): string => - ty ? typeChecker.compilerObject.typeToString(ty.compilerType) : '' - -const typeRefNode = entryPoint?.getLastChild() - -const RESULT_TYPE_NAME = '__$result' - -const [resultTypeNode] = sourceFile.addStatements(`type ${RESULT_TYPE_NAME} = {}`) - -const addResult = (name: string, ty: string): Node | undefined => { - if (resultTypeNode?.isKind(SyntaxKind.TypeAliasDeclaration)) { - const value = resultTypeNode.getChildAtIndex(3) - if (value.isKind(SyntaxKind.TypeLiteral)) { - return value.addProperty({ - name: JSON.stringify(name), - type: `{ output: ${ty} }`, - }) - } - } - return -} - -const customEffects: Record<string, (...args: Type[]) => any> = {} - -const evaluateType = async (effTyp: Type, node: Node): Promise<string[]> => { - const name = effTyp.getSymbol()?.getName() - // console.log(name) - - return match(name, { - DefineEffect: async () => { - const [nameTyp, exprTyp] = effTyp.getTypeArguments() - const name = nameTyp?.getLiteralValue() as string - const exprStr = exprTyp?.getLiteralValue() as string - const func = eval(exprStr) - - Object.assign(customEffects, { [name]: func }) - return [] - }, - - Print: async () => { - console.log(...effTyp.getTypeArguments().map(typeToString)); - return [] - }, - - PutString: async () => { - const [strinTyp] = effTyp.getTypeArguments() - const typString = typeToString(strinTyp) - const string = JSON.parse(!typString.startsWith('"') ? `"${typString}"` : typString) - process.stdout.write(string); - return [] - }, - - Debug: async () => { - const [labelTyp, valueTyp] = effTyp.getTypeArguments() - const label = JSON.parse(typeToString(labelTyp)) - const value = typeToString(valueTyp) - console.log(label, value) - // TODO: Return value - return [] - }, - - ReadFile: async () => { - const [pathTyp] = effTyp.getTypeArguments() - const filePath = JSON.parse(typeToString(pathTyp)) - const contents = await fs.readFile(filePath, 'utf-8') - const hash = uuid() - addResult(hash, JSON.stringify(contents)) - return [hash] - }, - - WriteFile: async () => { - const [pathTyp, contentsTyp] = effTyp.getTypeArguments() - const filePath = JSON.parse(typeToString(pathTyp)) - const contents = JSON.parse(typeToString(contentsTyp)) - await fs.writeFile(filePath, contents) - return [] - }, - - Bind: async () => { - const [inputTyp, chainToKind] = effTyp.getTypeArguments() - const [resultKey] = inputTyp ? await evaluateType(inputTyp, node) : [] - - const hash = uuid() - const compNode = addResult(hash, - `(${typeToString(chainToKind)} & { input: ${RESULT_TYPE_NAME}[${JSON.stringify(resultKey)}]['output'] })['return']`) - const compTyp = compNode?.getType().getProperty('output')?.getTypeAtLocation(node) - - return compTyp ? await evaluateType(compTyp, node) : [] - }, - - GetEnv: async () => { - const [envTyp] = effTyp.getTypeArguments() - const envName = JSON.parse(typeToString(envTyp)) - const hash = uuid() - addResult(hash, `${JSON.stringify(process.env[envName] ?? '')}`) - return [hash] - }, - - GetArgs: async () => { - const hash = uuid() - addResult(hash, `${JSON.stringify(process.argv.slice(2))}`) - return [hash] - }, - - ReadLine: async () => { - const line = await readLineFromStdin() - const hash = uuid() - addResult(hash, `${JSON.stringify(line)}`) - return [hash] - }, - - JsExpr: async () => { - const [exprTyp] = effTyp.getTypeArguments() - const exprStr = JSON.parse(typeToString(exprTyp)) - const result = eval(`JSON.stringify(${exprStr})`) - const hash = uuid() - addResult(hash, `${result}`) - return [hash] - }, - - Seq: async () => { - const [effectTyps] = effTyp.getTypeArguments() - const effectResults = await evalList(effectTyps?.getTupleElements() ?? [], node) - const hash = uuid() - addResult(hash, `[ - ${effectResults.map(r => `${RESULT_TYPE_NAME}[${JSON.stringify(r)}]`).join(', ')} - ]`) - return [hash] - }, - - Do: async () => { - const [effectTyps] = effTyp.getTypeArguments() - const effectResults = await evalList(effectTyps?.getTupleElements() ?? [], node) - const resultKey = effectResults[effectResults.length - 1] - const hash = uuid() - addResult(hash, `${RESULT_TYPE_NAME}[${JSON.stringify(resultKey)}]['output']`) - return [hash] - }, - - _: async () => { - if (name && customEffects[name]) { - const out = await customEffects[name]?.(...effTyp.getTypeArguments()) - if (out) { - const hash = uuid() - addResult(hash, `${JSON.stringify(out)}`) - return [hash] - } - } else { - console.log(`${name} result effect is unhandled`) - } - return [] - }, - }) -} - -const evalList = async (effectTyps: Type[], node: Node) => { - const effectResults: string[] = [] - for (const item of effectTyps ?? []) { - effectResults.push(...(await evaluateType(item, node))) - } - return effectResults -} +import { createContext } from './context'; +import { evalList } from './eval'; const main = async () => { - if (typeRefNode) { - const resultType = entryPoint?.getType() - - if (resultType) { - const effects = resultType.isTuple() ? resultType.getTupleElements() : [resultType] - await evalList(effects, typeRefNode) - } - } + const ctx = createContext() + const resultType = ctx.entryPoint.getType() + const effects = resultType.isTuple() ? resultType.getTupleElements() : [resultType] + await evalList(ctx, effects) } main() - .then(() => { - // console.log(entryPoint?.print()) - // console.log(resultTypeNode?.print()) - rl.close() - process.exit(0) - }) + .then(() => process.exit(0)) .catch(e => (console.error(e), process.exit(1))) diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a3cf3bb --- /dev/null +++ b/src/types.ts @@ -0,0 +1,17 @@ +import { ExportedDeclarations, Node, SourceFile, Type, TypeChecker } from "ts-morph" + +export interface Ctx { + sourceFile: SourceFile + typeChecker: TypeChecker + entryPoint: ExportedDeclarations + typeToString: (ty: Type | undefined) => string + + createResult: (ty: string) => [string, Node | undefined] + getResultExpr: (key?: string) => string + printResultNode: () => void + + addCustomEffect: (name: string, expr: string) => void + runCustomEffect: (name: string, args: Type[]) => Promise<string[]> + hasCustomEffect: (name: string) => boolean +} + |
