diff --git a/common/space_lua.ts b/common/space_lua.ts index bcd6a4c7..074c02be 100644 --- a/common/space_lua.ts +++ b/common/space_lua.ts @@ -4,6 +4,7 @@ import { LuaEnv, LuaFunction, LuaRuntimeError, + LuaStackFrame, } from "$common/space_lua/runtime.ts"; import { parse as parseLua } from "$common/space_lua/parse.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; @@ -39,10 +40,11 @@ export class SpaceLuaEnvironment { const ast = parseLua(script.script, { ref: script.ref }); // We create a local scope for each script const scriptEnv = new LuaEnv(this.env); - await evalStatement(ast, scriptEnv); + const sf = new LuaStackFrame(new LuaEnv(), ast.ctx); + await evalStatement(ast, scriptEnv, sf); } catch (e: any) { if (e instanceof LuaRuntimeError) { - const origin = resolveASTReference(e.context); + const origin = resolveASTReference(e.sf.astCtx!); if (origin) { console.error( `Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`, @@ -62,7 +64,8 @@ export class SpaceLuaEnvironment { if (value instanceof LuaFunction) { console.log("Now registering Lua function", globalName); scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => { - return luaValueToJS(value.call(...args.map(jsToLuaValue))); + const sf = new LuaStackFrame(new LuaEnv(), value.body.ctx); + return luaValueToJS(value.call(sf, ...args.map(jsToLuaValue))); }); } } diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index 086510e7..38a55510 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -2,6 +2,7 @@ import { assertEquals } from "@std/assert/equals"; import { LuaEnv, LuaNativeJSFunction, + LuaStackFrame, luaValueToJS, singleResult, } from "./runtime.ts"; @@ -11,15 +12,19 @@ import { evalExpression, evalStatement } from "./eval.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; function evalExpr(s: string, e = new LuaEnv()): any { + const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement; + const sf = new LuaStackFrame(e, node.ctx); return evalExpression( - (parse(`e(${s})`).statements[0] as LuaFunctionCallStatement).call - .args[0], + node.call.args[0], e, + sf, ); } function evalBlock(s: string, e = new LuaEnv()): Promise { - return evalStatement(parse(s) as LuaBlock, e); + const node = parse(s) as LuaBlock; + const sf = new LuaStackFrame(e, node.ctx); + return evalStatement(node, e, sf); } Deno.test("Evaluator test", async () => { diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index f75bf679..762579a3 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -1,10 +1,15 @@ import type { + ASTCtx, LuaExpression, LuaLValue, LuaStatement, } from "$common/space_lua/ast.ts"; import { evalPromiseValues } from "$common/space_lua/util.ts"; -import { luaCall, luaSet } from "$common/space_lua/runtime.ts"; +import { + luaCall, + luaSet, + type LuaStackFrame, +} from "$common/space_lua/runtime.ts"; import { type ILuaFunction, type ILuaGettable, @@ -28,6 +33,7 @@ import { export function evalExpression( e: LuaExpression, env: LuaEnv, + sf: LuaStackFrame, ): Promise | LuaValue { try { switch (e.type) { @@ -41,23 +47,31 @@ export function evalExpression( return null; case "Binary": { const values = evalPromiseValues([ - evalExpression(e.left, env), - evalExpression(e.right, env), + evalExpression(e.left, env, sf), + evalExpression(e.right, env, sf), ]); if (values instanceof Promise) { return values.then(([left, right]) => - luaOp(e.operator, singleResult(left), singleResult(right)) + luaOp( + e.operator, + singleResult(left), + singleResult(right), + e.ctx, + sf, + ) ); } else { return luaOp( e.operator, singleResult(values[0]), singleResult(values[1]), + e.ctx, + sf, ); } } case "Unary": { - const value = evalExpression(e.argument, env); + const value = evalExpression(e.argument, env, sf); if (value instanceof Promise) { return value.then((value) => { switch (e.operator) { @@ -97,29 +111,30 @@ export function evalExpression( case "FunctionCall": case "TableAccess": case "PropertyAccess": - return evalPrefixExpression(e, env); + return evalPrefixExpression(e, env, sf); 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); + const value = evalExpression(field.value, env, sf); if (value instanceof Promise) { promises.push(value.then((value) => { table.set( field.key, singleResult(value), + sf, ); })); } else { - table.set(field.key, singleResult(value)); + table.set(field.key, singleResult(value), sf); } break; } case "DynamicField": { - const key = evalExpression(field.key, env); - const value = evalExpression(field.value, env); + const key = evalExpression(field.key, env, sf); + const value = evalExpression(field.value, env, sf); if ( key instanceof Promise || value instanceof Promise ) { @@ -131,6 +146,7 @@ export function evalExpression( table.set( singleResult(key), singleResult(value), + sf, ); }), ); @@ -138,18 +154,20 @@ export function evalExpression( table.set( singleResult(key), singleResult(value), + sf, ); } break; } case "ExpressionField": { - const value = evalExpression(field.value, env); + const value = evalExpression(field.value, env, sf); if (value instanceof Promise) { promises.push(value.then((value) => { // +1 because Lua tables are 1-indexed table.set( table.length + 1, singleResult(value), + sf, ); })); } else { @@ -157,6 +175,7 @@ export function evalExpression( table.set( table.length + 1, singleResult(value), + sf, ); } break; @@ -178,7 +197,7 @@ export function evalExpression( } catch (err: any) { // Repackage any non Lua-specific exceptions with some position information if (!err.constructor.name.startsWith("Lua")) { - throw new LuaRuntimeError(err.message, e.ctx, err); + throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err); } else { throw err; } @@ -188,6 +207,7 @@ export function evalExpression( function evalPrefixExpression( e: LuaExpression, env: LuaEnv, + sf: LuaStackFrame, ): Promise | LuaValue { switch (e.type) { case "Variable": { @@ -199,43 +219,43 @@ function evalPrefixExpression( } } case "Parenthesized": - return evalExpression(e.expression, env); + return evalExpression(e.expression, env, sf); // <>[<>] case "TableAccess": { const values = evalPromiseValues([ - evalPrefixExpression(e.object, env), - evalExpression(e.key, env), + evalPrefixExpression(e.object, env, sf), + evalExpression(e.key, env, sf), ]); if (values instanceof Promise) { return values.then(([table, key]) => { table = singleResult(table); key = singleResult(key); - return luaGet(table, key, e.ctx); + return luaGet(table, key, sf.withCtx(e.ctx)); }); } else { const table = singleResult(values[0]); const key = singleResult(values[1]); - return luaGet(table, singleResult(key), e.ctx); + return luaGet(table, singleResult(key), sf.withCtx(e.ctx)); } } // .property case "PropertyAccess": { - const obj = evalPrefixExpression(e.object, env); + const obj = evalPrefixExpression(e.object, env, sf); if (obj instanceof Promise) { return obj.then((obj) => { - return luaGet(obj, e.property, e.ctx); + return luaGet(obj, e.property, sf.withCtx(e.ctx)); }); } else { - return luaGet(obj, e.property, e.ctx); + return luaGet(obj, e.property, sf.withCtx(e.ctx)); } } case "FunctionCall": { - let prefixValue = evalPrefixExpression(e.prefix, env); + let prefixValue = evalPrefixExpression(e.prefix, env, sf); if (!prefixValue) { throw new LuaRuntimeError( `Attempting to call nil as a function`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } if (prefixValue instanceof Promise) { @@ -243,7 +263,7 @@ function evalPrefixExpression( if (!prefixValue) { throw new LuaRuntimeError( `Attempting to call a nil value`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } let selfArgs: LuaValue[] = []; @@ -251,7 +271,7 @@ function evalPrefixExpression( if (e.name && !prefixValue.get) { throw new LuaRuntimeError( `Attempting to index a non-table: ${prefixValue}`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } else if (e.name) { // Two things need to happen: the actual function be called needs to be looked up in the table, and the table itself needs to be passed as the first argument @@ -261,18 +281,18 @@ function evalPrefixExpression( if (!prefixValue.call) { throw new LuaRuntimeError( `Attempting to call ${prefixValue} as a function`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } const args = evalPromiseValues( - e.args.map((arg) => evalExpression(arg, env)), + e.args.map((arg) => evalExpression(arg, env, sf)), ); if (args instanceof Promise) { return args.then((args) => - luaCall(prefixValue, [...selfArgs, ...args], e.ctx) + luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf) ); } else { - return luaCall(prefixValue, [...selfArgs, ...args], e.ctx); + return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf); } }); } else { @@ -281,7 +301,7 @@ function evalPrefixExpression( if (e.name && !prefixValue.get) { throw new LuaRuntimeError( `Attempting to index a non-table: ${prefixValue}`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } else if (e.name) { // Two things need to happen: the actual function be called needs to be looked up in the table, and the table itself needs to be passed as the first argument @@ -291,18 +311,18 @@ function evalPrefixExpression( if (!prefixValue.call) { throw new LuaRuntimeError( `Attempting to call ${prefixValue} as a function`, - e.prefix.ctx, + sf.withCtx(e.prefix.ctx), ); } const args = evalPromiseValues( - e.args.map((arg) => evalExpression(arg, env)), + e.args.map((arg) => evalExpression(arg, env, sf)), ); if (args instanceof Promise) { return args.then((args) => - luaCall(prefixValue, [...selfArgs, ...args], e.ctx) + luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf) ); } else { - return luaCall(prefixValue, [...selfArgs, ...args], e.ctx); + return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf); } } } @@ -315,7 +335,12 @@ function evalPrefixExpression( type LuaMetaMethod = Record LuaValue; + nativeImplementation: ( + a: LuaValue, + b: LuaValue, + ctx: ASTCtx, + sf: LuaStackFrame, + ) => LuaValue; }>; const operatorsMetaMethods: LuaMetaMethod = { @@ -372,10 +397,10 @@ const operatorsMetaMethods: LuaMetaMethod = { nativeImplementation: (a, b) => a <= b, }, ">": { - nativeImplementation: (a, b) => !luaOp("<=", a, b), + nativeImplementation: (a, b, ctx, sf) => !luaOp("<=", a, b, ctx, sf), }, ">=": { - nativeImplementation: (a, b) => !luaOp("<", a, b), + nativeImplementation: (a, b, ctx, cf) => !luaOp("<", a, b, ctx, cf), }, and: { metaMethod: "__and", @@ -387,63 +412,59 @@ const operatorsMetaMethods: LuaMetaMethod = { }, }; -function luaOp(op: string, left: any, right: any): any { +function luaOp( + op: string, + left: any, + right: any, + ctx: ASTCtx, + sf: LuaStackFrame, +): any { const operatorHandler = operatorsMetaMethods[op]; if (!operatorHandler) { - throw new Error(`Unknown operator ${op}`); + throw new LuaRuntimeError(`Unknown operator ${op}`, sf.withCtx(ctx)); } if (operatorHandler.metaMethod) { if (left?.metatable?.has(operatorHandler.metaMethod)) { const fn = left.metatable.get(operatorHandler.metaMethod); - if (!fn.call) { - throw new Error( - `Meta method ${operatorHandler.metaMethod} is not callable`, - ); - } else { - return fn.call(left, right); - } + return luaCall(fn, [left, right], ctx, sf); } else if (right?.metatable?.has(operatorHandler.metaMethod)) { const fn = right.metatable.get(operatorHandler.metaMethod); - if (!fn.call) { - throw new Error( - `Meta method ${operatorHandler.metaMethod} is not callable`, - ); - } else { - return fn.call(right, left); - } + return luaCall(fn, [left, right], ctx, sf); } } - return operatorHandler.nativeImplementation(left, right); + return operatorHandler.nativeImplementation(left, right, ctx, sf); } async function evalExpressions( es: LuaExpression[], env: LuaEnv, + sf: LuaStackFrame, ): Promise { return new LuaMultiRes( - await Promise.all(es.map((e) => evalExpression(e, env))), + await Promise.all(es.map((e) => evalExpression(e, env, sf))), ).flatten().values; } export async function evalStatement( s: LuaStatement, env: LuaEnv, + sf: LuaStackFrame, ): Promise { switch (s.type) { case "Assignment": { - const values = await evalExpressions(s.expressions, env); + const values = await evalExpressions(s.expressions, env, sf); const lvalues = await evalPromiseValues(s.variables - .map((lval) => evalLValue(lval, env))); + .map((lval) => evalLValue(lval, env, sf))); for (let i = 0; i < lvalues.length; i++) { - luaSet(lvalues[i].env, lvalues[i].key, values[i], s.ctx); + luaSet(lvalues[i].env, lvalues[i].key, values[i], sf.withCtx(s.ctx)); } break; } case "Local": { if (s.expressions) { - const values = await evalExpressions(s.expressions, env); + const values = await evalExpressions(s.expressions, env, sf); for (let i = 0; i < s.names.length; i++) { env.setLocal(s.names[i].name, values[i]); } @@ -462,25 +483,25 @@ export async function evalStatement( case "Block": { const newEnv = new LuaEnv(env); for (const statement of s.statements) { - await evalStatement(statement, newEnv); + await evalStatement(statement, newEnv, sf); } break; } case "If": { for (const cond of s.conditions) { - if (luaTruthy(await evalExpression(cond.condition, env))) { - return evalStatement(cond.block, env); + if (luaTruthy(await evalExpression(cond.condition, env, sf))) { + return evalStatement(cond.block, env, sf); } } if (s.elseBlock) { - return evalStatement(s.elseBlock, env); + return evalStatement(s.elseBlock, env, sf); } break; } case "While": { - while (luaTruthy(await evalExpression(s.condition, env))) { + while (luaTruthy(await evalExpression(s.condition, env, sf))) { try { - await evalStatement(s.block, env); + await evalStatement(s.block, env, sf); } catch (e: any) { if (e instanceof LuaBreak) { break; @@ -494,7 +515,7 @@ export async function evalStatement( case "Repeat": { do { try { - await evalStatement(s.block, env); + await evalStatement(s.block, env, sf); } catch (e: any) { if (e instanceof LuaBreak) { break; @@ -502,13 +523,13 @@ export async function evalStatement( throw e; } } - } while (!luaTruthy(await evalExpression(s.condition, env))); + } while (!luaTruthy(await evalExpression(s.condition, env, sf))); break; } case "Break": throw new LuaBreak(); case "FunctionCallStatement": { - return evalExpression(s.call, env); + return evalExpression(s.call, env, sf); } case "Function": { let body = s.body; @@ -527,7 +548,7 @@ export async function evalStatement( if (!settable) { throw new LuaRuntimeError( `Cannot find property ${propNames[i]}`, - s.name.ctx, + sf.withCtx(s.name.ctx), ); } } @@ -549,14 +570,14 @@ export async function evalStatement( // be optimized for the common case later throw new LuaReturn( await evalPromiseValues( - s.expressions.map((value) => evalExpression(value, env)), + s.expressions.map((value) => evalExpression(value, env, sf)), ), ); } 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 start = await evalExpression(s.start, env, sf); + const end = await evalExpression(s.end, env, sf); + const step = s.step ? await evalExpression(s.step, env, sf) : 1; const localEnv = new LuaEnv(env); for ( let i = start; @@ -565,7 +586,7 @@ export async function evalStatement( ) { localEnv.setLocal(s.name, i); try { - await evalStatement(s.block, localEnv); + await evalStatement(s.block, localEnv, sf); } catch (e: any) { if (e instanceof LuaBreak) { break; @@ -579,7 +600,7 @@ export async function evalStatement( case "ForIn": { const iteratorMultiRes = new LuaMultiRes( await evalPromiseValues( - s.expressions.map((e) => evalExpression(e, env)), + s.expressions.map((e) => evalExpression(e, env, sf)), ), ).flatten(); const iteratorFunction: ILuaFunction | undefined = @@ -588,7 +609,7 @@ export async function evalStatement( console.error("Cannot iterate over", iteratorMultiRes.values[0]); throw new LuaRuntimeError( `Cannot iterate over ${iteratorMultiRes.values[0]}`, - s.ctx, + sf.withCtx(s.ctx), ); } @@ -597,7 +618,7 @@ export async function evalStatement( while (true) { const iterResult = new LuaMultiRes( - await iteratorFunction.call(state, control), + await luaCall(iteratorFunction, [state, control], s.ctx, sf), ).flatten(); if ( iterResult.values[0] === null || iterResult.values[0] === undefined @@ -609,7 +630,7 @@ export async function evalStatement( localEnv.setLocal(s.names[i], iterResult.values[i]); } try { - await evalStatement(s.block, localEnv); + await evalStatement(s.block, localEnv, sf); } catch (e: any) { if (e instanceof LuaBreak) { break; @@ -626,6 +647,7 @@ export async function evalStatement( function evalLValue( lval: LuaLValue, env: LuaEnv, + sf: LuaStackFrame, ): LuaLValueContainer | Promise { switch (lval.type) { case "Variable": @@ -634,8 +656,9 @@ function evalLValue( const objValue = evalExpression( lval.object, env, + sf, ); - const keyValue = evalExpression(lval.key, env); + const keyValue = evalExpression(lval.key, env, sf); if ( objValue instanceof Promise || keyValue instanceof Promise @@ -658,6 +681,7 @@ function evalLValue( const objValue = evalExpression( lval.object, env, + sf, ); if (objValue instanceof Promise) { return objValue.then((objValue) => { diff --git a/common/space_lua/language.test.ts b/common/space_lua/language.test.ts index 698287b5..372e2afc 100644 --- a/common/space_lua/language.test.ts +++ b/common/space_lua/language.test.ts @@ -1,9 +1,12 @@ import { parse } from "$common/space_lua/parse.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; -import { LuaEnv, type LuaRuntimeError } from "$common/space_lua/runtime.ts"; +import { + LuaEnv, + type LuaRuntimeError, + LuaStackFrame, +} from "$common/space_lua/runtime.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; import { assert } from "@std/assert/assert"; - Deno.test("Lua language tests", async () => { // Read the Lua file const luaFile = await Deno.readTextFile( @@ -11,9 +14,10 @@ Deno.test("Lua language tests", async () => { ); const chunk = parse(luaFile, {}); const env = new LuaEnv(luaBuildStandardEnv()); + const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx); try { - await evalStatement(chunk, env); + await evalStatement(chunk, env, sf); } catch (e: any) { console.error(`Error evaluating script:`, toPrettyString(e, luaFile)); assert(false); @@ -21,22 +25,32 @@ Deno.test("Lua language tests", async () => { }); function toPrettyString(err: LuaRuntimeError, code: string): string { - if (!err.context || !err.context.from || !err.context.to) { + if (!err.sf || !err.sf.astCtx?.from || !err.sf.astCtx?.to) { return err.toString(); } - const from = err.context.from; - // Find the line and column - let line = 1; - let column = 0; - for (let i = 0; i < from; i++) { - if (code[i] === "\n") { - line++; - column = 0; - } else { - column++; + let traceStr = ""; + let current: LuaStackFrame | undefined = err.sf; + while (current) { + const ctx = current.astCtx; + if (!ctx || !ctx.from || !ctx.to) { + break; + } + // Find the line and column + let line = 1; + let column = 0; + for (let i = 0; i < ctx.from; i++) { + if (code[i] === "\n") { + line++; + column = 0; + } else { + column++; + } } + traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n ${ + code.substring(ctx.from, ctx.to) + }\n`; + current = current.parent; } - return `LuaRuntimeError: ${err.message} at ${line}:${column}:\n ${ - code.substring(from, err.context.to) - }`; + + return `LuaRuntimeError: ${err.message} ${traceStr}`; } diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_test.lua index eae2ac13..0da9956c 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_test.lua @@ -125,7 +125,7 @@ mt = { t = setmetatable({}, mt) t.bar = "bar" assert(t.bar == "bar") -assert(t.foo == "Key not found: foo") +assert_equal(t.foo, "Key not found: foo") -- Test the __newindex metamethod t = setmetatable( diff --git a/common/space_lua/runtime.test.ts b/common/space_lua/runtime.test.ts index fdefe30f..1d38cc50 100644 --- a/common/space_lua/runtime.test.ts +++ b/common/space_lua/runtime.test.ts @@ -3,6 +3,7 @@ import { jsToLuaValue, luaLen, LuaMultiRes, + LuaStackFrame, } from "$common/space_lua/runtime.ts"; Deno.test("Test Lua Rutime", () => { @@ -40,5 +41,5 @@ Deno.test("Test Lua Rutime", () => { assertEquals(luaVal.get(2).get("name"), "John"); // Functions in objects luaVal = jsToLuaValue({ name: "Pete", first: (l: any[]) => l[0] }); - assertEquals(luaVal.get("first").call([1, 2, 3]), 1); + assertEquals(luaVal.get("first").call(LuaStackFrame.lostFrame, [1, 2, 3]), 1); }); diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 4251074b..f2677102 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -17,16 +17,16 @@ export type LuaValue = any; export type JSValue = any; export interface ILuaFunction { - call(...args: LuaValue[]): Promise | LuaValue; + call(sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue; toString(): string; } export interface ILuaSettable { - set(key: LuaValue, value: LuaValue): void; + set(key: LuaValue, value: LuaValue, sf?: LuaStackFrame): void; } export interface ILuaGettable { - get(key: LuaValue): LuaValue | undefined; + get(key: LuaValue, sf?: LuaStackFrame): LuaValue | undefined; } export class LuaEnv implements ILuaSettable, ILuaGettable { @@ -39,20 +39,30 @@ export class LuaEnv implements ILuaSettable, ILuaGettable { this.variables.set(name, value); } - set(key: string, value: LuaValue): void { + set(key: string, value: LuaValue, sf?: LuaStackFrame): void { if (this.variables.has(key) || !this.parent) { this.variables.set(key, value); } else { - this.parent.set(key, value); + this.parent.set(key, value, sf); } } - get(name: string): LuaValue | undefined { + has(key: string): boolean { + if (this.variables.has(key)) { + return true; + } + if (this.parent) { + return this.parent.has(key); + } + return false; + } + + get(name: string, sf?: LuaStackFrame): LuaValue | undefined { if (this.variables.has(name)) { return this.variables.get(name); } if (this.parent) { - return this.parent.get(name); + return this.parent.get(name, sf); } return undefined; } @@ -69,6 +79,21 @@ export class LuaEnv implements ILuaSettable, ILuaGettable { } } +export class LuaStackFrame { + constructor( + readonly threadLocal: LuaEnv, + readonly astCtx: ASTCtx | null, + readonly parent?: LuaStackFrame, + ) { + } + + withCtx(ctx: ASTCtx): LuaStackFrame { + return new LuaStackFrame(this.threadLocal, ctx, this); + } + + static lostFrame = new LuaStackFrame(new LuaEnv(), null); +} + export class LuaMultiRes { values: any[]; @@ -110,12 +135,16 @@ export function singleResult(value: any): any { } export class LuaFunction implements ILuaFunction { - constructor(private body: LuaFunctionBody, private closure: LuaEnv) { + constructor(readonly body: LuaFunctionBody, private closure: LuaEnv) { } - call(...args: LuaValue[]): Promise | LuaValue { + call(sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue { // Create a new environment for this function call const env = new LuaEnv(this.closure); + if (!sf) { + console.trace(sf); + } + env.setLocal("_CTX", sf.threadLocal); // Assign the passed arguments to the parameters for (let i = 0; i < this.body.parameters.length; i++) { let arg = args[i]; @@ -124,7 +153,7 @@ export class LuaFunction implements ILuaFunction { } env.setLocal(this.body.parameters[i], arg); } - return evalStatement(this.body.block, env).catch((e: any) => { + return evalStatement(this.body.block, env, sf).catch((e: any) => { if (e instanceof LuaReturn) { if (e.values.length === 0) { return; @@ -149,7 +178,7 @@ export class LuaNativeJSFunction implements ILuaFunction { } // Performs automatic conversion between Lua and JS values - call(...args: LuaValue[]): Promise | LuaValue { + call(_sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue { const result = this.fn(...args.map(luaValueToJS)); if (result instanceof Promise) { return result.then(jsToLuaValue); @@ -164,11 +193,13 @@ export class LuaNativeJSFunction implements ILuaFunction { } export class LuaBuiltinFunction implements ILuaFunction { - constructor(readonly fn: (...args: LuaValue[]) => LuaValue) { + constructor( + readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue, + ) { } - call(...args: LuaValue[]): Promise | LuaValue { - return this.fn(...args); + call(sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue { + return this.fn(sf, ...args); } toString(): string { @@ -236,14 +267,13 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } } - set(key: LuaValue, value: LuaValue): void { + set(key: LuaValue, value: LuaValue, sf?: LuaStackFrame): void { // New index handling for metatables if (this.metatable && this.metatable.has("__newindex") && !this.has(key)) { - const metaValue = this.metatable.get("__newindex"); - if (metaValue.call) { - metaValue.call(this, key, value); - return; - } + const metaValue = this.metatable.get("__newindex", sf); + // TODO: This may return a promise, we should handle that + luaCall(metaValue, [this, key, value], metaValue.ctx, sf); + return; } this.rawSet(key, value); @@ -259,16 +289,16 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } } - get(key: LuaValue): LuaValue | null { + get(key: LuaValue, sf?: LuaStackFrame): LuaValue | null { const value = this.rawGet(key); if (value === undefined || value === null) { // Invoke the meta table if (this.metatable) { - const metaValue = this.metatable.get("__index"); + const metaValue = this.metatable.get("__index", sf); if (metaValue.call) { - return metaValue.call(this, key); + return metaValue.call(sf, this, key); } else if (metaValue instanceof LuaTable) { - return metaValue.get(key); + return metaValue.get(key, sf); } else { throw new Error("Meta table __index must be a function or table"); } @@ -285,10 +315,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable { this.arrayPart.splice(pos - 1, 1); } - async sort(fn?: ILuaFunction) { - if (fn) { + async sort(fn?: ILuaFunction, sf?: LuaStackFrame) { + if (fn && sf) { this.arrayPart = await asyncQuickSort(this.arrayPart, async (a, b) => { - return (await fn.call(a, b)) ? -1 : 1; + return (await fn.call(sf, a, b)) ? -1 : 1; }); } else { this.arrayPart.sort(); @@ -311,7 +341,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable { if (this.metatable?.has("__tostring")) { const metaValue = this.metatable.get("__tostring"); if (metaValue.call) { - return metaValue.call(this); + return metaValue.call(LuaStackFrame.lostFrame, this); } else { throw new Error("Meta table __tostring must be a function"); } @@ -342,37 +372,37 @@ export class LuaTable implements ILuaSettable, ILuaGettable { export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue }; -export function luaSet(obj: any, key: any, value: any, ctx: ASTCtx) { +export function luaSet(obj: any, key: any, value: any, sf: LuaStackFrame) { if (!obj) { throw new LuaRuntimeError( `Not a settable object: nil`, - ctx, + sf, ); } if (obj instanceof LuaTable || obj instanceof LuaEnv) { - obj.set(key, value); + obj.set(key, value, sf); } else { obj[key] = value; } } -export function luaGet(obj: any, key: any, ctx: ASTCtx): any { +export function luaGet(obj: any, key: any, sf: LuaStackFrame): any { if (!obj) { throw new LuaRuntimeError( `Attempting to index a nil value`, - ctx, + sf, ); } if (key === null || key === undefined) { throw new LuaRuntimeError( `Attempting to index with a nil key`, - ctx, + sf, ); } if (obj instanceof LuaTable || obj instanceof LuaEnv) { - return obj.get(key); + return obj.get(key, sf); } else if (typeof key === "number") { return obj[key - 1]; } else { @@ -397,11 +427,16 @@ export function luaLen(obj: any): number { } } -export function luaCall(fn: any, args: any[], ctx: ASTCtx): any { +export function luaCall( + fn: any, + args: any[], + ctx: ASTCtx, + sf?: LuaStackFrame, +): any { if (!fn) { throw new LuaRuntimeError( `Attempting to call a nil value`, - ctx, + (sf || LuaStackFrame.lostFrame).withCtx(ctx), ); } if (typeof fn === "function") { @@ -409,7 +444,13 @@ export function luaCall(fn: any, args: any[], ctx: ASTCtx): any { // Native JS function return fn(...jsArgs); } - return fn.call(...args); + if (!fn.call) { + throw new LuaRuntimeError( + `Attempting to call a non-callable value`, + (sf || LuaStackFrame.lostFrame).withCtx(ctx), + ); + } + return fn.call((sf || LuaStackFrame.lostFrame).withCtx(ctx), ...args); } export function luaTypeOf(val: any): LuaType { @@ -445,14 +486,14 @@ export class LuaReturn extends Error { export class LuaRuntimeError extends Error { constructor( override readonly message: string, - public context: ASTCtx, + public sf: LuaStackFrame, cause?: Error, ) { super(message, cause); } override toString() { - return `LuaRuntimeError: ${this.message} at ${this.context.from}, ${this.context.to}`; + return `LuaRuntimeError: ${this.message} at ${this.sf.astCtx?.from}, ${this.sf.astCtx?.to}`; } } diff --git a/common/space_lua/stdlib.ts b/common/space_lua/stdlib.ts index e44a5d14..f039ee9e 100644 --- a/common/space_lua/stdlib.ts +++ b/common/space_lua/stdlib.ts @@ -1,8 +1,10 @@ import { type ILuaFunction, LuaBuiltinFunction, + luaCall, LuaEnv, LuaMultiRes, + LuaRuntimeError, type LuaTable, luaToString, luaTypeOf, @@ -13,19 +15,19 @@ import { tableApi } from "$common/space_lua/stdlib/table.ts"; import { osApi } from "$common/space_lua/stdlib/os.ts"; import { jsApi } from "$common/space_lua/stdlib/js.ts"; -const printFunction = new LuaBuiltinFunction((...args) => { +const printFunction = new LuaBuiltinFunction((_sf, ...args) => { console.log("[Lua]", ...args.map(luaToString)); }); const assertFunction = new LuaBuiltinFunction( - async (value: any, message?: string) => { + async (sf, value: any, message?: string) => { if (!await value) { - throw new Error(`Assertion failed: ${message}`); + throw new LuaRuntimeError(`Assertion failed: ${message}`, sf); } }, ); -const ipairsFunction = new LuaBuiltinFunction((ar: LuaTable) => { +const ipairsFunction = new LuaBuiltinFunction((_sf, ar: LuaTable) => { let i = 1; return () => { if (i > ar.length) { @@ -37,7 +39,7 @@ const ipairsFunction = new LuaBuiltinFunction((ar: LuaTable) => { }; }); -const pairsFunction = new LuaBuiltinFunction((t: LuaTable) => { +const pairsFunction = new LuaBuiltinFunction((_sf, t: LuaTable) => { const keys = t.keys(); let i = 0; return () => { @@ -50,7 +52,7 @@ const pairsFunction = new LuaBuiltinFunction((t: LuaTable) => { }; }); -const unpackFunction = new LuaBuiltinFunction((t: LuaTable) => { +const unpackFunction = new LuaBuiltinFunction((_sf, t: LuaTable) => { const values: LuaValue[] = []; for (let i = 1; i <= t.length; i++) { values.push(t.get(i)); @@ -58,26 +60,26 @@ const unpackFunction = new LuaBuiltinFunction((t: LuaTable) => { return new LuaMultiRes(values); }); -const typeFunction = new LuaBuiltinFunction((value: LuaValue): string => { +const typeFunction = new LuaBuiltinFunction((_sf, value: LuaValue): string => { return luaTypeOf(value); }); -const tostringFunction = new LuaBuiltinFunction((value: any) => { +const tostringFunction = new LuaBuiltinFunction((_sf, value: any) => { return luaToString(value); }); -const tonumberFunction = new LuaBuiltinFunction((value: LuaValue) => { +const tonumberFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => { return Number(value); }); -const errorFunction = new LuaBuiltinFunction((message: string) => { +const errorFunction = new LuaBuiltinFunction((_sf, message: string) => { throw new Error(message); }); const pcallFunction = new LuaBuiltinFunction( - async (fn: ILuaFunction, ...args) => { + async (sf, fn: ILuaFunction, ...args) => { try { - return new LuaMultiRes([true, await fn.call(...args)]); + return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]); } catch (e: any) { return new LuaMultiRes([false, e.message]); } @@ -85,30 +87,33 @@ const pcallFunction = new LuaBuiltinFunction( ); const xpcallFunction = new LuaBuiltinFunction( - async (fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => { + async (sf, fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => { try { - return new LuaMultiRes([true, await fn.call(...args)]); + return new LuaMultiRes([true, await fn.call(sf, ...args)]); } catch (e: any) { - return new LuaMultiRes([false, await errorHandler.call(e.message)]); + return new LuaMultiRes([ + false, + await luaCall(errorHandler, [e.message], sf.astCtx!, sf), + ]); } }, ); const setmetatableFunction = new LuaBuiltinFunction( - (table: LuaTable, metatable: LuaTable) => { + (_sf, table: LuaTable, metatable: LuaTable) => { table.metatable = metatable; return table; }, ); const rawsetFunction = new LuaBuiltinFunction( - (table: LuaTable, key: LuaValue, value: LuaValue) => { + (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => { table.rawSet(key, value); return table; }, ); -const getmetatableFunction = new LuaBuiltinFunction((table: LuaTable) => { +const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => { return table.metatable; }); diff --git a/common/space_lua/stdlib/js.ts b/common/space_lua/stdlib/js.ts index b26d2109..7093b77f 100644 --- a/common/space_lua/stdlib/js.ts +++ b/common/space_lua/stdlib/js.ts @@ -13,13 +13,13 @@ export const jsApi = new LuaTable({ ); }, ), - importModule: new LuaBuiltinFunction((url) => { + importModule: new LuaBuiltinFunction((_sf, url) => { return import(url); }), - tolua: new LuaBuiltinFunction(jsToLuaValue), - tojs: new LuaBuiltinFunction(luaValueToJS), - log: new LuaBuiltinFunction((...args) => { + tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val)), + tojs: new LuaBuiltinFunction((_sf, val) => luaValueToJS(val)), + log: new LuaBuiltinFunction((_sf, ...args) => { console.log(...args); }), // assignGlobal: new LuaBuiltinFunction((name: string, value: any) => { diff --git a/common/space_lua/stdlib/os.ts b/common/space_lua/stdlib/os.ts index 39ede481..947e975f 100644 --- a/common/space_lua/stdlib/os.ts +++ b/common/space_lua/stdlib/os.ts @@ -1,7 +1,7 @@ import { LuaBuiltinFunction, LuaTable } from "$common/space_lua/runtime.ts"; export const osApi = new LuaTable({ - time: new LuaBuiltinFunction((tbl?: LuaTable) => { + time: new LuaBuiltinFunction((_sf, tbl?: LuaTable) => { if (tbl) { // Build a date object from the table const date = new Date(); @@ -32,7 +32,7 @@ export const osApi = new LuaTable({ * If format is not "*t", then date returns the date as a string, formatted according to the same rules as the ISO C function strftime. * If format is absent, it defaults to "%c", which gives a human-readable date and time representation using the current locale. */ - date: new LuaBuiltinFunction((format: string, timestamp?: number) => { + date: new LuaBuiltinFunction((_sf, format: string, timestamp?: number) => { const date = timestamp ? new Date(timestamp * 1000) : new Date(); // Default Lua-like format when no format string is provided diff --git a/common/space_lua/stdlib/string.ts b/common/space_lua/stdlib/string.ts index ae3d1031..d8aa4c32 100644 --- a/common/space_lua/stdlib/string.ts +++ b/common/space_lua/stdlib/string.ts @@ -6,7 +6,7 @@ import { } from "$common/space_lua/runtime.ts"; export const stringApi = new LuaTable({ - byte: new LuaBuiltinFunction((s: string, i?: number, j?: number) => { + byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => { i = i ?? 1; j = j ?? i; const result = []; @@ -15,11 +15,11 @@ export const stringApi = new LuaTable({ } return new LuaMultiRes(result); }), - char: new LuaBuiltinFunction((...args: number[]) => { + char: new LuaBuiltinFunction((_sf, ...args: number[]) => { return String.fromCharCode(...args); }), find: new LuaBuiltinFunction( - (s: string, pattern: string, init?: number, plain?: boolean) => { + (_sf, s: string, pattern: string, init?: number, plain?: boolean) => { init = init ?? 1; plain = plain ?? false; const result = s.slice(init - 1).match(pattern); @@ -32,7 +32,7 @@ export const stringApi = new LuaTable({ ]); }, ), - format: new LuaBuiltinFunction((format: string, ...args: any[]) => { + format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => { return format.replace(/%./g, (match) => { switch (match) { case "%s": @@ -44,7 +44,7 @@ export const stringApi = new LuaTable({ } }); }), - gmatch: new LuaBuiltinFunction((s: string, pattern: string) => { + gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => { const regex = new RegExp(pattern, "g"); return () => { const result = regex.exec(s); @@ -55,7 +55,7 @@ export const stringApi = new LuaTable({ }; }), gsub: new LuaBuiltinFunction( - (s: string, pattern: string, repl: string, n?: number) => { + (_sf, s: string, pattern: string, repl: string, n?: number) => { n = n ?? Infinity; const regex = new RegExp(pattern, "g"); let result = s; @@ -70,17 +70,17 @@ export const stringApi = new LuaTable({ return result; }, ), - len: new LuaBuiltinFunction((s: string) => { + len: new LuaBuiltinFunction((_sf, s: string) => { return s.length; }), - lower: new LuaBuiltinFunction((s: string) => { + lower: new LuaBuiltinFunction((_sf, s: string) => { return luaToString(s.toLowerCase()); }), - upper: new LuaBuiltinFunction((s: string) => { + upper: new LuaBuiltinFunction((_sf, s: string) => { return luaToString(s.toUpperCase()); }), match: new LuaBuiltinFunction( - (s: string, pattern: string, init?: number) => { + (_sf, s: string, pattern: string, init?: number) => { init = init ?? 1; const result = s.slice(init - 1).match(pattern); if (!result) { @@ -89,18 +89,18 @@ export const stringApi = new LuaTable({ return new LuaMultiRes(result.slice(1)); }, ), - rep: new LuaBuiltinFunction((s: string, n: number, sep?: string) => { + rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => { sep = sep ?? ""; return s.repeat(n) + sep; }), - reverse: new LuaBuiltinFunction((s: string) => { + reverse: new LuaBuiltinFunction((_sf, s: string) => { return s.split("").reverse().join(""); }), - sub: new LuaBuiltinFunction((s: string, i: number, j?: number) => { + sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => { j = j ?? s.length; return s.slice(i - 1, j); }), - split: new LuaBuiltinFunction((s: string, sep: string) => { + split: new LuaBuiltinFunction((_sf, s: string, sep: string) => { return s.split(sep); }), }); diff --git a/common/space_lua/stdlib/table.ts b/common/space_lua/stdlib/table.ts index b6644c03..72c03c49 100644 --- a/common/space_lua/stdlib/table.ts +++ b/common/space_lua/stdlib/table.ts @@ -6,7 +6,7 @@ import { export const tableApi = new LuaTable({ concat: new LuaBuiltinFunction( - (tbl: LuaTable, sep?: string, i?: number, j?: number) => { + (_sf, tbl: LuaTable, sep?: string, i?: number, j?: number) => { sep = sep ?? ""; i = i ?? 1; j = j ?? tbl.length; @@ -18,7 +18,7 @@ export const tableApi = new LuaTable({ }, ), insert: new LuaBuiltinFunction( - (tbl: LuaTable, posOrValue: number | any, value?: any) => { + (_sf, tbl: LuaTable, posOrValue: number | any, value?: any) => { if (value === undefined) { value = posOrValue; posOrValue = tbl.length + 1; @@ -26,11 +26,11 @@ export const tableApi = new LuaTable({ tbl.insert(posOrValue, value); }, ), - remove: new LuaBuiltinFunction((tbl: LuaTable, pos?: number) => { + remove: new LuaBuiltinFunction((_sf, tbl: LuaTable, pos?: number) => { pos = pos ?? tbl.length; tbl.remove(pos); }), - sort: new LuaBuiltinFunction((tbl: LuaTable, comp?: ILuaFunction) => { - return tbl.sort(comp); + sort: new LuaBuiltinFunction((sf, tbl: LuaTable, comp?: ILuaFunction) => { + return tbl.sort(comp, sf); }), }); diff --git a/common/space_lua_api.ts b/common/space_lua_api.ts index 8b1be345..2444a774 100644 --- a/common/space_lua_api.ts +++ b/common/space_lua_api.ts @@ -5,6 +5,7 @@ import { LuaBuiltinFunction, LuaEnv, LuaNativeJSFunction, + LuaStackFrame, LuaTable, } from "$common/space_lua/runtime.ts"; import type { System } from "$lib/plugos/system.ts"; @@ -24,17 +25,18 @@ export function buildLuaEnv(system: System, scriptEnv: ScriptEnvironment) { function exposeSyscalls(env: LuaEnv, system: System) { // Expose all syscalls to Lua + const nativeFs = new LuaStackFrame(env, null); for (const syscallName of system.registeredSyscalls.keys()) { const [ns, fn] = syscallName.split("."); - if (!env.get(ns)) { - env.set(ns, new LuaTable()); + if (!env.has(ns)) { + env.set(ns, new LuaTable(), nativeFs); } const luaFn = new LuaNativeJSFunction((...args) => { return system.localSyscall(syscallName, args); }); // Register the function with the same name as the syscall both in regular and snake_case - env.get(ns).set(fn, luaFn); - env.get(ns).set(snakeCase(fn), luaFn); + env.get(ns, nativeFs).set(fn, luaFn, nativeFs); + env.get(ns, nativeFs).set(snakeCase(fn), luaFn, nativeFs); } } @@ -47,7 +49,7 @@ function exposeDefinitions( env.set( "define_command", new LuaBuiltinFunction( - (def: LuaTable) => { + (_sf, def: LuaTable) => { if (def.get(1) === undefined) { throw new Error("Callback is required"); } @@ -65,10 +67,9 @@ function exposeDefinitions( hide: def.get("hide"), } as CommandDef, async (...args: any[]) => { + const sf = new LuaStackFrame(new LuaEnv(), null); try { - return await def.get(1).call( - ...args.map(jsToLuaValue), - ); + return await def.get(1).call(sf, ...args.map(jsToLuaValue)); } catch (e: any) { await handleLuaError(e, system); } @@ -79,7 +80,7 @@ function exposeDefinitions( ); env.set( "define_event_listener", - new LuaBuiltinFunction((def: LuaTable) => { + new LuaBuiltinFunction((_sf, def: LuaTable) => { if (def.get(1) === undefined) { throw new Error("Callback is required"); } @@ -90,10 +91,9 @@ function exposeDefinitions( scriptEnv.registerEventListener( { name: def.get("event") }, async (...args: any[]) => { + const sf = new LuaStackFrame(new LuaEnv(), null); try { - return await def.get(1).call( - ...args.map(jsToLuaValue), - ); + return await def.get(1).call(sf, ...args.map(jsToLuaValue)); } catch (e: any) { await handleLuaError(e, system); } diff --git a/web/cm_plugins/lua_directive.ts b/web/cm_plugins/lua_directive.ts index e41b499c..827f9654 100644 --- a/web/cm_plugins/lua_directive.ts +++ b/web/cm_plugins/lua_directive.ts @@ -14,7 +14,11 @@ import type { LuaFunctionCallStatement, } from "$common/space_lua/ast.ts"; import { evalExpression } from "$common/space_lua/eval.ts"; -import { luaValueToJS } from "$common/space_lua/runtime.ts"; +import { + LuaEnv, + LuaStackFrame, + luaValueToJS, +} from "$common/space_lua/runtime.ts"; import { LuaRuntimeError } from "$common/space_lua/runtime.ts"; import { encodePageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; import { resolveASTReference } from "$common/space_lua.ts"; @@ -54,16 +58,18 @@ export function luaDirectivePlugin(client: Client) { (parsedLua.statements[0] as LuaFunctionCallStatement).call .args[0]; + const sf = new LuaStackFrame(new LuaEnv(), expr.ctx); return luaValueToJS( await evalExpression( expr, client.clientSystem.spaceLuaEnv.env, + sf, ), ); } catch (e: any) { if (e instanceof LuaRuntimeError) { - if (e.context.ref) { - const source = resolveASTReference(e.context); + if (e.sf.astCtx) { + const source = resolveASTReference(e.sf.astCtx); if (source) { // We know the origin node of the error, let's reference it return {