First integration of Lua into the core (via space-lua code blocks)

pull/1110/head
Zef Hemel 2024-10-03 17:55:51 +02:00
parent c0a248daba
commit 3cf7b72ebb
22 changed files with 1123 additions and 492 deletions

View File

@ -76,6 +76,7 @@ export abstract class CommonSystem {
this.commandHook.throttledBuildAllCommands();
}
// Swap in the expanded function map
this.ds.functionMap = functions;
}

View File

@ -46,6 +46,7 @@ import {
} from "./markdown_parser/parser.ts";
import { cssLanguage } from "@codemirror/lang-css";
import { nixLanguage } from "@replit/codemirror-lang-nix";
import { luaLanguage } from "$common/space_lua/parse.ts";
const yamlStreamLanguage = StreamLanguage.define(yamlLanguage);
@ -120,6 +121,7 @@ export const builtinLanguages: Record<string, Language> = {
name: "query",
parser: highlightingQueryParser,
}),
"space-lua": luaLanguage,
"template": extendedMarkdownLanguage,
"expression": LRLanguage.define({
name: "expression",

View File

@ -3,6 +3,7 @@ import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts";
import { parse } from "./parse.ts";
import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } from "./eval.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
function evalExpr(s: string, e = new LuaEnv()): any {
return evalExpression(
@ -39,22 +40,22 @@ Deno.test("Evaluator test", async () => {
assertEquals(tbl.get(1), 3);
assertEquals(tbl.get(2), 1);
assertEquals(tbl.get(3), 2);
assertEquals(tbl.toArray(), [3, 1, 2]);
assertEquals(tbl.toJSArray(), [3, 1, 2]);
assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toObject(), {
assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toJSObject(), {
name: "Zef",
age: 100,
});
assertEquals(
(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toObject(),
(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toJSObject(),
{
name: "Zef",
age: 100,
},
);
assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toObject(), {
assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toJSObject(), {
5: 1,
ab: 2,
});
@ -68,6 +69,10 @@ Deno.test("Evaluator test", async () => {
// Function calls
assertEquals(singleResult(evalExpr(`test(3)`, env)), 3);
assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4);
// Function definitions
const fn = evalExpr(`function(a, b) return a + b end`);
assertEquals(fn.body.parameters, ["a", "b"]);
});
Deno.test("Statement evaluation", async () => {
@ -93,7 +98,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").toArray(), [3, 2, 3]);
assertEquals(env3.get("tbl").toJSArray(), [3, 2, 3]);
await evalBlock("tbl.name = 'Zef'", env3);
assertEquals(env3.get("tbl").get("name"), "Zef");
await evalBlock(`tbl[2] = {age=10}`, env3);
@ -105,7 +110,7 @@ Deno.test("Statement evaluation", async () => {
env4.set("print", new LuaNativeJSFunction(console.log));
await evalBlock(
`
a = 1
a = 1
do
-- sets global a to 3
a = 3
@ -198,4 +203,62 @@ Deno.test("Statement evaluation", async () => {
`,
env8,
);
// Local fucntion definition
const env9 = new LuaEnv();
env9.set("print", new LuaNativeJSFunction(console.log));
await evalBlock(
`
local function test(a)
return a + 1
end
print("3 + 1 = " .. test(3))
`,
env9,
);
// For loop over range
const env10 = new LuaEnv();
await evalBlock(
`
c = 0
for i = 1, 3 do
c = c + i
end
`,
env10,
);
assertEquals(env10.get("c"), 6);
// For loop over iterator
const env11 = new LuaEnv(luaBuildStandardEnv());
await evalBlock(
`
function fruits()
local list = { "apple", "banana", "cherry" }
-- Track index internally
local index = 0
return function()
index = index + 1
if list[index] then
return list[index]
end
end
end
for fruit in fruits() do
print("Fruit: " .. fruit)
end
`,
env11,
);
await evalBlock(
`
for _, f in ipairs({ "apple", "banana", "cherry" }) do
print("Fruit: " .. f)
end`,
luaBuildStandardEnv(),
);
});

View File

@ -1,466 +1,561 @@
import type {
LuaExpression,
LuaLValue,
LuaStatement,
LuaExpression,
LuaLValue,
LuaStatement,
} from "$common/space_lua/ast.ts";
import { evalPromiseValues } from "$common/space_lua/util.ts";
import {
type ILuaFunction,
type ILuaGettable,
type ILuaSettable,
LuaBreak,
LuaEnv,
LuaFunction,
luaGet,
luaLen,
type LuaLValueContainer,
LuaReturn,
LuaTable,
luaTruthy,
type LuaValue,
singleResult,
type ILuaFunction,
type ILuaGettable,
type ILuaSettable,
LuaBreak,
LuaEnv,
LuaFunction,
luaGet,
luaLen,
type LuaLValueContainer,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
LuaTable,
luaToString,
luaTruthy,
type LuaValue,
singleResult,
} from "./runtime.ts";
export function evalExpression(
e: LuaExpression,
env: LuaEnv,
e: LuaExpression,
env: LuaEnv,
): Promise<LuaValue> | LuaValue {
switch (e.type) {
case "String":
// TODO: Deal with escape sequences
return e.value;
case "Number":
return e.value;
case "Boolean":
return e.value;
case "Nil":
return null;
case "Binary": {
const values = evalPromiseValues([
evalExpression(e.left, env),
evalExpression(e.right, env),
]);
if (values instanceof Promise) {
return values.then(([left, right]) =>
luaOp(e.operator, singleResult(left), singleResult(right))
);
} else {
return luaOp(
e.operator,
singleResult(values[0]),
singleResult(values[1]),
);
}
}
case "Unary": {
const value = evalExpression(e.argument, env);
if (value instanceof Promise) {
return value.then((value) => {
switch (e.operator) {
case "-":
return -singleResult(value);
case "+":
return +singleResult(value);
case "not":
return !singleResult(value);
case "#":
return luaLen(singleResult(value));
default:
throw new Error(
`Unknown unary operator ${e.operator}`,
);
}
});
} else {
switch (e.operator) {
case "-":
return -singleResult(value);
case "+":
return +singleResult(value);
case "not":
return !singleResult(value);
case "#":
return luaLen(singleResult(value));
default:
throw new Error(
`Unknown unary operator ${e.operator}`,
);
}
}
}
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 "Variable":
case "FunctionCall":
return evalPrefixExpression(e, env);
case "TableConstructor": {
const table = new LuaTable();
const promises: Promise<void>[] = [];
for (const field of e.fields) {
switch (field.type) {
case "PropField": {
const value = evalExpression(field.value, env);
if (value instanceof Promise) {
promises.push(value.then((value) => {
table.set(
field.key,
singleResult(value),
);
}));
} else {
table.set(field.key, singleResult(value));
}
break;
}
case "DynamicField": {
const key = evalExpression(field.key, env);
const value = evalExpression(field.value, env);
if (
key instanceof Promise || value instanceof Promise
) {
promises.push(
Promise.all([
key instanceof Promise
? key
: Promise.resolve(key),
value instanceof Promise
? value
: Promise.resolve(value),
]).then(([key, value]) => {
table.set(
singleResult(key),
singleResult(value),
);
}),
);
} else {
table.set(
singleResult(key),
singleResult(value),
);
}
break;
}
case "ExpressionField": {
const value = evalExpression(field.value, env);
if (value instanceof Promise) {
promises.push(value.then((value) => {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
);
}));
} else {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
);
}
break;
}
}
}
if (promises.length > 0) {
return Promise.all(promises).then(() => table);
} else {
return table;
}
}
default:
throw new Error(`Unknown expression type ${e.type}`);
switch (e.type) {
case "String":
// TODO: Deal with escape sequences
return e.value;
case "Number":
return e.value;
case "Boolean":
return e.value;
case "Nil":
return null;
case "Binary": {
const values = evalPromiseValues([
evalExpression(e.left, env),
evalExpression(e.right, env),
]);
if (values instanceof Promise) {
return values.then(([left, right]) =>
luaOp(e.operator, singleResult(left), singleResult(right))
);
} else {
return luaOp(
e.operator,
singleResult(values[0]),
singleResult(values[1]),
);
}
}
case "Unary": {
const value = evalExpression(e.argument, env);
if (value instanceof Promise) {
return value.then((value) => {
switch (e.operator) {
case "-":
return -singleResult(value);
case "+":
return +singleResult(value);
case "not":
return !singleResult(value);
case "#":
return luaLen(singleResult(value));
default:
throw new Error(
`Unknown unary operator ${e.operator}`,
);
}
});
} else {
switch (e.operator) {
case "-":
return -singleResult(value);
case "+":
return +singleResult(value);
case "not":
return !singleResult(value);
case "#":
return luaLen(singleResult(value));
default:
throw new Error(
`Unknown unary operator ${e.operator}`,
);
}
}
}
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 "Variable":
case "FunctionCall":
return evalPrefixExpression(e, env);
case "TableConstructor": {
const table = new LuaTable();
const promises: Promise<void>[] = [];
for (const field of e.fields) {
switch (field.type) {
case "PropField": {
const value = evalExpression(field.value, env);
if (value instanceof Promise) {
promises.push(value.then((value) => {
table.set(
field.key,
singleResult(value),
);
}));
} else {
table.set(field.key, singleResult(value));
}
break;
}
case "DynamicField": {
const key = evalExpression(field.key, env);
const value = evalExpression(field.value, env);
if (
key instanceof Promise || value instanceof Promise
) {
promises.push(
Promise.all([
key instanceof Promise ? key : Promise.resolve(key),
value instanceof Promise ? value : Promise.resolve(value),
]).then(([key, value]) => {
table.set(
singleResult(key),
singleResult(value),
);
}),
);
} else {
table.set(
singleResult(key),
singleResult(value),
);
}
break;
}
case "ExpressionField": {
const value = evalExpression(field.value, env);
if (value instanceof Promise) {
promises.push(value.then((value) => {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
);
}));
} else {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
);
}
break;
}
}
}
if (promises.length > 0) {
return Promise.all(promises).then(() => table);
} else {
return table;
}
}
case "FunctionDefinition": {
return new LuaFunction(e.body, env);
}
default:
throw new Error(`Unknown expression type ${e.type}`);
}
}
function evalPrefixExpression(
e: LuaExpression,
env: LuaEnv,
e: LuaExpression,
env: LuaEnv,
): Promise<LuaValue> | LuaValue {
switch (e.type) {
case "Variable": {
const value = env.get(e.name);
if (value === undefined) {
throw new Error(`Undefined variable ${e.name}`);
} else {
return value;
}
}
case "Parenthesized":
return evalExpression(e.expression, env);
case "FunctionCall": {
const fn = evalPrefixExpression(e.prefix, env);
if (fn instanceof Promise) {
return fn.then((fn: ILuaFunction) => {
if (!fn.call) {
throw new Error(`Not a function: ${fn}`);
}
const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)),
);
if (args instanceof Promise) {
return args.then((args) => fn.call(...args));
} else {
return fn.call(...args);
}
});
} else {
if (!fn.call) {
throw new Error(`Not a function: ${fn}`);
}
const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)),
);
if (args instanceof Promise) {
return args.then((args) => fn.call(...args));
} else {
return fn.call(...args);
}
}
}
default:
throw new Error(`Unknown prefix expression type ${e.type}`);
switch (e.type) {
case "Variable": {
const value = env.get(e.name);
if (value === undefined) {
throw new Error(`Undefined variable ${e.name}`);
} else {
return value;
}
}
case "Parenthesized":
return evalExpression(e.expression, env);
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 "FunctionCall": {
const fn = evalPrefixExpression(e.prefix, env);
if (fn instanceof Promise) {
return fn.then((fn: ILuaFunction) => {
if (!fn.call) {
throw new Error(`Not a function: ${fn}`);
}
const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)),
);
if (args instanceof Promise) {
return args.then((args) => fn.call(...args));
} else {
return fn.call(...args);
}
});
} else {
if (!fn.call) {
throw new Error(`Not a function: ${fn}`);
}
const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)),
);
if (args instanceof Promise) {
return args.then((args) => fn.call(...args));
} else {
return fn.call(...args);
}
}
}
default:
throw new Error(`Unknown prefix expression type ${e.type}`);
}
}
// TODO: Handle metatables and possibly do type checking
function luaOp(op: string, left: any, right: any): any {
switch (op) {
case "+":
return left + right;
case "-":
return left - right;
case "*":
return left * right;
case "/":
return left / right;
case "//":
return Math.floor(left / right);
case "%":
return left % right;
case "^":
return left ** right;
case "..":
return left + right;
case "==":
return left === right;
case "~=":
case "!=":
case "/=":
return left !== right;
case "<":
return left < right;
case "<=":
return left <= right;
case ">":
return left > right;
case ">=":
return left >= right;
case "and":
return left && right;
case "or":
return left || right;
default:
throw new Error(`Unknown operator ${op}`);
}
switch (op) {
case "+":
return left + right;
case "-":
return left - right;
case "*":
return left * right;
case "/":
return left / right;
case "//":
return Math.floor(left / right);
case "%":
return left % right;
case "^":
return left ** right;
case "..":
return luaToString(left) + luaToString(right);
case "==":
return left === right;
case "~=":
case "!=":
case "/=":
return left !== right;
case "<":
return left < right;
case "<=":
return left <= right;
case ">":
return left > right;
case ">=":
return left >= right;
case "and":
return left && right;
case "or":
return left || right;
default:
throw new Error(`Unknown operator ${op}`);
}
}
export async function evalStatement(
s: LuaStatement,
env: LuaEnv,
s: LuaStatement,
env: LuaEnv,
): Promise<void> {
switch (s.type) {
case "Assignment": {
const values = await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)),
);
const lvalues = await evalPromiseValues(s.variables
.map((lval) => evalLValue(lval, env)));
switch (s.type) {
case "Assignment": {
const values = await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)),
);
const lvalues = await evalPromiseValues(s.variables
.map((lval) => evalLValue(lval, env)));
for (let i = 0; i < lvalues.length; i++) {
lvalues[i].env.set(lvalues[i].key, values[i]);
}
for (let i = 0; i < lvalues.length; i++) {
lvalues[i].env.set(lvalues[i].key, values[i]);
}
break;
}
case "Local": {
for (let i = 0; i < s.names.length; i++) {
if (!s.expressions || s.expressions[i] === undefined) {
env.setLocal(s.names[i].name, null);
} else {
const value = await evalExpression(s.expressions[i], env);
env.setLocal(s.names[i].name, value);
}
}
break;
}
case "Semicolon":
break;
case "Label":
case "Goto":
throw new Error("Labels and gotos are not supported yet");
case "Block": {
const newEnv = new LuaEnv(env);
for (const statement of s.statements) {
await evalStatement(statement, newEnv);
}
break;
}
case "If": {
for (const cond of s.conditions) {
if (luaTruthy(await evalExpression(cond.condition, env))) {
return evalStatement(cond.block, env);
}
}
if (s.elseBlock) {
return evalStatement(s.elseBlock, env);
}
break;
}
case "While": {
while (luaTruthy(await evalExpression(s.condition, env))) {
try {
await evalStatement(s.block, env);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
case "Repeat": {
do {
try {
await evalStatement(s.block, env);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
} while (!luaTruthy(await evalExpression(s.condition, env)));
break;
}
case "Break":
throw new LuaBreak();
case "FunctionCallStatement": {
return evalExpression(s.call, env);
}
case "Function": {
let body = s.body;
let propNames = s.name.propNames;
if (s.name.colonName) {
// function hello:there() -> function hello.there(self) transformation
body = {
...s.body,
parameters: ["self", ...s.body.parameters],
};
propNames = [...s.name.propNames, s.name.colonName];
}
let settable: ILuaSettable & ILuaGettable = env;
for (let i = 0; i < propNames.length - 1; i++) {
settable = settable.get(propNames[i]);
if (!settable) {
throw new Error(
`Cannot find property ${propNames[i]}`,
);
}
}
settable.set(
propNames[propNames.length - 1],
new LuaFunction(body, env),
);
break;
}
case "Return": {
throw new LuaReturn(
await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)),
),
);
}
default:
throw new Error(`Unknown statement type ${s.type}`);
break;
}
case "Local": {
for (let i = 0; i < s.names.length; i++) {
if (!s.expressions || s.expressions[i] === undefined) {
env.setLocal(s.names[i].name, null);
} else {
const value = await evalExpression(s.expressions[i], env);
env.setLocal(s.names[i].name, value);
}
}
break;
}
case "Semicolon":
break;
case "Label":
case "Goto":
throw new Error("Labels and gotos are not supported yet");
case "Block": {
const newEnv = new LuaEnv(env);
for (const statement of s.statements) {
await evalStatement(statement, newEnv);
}
break;
}
case "If": {
for (const cond of s.conditions) {
if (luaTruthy(await evalExpression(cond.condition, env))) {
return evalStatement(cond.block, env);
}
}
if (s.elseBlock) {
return evalStatement(s.elseBlock, env);
}
break;
}
case "While": {
while (luaTruthy(await evalExpression(s.condition, env))) {
try {
await evalStatement(s.block, env);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
case "Repeat": {
do {
try {
await evalStatement(s.block, env);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
} while (!luaTruthy(await evalExpression(s.condition, env)));
break;
}
case "Break":
throw new LuaBreak();
case "FunctionCallStatement": {
return evalExpression(s.call, env);
}
case "Function": {
let body = s.body;
let propNames = s.name.propNames;
if (s.name.colonName) {
// function hello:there() -> function hello.there(self) transformation
body = {
...s.body,
parameters: ["self", ...s.body.parameters],
};
propNames = [...s.name.propNames, s.name.colonName];
}
let settable: ILuaSettable & ILuaGettable = env;
for (let i = 0; i < propNames.length - 1; i++) {
settable = settable.get(propNames[i]);
if (!settable) {
throw new Error(
`Cannot find property ${propNames[i]}`,
);
}
}
settable.set(
propNames[propNames.length - 1],
new LuaFunction(body, env),
);
break;
}
case "LocalFunction": {
env.setLocal(
s.name,
new LuaFunction(s.body, env),
);
break;
}
case "Return": {
// A return statement for now is implemented by throwing the value as an exception, this should
// be optimized for the common case later
throw new LuaReturn(
await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)),
),
);
}
case "For": {
const start = await evalExpression(s.start, env);
const end = await evalExpression(s.end, env);
const step = s.step ? await evalExpression(s.step, env) : 1;
const localEnv = new LuaEnv(env);
for (
let i = start;
step > 0 ? i <= end : i >= end;
i += step
) {
localEnv.setLocal(s.name, i);
try {
await evalStatement(s.block, localEnv);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
case "ForIn": {
const iteratorMultiRes = new LuaMultiRes(
await evalPromiseValues(
s.expressions.map((e) => evalExpression(e, env)),
),
).flatten();
const iteratorFunction: ILuaFunction | undefined =
iteratorMultiRes.values[0];
if (!iteratorFunction?.call) {
console.error("Cannot iterate over", iteratorMultiRes.values[0]);
throw new LuaRuntimeError(
`Cannot iterate over ${iteratorMultiRes.values[0]}`,
s,
);
}
const state: LuaValue = iteratorMultiRes.values[1] || null;
const control: LuaValue = iteratorMultiRes.values[2] || null;
while (true) {
const iterResult = new LuaMultiRes(
await iteratorFunction.call(state, control),
).flatten();
if (
iterResult.values[0] === null || iterResult.values[0] === undefined
) {
break;
}
const localEnv = new LuaEnv(env);
for (let i = 0; i < s.names.length; i++) {
localEnv.setLocal(s.names[i], iterResult.values[i]);
}
try {
await evalStatement(s.block, localEnv);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
// default:
// throw new Error(`Unknown statement type ${s.type}`);
}
}
function evalLValue(
lval: LuaLValue,
env: LuaEnv,
lval: LuaLValue,
env: LuaEnv,
): LuaLValueContainer | Promise<LuaLValueContainer> {
switch (lval.type) {
case "Variable":
return { env, key: lval.name };
case "TableAccess": {
const objValue = evalExpression(
lval.object,
env,
);
const keyValue = evalExpression(lval.key, env);
if (
objValue instanceof Promise ||
keyValue instanceof Promise
) {
return Promise.all([
objValue instanceof Promise
? objValue
: Promise.resolve(objValue),
keyValue instanceof Promise
? keyValue
: Promise.resolve(keyValue),
]).then(([objValue, keyValue]) => ({
env: singleResult(objValue),
key: singleResult(keyValue),
}));
} else {
return {
env: singleResult(objValue),
key: singleResult(keyValue),
};
}
}
case "PropertyAccess": {
const objValue = evalExpression(
lval.object,
env,
);
if (objValue instanceof Promise) {
return objValue.then((objValue) => {
if (!objValue.set) {
throw new Error(
`Not a settable object: ${objValue}`,
);
}
return {
env: objValue,
key: lval.property,
};
});
} else {
if (!objValue.set) {
throw new Error(
`Not a settable object: ${objValue}`,
);
}
return {
env: objValue,
key: lval.property,
};
}
}
switch (lval.type) {
case "Variable":
return { env, key: lval.name };
case "TableAccess": {
const objValue = evalExpression(
lval.object,
env,
);
const keyValue = evalExpression(lval.key, env);
if (
objValue instanceof Promise ||
keyValue instanceof Promise
) {
return Promise.all([
objValue instanceof Promise ? objValue : Promise.resolve(objValue),
keyValue instanceof Promise ? keyValue : Promise.resolve(keyValue),
]).then(([objValue, keyValue]) => ({
env: singleResult(objValue),
key: singleResult(keyValue),
}));
} else {
return {
env: singleResult(objValue),
key: singleResult(keyValue),
};
}
}
case "PropertyAccess": {
const objValue = evalExpression(
lval.object,
env,
);
if (objValue instanceof Promise) {
return objValue.then((objValue) => {
if (!objValue.set) {
throw new Error(
`Not a settable object: ${objValue}`,
);
}
return {
env: objValue,
key: lval.property,
};
});
} else {
if (!objValue.set) {
throw new Error(
`Not a settable object: ${objValue}`,
);
}
return {
env: objValue,
key: lval.property,
};
}
}
}
}

View File

@ -133,7 +133,7 @@ TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" }
@tokens {
CompareOp { "<" | ">" | $[<>=~/!] "=" }
word { std.asciiLetter (std.digit | std.asciiLetter)* }
word { (std.asciiLetter | "_") (std.digit | std.asciiLetter | "_")* }
identifier { word }

View File

@ -13,7 +13,7 @@ export const parser = LRParser.deserialize({
],
skippedNodes: [0,1],
repeatNodeCount: 9,
tokenData: "7X~RtXY#cYZ#}[]#c]^$[pq#cqr$drs$ost)ruv)wvw)|wx*Rxy/Pyz/Uz{/Z{|/`|}/e}!O/l!O!P0`!P!Q0u!Q!R1V!R![2k![!]4o!]!^4|!^!_5T!_!`5g!`!a5o!c!}6R!}#O6a#O#P#t#P#Q6f#Q#R6k#T#o6R#o#p6p#p#q6u#q#r6z#r#s7P~#hS#V~XY#c[]#cpq#c#O#P#t~#wQYZ#c]^#c~$SP#U~]^$V~$[O#U~~$aP#U~YZ$VT$gP!_!`$jT$oOzT~$rWOY%[Z]%[^r%[s#O%[#O#P&P#P;'S%[;'S;=`(a<%lO%[~%_XOY%[Z]%[^r%[rs%zs#O%[#O#P&P#P;'S%[;'S;=`(a<%lO%[~&PO#Z~~&SZrs%[wx%[!Q![&u#O#P%[#T#U%[#U#V%[#Y#Z%[#b#c%[#i#j(g#l#m)Y#n#o%[~&xZOY%[Z]%[^r%[rs%zs!Q%[!Q!['k![#O%[#O#P&P#P;'S%[;'S;=`(a<%lO%[~'nZOY%[Z]%[^r%[rs%zs!Q%[!Q![%[![#O%[#O#P&P#P;'S%[;'S;=`(a<%lO%[~(dP;=`<%l%[~(jP#o#p(m~(pR!Q![(y!c!i(y#T#Z(y~(|S!Q![(y!c!i(y#T#Z(y#q#r%[~)]R!Q![)f!c!i)f#T#Z)f~)iR!Q![%[!c!i%[#T#Z%[~)wO#p~~)|O#m~~*RO#e~~*UWOY*nZ]*n^w*nx#O*n#O#P+^#P;'S*n;'S;=`-n<%lO*n~*qXOY*nZ]*n^w*nwx%zx#O*n#O#P+^#P;'S*n;'S;=`-n<%lO*n~+aZrs*nwx*n!Q![,S#O#P*n#T#U*n#U#V*n#Y#Z*n#b#c*n#i#j-t#l#m.g#n#o*n~,VZOY*nZ]*n^w*nwx%zx!Q*n!Q![,x![#O*n#O#P+^#P;'S*n;'S;=`-n<%lO*n~,{ZOY*nZ]*n^w*nwx%zx!Q*n!Q![*n![#O*n#O#P+^#P;'S*n;'S;=`-n<%lO*n~-qP;=`<%l*n~-wP#o#p-z~-}R!Q![.W!c!i.W#T#Z.W~.ZS!Q![.W!c!i.W#T#Z.W#q#r*n~.jR!Q![.s!c!i.s#T#Z.s~.vR!Q![*n!c!i*n#T#Z*n~/UOl~~/ZOm~~/`O#k~~/eO#i~V/lOvR#aS~/qP#j~}!O/t~/yTP~OY/tZ]/t^;'S/t;'S;=`0Y<%lO/t~0]P;=`<%l/tV0ePgT!O!P0hV0mP!PT!O!P0pQ0uOcQ~0zQ#l~!P!Q1Q!_!`$j~1VO#n~~1[Ud~!O!P1n!Q![2k!g!h2S!z!{2|#X#Y2S#l#m2|~1qP!Q![1t~1yRd~!Q![1t!g!h2S#X#Y2S~2VQ{|2]}!O2]~2`P!Q![2c~2hPd~!Q![2c~2pSd~!O!P1n!Q![2k!g!h2S#X#Y2S~3PR!Q![3Y!c!i3Y#T#Z3Y~3_Ud~!O!P3q!Q![3Y!c!i3Y!r!s4c#T#Z3Y#d#e4c~3tR!Q![3}!c!i3}#T#Z3}~4STd~!Q![3}!c!i3}!r!s4c#T#Z3}#d#e4c~4fR{|2]}!O2]!P!Q2]~4tPo~![!]4w~4|OU~V5TOSR#aSV5[Q#uQzT!^!_5b!_!`$jT5gO#gT~5lP#`~!_!`$jV5vQ#vQzT!_!`$j!`!a5|T6RO#hT~6WR#X~!Q![6R!c!}6R#T#o6R~6fOi~~6kOj~~6pO#o~~6uOq~~6zO#d~~7POu~~7UP#f~!_!`$j",
tokenData: "7_~RuXY#fYZ$Q[]#f]^$_pq#fqr$grs$rst)uuv)zvw*Pwx*Uxy/Syz/Xz{/^{|/c|}/h}!O/o!O!P0c!P!Q0x!Q!R1Y!R![2n![!]4r!]!^5P!^!_5W!_!`5j!`!a5r!c!}6U!}#O6g#O#P#w#P#Q6l#Q#R6q#R#S6U#T#o6U#o#p6v#p#q6{#q#r7Q#r#s7V~#kS#V~XY#f[]#fpq#f#O#P#w~#zQYZ#f]^#f~$VP#U~]^$Y~$_O#U~~$dP#U~YZ$YT$jP!_!`$mT$rOzT~$uWOY%_Z]%_^r%_s#O%_#O#P&S#P;'S%_;'S;=`(d<%lO%_~%bXOY%_Z]%_^r%_rs%}s#O%_#O#P&S#P;'S%_;'S;=`(d<%lO%_~&SO#Z~~&VZrs%_wx%_!Q![&x#O#P%_#T#U%_#U#V%_#Y#Z%_#b#c%_#i#j(j#l#m)]#n#o%_~&{ZOY%_Z]%_^r%_rs%}s!Q%_!Q!['n![#O%_#O#P&S#P;'S%_;'S;=`(d<%lO%_~'qZOY%_Z]%_^r%_rs%}s!Q%_!Q![%_![#O%_#O#P&S#P;'S%_;'S;=`(d<%lO%_~(gP;=`<%l%_~(mP#o#p(p~(sR!Q![(|!c!i(|#T#Z(|~)PS!Q![(|!c!i(|#T#Z(|#q#r%_~)`R!Q![)i!c!i)i#T#Z)i~)lR!Q![%_!c!i%_#T#Z%_~)zO#p~~*PO#m~~*UO#e~~*XWOY*qZ]*q^w*qx#O*q#O#P+a#P;'S*q;'S;=`-q<%lO*q~*tXOY*qZ]*q^w*qwx%}x#O*q#O#P+a#P;'S*q;'S;=`-q<%lO*q~+dZrs*qwx*q!Q![,V#O#P*q#T#U*q#U#V*q#Y#Z*q#b#c*q#i#j-w#l#m.j#n#o*q~,YZOY*qZ]*q^w*qwx%}x!Q*q!Q![,{![#O*q#O#P+a#P;'S*q;'S;=`-q<%lO*q~-OZOY*qZ]*q^w*qwx%}x!Q*q!Q![*q![#O*q#O#P+a#P;'S*q;'S;=`-q<%lO*q~-tP;=`<%l*q~-zP#o#p-}~.QR!Q![.Z!c!i.Z#T#Z.Z~.^S!Q![.Z!c!i.Z#T#Z.Z#q#r*q~.mR!Q![.v!c!i.v#T#Z.v~.yR!Q![*q!c!i*q#T#Z*q~/XOl~~/^Om~~/cO#k~~/hO#i~V/oOvR#aS~/tP#j~}!O/w~/|TP~OY/wZ]/w^;'S/w;'S;=`0]<%lO/w~0`P;=`<%l/wV0hPgT!O!P0kV0pP!PT!O!P0sQ0xOcQ~0}Q#l~!P!Q1T!_!`$m~1YO#n~~1_Ud~!O!P1q!Q![2n!g!h2V!z!{3P#X#Y2V#l#m3P~1tP!Q![1w~1|Rd~!Q![1w!g!h2V#X#Y2V~2YQ{|2`}!O2`~2cP!Q![2f~2kPd~!Q![2f~2sSd~!O!P1q!Q![2n!g!h2V#X#Y2V~3SR!Q![3]!c!i3]#T#Z3]~3bUd~!O!P3t!Q![3]!c!i3]!r!s4f#T#Z3]#d#e4f~3wR!Q![4Q!c!i4Q#T#Z4Q~4VTd~!Q![4Q!c!i4Q!r!s4f#T#Z4Q#d#e4f~4iR{|2`}!O2`!P!Q2`~4wPo~![!]4z~5POU~V5WOSR#aSV5_Q#uQzT!^!_5e!_!`$mT5jO#gT~5oP#`~!_!`$mV5yQ#vQzT!_!`$m!`!a6PT6UO#hT~6ZS#X~!Q![6U!c!}6U#R#S6U#T#o6U~6lOi~~6qOj~~6vO#o~~6{Oq~~7QO#d~~7VOu~~7[P#f~!_!`$m",
tokenizers: [0, 1, 2],
topRules: {"Chunk":[0,2]},
dynamicPrecedences: {"110":1},

View File

@ -25,6 +25,8 @@ Deno.test("Test Lua parser", () => {
parse(`e({1 ; 2 ; 3})`);
parse(`e({a = 1, b = 2, c = 3})`);
parse(`e({[3] = 1, [10 * 10] = "sup"})`);
parse(`e(tbl.name)`);
parse(`e(tbl["name" + 10])`);
// Function calls
parse(`e(func(), func(1, 2, 3), a.b(), a.b.c:hello(), (a.b)(7))`);
@ -81,5 +83,13 @@ Deno.test("Test Lua parser", () => {
parse(`return`);
parse(`return 1`);
parse(`return 1, 2, 3`);
// return;
});
Deno.test("Test comment handling", () => {
parse(`
-- Single line comment
--[[ Multi
line
comment ]]
f()`);
});

View File

@ -5,6 +5,7 @@ import {
} from "@silverbulletmd/silverbullet/lib/tree";
import { parser } from "./parse-lua.js";
import { styleTags } from "@lezer/highlight";
import { indentNodeProp, LRLanguage } from "@codemirror/language";
import type {
LuaAttName,
LuaBlock,
@ -17,25 +18,36 @@ import type {
LuaStatement,
LuaTableField,
} from "./ast.ts";
import { tags as t } from "@lezer/highlight";
const luaStyleTags = styleTags({
// Identifier: t.variableName,
// TagIdentifier: t.variableName,
// GlobalIdentifier: t.variableName,
// String: t.string,
// Number: t.number,
// PageRef: ct.WikiLinkTag,
// BinExpression: t.operator,
// TernaryExpression: t.operator,
// Regex: t.regexp,
// "where limit select render Order OrderKW and or null as InKW NotKW BooleanKW each all":
// t.keyword,
Name: t.variableName,
LiteralString: t.string,
Number: t.number,
CompareOp: t.operator,
"true false": t.bool,
Comment: t.lineComment,
"return break goto do end while repeat until function local if then else elseif in for nil or and not":
t.keyword,
});
export const highlightingQueryParser = parser.configure({
props: [
luaStyleTags,
],
const customIndent = indentNodeProp.add({
"IfStatement FuncBody WhileStatement ForStatement TableConstructor": (
context,
) => {
return context.lineIndent(context.node.from) + context.unit;
},
});
// Use the customIndent in your language support
export const luaLanguage = LRLanguage.define({
name: "space-lua",
parser: parser.configure({
props: [
luaStyleTags,
customIndent,
],
}),
});
function parseChunk(t: ParseTree): LuaBlock {
@ -100,7 +112,6 @@ function parseStatement(t: ParseTree): LuaStatement {
}[] = [];
let elseBlock: LuaBlock | undefined = undefined;
for (let i = 0; i < t.children!.length; i += 4) {
console.log("Looking at", t.children![i]);
const child = t.children![i];
if (
child.children![0].text === "if" ||
@ -350,6 +361,15 @@ function parseExpression(t: ParseTree): LuaExpression {
to: t.to,
};
case "MemberExpression":
return {
type: "TableAccess",
object: parsePrefixExpression(t.children![0]),
key: parseExpression(t.children![2]),
from: t.from,
to: t.to,
};
case "Parens":
return parseExpression(t.children![1]);
case "FunctionCall": {
@ -411,7 +431,6 @@ function parseExpression(t: ParseTree): LuaExpression {
}
function parseFunctionArgs(ts: ParseTree[]): LuaExpression[] {
console.log("Parsing function args", JSON.stringify(ts, null, 2));
return ts.filter((t) => ![",", "(", ")"].includes(t.type!)).map(
parseExpression,
);
@ -502,11 +521,61 @@ function parseTableField(t: ParseTree): LuaTableField {
}
}
function stripLuaComments(s: string): string {
// Strips Lua comments (single-line and multi-line) and replaces them with equivalent length whitespace
let result = "";
let inString = false;
let inComment = false;
let inMultilineComment = false;
for (let i = 0; i < s.length; i++) {
// Handle string detection (to avoid stripping comments inside strings)
if (s[i] === '"' && !inComment && !inMultilineComment) {
inString = !inString;
}
// Handle single-line comments (starting with "--")
if (!inString && !inMultilineComment && s[i] === "-" && s[i + 1] === "-") {
if (s[i + 2] === "[" && s[i + 3] === "[") {
// Detect multi-line comment start "--[["
inMultilineComment = true;
i += 3; // Skip over "--[["
result += " "; // Add equivalent length spaces for "--[["
continue;
} else {
inComment = true;
}
}
// Handle end of single-line comment
if (inComment && s[i] === "\n") {
inComment = false;
}
// Handle multi-line comment ending "]]"
if (inMultilineComment && s[i] === "]" && s[i + 1] === "]") {
inMultilineComment = false;
i += 1; // Skip over "]]"
result += " "; // Add equivalent length spaces for "]]"
continue;
}
// Replace comment content with spaces, or copy original content if not in comment
if (inComment || inMultilineComment) {
result += " "; // Replace comment characters with a space
} else {
result += s[i];
}
}
return result;
}
export function parse(s: string): LuaBlock {
const t = parseToCrudeAST(s);
console.log("Clean tree", JSON.stringify(t, null, 2));
const t = parseToCrudeAST(stripLuaComments(s));
// console.log("Clean tree", JSON.stringify(t, null, 2));
const result = parseChunk(t);
console.log("Parsed AST", JSON.stringify(result, null, 2));
// console.log("Parsed AST", JSON.stringify(result, null, 2));
return result;
}

View File

@ -0,0 +1,17 @@
import { assertEquals } from "@std/assert/equals";
import { 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(
new LuaMultiRes([1, new LuaMultiRes([2, 3])]).flatten().values,
[
1,
2,
3,
],
);
});

View File

@ -1,6 +1,32 @@
import type { LuaFunctionBody } from "./ast.ts";
import { evalStatement } from "$common/space_lua/eval.ts";
export type LuaType =
| "nil"
| "boolean"
| "number"
| "string"
| "table"
| "function"
| "userdata"
| "thread";
// These types are for documentation only
export type LuaValue = any;
export type JSValue = any;
export interface ILuaFunction {
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
}
export interface ILuaSettable {
set(key: LuaValue, value: LuaValue): void;
}
export interface ILuaGettable {
get(key: LuaValue): LuaValue | undefined;
}
export class LuaEnv implements ILuaSettable, ILuaGettable {
variables = new Map<string, LuaValue>();
@ -31,7 +57,14 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
}
export class LuaMultiRes {
constructor(readonly values: any[]) {
values: any[];
constructor(values: LuaValue[] | LuaValue) {
if (values instanceof LuaMultiRes) {
this.values = values.values;
} else {
this.values = Array.isArray(values) ? values : [values];
}
}
unwrap(): any {
@ -40,6 +73,19 @@ export class LuaMultiRes {
}
return this.values[0];
}
// Takes an array of either LuaMultiRes or LuaValue and flattens them into a single LuaMultiRes
flatten(): LuaMultiRes {
const result: any[] = [];
for (const value of this.values) {
if (value instanceof LuaMultiRes) {
result.push(...value.values);
} else {
result.push(value);
}
}
return new LuaMultiRes(result);
}
}
export function singleResult(value: any): any {
@ -50,22 +96,6 @@ export function singleResult(value: any): any {
}
}
// These types are for documentation only
export type LuaValue = any;
export type JSValue = any;
export interface ILuaFunction {
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
}
export interface ILuaSettable {
set(key: LuaValue, value: LuaValue): void;
}
export interface ILuaGettable {
get(key: LuaValue): LuaValue | undefined;
}
export class LuaFunction implements ILuaFunction {
constructor(private body: LuaFunctionBody, private closure: LuaEnv) {
}
@ -101,6 +131,7 @@ export class LuaNativeJSFunction implements ILuaFunction {
constructor(readonly fn: (...args: JSValue[]) => JSValue) {
}
// Performs automatic conversion between Lua and JS values
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
const result = this.fn(...args.map(luaValueToJS));
if (result instanceof Promise) {
@ -111,6 +142,15 @@ export class LuaNativeJSFunction implements ILuaFunction {
}
}
export class LuaBuiltinFunction implements ILuaFunction {
constructor(readonly fn: (...args: LuaValue[]) => LuaValue) {
}
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
return this.fn(...args);
}
}
export class LuaTable implements ILuaSettable, ILuaGettable {
// To optimize the table implementation we use a combination of different data structures
// When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
@ -135,6 +175,22 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return this.arrayPart.length;
}
keys(): any[] {
const keys: any[] = Object.keys(this.stringKeys);
for (let i = 0; i < this.arrayPart.length; i++) {
keys.push(i + 1);
}
for (const key of Object.keys(this.stringKeys)) {
keys.push(key);
}
if (this.otherKeys) {
for (const key of this.otherKeys.keys()) {
keys.push(key);
}
}
return keys;
}
set(key: LuaValue, value: LuaValue) {
if (typeof key === "string") {
this.stringKeys[key] = value;
@ -159,11 +215,11 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return undefined;
}
toArray(): JSValue[] {
toJSArray(): JSValue[] {
return this.arrayPart;
}
toObject(): Record<string, JSValue> {
toJSObject(): Record<string, JSValue> {
const result = { ...this.stringKeys };
for (const i in this.arrayPart) {
result[parseInt(i) + 1] = this.arrayPart[i];
@ -171,7 +227,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return result;
}
static fromArray(arr: JSValue[]): LuaTable {
static fromJSArray(arr: JSValue[]): LuaTable {
const table = new LuaTable();
for (let i = 0; i < arr.length; i++) {
table.set(i + 1, arr[i]);
@ -179,7 +235,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return table;
}
static fromObject(obj: Record<string, JSValue>): LuaTable {
static fromJSObject(obj: Record<string, JSValue>): LuaTable {
const table = new LuaTable();
for (const key in obj) {
table.set(key, obj[key]);
@ -208,7 +264,7 @@ export function luaGet(obj: any, key: any): any {
export function luaLen(obj: any): number {
if (obj instanceof LuaTable) {
return obj.toArray().length;
return obj.toJSArray().length;
} else if (Array.isArray(obj)) {
return obj.length;
} else {
@ -216,6 +272,27 @@ export function luaLen(obj: any): number {
}
}
export function luaTypeOf(val: any): LuaType {
if (val === null || val === undefined) {
return "nil";
} else if (typeof val === "boolean") {
return "boolean";
} else if (typeof val === "number") {
return "number";
} else if (typeof val === "string") {
return "string";
} else if (val instanceof LuaTable) {
return "table";
} else if (Array.isArray(val)) {
return "table";
} else if (typeof val === "function") {
return "function";
} else {
return "userdata";
}
}
// Both `break` and `return` are implemented by exception throwing
export class LuaBreak extends Error {
}
@ -225,6 +302,19 @@ export class LuaReturn extends Error {
}
}
export class LuaRuntimeError extends Error {
constructor(
readonly message: string,
readonly astNode: { from?: number; to?: number },
) {
super(message);
}
toString() {
return `LuaRuntimeErrorr: ${this.message} at ${this.astNode.from}, ${this.astNode.to}`;
}
}
export function luaTruthy(value: any): boolean {
if (value === undefined || value === null || value === false) {
return false;
@ -235,13 +325,18 @@ export function luaTruthy(value: any): boolean {
return true;
}
export function luaToString(value: any): string {
// Implementation to be refined
return String(value);
}
export function jsToLuaValue(value: any): any {
if (value instanceof LuaTable) {
return value;
} else if (Array.isArray(value)) {
return LuaTable.fromArray(value.map(jsToLuaValue));
return LuaTable.fromJSArray(value.map(jsToLuaValue));
} else if (typeof value === "object") {
return LuaTable.fromObject(value);
return LuaTable.fromJSObject(value);
} else {
return value;
}
@ -251,9 +346,9 @@ export function luaValueToJS(value: any): any {
if (value instanceof LuaTable) {
// This is a heuristic: if this table is used as an array, we return an array
if (value.length > 0) {
return value.toArray();
return value.toJSArray();
} else {
return value.toObject();
return value.toJSObject();
}
} else {
return value;

View File

@ -0,0 +1,40 @@
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

@ -0,0 +1,75 @@
import {
LuaBuiltinFunction,
LuaEnv,
LuaMultiRes,
LuaNativeJSFunction,
type LuaTable,
luaTypeOf,
} from "$common/space_lua/runtime.ts";
const printFunction = new LuaNativeJSFunction((...args) => {
console.log("[Lua]", ...args);
});
const assertFunction = new LuaNativeJSFunction(
(value: any, message?: string) => {
if (!value) {
throw new Error(`Assertion failed: ${message}`);
}
},
);
const ipairsFunction = new LuaNativeJSFunction((ar: any[]) => {
let i = 0;
return () => {
if (i >= ar.length) {
return;
}
const result = new LuaMultiRes([i, ar[i]]);
i++;
return result;
};
});
const pairsFunction = new LuaBuiltinFunction((t: LuaTable) => {
const keys = t.keys();
let i = 0;
return () => {
if (i >= keys.length) {
return;
}
const key = keys[i];
const result = new LuaMultiRes([key, t.get(key)]);
i++;
return result;
};
});
const typeFunction = new LuaNativeJSFunction((value: any) => {
return luaTypeOf(value);
});
const tostringFunction = new LuaNativeJSFunction((value: any) => {
return String(value);
});
const tonumberFunction = new LuaNativeJSFunction((value: any) => {
return Number(value);
});
const errorFunction = new LuaNativeJSFunction((message: string) => {
throw new Error(message);
});
export function luaBuildStandardEnv() {
const env = new LuaEnv();
env.set("print", printFunction);
env.set("assert", assertFunction);
env.set("pairs", pairsFunction);
env.set("ipairs", ipairsFunction);
env.set("type", typeFunction);
env.set("tostring", tostringFunction);
env.set("tonumber", tonumberFunction);
env.set("error", errorFunction);
return env;
}

View File

@ -4,6 +4,13 @@ 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";
// @ts-ignore: Temporal polyfill
Date.prototype.toTemporalInstant = toTemporalInstant;
@ -137,5 +144,67 @@ 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());
env.set(
"flash",
new LuaNativeJSFunction((...args) => {
if (system.registeredSyscalls.has("editor.flashNotification")) {
return system.localSyscall("editor.flashNotification", args);
} else {
console.log("[Flash]", ...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,
(...args: any[]) => {
return def.get(1).call(...args.map(jsToLuaValue));
},
);
},
),
);
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);
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");
}
}

10
common/syscalls/lua.ts Normal file
View File

@ -0,0 +1,10 @@
import type { SysCallMapping } from "$lib/plugos/system.ts";
import { parse } from "../space_lua/parse.ts";
export function luaSyscalls(): SysCallMapping {
return {
"lua.parse": (_ctx, code: string) => {
return parse(code);
},
};
}

View File

@ -253,7 +253,7 @@ export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
to: tree.to,
};
for (const node of tree.children!) {
if (node.type && !node.type.endsWith("Mark") && node.type !== "Comment") {
if (node.type && node.type !== "Comment") {
ast.children!.push(cleanTree(node, omitTrimmable));
}
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {

View File

@ -15,5 +15,6 @@ export * as YAML from "./syscalls/yaml.ts";
export * as mq from "./syscalls/mq.ts";
export * as datastore from "./syscalls/datastore.ts";
export * as jsonschema from "./syscalls/jsonschema.ts";
export * as lua from "./syscalls/lua.ts";
export * from "./syscall.ts";

8
plug-api/syscalls/lua.ts Normal file
View File

@ -0,0 +1,8 @@
import { syscall } from "../syscall.ts";
import type { ParseTree } from "../lib/tree.ts";
export function parse(
code: string,
): Promise<ParseTree> {
return syscall("lua.parse", code);
}

View File

@ -129,6 +129,10 @@ functions:
path: script.ts:indexSpaceScript
events:
- page:index
indexSpaceLua:
path: script.ts:indexSpaceLua
events:
- page:index
# Style
indexSpaceStyle:
@ -204,6 +208,11 @@ functions:
events:
- editor:lint
lintLua:
path: lint.ts:lintLua
events:
- editor:lint
# Tag file system
readFileTag:
path: tag_page.ts:readFileTag

View File

@ -1,5 +1,6 @@
import {
jsonschema,
lua,
system,
YAML,
} from "@silverbulletmd/silverbullet/syscalls";
@ -211,3 +212,39 @@ async function lintYaml(
}
}
}
export async function lintLua({ tree }: LintEvent): Promise<LintDiagnostic[]> {
const diagnostics: LintDiagnostic[] = [];
await traverseTreeAsync(tree, async (node) => {
if (node.type === "FencedCode") {
const codeInfo = findNodeOfType(node, "CodeInfo")!;
if (!codeInfo) {
return true;
}
const codeLang = codeInfo.children![0].text!;
if (codeLang !== "space-lua") {
return true;
}
const codeText = findNodeOfType(node, "CodeText");
if (!codeText) {
return true;
}
const luaCode = renderToText(codeText);
try {
await lua.parse(luaCode);
} catch (e: any) {
diagnostics.push({
from: codeText.from!,
to: codeText.to!,
severity: "error",
message: e.message,
});
console.log("Lua error", e);
}
return true;
}
return false;
});
return diagnostics;
}

View File

@ -32,3 +32,29 @@ export async function indexSpaceScript({ name, tree }: IndexTreeEvent) {
});
await indexObjects<ScriptObject>(name, allScripts);
}
export async function indexSpaceLua({ name, tree }: IndexTreeEvent) {
const allScripts: ScriptObject[] = [];
collectNodesOfType(tree, "FencedCode").map((t) => {
const codeInfoNode = findNodeOfType(t, "CodeInfo");
if (!codeInfoNode) {
return;
}
const fenceType = codeInfoNode.children![0].text!;
if (fenceType !== "space-lua") {
return;
}
const codeTextNode = findNodeOfType(t, "CodeText");
if (!codeTextNode) {
// Honestly, this shouldn't happen
return;
}
const codeText = codeTextNode.children![0].text!;
allScripts.push({
ref: `${name}@${t.from!}`,
tag: "space-lua",
script: codeText,
});
});
await indexObjects<ScriptObject>(name, allScripts);
}

View File

@ -8,6 +8,7 @@ import type { EventHook } from "../common/hooks/event.ts";
import { MQHook } from "../lib/plugos/hooks/mq.ts";
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
import { eventSyscalls } from "../lib/plugos/syscalls/event.ts";
import { luaSyscalls } from "$common/syscalls/lua.ts";
import { mqSyscalls } from "../lib/plugos/syscalls/mq.ts";
import { System } from "../lib/plugos/system.ts";
import { Space } from "../common/space.ts";
@ -132,6 +133,7 @@ export class ServerSystem extends CommonSystem {
mqSyscalls(this.mq),
languageSyscalls(),
jsonschemaSyscalls(),
luaSyscalls(),
templateSyscalls(this.ds),
dataStoreReadSyscalls(this.ds),
codeWidgetSyscalls(codeWidgetHook),

View File

@ -43,6 +43,7 @@ import { CommonSystem } from "$common/common_system.ts";
import type { DataStoreMQ } from "$lib/data/mq.datastore.ts";
import { plugPrefix } from "$common/spaces/constants.ts";
import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts";
import { luaSyscalls } from "$common/syscalls/lua.ts";
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
@ -161,6 +162,7 @@ export class ClientSystem extends CommonSystem {
clientCodeWidgetSyscalls(),
languageSyscalls(),
jsonschemaSyscalls(),
luaSyscalls(),
this.client.syncMode
// In sync mode handle locally
? mqSyscalls(this.mq)