From 654151437c1f754dc97d69d9f0ff8adc8bbc2050 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sun, 9 Feb 2025 11:06:15 +0100 Subject: [PATCH] Lua: Set string as metatable of string values, allowing for strVal:upper() style invocations --- common/space_lua/eval.ts | 71 ++++++++++++++---- common/space_lua/runtime.ts | 99 ++++++++++++++++--------- common/space_lua/stdlib/string_test.lua | 5 +- common/space_lua/stdlib/table.ts | 1 - 4 files changed, 124 insertions(+), 52 deletions(-) diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 8b3631f7..9da3d6fc 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -8,6 +8,7 @@ import { evalPromiseValues } from "$common/space_lua/util.ts"; import { luaCall, luaEquals, + luaIndexValue, luaSet, type LuaStackFrame, } from "$common/space_lua/runtime.ts"; @@ -407,12 +408,17 @@ function evalPrefixExpression( ); } - const handleFunctionCall = (prefixValue: LuaValue) => { + let selfArgs: LuaValue[] = []; + + const handleFunctionCall = ( + prefixValue: LuaValue, + ): LuaValue | Promise => { // Special handling for f(...) - propagate varargs if ( e.args.length === 1 && e.args[0].type === "Variable" && e.args[0].name === "..." ) { + // TODO: Clean this up const varargs = env.get("..."); const resolveVarargs = async () => { const resolvedVarargs = await Promise.resolve(varargs); @@ -439,16 +445,19 @@ function evalPrefixExpression( } } - // Normal argument handling - let selfArgs: LuaValue[] = []; - if (e.name && !prefixValue.get) { - throw new LuaRuntimeError( - `Attempting to index a non-table: ${prefixValue}`, - sf.withCtx(e.prefix.ctx), - ); - } else if (e.name) { + // Normal argument handling for hello:there(a, b, c) type calls + if (e.name) { selfArgs = [prefixValue]; - prefixValue = prefixValue.get(e.name); + prefixValue = luaIndexValue(prefixValue, e.name, sf); + if (prefixValue === null) { + throw new LuaRuntimeError( + `Attempting to index a non-table: ${prefixValue}`, + sf.withCtx(e.prefix.ctx), + ); + } + if (prefixValue instanceof Promise) { + return prefixValue.then(handleFunctionCall); + } } if (!prefixValue.call) { throw new LuaRuntimeError( @@ -486,15 +495,49 @@ function evalMetamethod( ctx: ASTCtx, sf: LuaStackFrame, ): LuaValue | undefined { - if (left?.metatable?.has(metaMethod)) { - const fn = left.metatable.get(metaMethod); + const leftMetatable = getMetatable(left, sf); + const rightMetatable = getMetatable(right, sf); + if (leftMetatable?.has(metaMethod)) { + const fn = leftMetatable.get(metaMethod); return luaCall(fn, [left, right], ctx, sf); - } else if (right?.metatable?.has(metaMethod)) { - const fn = right.metatable.get(metaMethod); + } else if (rightMetatable?.has(metaMethod)) { + const fn = rightMetatable.get(metaMethod); return luaCall(fn, [left, right], ctx, sf); } } +export function getMetatable( + value: LuaValue, + sf?: LuaStackFrame, +): LuaValue | null { + if (value === null || value === undefined) { + return null; + } + if (typeof value === "string") { + // Add a metatable to the string value on the fly + if (!sf) { + console.warn( + "metatable lookup with string value but no stack frame, returning nil", + ); + return null; + } + if (!sf.threadLocal.get("_GLOBAL")) { + console.warn( + "metatable lookup with string value but no _GLOBAL, returning nil", + ); + return null; + } + const stringMetatable = new LuaTable(); + stringMetatable.set("__index", sf.threadLocal.get("_GLOBAL").get("string")); + return stringMetatable; + } + if (value.metatable) { + return value.metatable; + } else { + return null; + } +} + // Simplified operator definitions const operatorsMetaMethods: Record | void { - if (this.metatable && this.metatable.has("__newindex") && !this.has(key)) { + const metatable = getMetatable(this, sf); + if (metatable && metatable.has("__newindex") && !this.has(key)) { // Invoke the meta table! - const metaValue = this.metatable.get("__newindex", sf); + const metaValue = metatable.get("__newindex", sf); if (metaValue.then) { // This is a promise, we need to wait for it return metaValue.then((metaValue: any) => { @@ -411,37 +412,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise | null { - const value = this.rawGet(key); - if (value === undefined || value === null) { - if (this.metatable && this.metatable.has("__index")) { - // Invoke the meta table - const metaValue = this.metatable.get("__index", sf); - if (metaValue.then) { - // Got a promise, we need to wait for it - return metaValue.then((metaValue: any) => { - if (metaValue.call) { - return metaValue.call(sf, this, key); - } else if (metaValue instanceof LuaTable) { - return metaValue.get(key, sf); - } else { - throw new Error("Meta table __index must be a function or table"); - } - }); - } else { - if (metaValue.call) { - return metaValue.call(sf, this, key); - } else if (metaValue instanceof LuaTable) { - return metaValue.get(key, sf); - } else { - throw new Error("Meta table __index must be a function or table"); - } - } - } else { - return null; - } - } else { - return value; - } + return luaIndexValue(this, key, sf); } insert(value: LuaValue, pos: number) { @@ -483,8 +454,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } async toStringAsync(): Promise { - if (this.metatable?.has("__tostring")) { - const metaValue = await this.metatable.get("__tostring"); + const metatable = getMetatable(this); + if (metatable && metatable.has("__tostring")) { + const metaValue = await metatable.get("__tostring"); if (metaValue.call) { return metaValue.call(LuaStackFrame.lostFrame, this); } else { @@ -515,6 +487,59 @@ export class LuaTable implements ILuaSettable, ILuaGettable { } } +/** + * Lookup a key in a table or a metatable + */ +export function luaIndexValue( + value: LuaValue, + key: LuaValue, + sf?: LuaStackFrame, +): LuaValue | Promise | null { + if (value === null || value === undefined) { + return null; + } + // The value is a table, so we can try to get the value directly + if (value instanceof LuaTable) { + const rawValue = value.rawGet(key); + if (rawValue !== undefined) { + return rawValue; + } + } + // If not, let's see if the value has a metatable and if it has a __index metamethod + const metatable = getMetatable(value, sf); + if (metatable && metatable.has("__index")) { + // Invoke the meta table + const metaValue = metatable.get("__index", sf); + if (metaValue.then) { + // Got a promise, we need to wait for it + return metaValue.then((metaValue: any) => { + if (metaValue.call) { + return metaValue.call(sf, value, key); + } else if (metaValue instanceof LuaTable) { + return metaValue.get(key, sf); + } else { + throw new Error("Meta table __index must be a function or table"); + } + }); + } else { + if (metaValue.call) { + return metaValue.call(sf, value, key); + } else if (metaValue instanceof LuaTable) { + return metaValue.get(key, sf); + } else { + throw new Error("Meta table __index must be a function or table"); + } + } + } + // If not, perhaps let's assume this is a plain JavaScript object and we just index into it + const objValue = value[key]; + if (objValue === undefined || objValue === null) { + return null; + } else { + return objValue; + } +} + export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue }; export async function luaSet( @@ -576,6 +601,8 @@ export function luaLen(obj: any): number { return obj.length; } else if (Array.isArray(obj)) { return obj.length; + } else if (typeof obj === "string") { + return obj.length; } else { return 0; } diff --git a/common/space_lua/stdlib/string_test.lua b/common/space_lua/stdlib/string_test.lua index 82a2a2b7..a5ae2a60 100644 --- a/common/space_lua/stdlib/string_test.lua +++ b/common/space_lua/stdlib/string_test.lua @@ -14,6 +14,10 @@ assert(string.sub("Hello", 2, 4) == "ell") assert(string.upper("Hello") == "HELLO") assert(string.lower("Hello") == "hello") +-- Invoke string metatable methods +assertEqual(("hello"):len(), 5) +assertEqual(("hello"):upper(), "HELLO") + -- Test string.gsub with various replacement types -- Simple string replacement local result, count = string.gsub("hello world", "hello", "hi") @@ -79,7 +83,6 @@ assertEqual(m2, "ello") -- Test with pattern with character class assertEqual(string.match("c", "[abc]"), "c") - -- Test match with init position - need to capture the group local initMatch = string.match("hello world", "(world)", 7) assertEqual(initMatch, "world") diff --git a/common/space_lua/stdlib/table.ts b/common/space_lua/stdlib/table.ts index cb275372..ac7c6099 100644 --- a/common/space_lua/stdlib/table.ts +++ b/common/space_lua/stdlib/table.ts @@ -75,7 +75,6 @@ export const tableApi = new LuaTable({ * @returns The keys of the table. */ keys: new LuaBuiltinFunction((_sf, tbl: LuaTable | LuaEnv) => { - console.log("Keys", tbl); return tbl.keys(); }), /**