From 3cf7b72ebb2e40926d3022d5266330cadc660928 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 3 Oct 2024 17:55:51 +0200 Subject: [PATCH] First integration of Lua into the core (via space-lua code blocks) --- common/common_system.ts | 1 + common/languages.ts | 2 + common/space_lua/eval.test.ts | 75 ++- common/space_lua/eval.ts | 967 +++++++++++++++++-------------- common/space_lua/lua.grammar | 2 +- common/space_lua/parse-lua.js | 2 +- common/space_lua/parse.test.ts | 12 +- common/space_lua/parse.ts | 109 +++- common/space_lua/runtime.test.ts | 17 + common/space_lua/runtime.ts | 147 ++++- common/space_lua/stdlib.test.ts | 40 ++ common/space_lua/stdlib.ts | 75 +++ common/space_script.ts | 69 +++ common/syscalls/lua.ts | 10 + plug-api/lib/tree.ts | 2 +- plug-api/syscalls.ts | 1 + plug-api/syscalls/lua.ts | 8 + plugs/index/index.plug.yaml | 9 + plugs/index/lint.ts | 37 ++ plugs/index/script.ts | 26 + server/server_system.ts | 2 + web/client_system.ts | 2 + 22 files changed, 1123 insertions(+), 492 deletions(-) create mode 100644 common/space_lua/runtime.test.ts create mode 100644 common/space_lua/stdlib.test.ts create mode 100644 common/space_lua/stdlib.ts create mode 100644 common/syscalls/lua.ts create mode 100644 plug-api/syscalls/lua.ts diff --git a/common/common_system.ts b/common/common_system.ts index 947b0c03..e30924dd 100644 --- a/common/common_system.ts +++ b/common/common_system.ts @@ -76,6 +76,7 @@ export abstract class CommonSystem { this.commandHook.throttledBuildAllCommands(); } + // Swap in the expanded function map this.ds.functionMap = functions; } diff --git a/common/languages.ts b/common/languages.ts index 713c7864..84fd4599 100644 --- a/common/languages.ts +++ b/common/languages.ts @@ -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 = { name: "query", parser: highlightingQueryParser, }), + "space-lua": luaLanguage, "template": extendedMarkdownLanguage, "expression": LRLanguage.define({ name: "expression", diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index 76783f1a..31c1ccb4 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -3,6 +3,7 @@ import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts"; import { parse } from "./parse.ts"; import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts"; import { evalExpression, evalStatement } from "./eval.ts"; +import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; function evalExpr(s: string, e = new LuaEnv()): any { return evalExpression( @@ -39,22 +40,22 @@ Deno.test("Evaluator test", async () => { assertEquals(tbl.get(1), 3); assertEquals(tbl.get(2), 1); assertEquals(tbl.get(3), 2); - assertEquals(tbl.toArray(), [3, 1, 2]); + assertEquals(tbl.toJSArray(), [3, 1, 2]); - assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toObject(), { + assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toJSObject(), { name: "Zef", age: 100, }); assertEquals( - (await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toObject(), + (await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toJSObject(), { name: "Zef", age: 100, }, ); - assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toObject(), { + assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toJSObject(), { 5: 1, ab: 2, }); @@ -68,6 +69,10 @@ Deno.test("Evaluator test", async () => { // Function calls assertEquals(singleResult(evalExpr(`test(3)`, env)), 3); assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4); + + // Function definitions + const fn = evalExpr(`function(a, b) return a + b end`); + assertEquals(fn.body.parameters, ["a", "b"]); }); Deno.test("Statement evaluation", async () => { @@ -93,7 +98,7 @@ Deno.test("Statement evaluation", async () => { const env3 = new LuaEnv(); await evalBlock(`tbl = {1, 2, 3}`, env3); await evalBlock(`tbl[1] = 3`, env3); - assertEquals(env3.get("tbl").toArray(), [3, 2, 3]); + assertEquals(env3.get("tbl").toJSArray(), [3, 2, 3]); await evalBlock("tbl.name = 'Zef'", env3); assertEquals(env3.get("tbl").get("name"), "Zef"); await evalBlock(`tbl[2] = {age=10}`, env3); @@ -105,7 +110,7 @@ Deno.test("Statement evaluation", async () => { env4.set("print", new LuaNativeJSFunction(console.log)); await evalBlock( ` - a = 1 + a = 1 do -- sets global a to 3 a = 3 @@ -198,4 +203,62 @@ Deno.test("Statement evaluation", async () => { `, env8, ); + + // Local fucntion definition + const env9 = new LuaEnv(); + env9.set("print", new LuaNativeJSFunction(console.log)); + await evalBlock( + ` + local function test(a) + return a + 1 + end + print("3 + 1 = " .. test(3)) + `, + env9, + ); + + // For loop over range + const env10 = new LuaEnv(); + await evalBlock( + ` + c = 0 + for i = 1, 3 do + c = c + i + end + `, + env10, + ); + assertEquals(env10.get("c"), 6); + + // For loop over iterator + const env11 = new LuaEnv(luaBuildStandardEnv()); + await evalBlock( + ` + function fruits() + local list = { "apple", "banana", "cherry" } + -- Track index internally + local index = 0 + + return function() + index = index + 1 + if list[index] then + return list[index] + end + end + end + + for fruit in fruits() do + print("Fruit: " .. fruit) + end + `, + env11, + ); + + await evalBlock( + ` + for _, f in ipairs({ "apple", "banana", "cherry" }) do + print("Fruit: " .. f) + end`, + luaBuildStandardEnv(), + ); }); diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index b470dc48..e3a17d23 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -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 { - 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[] = []; - 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[] = []; + 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 { - 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 { - 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 { - 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, + }; + } + } + } } diff --git a/common/space_lua/lua.grammar b/common/space_lua/lua.grammar index 74da6369..8b4d5da3 100644 --- a/common/space_lua/lua.grammar +++ b/common/space_lua/lua.grammar @@ -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 } diff --git a/common/space_lua/parse-lua.js b/common/space_lua/parse-lua.js index 1d20ad88..3157e7ca 100644 --- a/common/space_lua/parse-lua.js +++ b/common/space_lua/parse-lua.js @@ -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}, diff --git a/common/space_lua/parse.test.ts b/common/space_lua/parse.test.ts index 319bfa87..bbe91d1e 100644 --- a/common/space_lua/parse.test.ts +++ b/common/space_lua/parse.test.ts @@ -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()`); }); diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts index a878a4f5..c371ebd5 100644 --- a/common/space_lua/parse.ts +++ b/common/space_lua/parse.ts @@ -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; } diff --git a/common/space_lua/runtime.test.ts b/common/space_lua/runtime.test.ts new file mode 100644 index 00000000..817cfec9 --- /dev/null +++ b/common/space_lua/runtime.test.ts @@ -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, + ], + ); +}); diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 18e17810..5d5746dd 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -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; +} + +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(); @@ -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; -} - -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 { 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 { + 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 { + toJSObject(): Record { 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): LuaTable { + static fromJSObject(obj: Record): 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; diff --git a/common/space_lua/stdlib.test.ts b/common/space_lua/stdlib.test.ts new file mode 100644 index 00000000..688d5340 --- /dev/null +++ b/common/space_lua/stdlib.test.ts @@ -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"); +}); diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts new file mode 100644 index 00000000..48b2cea3 --- /dev/null +++ b/common/space_lua/stdlib.ts @@ -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; +} diff --git a/common/space_script.ts b/common/space_script.ts index d7b0d6a5..067b2ede 100644 --- a/common/space_script.ts +++ b/common/space_script.ts @@ -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) { + 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"); } } diff --git a/common/syscalls/lua.ts b/common/syscalls/lua.ts new file mode 100644 index 00000000..bd675e31 --- /dev/null +++ b/common/syscalls/lua.ts @@ -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); + }, + }; +} diff --git a/plug-api/lib/tree.ts b/plug-api/lib/tree.ts index c4ce234d..50385a1f 100644 --- a/plug-api/lib/tree.ts +++ b/plug-api/lib/tree.ts @@ -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)) { diff --git a/plug-api/syscalls.ts b/plug-api/syscalls.ts index e636252b..f1430204 100644 --- a/plug-api/syscalls.ts +++ b/plug-api/syscalls.ts @@ -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"; diff --git a/plug-api/syscalls/lua.ts b/plug-api/syscalls/lua.ts new file mode 100644 index 00000000..da1cbe35 --- /dev/null +++ b/plug-api/syscalls/lua.ts @@ -0,0 +1,8 @@ +import { syscall } from "../syscall.ts"; +import type { ParseTree } from "../lib/tree.ts"; + +export function parse( + code: string, +): Promise { + return syscall("lua.parse", code); +} diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index e7cd0dea..1012b0f6 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -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 diff --git a/plugs/index/lint.ts b/plugs/index/lint.ts index 2d473f72..e95396f6 100644 --- a/plugs/index/lint.ts +++ b/plugs/index/lint.ts @@ -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 { + 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; +} diff --git a/plugs/index/script.ts b/plugs/index/script.ts index 8b53a67b..433ffbfd 100644 --- a/plugs/index/script.ts +++ b/plugs/index/script.ts @@ -32,3 +32,29 @@ export async function indexSpaceScript({ name, tree }: IndexTreeEvent) { }); await indexObjects(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(name, allScripts); +} diff --git a/server/server_system.ts b/server/server_system.ts index 5e480b8a..b0a19108 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -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), diff --git a/web/client_system.ts b/web/client_system.ts index 6fbb9a3a..d8c0ea2c 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -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)