Lua: Set string as metatable of string values, allowing for strVal:upper() style invocations

pull/1219/merge
Zef Hemel 2025-02-09 11:06:15 +01:00
parent 49b8a0f7dc
commit 654151437c
4 changed files with 124 additions and 52 deletions

View File

@ -8,6 +8,7 @@ import { evalPromiseValues } from "$common/space_lua/util.ts";
import { import {
luaCall, luaCall,
luaEquals, luaEquals,
luaIndexValue,
luaSet, luaSet,
type LuaStackFrame, type LuaStackFrame,
} from "$common/space_lua/runtime.ts"; } 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<LuaValue> => {
// Special handling for f(...) - propagate varargs // Special handling for f(...) - propagate varargs
if ( if (
e.args.length === 1 && e.args[0].type === "Variable" && e.args.length === 1 && e.args[0].type === "Variable" &&
e.args[0].name === "..." e.args[0].name === "..."
) { ) {
// TODO: Clean this up
const varargs = env.get("..."); const varargs = env.get("...");
const resolveVarargs = async () => { const resolveVarargs = async () => {
const resolvedVarargs = await Promise.resolve(varargs); const resolvedVarargs = await Promise.resolve(varargs);
@ -439,16 +445,19 @@ function evalPrefixExpression(
} }
} }
// Normal argument handling // Normal argument handling for hello:there(a, b, c) type calls
let selfArgs: LuaValue[] = []; if (e.name) {
if (e.name && !prefixValue.get) { selfArgs = [prefixValue];
prefixValue = luaIndexValue(prefixValue, e.name, sf);
if (prefixValue === null) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Attempting to index a non-table: ${prefixValue}`, `Attempting to index a non-table: ${prefixValue}`,
sf.withCtx(e.prefix.ctx), sf.withCtx(e.prefix.ctx),
); );
} else if (e.name) { }
selfArgs = [prefixValue]; if (prefixValue instanceof Promise) {
prefixValue = prefixValue.get(e.name); return prefixValue.then(handleFunctionCall);
}
} }
if (!prefixValue.call) { if (!prefixValue.call) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
@ -486,15 +495,49 @@ function evalMetamethod(
ctx: ASTCtx, ctx: ASTCtx,
sf: LuaStackFrame, sf: LuaStackFrame,
): LuaValue | undefined { ): LuaValue | undefined {
if (left?.metatable?.has(metaMethod)) { const leftMetatable = getMetatable(left, sf);
const fn = left.metatable.get(metaMethod); const rightMetatable = getMetatable(right, sf);
if (leftMetatable?.has(metaMethod)) {
const fn = leftMetatable.get(metaMethod);
return luaCall(fn, [left, right], ctx, sf); return luaCall(fn, [left, right], ctx, sf);
} else if (right?.metatable?.has(metaMethod)) { } else if (rightMetatable?.has(metaMethod)) {
const fn = right.metatable.get(metaMethod); const fn = rightMetatable.get(metaMethod);
return luaCall(fn, [left, right], ctx, sf); 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 // Simplified operator definitions
const operatorsMetaMethods: Record<string, { const operatorsMetaMethods: Record<string, {
metaMethod?: string; metaMethod?: string;

View File

@ -1,5 +1,5 @@
import type { ASTCtx, LuaFunctionBody } from "./ast.ts"; import type { ASTCtx, LuaFunctionBody } from "./ast.ts";
import { evalStatement } from "./eval.ts"; import { evalStatement, getMetatable } from "./eval.ts";
import { asyncQuickSort, evalPromiseValues } from "./util.ts"; import { asyncQuickSort, evalPromiseValues } from "./util.ts";
export type LuaType = export type LuaType =
@ -383,9 +383,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
value: LuaValue, value: LuaValue,
sf?: LuaStackFrame, sf?: LuaStackFrame,
): Promise<void> | void { ): Promise<void> | 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! // Invoke the meta table!
const metaValue = this.metatable.get("__newindex", sf); const metaValue = metatable.get("__newindex", sf);
if (metaValue.then) { if (metaValue.then) {
// This is a promise, we need to wait for it // This is a promise, we need to wait for it
return metaValue.then((metaValue: any) => { return metaValue.then((metaValue: any) => {
@ -411,37 +412,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
} }
get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null { get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null {
const value = this.rawGet(key); return luaIndexValue(this, key, sf);
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;
}
} }
insert(value: LuaValue, pos: number) { insert(value: LuaValue, pos: number) {
@ -483,8 +454,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
} }
async toStringAsync(): Promise<string> { async toStringAsync(): Promise<string> {
if (this.metatable?.has("__tostring")) { const metatable = getMetatable(this);
const metaValue = await this.metatable.get("__tostring"); if (metatable && metatable.has("__tostring")) {
const metaValue = await metatable.get("__tostring");
if (metaValue.call) { if (metaValue.call) {
return metaValue.call(LuaStackFrame.lostFrame, this); return metaValue.call(LuaStackFrame.lostFrame, this);
} else { } 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<LuaValue> | 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 type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
export async function luaSet( export async function luaSet(
@ -576,6 +601,8 @@ export function luaLen(obj: any): number {
return obj.length; return obj.length;
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
return obj.length; return obj.length;
} else if (typeof obj === "string") {
return obj.length;
} else { } else {
return 0; return 0;
} }

View File

@ -14,6 +14,10 @@ assert(string.sub("Hello", 2, 4) == "ell")
assert(string.upper("Hello") == "HELLO") assert(string.upper("Hello") == "HELLO")
assert(string.lower("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 -- Test string.gsub with various replacement types
-- Simple string replacement -- Simple string replacement
local result, count = string.gsub("hello world", "hello", "hi") local result, count = string.gsub("hello world", "hello", "hi")
@ -79,7 +83,6 @@ assertEqual(m2, "ello")
-- Test with pattern with character class -- Test with pattern with character class
assertEqual(string.match("c", "[abc]"), "c") assertEqual(string.match("c", "[abc]"), "c")
-- Test match with init position - need to capture the group -- Test match with init position - need to capture the group
local initMatch = string.match("hello world", "(world)", 7) local initMatch = string.match("hello world", "(world)", 7)
assertEqual(initMatch, "world") assertEqual(initMatch, "world")

View File

@ -75,7 +75,6 @@ export const tableApi = new LuaTable({
* @returns The keys of the table. * @returns The keys of the table.
*/ */
keys: new LuaBuiltinFunction((_sf, tbl: LuaTable | LuaEnv) => { keys: new LuaBuiltinFunction((_sf, tbl: LuaTable | LuaEnv) => {
console.log("Keys", tbl);
return tbl.keys(); return tbl.keys();
}), }),
/** /**