Lua: more fixes and work on stdlib

pull/1066/head
Zef Hemel 2024-10-08 19:53:09 +02:00
parent f74bab0aca
commit 899c2556cb
11 changed files with 555 additions and 239 deletions

View File

@ -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!,
};
}

View File

@ -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);

View File

@ -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 {

View File

@ -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")

View File

@ -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);
}); });

View File

@ -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 = [];
for (let i = 0; i < value.length; i++) {
result.push(luaValueToJS(value.get(i + 1)));
}
return result;
} else { } else {
return value.toJSObject(); 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;
} }

View File

@ -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");
});

View File

@ -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;
} }

View File

@ -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;
}

View File

@ -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");
} }
} }

View File

@ -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}`,
}; };