Lua: Set string as metatable of string values, allowing for strVal:upper() style invocations
parent
49b8a0f7dc
commit
654151437c
|
@ -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) {
|
|
||||||
throw new LuaRuntimeError(
|
|
||||||
`Attempting to index a non-table: ${prefixValue}`,
|
|
||||||
sf.withCtx(e.prefix.ctx),
|
|
||||||
);
|
|
||||||
} else if (e.name) {
|
|
||||||
selfArgs = [prefixValue];
|
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) {
|
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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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();
|
||||||
}),
|
}),
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue