Skip to content

Commit

Permalink
Lua: more fixes and work on stdlib
Browse files Browse the repository at this point in the history
  • Loading branch information
zefhemel committed Oct 8, 2024
1 parent f74bab0 commit 899c255
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 239 deletions.
29 changes: 27 additions & 2 deletions common/space_lua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ 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";
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();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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}`,
);
Expand All @@ -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!,
};
}
22 changes: 13 additions & 9 deletions common/space_lua/eval.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
98 changes: 58 additions & 40 deletions common/space_lua/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
);
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
104 changes: 103 additions & 1 deletion common/space_lua/language_test.lua
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -17,6 +23,7 @@ assert("Hello " .. "world" == "Hello world")
function f1()
return 1
end

assert(f1() == 1)

function sqr(a)
Expand All @@ -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)

Expand Down Expand Up @@ -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")
Loading

0 comments on commit 899c255

Please sign in to comment.