diff options
Diffstat (limited to 'src/runtime.ts')
| -rw-r--r-- | src/runtime.ts | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 0000000..11a7e61 --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,194 @@ +import { Project, ScriptTarget, Type, Node, StringLiteral, TypeFormatFlags, SyntaxKind } from 'ts-morph' +import path from 'path' +import { promises as fs } from 'fs' +import readline from 'readline'; + +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 sourceFile = project.addSourceFileAtPath(path.resolve("./src/index.ts")) + +const entryPoint = sourceFile.getExportedDeclarations().get('main')?.[0] + +const typeToString = (ty: Type | undefined): string => + ty ? typeChecker.compilerObject.typeToString(ty.compilerType) : '' + +const getPropertyType = (n: Node, prop: string): Type | undefined => { + const tt = typeChecker.getTypeAtLocation(n) + const propSym = tt.getProperty(prop) + const ty = propSym && typeChecker.getTypeOfSymbolAtLocation(propSym, n) + return ty +} + +const typeRefNode = entryPoint?.getLastChild() + +const RESULT_TYPE_NAME = '__$result' + +const [statement] = sourceFile.addStatements(`type ${RESULT_TYPE_NAME} = {}`) + +const addResult = (name: string, ty: string) => { + if (statement.isKind(SyntaxKind.TypeAliasDeclaration)) { + const value = statement.getChildAtIndex(3) + if (value.isKind(SyntaxKind.TypeLiteral)) { + value.addProperty({ + name: JSON.stringify(name), + type: `{ output: ${ty} }`, + }) + } + } +} + +const createHash = () => + Math.random().toFixed(8).slice(2) + +const match = <K extends string, R>(k: K | undefined, pattern: { [key in K | '_']: () => R }) => + k && pattern[k] ? pattern[k]() : pattern._() + +const accumulateResults = async (effTyp: Type, node: Node): Promise<string[]> => { + const name = effTyp.getSymbol()?.getName() + + return match(name, { + ReadFile: async () => { + const [pathTyp] = effTyp.getTypeArguments() + const filePath = JSON.parse(typeToString(pathTyp)) + const contents = await fs.readFile(filePath, 'utf-8') + const hash = createHash() + addResult(hash, JSON.stringify(contents)) + return [hash] + }, + + ChainIO: async () => { + const inputTyp = effTyp.getProperty('input')?.getTypeAtLocation(node) + const inputResults = inputTyp && await accumulateResults(inputTyp, node) + return inputResults ?? [] + }, + + GetEnv: async () => { + const [envTyp] = effTyp.getTypeArguments() + const envName = JSON.parse(typeToString(envTyp)) + const hash = createHash() + addResult(hash, `${JSON.stringify(process.env[envName] ?? '')}`) + return [hash] + }, + + ReadLine: async () => { + const line = await readLineFromStdin() + const hash = createHash() + addResult(hash, `${JSON.stringify(line)}`) + return [hash] + }, + + JsExpr: async () => { + const [exprTyp] = effTyp.getTypeArguments() + const exprStr = JSON.parse(typeToString(exprTyp)) + const result = eval(exprStr) + const hash = createHash() + addResult(hash, `${JSON.stringify(result)}`) + return [hash] + }, + + _: async () => { + console.log(`${name} result effect is unhandled`) + return [] + }, + }) +} + +const evalAccumulator = async (effNode: Node, node: Node) => { + const effTyp = effNode.getType() + const name = effTyp.getSymbol()?.getName() + + return match(name, { + Print: async () => { + console.log(...effTyp.getTypeArguments().map(typeToString)); + }, + + Debug: async () => { + const [labelTyp, valueTyp] = effTyp.getTypeArguments() + const label = JSON.parse(typeToString(labelTyp)) + const value = JSON.parse(typeToString(valueTyp)) + console.log(label, value) + }, + + ReadFile: async () => { + const [hash] = await accumulateResults(effTyp, node) + effNode.replaceWithText(`${RESULT_TYPE_NAME}[${JSON.stringify(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) + }, + + ChainIO: async () => { + const chainToKind = effTyp.getProperty('chainTo')?.getTypeAtLocation(node) + const [hashRes] = await accumulateResults(effTyp, node) + const chainRes = `(${typeToString(chainToKind)} & { input: ${RESULT_TYPE_NAME}[${JSON.stringify(hashRes)}]['output'] })['return']` + const updateEffNode = effNode.replaceWithText(chainRes) + await evalAccumulator(updateEffNode, node) + }, + + _: async () => { + console.log(effNode.print()) + console.log('TTTT', typeToString(effTyp)) + console.log(`${name} effect is unhandled`) + } + }) +} + +const main = async () => { + if (typeRefNode) { + const resultType = entryPoint?.getType() + + if (typeRefNode && entryPoint && resultType?.getSymbol()?.getName() === 'Program') { + const exitCodeTy = getPropertyType(typeRefNode, 'exitCode') + const effectTypes = getPropertyType(typeRefNode, 'effects') + if (effectTypes?.isTuple()) { + const effectNodes = entryPoint.getChildrenOfKind(SyntaxKind.TypeReference) + .flatMap(n => n.getChildrenOfKind(SyntaxKind.TupleType)) + .flatMap(tt => tt.getChildrenOfKind(SyntaxKind.SyntaxList)) + .flatMap(n => n.getChildren()) + .filter(n => !n.isKind(SyntaxKind.CommaToken)) + + for (const n of effectNodes) { + await evalAccumulator(n, typeRefNode) + } + } + + const exitCode = exitCodeTy?.getLiteralValue() as number + + if (exitCode !== 0) { + process.exit(exitCode) + } + } else { + const ty = typeChecker.getTypeAtLocation(typeRefNode) + console.log(typeToString(ty)) + } + } +} + +main() + .then(() => { + // console.log(entryPoint?.print()) + // console.log(statement?.print()) + process.exit(0) + }) + .catch(e => (console.error(e), process.exit(1))) + |
