Lua: experimental string interpolation

main
Zef Hemel 2025-01-09 11:45:15 +01:00
parent b6f1977cec
commit 86f31e3a00
5 changed files with 124 additions and 5 deletions

View File

@ -304,6 +304,8 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
const env = new LuaEnv(luaBuildStandardEnv());
const threadLocal = new LuaEnv();
env.setLocal("globalEnv", "GLOBAL");
// Set up some thread local values
threadLocal.setLocal("user", "alice");
threadLocal.setLocal("permissions", new LuaTable());
@ -423,4 +425,25 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
assertEquals(await evalExpr("errorTest()", env, sf), "caught");
assertEquals(threadLocal.get("error"), "caught");
// Test string interpolation
sf.threadLocal.setLocal("_GLOBAL", env);
assertEquals(
await evalExpr(
"interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
env,
sf,
),
"Hello, GLOBAL and local!",
);
// Some more complex string interpolation with more complex lua expressions, with nested {}
assertEquals(
await evalExpr(
`interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
env,
sf,
),
`Some JSON {"name":"Pete"}!`,
);
});

View File

@ -75,6 +75,17 @@ do
end
assert(a == 1)
-- Async function calling
function multiplier(a)
-- Anything will be async in practice
return function(b)
return a * b
end
end
local multiplier = multiplier(2)
assert(multiplier(3) == 6)
-- Function definitions in tables
ns = { name = "Pete" }
function ns.returnOne()
@ -308,12 +319,10 @@ assert(result == "Hi world", "Function replacement with single capture failed")
-- Function replacement with multiple captures
result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2)
print("Captures:", h, e, l1, l2) -- Debug what captures we're getting
assert(h == "h" and e == "e" and l1 == "l" and l2 == "l",
"Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2)
return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o"
end)
print("Result:", result) -- Debug the actual result
assert(result == "HEllo world", "Function replacement with multiple captures failed")
-- Function returning nil (should keep original match)

View File

@ -10,11 +10,18 @@ import {
luaToString,
luaTypeOf,
type LuaValue,
luaValueToJS,
} from "$common/space_lua/runtime.ts";
import { stringApi } from "$common/space_lua/stdlib/string.ts";
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";
import { parse } from "$common/space_lua/parse.ts";
import type {
LuaBlock,
LuaFunctionCallStatement,
} from "$common/space_lua/ast.ts";
import { evalExpression } from "$common/space_lua/eval.ts";
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
@ -122,6 +129,82 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
return table.metatable;
});
/**
* This is not standard Lua, but it's a useful feature for us
*/
const interpolateFunction = new LuaBuiltinFunction(
async (sf, template: string, expandedEnv?: LuaTable) => {
let result = "";
let currentIndex = 0;
while (true) {
const startIndex = template.indexOf("${", currentIndex);
if (startIndex === -1) {
result += template.slice(currentIndex);
break;
}
result += template.slice(currentIndex, startIndex);
// Find matching closing brace by counting nesting
let nestLevel = 1;
let endIndex = startIndex + 2;
while (nestLevel > 0 && endIndex < template.length) {
if (template[endIndex] === "{") {
nestLevel++;
} else if (template[endIndex] === "}") {
nestLevel--;
}
if (nestLevel > 0) {
endIndex++;
}
}
if (nestLevel > 0) {
throw new LuaRuntimeError("Unclosed interpolation expression", sf);
}
const expr = template.slice(startIndex + 2, endIndex);
try {
const parsedLua = parse(`_(${expr})`) as LuaBlock;
const parsedExpr =
(parsedLua.statements[0] as LuaFunctionCallStatement).call
.args[0];
const globalEnv = sf.threadLocal.get("_GLOBAL");
if (!globalEnv) {
throw new Error("_GLOBAL not defined");
}
// Create a new env with the global env as the parent, augmented with the expandedEnv
const env = new LuaEnv(globalEnv);
if (expandedEnv) {
// Iterate over the keys in the expandedEnv and set them in the new env
for (const key of expandedEnv.keys()) {
env.setLocal(key, expandedEnv.rawGet(key));
}
}
const luaResult = luaValueToJS(
await evalExpression(
parsedExpr,
env,
sf,
),
);
result += luaToString(luaResult);
} catch (e: any) {
throw new LuaRuntimeError(
`Error evaluating "${expr}": ${e.message}`,
sf,
);
}
currentIndex = endIndex + 1;
}
return result;
},
);
export function luaBuildStandardEnv() {
const env = new LuaEnv();
// Top-level builtins
@ -142,6 +225,8 @@ export function luaBuildStandardEnv() {
env.set("error", errorFunction);
env.set("pcall", pcallFunction);
env.set("xpcall", xpcallFunction);
// String interpolation
env.set("interpolate", interpolateFunction);
// APIs
env.set("string", stringApi);

View File

@ -72,7 +72,7 @@ function exposeDefinitions(
hide: def.get("hide"),
} as CommandDef,
async (...args: any[]) => {
const tl = await buildThreadLocalEnv(system);
const tl = await buildThreadLocalEnv(system, env);
const sf = new LuaStackFrame(tl, null);
try {
return await fn.call(sf, ...args.map(jsToLuaValue));
@ -102,7 +102,7 @@ function exposeDefinitions(
scriptEnv.registerEventListener(
{ name: def.get("event") },
async (...args: any[]) => {
const tl = await buildThreadLocalEnv(system);
const tl = await buildThreadLocalEnv(system, env);
const sf = new LuaStackFrame(tl, null);
try {
return await fn.call(sf, ...args.map(jsToLuaValue));
@ -115,13 +115,14 @@ function exposeDefinitions(
);
}
async function buildThreadLocalEnv(system: System<any>) {
async function buildThreadLocalEnv(system: System<any>, globalEnv: LuaEnv) {
const tl = new LuaEnv();
const currentPageMeta = await system.localSyscall(
"editor.getCurrentPageMeta",
[],
);
tl.setLocal("pageMeta", currentPageMeta);
tl.setLocal("_GLOBAL", globalEnv);
return tl;
}

View File

@ -60,6 +60,7 @@ export function luaDirectivePlugin(client: Client) {
const tl = new LuaEnv();
tl.setLocal("pageMeta", currentPageMeta);
tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env);
const sf = new LuaStackFrame(tl, expr.ctx);
const threadLocalizedEnv = new LuaEnv(
client.clientSystem.spaceLuaEnv.env,