First integration of Lua into the core (via space-lua code blocks)
parent
c0a248daba
commit
3cf7b72ebb
|
@ -76,6 +76,7 @@ export abstract class CommonSystem {
|
|||
|
||||
this.commandHook.throttledBuildAllCommands();
|
||||
}
|
||||
|
||||
// Swap in the expanded function map
|
||||
this.ds.functionMap = functions;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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()`);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue