diff options
| -rw-r--r-- | examples/test-runner.ts | 39 | ||||
| -rw-r--r-- | src/context.ts | 9 | ||||
| -rw-r--r-- | src/eval-env/builtins.ts | 142 | ||||
| -rw-r--r-- | src/eval-env/node.ts | 61 | ||||
| -rw-r--r-- | src/eval-env/test.ts | 31 | ||||
| -rw-r--r-- | src/eval.ts | 221 | ||||
| -rw-r--r-- | src/types.ts | 5 | ||||
| -rw-r--r-- | stdlib/sys.ts | 2 | ||||
| -rw-r--r-- | stdlib/test.ts | 10 | ||||
| -rw-r--r-- | stdlib/util.ts | 6 |
10 files changed, 314 insertions, 212 deletions
diff --git a/examples/test-runner.ts b/examples/test-runner.ts index fecd3e4..3302fbb 100644 --- a/examples/test-runner.ts +++ b/examples/test-runner.ts @@ -1,44 +1,29 @@ -import { Do, Effect } from '../stdlib/effect' -import { PutStringLn } from '../stdlib/stdio' -import { DefineEffect } from '../stdlib/sys' +import { Do, Kind1 } from '../stdlib/effect' +import { Print, PutStringLn } from '../stdlib/stdio' +import { SetEvalEnvironment } from '../stdlib/sys' +import { Test, Assert } from '../stdlib/test' +import { Equals, Not } from '../stdlib/util' -type Test<m extends string, effs extends Effect[]> = [ - PutStringLn<`* ${m}`>, - ...effs, -] - -type Equals<Left, Right> = - [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false - -type Not<B extends boolean> = B extends true ? false : true - -interface TestConfig { - CompileTestFailures: false +interface PrintK extends Kind1 { + return: Print<this['input']> } -type Assertion = TestConfig['CompileTestFailures'] extends true ? true : boolean -interface Assert<_B extends Assertion> extends Effect { } - export type main = [ - DefineEffect<'Assert', `([b], ctx) => { - if (!ctx.getTypeValue(b)) { - throw new Error('AAAAAAA') - } - }`>, + SetEvalEnvironment<'test.node'>, PutStringLn<"Running tests...">, Do<[ - ...Test<"should do some stuff", [ - Assert<Equals<1, 1>>, + Test<"should do some stuff", [ + Assert<Equals<1, 2>>, Assert<Not<Equals<2, 1>>>, ]>, - ...Test<"hello world", [ + Test<"hello world", [ Assert<Equals<1, 1>>, ]>, - ...Test<"should do some other stuff", [ + Test<"should do some other stuff", [ Assert<Equals<1, 1>>, ]>, ]>, diff --git a/src/context.ts b/src/context.ts index 6b7875c..485083a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,6 +2,7 @@ import { Project, ScriptTarget, Type, Node, SyntaxKind } from 'ts-morph' import path from 'path' import { v4 as uuid } from 'uuid' import { Ctx } from './types' +import { evaluateType } from './eval' const RESULT_TYPE_NAME = '__$result' @@ -72,6 +73,9 @@ export const createContext = (options: CtxOptions): Ctx => { return key } + let currentEnv = 'node' + const setEnv = (e: string) => (currentEnv = e) + const ctx: Ctx = { sourceFile, typeChecker, @@ -79,6 +83,9 @@ export const createContext = (options: CtxOptions): Ctx => { typeToString, getTypeValue, + get currentEnv() { return currentEnv }, + setEnv, + createRef, getRef: (k: string) => refMap.get(k), setRef: (k: string, ty: string) => refMap.set(k, ty), @@ -101,6 +108,8 @@ export const createContext = (options: CtxOptions): Ctx => { return [] }, hasCustomEffect: (name) => customEffects[name] !== undefined, + + evaluateType, } return ctx diff --git a/src/eval-env/builtins.ts b/src/eval-env/builtins.ts new file mode 100644 index 0000000..3adde06 --- /dev/null +++ b/src/eval-env/builtins.ts @@ -0,0 +1,142 @@ +import { Type } from 'ts-morph' +import { Ctx } from "../types" + +export default (ctx: Ctx, args: Type[]) => ({ + SetEvalEnvironment: async () => { + ctx.setEnv(ctx.getTypeValue(args[0])) + return [] + }, + + DefineEffect: async () => { + const [nameTyp, exprTyp] = args + const name = nameTyp?.getLiteralValue() as string + const exprStr = exprTyp?.getLiteralValue() as string + + ctx.addCustomEffect(name, exprStr) + return [] + }, + + CreateRef: async () => { + const val = ctx.typeToString(args[0]) + const refKey = ctx.createRef(val) + const [resultKey, _] = ctx.createResult(JSON.stringify(refKey)) + return [resultKey] + }, + + GetRef: async () => { + const refKey = ctx.getTypeValue(args[0]) + const val = ctx.getRef(refKey) + if (!val) throw new Error('Ref has been deleted') + const [resultKey, _] = ctx.createResult(val) + return [resultKey] + }, + + SetRef: async () => { + const [ keyTy, valTyp ] = args + ctx.setRef(ctx.getTypeValue(keyTy), ctx.typeToString(valTyp)) + return [] + }, + + DeleteRef: async () => { + ctx.deleteRef(ctx.getTypeValue(args[0])) + return [] + }, + + Pure: async () => { + const [valTyp] = args + const [resultKey, _] = ctx.createResult(ctx.typeToString(valTyp)) + return [resultKey] + }, + + Print: async () => { + console.log(...args.map(ctx.typeToString)) + return [] + }, + + PutString: async () => { + const [strinTyp] = args + const typString = ctx.getTypeValue(strinTyp) ?? ctx.typeToString(strinTyp) + process.stdout.write(typString) + return [] + }, + + Debug: async () => { + const [labelTyp, valueTyp] = args + const label = ctx.getTypeValue(labelTyp) + const value = ctx.typeToString(valueTyp) + console.log(label, value) + const [resultKey, _] = ctx.createResult(JSON.stringify(value)) + return [resultKey] + }, + + Bind: async () => { + const [inputTyp, chainToKind] = args + const [resultKey] = inputTyp ? await ctx.evaluateType(ctx, inputTyp) : [] + + // TODO: Handle resultKey undefined case + 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 ctx.evaluateType(ctx, compTyp) : [] + }, + + Try: async () => { + const [effTyp, catchK] = args + + try { + if (!effTyp) throw new Error('wow') + return await ctx.evaluateType(ctx, effTyp) + } catch(e) { + const error = JSON.stringify((e as any)?.message ?? e) + const catchResExpr = `(${ctx.typeToString(catchK)} & { input: ${error} })['return']` + const [resultKey, _] = ctx.createResult(catchResExpr) + return [resultKey] + } + }, + + Throw: async () => { + throw args[0] && ctx.getTypeValue(args[0]) + }, + + JsExpr: async () => { + const [exprTyp] = args + const exprStr = ctx.getTypeValue(exprTyp) + const result = eval(`JSON.stringify(${exprStr})`) + const [resultKey, _] = ctx.createResult(`${result}`) + return [resultKey] + }, + + Seq: async () => { + const [effectTyps] = args + const effectResults = await evalList( + ctx, + effectTyps?.getTupleElements() ?? [] + ) + const [resultKey, _] = ctx.createResult(`[ + ${effectResults.map(ctx.getResultExpr).join(', ')} + ]`) + return [resultKey] + }, + + Do: async () => { + const [effectTyps] = args + const effectResults = await evalList( + ctx, + effectTyps?.getTupleElements() ?? [] + ) + // TODO: Use last type's result instead of last result key + const lastResKey = effectResults[effectResults.length - 1] + const [resultKey, _] = ctx.createResult( + `(${ctx.getResultExpr(lastResKey)})['output']` + ) + return [resultKey] + }, +}) diff --git a/src/eval-env/node.ts b/src/eval-env/node.ts new file mode 100644 index 0000000..5e73987 --- /dev/null +++ b/src/eval-env/node.ts @@ -0,0 +1,61 @@ +import { Type } from 'ts-morph' +import { promises as fs } from 'fs' +import readline from 'readline' +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 cleanup = () => { + rl.close() +} + +export default (ctx: Ctx, args: Type[]) => ({ + ReadFile: async () => { + const [pathTyp] = args + const filePath = ctx.getTypeValue(pathTyp) + const contents = await fs.readFile(filePath, 'utf-8') + const [resultKey, _] = ctx.createResult(JSON.stringify(contents)) + return [resultKey] + }, + + WriteFile: async () => { + const [pathTyp, contentsTyp] = args + const filePath = ctx.getTypeValue(pathTyp) + const contents = ctx.getTypeValue(contentsTyp) + await fs.writeFile(filePath, contents) + return [] + }, + + GetEnv: async () => { + const [envTyp] = args + const envName = ctx.getTypeValue(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] + }, + + Exit: async () => { + process.exit(args[0] && ctx.getTypeValue(args[0])) + }, + + ReadLine: async () => { + const line = await readLineFromStdin() + const [resultKey, _] = ctx.createResult(`${JSON.stringify(line)}`) + return [resultKey] + }, +}) diff --git a/src/eval-env/test.ts b/src/eval-env/test.ts new file mode 100644 index 0000000..c394752 --- /dev/null +++ b/src/eval-env/test.ts @@ -0,0 +1,31 @@ +import { Type } from 'ts-morph' +import { Ctx } from '../types' + +export const cleanup = () => {} + +export default (ctx: Ctx, args: Type[]) => ({ + Test: async () => { + const [msg, effs] = args + process.stdout.write(` - ${ctx.getTypeValue(msg)}`) + + try { + for (const eff of effs?.getTupleElements() ?? []) { + await ctx.evaluateType(ctx, eff) + } + + console.log(' [✓]') + } catch(e) { + console.log(' [TEST FAILED]') + throw e + } + return [] + }, + + Assert: async () => { + const [b] = args + if (!ctx.getTypeValue(b)) { + throw new Error('Assertion failed') + } + return [] + }, +}) diff --git a/src/eval.ts b/src/eval.ts index 8b53982..fdffed7 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -1,22 +1,28 @@ import { Type } from 'ts-morph' -import { promises as fs } from 'fs' -import readline from 'readline' import { match } from './util' import { Ctx } from './types' +import * as builtins from './eval-env/builtins' -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false, +type EffDefn = { + default: (ctx: Ctx, args: Type[]) => Record<string, () => Promise<string[]>>, + cleanup: () => void, +} +const mergeEffDefns = (a: EffDefn, b: EffDefn): EffDefn => ({ + default: (ctx: Ctx, args: Type[]) => ({ + ...a.default(ctx, args), + ...b.default(ctx, args), + }), + cleanup: () => { + a.cleanup() + b.cleanup() + }, }) -export const cleanup = () => { - rl.close() -} +const cleanupActions = new Set<() => void>() +export const cleanup = () => cleanupActions.forEach(f => f()) -const readLineFromStdin = (): Promise<string> => - new Promise((res) => rl.on('line', res)) +let prevEnv: string export const evaluateType = async ( ctx: Ctx, @@ -28,191 +34,36 @@ export const evaluateType = async ( // console.log(ctx.typeToString(effTyp)) // console.log(name, args.map(ctx.typeToString)) - return match(name, { - DefineEffect: async () => { - const [nameTyp, exprTyp] = args - const name = nameTyp?.getLiteralValue() as string - const exprStr = exprTyp?.getLiteralValue() as string - - ctx.addCustomEffect(name, exprStr) - return [] - }, - - CreateRef: async () => { - const val = ctx.typeToString(args[0]) - const refKey = ctx.createRef(val) - const [resultKey, _] = ctx.createResult(JSON.stringify(refKey)) - return [resultKey] - }, - - GetRef: async () => { - const refKey = ctx.getTypeValue(args[0]) - const val = ctx.getRef(refKey) - if (!val) throw new Error('Ref has been deleted') - const [resultKey, _] = ctx.createResult(val) - return [resultKey] - }, - - SetRef: async () => { - const [ keyTy, valTyp ] = args - ctx.setRef(ctx.getTypeValue(keyTy), ctx.typeToString(valTyp)) - return [] - }, - - DeleteRef: async () => { - ctx.deleteRef(ctx.getTypeValue(args[0])) - return [] - }, - - Pure: async () => { - const [valTyp] = args - const [resultKey, _] = ctx.createResult(ctx.typeToString(valTyp)) - return [resultKey] - }, - - Print: async () => { - console.log(...args.map(ctx.typeToString)) - return [] - }, - - PutString: async () => { - const [strinTyp] = args - const typString = ctx.getTypeValue(strinTyp) ?? ctx.typeToString(strinTyp) - process.stdout.write(typString) - return [] - }, - - Debug: async () => { - const [labelTyp, valueTyp] = args - const label = ctx.getTypeValue(labelTyp) - const value = ctx.typeToString(valueTyp) - console.log(label, value) - const [resultKey, _] = ctx.createResult(JSON.stringify(value)) - return [resultKey] - }, - - ReadFile: async () => { - const [pathTyp] = args - const filePath = ctx.getTypeValue(pathTyp) - const contents = await fs.readFile(filePath, 'utf-8') - const [resultKey, _] = ctx.createResult(JSON.stringify(contents)) - return [resultKey] - }, - - WriteFile: async () => { - const [pathTyp, contentsTyp] = args - const filePath = ctx.getTypeValue(pathTyp) - const contents = ctx.getTypeValue(contentsTyp) - await fs.writeFile(filePath, contents) - return [] - }, - - Bind: async () => { - const [inputTyp, chainToKind] = args - const [resultKey] = inputTyp ? await evaluateType(ctx, inputTyp) : [] - - // TODO: Handle resultKey undefined case - 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] = args - const envName = ctx.getTypeValue(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] - }, - - Exit: async () => { - process.exit(args[0] && ctx.getTypeValue(args[0])) - }, - - Try: async () => { - const [effTyp, catchK] = args - - try { - if (!effTyp) throw new Error('wow') - return await evaluateType(ctx, effTyp) - } catch(e) { - const error = JSON.stringify((e as any)?.message ?? e) - const catchResExpr = `(${ctx.typeToString(catchK)} & { input: ${error} })['return']` - const [resultKey, _] = ctx.createResult(catchResExpr) - return [resultKey] - } - }, - - Throw: async () => { - throw args[0] && ctx.getTypeValue(args[0]) - }, - - ReadLine: async () => { - const line = await readLineFromStdin() - const [resultKey, _] = ctx.createResult(`${JSON.stringify(line)}`) - return [resultKey] + const envDefns = await match(ctx.currentEnv, { + node: () => import('./eval-env/node') as Promise<EffDefn>, + 'test.node': async () => mergeEffDefns( + await import('./eval-env/test'), + await import('./eval-env/node'), + ), + _: async () => { + throw new Error(`Invalid env: ${ctx.currentEnv}`) }, + }) - JsExpr: async () => { - const [exprTyp] = args - const exprStr = ctx.getTypeValue(exprTyp) - const result = eval(`JSON.stringify(${exprStr})`) - const [resultKey, _] = ctx.createResult(`${result}`) - return [resultKey] - }, + const { default: envEffects, cleanup } = mergeEffDefns(builtins as unknown as EffDefn, envDefns) - Seq: async () => { - const [effectTyps] = args - const effectResults = await evalList( - ctx, - effectTyps?.getTupleElements() ?? [] - ) - const [resultKey, _] = ctx.createResult(`[ - ${effectResults.map(ctx.getResultExpr).join(', ')} - ]`) - return [resultKey] - }, + // Update cleanup if env has changed + if (prevEnv !== ctx.currentEnv) { + cleanupActions.add(cleanup) + prevEnv = ctx.currentEnv + } - Do: async () => { - const [effectTyps] = args - const effectResults = await evalList( - ctx, - effectTyps?.getTupleElements() ?? [] - ) - // TODO: Use last type's result instead of last result key - const lastResKey = effectResults[effectResults.length - 1] - const [resultKey, _] = ctx.createResult( - `(${ctx.getResultExpr(lastResKey)})['output']` - ) - return [resultKey] - }, + return match(name, { + // TODO: Allow overriding effects + ...envEffects(ctx, args), _: async () => { if (name && ctx.hasCustomEffect(name)) { return ctx.runCustomEffect(name, args) } - console.log(`${name} effect is not handled`) console.log(ctx.typeToString(effTyp)) - // TODO: Maybe throw? - return [] + throw new Error(`${name} effect is not handled`) }, }) } diff --git a/src/types.ts b/src/types.ts index 059c0c2..d9e874e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,9 @@ export interface Ctx { getResultExpr: (key?: string) => string printResultNode: () => void + currentEnv: string, + setEnv: (e: string) => void, + createRef: (ty: string) => string, getRef: (key: string) => any, setRef: (key: string, ty: string) => void, @@ -25,4 +28,6 @@ export interface Ctx { addCustomEffect: (name: string, expr: string) => void runCustomEffect: (name: string, args: Type[]) => Promise<string[]> hasCustomEffect: (name: string) => boolean + + evaluateType: (ctx: Ctx, effTyp: Type) => Promise<string[]> } diff --git a/stdlib/sys.ts b/stdlib/sys.ts index 68f713d..9fff7a9 100644 --- a/stdlib/sys.ts +++ b/stdlib/sys.ts @@ -11,3 +11,5 @@ export interface DefineEffect<_Name extends string, _Func extends string> export interface Exit<_ extends number | undefined = undefined> extends Effect {} +export interface SetEvalEnvironment<_Env extends 'test.node' | 'node'> extends Effect {} + diff --git a/stdlib/test.ts b/stdlib/test.ts new file mode 100644 index 0000000..49c6e0e --- /dev/null +++ b/stdlib/test.ts @@ -0,0 +1,10 @@ +import { Effect } from "./effect"; + +export interface Config { + compileTimeTestFailures: false +} + +type Assertion = Config['compileTimeTestFailures'] extends true ? true : boolean + +export interface Assert<_B extends Assertion> extends Effect { } +export interface Test<_m extends string, _effs extends Effect[]> extends Effect { } diff --git a/stdlib/util.ts b/stdlib/util.ts index cf19589..4851392 100644 --- a/stdlib/util.ts +++ b/stdlib/util.ts @@ -20,3 +20,9 @@ export type ADT<D extends Record<string, any>> = { } extends infer Rec extends Pat ? { t: Rec[keyof Rec] } & { [k in keyof Rec]: ADTConstructor<Rec[k]> } : never + +export type Equals<Left, Right> = + [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false + +export type Not<B extends boolean> = B extends true ? false : true + |
