import { type ILuaFunction, LuaBuiltinFunction, luaCall, LuaEnv, luaGet, luaKeys, LuaMultiRes, LuaRuntimeError, type LuaTable, luaToString, luaTypeOf, type LuaValue, } from "./runtime.ts"; import { stringApi } from "./stdlib/string.ts"; import { tableApi } from "./stdlib/table.ts"; import { osApi } from "./stdlib/os.ts"; import { jsApi } from "./stdlib/js.ts"; import { spaceluaApi } from "./stdlib/space_lua.ts"; import { mathApi } from "./stdlib/math.ts"; import { parse } from "./parse.ts"; import { evalStatement } from "./eval.ts"; const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { console.log( "[Lua]", ...(await Promise.all(args.map((v) => luaToString(v)))), ); }); const assertFunction = new LuaBuiltinFunction( async (sf, value: any, message?: string) => { if (!await value) { throw new LuaRuntimeError(`Assertion failed: ${message}`, sf); } }, ); const ipairsFunction = new LuaBuiltinFunction((sf, ar: LuaTable | any[]) => { let i = 1; return async () => { if (i > ar.length) { return; } const result = new LuaMultiRes([i, await luaGet(ar, i, sf)]); i++; return result; }; }); const pairsFunction = new LuaBuiltinFunction( (sf, t: LuaTable | any[] | Record) => { let keys: (string | number)[]; if (Array.isArray(t)) { keys = Array.from({ length: t.length }, (_, i) => i + 1); // For arrays, generate 1-based indices } else if (t.keys) { keys = t.keys(); } else { // For plain JavaScript objects case, note: this will also include keys from the prototype keys = []; for (const key in t) { keys.push(key); } } let i = 0; return async () => { if (i >= keys.length) { return; } const key = keys[i]; i++; const value = await luaGet(t, key, sf); return new LuaMultiRes([key, value]); }; }, ); export const eachFunction = new LuaBuiltinFunction( (sf, ar: LuaTable | any[]) => { let i = 1; const length = ar.length; return async () => { if (i > length) { return; } const result = await luaGet(ar, i, sf); i++; return result; }; }, ); const unpackFunction = new LuaBuiltinFunction(async (sf, t: LuaTable) => { const values: LuaValue[] = []; for (let i = 1; i <= t.length; i++) { values.push(await luaGet(t, i, sf)); } return new LuaMultiRes(values); }); const typeFunction = new LuaBuiltinFunction( (_sf, value: LuaValue): string | Promise => { return luaTypeOf(value); }, ); const tostringFunction = new LuaBuiltinFunction((_sf, value: any) => { return luaToString(value); }); const tonumberFunction = new LuaBuiltinFunction( (_sf, value: LuaValue, base?: number) => { if (typeof value === "string" && value.trim() === "") { return null; } if (base !== undefined) { if (base < 2 || base > 36) { return null; } const n = parseInt(String(value), base); if (isNaN(n)) { return null; } return n; } const n = Number(value); if (isNaN(n)) { return null; } return n; }, ); const errorFunction = new LuaBuiltinFunction((sf, message: string) => { throw new LuaRuntimeError(message, sf); }); const pcallFunction = new LuaBuiltinFunction( async (sf, fn: ILuaFunction, ...args) => { try { return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]); } catch (e: any) { if (e instanceof LuaRuntimeError) { return new LuaMultiRes([false, e.message]); } return new LuaMultiRes([false, e.message]); } }, ); const xpcallFunction = new LuaBuiltinFunction( async (sf, fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => { try { return new LuaMultiRes([true, await fn.call(sf, ...args)]); } catch (e: any) { const errorMsg = e instanceof LuaRuntimeError ? e.message : e.message; return new LuaMultiRes([ false, await luaCall(errorHandler, [errorMsg], sf.astCtx!, sf), ]); } }, ); const setmetatableFunction = new LuaBuiltinFunction( (sf, table: LuaTable, metatable: LuaTable) => { if (!metatable) { throw new LuaRuntimeError("metatable cannot be set to nil", sf); } table.metatable = metatable; return table; }, ); const rawsetFunction = new LuaBuiltinFunction( (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => { return table.rawSet(key, value); }, ); const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => { return table.metatable; }); const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => { const global = sf.threadLocal.get("_GLOBAL"); const file = await luaCall( global.get("space").get("readFile"), [filename], sf.astCtx!, sf, ) as Uint8Array; const code = new TextDecoder().decode(file); try { const parsedExpr = parse(code); const env = new LuaEnv(global); await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx)); } catch (e: any) { throw new LuaRuntimeError( `Error evaluating "${filename}": ${e.message}`, sf, ); } }); /** * From the Lua docs: * If index is a number, returns all arguments after argument number index; a negative number indexes * from the end (-1 is the last argument). Otherwise, index must be the string "#", and select returns * the total number of extra arguments it received. */ const selectFunction = new LuaBuiltinFunction( (_sf, index: number | "#", ...args: LuaValue[]) => { if (index === "#") { return args.length; } else if (typeof index === "number") { if (index >= 0) { return new LuaMultiRes(args.slice(index - 1)); } else { return new LuaMultiRes(args.slice(args.length + index)); } } }, ); // Non-standard, but useful const someFunction = new LuaBuiltinFunction(async (_sf, value: any) => { switch (await luaTypeOf(value)) { case "number": if (!isFinite(value)) return null; break; case "string": if (value.trim() === "") return null; break; case "table": if (luaKeys(value).length === 0) return null; } return value; }); export function luaBuildStandardEnv() { const env = new LuaEnv(); // Top-level builtins env.set("print", printFunction); env.set("assert", assertFunction); env.set("type", typeFunction); env.set("tostring", tostringFunction); env.set("tonumber", tonumberFunction); env.set("unpack", unpackFunction); env.set("select", selectFunction); // Iterators env.set("pairs", pairsFunction); env.set("ipairs", ipairsFunction); // meta table stuff env.set("setmetatable", setmetatableFunction); env.set("getmetatable", getmetatableFunction); env.set("rawset", rawsetFunction); env.set("dofile", dofileFunction); // Error handling env.set("error", errorFunction); env.set("pcall", pcallFunction); env.set("xpcall", xpcallFunction); // APIs env.set("string", stringApi); env.set("table", tableApi); env.set("os", osApi); env.set("js", jsApi); env.set("math", mathApi); // Non-standard env.set("each", eachFunction); env.set("spacelua", spaceluaApi); env.set("some", someFunction); return env; }