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 { 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 { 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 { 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); } // <>[<>] 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)); } } // .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 => { // 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 | 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, ): 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 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[] { 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 { 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 { 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; }