Lua: language fixes, and new space_lua APIs

main
Zef Hemel 2025-01-09 18:22:12 +01:00
parent 86f31e3a00
commit 15ad6f3129
9 changed files with 164 additions and 90 deletions

View File

@ -430,7 +430,7 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
sf.threadLocal.setLocal("_GLOBAL", env);
assertEquals(
await evalExpr(
"interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
"space_lua.interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
env,
sf,
),
@ -440,7 +440,7 @@ Deno.test("Thread local _CTX - advanced cases", async () => {
// Some more complex string interpolation with more complex lua expressions, with nested {}
assertEquals(
await evalExpr(
`interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
`space_lua.interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
env,
sf,
),

View File

@ -304,7 +304,7 @@ function evalPrefixExpression(
}
}
case "FunctionCall": {
let prefixValue = evalPrefixExpression(e.prefix, env, sf);
const prefixValue = evalPrefixExpression(e.prefix, env, sf);
if (!prefixValue) {
throw new LuaRuntimeError(
`Attempting to call nil as a function`,

View File

@ -13,6 +13,7 @@ Deno.test("Lua language tests", async () => {
const chunk = parse(luaFile, {});
const env = new LuaEnv(luaBuildStandardEnv());
const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx);
sf.threadLocal.setLocal("_GLOBAL", env);
try {
await evalStatement(chunk, env, sf);

View File

@ -690,3 +690,13 @@ end
assert(#points == 6, "Grid should generate 6 points")
assert(points[1][1] == 1 and points[1][2] == 1, "First point should be (1,1)")
assert(points[6][1] == 2 and points[6][2] == 3, "Last point should be (2,3)")
-- Test space_lua stuff
local parsedExpr = space_lua.parse_expression("1 + 1")
local evalResult = space_lua.eval_expression(parsedExpr)
assert(evalResult == 2, "Eval should return 2")
-- Slightly more advanced example with augmented environment
local parsedExpr = space_lua.parse_expression("tostring(a + 1)")
local evalResult = space_lua.eval_expression(parsedExpr, { a = 1 })
assert(evalResult == "2", "Eval should return 2 as a string")

View File

@ -13,6 +13,7 @@ import type {
LuaExpression,
LuaFunctionBody,
LuaFunctionCallExpression,
LuaFunctionCallStatement,
LuaFunctionName,
LuaLValue,
LuaPrefixExpression,
@ -640,3 +641,13 @@ export function parse(s: string, ctx: ASTCtx = {}): LuaBlock {
export function parseToCrudeAST(t: string): ParseTree {
return cleanTree(lezerToParseTree(t, parser.parse(t).topNode), true);
}
/**
* Helper function to parse a Lua expression string
*/
export function parseExpressionString(
expr: string,
): LuaExpression {
const parsedLua = parse(`_(${expr})`) as LuaBlock;
return (parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0];
}

View File

@ -557,6 +557,16 @@ export function luaCall(
return fn.call((sf || LuaStackFrame.lostFrame).withCtx(ctx), ...args);
}
export function luaKeys(val: any): any[] {
if (val instanceof LuaTable) {
return val.keys();
} else if (Array.isArray(val)) {
return val.map((_, i) => i + 1);
} else {
return Object.keys(val);
}
}
export function luaTypeOf(val: any): LuaType {
if (val === null || val === undefined) {
return "nil";

View File

@ -6,22 +6,16 @@ import {
luaGet,
LuaMultiRes,
LuaRuntimeError,
LuaTable,
type LuaTable,
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";
import { spaceLuaApi } from "$common/space_lua/stdlib/space_lua.ts";
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
@ -129,82 +123,6 @@ 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
@ -225,13 +143,12 @@ 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);
env.set("table", tableApi);
env.set("os", osApi);
env.set("js", jsApi);
env.set("space_lua", spaceLuaApi);
return env;
}

View File

@ -0,0 +1,126 @@
import { parseExpressionString } from "$common/space_lua/parse.ts";
import type { LuaExpression } from "$common/space_lua/ast.ts";
import { evalExpression } from "$common/space_lua/eval.ts";
import {
LuaBuiltinFunction,
LuaEnv,
LuaRuntimeError,
type LuaStackFrame,
LuaTable,
luaToString,
luaValueToJS,
} from "$common/space_lua/runtime.ts";
/**
* These are Space Lua specific functions that are available to all scripts, but are not part of the standard Lua language.
*/
/**
* Helper function to create an augmented environment
*/
function createAugmentedEnv(
sf: LuaStackFrame,
envAugmentation?: LuaTable,
): LuaEnv {
const globalEnv = sf.threadLocal.get("_GLOBAL");
if (!globalEnv) {
throw new Error("_GLOBAL not defined");
}
const env = new LuaEnv(globalEnv);
if (envAugmentation) {
for (const key of envAugmentation.keys()) {
env.setLocal(key, envAugmentation.rawGet(key));
}
}
return env;
}
export const spaceLuaApi = new LuaTable({
/**
* Parses a lua expression and returns the parsed expression.
*
* @param sf - The current space_lua state.
* @param luaExpression - The lua expression to parse.
* @returns The parsed expression.
*/
parse_expression: new LuaBuiltinFunction(
(_sf, luaExpression: string) => {
return parseExpressionString(luaExpression);
},
),
/**
* Evaluates a parsed lua expression and returns the result.
*
* @param sf - The current space_lua state.
* @param parsedExpr - The parsed lua expression to evaluate.
* @param envAugmentation - An optional environment to augment the global environment with.
* @returns The result of the evaluated expression.
*/
eval_expression: new LuaBuiltinFunction(
async (sf, parsedExpr: LuaExpression, envAugmentation?: LuaTable) => {
const env = createAugmentedEnv(sf, envAugmentation);
return luaValueToJS(await evalExpression(parsedExpr, env, sf));
},
),
/**
* Interpolates a string with lua expressions and returns the result.
*
* @param sf - The current space_lua state.
* @param template - The template string to interpolate.
* @param envAugmentation - An optional environment to augment the global environment with.
* @returns The interpolated string.
*/
interpolate: new LuaBuiltinFunction(
async (sf, template: string, envAugmentation?: 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 parsedExpr = parseExpressionString(expr);
const env = createAugmentedEnv(sf, envAugmentation);
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;
},
),
});

View File

@ -1,7 +1,6 @@
import {
LuaBuiltinFunction,
luaCall,
LuaFunction,
LuaMultiRes,
LuaTable,
luaToString,