Files
plainleaf/client/space_lua/eval.ts
T
Matouš Jan FialkaandGitHub cd6f4e9c58 Space Lua: Align .. (concatenation) operator (#1648)
* Make fallback accept only strings and numbers (as per standard Lua).

* Remove `luaToString` from the fallback. It fits well presentation
  purposes but is too complicated for strict semantic checking in the
  evaluator and also wrongly coersed non-string and non-number value.
  It may also return a promise but we require synchronous error path.

* Concatenating `nil` now throws "attempt to concatenate nil value" as
  per standard Lua.

* Other types throw "attempt to concatenate a non-string or non-number"
  which is simplified message diverting from standard Lua which throws
  typed error but we do not strictly need that for Space Lua (and can be
  easily added anytime in the future).

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

1320 lines
33 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,
type LuaLValueContainer,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
luaSet,
type LuaStackFrame,
LuaTable,
luaTruthy,
type LuaType,
luaTypeOf,
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 !luaTruthy(singleResult(value));
}
case "~": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__bnot",
e.ctx,
sf,
() => ~exactInt(arg, e.ctx, sf),
);
}
case "#": {
return luaLengthOp(singleResult(value), e.ctx, sf);
}
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 !luaTruthy(singleResult(value));
}
case "~": {
const arg = singleResult(value);
return unaryWithMeta(
arg,
"__bnot",
e.ctx,
sf,
() => ~exactInt(arg, e.ctx, sf),
);
}
case "#": {
return luaLengthOp(singleResult(value), e.ctx, sf);
}
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 === null || prefixValue === undefined) {
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 singleResult(lv);
}
const rv = evalExpression(rightExpr, env, sf);
return rv instanceof Promise ? rv.then(singleResult) : singleResult(rv);
} else {
if (!luaTruthy(lv)) {
return singleResult(lv);
}
const rv = evalExpression(rightExpr, env, sf);
return rv instanceof Promise ? rv.then(singleResult) : singleResult(rv);
}
};
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, ctx, sf) => {
// Accepts only strings or numbers (coerced to strings)
const coerce = (v: any): string => {
if (v === null || v === undefined) {
throw new LuaRuntimeError(
"attempt to concatenate a nil value",
sf.withCtx(ctx),
);
}
if (typeof v === "string") {
return v as string;
}
if (typeof v === "number" || v instanceof Number) {
return String(v instanceof Number ? Number(v) : v);
}
throw new LuaRuntimeError(
"attempt to concatenate a non-string or non-number",
sf.withCtx(ctx),
);
};
return coerce(a) + coerce(b);
},
},
"==": {
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);
}
/**
* Length operator:
* - for strings return byte length, ignore `__len`,
* - for Lua tables if metatable has `__len` metamethod then call it;
* use table length otherwise,
* - for other values (userdata): honor `__len` if present,
* - for JavaScript arrays return length,
* - throw error otherwise.
*/
function luaLengthOp(
val: any,
ctx: ASTCtx,
sf: LuaStackFrame,
): LuaValue {
// Strings: ignore `__len`
if (typeof val === "string") {
return val.length;
}
// Tables: prefer metatable `__len` to raw length
if (val instanceof LuaTable) {
const mt = getMetatable(val, sf);
if (mt && mt.has("__len")) {
const fn = mt.get("__len");
return luaCall(fn, [val], ctx, sf);
}
return val.length;
}
// Other values: allow metatable `__len` first
{
const mt = getMetatable(val, sf);
if (mt && mt.has("__len")) {
const fn = mt.get("__len");
return luaCall(fn, [val], ctx, sf);
}
}
// JS arrays (interop): length if no `__len` override
if (Array.isArray(val)) {
return val.length;
}
// Otherwise error with type
const t = luaTypeOf(val) as LuaType;
throw new LuaRuntimeError(
`attempt to get length of a ${t} value`,
sf.withCtx(ctx),
);
}
function evalExpressions(
es: LuaExpression[],
env: LuaEnv,
sf: LuaStackFrame,
): Promise<LuaValue[]> | LuaValue[] {
const argsVal = evalPromiseValues(
es.map((arg) => evalExpression(arg, env, sf)),
);
// In Lua multi-returns propagate only in tail position of an expression
// list.
const finalize = (argsResolved: any[]) => {
if (argsResolved.length === 0) {
return [];
}
const out: LuaValue[] = [];
// All but last expression produce a single value
for (let i = 0; i < argsResolved.length - 1; i++) {
out.push(singleResult(argsResolved[i]));
}
// Last expression preserves multiple results
const last = argsResolved[argsResolved.length - 1];
if (last instanceof LuaMultiRes) {
out.push(...last.flatten().values);
} else {
out.push(singleResult(last));
}
return out;
};
if (argsVal instanceof Promise) {
return argsVal.then(finalize);
} else {
return finalize(argsVal as any[]);
}
}
/**
* 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;
}