diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts new file mode 100644 index 00000000..2bc7380f --- /dev/null +++ b/common/space_lua/eval.test.ts @@ -0,0 +1,26 @@ +import { assertEquals } from "@std/assert/equals"; +import { + evalExpression, + LuaEnv, + LuaNativeJSFunction, + singleResult, +} from "./eval.ts"; +import { type LuaFunctionCallStatement, parse } from "./parse.ts"; + +function evalExpr(s: string, e = new LuaEnv()): any { + return evalExpression( + (parse(`e(${s})`).statements[0] as LuaFunctionCallStatement).call + .args[0], + e, + ); +} + +Deno.test("Evaluator test", async () => { + const env = new LuaEnv(); + env.set("test", new LuaNativeJSFunction(() => 3)); + env.set("asyncTest", new LuaNativeJSFunction(() => Promise.resolve(3))); + assertEquals(evalExpr(`1 + 2`), 3); + + assertEquals(singleResult(evalExpr(`test()`, env)), 3); + assertEquals(singleResult(await evalExpr(`asyncTest() + 1`, env)), 4); +}); diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts new file mode 100644 index 00000000..b4efd9bf --- /dev/null +++ b/common/space_lua/eval.ts @@ -0,0 +1,240 @@ +import type { + LuaExpression, + LuaFunctionBody, +} from "$common/space_lua/parse.ts"; +import { evalPromiseValues } from "$common/space_lua/util.ts"; + +export class LuaEnv { + variables = new Map(); + constructor(readonly parent?: LuaEnv) { + } + + set(name: string, value: any) { + this.variables.set(name, value); + } + + get(name: string): any { + if (this.variables.has(name)) { + return this.variables.get(name); + } + if (this.parent) { + return this.parent.get(name); + } + return undefined; + } +} + +export class LuaMultiRes { + constructor(readonly values: any[]) { + } + + unwrap(): any { + if (this.values.length !== 1) { + throw new Error("Cannot unwrap multiple values"); + } + return this.values[0]; + } +} + +export function singleResult(value: any): any { + if (value instanceof LuaMultiRes) { + return value.unwrap(); + } else { + return value; + } +} + +interface ILuaFunction { + call(...args: any[]): Promise | LuaMultiRes; +} + +export class LuaFunction implements ILuaFunction { + constructor(readonly body: LuaFunctionBody) { + } + + call(...args: any[]): Promise | LuaMultiRes { + throw new Error("Not yet implemented funciton call"); + } +} + +export class LuaNativeJSFunction implements ILuaFunction { + constructor(readonly fn: (...args: any[]) => any) { + } + + call(...args: any[]): Promise | LuaMultiRes { + const result = this.fn(...args); + if (result instanceof Promise) { + return result.then((result) => new LuaMultiRes([result])); + } else { + return new LuaMultiRes([result]); + } + } +} + +export class LuaTable { + constructor(readonly entries: Map = new Map()) { + } + + get(key: any): any { + return this.entries.get(key); + } + + set(key: any, value: any) { + this.entries.set(key, value); + } +} + +function luaSet(obj: any, key: any, value: any) { + if (obj instanceof LuaTable) { + obj.set(key, value); + } else { + obj[key] = value; + } +} + +function luaGet(obj: any, key: any): any { + if (obj instanceof LuaTable) { + return obj.get(key); + } else { + return obj[key]; + } +} + +export function evalExpression( + e: LuaExpression, + env: LuaEnv, +): Promise | any { + switch (e.type) { + case "String": + // TODO: Deal with escape sequences + return e.value; + case "Number": + return e.value; + case "Boolean": + return e.value; + case "Nil": + return null; + case "Binary": { + const values = evalPromiseValues([ + evalExpression(e.left, env), + evalExpression(e.right, env), + ]); + if (values instanceof Promise) { + return values.then(([left, right]) => + luaOp(e.operator, singleResult(left), singleResult(right)) + ); + } else { + return luaOp( + e.operator, + singleResult(values[0]), + singleResult(values[1]), + ); + } + } + case "TableAccess": { + const values = evalPromiseValues([ + evalPrefixExpression(e.object, env), + evalExpression(e.key, env), + ]); + if (values instanceof Promise) { + return values.then(([table, key]) => + luaGet(singleResult(table), singleResult(key)) + ); + } else { + return luaGet(singleResult(values[0]), singleResult(values[1])); + } + } + case "Variable": + case "FunctionCall": + return evalPrefixExpression(e, env); + default: + throw new Error(`Unknown expression type ${e.type}`); + } +} + +function evalPrefixExpression( + e: LuaExpression, + env: LuaEnv, +): Promise | any { + switch (e.type) { + case "Variable": { + const value = env.get(e.name); + if (value === undefined) { + throw new Error(`Undefined variable ${e.name}`); + } else { + return value; + } + } + case "Parenthesized": + return evalExpression(e.expression, env); + case "FunctionCall": { + const fn = evalPrefixExpression(e.prefix, env); + if (fn instanceof Promise) { + return fn.then((fn: ILuaFunction) => { + if (!fn.call) { + throw new Error(`Not a function: ${fn}`); + } + const args = evalPromiseValues( + e.args.map((arg) => evalExpression(arg, env)), + ); + if (args instanceof Promise) { + return args.then((args) => fn.call(...args)); + } else { + return fn.call(...args); + } + }); + } else { + if (!fn.call) { + throw new Error(`Not a function: ${fn}`); + } + const args = evalPromiseValues( + e.args.map((arg) => evalExpression(arg, env)), + ); + if (args instanceof Promise) { + return args.then((args) => fn.call(...args)); + } else { + return fn.call(...args); + } + } + } + default: + throw new Error(`Unknown prefix expression type ${e.type}`); + } +} + +function luaOp(op: string, left: any, right: any): any { + switch (op) { + case "+": + return left + right; + case "-": + return left - right; + case "*": + return left * right; + case "/": + return left / right; + case "%": + return left % right; + case "^": + return left ** right; + case "..": + return left + right; + case "==": + return left === right; + case "~=": + return left !== right; + case "<": + return left < right; + case "<=": + return left <= right; + case ">": + return left > right; + case ">=": + return left >= right; + case "and": + return left && right; + case "or": + return left || right; + default: + throw new Error(`Unknown operator ${op}`); + } +} diff --git a/common/space_lua/util.test.ts b/common/space_lua/util.test.ts new file mode 100644 index 00000000..8854bdc6 --- /dev/null +++ b/common/space_lua/util.test.ts @@ -0,0 +1,21 @@ +import { evalPromiseValues } from "$common/space_lua/util.ts"; +import { assertEquals } from "@std/assert/equals"; +import { assert } from "@std/assert"; + +Deno.test("Test promise helpers", async () => { + const r = evalPromiseValues([1, 2, 3]); + // should return the same array not as a promise + assertEquals(r, [1, 2, 3]); + const asyncR = evalPromiseValues([ + new Promise((resolve) => { + setTimeout(() => { + resolve(1); + }, 5); + }), + Promise.resolve(2), + 3, + ]); + // should return a promise + assert(asyncR instanceof Promise); + assertEquals(await asyncR, [1, 2, 3]); +}); diff --git a/common/space_lua/util.ts b/common/space_lua/util.ts new file mode 100644 index 00000000..dfd628ef --- /dev/null +++ b/common/space_lua/util.ts @@ -0,0 +1,16 @@ +export function evalPromiseValues(vals: any[]): Promise | any[] { + const promises = []; + const promiseResults = new Array(vals.length); + for (let i = 0; i < vals.length; i++) { + if (vals[i] instanceof Promise) { + promises.push(vals[i].then((v: any) => promiseResults[i] = v)); + } else { + promiseResults[i] = vals[i]; + } + } + if (promises.length === 0) { + return promiseResults; + } else { + return Promise.all(promises).then(() => promiseResults); + } +}