Lua stack frame refactor

pull/1127/head
Zef Hemel 2024-10-20 15:06:23 +02:00
parent 64c98678bc
commit 8acb112e4e
14 changed files with 300 additions and 201 deletions

View File

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

View File

@ -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<void> {
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 () => {

View File

@ -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> | 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<void>[] = [];
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> | 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);
// <<expr>>[<<expr>>]
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));
}
}
// <expr>.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<string, {
metaMethod?: string;
nativeImplementation: (a: LuaValue, b: LuaValue) => 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<LuaValue[]> {
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<void> {
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<LuaLValueContainer> {
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) => {

View File

@ -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,14 +25,20 @@ 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;
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 < from; i++) {
for (let i = 0; i < ctx.from; i++) {
if (code[i] === "\n") {
line++;
column = 0;
@ -36,7 +46,11 @@ function toPrettyString(err: LuaRuntimeError, code: string): string {
column++;
}
}
return `LuaRuntimeError: ${err.message} at ${line}:${column}:\n ${
code.substring(from, err.context.to)
}`;
traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n ${
code.substring(ctx.from, ctx.to)
}\n`;
current = current.parent;
}
return `LuaRuntimeError: ${err.message} ${traceStr}`;
}

View File

@ -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(

View File

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

View File

@ -17,16 +17,16 @@ export type LuaValue = any;
export type JSValue = any;
export interface ILuaFunction {
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | 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> | LuaValue {
call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | 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> | LuaValue {
call(_sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | 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> | LuaValue {
return this.fn(...args);
call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
return this.fn(sf, ...args);
}
toString(): string {
@ -236,15 +267,14 @@ 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);
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}`;
}
}

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

@ -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<any>, scriptEnv: ScriptEnvironment) {
function exposeSyscalls(env: LuaEnv, system: System<any>) {
// 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);
}

View File

@ -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 {