Lua: more fixes and work on stdlib
parent
f74bab0aca
commit
899c2556cb
|
@ -4,6 +4,7 @@ import {
|
||||||
LuaEnv,
|
LuaEnv,
|
||||||
LuaFunction,
|
LuaFunction,
|
||||||
LuaNativeJSFunction,
|
LuaNativeJSFunction,
|
||||||
|
LuaRuntimeError,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
||||||
import { parse as parseLua } from "$common/space_lua/parse.ts";
|
import { parse as parseLua } from "$common/space_lua/parse.ts";
|
||||||
|
@ -11,9 +12,13 @@ import { evalStatement } from "$common/space_lua/eval.ts";
|
||||||
import { jsToLuaValue } from "$common/space_lua/runtime.ts";
|
import { jsToLuaValue } from "$common/space_lua/runtime.ts";
|
||||||
import { LuaBuiltinFunction } from "$common/space_lua/runtime.ts";
|
import { LuaBuiltinFunction } from "$common/space_lua/runtime.ts";
|
||||||
import { LuaTable } from "$common/space_lua/runtime.ts";
|
import { LuaTable } from "$common/space_lua/runtime.ts";
|
||||||
import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
|
import {
|
||||||
|
type PageRef,
|
||||||
|
parsePageRef,
|
||||||
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
import type { ScriptEnvironment } from "$common/space_script.ts";
|
import type { ScriptEnvironment } from "$common/space_script.ts";
|
||||||
import { luaValueToJS } from "$common/space_lua/runtime.ts";
|
import { luaValueToJS } from "$common/space_lua/runtime.ts";
|
||||||
|
import type { ASTCtx } from "$common/space_lua/ast.ts";
|
||||||
|
|
||||||
export class SpaceLuaEnvironment {
|
export class SpaceLuaEnvironment {
|
||||||
env: LuaEnv = new LuaEnv();
|
env: LuaEnv = new LuaEnv();
|
||||||
|
@ -53,7 +58,7 @@ export class SpaceLuaEnvironment {
|
||||||
throw new Error("Callback is required");
|
throw new Error("Callback is required");
|
||||||
}
|
}
|
||||||
scriptEnv.registerCommand(
|
scriptEnv.registerCommand(
|
||||||
def.toJSObject() as any,
|
luaValueToJS(def) as any,
|
||||||
async (...args: any[]) => {
|
async (...args: any[]) => {
|
||||||
try {
|
try {
|
||||||
return await def.get(1).call(...args.map(jsToLuaValue));
|
return await def.get(1).call(...args.map(jsToLuaValue));
|
||||||
|
@ -93,6 +98,15 @@ export class SpaceLuaEnvironment {
|
||||||
const scriptEnv = new LuaEnv(env);
|
const scriptEnv = new LuaEnv(env);
|
||||||
await evalStatement(ast, scriptEnv);
|
await evalStatement(ast, scriptEnv);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
if (e instanceof LuaRuntimeError) {
|
||||||
|
const origin = resolveASTReference(e.context);
|
||||||
|
if (origin) {
|
||||||
|
console.error(
|
||||||
|
`Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
console.error(
|
console.error(
|
||||||
`Error evaluating script: ${e.message} for script: ${script.script}`,
|
`Error evaluating script: ${e.message} for script: ${script.script}`,
|
||||||
);
|
);
|
||||||
|
@ -111,3 +125,14 @@ export class SpaceLuaEnvironment {
|
||||||
console.log("Loaded", allScripts.length, "Lua scripts");
|
console.log("Loaded", allScripts.length, "Lua scripts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveASTReference(ctx?: ASTCtx): PageRef | null {
|
||||||
|
if (!ctx?.ref) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pageRef = parsePageRef(ctx.ref);
|
||||||
|
return {
|
||||||
|
page: pageRef.page,
|
||||||
|
pos: (pageRef.pos as number) + "```space-lua\n".length + ctx.from!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { assertEquals } from "@std/assert/equals";
|
import { assertEquals } from "@std/assert/equals";
|
||||||
import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts";
|
import {
|
||||||
|
LuaEnv,
|
||||||
|
LuaNativeJSFunction,
|
||||||
|
luaValueToJS,
|
||||||
|
singleResult,
|
||||||
|
} from "./runtime.ts";
|
||||||
import { parse } from "./parse.ts";
|
import { parse } from "./parse.ts";
|
||||||
import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
|
import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
|
||||||
import { evalExpression, evalStatement } from "./eval.ts";
|
import { evalExpression, evalStatement } from "./eval.ts";
|
||||||
|
@ -40,25 +45,24 @@ Deno.test("Evaluator test", async () => {
|
||||||
assertEquals(tbl.get(1), 3);
|
assertEquals(tbl.get(1), 3);
|
||||||
assertEquals(tbl.get(2), 1);
|
assertEquals(tbl.get(2), 1);
|
||||||
assertEquals(tbl.get(3), 2);
|
assertEquals(tbl.get(3), 2);
|
||||||
assertEquals(tbl.toJSArray(), [3, 1, 2]);
|
assertEquals(luaValueToJS(tbl), [3, 1, 2]);
|
||||||
|
|
||||||
assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toJSObject(), {
|
assertEquals(luaValueToJS(evalExpr(`{name=test("Zef"), age=100}`, env)), {
|
||||||
name: "Zef",
|
name: "Zef",
|
||||||
age: 100,
|
age: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toJSObject(),
|
luaValueToJS(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)),
|
||||||
{
|
{
|
||||||
name: "Zef",
|
name: "Zef",
|
||||||
age: 100,
|
age: 100,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toJSObject(), {
|
const result = evalExpr(`{[3+2]=1, ["a".."b"]=2}`);
|
||||||
5: 1,
|
assertEquals(result.get(5), 1);
|
||||||
ab: 2,
|
assertEquals(result.get("ab"), 2);
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(evalExpr(`#{}`), 0);
|
assertEquals(evalExpr(`#{}`), 0);
|
||||||
assertEquals(evalExpr(`#{1, 2, 3}`), 3);
|
assertEquals(evalExpr(`#{1, 2, 3}`), 3);
|
||||||
|
@ -104,7 +108,7 @@ Deno.test("Statement evaluation", async () => {
|
||||||
const env3 = new LuaEnv();
|
const env3 = new LuaEnv();
|
||||||
await evalBlock(`tbl = {1, 2, 3}`, env3);
|
await evalBlock(`tbl = {1, 2, 3}`, env3);
|
||||||
await evalBlock(`tbl[1] = 3`, env3);
|
await evalBlock(`tbl[1] = 3`, env3);
|
||||||
assertEquals(env3.get("tbl").toJSArray(), [3, 2, 3]);
|
assertEquals(luaValueToJS(env3.get("tbl")), [3, 2, 3]);
|
||||||
await evalBlock("tbl.name = 'Zef'", env3);
|
await evalBlock("tbl.name = 'Zef'", env3);
|
||||||
assertEquals(env3.get("tbl").get("name"), "Zef");
|
assertEquals(env3.get("tbl").get("name"), "Zef");
|
||||||
await evalBlock(`tbl[2] = {age=10}`, env3);
|
await evalBlock(`tbl[2] = {age=10}`, env3);
|
||||||
|
|
|
@ -91,41 +91,11 @@ export function evalExpression(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 "PropertyAccess": {
|
|
||||||
const obj = evalPrefixExpression(e.object, env);
|
|
||||||
if (obj instanceof Promise) {
|
|
||||||
return obj.then((obj) => {
|
|
||||||
if (!obj.get) {
|
|
||||||
throw new Error(
|
|
||||||
`Not a gettable object: ${obj}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return obj.get(e.property);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!obj.get) {
|
|
||||||
throw new Error(
|
|
||||||
`Not a gettable object: ${obj}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return obj.get(e.property);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "Variable":
|
case "Variable":
|
||||||
case "FunctionCall":
|
case "FunctionCall":
|
||||||
|
case "TableAccess":
|
||||||
|
case "PropertyAccess":
|
||||||
return evalPrefixExpression(e, env);
|
return evalPrefixExpression(e, env);
|
||||||
case "TableConstructor": {
|
case "TableConstructor": {
|
||||||
const table = new LuaTable();
|
const table = new LuaTable();
|
||||||
|
@ -229,21 +199,66 @@ function evalPrefixExpression(
|
||||||
}
|
}
|
||||||
case "Parenthesized":
|
case "Parenthesized":
|
||||||
return evalExpression(e.expression, env);
|
return evalExpression(e.expression, env);
|
||||||
|
// <<expr>>[<<expr>>]
|
||||||
|
case "TableAccess": {
|
||||||
|
const values = evalPromiseValues([
|
||||||
|
evalPrefixExpression(e.object, env),
|
||||||
|
evalExpression(e.key, env),
|
||||||
|
]);
|
||||||
|
if (values instanceof Promise) {
|
||||||
|
return values.then(([table, key]) => {
|
||||||
|
table = singleResult(table);
|
||||||
|
key = singleResult(key);
|
||||||
|
if (!table) {
|
||||||
|
throw new LuaRuntimeError(
|
||||||
|
`Attempting to index a nil value`,
|
||||||
|
e.object.ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === null || key === undefined) {
|
||||||
|
throw new LuaRuntimeError(
|
||||||
|
`Attempting to index with a nil key`,
|
||||||
|
e.key.ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return luaGet(table, key);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const table = singleResult(values[0]);
|
||||||
|
const key = singleResult(values[1]);
|
||||||
|
if (!table) {
|
||||||
|
throw new LuaRuntimeError(
|
||||||
|
`Attempting to index a nil value`,
|
||||||
|
e.object.ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === null || key === undefined) {
|
||||||
|
throw new LuaRuntimeError(
|
||||||
|
`Attempting to index with a nil key`,
|
||||||
|
e.key.ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return luaGet(table, singleResult(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// <expr>.property
|
||||||
case "PropertyAccess": {
|
case "PropertyAccess": {
|
||||||
const obj = evalPrefixExpression(e.object, env);
|
const obj = evalPrefixExpression(e.object, env);
|
||||||
if (obj instanceof Promise) {
|
if (obj instanceof Promise) {
|
||||||
return obj.then((obj) => {
|
return obj.then((obj) => {
|
||||||
if (!obj?.get) {
|
if (!obj?.get) {
|
||||||
throw new Error(
|
throw new LuaRuntimeError(
|
||||||
`Attempting to index non-indexable object: ${obj}`,
|
`Attempting to index a nil value`,
|
||||||
|
e.object.ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return obj.get(e.property);
|
return obj.get(e.property);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!obj?.get) {
|
if (!obj?.get) {
|
||||||
throw new Error(
|
throw new LuaRuntimeError(
|
||||||
`Attempting to index non-indexable object: ${obj}`,
|
`Attempting to index a nil value`,
|
||||||
|
e.object.ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return obj.get(e.property);
|
return obj.get(e.property);
|
||||||
|
@ -540,8 +555,9 @@ export async function evalStatement(
|
||||||
for (let i = 0; i < propNames.length - 1; i++) {
|
for (let i = 0; i < propNames.length - 1; i++) {
|
||||||
settable = settable.get(propNames[i]);
|
settable = settable.get(propNames[i]);
|
||||||
if (!settable) {
|
if (!settable) {
|
||||||
throw new Error(
|
throw new LuaRuntimeError(
|
||||||
`Cannot find property ${propNames[i]}`,
|
`Cannot find property ${propNames[i]}`,
|
||||||
|
s.name.ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -676,8 +692,9 @@ function evalLValue(
|
||||||
if (objValue instanceof Promise) {
|
if (objValue instanceof Promise) {
|
||||||
return objValue.then((objValue) => {
|
return objValue.then((objValue) => {
|
||||||
if (!objValue.set) {
|
if (!objValue.set) {
|
||||||
throw new Error(
|
throw new LuaRuntimeError(
|
||||||
`Not a settable object: ${objValue}`,
|
`Not a settable object: ${objValue}`,
|
||||||
|
lval.object.ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -687,8 +704,9 @@ function evalLValue(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!objValue.set) {
|
if (!objValue.set) {
|
||||||
throw new Error(
|
throw new LuaRuntimeError(
|
||||||
`Not a settable object: ${objValue}`,
|
`Not a settable object: ${objValue}`,
|
||||||
|
lval.object.ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
local function assert_equal(a, b)
|
||||||
|
if a ~= b then
|
||||||
|
error("Assertion failed: " .. a .. " is not equal to " .. b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Basic checks
|
-- Basic checks
|
||||||
assert(true, "True is true")
|
assert(true, "True is true")
|
||||||
|
|
||||||
|
@ -17,6 +23,7 @@ assert("Hello " .. "world" == "Hello world")
|
||||||
function f1()
|
function f1()
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(f1() == 1)
|
assert(f1() == 1)
|
||||||
|
|
||||||
function sqr(a)
|
function sqr(a)
|
||||||
|
@ -42,6 +49,7 @@ assert(apply(sqr, 3) == 9)
|
||||||
function multi_return()
|
function multi_return()
|
||||||
return 1, 2
|
return 1, 2
|
||||||
end
|
end
|
||||||
|
|
||||||
local a, b = multi_return()
|
local a, b = multi_return()
|
||||||
assert(a == 1 and b == 2)
|
assert(a == 1 and b == 2)
|
||||||
|
|
||||||
|
@ -201,3 +209,97 @@ assert(not deepCompare(
|
||||||
-- String serialization
|
-- String serialization
|
||||||
assert(tostring({ 1, 2, 3 }) == "{1, 2, 3}")
|
assert(tostring({ 1, 2, 3 }) == "{1, 2, 3}")
|
||||||
assert(tostring({ a = 1, b = 2 }) == "{a = 1, b = 2}")
|
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
|
||||||
|
|
||||||
|
-- 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.format("Hello %s", "world") == "Hello world")
|
||||||
|
assert(string.rep("Hello", 3) == "HelloHelloHello")
|
||||||
|
assert(string.sub("Hello", 2, 4) == "ell")
|
||||||
|
assert(string.upper("Hello") == "HELLO")
|
||||||
|
assert(string.lower("Hello") == "hello")
|
||||||
|
|
||||||
|
-- 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")
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { assertEquals } from "@std/assert/equals";
|
import { assertEquals } from "@std/assert/equals";
|
||||||
import { LuaMultiRes } from "$common/space_lua/runtime.ts";
|
import {
|
||||||
|
jsToLuaValue,
|
||||||
|
luaLen,
|
||||||
|
LuaMultiRes,
|
||||||
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
|
||||||
Deno.test("Test Lua Rutime", () => {
|
Deno.test("Test Lua Rutime", () => {
|
||||||
// Test LuaMultires
|
// Test LuaMultires
|
||||||
|
|
||||||
assertEquals(new LuaMultiRes([]).flatten().values, []);
|
assertEquals(new LuaMultiRes([]).flatten().values, []);
|
||||||
assertEquals(new LuaMultiRes([1, 2, 3]).flatten().values, [1, 2, 3]);
|
assertEquals(new LuaMultiRes([1, 2, 3]).flatten().values, [1, 2, 3]);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -14,4 +17,28 @@ Deno.test("Test Lua Rutime", () => {
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test JavaScript to Lua conversion
|
||||||
|
assertEquals(jsToLuaValue(1), 1);
|
||||||
|
assertEquals(jsToLuaValue("hello"), "hello");
|
||||||
|
// Arrays
|
||||||
|
let luaVal = jsToLuaValue([1, 2, 3]);
|
||||||
|
assertEquals(luaLen(luaVal), 3);
|
||||||
|
assertEquals(luaVal.get(1), 1);
|
||||||
|
// Objects
|
||||||
|
luaVal = jsToLuaValue({ name: "Pete", age: 10 });
|
||||||
|
assertEquals(luaVal.get("name"), "Pete");
|
||||||
|
assertEquals(luaVal.get("age"), 10);
|
||||||
|
// Nested objects
|
||||||
|
luaVal = jsToLuaValue({ name: "Pete", list: [1, 2, 3] });
|
||||||
|
assertEquals(luaVal.get("name"), "Pete");
|
||||||
|
assertEquals(luaLen(luaVal.get("list")), 3);
|
||||||
|
assertEquals(luaVal.get("list").get(2), 2);
|
||||||
|
luaVal = jsToLuaValue([{ name: "Pete" }, { name: "John" }]);
|
||||||
|
assertEquals(luaLen(luaVal), 2);
|
||||||
|
assertEquals(luaVal.get(1).get("name"), "Pete");
|
||||||
|
assertEquals(luaVal.get(2).get("name"), "John");
|
||||||
|
// Functions in objects
|
||||||
|
luaVal = jsToLuaValue({ name: "Pete", first: (l: any[]) => l[0] });
|
||||||
|
assertEquals(luaVal.get("first").call([1, 2, 3]), 1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ASTCtx, LuaFunctionBody } from "./ast.ts";
|
import type { ASTCtx, LuaFunctionBody } from "./ast.ts";
|
||||||
import { evalStatement } from "$common/space_lua/eval.ts";
|
import { evalStatement } from "$common/space_lua/eval.ts";
|
||||||
|
import { asyncQuickSort } from "$common/space_lua/util.ts";
|
||||||
|
|
||||||
export type LuaType =
|
export type LuaType =
|
||||||
| "nil"
|
| "nil"
|
||||||
|
@ -17,6 +18,7 @@ export type JSValue = any;
|
||||||
|
|
||||||
export interface ILuaFunction {
|
export interface ILuaFunction {
|
||||||
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
|
||||||
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILuaSettable {
|
export interface ILuaSettable {
|
||||||
|
@ -79,8 +81,8 @@ export class LuaMultiRes {
|
||||||
}
|
}
|
||||||
|
|
||||||
unwrap(): any {
|
unwrap(): any {
|
||||||
if (this.values.length !== 1) {
|
if (this.values.length === 0) {
|
||||||
throw new Error("Cannot unwrap multiple values");
|
return null;
|
||||||
}
|
}
|
||||||
return this.values[0];
|
return this.values[0];
|
||||||
}
|
}
|
||||||
|
@ -136,6 +138,10 @@ export class LuaFunction implements ILuaFunction {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `<lua function(${this.body.parameters.join(", ")})>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaNativeJSFunction implements ILuaFunction {
|
export class LuaNativeJSFunction implements ILuaFunction {
|
||||||
|
@ -151,6 +157,10 @@ export class LuaNativeJSFunction implements ILuaFunction {
|
||||||
return jsToLuaValue(result);
|
return jsToLuaValue(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `<native js function: ${this.fn.name}>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaBuiltinFunction implements ILuaFunction {
|
export class LuaBuiltinFunction implements ILuaFunction {
|
||||||
|
@ -160,6 +170,10 @@ export class LuaBuiltinFunction implements ILuaFunction {
|
||||||
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
||||||
return this.fn(...args);
|
return this.fn(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `<builtin lua function>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaTable implements ILuaSettable, ILuaGettable {
|
export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||||
|
@ -173,10 +187,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||||
|
|
||||||
public metatable: LuaTable | null;
|
public metatable: LuaTable | null;
|
||||||
|
|
||||||
constructor() {
|
constructor(init?: any[] | Record<string, any>) {
|
||||||
// For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
|
// For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
|
||||||
this.stringKeys = {};
|
this.arrayPart = Array.isArray(init) ? init : [];
|
||||||
this.arrayPart = [];
|
this.stringKeys = init && !Array.isArray(init) ? init : {};
|
||||||
this.otherKeys = null; // Only create this when needed
|
this.otherKeys = null; // Only create this when needed
|
||||||
this.metatable = null;
|
this.metatable = null;
|
||||||
}
|
}
|
||||||
|
@ -263,16 +277,22 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSArray(): JSValue[] {
|
insert(value: LuaValue, pos: number) {
|
||||||
return this.arrayPart;
|
this.arrayPart.splice(pos - 1, 0, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSObject(): Record<string, JSValue> {
|
remove(pos: number) {
|
||||||
const result = { ...this.stringKeys };
|
this.arrayPart.splice(pos - 1, 1);
|
||||||
for (const i in this.arrayPart) {
|
}
|
||||||
result[parseInt(i) + 1] = this.arrayPart[i];
|
|
||||||
|
async sort(fn?: ILuaFunction) {
|
||||||
|
if (fn) {
|
||||||
|
this.arrayPart = await asyncQuickSort(this.arrayPart, async (a, b) => {
|
||||||
|
return (await fn.call(a, b)) ? -1 : 1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.arrayPart.sort();
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
|
@ -306,22 +326,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||||
result += "}";
|
result += "}";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJSArray(arr: JSValue[]): LuaTable {
|
|
||||||
const table = new LuaTable();
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
table.set(i + 1, arr[i]);
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJSObject(obj: Record<string, JSValue>): LuaTable {
|
|
||||||
const table = new LuaTable();
|
|
||||||
for (const key in obj) {
|
|
||||||
table.set(key, obj[key]);
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
|
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
|
||||||
|
@ -344,7 +348,7 @@ export function luaGet(obj: any, key: any): any {
|
||||||
|
|
||||||
export function luaLen(obj: any): number {
|
export function luaLen(obj: any): number {
|
||||||
if (obj instanceof LuaTable) {
|
if (obj instanceof LuaTable) {
|
||||||
return obj.toJSArray().length;
|
return obj.length;
|
||||||
} else if (Array.isArray(obj)) {
|
} else if (Array.isArray(obj)) {
|
||||||
return obj.length;
|
return obj.length;
|
||||||
} else {
|
} else {
|
||||||
|
@ -365,7 +369,7 @@ export function luaTypeOf(val: any): LuaType {
|
||||||
return "table";
|
return "table";
|
||||||
} else if (Array.isArray(val)) {
|
} else if (Array.isArray(val)) {
|
||||||
return "table";
|
return "table";
|
||||||
} else if (typeof val === "function") {
|
} else if (typeof val === "function" || val.call) {
|
||||||
return "function";
|
return "function";
|
||||||
} else {
|
} else {
|
||||||
return "userdata";
|
return "userdata";
|
||||||
|
@ -423,25 +427,50 @@ export function jsToLuaValue(value: any): any {
|
||||||
if (value instanceof LuaTable) {
|
if (value instanceof LuaTable) {
|
||||||
return value;
|
return value;
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
return LuaTable.fromJSArray(value.map(jsToLuaValue));
|
const table = new LuaTable();
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
table.set(i + 1, jsToLuaValue(value[i]));
|
||||||
|
}
|
||||||
|
return table;
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
return LuaTable.fromJSObject(value);
|
const table = new LuaTable();
|
||||||
|
for (const key in value) {
|
||||||
|
table.set(key, jsToLuaValue(value[key]));
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
} else if (typeof value === "function") {
|
||||||
|
return new LuaNativeJSFunction(value);
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inverse of jsToLuaValue
|
||||||
export function luaValueToJS(value: any): any {
|
export function luaValueToJS(value: any): any {
|
||||||
if (value instanceof Promise) {
|
if (value instanceof Promise) {
|
||||||
return value.then(luaValueToJS);
|
return value.then(luaValueToJS);
|
||||||
}
|
}
|
||||||
if (value instanceof LuaTable) {
|
if (value instanceof LuaTable) {
|
||||||
// This is a heuristic: if this table is used as an array, we return an array
|
// We'll go a bit on heuristics here
|
||||||
|
// If the table has a length > 0 we'll assume it's a pure array
|
||||||
|
// Otherwise we'll assume it's a pure object
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
return value.toJSArray();
|
const result = [];
|
||||||
} else {
|
for (let i = 0; i < value.length; i++) {
|
||||||
return value.toJSObject();
|
result.push(luaValueToJS(value.get(i + 1)));
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const key of value.keys()) {
|
||||||
|
result[key] = luaValueToJS(value.get(key));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (value instanceof LuaNativeJSFunction) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
return jsToLuaValue(value.fn(...args.map(luaValueToJS)));
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
|
||||||
import { assert } from "@std/assert/assert";
|
|
||||||
import { assertEquals } from "@std/assert/equals";
|
|
||||||
import { LuaTable } from "$common/space_lua/runtime.ts";
|
|
||||||
|
|
||||||
Deno.test("Lua Standard Library test", () => {
|
|
||||||
const stdlib = luaBuildStandardEnv();
|
|
||||||
stdlib.get("print").call([1, 2, 3]);
|
|
||||||
stdlib.get("assert").call(true);
|
|
||||||
try {
|
|
||||||
stdlib.get("assert").call(false, "This should fail");
|
|
||||||
assert(false);
|
|
||||||
} catch (e: any) {
|
|
||||||
assert(e.message.includes("This should fail"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipairs = stdlib.get("ipairs").call(["a", "b", "c"]);
|
|
||||||
assertEquals(ipairs().values, [0, "a"]);
|
|
||||||
assertEquals(ipairs().values, [1, "b"]);
|
|
||||||
assertEquals(ipairs().values, [2, "c"]);
|
|
||||||
assertEquals(ipairs(), undefined);
|
|
||||||
|
|
||||||
const tbl = new LuaTable();
|
|
||||||
tbl.set("a", 1);
|
|
||||||
tbl.set("b", 2);
|
|
||||||
tbl.set("c", 3);
|
|
||||||
tbl.set(1, "a");
|
|
||||||
const pairs = stdlib.get("pairs").call(tbl);
|
|
||||||
assertEquals(pairs().values, ["a", 1]);
|
|
||||||
assertEquals(pairs().values, ["b", 2]);
|
|
||||||
assertEquals(pairs().values, ["c", 3]);
|
|
||||||
assertEquals(pairs().values, [1, "a"]);
|
|
||||||
|
|
||||||
assertEquals(stdlib.get("type").call(1), "number");
|
|
||||||
assertEquals(stdlib.get("type").call("a"), "string");
|
|
||||||
assertEquals(stdlib.get("type").call(true), "boolean");
|
|
||||||
assertEquals(stdlib.get("type").call(null), "nil");
|
|
||||||
assertEquals(stdlib.get("type").call(undefined), "nil");
|
|
||||||
assertEquals(stdlib.get("type").call(tbl), "table");
|
|
||||||
});
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
|
type ILuaFunction,
|
||||||
LuaBuiltinFunction,
|
LuaBuiltinFunction,
|
||||||
LuaEnv,
|
LuaEnv,
|
||||||
LuaMultiRes,
|
LuaMultiRes,
|
||||||
LuaNativeJSFunction,
|
LuaTable,
|
||||||
type LuaTable,
|
|
||||||
luaToString,
|
luaToString,
|
||||||
luaTypeOf,
|
luaTypeOf,
|
||||||
type LuaValue,
|
type LuaValue,
|
||||||
|
@ -13,21 +13,21 @@ const printFunction = new LuaBuiltinFunction((...args) => {
|
||||||
console.log("[Lua]", ...args.map(luaToString));
|
console.log("[Lua]", ...args.map(luaToString));
|
||||||
});
|
});
|
||||||
|
|
||||||
const assertFunction = new LuaNativeJSFunction(
|
const assertFunction = new LuaBuiltinFunction(
|
||||||
(value: any, message?: string) => {
|
async (value: any, message?: string) => {
|
||||||
if (!value) {
|
if (!await value) {
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
throw new Error(`Assertion failed: ${message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const ipairsFunction = new LuaNativeJSFunction((ar: any[]) => {
|
const ipairsFunction = new LuaBuiltinFunction((ar: LuaTable) => {
|
||||||
let i = 0;
|
let i = 1;
|
||||||
return () => {
|
return () => {
|
||||||
if (i >= ar.length) {
|
if (i > ar.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = new LuaMultiRes([i, ar[i]]);
|
const result = new LuaMultiRes([i, ar.get(i)]);
|
||||||
i++;
|
i++;
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -41,14 +41,17 @@ const pairsFunction = new LuaBuiltinFunction((t: LuaTable) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = keys[i];
|
const key = keys[i];
|
||||||
const result = new LuaMultiRes([key, t.get(key)]);
|
|
||||||
i++;
|
i++;
|
||||||
return result;
|
return new LuaMultiRes([key, t.get(key)]);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpackFunction = new LuaBuiltinFunction((t: LuaTable) => {
|
const unpackFunction = new LuaBuiltinFunction((t: LuaTable) => {
|
||||||
return new LuaMultiRes(t.toJSArray());
|
const values: LuaValue[] = [];
|
||||||
|
for (let i = 1; i <= t.length; i++) {
|
||||||
|
values.push(t.get(i));
|
||||||
|
}
|
||||||
|
return new LuaMultiRes(values);
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeFunction = new LuaBuiltinFunction((value: LuaValue): string => {
|
const typeFunction = new LuaBuiltinFunction((value: LuaValue): string => {
|
||||||
|
@ -59,14 +62,34 @@ const tostringFunction = new LuaBuiltinFunction((value: any) => {
|
||||||
return luaToString(value);
|
return luaToString(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tonumberFunction = new LuaNativeJSFunction((value: any) => {
|
const tonumberFunction = new LuaBuiltinFunction((value: LuaValue) => {
|
||||||
return Number(value);
|
return Number(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorFunction = new LuaNativeJSFunction((message: string) => {
|
const errorFunction = new LuaBuiltinFunction((message: string) => {
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pcallFunction = new LuaBuiltinFunction(
|
||||||
|
async (fn: ILuaFunction, ...args) => {
|
||||||
|
try {
|
||||||
|
return new LuaMultiRes([true, await fn.call(...args)]);
|
||||||
|
} catch (e: any) {
|
||||||
|
return new LuaMultiRes([false, e.message]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const xpcallFunction = new LuaBuiltinFunction(
|
||||||
|
async (fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => {
|
||||||
|
try {
|
||||||
|
return new LuaMultiRes([true, await fn.call(...args)]);
|
||||||
|
} catch (e: any) {
|
||||||
|
return new LuaMultiRes([false, await errorHandler.call(e.message)]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const setmetatableFunction = new LuaBuiltinFunction(
|
const setmetatableFunction = new LuaBuiltinFunction(
|
||||||
(table: LuaTable, metatable: LuaTable) => {
|
(table: LuaTable, metatable: LuaTable) => {
|
||||||
table.metatable = metatable;
|
table.metatable = metatable;
|
||||||
|
@ -85,6 +108,134 @@ const getmetatableFunction = new LuaBuiltinFunction((table: LuaTable) => {
|
||||||
return table.metatable;
|
return table.metatable;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stringFunctions = new LuaTable({
|
||||||
|
byte: new LuaBuiltinFunction((s: string, i?: number, j?: number) => {
|
||||||
|
i = i ?? 1;
|
||||||
|
j = j ?? i;
|
||||||
|
const result = [];
|
||||||
|
for (let k = i; k <= j; k++) {
|
||||||
|
result.push(s.charCodeAt(k - 1));
|
||||||
|
}
|
||||||
|
return new LuaMultiRes(result);
|
||||||
|
}),
|
||||||
|
char: new LuaBuiltinFunction((...args: number[]) => {
|
||||||
|
return String.fromCharCode(...args);
|
||||||
|
}),
|
||||||
|
find: new LuaBuiltinFunction(
|
||||||
|
(s: string, pattern: string, init?: number, plain?: boolean) => {
|
||||||
|
init = init ?? 1;
|
||||||
|
plain = plain ?? false;
|
||||||
|
const result = s.slice(init - 1).match(pattern);
|
||||||
|
if (!result) {
|
||||||
|
return new LuaMultiRes([]);
|
||||||
|
}
|
||||||
|
return new LuaMultiRes([
|
||||||
|
result.index! + 1,
|
||||||
|
result.index! + result[0].length,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
format: new LuaBuiltinFunction((format: string, ...args: any[]) => {
|
||||||
|
return format.replace(/%./g, (match) => {
|
||||||
|
switch (match) {
|
||||||
|
case "%s":
|
||||||
|
return luaToString(args.shift());
|
||||||
|
case "%d":
|
||||||
|
return String(args.shift());
|
||||||
|
default:
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
gmatch: new LuaBuiltinFunction((s: string, pattern: string) => {
|
||||||
|
const regex = new RegExp(pattern, "g");
|
||||||
|
return () => {
|
||||||
|
const result = regex.exec(s);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return new LuaMultiRes(result.slice(1));
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
gsub: new LuaBuiltinFunction(
|
||||||
|
(s: string, pattern: string, repl: string, n?: number) => {
|
||||||
|
n = n ?? Infinity;
|
||||||
|
const regex = new RegExp(pattern, "g");
|
||||||
|
let result = s;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
match = regex.exec(result);
|
||||||
|
if (!match) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result = result.replace(match[0], repl);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
len: new LuaBuiltinFunction((s: string) => {
|
||||||
|
return s.length;
|
||||||
|
}),
|
||||||
|
lower: new LuaBuiltinFunction((s: string) => {
|
||||||
|
return luaToString(s.toLowerCase());
|
||||||
|
}),
|
||||||
|
upper: new LuaBuiltinFunction((s: string) => {
|
||||||
|
return luaToString(s.toUpperCase());
|
||||||
|
}),
|
||||||
|
match: new LuaBuiltinFunction(
|
||||||
|
(s: string, pattern: string, init?: number) => {
|
||||||
|
init = init ?? 1;
|
||||||
|
const result = s.slice(init - 1).match(pattern);
|
||||||
|
if (!result) {
|
||||||
|
return new LuaMultiRes([]);
|
||||||
|
}
|
||||||
|
return new LuaMultiRes(result.slice(1));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
rep: new LuaBuiltinFunction((s: string, n: number, sep?: string) => {
|
||||||
|
sep = sep ?? "";
|
||||||
|
return s.repeat(n) + sep;
|
||||||
|
}),
|
||||||
|
reverse: new LuaBuiltinFunction((s: string) => {
|
||||||
|
return s.split("").reverse().join("");
|
||||||
|
}),
|
||||||
|
sub: new LuaBuiltinFunction((s: string, i: number, j?: number) => {
|
||||||
|
j = j ?? s.length;
|
||||||
|
return s.slice(i - 1, j);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableFunctions = new LuaTable({
|
||||||
|
concat: new LuaBuiltinFunction(
|
||||||
|
(tbl: LuaTable, sep?: string, i?: number, j?: number) => {
|
||||||
|
sep = sep ?? "";
|
||||||
|
i = i ?? 1;
|
||||||
|
j = j ?? tbl.length;
|
||||||
|
const result = [];
|
||||||
|
for (let k = i; k <= j; k++) {
|
||||||
|
result.push(tbl.get(k));
|
||||||
|
}
|
||||||
|
return result.join(sep);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
insert: new LuaBuiltinFunction(
|
||||||
|
(tbl: LuaTable, posOrValue: number | any, value?: any) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
value = posOrValue;
|
||||||
|
posOrValue = tbl.length + 1;
|
||||||
|
}
|
||||||
|
tbl.insert(posOrValue, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
remove: new LuaBuiltinFunction((tbl: LuaTable, pos?: number) => {
|
||||||
|
pos = pos ?? tbl.length;
|
||||||
|
tbl.remove(pos);
|
||||||
|
}),
|
||||||
|
sort: new LuaBuiltinFunction((tbl: LuaTable, comp?: ILuaFunction) => {
|
||||||
|
return tbl.sort(comp);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export function luaBuildStandardEnv() {
|
export function luaBuildStandardEnv() {
|
||||||
const env = new LuaEnv();
|
const env = new LuaEnv();
|
||||||
env.set("print", printFunction);
|
env.set("print", printFunction);
|
||||||
|
@ -95,9 +246,13 @@ export function luaBuildStandardEnv() {
|
||||||
env.set("tostring", tostringFunction);
|
env.set("tostring", tostringFunction);
|
||||||
env.set("tonumber", tonumberFunction);
|
env.set("tonumber", tonumberFunction);
|
||||||
env.set("error", errorFunction);
|
env.set("error", errorFunction);
|
||||||
|
env.set("pcall", pcallFunction);
|
||||||
|
env.set("xpcall", xpcallFunction);
|
||||||
env.set("unpack", unpackFunction);
|
env.set("unpack", unpackFunction);
|
||||||
env.set("setmetatable", setmetatableFunction);
|
env.set("setmetatable", setmetatableFunction);
|
||||||
env.set("getmetatable", getmetatableFunction);
|
env.set("getmetatable", getmetatableFunction);
|
||||||
env.set("rawset", rawsetFunction);
|
env.set("rawset", rawsetFunction);
|
||||||
|
env.set("string", stringFunctions);
|
||||||
|
env.set("table", tableFunctions);
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,3 +14,79 @@ export function evalPromiseValues(vals: any[]): Promise<any[]> | any[] {
|
||||||
return Promise.all(promises).then(() => promiseResults);
|
return Promise.all(promises).then(() => promiseResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the mid value among x, y, and z
|
||||||
|
* @param x
|
||||||
|
* @param y
|
||||||
|
* @param z
|
||||||
|
* @param compare
|
||||||
|
* @returns {Promise.<*>}
|
||||||
|
*/
|
||||||
|
async function getPivot(
|
||||||
|
x: any,
|
||||||
|
y: any,
|
||||||
|
z: any,
|
||||||
|
compare: (a: any, b: any) => Promise<number>,
|
||||||
|
) {
|
||||||
|
if (await compare(x, y) < 0) {
|
||||||
|
if (await compare(y, z) < 0) {
|
||||||
|
return y;
|
||||||
|
} else if (await compare(z, x) < 0) {
|
||||||
|
return x;
|
||||||
|
} else {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
} else if (await compare(y, z) > 0) {
|
||||||
|
return y;
|
||||||
|
} else if (await compare(z, x) > 0) {
|
||||||
|
return x;
|
||||||
|
} else {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* asynchronous quick sort
|
||||||
|
* @param arr array to sort
|
||||||
|
* @param compare asynchronous comparing function
|
||||||
|
* @param left index where the range of elements to be sorted starts
|
||||||
|
* @param right index where the range of elements to be sorted ends
|
||||||
|
* @returns {Promise.<*>}
|
||||||
|
*/
|
||||||
|
export async function asyncQuickSort(
|
||||||
|
arr: any[],
|
||||||
|
compare: (a: any, b: any) => Promise<number>,
|
||||||
|
left = 0,
|
||||||
|
right = arr.length - 1,
|
||||||
|
) {
|
||||||
|
if (left < right) {
|
||||||
|
let i = left, j = right, tmp;
|
||||||
|
const pivot = await getPivot(
|
||||||
|
arr[i],
|
||||||
|
arr[i + Math.floor((j - i) / 2)],
|
||||||
|
arr[j],
|
||||||
|
compare,
|
||||||
|
);
|
||||||
|
while (true) {
|
||||||
|
while (await compare(arr[i], pivot) < 0) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (await compare(pivot, arr[j]) < 0) {
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
if (i >= j) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tmp = arr[i];
|
||||||
|
arr[i] = arr[j];
|
||||||
|
arr[j] = tmp;
|
||||||
|
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
await asyncQuickSort(arr, compare, left, i - 1);
|
||||||
|
await asyncQuickSort(arr, compare, j + 1, right);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
|
@ -4,14 +4,6 @@ import type { ScriptObject } from "../plugs/index/script.ts";
|
||||||
import type { AppCommand, CommandDef } from "$lib/command.ts";
|
import type { AppCommand, CommandDef } from "$lib/command.ts";
|
||||||
import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
||||||
import * as syscalls from "@silverbulletmd/silverbullet/syscalls";
|
import * as syscalls from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import { LuaEnv, LuaNativeJSFunction } from "$common/space_lua/runtime.ts";
|
|
||||||
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
|
||||||
import { parse as parseLua } from "$common/space_lua/parse.ts";
|
|
||||||
import { evalStatement } from "$common/space_lua/eval.ts";
|
|
||||||
import { jsToLuaValue } from "$common/space_lua/runtime.ts";
|
|
||||||
import { LuaBuiltinFunction } from "$common/space_lua/runtime.ts";
|
|
||||||
import { LuaTable } from "$common/space_lua/runtime.ts";
|
|
||||||
import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
|
|
||||||
|
|
||||||
// @ts-ignore: Temporal polyfill
|
// @ts-ignore: Temporal polyfill
|
||||||
Date.prototype.toTemporalInstant = toTemporalInstant;
|
Date.prototype.toTemporalInstant = toTemporalInstant;
|
||||||
|
@ -145,92 +137,5 @@ export class ScriptEnvironment {
|
||||||
for (const script of allScripts) {
|
for (const script of allScripts) {
|
||||||
this.evalScript(script.script, system);
|
this.evalScript(script.script, system);
|
||||||
}
|
}
|
||||||
return this.loadLuaFromSystem(system);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadLuaFromSystem(system: System<any>) {
|
|
||||||
const allScripts: ScriptObject[] = await system.invokeFunction(
|
|
||||||
"index.queryObjects",
|
|
||||||
["space-lua", {}],
|
|
||||||
);
|
|
||||||
const env = new LuaEnv(luaBuildStandardEnv());
|
|
||||||
// Expose all syscalls to Lua
|
|
||||||
for (const [tl, value] of system.registeredSyscalls.entries()) {
|
|
||||||
const [ns, fn] = tl.split(".");
|
|
||||||
if (!env.get(ns)) {
|
|
||||||
env.set(ns, new LuaTable());
|
|
||||||
}
|
|
||||||
env.get(ns).set(
|
|
||||||
fn,
|
|
||||||
new LuaNativeJSFunction((...args) => {
|
|
||||||
return value.callback({}, ...args);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const sbApi = new LuaTable();
|
|
||||||
sbApi.set(
|
|
||||||
"register_command",
|
|
||||||
new LuaBuiltinFunction(
|
|
||||||
(def: LuaTable) => {
|
|
||||||
if (def.get(1) === undefined) {
|
|
||||||
throw new Error("Callback is required");
|
|
||||||
}
|
|
||||||
this.registerCommand(
|
|
||||||
def.toJSObject() as any,
|
|
||||||
async (...args: any[]) => {
|
|
||||||
try {
|
|
||||||
return await def.get(1).call(...args.map(jsToLuaValue));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Lua eval exception", e.message, e.context);
|
|
||||||
if (e.context && e.context.ref) {
|
|
||||||
const pageRef = parsePageRef(e.context.ref);
|
|
||||||
await system.localSyscall("editor.flashNotification", [
|
|
||||||
`Lua error: ${e.message}`,
|
|
||||||
"error",
|
|
||||||
]);
|
|
||||||
await system.localSyscall("editor.flashNotification", [
|
|
||||||
`Navigating to the place in the code where this error occurred in ${pageRef.page}`,
|
|
||||||
"info",
|
|
||||||
]);
|
|
||||||
await system.localSyscall("editor.navigate", [
|
|
||||||
{
|
|
||||||
page: pageRef.page,
|
|
||||||
pos: pageRef.pos + e.context.from +
|
|
||||||
"```space-lua\n".length,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
sbApi.set(
|
|
||||||
"register_function",
|
|
||||||
new LuaBuiltinFunction((def: LuaTable) => {
|
|
||||||
if (def.get(1) === undefined) {
|
|
||||||
throw new Error("Callback is required");
|
|
||||||
}
|
|
||||||
this.registerFunction(
|
|
||||||
def.toJSObject() as any,
|
|
||||||
(...args: any[]) => {
|
|
||||||
return def.get(1).call(...args.map(jsToLuaValue));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
env.set("silverbullet", sbApi);
|
|
||||||
for (const script of allScripts) {
|
|
||||||
try {
|
|
||||||
const ast = parseLua(script.script, { ref: script.ref });
|
|
||||||
await evalStatement(ast, env);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(
|
|
||||||
`Error evaluating script: ${e.message} for script: ${script.script}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log("Loaded", allScripts.length, "Lua scripts");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ import type {
|
||||||
} from "$common/space_lua/ast.ts";
|
} from "$common/space_lua/ast.ts";
|
||||||
import { evalExpression } from "$common/space_lua/eval.ts";
|
import { evalExpression } from "$common/space_lua/eval.ts";
|
||||||
import { MarkdownWidget } from "./markdown_widget.ts";
|
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||||
|
import { LuaRuntimeError } from "$common/space_lua/runtime.ts";
|
||||||
|
import { encodePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
|
import { resolveASTReference } from "$common/space_lua.ts";
|
||||||
|
|
||||||
export function luaDirectivePlugin(client: Client) {
|
export function luaDirectivePlugin(client: Client) {
|
||||||
return decoratorStateField((state: EditorState) => {
|
return decoratorStateField((state: EditorState) => {
|
||||||
|
@ -58,7 +61,19 @@ export function luaDirectivePlugin(client: Client) {
|
||||||
markdown: "" + result,
|
markdown: "" + result,
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Lua eval error", e);
|
if (e instanceof LuaRuntimeError) {
|
||||||
|
if (e.context.ref) {
|
||||||
|
const source = resolveASTReference(e.context);
|
||||||
|
if (source) {
|
||||||
|
// We know the origin node of the error, let's reference it
|
||||||
|
return {
|
||||||
|
markdown: `**Lua error:** ${e.message} (Origin: [[${
|
||||||
|
encodePageRef(source)
|
||||||
|
}]])`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
markdown: `**Lua error:** ${e.message}`,
|
markdown: `**Lua error:** ${e.message}`,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue