Lua: more fixes and work on stdlib
parent
f74bab0aca
commit
899c2556cb
|
@ -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!,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
// <<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": {
|
||||
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 {
|
||||
|
|
|
@ -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}")
|
||||
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 { 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);
|
||||
});
|
||||
|
|
|
@ -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> | 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 `<lua function(${this.body.parameters.join(", ")})>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class LuaNativeJSFunction implements ILuaFunction {
|
||||
|
@ -151,6 +157,10 @@ export class LuaNativeJSFunction implements ILuaFunction {
|
|||
return jsToLuaValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<native js function: ${this.fn.name}>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class LuaBuiltinFunction implements ILuaFunction {
|
||||
|
@ -160,6 +170,10 @@ export class LuaBuiltinFunction implements ILuaFunction {
|
|||
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
||||
return this.fn(...args);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<builtin lua function>`;
|
||||
}
|
||||
}
|
||||
|
||||
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<string, any>) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
toJSObject(): Record<string, JSValue> {
|
||||
const result = { ...this.stringKeys };
|
||||
for (const i in this.arrayPart) {
|
||||
result[parseInt(i) + 1] = this.arrayPart[i];
|
||||
remove(pos: number) {
|
||||
this.arrayPart.splice(pos - 1, 1);
|
||||
}
|
||||
|
||||
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<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 };
|
||||
|
@ -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<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 {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -14,3 +14,79 @@ export function evalPromiseValues(vals: any[]): Promise<any[]> | 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<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 { 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<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";
|
||||
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}`,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue