Lua: experimental string interpolation
parent
b6f1977cec
commit
86f31e3a00
|
@ -304,6 +304,8 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
|
||||||
const env = new LuaEnv(luaBuildStandardEnv());
|
const env = new LuaEnv(luaBuildStandardEnv());
|
||||||
const threadLocal = new LuaEnv();
|
const threadLocal = new LuaEnv();
|
||||||
|
|
||||||
|
env.setLocal("globalEnv", "GLOBAL");
|
||||||
|
|
||||||
// Set up some thread local values
|
// Set up some thread local values
|
||||||
threadLocal.setLocal("user", "alice");
|
threadLocal.setLocal("user", "alice");
|
||||||
threadLocal.setLocal("permissions", new LuaTable());
|
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(await evalExpr("errorTest()", env, sf), "caught");
|
||||||
assertEquals(threadLocal.get("error"), "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"}!`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -75,6 +75,17 @@ do
|
||||||
end
|
end
|
||||||
assert(a == 1)
|
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
|
-- Function definitions in tables
|
||||||
ns = { name = "Pete" }
|
ns = { name = "Pete" }
|
||||||
function ns.returnOne()
|
function ns.returnOne()
|
||||||
|
@ -308,12 +319,10 @@ assert(result == "Hi world", "Function replacement with single capture failed")
|
||||||
|
|
||||||
-- Function replacement with multiple captures
|
-- Function replacement with multiple captures
|
||||||
result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2)
|
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",
|
assert(h == "h" and e == "e" and l1 == "l" and l2 == "l",
|
||||||
"Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2)
|
"Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2)
|
||||||
return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o"
|
return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o"
|
||||||
end)
|
end)
|
||||||
print("Result:", result) -- Debug the actual result
|
|
||||||
assert(result == "HEllo world", "Function replacement with multiple captures failed")
|
assert(result == "HEllo world", "Function replacement with multiple captures failed")
|
||||||
|
|
||||||
-- Function returning nil (should keep original match)
|
-- Function returning nil (should keep original match)
|
||||||
|
|
|
@ -10,11 +10,18 @@ import {
|
||||||
luaToString,
|
luaToString,
|
||||||
luaTypeOf,
|
luaTypeOf,
|
||||||
type LuaValue,
|
type LuaValue,
|
||||||
|
luaValueToJS,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
import { stringApi } from "$common/space_lua/stdlib/string.ts";
|
import { stringApi } from "$common/space_lua/stdlib/string.ts";
|
||||||
import { tableApi } from "$common/space_lua/stdlib/table.ts";
|
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";
|
||||||
|
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) => {
|
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
|
||||||
console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
|
console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
|
||||||
|
@ -122,6 +129,82 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
|
||||||
return table.metatable;
|
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() {
|
export function luaBuildStandardEnv() {
|
||||||
const env = new LuaEnv();
|
const env = new LuaEnv();
|
||||||
// Top-level builtins
|
// Top-level builtins
|
||||||
|
@ -142,6 +225,8 @@ export function luaBuildStandardEnv() {
|
||||||
env.set("error", errorFunction);
|
env.set("error", errorFunction);
|
||||||
env.set("pcall", pcallFunction);
|
env.set("pcall", pcallFunction);
|
||||||
env.set("xpcall", xpcallFunction);
|
env.set("xpcall", xpcallFunction);
|
||||||
|
// String interpolation
|
||||||
|
env.set("interpolate", interpolateFunction);
|
||||||
|
|
||||||
// APIs
|
// APIs
|
||||||
env.set("string", stringApi);
|
env.set("string", stringApi);
|
||||||
|
|
|
@ -72,7 +72,7 @@ function exposeDefinitions(
|
||||||
hide: def.get("hide"),
|
hide: def.get("hide"),
|
||||||
} as CommandDef,
|
} as CommandDef,
|
||||||
async (...args: any[]) => {
|
async (...args: any[]) => {
|
||||||
const tl = await buildThreadLocalEnv(system);
|
const tl = await buildThreadLocalEnv(system, env);
|
||||||
const sf = new LuaStackFrame(tl, null);
|
const sf = new LuaStackFrame(tl, null);
|
||||||
try {
|
try {
|
||||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
return await fn.call(sf, ...args.map(jsToLuaValue));
|
||||||
|
@ -102,7 +102,7 @@ function exposeDefinitions(
|
||||||
scriptEnv.registerEventListener(
|
scriptEnv.registerEventListener(
|
||||||
{ name: def.get("event") },
|
{ name: def.get("event") },
|
||||||
async (...args: any[]) => {
|
async (...args: any[]) => {
|
||||||
const tl = await buildThreadLocalEnv(system);
|
const tl = await buildThreadLocalEnv(system, env);
|
||||||
const sf = new LuaStackFrame(tl, null);
|
const sf = new LuaStackFrame(tl, null);
|
||||||
try {
|
try {
|
||||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
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 tl = new LuaEnv();
|
||||||
const currentPageMeta = await system.localSyscall(
|
const currentPageMeta = await system.localSyscall(
|
||||||
"editor.getCurrentPageMeta",
|
"editor.getCurrentPageMeta",
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
tl.setLocal("pageMeta", currentPageMeta);
|
tl.setLocal("pageMeta", currentPageMeta);
|
||||||
|
tl.setLocal("_GLOBAL", globalEnv);
|
||||||
return tl;
|
return tl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ export function luaDirectivePlugin(client: Client) {
|
||||||
|
|
||||||
const tl = new LuaEnv();
|
const tl = new LuaEnv();
|
||||||
tl.setLocal("pageMeta", currentPageMeta);
|
tl.setLocal("pageMeta", currentPageMeta);
|
||||||
|
tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env);
|
||||||
const sf = new LuaStackFrame(tl, expr.ctx);
|
const sf = new LuaStackFrame(tl, expr.ctx);
|
||||||
const threadLocalizedEnv = new LuaEnv(
|
const threadLocalizedEnv = new LuaEnv(
|
||||||
client.clientSystem.spaceLuaEnv.env,
|
client.clientSystem.spaceLuaEnv.env,
|
||||||
|
|
Loading…
Reference in New Issue