diff --git a/client/space_lua/lua.test.ts b/client/space_lua/lua.test.ts index 88639534..cf6a4706 100644 --- a/client/space_lua/lua.test.ts +++ b/client/space_lua/lua.test.ts @@ -17,6 +17,10 @@ Deno.test("[Lua] Core language (arithmetic)", async () => { await runLuaTest("./arithmetic_test.lua"); }); +Deno.test("[Lua] Load tests", async () => { + await runLuaTest("./stdlib/load_test.lua"); +}); + Deno.test("[Lua] Table tests", async () => { await runLuaTest("./stdlib/table_test.lua"); }); diff --git a/client/space_lua/stdlib.ts b/client/space_lua/stdlib.ts index 38ac84a4..31c04a8a 100644 --- a/client/space_lua/stdlib.ts +++ b/client/space_lua/stdlib.ts @@ -22,6 +22,7 @@ import { parse } from "./parse.ts"; import { evalStatement } from "./eval.ts"; import { encodingApi } from "./stdlib/encoding.ts"; import { luaToNumber } from "./tonumber.ts"; +import { luaLoad } from "./stdlib/load.ts"; const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { console.log( @@ -265,6 +266,8 @@ const someFunction = new LuaBuiltinFunction(async (_sf, value: any) => { return value; }); +const loadFunction = new LuaBuiltinFunction((sf, s) => luaLoad(s, sf)); + export function luaBuildStandardEnv() { const env = new LuaEnv(); // Top-level builtins @@ -288,6 +291,8 @@ export function luaBuildStandardEnv() { env.set("error", errorFunction); env.set("pcall", pcallFunction); env.set("xpcall", xpcallFunction); + // Evaluation + env.set("load", loadFunction); // APIs env.set("string", stringApi); env.set("table", tableApi); diff --git a/client/space_lua/stdlib/load.ts b/client/space_lua/stdlib/load.ts new file mode 100644 index 00000000..7f0d88f4 --- /dev/null +++ b/client/space_lua/stdlib/load.ts @@ -0,0 +1,43 @@ +import { + LuaBuiltinFunction, + LuaEnv, + LuaMultiRes, + type LuaStackFrame, + type LuaValue, +} from "../runtime.ts"; +import { parse } from "../parse.ts"; +import { evalStatement } from "../eval.ts"; + +// Returns a function (callable chunk) or (nil, "error message") pair. +export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue { + const s = typeof code === "string" ? code : String(code); + + try { + const block = parse(s, sf.astCtx || {}); + const globalEnvMaybe = sf.threadLocal.get("_GLOBAL"); + + // Be vocal when no _GLOBAL is set + if (!globalEnvMaybe) { + console.warn( + "load() called without _GLOBAL in thread-local environment", + ); + return new LuaMultiRes([null, "Global environment not set"]); + } + + const globalEnv: LuaEnv = globalEnvMaybe as LuaEnv; + + const runner = new LuaBuiltinFunction(async (innerSf: LuaStackFrame) => { + const res = await evalStatement(block, globalEnv, innerSf, true); + if (res === undefined) { + return null; + } else { + return new LuaMultiRes(res); + } + }); + + return runner; + } catch (e: any) { + const msg = e && typeof e.message === "string" ? e.message : String(e); + return new LuaMultiRes([null, msg]); + } +} diff --git a/client/space_lua/stdlib/load_test.lua b/client/space_lua/stdlib/load_test.lua new file mode 100644 index 00000000..85d8cf2e --- /dev/null +++ b/client/space_lua/stdlib/load_test.lua @@ -0,0 +1,74 @@ +local function assert_eq(actual, expected, msg) + if actual ~= expected then + error('assert_eq failed: ' .. msg) + end +end + +local function assert_true(cond, msg) + if not cond then + error('assert_true failed: ' .. msg) + end +end + +local function assert_false(cond, msg) + if cond then + error('assert_false failed: ' .. msg) + end +end + +-- 1. Successful compile -> returns function; call yields value + +do + local f, err = load("return 40 + 2") + + assert_eq(type(f), "function", "load must return function on success") + assert_eq(err, nil, "load must not return error on success") + assert_eq(f(), 42, "loaded chunk executes and returns") +end + +-- 2. No explicit return -> nil on call + +do + local f, err = load("local x = 1; x = x + 1") + + assert_eq(type(f), "function", "load returns function even without returns") + assert_eq(err, nil, "no error expected") + assert_eq(f(), nil, "chunk without return yields nil") +end + +-- 3. Syntax error -> (nil, "error message"), no throw on load + +do + local f, err = load("return 1 +") + + assert_eq(f, nil, "syntax error: first result is nil") + assert_eq(type(err), "string", "syntax error: second result is error string") +end + +-- 4. Runtime error when calling the returned function + +do + local f = load("return 1 + {}") -- arithmetic on non-number at runtime + local ok, res = pcall(f) + + assert_false(ok, "pcall must be false on runtime error") + assert_eq(type(res), "string", "pcall error message is string") +end + +-- 5. Global reads are visible in loaded chunk + +do + g = 41 + local f = load("return g + 1") + + assert_eq(f(), 42, "loaded chunk sees global env (read)") +end + +-- 6. Global writes from loaded chunk update globals +do + h = 0 + local f = load("h = 9") + + assert_eq(f(), nil, "no explicit return") + assert_eq(h, 9, "global updated by loaded chunk (write)") +end