Skip to content

Commit

Permalink
Lua work
Browse files Browse the repository at this point in the history
  • Loading branch information
zefhemel committed Jan 8, 2025
1 parent 29e127c commit c2fea2d
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 32 deletions.
6 changes: 4 additions & 2 deletions common/space_lua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ export class SpaceLuaEnvironment {
for (const globalName of this.env.keys()) {
const value = this.env.get(globalName);
if (value instanceof LuaFunction) {
console.log("Now registering Lua function", globalName);
console.log(
`[Lua] Registering global function '${globalName}' (source: ${value.body.ctx.ref})`,
);
scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), value.body.ctx);
return luaValueToJS(value.call(sf, ...args.map(jsToLuaValue)));
});
}
}
console.log("Loaded", allScripts.length, "Lua scripts");
console.log("[Lua] Loaded", allScripts.length, "scripts");
}
}

Expand Down
151 changes: 149 additions & 2 deletions common/space_lua/eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
LuaEnv,
LuaNativeJSFunction,
LuaStackFrame,
LuaTable,
luaValueToJS,
singleResult,
} from "./runtime.ts";
Expand All @@ -11,9 +12,9 @@ import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } from "./eval.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";

function evalExpr(s: string, e = new LuaEnv()): any {
function evalExpr(s: string, e = new LuaEnv(), sf?: LuaStackFrame): any {
const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement;
const sf = new LuaStackFrame(e, node.ctx);
sf = sf || new LuaStackFrame(e, node.ctx);
return evalExpression(
node.call.args[0],
e,
Expand Down Expand Up @@ -277,3 +278,149 @@ Deno.test("Statement evaluation", async () => {
luaBuildStandardEnv(),
);
});

Deno.test("Thread local _CTX", async () => {
const env = new LuaEnv();
const threadLocal = new LuaEnv();
threadLocal.setLocal("threadValue", "test123");

const sf = new LuaStackFrame(threadLocal, null);

await evalBlock(
`
function test()
return _CTX.threadValue
end
`,
env,
);

const result = await evalExpr("test()", env, sf);
assertEquals(singleResult(result), "test123");
});

Deno.test("Thread local _CTX - advanced cases", async () => {
// Create environment with standard library
const env = new LuaEnv(luaBuildStandardEnv());
const threadLocal = new LuaEnv();

// Set up some thread local values
threadLocal.setLocal("user", "alice");
threadLocal.setLocal("permissions", new LuaTable());
threadLocal.get("permissions").set("admin", true);
threadLocal.setLocal("data", {
id: 123,
settings: { theme: "dark" },
});

const sf = new LuaStackFrame(threadLocal, null);

// Test 1: Nested function access
await evalBlock(
`
function outer()
local function inner()
return _CTX.user
end
return inner()
end
`,
env,
);
assertEquals(await evalExpr("outer()", env, sf), "alice");

// Test 2: Table access and modification
await evalBlock(
`
function checkAdmin()
return _CTX.permissions.admin
end
function revokeAdmin()
_CTX.permissions.admin = false
return _CTX.permissions.admin
end
`,
env,
);
assertEquals(await evalExpr("checkAdmin()", env, sf), true);
assertEquals(await evalExpr("revokeAdmin()", env, sf), false);
assertEquals(threadLocal.get("permissions").get("admin"), false);

// Test 3: Complex data structures
await evalBlock(
`
function getNestedData()
return _CTX.data.settings.theme
end
function updateTheme(newTheme)
_CTX.data.settings.theme = newTheme
return _CTX.data.settings.theme
end
`,
env,
);
assertEquals(await evalExpr("getNestedData()", env, sf), "dark");
assertEquals(await evalExpr("updateTheme('light')", env, sf), "light");

// Test 4: Multiple thread locals
const threadLocal2 = new LuaEnv();
threadLocal2.setLocal("user", "bob");
const sf2 = new LuaStackFrame(threadLocal2, null);

await evalBlock(
`
function getUser()
return _CTX.user
end
`,
env,
);

// Same function, different thread contexts
assertEquals(await evalExpr("getUser()", env, sf), "alice");
assertEquals(await evalExpr("getUser()", env, sf2), "bob");

// Test 5: Async operations with _CTX
env.set(
"asyncOperation",
new LuaNativeJSFunction(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
return "done";
}),
);

await evalBlock(
`
function asyncTest()
_CTX.status = "starting"
local result = asyncOperation()
_CTX.status = "completed"
return _CTX.status
end
`,
env,
);

assertEquals(await evalExpr("asyncTest()", env, sf), "completed");
assertEquals(threadLocal.get("status"), "completed");

// Test 6: Error handling with _CTX
await evalBlock(
`
function errorTest()
_CTX.error = nil
local status, err = pcall(function()
error("test error")
end)
_CTX.error = "caught"
return _CTX.error
end
`,
env,
);

assertEquals(await evalExpr("errorTest()", env, sf), "caught");
assertEquals(threadLocal.get("error"), "caught");
});
6 changes: 3 additions & 3 deletions common/space_lua/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ function evalPrefixExpression(
if (prefixValue instanceof Promise) {
return prefixValue.then(async (resolvedPrefix) => {
const args = await resolveVarargs();
return luaCall(resolvedPrefix, args, e.ctx, sf);
return luaCall(resolvedPrefix, args, e.ctx, sf.withCtx(e.ctx));
});
} else {
return resolveVarargs().then((args) =>
luaCall(prefixValue, args, e.ctx, sf)
luaCall(prefixValue, args, e.ctx, sf.withCtx(e.ctx))
);
}
}
Expand Down Expand Up @@ -409,7 +409,7 @@ const operatorsMetaMethods: Record<string, {
"/": { metaMethod: "__div", nativeImplementation: (a, b) => a / b },
"//": {
metaMethod: "__idiv",
nativeImplementation: (a, b, ctx, sf) => Math.floor(a / b),
nativeImplementation: (a, b) => Math.floor(a / b),
},
"%": { metaMethod: "__mod", nativeImplementation: (a, b) => a % b },
"^": { metaMethod: "__pow", nativeImplementation: (a, b) => a ** b },
Expand Down
24 changes: 20 additions & 4 deletions common/space_lua/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export class LuaFunction implements ILuaFunction {
if (!sf) {
console.trace(sf);
}
// Set _CTX to the thread local environment from the stack frame
env.setLocal("_CTX", sf.threadLocal);

// Assign the passed arguments to the parameters
Expand Down Expand Up @@ -271,6 +272,7 @@ export class LuaBuiltinFunction implements ILuaFunction {
}

call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
// _CTX is already available via the stack frame
return this.fn(sf, ...args);
}

Expand Down Expand Up @@ -608,21 +610,35 @@ export class LuaRuntimeError extends Error {
// Find the line and column
let line = 1;
let column = 0;
let lastNewline = -1;
for (let i = 0; i < ctx.from; i++) {
if (code[i] === "\n") {
line++;
lastNewline = i;
column = 0;
} else {
column++;
}
}
traceStr += `* ${
ctx.ref || "(unknown source)"
} @ ${line}:${column}:\n ${code.substring(ctx.from, ctx.to)}\n`;

// Get the full line of code for context
const lineStart = lastNewline + 1;
const lineEnd = code.indexOf("\n", ctx.from);
const codeLine = code.substring(
lineStart,
lineEnd === -1 ? undefined : lineEnd,
);

// Add position indicator
const pointer = " ".repeat(column) + "^";

traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
` ${codeLine}\n` +
` ${pointer}\n`;
current = current.parent;
}

return `LuaRuntimeError: ${this.message} ${traceStr}`;
return `LuaRuntimeError: ${this.message}\nStack trace:\n${traceStr}`;
}

override toString() {
Expand Down
10 changes: 7 additions & 3 deletions common/space_lua/stdlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,18 @@ const tonumberFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
return Number(value);
});

