Lua: language fixes, and new space_lua APIs
parent
86f31e3a00
commit
15ad6f3129
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
),
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
LuaBuiltinFunction,
|
||||
luaCall,
|
||||
LuaFunction,
|
||||
LuaMultiRes,
|
||||
LuaTable,
|
||||
luaToString,
|
||||
|
|
Loading…
Reference in New Issue