Files
plainleaf/client/space_lua/eval.ts
T
Matouš Jan FialkaandGitHub 27339c98f6 Space Lua: Align arithmetic model with standard Lua (#1611)
* Align Space Lua arithmetic model with standard Lua
* Add missing __unm and __bnot metamethods
- add new basic tests
- add helper for negative tests
* Add comments for ASCII numeric constants

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
2025-10-16 08:41:47 +02:00

1235 lines
31 KiB
TypeScript

import type {
ASTCtx,
LuaExpression,
LuaLValue,
LuaStatement,
NumericType,
} from "./ast.ts";
import { evalPromiseValues } from "./util.ts";
import {
type ILuaFunction,
type ILuaGettable,
type ILuaSettable,
jsToLuaValue,
LuaBreak,
luaCall,
LuaEnv,
luaEquals,
LuaFunction,
luaGet,
luaIndexValue,
luaLen,
type LuaLValueContainer,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
luaSet,
type LuaStackFrame,
LuaTable,
luaToString,
luaTruthy,
type LuaValue,
luaValueToJS,
singleResult,
} from "./runtime.ts";
import {
ArrayQueryCollection,
type LuaCollectionQuery,
} from "./query_collection.ts";
import {
boxZero,
coerceNumeric,
coerceNumericPair,
type OpHints,
} from "./numeric.ts";
function luaFloorDiv(
a: unknown,
b: unknown,
ctx: ASTCtx,
sf: LuaStackFrame,
hints?: OpHints,
): number {
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
if (bothInt && bx === 0) {
throw new LuaRuntimeError(
`attempt to divide by zero`,
sf.withCtx(ctx),
);
}
const q = Math.floor(ax / bx);
if (q === 0) {
if (bothInt) {
return boxZero("int");
}
return Object.is(q, -0) ? -0 : boxZero("float");
}
return q;
}
function luaMod(
a: unknown,
b: unknown,
ctx: ASTCtx,
sf: LuaStackFrame,
hints?: OpHints,
): number {
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
if (bothInt && bx === 0) {
throw new LuaRuntimeError(
`attempt to perform modulo by zero`,
sf.withCtx(ctx),
);
}
const q = Math.floor(ax / bx);
const r = ax - q * bx;
if (r === 0) {
if (Object.is(ax, -0)) {
return -0;
}
return boxZero(bothInt ? "int" : "float");
}
return r;
}
function luaLess(
a: any,
b: any,
ctx: ASTCtx,
sf: LuaStackFrame,
): boolean {
const { ta, tb, av, bv } = luaRelOperands(a, b);
if (ta === "number" && tb === "number") {
return av < bv;
}
if (ta === "string" && tb === "string") {
return av < bv;
}
throw new LuaRuntimeError(
`attempt to compare ${ta} with ${tb}`,
sf.withCtx(ctx),
);
}
function luaLessEqual(
a: any,
b: any,
ctx: ASTCtx,
sf: LuaStackFrame,
): boolean {
const { ta, tb, av, bv } = luaRelOperands(a, b);
if (ta === "number" && tb === "number") {
return av <= bv;
}
if (ta === "string" && tb === "string") {
return av <= bv;
}
throw new LuaRuntimeError(
`attempt to compare ${ta} with ${tb}`,
sf.withCtx(ctx),
);
}
function luaUnaryMinus(
v: any,
): number {
const { n, zeroKind } = coerceNumeric(v);
if (n === 0) {
if (Object.is(n, -0)) {
return boxZero("float");
}
if (zeroKind === "int") {
return boxZero("int");
}
if (zeroKind === "float") {
return -0;
}
return -0;
}
return -n;
}
async function handleTableFieldSync(
table: LuaTable,
field: any,
env: LuaEnv,
sf: LuaStackFrame,
): Promise<void> {
switch (field.type) {
case "PropField": {
const value = await evalExpression(field.value, env, sf);
table.set(field.key, singleResult(value), sf);
break;
}
case "DynamicField": {
const key = await evalExpression(field.key, env, sf);
const value = await evalExpression(field.value, env, sf);
table.set(singleResult(key), singleResult(value), sf);
break;
}
case "ExpressionField": {
const value = await evalExpression(field.value, env, sf);
if (value instanceof LuaMultiRes) {
for (const val of value.values) {
table.set(table.length + 1, val, sf);
}
} else {
table.set(table.length + 1, singleResult(value), sf);
}
break;
}
}
}
// Unwrap parentheses and unary +/- around a numeric literal
function astNumberKind(e: LuaExpression | undefined): NumericType | undefined {
if (!e) {
return undefined;
}
while (e.type === "Parenthesized") {
e = e.expression;
}
if (e.type === "Unary" && (e.operator === "-" || e.operator === "+")) {
return astNumberKind(e.argument);
}
if (e.type === "Number") {
return e.numericType === "int" ? "int" : "float";
}
return undefined;
}
export function evalExpression(
e: LuaExpression,
env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue> | LuaValue {
try {
switch (e.type) {
case "String": {
return e.value;
}
case "Number": {
return (e.value === 0 && !Object.is(e.value, -0))
? boxZero(e.numericType === "int" ? "int" : "float")
: e.value;
}
case "Boolean": {
return e.value;
}
case "Nil": {
return null;
}
case "Binary": {
if (e.operator === "or") {
// Special case: eagerly evaluate left before even attempting right
return evalLogical("or", e.left, e.right, env, sf);
} else if (e.operator === "and") {
// Special case: eagerly evaluate left before even attempting right
return evalLogical("and", e.left, e.right, env, sf);
}
// Enforce left-to-right evaluation
const hints: OpHints = opHintsFromBinary(e);
return evalBinaryWithLR(
e.operator,
e.left,
e.right,
e.ctx,
env,
sf,
hints,
);
}
case "Unary": {
const value = evalExpression(e.argument, env, sf);
if (value instanceof Promise) {
return value.then((value) => {
switch (e.operator) {
case "-": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__unm",
e.ctx,
sf,
() => luaUnaryMinus(arg),
);
}
case "+": {
return +singleResult(value);
}
case "not": {
return !singleResult(value);
}
case "~": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__bnot",
e.ctx,
sf,
() => ~exactInt(arg, e.ctx, sf),
);
}
case "#": {
return luaLen(singleResult(value));
}
default: {
throw new LuaRuntimeError(
`Unknown unary operator ${e.operator}`,
sf.withCtx(e.ctx),
);
}
}
});
} else {
switch (e.operator) {
case "-": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__unm",
e.ctx,
sf,
() => luaUnaryMinus(arg),
);
}
case "+": {
return +singleResult(value);
}
case "not": {
return !singleResult(value);
}
case "~": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__bnot",
e.ctx,
sf,
() => ~exactInt(arg, e.ctx, sf),
);
}
case "#": {
return luaLen(singleResult(value));
}
default: {
throw new LuaRuntimeError(
`Unknown unary operator ${e.operator}`,
sf.withCtx(e.ctx),
);
}
}
}
}
case "Variable":
case "FunctionCall":
case "TableAccess":
case "PropertyAccess": {
return evalPrefixExpression(e, env, sf);
}
case "TableConstructor": {
return Promise.resolve().then(async () => {
const table = new LuaTable();
for (const field of e.fields) {
await handleTableFieldSync(table, field, env, sf);
}
return table;
});
}
case "FunctionDefinition": {
return new LuaFunction(e.body, env);
}
case "Query": {
// console.log("Query", e);
const findFromClause = e.clauses.find((c) => c.type === "From");
if (!findFromClause) {
throw new LuaRuntimeError(
"No from clause found",
sf.withCtx(e.ctx),
);
}
const objectVariable = findFromClause.name;
const objectExpression = findFromClause.expression;
return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
async (collection: LuaValue) => {
if (!collection) {
throw new LuaRuntimeError(
"Collection is nil",
sf.withCtx(e.ctx),
);
}
collection = luaValueToJS(collection, sf);
// Check if collection is a queryable collection
if (!collection.query) {
// If not, try to convert it to JS and see if it's an array
collection = await luaValueToJS(collection, sf);
if (!Array.isArray(collection)) {
throw new LuaRuntimeError(
"Collection does not support query",
sf.withCtx(e.ctx),
);
}
collection = new ArrayQueryCollection(collection);
}
// Build up query object
const query: LuaCollectionQuery = {
objectVariable,
distinct: true,
};
// Map clauses to query parameters
for (const clause of e.clauses) {
switch (clause.type) {
case "Where": {
query.where = clause.expression;
break;
}
case "OrderBy": {
query.orderBy = clause.orderBy.map((o) => ({
expr: o.expression,
desc: o.direction === "desc",
}));
break;
}
case "Select": {
query.select = clause.expression;
break;
}
case "Limit": {
const limitVal = await evalExpression(clause.limit, env, sf);
query.limit = Number(limitVal);
if (clause.offset) {
const offsetVal = await evalExpression(
clause.offset,
env,
sf,
);
query.offset = Number(offsetVal);
}
break;
}
}
}
return collection.query(query, env, sf).then(jsToLuaValue);
},
);
}
default:
throw new LuaRuntimeError(
`Unknown expression type ${e.type}`,
sf.withCtx(e.ctx),
);
}
} catch (err: any) {
// Repackage any non Lua-specific exceptions with some position information
if (!err.constructor.name.startsWith("Lua")) {
throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
} else {
throw err;
}
}
}
function evalPrefixExpression(
e: LuaExpression,
env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue> | LuaValue {
switch (e.type) {
case "Variable": {
const value = env.get(e.name);
if (value === undefined) {
return null;
} else {
return value;
}
}
case "Parenthesized": {
return evalExpression(e.expression, env, sf);
}
// <<expr>>[<<expr>>]
case "TableAccess": {
const values = evalPromiseValues([
evalPrefixExpression(e.object, env, sf),
evalExpression(e.key, env, sf),
]);
if (values instanceof Promise) {
return values.then(([table, key]) => {
table = singleResult(table);
key = singleResult(key);
return luaGet(table, key, sf.withCtx(e.ctx));
});
} else {
const table = singleResult(values[0]);
const key = singleResult(values[1]);
return luaGet(table, singleResult(key), sf.withCtx(e.ctx));
}
}
// <expr>.property
case "PropertyAccess": {
const obj = evalPrefixExpression(e.object, env, sf);
if (obj instanceof Promise) {
return obj.then((obj) => {
return luaGet(obj, e.property, sf.withCtx(e.ctx));
});
} else {
return luaGet(obj, e.property, sf.withCtx(e.ctx));
}
}
case "FunctionCall": {
const prefixValue = evalPrefixExpression(e.prefix, env, sf);
if (!prefixValue) {
throw new LuaRuntimeError(
`Attempting to call nil as a function`,
sf.withCtx(e.prefix.ctx),
);
}
let selfArgs: LuaValue[] = [];
const handleFunctionCall = (
prefixValue: LuaValue,
): LuaValue | Promise<LuaValue> => {
// Normal argument handling for hello:there(a, b, c) type calls
if (e.name) {
selfArgs = [prefixValue];
prefixValue = luaIndexValue(prefixValue, e.name, sf);
if (prefixValue instanceof Promise) {
return prefixValue.then(handleFunctionCall);
}
}
// Unsure if part of the spec, but it seems to be common for lua implementations
// to evaluate all args before evaluating the callee
const args = evalExpressions(e.args, env, sf);
if (args instanceof Promise) {
return args.then((args) =>
luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf)
);
} else {
return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf);
}
};
if (prefixValue instanceof Promise) {
return prefixValue.then(handleFunctionCall);
} else {
return handleFunctionCall(prefixValue);
}
}
default:
throw new LuaRuntimeError(
`Unknown prefix expression type ${e.type}`,
sf.withCtx(e.ctx),
);
}
}
// Helper functions to reduce duplication
function evalMetamethod(
left: any,
right: any,
metaMethod: string,
ctx: ASTCtx,
sf: LuaStackFrame,
): LuaValue | undefined {
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 (rightMetatable?.has(metaMethod)) {
const fn = rightMetatable.get(metaMethod);
return luaCall(fn, [left, right], ctx, sf);
}
}
// Unary metamethod lookup and call
function evalUnaryMetamethod(
value: any,
metaMethod: "__unm" | "__bnot",
ctx: ASTCtx,
sf: LuaStackFrame,
): LuaValue | Promise<LuaValue> | undefined {
const mt = getMetatable(value, sf);
if (mt?.has(metaMethod)) {
const fn = mt.get(metaMethod);
return luaCall(fn, [value], ctx, sf);
}
return undefined;
}
// Unary metamethod handling (with fallback)
function unaryWithMeta(
arg: any,
meta: "__unm" | "__bnot",
ctx: ASTCtx,
sf: LuaStackFrame,
fallback: () => any,
): any {
const mm = evalUnaryMetamethod(arg, meta, ctx, sf);
if (mm !== undefined) {
return mm instanceof Promise ? mm.then(singleResult) : singleResult(mm);
}
return fallback();
}
// Logical short-circuit evaluation
function evalLogical(
op: "and" | "or",
leftExpr: LuaExpression,
rightExpr: LuaExpression,
env: LuaEnv,
sf: LuaStackFrame,
): any {
const left = evalExpression(leftExpr, env, sf);
const decide = (lv: any) => {
if (op === "or") {
if (luaTruthy(lv)) {
return lv;
}
return evalExpression(rightExpr, env, sf);
} else {
if (!luaTruthy(lv)) {
return lv;
}
return evalExpression(rightExpr, env, sf);
}
};
if (left instanceof Promise) {
return left.then(decide);
} else {
return decide(left);
}
}
function opHintsFromBinary(
e: Extract<LuaExpression, { type: "Binary" }>,
): OpHints {
return {
leftKind: astNumberKind(e.left),
rightKind: astNumberKind(e.right),
};
}
function evalBinaryWithLR(
op: string,
leftExpr: LuaExpression,
rightExpr: LuaExpression,
ctx: ASTCtx,
env: LuaEnv,
sf: LuaStackFrame,
hints?: OpHints,
): any {
const leftVal = evalExpression(leftExpr, env, sf);
const evalRightAndOp = (lv: any) => {
const rightVal = evalExpression(rightExpr, env, sf);
if (rightVal instanceof Promise) {
return rightVal.then((rv) =>
luaOp(
op,
singleResult(lv),
singleResult(rv),
ctx,
sf,
hints,
)
);
} else {
return luaOp(
op,
singleResult(leftVal instanceof Promise ? lv : leftVal),
singleResult(rightVal),
ctx,
sf,
hints,
);
}
};
if (leftVal instanceof Promise) {
return leftVal.then(evalRightAndOp);
} else {
return evalRightAndOp(leftVal);
}
}
// Relational comparison "prelude"
function luaRelOperands(
a: any,
b: any,
): {
ta: string;
tb: string;
av: any;
bv: any;
} {
const ta = (a instanceof Number) ? "number" : typeof a;
const tb = (b instanceof Number) ? "number" : typeof b;
const av = (a instanceof Number) ? Number(a) : a;
const bv = (b instanceof Number) ? Number(b) : b;
return { ta, tb, av, bv };
}
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<string, {
metaMethod?: string;
nativeImplementation: (
a: LuaValue,
b: LuaValue,
ctx: ASTCtx,
sf: LuaStackFrame,
hints?: OpHints,
) => LuaValue;
}> = {
"+": {
metaMethod: "__add",
nativeImplementation: (a, b, _ctx, _sf, hints) => {
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
const r = ax + bx;
if (r === 0) {
if (Object.is(r, -0)) {
return bothInt ? boxZero("int") : -0;
}
return boxZero(bothInt ? "int" : "float");
}
return r;
},
},
"-": {
metaMethod: "__sub",
nativeImplementation: (a, b, _ctx, _sf, hints) => {
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
const r = ax - bx;
if (r === 0) {
if (Object.is(r, -0)) {
return bothInt ? boxZero("int") : -0;
}
return boxZero(bothInt ? "int" : "float");
}
return r;
},
},
"*": {
metaMethod: "__mul",
nativeImplementation: (a, b, _ctx, _sf, hints) => {
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
const r = ax * bx;
if (r === 0) {
if (Object.is(r, -0)) {
return bothInt ? boxZero("int") : -0;
}
return boxZero(bothInt ? "int" : "float");
}
return r;
},
},
"/": {
metaMethod: "__div",
nativeImplementation: (a, b, _ctx, _sf, hints) => {
const { ax, bx } = coerceNumericPair(a, b, hints);
return ax / bx;
},
},
"//": {
metaMethod: "__idiv",
nativeImplementation: (a, b, ctx, sf, hints) =>
luaFloorDiv(a, b, ctx, sf, hints),
},
"%": {
metaMethod: "__mod",
nativeImplementation: (a, b, ctx, sf, hints) =>
luaMod(a, b, ctx, sf, hints),
},
"^": {
metaMethod: "__pow",
nativeImplementation: (a, b, _ctx, _sf, hints) => {
const { ax, bx } = coerceNumericPair(a, b, hints);
return ax ** bx;
},
},
"&": {
metaMethod: "__band",
nativeImplementation: (a, b, ctx, sf) =>
exactInt(a, ctx, sf) & exactInt(b, ctx, sf),
},
"|": {
metaMethod: "__bor",
nativeImplementation: (a, b, ctx, sf) =>
exactInt(a, ctx, sf) | exactInt(b, ctx, sf),
},
"~": {
metaMethod: "__bxor",
nativeImplementation: (a, b, ctx, sf) =>
exactInt(a, ctx, sf) ^ exactInt(b, ctx, sf),
},
"<<": {
metaMethod: "__shl",
nativeImplementation: (a, b, ctx, sf) =>
exactInt(a, ctx, sf) << exactInt(b, ctx, sf),
},
">>": {
metaMethod: "__shr",
nativeImplementation: (a, b, ctx, sf) =>
exactInt(a, ctx, sf) >> exactInt(b, ctx, sf),
},
"..": {
metaMethod: "__concat",
nativeImplementation: (a, b) => {
const aString = luaToString(a);
const bString = luaToString(b);
if (aString instanceof Promise || bString instanceof Promise) {
return Promise.all([aString, bString]).then(([a, b]) => a + b);
} else {
return aString + bString;
}
},
},
"==": {
metaMethod: "__eq",
nativeImplementation: (a, b) => luaEquals(a, b),
},
"~=": {
metaMethod: "__ne",
nativeImplementation: (a, b) => !luaEquals(a, b),
},
"!=": {
metaMethod: "__ne",
nativeImplementation: (a, b) => !luaEquals(a, b),
},
"<": {
metaMethod: "__lt",
nativeImplementation: (a, b, ctx, sf) => luaLess(a, b, ctx, sf),
},
"<=": {
metaMethod: "__le",
nativeImplementation: (a, b, ctx, sf) => luaLessEqual(a, b, ctx, sf),
},
">": {
nativeImplementation: (a, b, ctx, sf) => !luaOp("<=", a, b, ctx, sf),
},
">=": {
nativeImplementation: (a, b, ctx, sf) => !luaOp("<", a, b, ctx, sf),
},
};
function luaOp(
op: string,
left: any,
right: any,
ctx: ASTCtx,
sf: LuaStackFrame,
hints?: OpHints,
): any {
const handler = operatorsMetaMethods[op];
if (!handler) {
throw new LuaRuntimeError(`Unknown operator ${op}`, sf.withCtx(ctx));
}
if (handler.metaMethod) {
const metaResult = evalMetamethod(left, right, handler.metaMethod, ctx, sf);
if (metaResult !== undefined) {
return metaResult;
}
}
return handler.nativeImplementation(left, right, ctx, sf, hints);
}
function evalExpressions(
es: LuaExpression[],
env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue[]> | LuaValue[] {
const argsVal = evalPromiseValues(
es.map((arg) => evalExpression(arg, env, sf)),
);
if (argsVal instanceof Promise) {
return argsVal.then((argsResolved) =>
new LuaMultiRes(argsResolved).flatten().values
);
} else {
return new LuaMultiRes(argsVal).flatten().values;
}
}
/**
* Evaluates an expression in two possible modes:
* 1. with `returnOnReturn` set to `true` will return the value of a return statement
* 2. with `returnOnReturn` set to `false` will throw a LuaReturn exception if a return statement is encountered
*/
export async function evalStatement(
s: LuaStatement,
env: LuaEnv,
sf: LuaStackFrame,
returnOnReturn = false,
): Promise<void | LuaValue[]> {
switch (s.type) {
case "Assignment": {
const values = await evalExpressions(s.expressions, env, sf);
const lvalues = await evalPromiseValues(s.variables
.map((lval) => evalLValue(lval, env, sf)));
for (let i = 0; i < lvalues.length; i++) {
await luaSet(
lvalues[i].env,
lvalues[i].key,
values[i],
sf.withCtx(s.ctx),
);
}
break;
}
case "Local": {
if (s.expressions) {
const values = await evalExpressions(s.expressions, env, sf);
for (let i = 0; i < s.names.length; i++) {
env.setLocal(s.names[i].name, values[i]);
}
} else {
for (let i = 0; i < s.names.length; i++) {
env.setLocal(s.names[i].name, null);
}
}
break;
}
case "Semicolon": {
break;
}
case "Label":
case "Goto": {
throw new LuaRuntimeError(
"Labels and gotos are not supported",
sf.withCtx(s.ctx),
);
}
case "Block": {
const newEnv = new LuaEnv(env);
for (const statement of s.statements) {
const result = await evalStatement(
statement,
newEnv,
sf,
returnOnReturn,
);
// Will only happen with `return` statement
if (result !== undefined) {
return result;
}
}
break;
}
case "If": {
for (const cond of s.conditions) {
if (luaTruthy(await evalExpression(cond.condition, env, sf))) {
await evalStatement(cond.block, env, sf);
return;
}
}
if (s.elseBlock) {
await evalStatement(s.elseBlock, env, sf);
return;
}
break;
}
case "While": {
while (luaTruthy(await evalExpression(s.condition, env, sf))) {
try {
await evalStatement(s.block, env, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
case "Repeat": {
do {
try {
await evalStatement(s.block, env, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
} while (!luaTruthy(await evalExpression(s.condition, env, sf)));
break;
}
case "Break": {
throw new LuaBreak();
}
case "FunctionCallStatement": {
await evalExpression(s.call, env, sf);
return;
}
case "Function": {
let body = s.body;
let propNames = s.name.propNames;
if (s.name.colonName) {
// function hello:there() -> function hello.there(self) transformation
body = {
...s.body,
parameters: ["self", ...s.body.parameters],
};
propNames = [...s.name.propNames, s.name.colonName];
}
let settable: ILuaSettable & ILuaGettable = env;
for (let i = 0; i < propNames.length - 1; i++) {
settable = settable.get(propNames[i]);
if (!settable) {
throw new LuaRuntimeError(
`Cannot find property ${propNames[i]}`,
sf.withCtx(s.name.ctx),
);
}
}
settable.set(
propNames[propNames.length - 1],
new LuaFunction(body, env),
);
break;
}
case "LocalFunction": {
env.setLocal(
s.name,
new LuaFunction(s.body, env),
);
break;
}
case "Return": {
if (returnOnReturn) {
return await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env, sf)),
);
} else {
throw new LuaReturn(
await evalPromiseValues(
s.expressions.map((value) => evalExpression(value, env, sf)),
),
);
}
}
case "For": {
const start = await evalExpression(s.start, env, sf);
const end = await evalExpression(s.end, env, sf);
const step = s.step ? await evalExpression(s.step, env, sf) : 1;
for (
let i = start;
step > 0 ? i <= end : i >= end;
i += step
) {
const localEnv = new LuaEnv(env);
localEnv.setLocal(s.name, i);
try {
await evalStatement(s.block, localEnv, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
case "ForIn": {
const iteratorMultiRes = new LuaMultiRes(
await evalPromiseValues(
s.expressions.map((e) => evalExpression(e, env, sf)),
),
).flatten();
let iteratorValue: ILuaFunction | any = iteratorMultiRes.values[0];
// Handle the case where the iterator is a table and we need to call the each function
if (Array.isArray(iteratorValue) || iteratorValue instanceof LuaTable) {
iteratorValue = env.get("each").call(sf, iteratorValue);
}
if (!iteratorValue?.call) {
console.error("Cannot iterate over", iteratorMultiRes.values[0]);
throw new LuaRuntimeError(
`Cannot iterate over ${iteratorMultiRes.values[0]}`,
sf.withCtx(s.ctx),
);
}
const state: LuaValue = iteratorMultiRes.values[1] || null;
const control: LuaValue = iteratorMultiRes.values[2] || null;
while (true) {
const iterResult = new LuaMultiRes(
await luaCall(iteratorValue, [state, control], s.ctx, sf),
).flatten();
if (
iterResult.values[0] === null || iterResult.values[0] === undefined
) {
break;
}
const localEnv = new LuaEnv(env);
for (let i = 0; i < s.names.length; i++) {
localEnv.setLocal(s.names[i], iterResult.values[i]);
}
try {
await evalStatement(s.block, localEnv, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
}
}
break;
}
}
}
function evalLValue(
lval: LuaLValue,
env: LuaEnv,
sf: LuaStackFrame,
): LuaLValueContainer | Promise<LuaLValueContainer> {
switch (lval.type) {
case "Variable": {
return {
env,
key: lval.name,
};
}
case "TableAccess": {
const objValue = evalExpression(
lval.object,
env,
sf,
);
const keyValue = evalExpression(lval.key, env, sf);
if (
objValue instanceof Promise ||
keyValue instanceof Promise
) {
return Promise.all([
objValue instanceof Promise ? objValue : Promise.resolve(objValue),
keyValue instanceof Promise ? keyValue : Promise.resolve(keyValue),
]).then(([objValue, keyValue]) => ({
env: singleResult(objValue),
key: singleResult(keyValue),
}));
} else {
return {
env: singleResult(objValue),
key: singleResult(keyValue),
};
}
}
case "PropertyAccess": {
const objValue = evalExpression(
lval.object,
env,
sf,
);
if (objValue instanceof Promise) {
return objValue.then((objValue) => {
return {
env: objValue,
key: lval.property,
};
});
} else {
return {
env: objValue,
key: lval.property,
};
}
}
}
}
function exactInt(
num: any,
ctx: ASTCtx,
sf: LuaStackFrame,
): number {
// See conversion from float to integer https://www.lua.org/manual/5.4/manual.html#3.4.3
let n: number;
if (typeof num === "number") {
n = num;
} else if (num instanceof Number) {
n = Number(num);
} else {
throw new LuaRuntimeError(
`attempt to perform arithmetic on a non-number`,
sf.withCtx(ctx),
);
}
if (!Number.isInteger(n)) {
throw new LuaRuntimeError(
`Number ${n} has no integer representation (consider math.floor or math.ceil)`,
sf.withCtx(ctx),
);
}
return n;
}