const errorFunction = new LuaBuiltinFunction((_sf, message: string) => {
throw new Error(message);
const errorFunction = new LuaBuiltinFunction((sf, message: string) => {
throw new LuaRuntimeError(message, sf);
});

const pcallFunction = new LuaBuiltinFunction(
async (sf, fn: ILuaFunction, ...args) => {
try {
return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]);
} catch (e: any) {
if (e instanceof LuaRuntimeError) {
return new LuaMultiRes([false, e.message]);
}
return new LuaMultiRes([false, e.message]);
}
},
Expand All @@ -91,9 +94,10 @@ const xpcallFunction = new LuaBuiltinFunction(
try {
return new LuaMultiRes([true, await fn.call(sf, ...args)]);
} catch (e: any) {
const errorMsg = e instanceof LuaRuntimeError ? e.message : e.message;
return new LuaMultiRes([
false,
await luaCall(errorHandler, [e.message], sf.astCtx!, sf),
await luaCall(errorHandler, [errorMsg], sf.astCtx!, sf),
]);
}
},
Expand Down
1 change: 1 addition & 0 deletions common/space_lua/stdlib/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const jsApi = new LuaTable({
log: new LuaBuiltinFunction((_sf, ...args) => {
console.log(...args);
}),
stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)),
// assignGlobal: new LuaBuiltinFunction((name: string, value: any) => {
// (globalThis as any)[name] = value;
// }),
Expand Down
33 changes: 27 additions & 6 deletions common/space_lua_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ function exposeDefinitions(
if (!def.get("name")) {
throw new Error("Name is required");
}
console.log("Registering Lua command", def.get("name"));
const fn = def.get(1);
console.log(
`[Lua] Registering command '${
def.get("name")
}' (source: ${fn.body.ctx.ref})`,
);
scriptEnv.registerCommand(
{
name: def.get("name"),
Expand All @@ -67,10 +72,10 @@ function exposeDefinitions(
hide: def.get("hide"),
} as CommandDef,
async (...args: any[]) => {
const tl = new LuaEnv();
const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null);
try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue));
return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) {
await handleLuaError(e, system);
}
Expand All @@ -88,13 +93,19 @@ function exposeDefinitions(
if (!def.get("event")) {
throw new Error("Event is required");
}
console.log("Subscribing to Lua event", def.get("event"));
const fn = def.get(1);
console.log(
`[Lua] Subscribing to event '${
def.get("event")
}' (source: ${fn.body.ctx.ref})`,
);
scriptEnv.registerEventListener(
{ name: def.get("event") },
async (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), null);
const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null);
try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue));
return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) {
await handleLuaError(e, system);
}
Expand All @@ -104,6 +115,16 @@ function exposeDefinitions(
);
}

async function buildThreadLocalEnv(system: System<any>) {
const tl = new LuaEnv();
const currentPageMeta = await system.localSyscall(
"editor.getCurrentPageMeta",
[],
);
tl.setLocal("pageMeta", currentPageMeta);
return tl;
}

async function handleLuaError(e: any, system: System<any>) {
console.error(
"Lua eval exception",
Expand Down
3 changes: 3 additions & 0 deletions common/template/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ async function renderExpressionDirective(
variables,
functionMap,
);
return renderExpressionResult(result);
}

export function renderExpressionResult(result: any): string {
if (
Array.isArray(result) && result.length > 0 && typeof result[0] === "object"
) {
Expand Down
Loading

0 comments on commit c2fea2d

Please sign in to comment.