diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index e9c17ca6..2638106a 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -430,7 +430,7 @@ Deno.test("Thread local _CTX - advanced cases", async () => { sf.threadLocal.setLocal("_GLOBAL", env); assertEquals( await evalExpr( - "interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})", + "space_lua.interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})", env, sf, ), @@ -440,7 +440,7 @@ Deno.test("Thread local _CTX - advanced cases", async () => { // Some more complex string interpolation with more complex lua expressions, with nested {} assertEquals( await evalExpr( - `interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`, + `space_lua.interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`, env, sf, ), diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 970bee18..d56ca14a 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -304,7 +304,7 @@ function evalPrefixExpression( } } case "FunctionCall": { - let prefixValue = evalPrefixExpression(e.prefix, env, sf); + const prefixValue = evalPrefixExpression(e.prefix, env, sf); if (!prefixValue) { throw new LuaRuntimeError( `Attempting to call nil as a function`, diff --git a/common/space_lua/language.test.ts b/common/space_lua/language.test.ts index 56361583..037b4174 100644 --- a/common/space_lua/language.test.ts +++ b/common/space_lua/language.test.ts @@ -13,6 +13,7 @@ Deno.test("Lua language tests", async () => { const chunk = parse(luaFile, {}); const env = new LuaEnv(luaBuildStandardEnv()); const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx); + sf.threadLocal.setLocal("_GLOBAL", env); try { await evalStatement(chunk, env, sf); diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_test.lua index 9278a95d..a99fa1c2 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_test.lua @@ -690,3 +690,13 @@ end assert(#points == 6, "Grid should generate 6 points") assert(points[1][1] == 1 and points[1][2] == 1, "First point should be (1,1)") assert(points[6][1] == 2 and points[6][2] == 3, "Last point should be (2,3)") + +-- Test space_lua stuff +local parsedExpr = space_lua.parse_expression("1 + 1") +local evalResult = space_lua.eval_expression(parsedExpr) +assert(evalResult == 2, "Eval should return 2") + +-- Slightly more advanced example with augmented environment +local parsedExpr = space_lua.parse_expression("tostring(a + 1)") +local evalResult = space_lua.eval_expression(parsedExpr, { a = 1 }) +assert(evalResult == "2", "Eval should return 2 as a string") diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts index 02ae5e85..0216e73a 100644 --- a/common/space_lua/parse.ts +++ b/common/space_lua/parse.ts @@ -13,6 +13,7 @@ import type { LuaExpression, LuaFunctionBody, LuaFunctionCallExpression, + LuaFunctionCallStatement, LuaFunctionName, LuaLValue, LuaPrefixExpression, @@ -640,3 +641,13 @@ export function parse(s: string, ctx: ASTCtx = {}): LuaBlock { export function parseToCrudeAST(t: string): ParseTree { return cleanTree(lezerToParseTree(t, parser.parse(t).topNode), true); } + +/** + * Helper function to parse a Lua expression string + */ +export function parseExpressionString( + expr: string, +): LuaExpression { + const parsedLua = parse(`_(${expr})`) as LuaBlock; + return (parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0]; +} diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 33f174af..80d49e5d 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -557,6 +557,16 @@ export function luaCall( return fn.call((sf || LuaStackFrame.lostFrame).withCtx(ctx), ...args); } +export function luaKeys(val: any): any[] { + if (val instanceof LuaTable) { + return val.keys(); + } else if (Array.isArray(val)) { + return val.map((_, i) => i + 1); + } else { + return Object.keys(val); + } +} + export function luaTypeOf(val: any): LuaType { if (val === null || val === undefined) { return "nil"; diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts index dac59a4d..4262c8ba 100644 --- a/common/space_lua/stdlib.ts +++ b/common/space_lua/stdlib.ts @@ -6,22 +6,16 @@ import { luaGet, LuaMultiRes, LuaRuntimeError, - LuaTable, + type LuaTable, luaToString, luaTypeOf, type LuaValue, - luaValueToJS, } from "$common/space_lua/runtime.ts"; import { stringApi } from "$common/space_lua/stdlib/string.ts"; import { tableApi } from "$common/space_lua/stdlib/table.ts"; import { osApi } from "$common/space_lua/stdlib/os.ts"; import { jsApi } from "$common/space_lua/stdlib/js.ts"; -import { parse } from "$common/space_lua/parse.ts"; -import type { - LuaBlock, - LuaFunctionCallStatement, -} from "$common/space_lua/ast.ts"; -import { evalExpression } from "$common/space_lua/eval.ts"; +import { spaceLuaApi } from "$common/space_lua/stdlib/space_lua.ts"; const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { console.log("[Lua]", ...(await Promise.all(args.map(luaToString)))); @@ -129,82 +123,6 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => { return table.metatable; }); -/** - * This is not standard Lua, but it's a useful feature for us - */ -const interpolateFunction = new LuaBuiltinFunction( - async (sf, template: string, expandedEnv?: LuaTable) => { - let result = ""; - let currentIndex = 0; - - while (true) { - const startIndex = template.indexOf("${", currentIndex); - if (startIndex === -1) { - result += template.slice(currentIndex); - break; - } - - result += template.slice(currentIndex, startIndex); - - // Find matching closing brace by counting nesting - let nestLevel = 1; - let endIndex = startIndex + 2; - while (nestLevel > 0 && endIndex < template.length) { - if (template[endIndex] === "{") { - nestLevel++; - } else if (template[endIndex] === "}") { - nestLevel--; - } - if (nestLevel > 0) { - endIndex++; - } - } - - if (nestLevel > 0) { - throw new LuaRuntimeError("Unclosed interpolation expression", sf); - } - - const expr = template.slice(startIndex + 2, endIndex); - try { - const parsedLua = parse(`_(${expr})`) as LuaBlock; - const parsedExpr = - (parsedLua.statements[0] as LuaFunctionCallStatement).call - .args[0]; - - const globalEnv = sf.threadLocal.get("_GLOBAL"); - if (!globalEnv) { - throw new Error("_GLOBAL not defined"); - } - // Create a new env with the global env as the parent, augmented with the expandedEnv - const env = new LuaEnv(globalEnv); - if (expandedEnv) { - // Iterate over the keys in the expandedEnv and set them in the new env - for (const key of expandedEnv.keys()) { - env.setLocal(key, expandedEnv.rawGet(key)); - } - } - const luaResult = luaValueToJS( - await evalExpression( - parsedExpr, - env, - sf, - ), - ); - result += luaToString(luaResult); - } catch (e: any) { - throw new LuaRuntimeError( - `Error evaluating "${expr}": ${e.message}`, - sf, - ); - } - - currentIndex = endIndex + 1; - } - - return result; - }, -); - export function luaBuildStandardEnv() { const env = new LuaEnv(); // Top-level builtins @@ -225,13 +143,12 @@ export function luaBuildStandardEnv() { env.set("error", errorFunction); env.set("pcall", pcallFunction); env.set("xpcall", xpcallFunction); - // String interpolation - env.set("interpolate", interpolateFunction); // APIs env.set("string", stringApi); env.set("table", tableApi); env.set("os", osApi); env.set("js", jsApi); + env.set("space_lua", spaceLuaApi); return env; } diff --git a/common/space_lua/stdlib/space_lua.ts b/common/space_lua/stdlib/space_lua.ts new file mode 100644 index 00000000..d9ec5a68 --- /dev/null +++ b/common/space_lua/stdlib/space_lua.ts @@ -0,0 +1,126 @@ +import { parseExpressionString } from "$common/space_lua/parse.ts"; +import type { LuaExpression } from "$common/space_lua/ast.ts"; +import { evalExpression } from "$common/space_lua/eval.ts"; +import { + LuaBuiltinFunction, + LuaEnv, + LuaRuntimeError, + type LuaStackFrame, + LuaTable, + luaToString, + luaValueToJS, +} from "$common/space_lua/runtime.ts"; + +/** + * These are Space Lua specific functions that are available to all scripts, but are not part of the standard Lua language. + */ + +/** + * Helper function to create an augmented environment + */ +function createAugmentedEnv( + sf: LuaStackFrame, + envAugmentation?: LuaTable, +): LuaEnv { + const globalEnv = sf.threadLocal.get("_GLOBAL"); + if (!globalEnv) { + throw new Error("_GLOBAL not defined"); + } + const env = new LuaEnv(globalEnv); + if (envAugmentation) { + for (const key of envAugmentation.keys()) { + env.setLocal(key, envAugmentation.rawGet(key)); + } + } + return env; +} + +export const spaceLuaApi = new LuaTable({ + /** + * Parses a lua expression and returns the parsed expression. + * + * @param sf - The current space_lua state. + * @param luaExpression - The lua expression to parse. + * @returns The parsed expression. + */ + parse_expression: new LuaBuiltinFunction( + (_sf, luaExpression: string) => { + return parseExpressionString(luaExpression); + }, + ), + /** + * Evaluates a parsed lua expression and returns the result. + * + * @param sf - The current space_lua state. + * @param parsedExpr - The parsed lua expression to evaluate. + * @param envAugmentation - An optional environment to augment the global environment with. + * @returns The result of the evaluated expression. + */ + eval_expression: new LuaBuiltinFunction( + async (sf, parsedExpr: LuaExpression, envAugmentation?: LuaTable) => { + const env = createAugmentedEnv(sf, envAugmentation); + return luaValueToJS(await evalExpression(parsedExpr, env, sf)); + }, + ), + /** + * Interpolates a string with lua expressions and returns the result. + * + * @param sf - The current space_lua state. + * @param template - The template string to interpolate. + * @param envAugmentation - An optional environment to augment the global environment with. + * @returns The interpolated string. + */ + interpolate: new LuaBuiltinFunction( + async (sf, template: string, envAugmentation?: LuaTable) => { + let result = ""; + let currentIndex = 0; + + while (true) { + const startIndex = template.indexOf("${", currentIndex); + if (startIndex === -1) { + result += template.slice(currentIndex); + break; + } + + result += template.slice(currentIndex, startIndex); + + // Find matching closing brace by counting nesting + let nestLevel = 1; + let endIndex = startIndex + 2; + while (nestLevel > 0 && endIndex < template.length) { + if (template[endIndex] === "{") { + nestLevel++; + } else if (template[endIndex] === "}") { + nestLevel--; + } + if (nestLevel > 0) { + endIndex++; + } + } + + if (nestLevel > 0) { + throw new LuaRuntimeError("Unclosed interpolation expression", sf); + } + + const expr = template.slice(startIndex + 2, endIndex); + try { + const parsedExpr = parseExpressionString(expr); + const env = createAugmentedEnv(sf, envAugmentation); + const luaResult = luaValueToJS( + await evalExpression(parsedExpr, env, sf), + ); + result += luaToString(luaResult); + } catch (e: any) { + throw new LuaRuntimeError( + `Error evaluating "${expr}": ${e.message}`, + sf, + ); + } + + currentIndex = endIndex + 1; + } + + return result; + }, + ), +}); diff --git a/common/space_lua/stdlib/string.ts b/common/space_lua/stdlib/string.ts index bfe9b953..d811eae0 100644 --- a/common/space_lua/stdlib/string.ts +++ b/common/space_lua/stdlib/string.ts @@ -1,7 +1,6 @@ import { LuaBuiltinFunction, luaCall, - LuaFunction, LuaMultiRes, LuaTable, luaToString,