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

@ -14,8 +14,11 @@ import {
luaGet,
luaLen,
type LuaLValueContainer,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
LuaTable,
luaToString,
luaTruthy,
type LuaValue,
singleResult,
@ -131,12 +134,8 @@ export function evalExpression(
) {
promises.push(
Promise.all([
key instanceof Promise
? key
: Promise.resolve(key),
value instanceof Promise
? value
: Promise.resolve(value),
key instanceof Promise ? key : Promise.resolve(key),
value instanceof Promise ? value : Promise.resolve(value),
]).then(([key, value]) => {
table.set(
singleResult(key),
@ -179,6 +178,9 @@ export function evalExpression(
return table;
}
}
case "FunctionDefinition": {
return new LuaFunction(e.body, env);
}
default:
throw new Error(`Unknown expression type ${e.type}`);
}
@ -199,6 +201,26 @@ function evalPrefixExpression(
}
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) {
@ -234,6 +256,7 @@ function evalPrefixExpression(
}
}
// TODO: Handle metatables and possibly do type checking
function luaOp(op: string, left: any, right: any): any {
switch (op) {
case "+":
@ -251,7 +274,7 @@ function luaOp(op: string, left: any, right: any): any {
case "^":
return left ** right;
case "..":
return left + right;
return luaToString(left) + luaToString(right);
case "==":
return left === right;
case "~=":
@ -386,15 +409,91 @@ export async function evalStatement(
);
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)),
),
);
}
default:
throw new Error(`Unknown statement type ${s.type}`);
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}`);
}
}
@ -416,12 +515,8 @@ function evalLValue(
keyValue instanceof Promise
) {
return Promise.all([
objValue instanceof Promise
? objValue
: Promise.resolve(objValue),
keyValue instanceof Promise
? keyValue
: Promise.resolve(keyValue),
objValue instanceof Promise ? objValue : Promise.resolve(objValue),
keyValue instanceof Promise ? keyValue : Promise.resolve(keyValue),
]).then(([objValue, keyValue]) => ({
env: singleResult(objValue),
key: singleResult(keyValue),

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({
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)