From 899c2556cb8c5485d7f7361e0d9dc7f0ab17faca Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 8 Oct 2024 19:53:09 +0200 Subject: [PATCH] Lua: more fixes and work on stdlib --- common/space_lua.ts | 29 ++++- common/space_lua/eval.test.ts | 22 ++-- common/space_lua/eval.ts | 98 ++++++++------- common/space_lua/language_test.lua | 104 +++++++++++++++- common/space_lua/runtime.test.ts | 31 ++++- common/space_lua/runtime.ts | 99 ++++++++++------ common/space_lua/stdlib.test.ts | 40 ------- common/space_lua/stdlib.ts | 183 ++++++++++++++++++++++++++--- common/space_lua/util.ts | 76 ++++++++++++ common/space_script.ts | 95 --------------- web/cm_plugins/lua_directive.ts | 17 ++- 11 files changed, 555 insertions(+), 239 deletions(-) delete mode 100644 common/space_lua/stdlib.test.ts diff --git a/common/space_lua.ts b/common/space_lua.ts index bb2045b1..d0ccd1c5 100644 --- a/common/space_lua.ts +++ b/common/space_lua.ts @@ -4,6 +4,7 @@ import { LuaEnv, LuaFunction, LuaNativeJSFunction, + LuaRuntimeError, } from "$common/space_lua/runtime.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.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 { LuaBuiltinFunction } 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 { luaValueToJS } from "$common/space_lua/runtime.ts"; +import type { ASTCtx } from "$common/space_lua/ast.ts"; export class SpaceLuaEnvironment { env: LuaEnv = new LuaEnv(); @@ -53,7 +58,7 @@ export class SpaceLuaEnvironment { throw new Error("Callback is required"); } scriptEnv.registerCommand( - def.toJSObject() as any, + luaValueToJS(def) as any, async (...args: any[]) => { try { return await def.get(1).call(...args.map(jsToLuaValue)); @@ -93,6 +98,15 @@ export class SpaceLuaEnvironment { const scriptEnv = new LuaEnv(env); await evalStatement(ast, scriptEnv); } 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( `Error evaluating script: ${e.message} for script: ${script.script}`, ); @@ -111,3 +125,14 @@ export class SpaceLuaEnvironment { 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!, + }; +} diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index 3ef8602c..086510e7 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -1,5 +1,10 @@ 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 type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts"; import { evalExpression, evalStatement } from "./eval.ts"; @@ -40,25 +45,24 @@ Deno.test("Evaluator test", async () => { assertEquals(tbl.get(1), 3); assertEquals(tbl.get(2), 1); 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", age: 100, }); assertEquals( - (await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toJSObject(), + luaValueToJS(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)), { name: "Zef", age: 100, }, ); - assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toJSObject(), { - 5: 1, - ab: 2, - }); + 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); @@ -104,7 +108,7 @@ Deno.test("Statement evaluation", async () => { const env3 = new LuaEnv(); await evalBlock(`tbl = {1, 2, 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); assertEquals(env3.get("tbl").get("name"), "Zef"); await evalBlock(`tbl[2] = {age=10}`, env3); diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 0632b10d..9f054e4f 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -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 "FunctionCall": + case "TableAccess": + case "PropertyAccess": return evalPrefixExpression(e, env); case "TableConstructor": { const table = new LuaTable(); @@ -229,21 +199,66 @@ function evalPrefixExpression( } case "Parenthesized": return evalExpression(e.expression, env); + // <>[<>] + 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)); + } + } + // .property case "PropertyAccess": { const obj = evalPrefixExpression(e.object, env); if (obj instanceof Promise) { return obj.then((obj) => { if (!obj?.get) { - throw new Error( - `Attempting to index non-indexable object: ${obj}`, + throw new LuaRuntimeError( + `Attempting to index a nil value`, + e.object.ctx, ); } return obj.get(e.property); }); } else { if (!obj?.get) { - throw new Error( - `Attempting to index non-indexable object: ${obj}`, + throw new LuaRuntimeError( + `Attempting to index a nil value`, + e.object.ctx, ); } return obj.get(e.property); @@ -540,8 +555,9 @@ export async function evalStatement( for (let i = 0; i < propNames.length - 1; i++) { settable = settable.get(propNames[i]); if (!settable) { - throw new Error( + throw new LuaRuntimeError( `Cannot find property ${propNames[i]}`, + s.name.ctx, ); } } @@ -676,8 +692,9 @@ function evalLValue( if (objValue instanceof Promise) { return objValue.then((objValue) => { if (!objValue.set) { - throw new Error( + throw new LuaRuntimeError( `Not a settable object: ${objValue}`, + lval.object.ctx, ); } return { @@ -687,8 +704,9 @@ function evalLValue( }); } else { if (!objValue.set) { - throw new Error( + throw new LuaRuntimeError( `Not a settable object: ${objValue}`, + lval.object.ctx, ); } return { diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_test.lua index 55b2ea75..a361924d 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_test.lua @@ -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 assert(true, "True is true") @@ -17,6 +23,7 @@ assert("Hello " .. "world" == "Hello world") function f1() return 1 end + assert(f1() == 1) function sqr(a) @@ -42,6 +49,7 @@ assert(apply(sqr, 3) == 9) function multi_return() return 1, 2 end + local a, b = multi_return() assert(a == 1 and b == 2) @@ -200,4 +208,98 @@ assert(not deepCompare( -- String serialization assert(tostring({ 1, 2, 3 }) == "{1, 2, 3}") -assert(tostring({ a = 1, b = 2 }) == "{a = 1, b = 2}") \ No newline at end of file +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") diff --git a/common/space_lua/runtime.test.ts b/common/space_lua/runtime.test.ts index 817cfec9..2caf5f11 100644 --- a/common/space_lua/runtime.test.ts +++ b/common/space_lua/runtime.test.ts @@ -1,9 +1,12 @@ 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", () => { // Test LuaMultires - assertEquals(new LuaMultiRes([]).flatten().values, []); assertEquals(new LuaMultiRes([1, 2, 3]).flatten().values, [1, 2, 3]); assertEquals( @@ -14,4 +17,28 @@ Deno.test("Test Lua Rutime", () => { 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); }); diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 57b7fff5..e2061b8a 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -1,5 +1,6 @@ import type { ASTCtx, LuaFunctionBody } from "./ast.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; +import { asyncQuickSort } from "$common/space_lua/util.ts"; export type LuaType = | "nil" @@ -17,6 +18,7 @@ export type JSValue = any; export interface ILuaFunction { call(...args: LuaValue[]): Promise | LuaValue; + toString(): string; } export interface ILuaSettable { @@ -79,8 +81,8 @@ export class LuaMultiRes { } unwrap(): any { - if (this.values.length !== 1) { - throw new Error("Cannot unwrap multiple values"); + if (this.values.length === 0) { + return null; } return this.values[0]; } @@ -136,6 +138,10 @@ export class LuaFunction implements ILuaFunction { } }); } + + toString(): string { + return ``; + } } export class LuaNativeJSFunction implements ILuaFunction { @@ -151,6 +157,10 @@ export class LuaNativeJSFunction implements ILuaFunction { return jsToLuaValue(result); } } + + toString(): string { + return ``; + } } export class LuaBuiltinFunction implements ILuaFunction { @@ -160,6 +170,10 @@ export class LuaBuiltinFunction implements ILuaFunction { call(...args: LuaValue[]): Promise | LuaValue { return this.fn(...args); } + + toString(): string { + return ``; + } } export class LuaTable implements ILuaSettable, ILuaGettable { @@ -173,10 +187,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable { public metatable: LuaTable | null; - constructor() { + constructor(init?: any[] | Record) { // For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this) - this.stringKeys = {}; - this.arrayPart = []; + this.arrayPart = Array.isArray(init) ? init : []; + this.stringKeys = init && !Array.isArray(init) ? init : {}; this.otherKeys = null; // Only create this when needed this.metatable = null; } @@ -263,16 +277,22 @@ export class LuaTable implements ILuaSettable, ILuaGettable { return value; } - toJSArray(): JSValue[] { - return this.arrayPart; + insert(value: LuaValue, pos: number) { + this.arrayPart.splice(pos - 1, 0, value); + } + + remove(pos: number) { + this.arrayPart.splice(pos - 1, 1); } - toJSObject(): Record { - const result = { ...this.stringKeys }; - 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 { @@ -306,22 +326,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable { 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): LuaTable { - const table = new LuaTable(); - for (const key in obj) { - table.set(key, obj[key]); - } - return table; - } } 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 { if (obj instanceof LuaTable) { - return obj.toJSArray().length; + return obj.length; } else if (Array.isArray(obj)) { return obj.length; } else { @@ -365,7 +369,7 @@ export function luaTypeOf(val: any): LuaType { return "table"; } else if (Array.isArray(val)) { return "table"; - } else if (typeof val === "function") { + } else if (typeof val === "function" || val.call) { return "function"; } else { return "userdata"; @@ -423,25 +427,50 @@ export function jsToLuaValue(value: any): any { if (value instanceof LuaTable) { return 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") { - 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 { return value; } } +// Inverse of jsToLuaValue export function luaValueToJS(value: any): any { if (value instanceof Promise) { return value.then(luaValueToJS); } 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) { - return value.toJSArray(); + const result = []; + for (let i = 0; i < value.length; i++) { + result.push(luaValueToJS(value.get(i + 1))); + } + return result; } else { - return value.toJSObject(); + const result: Record = {}; + 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 { return value; } diff --git a/common/space_lua/stdlib.test.ts b/common/space_lua/stdlib.test.ts deleted file mode 100644 index 688d5340..00000000 --- a/common/space_lua/stdlib.test.ts +++ /dev/null @@ -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"); -}); diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts index d06e6198..4774e808 100644 --- a/common/space_lua/stdlib.ts +++ b/common/space_lua/stdlib.ts @@ -1,9 +1,9 @@ import { + type ILuaFunction, LuaBuiltinFunction, LuaEnv, LuaMultiRes, - LuaNativeJSFunction, - type LuaTable, + LuaTable, luaToString, luaTypeOf, type LuaValue, @@ -13,21 +13,21 @@ const printFunction = new LuaBuiltinFunction((...args) => { console.log("[Lua]", ...args.map(luaToString)); }); -const assertFunction = new LuaNativeJSFunction( - (value: any, message?: string) => { - if (!value) { +const assertFunction = new LuaBuiltinFunction( + async (value: any, message?: string) => { + if (!await value) { throw new Error(`Assertion failed: ${message}`); } }, ); -const ipairsFunction = new LuaNativeJSFunction((ar: any[]) => { - let i = 0; +const ipairsFunction = new LuaBuiltinFunction((ar: LuaTable) => { + let i = 1; return () => { - if (i >= ar.length) { + if (i > ar.length) { return; } - const result = new LuaMultiRes([i, ar[i]]); + const result = new LuaMultiRes([i, ar.get(i)]); i++; return result; }; @@ -41,14 +41,17 @@ const pairsFunction = new LuaBuiltinFunction((t: LuaTable) => { return; } const key = keys[i]; - const result = new LuaMultiRes([key, t.get(key)]); i++; - return result; + return new LuaMultiRes([key, t.get(key)]); }; }); 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 => { @@ -59,14 +62,34 @@ const tostringFunction = new LuaBuiltinFunction((value: any) => { return luaToString(value); }); -const tonumberFunction = new LuaNativeJSFunction((value: any) => { +const tonumberFunction = new LuaBuiltinFunction((value: LuaValue) => { return Number(value); }); -const errorFunction = new LuaNativeJSFunction((message: string) => { +const errorFunction = new LuaBuiltinFunction((message: string) => { 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( (table: LuaTable, metatable: LuaTable) => { table.metatable = metatable; @@ -85,6 +108,134 @@ const getmetatableFunction = new LuaBuiltinFunction((table: LuaTable) => { 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() { const env = new LuaEnv(); env.set("print", printFunction); @@ -95,9 +246,13 @@ export function luaBuildStandardEnv() { env.set("tostring", tostringFunction); env.set("tonumber", tonumberFunction); env.set("error", errorFunction); + env.set("pcall", pcallFunction); + env.set("xpcall", xpcallFunction); env.set("unpack", unpackFunction); env.set("setmetatable", setmetatableFunction); env.set("getmetatable", getmetatableFunction); env.set("rawset", rawsetFunction); + env.set("string", stringFunctions); + env.set("table", tableFunctions); return env; } diff --git a/common/space_lua/util.ts b/common/space_lua/util.ts index bf5f2a46..b00c1a50 100644 --- a/common/space_lua/util.ts +++ b/common/space_lua/util.ts @@ -14,3 +14,79 @@ export function evalPromiseValues(vals: any[]): Promise | any[] { 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, +) { + 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, + 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; +} diff --git a/common/space_script.ts b/common/space_script.ts index 79b8359a..d7b0d6a5 100644 --- a/common/space_script.ts +++ b/common/space_script.ts @@ -4,14 +4,6 @@ import type { ScriptObject } from "../plugs/index/script.ts"; import type { AppCommand, CommandDef } from "$lib/command.ts"; import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill"; 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 Date.prototype.toTemporalInstant = toTemporalInstant; @@ -145,92 +137,5 @@ export class ScriptEnvironment { for (const script of allScripts) { this.evalScript(script.script, system); } - return this.loadLuaFromSystem(system); - } - - async loadLuaFromSystem(system: System) { - 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"); } } diff --git a/web/cm_plugins/lua_directive.ts b/web/cm_plugins/lua_directive.ts index 31279b12..0521f3d9 100644 --- a/web/cm_plugins/lua_directive.ts +++ b/web/cm_plugins/lua_directive.ts @@ -15,6 +15,9 @@ import type { } from "$common/space_lua/ast.ts"; import { evalExpression } from "$common/space_lua/eval.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) { return decoratorStateField((state: EditorState) => { @@ -58,7 +61,19 @@ export function luaDirectivePlugin(client: Client) { markdown: "" + result, }; } 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 { markdown: `**Lua error:** ${e.message}`, };