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);
|
sf.threadLocal.setLocal("_GLOBAL", env);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await evalExpr(
|
await evalExpr(
|
||||||
"interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
|
"space_lua.interpolate('Hello, ${globalEnv} and ${loc}!', {loc='local'})",
|
||||||
env,
|
env,
|
||||||
sf,
|
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 {}
|
// Some more complex string interpolation with more complex lua expressions, with nested {}
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await evalExpr(
|
await evalExpr(
|
||||||
`interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
|
`space_lua.interpolate('Some JSON \${js.stringify(js.tojs({name="Pete"}))}!')`,
|
||||||
env,
|
env,
|
||||||
sf,
|
sf,
|
||||||
),
|
),
|
||||||
|
|
|
@ -304,7 +304,7 @@ function evalPrefixExpression(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "FunctionCall": {
|
case "FunctionCall": {
|
||||||
let prefixValue = evalPrefixExpression(e.prefix, env, sf);
|
const prefixValue = evalPrefixExpression(e.prefix, env, sf);
|
||||||
if (!prefixValue) {
|
if (!prefixValue) {
|
||||||
throw new LuaRuntimeError(
|
throw new LuaRuntimeError(
|
||||||
`Attempting to call nil as a function`,
|
`Attempting to call nil as a function`,
|
||||||
|
|
|
@ -13,6 +13,7 @@ Deno.test("Lua language tests", async () => {
|
||||||
const chunk = parse(luaFile, {});
|
const chunk = parse(luaFile, {});
|
||||||
const env = new LuaEnv(luaBuildStandardEnv());
|
const env = new LuaEnv(luaBuildStandardEnv());
|
||||||
const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx);
|
const sf = new LuaStackFrame(new LuaEnv(), chunk.ctx);
|
||||||
|
sf.threadLocal.setLocal("_GLOBAL", env);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await evalStatement(chunk, env, sf);
|
await evalStatement(chunk, env, sf);
|
||||||
|
|
|
@ -690,3 +690,13 @@ end
|
||||||
assert(#points == 6, "Grid should generate 6 points")
|
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[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)")
|
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,
|
LuaExpression,
|
||||||
LuaFunctionBody,
|
LuaFunctionBody,
|
||||||
LuaFunctionCallExpression,
|
LuaFunctionCallExpression,
|
||||||
|
LuaFunctionCallStatement,
|
||||||
LuaFunctionName,
|
LuaFunctionName,
|
||||||
LuaLValue,
|
LuaLValue,
|
||||||
LuaPrefixExpression,
|
LuaPrefixExpression,
|
||||||
|
@ -640,3 +641,13 @@ export function parse(s: string, ctx: ASTCtx = {}): LuaBlock {
|
||||||
export function parseToCrudeAST(t: string): ParseTree {
|
export function parseToCrudeAST(t: string): ParseTree {
|
||||||
return cleanTree(lezerToParseTree(t, parser.parse(t).topNode), true);
|
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);
|
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 {
|
export function luaTypeOf(val: any): LuaType {
|
||||||
if (val === null || val === undefined) {
|
if (val === null || val === undefined) {
|
||||||
return "nil";
|
return "nil";
|
||||||
|
|
|
@ -6,22 +6,16 @@ import {
|
||||||
luaGet,
|
luaGet,
|
||||||
LuaMultiRes,
|
LuaMultiRes,
|
||||||
LuaRuntimeError,
|
LuaRuntimeError,
|
||||||
LuaTable,
|
type LuaTable,
|
||||||
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 { spaceLuaApi } from "$common/space_lua/stdlib/space_lua.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))));
|
||||||
|
@ -129,82 +123,6 @@ 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
|
||||||
|
@ -225,13 +143,12 @@ 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);
|
||||||
env.set("table", tableApi);
|
env.set("table", tableApi);
|
||||||
env.set("os", osApi);
|
env.set("os", osApi);
|
||||||
env.set("js", jsApi);
|
env.set("js", jsApi);
|
||||||
|
env.set("space_lua", spaceLuaApi);
|
||||||
return env;
|
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 {
|
import {
|
||||||
LuaBuiltinFunction,
|
LuaBuiltinFunction,
|
||||||
luaCall,
|
luaCall,
|
||||||
LuaFunction,
|
|
||||||
LuaMultiRes,
|
LuaMultiRes,
|
||||||
LuaTable,
|
LuaTable,
|
||||||
luaToString,
|
luaToString,
|
||||||
|
|
Loading…
Reference in New Issue