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(); this.commandHook.throttledBuildAllCommands();
} }
// Swap in the expanded function map // Swap in the expanded function map
this.ds.functionMap = functions; this.ds.functionMap = functions;
} }

View File

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

View File

@ -3,6 +3,7 @@ import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts";
import { parse } from "./parse.ts"; import { parse } from "./parse.ts";
import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts"; import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } from "./eval.ts"; import { evalExpression, evalStatement } from "./eval.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
function evalExpr(s: string, e = new LuaEnv()): any { function evalExpr(s: string, e = new LuaEnv()): any {
return evalExpression( return evalExpression(
@ -39,22 +40,22 @@ Deno.test("Evaluator test", async () => {
assertEquals(tbl.get(1), 3); assertEquals(tbl.get(1), 3);
assertEquals(tbl.get(2), 1); assertEquals(tbl.get(2), 1);
assertEquals(tbl.get(3), 2); assertEquals(tbl.get(3), 2);
assertEquals(tbl.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", name: "Zef",
age: 100, age: 100,
}); });
assertEquals( assertEquals(
(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toObject(), (await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toJSObject(),
{ {
name: "Zef", name: "Zef",
age: 100, age: 100,
}, },
); );
assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toObject(), { assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toJSObject(), {
5: 1, 5: 1,
ab: 2, ab: 2,
}); });
@ -68,6 +69,10 @@ Deno.test("Evaluator test", async () => {
// Function calls // Function calls
assertEquals(singleResult(evalExpr(`test(3)`, env)), 3); assertEquals(singleResult(evalExpr(`test(3)`, env)), 3);
assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4); 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 () => { Deno.test("Statement evaluation", async () => {
@ -93,7 +98,7 @@ Deno.test("Statement evaluation", async () => {
const env3 = new LuaEnv(); const env3 = new LuaEnv();
await evalBlock(`tbl = {1, 2, 3}`, env3); await evalBlock(`tbl = {1, 2, 3}`, env3);
await evalBlock(`tbl[1] = 3`, env3); await evalBlock(`tbl[1] = 3`, env3);
assertEquals(env3.get("tbl").toArray(), [3, 2, 3]); assertEquals(env3.get("tbl").toJSArray(), [3, 2, 3]);
await evalBlock("tbl.name = 'Zef'", env3); await evalBlock("tbl.name = 'Zef'", env3);
assertEquals(env3.get("tbl").get("name"), "Zef"); assertEquals(env3.get("tbl").get("name"), "Zef");
await evalBlock(`tbl[2] = {age=10}`, env3); await evalBlock(`tbl[2] = {age=10}`, env3);
@ -105,7 +110,7 @@ Deno.test("Statement evaluation", async () => {
env4.set("print", new LuaNativeJSFunction(console.log)); env4.set("print", new LuaNativeJSFunction(console.log));
await evalBlock( await evalBlock(
` `
a = 1 a = 1
do do
-- sets global a to 3 -- sets global a to 3
a = 3 a = 3
@ -198,4 +203,62 @@ Deno.test("Statement evaluation", async () => {
`, `,
env8, 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 { import type {
LuaExpression, LuaExpression,
LuaLValue, LuaLValue,
LuaStatement, LuaStatement,
} from "$common/space_lua/ast.ts"; } from "$common/space_lua/ast.ts";
import { evalPromiseValues } from "$common/space_lua/util.ts"; import { evalPromiseValues } from "$common/space_lua/util.ts";
import { import {
type ILuaFunction, type ILuaFunction,
type ILuaGettable, type ILuaGettable,
type ILuaSettable, type ILuaSettable,
LuaBreak, LuaBreak,
LuaEnv, LuaEnv,
LuaFunction, LuaFunction,
luaGet, luaGet,
luaLen, luaLen,
type LuaLValueContainer, type LuaLValueContainer,
LuaReturn, LuaMultiRes,
LuaTable, LuaReturn,
luaTruthy, LuaRuntimeError,
type LuaValue, LuaTable,
singleResult, luaToString,
luaTruthy,
type LuaValue,
singleResult,
} from "./runtime.ts"; } from "./runtime.ts";
export function evalExpression( export function evalExpression(
e: LuaExpression, e: LuaExpression,
env: LuaEnv, env: LuaEnv,
): Promise<LuaValue> | LuaValue { ): Promise<LuaValue> | LuaValue {
switch (e.type) { switch (e.type) {
case "String": case "String":
// TODO: Deal with escape sequences // TODO: Deal with escape sequences
return e.value; return e.value;
case "Number": case "Number":
return e.value; return e.value;
case "Boolean": case "Boolean":
return e.value; return e.value;
case "Nil": case "Nil":
return null; return null;
case "Binary": { case "Binary": {
const values = evalPromiseValues([ const values = evalPromiseValues([
evalExpression(e.left, env), evalExpression(e.left, env),
evalExpression(e.right, env), evalExpression(e.right, env),
]); ]);
if (values instanceof Promise) { if (values instanceof Promise) {
return values.then(([left, right]) => return values.then(([left, right]) =>
luaOp(e.operator, singleResult(left), singleResult(right)) luaOp(e.operator, singleResult(left), singleResult(right))
); );
} else { } else {
return luaOp( return luaOp(
e.operator, e.operator,
singleResult(values[0]), singleResult(values[0]),
singleResult(values[1]), 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}`);
} }
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( function evalPrefixExpression(
e: LuaExpression, e: LuaExpression,
env: LuaEnv, env: LuaEnv,
): Promise<LuaValue> | LuaValue { ): Promise<LuaValue> | LuaValue {
switch (e.type) { switch (e.type) {
case "Variable": { case "Variable": {
const value = env.get(e.name); const value = env.get(e.name);
if (value === undefined) { if (value === undefined) {
throw new Error(`Undefined variable ${e.name}`); throw new Error(`Undefined variable ${e.name}`);
} else { } else {
return value; 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}`);
} }
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 { function luaOp(op: string, left: any, right: any): any {
switch (op) { switch (op) {
case "+": case "+":
return left + right; return left + right;
case "-": case "-":
return left - right; return left - right;
case "*": case "*":
return left * right; return left * right;
case "/": case "/":
return left / right; return left / right;
case "//": case "//":
return Math.floor(left / right); return Math.floor(left / right);
case "%": case "%":
return left % right; return left % right;
case "^": case "^":
return left ** right; return left ** right;
case "..": case "..":
return left + right; return luaToString(left) + luaToString(right);
case "==": case "==":
return left === right; return left === right;
case "~=": case "~=":
case "!=": case "!=":
case "/=": case "/=":
return left !== right; return left !== right;
case "<": case "<":
return left < right; return left < right;
case "<=": case "<=":
return left <= right; return left <= right;
case ">": case ">":
return left > right; return left > right;
case ">=": case ">=":
return left >= right; return left >= right;
case "and": case "and":
return left && right; return left && right;
case "or": case "or":
return left || right; return left || right;
default: default:
throw new Error(`Unknown operator ${op}`); throw new Error(`Unknown operator ${op}`);
} }
} }
export async function evalStatement( export async function evalStatement(
s: LuaStatement, s: LuaStatement,
env: LuaEnv, env: LuaEnv,
): Promise<void> { ): Promise<void> {
switch (s.type) { switch (s.type) {
case "Assignment": { case "Assignment": {
const values = await evalPromiseValues( const values = await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)), s.expressions.map((value) => evalExpression(value, env)),
); );
const lvalues = await evalPromiseValues(s.variables const lvalues = await evalPromiseValues(s.variables
.map((lval) => evalLValue(lval, env))); .map((lval) => evalLValue(lval, env)));
for (let i = 0; i < lvalues.length; i++) { for (let i = 0; i < lvalues.length; i++) {
lvalues[i].env.set(lvalues[i].key, values[i]); lvalues[i].env.set(lvalues[i].key, values[i]);
} }
break; 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}`);
} }
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( function evalLValue(
lval: LuaLValue, lval: LuaLValue,
env: LuaEnv, env: LuaEnv,
): LuaLValueContainer | Promise<LuaLValueContainer> { ): LuaLValueContainer | Promise<LuaLValueContainer> {
switch (lval.type) { switch (lval.type) {
case "Variable": case "Variable":
return { env, key: lval.name }; return { env, key: lval.name };
case "TableAccess": { case "TableAccess": {
const objValue = evalExpression( const objValue = evalExpression(
lval.object, lval.object,
env, env,
); );
const keyValue = evalExpression(lval.key, env); const keyValue = evalExpression(lval.key, env);
if ( if (
objValue instanceof Promise || objValue instanceof Promise ||
keyValue instanceof Promise keyValue instanceof Promise
) { ) {
return Promise.all([ return Promise.all([
objValue instanceof Promise objValue instanceof Promise ? objValue : Promise.resolve(objValue),
? objValue keyValue instanceof Promise ? keyValue : Promise.resolve(keyValue),
: Promise.resolve(objValue), ]).then(([objValue, keyValue]) => ({
keyValue instanceof Promise env: singleResult(objValue),
? keyValue key: singleResult(keyValue),
: Promise.resolve(keyValue), }));
]).then(([objValue, keyValue]) => ({ } else {
env: singleResult(objValue), return {
key: singleResult(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,
};
}
}
} }
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 { @tokens {
CompareOp { "<" | ">" | $[<>=~/!] "=" } CompareOp { "<" | ">" | $[<>=~/!] "=" }
word { std.asciiLetter (std.digit | std.asciiLetter)* } word { (std.asciiLetter | "_") (std.digit | std.asciiLetter | "_")* }
identifier { word } identifier { word }

View File

@ -13,7 +13,7 @@ export const parser = LRParser.deserialize({
], ],
skippedNodes: [0,1], skippedNodes: [0,1],
repeatNodeCount: 9, 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], tokenizers: [0, 1, 2],
topRules: {"Chunk":[0,2]}, topRules: {"Chunk":[0,2]},
dynamicPrecedences: {"110":1}, dynamicPrecedences: {"110":1},

View File

@ -25,6 +25,8 @@ Deno.test("Test Lua parser", () => {
parse(`e({1 ; 2 ; 3})`); parse(`e({1 ; 2 ; 3})`);
parse(`e({a = 1, b = 2, c = 3})`); parse(`e({a = 1, b = 2, c = 3})`);
parse(`e({[3] = 1, [10 * 10] = "sup"})`); parse(`e({[3] = 1, [10 * 10] = "sup"})`);
parse(`e(tbl.name)`);
parse(`e(tbl["name" + 10])`);
// Function calls // Function calls
parse(`e(func(), func(1, 2, 3), a.b(), a.b.c:hello(), (a.b)(7))`); 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`);
parse(`return 1`); parse(`return 1`);
parse(`return 1, 2, 3`); 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"; } from "@silverbulletmd/silverbullet/lib/tree";
import { parser } from "./parse-lua.js"; import { parser } from "./parse-lua.js";
import { styleTags } from "@lezer/highlight"; import { styleTags } from "@lezer/highlight";
import { indentNodeProp, LRLanguage } from "@codemirror/language";
import type { import type {
LuaAttName, LuaAttName,
LuaBlock, LuaBlock,
@ -17,25 +18,36 @@ import type {
LuaStatement, LuaStatement,
LuaTableField, LuaTableField,
} from "./ast.ts"; } from "./ast.ts";
import { tags as t } from "@lezer/highlight";
const luaStyleTags = styleTags({ const luaStyleTags = styleTags({
// Identifier: t.variableName, Name: t.variableName,
// TagIdentifier: t.variableName, LiteralString: t.string,
// GlobalIdentifier: t.variableName, Number: t.number,
// String: t.string, CompareOp: t.operator,
// Number: t.number, "true false": t.bool,
// PageRef: ct.WikiLinkTag, Comment: t.lineComment,
// BinExpression: t.operator, "return break goto do end while repeat until function local if then else elseif in for nil or and not":
// TernaryExpression: t.operator, t.keyword,
// Regex: t.regexp,
// "where limit select render Order OrderKW and or null as InKW NotKW BooleanKW each all":
// t.keyword,
}); });
export const highlightingQueryParser = parser.configure({ const customIndent = indentNodeProp.add({
props: [ "IfStatement FuncBody WhileStatement ForStatement TableConstructor": (
luaStyleTags, 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 { function parseChunk(t: ParseTree): LuaBlock {
@ -100,7 +112,6 @@ function parseStatement(t: ParseTree): LuaStatement {
}[] = []; }[] = [];
let elseBlock: LuaBlock | undefined = undefined; let elseBlock: LuaBlock | undefined = undefined;
for (let i = 0; i < t.children!.length; i += 4) { for (let i = 0; i < t.children!.length; i += 4) {
console.log("Looking at", t.children![i]);
const child = t.children![i]; const child = t.children![i];
if ( if (
child.children![0].text === "if" || child.children![0].text === "if" ||
@ -350,6 +361,15 @@ function parseExpression(t: ParseTree): LuaExpression {
to: t.to, 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": case "Parens":
return parseExpression(t.children![1]); return parseExpression(t.children![1]);
case "FunctionCall": { case "FunctionCall": {
@ -411,7 +431,6 @@ function parseExpression(t: ParseTree): LuaExpression {
} }
function parseFunctionArgs(ts: 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( return ts.filter((t) => ![",", "(", ")"].includes(t.type!)).map(
parseExpression, 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 { export function parse(s: string): LuaBlock {
const t = parseToCrudeAST(s); const t = parseToCrudeAST(stripLuaComments(s));
console.log("Clean tree", JSON.stringify(t, null, 2)); // console.log("Clean tree", JSON.stringify(t, null, 2));
const result = parseChunk(t); const result = parseChunk(t);
console.log("Parsed AST", JSON.stringify(result, null, 2)); // console.log("Parsed AST", JSON.stringify(result, null, 2));
return result; 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 type { LuaFunctionBody } from "./ast.ts";
import { evalStatement } from "$common/space_lua/eval.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 { export class LuaEnv implements ILuaSettable, ILuaGettable {
variables = new Map<string, LuaValue>(); variables = new Map<string, LuaValue>();
@ -31,7 +57,14 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
} }
export class LuaMultiRes { 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 { unwrap(): any {
@ -40,6 +73,19 @@ export class LuaMultiRes {
} }
return this.values[0]; 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 { 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 { export class LuaFunction implements ILuaFunction {
constructor(private body: LuaFunctionBody, private closure: LuaEnv) { constructor(private body: LuaFunctionBody, private closure: LuaEnv) {
} }
@ -101,6 +131,7 @@ export class LuaNativeJSFunction implements ILuaFunction {
constructor(readonly fn: (...args: JSValue[]) => JSValue) { constructor(readonly fn: (...args: JSValue[]) => JSValue) {
} }
// Performs automatic conversion between Lua and JS values
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue { call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
const result = this.fn(...args.map(luaValueToJS)); const result = this.fn(...args.map(luaValueToJS));
if (result instanceof Promise) { 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 { export class LuaTable implements ILuaSettable, ILuaGettable {
// To optimize the table implementation we use a combination of different data structures // 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 // 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; 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) { set(key: LuaValue, value: LuaValue) {
if (typeof key === "string") { if (typeof key === "string") {
this.stringKeys[key] = value; this.stringKeys[key] = value;
@ -159,11 +215,11 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return undefined; return undefined;
} }
toArray(): JSValue[] { toJSArray(): JSValue[] {
return this.arrayPart; return this.arrayPart;
} }
toObject(): Record<string, JSValue> { toJSObject(): Record<string, JSValue> {
const result = { ...this.stringKeys }; const result = { ...this.stringKeys };
for (const i in this.arrayPart) { for (const i in this.arrayPart) {
result[parseInt(i) + 1] = this.arrayPart[i]; result[parseInt(i) + 1] = this.arrayPart[i];
@ -171,7 +227,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return result; return result;
} }
static fromArray(arr: JSValue[]): LuaTable { static fromJSArray(arr: JSValue[]): LuaTable {
const table = new LuaTable(); const table = new LuaTable();
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
table.set(i + 1, arr[i]); table.set(i + 1, arr[i]);
@ -179,7 +235,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return table; return table;
} }
static fromObject(obj: Record<string, JSValue>): LuaTable { static fromJSObject(obj: Record<string, JSValue>): LuaTable {
const table = new LuaTable(); const table = new LuaTable();
for (const key in obj) { for (const key in obj) {
table.set(key, obj[key]); table.set(key, obj[key]);
@ -208,7 +264,7 @@ export function luaGet(obj: any, key: any): any {
export function luaLen(obj: any): number { export function luaLen(obj: any): number {
if (obj instanceof LuaTable) { if (obj instanceof LuaTable) {
return obj.toArray().length; return obj.toJSArray().length;
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
return obj.length; return obj.length;
} else { } 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 { 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 { export function luaTruthy(value: any): boolean {
if (value === undefined || value === null || value === false) { if (value === undefined || value === null || value === false) {
return false; return false;
@ -235,13 +325,18 @@ export function luaTruthy(value: any): boolean {
return true; return true;
} }
export function luaToString(value: any): string {
// Implementation to be refined
return String(value);
}
export function jsToLuaValue(value: any): any { export function jsToLuaValue(value: any): any {
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
return value; return value;
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
return LuaTable.fromArray(value.map(jsToLuaValue)); return LuaTable.fromJSArray(value.map(jsToLuaValue));
} else if (typeof value === "object") { } else if (typeof value === "object") {
return LuaTable.fromObject(value); return LuaTable.fromJSObject(value);
} else { } else {
return value; return value;
} }
@ -251,9 +346,9 @@ export function luaValueToJS(value: any): any {
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
// This is a heuristic: if this table is used as an array, we return an array // This is a heuristic: if this table is used as an array, we return an array
if (value.length > 0) { if (value.length > 0) {
return value.toArray(); return value.toJSArray();
} else { } else {
return value.toObject(); return value.toJSObject();
} }
} else { } else {
return value; 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 type { AppCommand, CommandDef } from "$lib/command.ts";
import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill"; import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill";
import * as syscalls from "@silverbulletmd/silverbullet/syscalls"; import * as syscalls from "@silverbulletmd/silverbullet/syscalls";
import { LuaEnv, LuaNativeJSFunction } from "$common/space_lua/runtime.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
import { parse as parseLua } from "$common/space_lua/parse.ts";
import { evalStatement } from "$common/space_lua/eval.ts";
import { jsToLuaValue } from "$common/space_lua/runtime.ts";
import { LuaBuiltinFunction } from "$common/space_lua/runtime.ts";
import { LuaTable } from "$common/space_lua/runtime.ts";
// @ts-ignore: Temporal polyfill // @ts-ignore: Temporal polyfill
Date.prototype.toTemporalInstant = toTemporalInstant; Date.prototype.toTemporalInstant = toTemporalInstant;
@ -137,5 +144,67 @@ export class ScriptEnvironment {
for (const script of allScripts) { for (const script of allScripts) {
this.evalScript(script.script, system); this.evalScript(script.script, system);
} }
return this.loadLuaFromSystem(system);
}
async loadLuaFromSystem(system: System<any>) {
const allScripts: ScriptObject[] = await system.invokeFunction(
"index.queryObjects",
["space-lua", {}],
);
const env = new LuaEnv(luaBuildStandardEnv());
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, to: tree.to,
}; };
for (const node of tree.children!) { 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)); ast.children!.push(cleanTree(node, omitTrimmable));
} }
if (node.text && (omitTrimmable && node.text.trim() || !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 mq from "./syscalls/mq.ts";
export * as datastore from "./syscalls/datastore.ts"; export * as datastore from "./syscalls/datastore.ts";
export * as jsonschema from "./syscalls/jsonschema.ts"; export * as jsonschema from "./syscalls/jsonschema.ts";
export * as lua from "./syscalls/lua.ts";
export * from "./syscall.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 path: script.ts:indexSpaceScript
events: events:
- page:index - page:index
indexSpaceLua:
path: script.ts:indexSpaceLua
events:
- page:index
# Style # Style
indexSpaceStyle: indexSpaceStyle:
@ -204,6 +208,11 @@ functions:
events: events:
- editor:lint - editor:lint
lintLua:
path: lint.ts:lintLua
events:
- editor:lint
# Tag file system # Tag file system
readFileTag: readFileTag:
path: tag_page.ts:readFileTag path: tag_page.ts:readFileTag

View File

@ -1,5 +1,6 @@
import { import {
jsonschema, jsonschema,
lua,
system, system,
YAML, YAML,
} from "@silverbulletmd/silverbullet/syscalls"; } 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); 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 { MQHook } from "../lib/plugos/hooks/mq.ts";
import assetSyscalls from "../lib/plugos/syscalls/asset.ts"; import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
import { eventSyscalls } from "../lib/plugos/syscalls/event.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 { mqSyscalls } from "../lib/plugos/syscalls/mq.ts";
import { System } from "../lib/plugos/system.ts"; import { System } from "../lib/plugos/system.ts";
import { Space } from "../common/space.ts"; import { Space } from "../common/space.ts";
@ -132,6 +133,7 @@ export class ServerSystem extends CommonSystem {
mqSyscalls(this.mq), mqSyscalls(this.mq),
languageSyscalls(), languageSyscalls(),
jsonschemaSyscalls(), jsonschemaSyscalls(),
luaSyscalls(),
templateSyscalls(this.ds), templateSyscalls(this.ds),
dataStoreReadSyscalls(this.ds), dataStoreReadSyscalls(this.ds),
codeWidgetSyscalls(codeWidgetHook), 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 type { DataStoreMQ } from "$lib/data/mq.datastore.ts";
import { plugPrefix } from "$common/spaces/constants.ts"; import { plugPrefix } from "$common/spaces/constants.ts";
import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts"; import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts";
import { luaSyscalls } from "$common/syscalls/lua.ts";
const plugNameExtractRegex = /\/(.+)\.plug\.js$/; const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
@ -161,6 +162,7 @@ export class ClientSystem extends CommonSystem {
clientCodeWidgetSyscalls(), clientCodeWidgetSyscalls(),
languageSyscalls(), languageSyscalls(),
jsonschemaSyscalls(), jsonschemaSyscalls(),
luaSyscalls(),
this.client.syncMode this.client.syncMode
// In sync mode handle locally // In sync mode handle locally
? mqSyscalls(this.mq) ? mqSyscalls(this.mq)