import { assertEquals } from "@std/assert/equals"; import { LuaEnv, LuaNativeJSFunction, LuaStackFrame, LuaTable, luaValueToJS, singleResult, } from "./runtime.ts"; import { parse } from "./parse.ts"; import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts"; import { evalExpression, evalStatement } from "./eval.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; function evalExpr(s: string, e = new LuaEnv(), sf?: LuaStackFrame): any { const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement; sf = sf || new LuaStackFrame(e, node.ctx); return evalExpression( node.call.args[0], e, sf, ); } function evalBlock(s: string, e = new LuaEnv()): Promise { const node = parse(s) as LuaBlock; const sf = new LuaStackFrame(e, node.ctx); return evalStatement(node, e, sf); } Deno.test("Evaluator test", async () => { const env = new LuaEnv(); env.set("test", new LuaNativeJSFunction((n) => n)); env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n))); // Basic arithmetic assertEquals(evalExpr(`1 + 2 + 3 - 3`), 3); assertEquals(evalExpr(`4 // 3`), 1); assertEquals(evalExpr(`4 % 3`), 1); // Strings assertEquals(evalExpr(`"a" .. "b"`), "ab"); // Logic assertEquals(evalExpr(`true and false`), false); assertEquals(evalExpr(`true or false`), true); assertEquals(evalExpr(`not true`), false); // Tables const tbl = evalExpr(`{3, 1, 2}`); assertEquals(tbl.get(1), 3); assertEquals(tbl.get(2), 1); assertEquals(tbl.get(3), 2); assertEquals(luaValueToJS(tbl), [3, 1, 2]); assertEquals(luaValueToJS(evalExpr(`{name=test("Zef"), age=100}`, env)), { name: "Zef", age: 100, }); assertEquals( luaValueToJS(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)), { name: "Zef", age: 100, }, ); const result = evalExpr(`{[3+2]=1, ["a".."b"]=2}`); assertEquals(result.get(5), 1); assertEquals(result.get("ab"), 2); assertEquals(evalExpr(`#{}`), 0); assertEquals(evalExpr(`#{1, 2, 3}`), 3); // Unary operators assertEquals(await evalExpr(`-asyncTest(3)`, env), -3); // Function calls assertEquals(singleResult(evalExpr(`test(3)`, env)), 3); assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4); // Function expressions and table access assertEquals( await evalExpr(`(function() return {name="John"} end)().name`), "John", ); // Function definitions const fn = evalExpr(`function(a, b) return a + b end`); assertEquals(fn.body.parameters, ["a", "b"]); }); Deno.test("Statement evaluation", async () => { const env = new LuaEnv(); env.set("test", new LuaNativeJSFunction((n) => n)); env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n))); assertEquals(undefined, await evalBlock(`a = 3`, env)); assertEquals(env.get("a"), 3); assertEquals(undefined, await evalBlock(`b = test(3)`, env)); assertEquals(env.get("b"), 3); await evalBlock(`c = asyncTest(3)`, env); assertEquals(env.get("c"), 3); // Multiple assignments const env2 = new LuaEnv(); assertEquals(undefined, await evalBlock(`a, b = 1, 2`, env2)); assertEquals(env2.get("a"), 1); assertEquals(env2.get("b"), 2); // Other lvalues const env3 = new LuaEnv(); await evalBlock(`tbl = {1, 2, 3}`, env3); await evalBlock(`tbl[1] = 3`, env3); assertEquals(luaValueToJS(env3.get("tbl")), [3, 2, 3]); await evalBlock("tbl.name = 'Zef'", env3); assertEquals(env3.get("tbl").get("name"), "Zef"); await evalBlock(`tbl[2] = {age=10}`, env3); await evalBlock(`tbl[2].age = 20`, env3); assertEquals(env3.get("tbl").get(2).get("age"), 20); // Blocks and scopes const env4 = new LuaEnv(); env4.set("print", new LuaNativeJSFunction(console.log)); await evalBlock( ` a = 1 do -- sets global a to 3 a = 3 print("The number is: " .. a) end`, env4, ); assertEquals(env4.get("a"), 3); const env5 = new LuaEnv(); env5.set("print", new LuaNativeJSFunction(console.log)); await evalBlock( ` a = 1 if a > 0 then a = 3 else a = 0 end`, env5, ); assertEquals(env5.get("a"), 3); await evalBlock( ` if a < 0 then a = -1 elseif a > 0 then a = 1 else a = 0 end`, env5, ); assertEquals(env5.get("a"), 1); await evalBlock( ` var = 1 do local var var = 2 end`, env5, ); assertEquals(env5.get("var"), 1); // While loop const env6 = new LuaEnv(); await evalBlock( ` c = 0 while true do c = c + 1 if c == 3 then break end end `, env6, ); assertEquals(env6.get("c"), 3); // Repeat loop const env7 = new LuaEnv(); await evalBlock( ` c = 0 repeat c = c + 1 if c == 3 then break end until false `, env7, ); assertEquals(env7.get("c"), 3); // Function definition and calling const env8 = new LuaEnv(); env8.set("print", new LuaNativeJSFunction(console.log)); await evalBlock( ` function test(a) return a + 1 end print("3 + 1 = " .. test(3)) `, env8, ); // Local fucntion definition const env9 = new LuaEnv(); env9.set("print", new LuaNativeJSFunction(console.log)); await evalBlock( ` local function test(a) return a + 1 end print("3 + 1 = " .. test(3)) `, env9, ); // For loop over range const env10 = new LuaEnv(); await evalBlock( ` c = 0 for i = 1, 3 do c = c + i end `, env10, ); assertEquals(env10.get("c"), 6); // For loop over iterator const env11 = new LuaEnv(luaBuildStandardEnv()); await evalBlock( ` function fruits() local list = { "apple", "banana", "cherry" } -- Track index internally local index = 0 return function() index = index + 1 if list[index] then return list[index] end end end for fruit in fruits() do print("Fruit: " .. fruit) end `, env11, ); await evalBlock( ` for _, f in ipairs({ "apple", "banana", "cherry" }) do print("Fruit: " .. f) end`, luaBuildStandardEnv(), ); }); Deno.test("Thread local _CTX", async () => { const env = new LuaEnv(); const threadLocal = new LuaEnv(); threadLocal.setLocal("threadValue", "test123"); const sf = new LuaStackFrame(threadLocal, null); await evalBlock( ` function test() return _CTX.threadValue end `, env, ); const result = await evalExpr("test()", env, sf); assertEquals(singleResult(result), "test123"); }); Deno.test("Thread local _CTX - advanced cases", async () => { // Create environment with standard library const env = new LuaEnv(luaBuildStandardEnv()); const threadLocal = new LuaEnv(); env.setLocal("globalEnv", "GLOBAL"); // Set up some thread local values threadLocal.setLocal("user", "alice"); threadLocal.setLocal("permissions", new LuaTable()); threadLocal.get("permissions").set("admin", true); threadLocal.setLocal("data", { id: 123, settings: { theme: "dark" }, }); const sf = new LuaStackFrame(threadLocal, null); // Test 1: Nested function access await evalBlock( ` function outer() local function inner() return _CTX.user end return inner() end `, env, ); assertEquals(await evalExpr("outer()", env, sf), "alice"); // Test 2: Table access and modification await evalBlock( ` function checkAdmin() return _CTX.permissions.admin end function revokeAdmin() _CTX.permissions.admin = false return _CTX.permissions.admin end `, env, ); assertEquals(await evalExpr("checkAdmin()", env, sf), true); assertEquals(await evalExpr("revokeAdmin()", env, sf), false); assertEquals(threadLocal.get("permissions").get("admin"), false); // Test 3: Complex data structures await evalBlock( ` function getNestedData() return _CTX.data.settings.theme end function updateTheme(newTheme) _CTX.data.settings.theme = newTheme return _CTX.data.settings.theme end `, env, ); assertEquals(await evalExpr("getNestedData()", env, sf), "dark"); assertEquals(await evalExpr("updateTheme('light')", env, sf), "light"); // Test 4: Multiple thread locals const threadLocal2 = new LuaEnv(); threadLocal2.setLocal("user", "bob"); const sf2 = new LuaStackFrame(threadLocal2, null); await evalBlock( ` function getUser() return _CTX.user end `, env, ); // Same function, different thread contexts assertEquals(await evalExpr("getUser()", env, sf), "alice"); assertEquals(await evalExpr("getUser()", env, sf2), "bob"); // Test 5: Async operations with _CTX env.set( "asyncOperation", new LuaNativeJSFunction(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); return "done"; }), ); await evalBlock( ` function asyncTest() _CTX.status = "starting" local result = asyncOperation() _CTX.status = "completed" return _CTX.status end `, env, ); assertEquals(await evalExpr("asyncTest()", env, sf), "completed"); assertEquals(threadLocal.get("status"), "completed"); // Test 6: Error handling with _CTX await evalBlock( ` function errorTest() _CTX.error = nil local status, err = pcall(function() error("test error") end) _CTX.error = "caught" return _CTX.error end `, env, ); assertEquals(await evalExpr("errorTest()", env, sf), "caught"); assertEquals(threadLocal.get("error"), "caught"); // Test string interpolation sf.threadLocal.setLocal("_GLOBAL", env); assertEquals( await evalExpr( "space_lua.interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})", env, sf, ), "Hello, GLOBAL and local!", ); // Some more complex string interpolation with more complex lua expressions, with nested {} assertEquals( await evalExpr( `space_lua.interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`, env, sf, ), `Some JSON {"name":"Pete"}!`, ); });