diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_core_test.lua similarity index 50% rename from common/space_lua/language_test.lua rename to common/space_lua/language_core_test.lua index 40ca9608..14d3b3b6 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_core_test.lua @@ -219,180 +219,6 @@ assert(not deepCompare( { a = { 1, 2, 3 }, b = { 4, 5, 7 } } )) --- String serialization -assert(tostring({ 1, 2, 3 }) == "{1, 2, 3}") -assert(tostring({ a = 1, b = 2 }) == "{a = 1, b = 2}") - --- Error handling -local status, err = pcall(function() - error("This is an error") -end) - -assert(not status) -assert(err == "This is an error") - -local status, err = xpcall(function() - error("This is an error") -end, function(err) - return "Caught error: " .. err -end) - -assert(not status) -assert_equal(err, "Caught error: This is an error") - --- ipairs -local p = ipairs({ 3, 2, 1 }) -local idx, value = p() -assert(idx == 1 and value == 3) -idx, value = p() -assert(idx == 2 and value == 2) -idx, value = p() -assert(idx == 3 and value == 1) -idx, value = p() -assert(idx == nil and value == nil) - -for index, value in ipairs({ 1, 2, 3 }) do - assert(index == value) -end - --- pairs -local p = pairs({ a = 1, b = 2, c = 3 }) -local key, value = p() -assert(key == "a" and value == 1) -key, value = p() -assert(key == "b" and value == 2) -key, value = p() -assert(key == "c" and value == 3) -key, value = p() -assert(key == nil and value == nil) -for key, value in pairs({ a = "a", b = "b" }) do - assert_equal(key, value) -end - --- for in over tables directly -local cnt = 1 -for val in { 1, 2, 3 } do - assert_equal(val, cnt) - cnt = cnt + 1 -end -assert_equal(cnt, 4) - -local cnt = 1 -for val in js.tojs({ 1, 2, 3 }) do - assert_equal(val, cnt) - cnt = cnt + 1 -end -assert_equal(cnt, 4) - - --- type -assert(type(1) == "number") -assert(type("Hello") == "string") -assert(type({}) == "table") -assert(type(nil) == "nil") -assert(type(true) == "boolean") -assert_equal(type(function() end), "function") - --- string functions -assert(string.len("Hello") == 5) -assert(string.byte("Hello", 1) == 72) -assert(string.char(72) == "H") -assert(string.find("Hello", "l") == 3) -assert(string.rep("Hello", 3) == "HelloHelloHello") -assert(string.sub("Hello", 2, 4) == "ell") -assert(string.upper("Hello") == "HELLO") -assert(string.lower("Hello") == "hello") - --- Test string.gsub with various replacement types --- Simple string replacement -local result, count = string.gsub("hello world", "hello", "hi") -assert(result == "hi world", "Basic string replacement failed") -assert(count == 1, "Basic replacement count failed") - --- Multiple replacements -result, count = string.gsub("hello hello hello", "hello", "hi") -assert(result == "hi hi hi", "Multiple replacements failed") -assert(count == 3, "Multiple replacement count failed") - --- Limited replacements with n parameter -result, count = string.gsub("hello hello hello", "hello", "hi", 2) -assert(result == "hi hi hello", "Limited replacements failed") -assert(count == 2, "Limited replacement count failed") - --- Function replacement without captures -result = string.gsub("hello world", "hello", function(match) - assert(match == "hello", "Function received incorrect match") - return string.upper(match) -end) -assert(result == "HELLO world", "Function replacement without captures failed") - --- Function replacement with single capture -result = string.gsub("hello world", "(h)ello", function(h) - assert(h == "h", "Function received incorrect capture") - return string.upper(h) .. "i" -end) -assert(result == "Hi world", "Function replacement with single capture failed") - --- Function replacement with multiple captures -result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2) - assert(h == "h" and e == "e" and l1 == "l" and l2 == "l", - "Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2) - return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o" -end) -assert(result == "HEllo world", "Function replacement with multiple captures failed") - --- Function returning nil (should keep original match) -result = string.gsub("hello world", "hello", function() return nil end) -assert(result == "hello world", "Function returning nil failed") - --- Pattern with multiple matches on same position -result = string.gsub("hello world", "h?e", "X") -assert(result == "Xllo world", "Overlapping matches failed") - --- Empty captures -result = string.gsub("hello", "(h()e)", function(full, empty) - assert(full == "he" and empty == "", "Empty capture handling failed") - return "XX" -end) -assert(result == "XXllo", "Empty capture replacement failed") - --- Patterns with magic characters -result = string.gsub("hello.world", "%.", "-") -assert(result == "hello-world", "Magic character replacement failed") - --- table functions -local t = { 1, 2, 3 } -table.insert(t, 4) -assert_equal(t[4], 4) -table.remove(t, 1) -assert_equal(t[1], 2) -table.insert(t, 1, 1) -assert_equal(t[1], 1) -assert_equal(table.concat({ "Hello", "world" }, " "), "Hello world") - -local t = { 3, 1, 2 } -table.sort(t) -assert_equal(t[1], 1) -assert_equal(t[2], 2) -assert_equal(t[3], 3) -table.sort(t, function(a, b) - return a > b -end) -assert_equal(t[1], 3) -assert_equal(t[2], 2) -assert_equal(t[3], 1) - -local data = { { name = "John", age = 30 }, { name = "Jane", age = 25 } } -table.sort(data, function(a, b) - return a.age < b.age -end) -assert_equal(data[1].name, "Jane") -assert_equal(data[2].name, "John") - --- os functions -assert(os.time() > 0) -assert(os.date("%Y-%m-%d", os.time({ year = 2020, month = 1, day = 1 })) == "2020-01-01") - -- Closure tests local function make_counter() local count = 0 @@ -485,119 +311,6 @@ local final = counter.reset() assert(final == 2, "Reset should return last value") assert(counter.increment() == 0, "Counter should start fresh after reset") - --- Advanced closure tests with multiple functions sharing state --- Counter that can count by custom steps -local function make_counter_with_step() - local count = 0 - return { - increment = function(step) - count = count + (step or 1) - return count - end, - decrement = function(step) - count = count - (step or 1) - return count - end, - get = function() - return count - end - } -end - -local counter = make_counter_with_step() -assert(counter.increment(5) == 5, "Counter should increment by 5") -assert(counter.decrement(2) == 3, "Counter should decrement by 2") -assert(counter.get() == 3, "Counter should maintain state") -assert(counter.increment() == 4, "Counter should default to 1") - --- Test multiple independent counters -local c1 = make_counter_with_step() -local c2 = make_counter_with_step() -c1.increment(10) -c2.increment(5) -assert(c1.get() == 10, "First counter should be independent") -assert(c2.get() == 5, "Second counter should be independent") - --- Test varargs handling --- Basic varargs sum function -local function sum(...) - local total = 0 - for i, v in ipairs({ ... }) do - total = total + v - end - return total -end - -assert(sum() == 0, "Sum should handle no arguments") -assert(sum(42) == 42, "Sum should handle single argument") -assert(sum(1, 2) == 3, "Sum should handle two arguments") - --- Test varargs propagation -local function pass_varargs(...) - return sum(...) -end - -assert(pass_varargs() == 0, "Should propagate empty varargs") -assert(pass_varargs(1, 2, 3) == 6, "Should propagate varargs") - --- Test closure behavior --- Counter that can count by custom steps -local function make_counter_with_step() - local count = 0 - return { - increment = function(step) - count = count + (step or 1) - return count - end, - decrement = function(step) - count = count - (step or 1) - return count - end, - get = function() - return count - end - } -end - -local counter = make_counter_with_step() -assert(counter.increment(5) == 5, "Counter should increment by 5") -assert(counter.decrement(2) == 3, "Counter should decrement by 2") -assert(counter.get() == 3, "Counter should maintain state") -assert(counter.increment() == 4, "Counter should default to 1") - --- Test multiple independent counters -local c1 = make_counter_with_step() -local c2 = make_counter_with_step() -c1.increment(10) -c2.increment(5) -assert(c1.get() == 10, "First counter should be independent") -assert(c2.get() == 5, "Second counter should be independent") - --- Test closures with shared upvalues -local function make_shared_counter() - local count = 0 - local function inc() - count = count + 1 - return count - end - local function dec() - count = count - 1 - return count - end - local function get() - return count - end - return inc, dec, get -end - -local inc, dec, get = make_shared_counter() -assert(inc() == 1, "First increment") -assert(inc() == 2, "Second increment") -assert(dec() == 1, "First decrement") -assert(get() == 1, "Get should return current value") - - -- Test custom iterators -- Basic iterator that counts down from n to 1 local function countdown(n) @@ -705,51 +418,4 @@ 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") - --- Test query -local data = { { name = "John", lastModified = 1, age = 20 }, { name = "Jane", lastModified = 2, age = 21 } } -local r = query [[from p = data limit 1]] -assert_equal(#r, 1) -assert_equal(r[1].name, "John") -assert_equal(r[1].lastModified, 1) - -local r = query [[from p = data order by p.lastModified desc]] -assert_equal(#r, 2) -assert_equal(r[1].name, "Jane") -assert_equal(r[1].lastModified, 2) -assert_equal(r[2].name, "John") -assert_equal(r[2].lastModified, 1) - -local r = query [[from p = data order by p.lastModified]] -assert_equal(#r, 2) -assert_equal(r[1].name, "John") -assert_equal(r[1].lastModified, 1) -assert_equal(r[2].name, "Jane") -assert_equal(r[2].lastModified, 2) - -local r = query [[from p = data order by p.age select {name=p.name, age=p.age}]] -assert_equal(#r, 2) -assert_equal(r[1].name, "John") -assert_equal(r[1].age, 20) -assert_equal(r[2].name, "Jane") -assert_equal(r[2].age, 21) -assert_equal(r[1].lastModified, nil) -assert_equal(r[2].lastModified, nil) - --- Random select test -local r = query [[from {1, 2, 3} select _ + 1]] -assert_equal(#r, 3) -assert_equal(r[1], 2) -assert_equal(r[2], 3) -assert_equal(r[3], 4) +assert(points[6][1] == 2 and points[6][2] == 3, "Last point should be (2,3)") \ No newline at end of file diff --git a/common/space_lua/language.test.ts b/common/space_lua/lua.test.ts similarity index 62% rename from common/space_lua/language.test.ts rename to common/space_lua/lua.test.ts index c9e88d4f..6f23083a 100644 --- a/common/space_lua/language.test.ts +++ b/common/space_lua/lua.test.ts @@ -9,8 +9,28 @@ import { evalStatement } from "$common/space_lua/eval.ts"; import { assert } from "@std/assert/assert"; import { fileURLToPath } from "node:url"; -Deno.test("Lua language tests", async () => { - await runLuaTest("./language_test.lua"); +Deno.test("[Lua] Core language", async () => { + await runLuaTest("./language_core_test.lua"); +}); + +Deno.test("[Lua] Table tests", async () => { + await runLuaTest("./stdlib/table_test.lua"); +}); + +Deno.test("[Lua] String tests", async () => { + await runLuaTest("./stdlib/string_test.lua"); +}); + +Deno.test("[Lua] Space Lua tests", async () => { + await runLuaTest("./stdlib/space_lua_test.lua"); +}); + +Deno.test("[Lua] OS tests", async () => { + await runLuaTest("./stdlib/os_test.lua"); +}); + +Deno.test("[Lua] JS tests", async () => { + await runLuaTest("./stdlib/js_test.lua"); }); async function runLuaTest(luaPath: string) { diff --git a/common/space_lua/query_test.lua b/common/space_lua/query_test.lua new file mode 100644 index 00000000..7cc056fc --- /dev/null +++ b/common/space_lua/query_test.lua @@ -0,0 +1,47 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +-- Test query functionality +local data = { { name = "John", lastModified = 1, age = 20 }, { name = "Jane", lastModified = 2, age = 21 } } + +-- Basic limit query +local r = query [[from p = data limit 1]] +assert_equal(#r, 1) +assert_equal(r[1].name, "John") +assert_equal(r[1].lastModified, 1) + +-- Order by descending +local r = query [[from p = data order by p.lastModified desc]] +assert_equal(#r, 2) +assert_equal(r[1].name, "Jane") +assert_equal(r[1].lastModified, 2) +assert_equal(r[2].name, "John") +assert_equal(r[2].lastModified, 1) + +-- Order by ascending +local r = query [[from p = data order by p.lastModified]] +assert_equal(#r, 2) +assert_equal(r[1].name, "John") +assert_equal(r[1].lastModified, 1) +assert_equal(r[2].name, "Jane") +assert_equal(r[2].lastModified, 2) + +-- Select specific fields +local r = query [[from p = data order by p.age select {name=p.name, age=p.age}]] +assert_equal(#r, 2) +assert_equal(r[1].name, "John") +assert_equal(r[1].age, 20) +assert_equal(r[2].name, "Jane") +assert_equal(r[2].age, 21) +assert_equal(r[1].lastModified, nil) +assert_equal(r[2].lastModified, nil) + +-- Array transformation +local r = query [[from {1, 2, 3} select _ + 1]] +assert_equal(#r, 3) +assert_equal(r[1], 2) +assert_equal(r[2], 3) +assert_equal(r[3], 4) \ No newline at end of file diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index e61b8a6a..e54aef04 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -529,6 +529,18 @@ export function luaGet( } } +function isClass(obj: any) { + const isCtorClass = obj.constructor && + obj.constructor.toString().substring(0, 5) === "class"; + if (obj.prototype === undefined) { + return isCtorClass; + } + const isPrototypeCtorClass = obj.prototype.constructor && + obj.prototype.constructor.toString && + obj.prototype.constructor.toString().substring(0, 5) === "class"; + return isCtorClass || isPrototypeCtorClass; +} + export function luaLen(obj: any): number { if (obj instanceof LuaTable) { return obj.length; diff --git a/common/space_lua/stdlib/js.ts b/common/space_lua/stdlib/js.ts index b40173c3..23ee8838 100644 --- a/common/space_lua/stdlib/js.ts +++ b/common/space_lua/stdlib/js.ts @@ -6,24 +6,56 @@ import { } from "$common/space_lua/runtime.ts"; export const jsApi = new LuaTable({ + /** + * Creates a new instance of a JavaScript class. + * @param constructorFn - The constructor function. + * @param args - The arguments to pass to the constructor. + * @returns The new instance. + */ new: new LuaBuiltinFunction( - (constructorFn: any, ...args) => { + (_sf, constructorFn: any, ...args) => { + console.log("New", constructorFn, args); return new constructorFn( ...args.map(luaValueToJS), ); }, ), - importModule: new LuaBuiltinFunction((_sf, url) => { - return import(url); + /** + * Imports a JavaScript module. + * @param url - The URL of the module to import. + * @returns The imported module. + */ + import: new LuaBuiltinFunction(async (_sf, url) => { + let m = await import(url); + // Unwrap default if it exists + if (Object.keys(m).length === 1 && m.default) { + m = m.default; + } + return m; }), - + /** + * Converts a JavaScript value to a Lua value. + * @param val - The JavaScript value to convert. + * @returns The Lua value. + */ tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val)), + /** + * Converts a Lua value to a JavaScript value. + * @param val - The Lua value to convert. + * @returns The JavaScript value. + */ tojs: new LuaBuiltinFunction((_sf, val) => luaValueToJS(val)), + /** + * Logs a message to the console. + * @param args - The arguments to log. + */ log: new LuaBuiltinFunction((_sf, ...args) => { console.log(...args); }), + /** + * Converts a Lua value to a JSON string. + * @param val - The Lua value to convert. + * @returns The JSON string. + */ stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)), - // assignGlobal: new LuaBuiltinFunction((name: string, value: any) => { - // (globalThis as any)[name] = value; - // }), }); diff --git a/common/space_lua/stdlib/js_test.lua b/common/space_lua/stdlib/js_test.lua new file mode 100644 index 00000000..6265dce7 --- /dev/null +++ b/common/space_lua/stdlib/js_test.lua @@ -0,0 +1,14 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +local lodash = js.import("https://esm.sh/lodash@4.17.21") + +assert_equal(js.stringify(lodash.chunk({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 3)), '[[1,2,3],[4,5,6],[7,8,9],[10]]') + +local moment = js.import("https://esm.sh/moment@2.30.1") + +local day = moment("1995-12-25"); +assert_equal(day.format("DD-MM-YYYY"), "25-12-1995") diff --git a/common/space_lua/stdlib/os_test.lua b/common/space_lua/stdlib/os_test.lua new file mode 100644 index 00000000..85c7d35e --- /dev/null +++ b/common/space_lua/stdlib/os_test.lua @@ -0,0 +1,9 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +-- Basic OS functions +assert(os.time() > 0) +assert(os.date("%Y-%m-%d", os.time({ year = 2020, month = 1, day = 1 })) == "2020-01-01") \ No newline at end of file diff --git a/common/space_lua/stdlib/space_lua_test.lua b/common/space_lua/stdlib/space_lua_test.lua new file mode 100644 index 00000000..3775e246 --- /dev/null +++ b/common/space_lua/stdlib/space_lua_test.lua @@ -0,0 +1,15 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +-- 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") \ No newline at end of file diff --git a/common/space_lua/stdlib/string.ts b/common/space_lua/stdlib/string.ts index 4683e5a0..afaeb3ce 100644 --- a/common/space_lua/stdlib/string.ts +++ b/common/space_lua/stdlib/string.ts @@ -34,13 +34,32 @@ export const stringApi = new LuaTable({ }, ), gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => { - const regex = new RegExp(pattern, "g"); + const jsPattern = pattern + .replace(/%(.)/g, (_, char) => { + switch (char) { + case ".": + return "[.]"; + case "%": + return "%"; + case "d": + return "\\d"; + case "s": + return "\\s"; + case "w": + return "\\w"; + default: + return char; + } + }); + + const regex = new RegExp(jsPattern, "g"); return () => { const result = regex.exec(s); if (!result) { return; } - return new LuaMultiRes(result.slice(1)); + const captures = result.slice(1); + return new LuaMultiRes(captures.length > 0 ? captures : [result[0]]); }; }), gsub: new LuaBuiltinFunction( diff --git a/common/space_lua/stdlib/string_test.lua b/common/space_lua/stdlib/string_test.lua new file mode 100644 index 00000000..0edbac88 --- /dev/null +++ b/common/space_lua/stdlib/string_test.lua @@ -0,0 +1,109 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +-- Basic string functions +assert(string.len("Hello") == 5) +assert(string.byte("Hello", 1) == 72) +assert(string.char(72) == "H") +assert(string.find("Hello", "l") == 3) +assert(string.rep("Hello", 3) == "HelloHelloHello") +assert(string.sub("Hello", 2, 4) == "ell") +assert(string.upper("Hello") == "HELLO") +assert(string.lower("Hello") == "hello") + +-- Test string.gsub with various replacement types +-- Simple string replacement +local result, count = string.gsub("hello world", "hello", "hi") +assert(result == "hi world", "Basic string replacement failed") +assert(count == 1, "Basic replacement count failed") + +-- Multiple replacements +result, count = string.gsub("hello hello hello", "hello", "hi") +assert(result == "hi hi hi", "Multiple replacements failed") +assert(count == 3, "Multiple replacement count failed") + +-- Limited replacements with n parameter +result, count = string.gsub("hello hello hello", "hello", "hi", 2) +assert(result == "hi hi hello", "Limited replacements failed") +assert(count == 2, "Limited replacement count failed") + +-- Function replacement without captures +result = string.gsub("hello world", "hello", function(match) + assert(match == "hello", "Function received incorrect match") + return string.upper(match) +end) +assert(result == "HELLO world", "Function replacement without captures failed") + +-- Function replacement with single capture +result = string.gsub("hello world", "(h)ello", function(h) + assert(h == "h", "Function received incorrect capture") + return string.upper(h) .. "i" +end) +assert(result == "Hi world", "Function replacement with single capture failed") + +-- Function replacement with multiple captures +result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2) + assert(h == "h" and e == "e" and l1 == "l" and l2 == "l", + "Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2) + return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o" +end) +assert(result == "HEllo world", "Function replacement with multiple captures failed") + +-- Function returning nil (should keep original match) +result = string.gsub("hello world", "hello", function() return nil end) +assert(result == "hello world", "Function returning nil failed") + +-- Pattern with multiple matches on same position +result = string.gsub("hello world", "h?e", "X") +assert(result == "Xllo world", "Overlapping matches failed") + +-- Empty captures +result = string.gsub("hello", "(h()e)", function(full, empty) + assert(full == "he" and empty == "", "Empty capture handling failed") + return "XX" +end) +assert(result == "XXllo", "Empty capture replacement failed") + +-- Patterns with magic characters +result = string.gsub("hello.world", "%.", "-") +assert(result == "hello-world", "Magic character replacement failed") + +-- Test string.match +local m1, m2 = string.match("hello world", "(h)(ello)") +assert_equal(m1, "h") +assert_equal(m2, "ello") + +-- Test match with init position - need to capture the group +local init_match = string.match("hello world", "(world)", 7) +assert_equal(init_match, "world") + +-- Test string.gmatch +local words = {} +for word in string.gmatch("hello world lua", "%w+") do + table.insert(words, word) +end +assert_equal(words[1], "hello") +assert_equal(words[2], "world") +assert_equal(words[3], "lua") + +-- Test string.reverse +assert_equal(string.reverse("hello"), "olleh") +assert_equal(string.reverse(""), "") + +-- Test string.split +local parts = string.split("a,b,c", ",") +assert_equal(parts[1], "a") +assert_equal(parts[2], "b") +assert_equal(parts[3], "c") + +-- Test non-standard string extensions +assert_equal(string.startswith("hello world", "hello"), true) +assert_equal(string.startswith("hello world", "world"), false) + +assert_equal(string.endswith("hello world", "world"), true) +assert_equal(string.endswith("hello world", "hello"), false) + + diff --git a/common/space_lua/stdlib/table.ts b/common/space_lua/stdlib/table.ts index b67e61cd..0d063f78 100644 --- a/common/space_lua/stdlib/table.ts +++ b/common/space_lua/stdlib/table.ts @@ -8,6 +8,14 @@ import { } from "$common/space_lua/runtime.ts"; export const tableApi = new LuaTable({ + /** + * Concatenates the elements of a table into a string, using a separator. + * @param tbl - The table to concatenate. + * @param sep - The separator to use between elements. + * @param i - The start index. + * @param j - The end index. + * @returns The concatenated string. + */ concat: new LuaBuiltinFunction( (_sf, tbl: LuaTable, sep?: string, i?: number, j?: number) => { sep = sep ?? ""; @@ -20,6 +28,12 @@ export const tableApi = new LuaTable({ return result.join(sep); }, ), + /** + * Inserts an element into a table at a specified position. + * @param tbl - The table to insert the element into. + * @param posOrValue - The position or value to insert. + * @param value - The value to insert. + */ insert: new LuaBuiltinFunction( (_sf, tbl: LuaTable, posOrValue: number | any, value?: any) => { if (value === undefined) { @@ -33,16 +47,38 @@ export const tableApi = new LuaTable({ } }, ), + /** + * Removes an element from a table at a specified position. + * @param tbl - The table to remove the element from. + * @param pos - The position of the element to remove. + */ remove: new LuaBuiltinFunction((_sf, tbl: LuaTable, pos?: number) => { pos = pos ?? tbl.length; tbl.remove(pos); }), + /** + * Sorts a table. + * @param tbl - The table to sort. + * @param comp - The comparison function. + * @returns The sorted table. + */ sort: new LuaBuiltinFunction((sf, tbl: LuaTable, comp?: ILuaFunction) => { return tbl.sort(comp, sf); }), + /** + * Returns the keys of a table. + * @param tbl - The table to get the keys from. + * @returns The keys of the table. + */ keys: new LuaBuiltinFunction((_sf, tbl: LuaTable) => { return tbl.keys(); }), + /** + * Checks if a table (used as an array) contains a value. + * @param tbl - The table to check. + * @param value - The value to check for. + * @returns True if the value is in the table, false otherwise. + */ includes: new LuaBuiltinFunction( (sf, tbl: LuaTable | Record, value: LuaValue) => { if (tbl instanceof LuaTable) { diff --git a/common/space_lua/stdlib/table_test.lua b/common/space_lua/stdlib/table_test.lua new file mode 100644 index 00000000..2490851e --- /dev/null +++ b/common/space_lua/stdlib/table_test.lua @@ -0,0 +1,105 @@ +local function assert_equal(a, b) + if a ~= b then + error("Assertion failed: " .. a .. " is not equal to " .. b) + end +end + +-- Basic table operations +local t = { 1, 2, 3 } +table.insert(t, 4) +assert_equal(t[4], 4) +table.remove(t, 1) +assert_equal(t[1], 2) +table.insert(t, 1, 1) +assert_equal(t[1], 1) +assert_equal(table.concat({ "Hello", "world" }, " "), "Hello world") + +-- Table sorting +local t = { 3, 1, 2 } +table.sort(t) +assert_equal(t[1], 1) +assert_equal(t[2], 2) +assert_equal(t[3], 3) + +-- Table sorting with custom comparator +table.sort(t, function(a, b) + return a > b +end) +assert_equal(t[1], 3) +assert_equal(t[2], 2) +assert_equal(t[3], 1) + +-- Table sorting with complex objects +local data = { { name = "John", age = 30 }, { name = "Jane", age = 25 } } +table.sort(data, function(a, b) + return a.age < b.age +end) +assert_equal(data[1].name, "Jane") +assert_equal(data[2].name, "John") + +-- ipairs tests +local p = ipairs({ 3, 2, 1 }) +local idx, value = p() +assert(idx == 1 and value == 3) +idx, value = p() +assert(idx == 2 and value == 2) +idx, value = p() +assert(idx == 3 and value == 1) +idx, value = p() +assert(idx == nil and value == nil) + +for index, value in ipairs({ 1, 2, 3 }) do + assert(index == value) +end + +-- pairs tests +local p = pairs({ a = 1, b = 2, c = 3 }) +local key, value = p() +assert(key == "a" and value == 1) +key, value = p() +assert(key == "b" and value == 2) +key, value = p() +assert(key == "c" and value == 3) +key, value = p() +assert(key == nil and value == nil) + +for key, value in pairs({ a = "a", b = "b" }) do + assert_equal(key, value) +end + +-- for in over tables directly +local cnt = 1 +for val in { 1, 2, 3 } do + assert_equal(val, cnt) + cnt = cnt + 1 +end +assert_equal(cnt, 4) + +local cnt = 1 +for val in js.tojs({ 1, 2, 3 }) do + assert_equal(val, cnt) + cnt = cnt + 1 +end +assert_equal(cnt, 4) + +-- Table keys tests +local t = { a = 1, b = 2, c = 3 } +local keys = table.keys(t) +assert(table.includes(keys, "a")) +assert(table.includes(keys, "b")) +assert(table.includes(keys, "c")) + +-- Table includes tests with different value types +local t = { 1, 2, "three", true } +assert(table.includes(t, 1)) +assert(table.includes(t, "three")) +assert(table.includes(t, true)) +assert(not table.includes(t, "missing")) + + +-- Error cases +local success, error = pcall(function() + table.includes("not a table", 1) +end) +assert(not success) +assert(string.find(error, "Cannot use includes")) \ No newline at end of file