aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/test-runner.ts39
-rw-r--r--src/context.ts9
-rw-r--r--src/eval-env/builtins.ts142
-rw-r--r--src/eval-env/node.ts61
-rw-r--r--src/eval-env/test.ts31
-rw-r--r--src/eval.ts221
-rw-r--r--src/types.ts5
-rw-r--r--stdlib/sys.ts2
-rw-r--r--stdlib/test.ts10
-rw-r--r--stdlib/util.ts6
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
+