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, LuaEnv,
LuaFunction, LuaFunction,
LuaRuntimeError, LuaRuntimeError,
LuaStackFrame,
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
import { parse as parseLua } from "$common/space_lua/parse.ts"; import { parse as parseLua } from "$common/space_lua/parse.ts";
import { evalStatement } from "$common/space_lua/eval.ts"; import { evalStatement } from "$common/space_lua/eval.ts";
@ -39,10 +40,11 @@ export class SpaceLuaEnvironment {
const ast = parseLua(script.script, { ref: script.ref }); const ast = parseLua(script.script, { ref: script.ref });
// We create a local scope for each script // We create a local scope for each script
const scriptEnv = new LuaEnv(this.env); 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) { } catch (e: any) {
if (e instanceof LuaRuntimeError) { if (e instanceof LuaRuntimeError) {
const origin = resolveASTReference(e.context); const origin = resolveASTReference(e.sf.astCtx!);
if (origin) { if (origin) {
console.error( console.error(
`Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`, `Error evaluating script: ${e.message} at [[${origin.page}@${origin.pos}]]`,
@ -62,7 +64,8 @@ export class SpaceLuaEnvironment {
if (value instanceof LuaFunction) { if (value instanceof LuaFunction) {
console.log("Now registering Lua function", globalName); console.log("Now registering Lua function", globalName);
scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => { 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 { import {
LuaEnv, LuaEnv,
LuaNativeJSFunction, LuaNativeJSFunction,
LuaStackFrame,
luaValueToJS, luaValueToJS,
singleResult, singleResult,
} from "./runtime.ts"; } from "./runtime.ts";
@ -11,15 +12,19 @@ import { evalExpression, evalStatement } from "./eval.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
function evalExpr(s: string, e = new LuaEnv()): any { 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( return evalExpression(
(parse(`e(${s})`).statements[0] as LuaFunctionCallStatement).call node.call.args[0],
.args[0],
e, e,
sf,
); );
} }
function evalBlock(s: string, e = new LuaEnv()): Promise<void> { 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 () => { Deno.test("Evaluator test", async () => {

View File

@ -1,10 +1,15 @@
import type { import type {
ASTCtx,
LuaExpression, LuaExpression,
LuaLValue, LuaLValue,
LuaStatement, LuaStatement,
} from "$common/space_lua/ast.ts"; } from "$common/space_lua/ast.ts";
import { evalPromiseValues } from "$common/space_lua/util.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 { import {
type ILuaFunction, type ILuaFunction,
type ILuaGettable, type ILuaGettable,
@ -28,6 +33,7 @@ import {
export function evalExpression( export function evalExpression(
e: LuaExpression, e: LuaExpression,
env: LuaEnv, env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue> | LuaValue { ): Promise<LuaValue> | LuaValue {
try { try {
switch (e.type) { switch (e.type) {
@ -41,23 +47,31 @@ export function evalExpression(
return null; return null;
case "Binary": { case "Binary": {
const values = evalPromiseValues([ const values = evalPromiseValues([
evalExpression(e.left, env), evalExpression(e.left, env, sf),
evalExpression(e.right, env), evalExpression(e.right, env, sf),
]); ]);
if (values instanceof Promise) { if (values instanceof Promise) {
return values.then(([left, right]) => return values.then(([left, right]) =>
luaOp(e.operator, singleResult(left), singleResult(right)) luaOp(
e.operator,
singleResult(left),
singleResult(right),
e.ctx,
sf,
)
); );
} else { } else {
return luaOp( return luaOp(
e.operator, e.operator,
singleResult(values[0]), singleResult(values[0]),
singleResult(values[1]), singleResult(values[1]),
e.ctx,
sf,
); );
} }
} }
case "Unary": { case "Unary": {
const value = evalExpression(e.argument, env); const value = evalExpression(e.argument, env, sf);
if (value instanceof Promise) { if (value instanceof Promise) {
return value.then((value) => { return value.then((value) => {
switch (e.operator) { switch (e.operator) {
@ -97,29 +111,30 @@ export function evalExpression(
case "FunctionCall": case "FunctionCall":
case "TableAccess": case "TableAccess":
case "PropertyAccess": case "PropertyAccess":
return evalPrefixExpression(e, env); return evalPrefixExpression(e, env, sf);
case "TableConstructor": { case "TableConstructor": {
const table = new LuaTable(); const table = new LuaTable();
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
for (const field of e.fields) { for (const field of e.fields) {
switch (field.type) { switch (field.type) {
case "PropField": { case "PropField": {
const value = evalExpression(field.value, env); const value = evalExpression(field.value, env, sf);
if (value instanceof Promise) { if (value instanceof Promise) {
promises.push(value.then((value) => { promises.push(value.then((value) => {
table.set( table.set(
field.key, field.key,
singleResult(value), singleResult(value),
sf,
); );
})); }));
} else { } else {
table.set(field.key, singleResult(value)); table.set(field.key, singleResult(value), sf);
} }
break; break;
} }
case "DynamicField": { case "DynamicField": {
const key = evalExpression(field.key, env); const key = evalExpression(field.key, env, sf);
const value = evalExpression(field.value, env); const value = evalExpression(field.value, env, sf);
if ( if (
key instanceof Promise || value instanceof Promise key instanceof Promise || value instanceof Promise
) { ) {
@ -131,6 +146,7 @@ export function evalExpression(
table.set( table.set(
singleResult(key), singleResult(key),
singleResult(value), singleResult(value),
sf,
); );
}), }),
); );
@ -138,18 +154,20 @@ export function evalExpression(
table.set( table.set(
singleResult(key), singleResult(key),
singleResult(value), singleResult(value),
sf,
); );
} }
break; break;
} }
case "ExpressionField": { case "ExpressionField": {
const value = evalExpression(field.value, env); const value = evalExpression(field.value, env, sf);
if (value instanceof Promise) { if (value instanceof Promise) {
promises.push(value.then((value) => { promises.push(value.then((value) => {
// +1 because Lua tables are 1-indexed // +1 because Lua tables are 1-indexed
table.set( table.set(
table.length + 1, table.length + 1,
singleResult(value), singleResult(value),
sf,
); );
})); }));
} else { } else {
@ -157,6 +175,7 @@ export function evalExpression(
table.set( table.set(
table.length + 1, table.length + 1,
singleResult(value), singleResult(value),
sf,
); );
} }
break; break;
@ -178,7 +197,7 @@ export function evalExpression(
} catch (err: any) { } catch (err: any) {
// Repackage any non Lua-specific exceptions with some position information // Repackage any non Lua-specific exceptions with some position information
if (!err.constructor.name.startsWith("Lua")) { 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 { } else {
throw err; throw err;
} }
@ -188,6 +207,7 @@ export function evalExpression(
function evalPrefixExpression( function evalPrefixExpression(
e: LuaExpression, e: LuaExpression,
env: LuaEnv, env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue> | LuaValue { ): Promise<LuaValue> | LuaValue {
switch (e.type) { switch (e.type) {
case "Variable": { case "Variable": {
@ -199,43 +219,43 @@ function evalPrefixExpression(
} }
} }
case "Parenthesized": case "Parenthesized":
return evalExpression(e.expression, env); return evalExpression(e.expression, env, sf);
// <<expr>>[<<expr>>] // <<expr>>[<<expr>>]
case "TableAccess": { case "TableAccess": {
const values = evalPromiseValues([ const values = evalPromiseValues([
evalPrefixExpression(e.object, env), evalPrefixExpression(e.object, env, sf),
evalExpression(e.key, env), evalExpression(e.key, env, sf),
]); ]);
if (values instanceof Promise) { if (values instanceof Promise) {
return values.then(([table, key]) => { return values.then(([table, key]) => {
table = singleResult(table); table = singleResult(table);
key = singleResult(key); key = singleResult(key);
return luaGet(table, key, e.ctx); return luaGet(table, key, sf.withCtx(e.ctx));
}); });
} else { } else {
const table = singleResult(values[0]); const table = singleResult(values[0]);
const key = singleResult(values[1]); const key = singleResult(values[1]);
return luaGet(table, singleResult(key), e.ctx); return luaGet(table, singleResult(key), sf.withCtx(e.ctx));
} }
} }
// <expr>.property // <expr>.property
case "PropertyAccess": { case "PropertyAccess": {
const obj = evalPrefixExpression(e.object, env); const obj = evalPrefixExpression(e.object, env, sf);
if (obj instanceof Promise) { if (obj instanceof Promise) {
return obj.then((obj) => { return obj.then((obj) => {
return luaGet(obj, e.property, e.ctx); return luaGet(obj, e.property, sf.withCtx(e.ctx));
}); });
} else { } else {
return luaGet(obj, e.property, e.ctx); return luaGet(obj, e.property, sf.withCtx(e.ctx));
} }
} }
case "FunctionCall": { case "FunctionCall": {
let prefixValue = evalPrefixExpression(e.prefix, env); let prefixValue = evalPrefixExpression(e.prefix, env, sf);
if (!prefixValue) { if (!prefixValue) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to call nil as a function`, `Attempting to call nil as a function`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} }
if (prefixValue instanceof Promise) { if (prefixValue instanceof Promise) {
@ -243,7 +263,7 @@ function evalPrefixExpression(
if (!prefixValue) { if (!prefixValue) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to call a nil value`, `Attempting to call a nil value`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} }
let selfArgs: LuaValue[] = []; let selfArgs: LuaValue[] = [];
@ -251,7 +271,7 @@ function evalPrefixExpression(
if (e.name && !prefixValue.get) { if (e.name && !prefixValue.get) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to index a non-table: ${prefixValue}`, `Attempting to index a non-table: ${prefixValue}`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} else if (e.name) { } 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 // 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) { if (!prefixValue.call) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to call ${prefixValue} as a function`, `Attempting to call ${prefixValue} as a function`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} }
const args = evalPromiseValues( const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)), e.args.map((arg) => evalExpression(arg, env, sf)),
); );
if (args instanceof Promise) { if (args instanceof Promise) {
return args.then((args) => return args.then((args) =>
luaCall(prefixValue, [...selfArgs, ...args], e.ctx) luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf)
); );
} else { } else {
return luaCall(prefixValue, [...selfArgs, ...args], e.ctx); return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf);
} }
}); });
} else { } else {
@ -281,7 +301,7 @@ function evalPrefixExpression(
if (e.name && !prefixValue.get) { if (e.name && !prefixValue.get) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to index a non-table: ${prefixValue}`, `Attempting to index a non-table: ${prefixValue}`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} else if (e.name) { } 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 // 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) { if (!prefixValue.call) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to call ${prefixValue} as a function`, `Attempting to call ${prefixValue} as a function`,
e.prefix.ctx, sf.withCtx(e.prefix.ctx),
); );
} }
const args = evalPromiseValues( const args = evalPromiseValues(
e.args.map((arg) => evalExpression(arg, env)), e.args.map((arg) => evalExpression(arg, env, sf)),
); );
if (args instanceof Promise) { if (args instanceof Promise) {
return args.then((args) => return args.then((args) =>
luaCall(prefixValue, [...selfArgs, ...args], e.ctx) luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf)
); );
} else { } 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, { type LuaMetaMethod = Record<string, {
metaMethod?: string; metaMethod?: string;
nativeImplementation: (a: LuaValue, b: LuaValue) => LuaValue; nativeImplementation: (
a: LuaValue,
b: LuaValue,
ctx: ASTCtx,
sf: LuaStackFrame,
) => LuaValue;
}>; }>;
const operatorsMetaMethods: LuaMetaMethod = { const operatorsMetaMethods: LuaMetaMethod = {
@ -372,10 +397,10 @@ const operatorsMetaMethods: LuaMetaMethod = {
nativeImplementation: (a, b) => a <= b, 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: { and: {
metaMethod: "__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]; const operatorHandler = operatorsMetaMethods[op];
if (!operatorHandler) { if (!operatorHandler) {
throw new Error(`Unknown operator ${op}`); throw new LuaRuntimeError(`Unknown operator ${op}`, sf.withCtx(ctx));
} }
if (operatorHandler.metaMethod) { if (operatorHandler.metaMethod) {
if (left?.metatable?.has(operatorHandler.metaMethod)) { if (left?.metatable?.has(operatorHandler.metaMethod)) {
const fn = left.metatable.get(operatorHandler.metaMethod); const fn = left.metatable.get(operatorHandler.metaMethod);
if (!fn.call) { return luaCall(fn, [left, right], ctx, sf);
throw new Error(
`Meta method ${operatorHandler.metaMethod} is not callable`,
);
} else {
return fn.call(left, right);
}
} else if (right?.metatable?.has(operatorHandler.metaMethod)) { } else if (right?.metatable?.has(operatorHandler.metaMethod)) {
const fn = right.metatable.get(operatorHandler.metaMethod); const fn = right.metatable.get(operatorHandler.metaMethod);
if (!fn.call) { return luaCall(fn, [left, right], ctx, sf);
throw new Error(
`Meta method ${operatorHandler.metaMethod} is not callable`,
);
} else {
return fn.call(right, left);
}
} }
} }
return operatorHandler.nativeImplementation(left, right); return operatorHandler.nativeImplementation(left, right, ctx, sf);
} }
async function evalExpressions( async function evalExpressions(
es: LuaExpression[], es: LuaExpression[],
env: LuaEnv, env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue[]> { ): Promise<LuaValue[]> {
return new LuaMultiRes( return new LuaMultiRes(
await Promise.all(es.map((e) => evalExpression(e, env))), await Promise.all(es.map((e) => evalExpression(e, env, sf))),
).flatten().values; ).flatten().values;
} }
export async function evalStatement( export async function evalStatement(
s: LuaStatement, s: LuaStatement,
env: LuaEnv, env: LuaEnv,
sf: LuaStackFrame,
): Promise<void> { ): Promise<void> {
switch (s.type) { switch (s.type) {
case "Assignment": { case "Assignment": {
const values = await evalExpressions(s.expressions, env); const values = await evalExpressions(s.expressions, env, sf);
const lvalues = await evalPromiseValues(s.variables 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++) { 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; break;
} }
case "Local": { case "Local": {
if (s.expressions) { 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++) { for (let i = 0; i < s.names.length; i++) {
env.setLocal(s.names[i].name, values[i]); env.setLocal(s.names[i].name, values[i]);
} }
@ -462,25 +483,25 @@ export async function evalStatement(
case "Block": { case "Block": {
const newEnv = new LuaEnv(env); const newEnv = new LuaEnv(env);
for (const statement of s.statements) { for (const statement of s.statements) {
await evalStatement(statement, newEnv); await evalStatement(statement, newEnv, sf);
} }
break; break;
} }
case "If": { case "If": {
for (const cond of s.conditions) { for (const cond of s.conditions) {
if (luaTruthy(await evalExpression(cond.condition, env))) { if (luaTruthy(await evalExpression(cond.condition, env, sf))) {
return evalStatement(cond.block, env); return evalStatement(cond.block, env, sf);
} }
} }
if (s.elseBlock) { if (s.elseBlock) {
return evalStatement(s.elseBlock, env); return evalStatement(s.elseBlock, env, sf);
} }
break; break;
} }
case "While": { case "While": {
while (luaTruthy(await evalExpression(s.condition, env))) { while (luaTruthy(await evalExpression(s.condition, env, sf))) {
try { try {
await evalStatement(s.block, env); await evalStatement(s.block, env, sf);
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaBreak) { if (e instanceof LuaBreak) {
break; break;
@ -494,7 +515,7 @@ export async function evalStatement(
case "Repeat": { case "Repeat": {
do { do {
try { try {
await evalStatement(s.block, env); await evalStatement(s.block, env, sf);
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaBreak) { if (e instanceof LuaBreak) {
break; break;
@ -502,13 +523,13 @@ export async function evalStatement(
throw e; throw e;
} }
} }
} while (!luaTruthy(await evalExpression(s.condition, env))); } while (!luaTruthy(await evalExpression(s.condition, env, sf)));
break; break;
} }
case "Break": case "Break":
throw new LuaBreak(); throw new LuaBreak();
case "FunctionCallStatement": { case "FunctionCallStatement": {
return evalExpression(s.call, env); return evalExpression(s.call, env, sf);
} }
case "Function": { case "Function": {
let body = s.body; let body = s.body;
@ -527,7 +548,7 @@ export async function evalStatement(
if (!settable) { if (!settable) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Cannot find property ${propNames[i]}`, `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 // be optimized for the common case later
throw new LuaReturn( throw new LuaReturn(
await evalPromiseValues( await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env)), s.expressions.map((value) => evalExpression(value, env, sf)),
), ),
); );
} }
case "For": { case "For": {
const start = await evalExpression(s.start, env); const start = await evalExpression(s.start, env, sf);
const end = await evalExpression(s.end, env); const end = await evalExpression(s.end, env, sf);
const step = s.step ? await evalExpression(s.step, env) : 1; const step = s.step ? await evalExpression(s.step, env, sf) : 1;
const localEnv = new LuaEnv(env); const localEnv = new LuaEnv(env);
for ( for (
let i = start; let i = start;
@ -565,7 +586,7 @@ export async function evalStatement(
) { ) {
localEnv.setLocal(s.name, i); localEnv.setLocal(s.name, i);
try { try {
await evalStatement(s.block, localEnv); await evalStatement(s.block, localEnv, sf);
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaBreak) { if (e instanceof LuaBreak) {
break; break;
@ -579,7 +600,7 @@ export async function evalStatement(
case "ForIn": { case "ForIn": {
const iteratorMultiRes = new LuaMultiRes( const iteratorMultiRes = new LuaMultiRes(
await evalPromiseValues( await evalPromiseValues(
s.expressions.map((e) => evalExpression(e, env)), s.expressions.map((e) => evalExpression(e, env, sf)),
), ),
).flatten(); ).flatten();
const iteratorFunction: ILuaFunction | undefined = const iteratorFunction: ILuaFunction | undefined =
@ -588,7 +609,7 @@ export async function evalStatement(
console.error("Cannot iterate over", iteratorMultiRes.values[0]); console.error("Cannot iterate over", iteratorMultiRes.values[0]);
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Cannot iterate over ${iteratorMultiRes.values[0]}`, `Cannot iterate over ${iteratorMultiRes.values[0]}`,
s.ctx, sf.withCtx(s.ctx),
); );
} }
@ -597,7 +618,7 @@ export async function evalStatement(
while (true) { while (true) {
const iterResult = new LuaMultiRes( const iterResult = new LuaMultiRes(
await iteratorFunction.call(state, control), await luaCall(iteratorFunction, [state, control], s.ctx, sf),
).flatten(); ).flatten();
if ( if (
iterResult.values[0] === null || iterResult.values[0] === undefined iterResult.values[0] === null || iterResult.values[0] === undefined
@ -609,7 +630,7 @@ export async function evalStatement(
localEnv.setLocal(s.names[i], iterResult.values[i]); localEnv.setLocal(s.names[i], iterResult.values[i]);
} }
try { try {
await evalStatement(s.block, localEnv); await evalStatement(s.block, localEnv, sf);
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaBreak) { if (e instanceof LuaBreak) {
break; break;
@ -626,6 +647,7 @@ export async function evalStatement(
function evalLValue( function evalLValue(
lval: LuaLValue, lval: LuaLValue,
env: LuaEnv, env: LuaEnv,
sf: LuaStackFrame,
): LuaLValueContainer | Promise<LuaLValueContainer> { ): LuaLValueContainer | Promise<LuaLValueContainer> {
switch (lval.type) { switch (lval.type) {
case "Variable": case "Variable":
@ -634,8 +656,9 @@ function evalLValue(
const objValue = evalExpression( const objValue = evalExpression(
lval.object, lval.object,
env, env,
sf,
); );
const keyValue = evalExpression(lval.key, env); const keyValue = evalExpression(lval.key, env, sf);
if ( if (
objValue instanceof Promise || objValue instanceof Promise ||
keyValue instanceof Promise keyValue instanceof Promise
@ -658,6 +681,7 @@ function evalLValue(
const objValue = evalExpression( const objValue = evalExpression(
lval.object, lval.object,
env, env,
sf,
); );
if (objValue instanceof Promise) { if (objValue instanceof Promise) {
return objValue.then((objValue) => { return objValue.then((objValue) => {

View File

@ -1,9 +1,12 @@
import { parse } from "$common/space_lua/parse.ts"; import { parse } from "$common/space_lua/parse.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.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 { evalStatement } from "$common/space_lua/eval.ts";
import { assert } from "@std/assert/assert"; import { assert } from "@std/assert/assert";
Deno.test("Lua language tests", async () => { Deno.test("Lua language tests", async () => {
// Read the Lua file // Read the Lua file
const luaFile = await Deno.readTextFile( const luaFile = await Deno.readTextFile(
@ -11,9 +14,10 @@ Deno.test("Lua language tests", async () => {
); );
const chunk = parse(luaFile, {}); const chunk = parse(luaFile, {});
const env = new LuaEnv(luaBuildStandardEnv()); const env = new LuaEnv(luaBuildStandardEnv());
const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx);
try { try {
await evalStatement(chunk, env); await evalStatement(chunk, env, sf);
} catch (e: any) { } catch (e: any) {
console.error(`Error evaluating script:`, toPrettyString(e, luaFile)); console.error(`Error evaluating script:`, toPrettyString(e, luaFile));
assert(false); assert(false);
@ -21,22 +25,32 @@ Deno.test("Lua language tests", async () => {
}); });
function toPrettyString(err: LuaRuntimeError, code: string): string { 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(); return err.toString();
} }
const from = err.context.from; let traceStr = "";
// Find the line and column let current: LuaStackFrame | undefined = err.sf;
let line = 1; while (current) {
let column = 0; const ctx = current.astCtx;
for (let i = 0; i < from; i++) { if (!ctx || !ctx.from || !ctx.to) {
if (code[i] === "\n") { break;
line++;
column = 0;
} else {
column++;
} }
// 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}`;
}`;
} }

View File

@ -125,7 +125,7 @@ mt = {
t = setmetatable({}, mt) t = setmetatable({}, mt)
t.bar = "bar" t.bar = "bar"
assert(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 -- Test the __newindex metamethod
t = setmetatable( t = setmetatable(

View File

@ -3,6 +3,7 @@ import {
jsToLuaValue, jsToLuaValue,
luaLen, luaLen,
LuaMultiRes, LuaMultiRes,
LuaStackFrame,
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
Deno.test("Test Lua Rutime", () => { Deno.test("Test Lua Rutime", () => {
@ -40,5 +41,5 @@ Deno.test("Test Lua Rutime", () => {
assertEquals(luaVal.get(2).get("name"), "John"); assertEquals(luaVal.get(2).get("name"), "John");
// Functions in objects // Functions in objects
luaVal = jsToLuaValue({ name: "Pete", first: (l: any[]) => l[0] }); 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 type JSValue = any;
export interface ILuaFunction { export interface ILuaFunction {
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue; call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue;
toString(): string; toString(): string;
} }
export interface ILuaSettable { export interface ILuaSettable {
set(key: LuaValue, value: LuaValue): void; set(key: LuaValue, value: LuaValue, sf?: LuaStackFrame): void;
} }
export interface ILuaGettable { export interface ILuaGettable {
get(key: LuaValue): LuaValue | undefined; get(key: LuaValue, sf?: LuaStackFrame): LuaValue | undefined;
} }
export class LuaEnv implements ILuaSettable, ILuaGettable { export class LuaEnv implements ILuaSettable, ILuaGettable {
@ -39,20 +39,30 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
this.variables.set(name, value); 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) { if (this.variables.has(key) || !this.parent) {
this.variables.set(key, value); this.variables.set(key, value);
} else { } 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)) { if (this.variables.has(name)) {
return this.variables.get(name); return this.variables.get(name);
} }
if (this.parent) { if (this.parent) {
return this.parent.get(name); return this.parent.get(name, sf);
} }
return undefined; 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 { export class LuaMultiRes {
values: any[]; values: any[];
@ -110,12 +135,16 @@ export function singleResult(value: any): any {
} }
export class LuaFunction implements ILuaFunction { 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 // Create a new environment for this function call
const env = new LuaEnv(this.closure); const env = new LuaEnv(this.closure);
if (!sf) {
console.trace(sf);
}
env.setLocal("_CTX", sf.threadLocal);
// Assign the passed arguments to the parameters // Assign the passed arguments to the parameters
for (let i = 0; i < this.body.parameters.length; i++) { for (let i = 0; i < this.body.parameters.length; i++) {
let arg = args[i]; let arg = args[i];
@ -124,7 +153,7 @@ export class LuaFunction implements ILuaFunction {
} }
env.setLocal(this.body.parameters[i], arg); 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 instanceof LuaReturn) {
if (e.values.length === 0) { if (e.values.length === 0) {
return; return;
@ -149,7 +178,7 @@ export class LuaNativeJSFunction implements ILuaFunction {
} }
// Performs automatic conversion between Lua and JS values // 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)); const result = this.fn(...args.map(luaValueToJS));
if (result instanceof Promise) { if (result instanceof Promise) {
return result.then(jsToLuaValue); return result.then(jsToLuaValue);
@ -164,11 +193,13 @@ export class LuaNativeJSFunction implements ILuaFunction {
} }
export class LuaBuiltinFunction 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 { call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
return this.fn(...args); return this.fn(sf, ...args);
} }
toString(): string { 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 // New index handling for metatables
if (this.metatable && this.metatable.has("__newindex") && !this.has(key)) { if (this.metatable && this.metatable.has("__newindex") && !this.has(key)) {
const metaValue = this.metatable.get("__newindex"); const metaValue = this.metatable.get("__newindex", sf);
if (metaValue.call) { // TODO: This may return a promise, we should handle that
metaValue.call(this, key, value); luaCall(metaValue, [this, key, value], metaValue.ctx, sf);
return; return;
}
} }
this.rawSet(key, value); 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); const value = this.rawGet(key);
if (value === undefined || value === null) { if (value === undefined || value === null) {
// Invoke the meta table // Invoke the meta table
if (this.metatable) { if (this.metatable) {
const metaValue = this.metatable.get("__index"); const metaValue = this.metatable.get("__index", sf);
if (metaValue.call) { if (metaValue.call) {
return metaValue.call(this, key); return metaValue.call(sf, this, key);
} else if (metaValue instanceof LuaTable) { } else if (metaValue instanceof LuaTable) {
return metaValue.get(key); return metaValue.get(key, sf);
} else { } else {
throw new Error("Meta table __index must be a function or table"); 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); this.arrayPart.splice(pos - 1, 1);
} }
async sort(fn?: ILuaFunction) { async sort(fn?: ILuaFunction, sf?: LuaStackFrame) {
if (fn) { if (fn && sf) {
this.arrayPart = await asyncQuickSort(this.arrayPart, async (a, b) => { 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 { } else {
this.arrayPart.sort(); this.arrayPart.sort();
@ -311,7 +341,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
if (this.metatable?.has("__tostring")) { if (this.metatable?.has("__tostring")) {
const metaValue = this.metatable.get("__tostring"); const metaValue = this.metatable.get("__tostring");
if (metaValue.call) { if (metaValue.call) {
return metaValue.call(this); return metaValue.call(LuaStackFrame.lostFrame, this);
} else { } else {
throw new Error("Meta table __tostring must be a function"); 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 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) { if (!obj) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Not a settable object: nil`, `Not a settable object: nil`,
ctx, sf,
); );
} }
if (obj instanceof LuaTable || obj instanceof LuaEnv) { if (obj instanceof LuaTable || obj instanceof LuaEnv) {
obj.set(key, value); obj.set(key, value, sf);
} else { } else {
obj[key] = value; 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) { if (!obj) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to index a nil value`, `Attempting to index a nil value`,
ctx, sf,
); );
} }
if (key === null || key === undefined) { if (key === null || key === undefined) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to index with a nil key`, `Attempting to index with a nil key`,
ctx, sf,
); );
} }
if (obj instanceof LuaTable || obj instanceof LuaEnv) { if (obj instanceof LuaTable || obj instanceof LuaEnv) {
return obj.get(key); return obj.get(key, sf);
} else if (typeof key === "number") { } else if (typeof key === "number") {
return obj[key - 1]; return obj[key - 1];
} else { } 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) { if (!fn) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to call a nil value`, `Attempting to call a nil value`,
ctx, (sf || LuaStackFrame.lostFrame).withCtx(ctx),
); );
} }
if (typeof fn === "function") { if (typeof fn === "function") {
@ -409,7 +444,13 @@ export function luaCall(fn: any, args: any[], ctx: ASTCtx): any {
// Native JS function // Native JS function
return fn(...jsArgs); 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 { export function luaTypeOf(val: any): LuaType {
@ -445,14 +486,14 @@ export class LuaReturn extends Error {
export class LuaRuntimeError extends Error { export class LuaRuntimeError extends Error {
constructor( constructor(
override readonly message: string, override readonly message: string,
public context: ASTCtx, public sf: LuaStackFrame,
cause?: Error, cause?: Error,
) { ) {
super(message, cause); super(message, cause);
} }
override toString() { 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 { import {
type ILuaFunction, type ILuaFunction,
LuaBuiltinFunction, LuaBuiltinFunction,
luaCall,
LuaEnv, LuaEnv,
LuaMultiRes, LuaMultiRes,
LuaRuntimeError,
type LuaTable, type LuaTable,
luaToString, luaToString,
luaTypeOf, luaTypeOf,
@ -13,19 +15,19 @@ import { tableApi } from "$common/space_lua/stdlib/table.ts";
import { osApi } from "$common/space_lua/stdlib/os.ts"; import { osApi } from "$common/space_lua/stdlib/os.ts";
import { jsApi } from "$common/space_lua/stdlib/js.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)); console.log("[Lua]", ...args.map(luaToString));
}); });
const assertFunction = new LuaBuiltinFunction( const assertFunction = new LuaBuiltinFunction(
async (value: any, message?: string) => { async (sf, value: any, message?: string) => {
if (!await value) { 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; let i = 1;
return () => { return () => {
if (i > ar.length) { 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(); const keys = t.keys();
let i = 0; let i = 0;
return () => { 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[] = []; const values: LuaValue[] = [];
for (let i = 1; i <= t.length; i++) { for (let i = 1; i <= t.length; i++) {
values.push(t.get(i)); values.push(t.get(i));
@ -58,26 +60,26 @@ const unpackFunction = new LuaBuiltinFunction((t: LuaTable) => {
return new LuaMultiRes(values); return new LuaMultiRes(values);
}); });
const typeFunction = new LuaBuiltinFunction((value: LuaValue): string => { const typeFunction = new LuaBuiltinFunction((_sf, value: LuaValue): string => {
return luaTypeOf(value); return luaTypeOf(value);
}); });
const tostringFunction = new LuaBuiltinFunction((value: any) => { const tostringFunction = new LuaBuiltinFunction((_sf, value: any) => {
return luaToString(value); return luaToString(value);
}); });
const tonumberFunction = new LuaBuiltinFunction((value: LuaValue) => { const tonumberFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
return Number(value); return Number(value);
}); });
const errorFunction = new LuaBuiltinFunction((message: string) => { const errorFunction = new LuaBuiltinFunction((_sf, message: string) => {
throw new Error(message); throw new Error(message);
}); });
const pcallFunction = new LuaBuiltinFunction( const pcallFunction = new LuaBuiltinFunction(
async (fn: ILuaFunction, ...args) => { async (sf, fn: ILuaFunction, ...args) => {
try { try {
return new LuaMultiRes([true, await fn.call(...args)]); return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]);
} catch (e: any) { } catch (e: any) {
return new LuaMultiRes([false, e.message]); return new LuaMultiRes([false, e.message]);
} }
@ -85,30 +87,33 @@ const pcallFunction = new LuaBuiltinFunction(
); );
const xpcallFunction = new LuaBuiltinFunction( const xpcallFunction = new LuaBuiltinFunction(
async (fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => { async (sf, fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => {
try { try {
return new LuaMultiRes([true, await fn.call(...args)]); return new LuaMultiRes([true, await fn.call(sf, ...args)]);
} catch (e: any) { } 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( const setmetatableFunction = new LuaBuiltinFunction(
(table: LuaTable, metatable: LuaTable) => { (_sf, table: LuaTable, metatable: LuaTable) => {
table.metatable = metatable; table.metatable = metatable;
return table; return table;
}, },
); );
const rawsetFunction = new LuaBuiltinFunction( const rawsetFunction = new LuaBuiltinFunction(
(table: LuaTable, key: LuaValue, value: LuaValue) => { (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
table.rawSet(key, value); table.rawSet(key, value);
return table; return table;
}, },
); );
const getmetatableFunction = new LuaBuiltinFunction((table: LuaTable) => { const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
return table.metatable; 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); return import(url);
}), }),
tolua: new LuaBuiltinFunction(jsToLuaValue), tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val)),
tojs: new LuaBuiltinFunction(luaValueToJS), tojs: new LuaBuiltinFunction((_sf, val) => luaValueToJS(val)),
log: new LuaBuiltinFunction((...args) => { log: new LuaBuiltinFunction((_sf, ...args) => {
console.log(...args); console.log(...args);
}), }),
// assignGlobal: new LuaBuiltinFunction((name: string, value: any) => { // assignGlobal: new LuaBuiltinFunction((name: string, value: any) => {

View File

@ -1,7 +1,7 @@
import { LuaBuiltinFunction, LuaTable } from "$common/space_lua/runtime.ts"; import { LuaBuiltinFunction, LuaTable } from "$common/space_lua/runtime.ts";
export const osApi = new LuaTable({ export const osApi = new LuaTable({
time: new LuaBuiltinFunction((tbl?: LuaTable) => { time: new LuaBuiltinFunction((_sf, tbl?: LuaTable) => {
if (tbl) { if (tbl) {
// Build a date object from the table // Build a date object from the table
const date = new Date(); 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 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. * 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(); const date = timestamp ? new Date(timestamp * 1000) : new Date();
// Default Lua-like format when no format string is provided // Default Lua-like format when no format string is provided

View File

@ -6,7 +6,7 @@ import {
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
export const stringApi = new LuaTable({ 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; i = i ?? 1;
j = j ?? i; j = j ?? i;
const result = []; const result = [];
@ -15,11 +15,11 @@ export const stringApi = new LuaTable({
} }
return new LuaMultiRes(result); return new LuaMultiRes(result);
}), }),
char: new LuaBuiltinFunction((...args: number[]) => { char: new LuaBuiltinFunction((_sf, ...args: number[]) => {
return String.fromCharCode(...args); return String.fromCharCode(...args);
}), }),
find: new LuaBuiltinFunction( find: new LuaBuiltinFunction(
(s: string, pattern: string, init?: number, plain?: boolean) => { (_sf, s: string, pattern: string, init?: number, plain?: boolean) => {
init = init ?? 1; init = init ?? 1;
plain = plain ?? false; plain = plain ?? false;
const result = s.slice(init - 1).match(pattern); 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) => { return format.replace(/%./g, (match) => {
switch (match) { switch (match) {
case "%s": 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"); const regex = new RegExp(pattern, "g");
return () => { return () => {
const result = regex.exec(s); const result = regex.exec(s);
@ -55,7 +55,7 @@ export const stringApi = new LuaTable({
}; };
}), }),
gsub: new LuaBuiltinFunction( gsub: new LuaBuiltinFunction(
(s: string, pattern: string, repl: string, n?: number) => { (_sf, s: string, pattern: string, repl: string, n?: number) => {
n = n ?? Infinity; n = n ?? Infinity;
const regex = new RegExp(pattern, "g"); const regex = new RegExp(pattern, "g");
let result = s; let result = s;
@ -70,17 +70,17 @@ export const stringApi = new LuaTable({
return result; return result;
}, },
), ),
len: new LuaBuiltinFunction((s: string) => { len: new LuaBuiltinFunction((_sf, s: string) => {
return s.length; return s.length;
}), }),
lower: new LuaBuiltinFunction((s: string) => { lower: new LuaBuiltinFunction((_sf, s: string) => {
return luaToString(s.toLowerCase()); return luaToString(s.toLowerCase());
}), }),
upper: new LuaBuiltinFunction((s: string) => { upper: new LuaBuiltinFunction((_sf, s: string) => {
return luaToString(s.toUpperCase()); return luaToString(s.toUpperCase());
}), }),
match: new LuaBuiltinFunction( match: new LuaBuiltinFunction(
(s: string, pattern: string, init?: number) => { (_sf, s: string, pattern: string, init?: number) => {
init = init ?? 1; init = init ?? 1;
const result = s.slice(init - 1).match(pattern); const result = s.slice(init - 1).match(pattern);
if (!result) { if (!result) {
@ -89,18 +89,18 @@ export const stringApi = new LuaTable({
return new LuaMultiRes(result.slice(1)); 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 ?? ""; sep = sep ?? "";
return s.repeat(n) + sep; return s.repeat(n) + sep;
}), }),
reverse: new LuaBuiltinFunction((s: string) => { reverse: new LuaBuiltinFunction((_sf, s: string) => {
return s.split("").reverse().join(""); 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; j = j ?? s.length;
return s.slice(i - 1, j); 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); return s.split(sep);
}), }),
}); });

View File

@ -6,7 +6,7 @@ import {
export const tableApi = new LuaTable({ export const tableApi = new LuaTable({
concat: new LuaBuiltinFunction( concat: new LuaBuiltinFunction(
(tbl: LuaTable, sep?: string, i?: number, j?: number) => { (_sf, tbl: LuaTable, sep?: string, i?: number, j?: number) => {
sep = sep ?? ""; sep = sep ?? "";
i = i ?? 1; i = i ?? 1;
j = j ?? tbl.length; j = j ?? tbl.length;
@ -18,7 +18,7 @@ export const tableApi = new LuaTable({
}, },
), ),
insert: new LuaBuiltinFunction( insert: new LuaBuiltinFunction(
(tbl: LuaTable, posOrValue: number | any, value?: any) => { (_sf, tbl: LuaTable, posOrValue: number | any, value?: any) => {
if (value === undefined) { if (value === undefined) {
value = posOrValue; value = posOrValue;
posOrValue = tbl.length + 1; posOrValue = tbl.length + 1;
@ -26,11 +26,11 @@ export const tableApi = new LuaTable({
tbl.insert(posOrValue, value); tbl.insert(posOrValue, value);
}, },
), ),
remove: new LuaBuiltinFunction((tbl: LuaTable, pos?: number) => { remove: new LuaBuiltinFunction((_sf, tbl: LuaTable, pos?: number) => {
pos = pos ?? tbl.length; pos = pos ?? tbl.length;
tbl.remove(pos); tbl.remove(pos);
}), }),
sort: new LuaBuiltinFunction((tbl: LuaTable, comp?: ILuaFunction) => { sort: new LuaBuiltinFunction((sf, tbl: LuaTable, comp?: ILuaFunction) => {
return tbl.sort(comp); return tbl.sort(comp, sf);
}), }),
}); });

View File

@ -5,6 +5,7 @@ import {
LuaBuiltinFunction, LuaBuiltinFunction,
LuaEnv, LuaEnv,
LuaNativeJSFunction, LuaNativeJSFunction,
LuaStackFrame,
LuaTable, LuaTable,
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
import type { System } from "$lib/plugos/system.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>) { function exposeSyscalls(env: LuaEnv, system: System<any>) {
// Expose all syscalls to Lua // Expose all syscalls to Lua
const nativeFs = new LuaStackFrame(env, null);
for (const syscallName of system.registeredSyscalls.keys()) { for (const syscallName of system.registeredSyscalls.keys()) {
const [ns, fn] = syscallName.split("."); const [ns, fn] = syscallName.split(".");
if (!env.get(ns)) { if (!env.has(ns)) {
env.set(ns, new LuaTable()); env.set(ns, new LuaTable(), nativeFs);
} }
const luaFn = new LuaNativeJSFunction((...args) => { const luaFn = new LuaNativeJSFunction((...args) => {
return system.localSyscall(syscallName, args); return system.localSyscall(syscallName, args);
}); });
// Register the function with the same name as the syscall both in regular and snake_case // 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, nativeFs).set(fn, luaFn, nativeFs);
env.get(ns).set(snakeCase(fn), luaFn); env.get(ns, nativeFs).set(snakeCase(fn), luaFn, nativeFs);
} }
} }
@ -47,7 +49,7 @@ function exposeDefinitions(
env.set( env.set(
"define_command", "define_command",
new LuaBuiltinFunction( new LuaBuiltinFunction(
(def: LuaTable) => { (_sf, def: LuaTable) => {
if (def.get(1) === undefined) { if (def.get(1) === undefined) {
throw new Error("Callback is required"); throw new Error("Callback is required");
} }
@ -65,10 +67,9 @@ function exposeDefinitions(
hide: def.get("hide"), hide: def.get("hide"),
} as CommandDef, } as CommandDef,
async (...args: any[]) => { async (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), null);
try { try {
return await def.get(1).call( return await def.get(1).call(sf, ...args.map(jsToLuaValue));
...args.map(jsToLuaValue),
);
} catch (e: any) { } catch (e: any) {
await handleLuaError(e, system); await handleLuaError(e, system);
} }
@ -79,7 +80,7 @@ function exposeDefinitions(
); );
env.set( env.set(
"define_event_listener", "define_event_listener",
new LuaBuiltinFunction((def: LuaTable) => { new LuaBuiltinFunction((_sf, def: LuaTable) => {
if (def.get(1) === undefined) { if (def.get(1) === undefined) {
throw new Error("Callback is required"); throw new Error("Callback is required");
} }
@ -90,10 +91,9 @@ function exposeDefinitions(
scriptEnv.registerEventListener( scriptEnv.registerEventListener(
{ name: def.get("event") }, { name: def.get("event") },
async (...args: any[]) => { async (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), null);
try { try {
return await def.get(1).call( return await def.get(1).call(sf, ...args.map(jsToLuaValue));
...args.map(jsToLuaValue),
);
} catch (e: any) { } catch (e: any) {
await handleLuaError(e, system); await handleLuaError(e, system);
} }

View File

@ -14,7 +14,11 @@ import type {
LuaFunctionCallStatement, LuaFunctionCallStatement,
} from "$common/space_lua/ast.ts"; } from "$common/space_lua/ast.ts";
import { evalExpression } from "$common/space_lua/eval.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 { LuaRuntimeError } from "$common/space_lua/runtime.ts";
import { encodePageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; import { encodePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
import { resolveASTReference } from "$common/space_lua.ts"; import { resolveASTReference } from "$common/space_lua.ts";
@ -54,16 +58,18 @@ export function luaDirectivePlugin(client: Client) {
(parsedLua.statements[0] as LuaFunctionCallStatement).call (parsedLua.statements[0] as LuaFunctionCallStatement).call
.args[0]; .args[0];
const sf = new LuaStackFrame(new LuaEnv(), expr.ctx);
return luaValueToJS( return luaValueToJS(
await evalExpression( await evalExpression(
expr, expr,
client.clientSystem.spaceLuaEnv.env, client.clientSystem.spaceLuaEnv.env,
sf,
), ),
); );
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaRuntimeError) { if (e instanceof LuaRuntimeError) {
if (e.context.ref) { if (e.sf.astCtx) {
const source = resolveASTReference(e.context); const source = resolveASTReference(e.sf.astCtx);
if (source) { if (source) {
// We know the origin node of the error, let's reference it // We know the origin node of the error, let's reference it
return { return {