* Space Lua: Add AST block type label/goto and duplicated label bools * Space Lua: Add label/goto resolution and evaluation Add per block map of labels metadata, per function block metadata cache and `processBlock` to track AST blocks (`If`, `While`, `Repeat`, `For` and `ForIn`, and `Do`) while recording labels and their positions. Align with Lua scoping rules (_e.g._, a `goto` can not jump into the scope of a local). Also allow the super-special "safe" end of block cases (except inside `Repeat`). Duplicated labels are detected (error `label 'foo' is already defined` etc.). The AST context for error reporting is preserved. The `WeakMap` caches map any block to its function's metadata. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: The `goto` parse flags and eval path At parser mark each block with `hasLabel`/`hasGoto` and record the first duplicated label. Flags then propagate through blocks and control flow. If no `goto`, run the block in a simple loop and raise duplicate label error if any is present. If `goto` exists, jump via a label map. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Add labels and `goto` statement test suite Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Minor `goto`/labels improvement and test hotfix - The `getBlockGotoMeta` now called only when a block has either `hasGoto` or `hasLabel`. - Add `BlocksFlags` type to get rid of some `(... as any)`s by view. - Add `blockMetaOrThrow` helper. - Fix test to reflect standard Lua semantics. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add missing Lua semantics checks Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add Lua semantics checks also covered in Lua tests Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Fix semantic divergence revealed by fixing unfinished test Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix typo Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Optimize block env creation and cache goto metadata Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Avoid using the venomous snake case Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Add comment for WeakMap use Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Block Label Fast-path Optimization Add block flag `hasLabelHere` during parsing and use it in eval to skip expensive metadata generation for blocks and entire functions without label or goto. Refactor eval logic: - early fast path when a function is known to have no label/goto, - later fast path for blocks in functions with goto. Fetch metadata only when needed. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Remove `BlockFlags` indirection from eval - Add `hasLabelHere` flag to `LuaBlock`. - Remove `BlockFlags` indirection from eval. - Set `hasLabelHere` directly. - Add some "guards". - Improve consistency of child filtering. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove redundant return statement Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
2082 lines
56 KiB
TypeScript
2082 lines
56 KiB
TypeScript
import type {
|
|
ASTCtx,
|
|
LuaBlock,
|
|
LuaExpression,
|
|
LuaLValue,
|
|
LuaStatement,
|
|
NumericType,
|
|
} from "./ast.ts";
|
|
import { evalPromiseValues } from "./util.ts";
|
|
import {
|
|
getMetatable,
|
|
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";
|
|
import { isPromise, rpAll, rpThen } from "./rp.ts";
|
|
import {
|
|
asAssignment,
|
|
asBinary,
|
|
asBlock,
|
|
asFor,
|
|
asForIn,
|
|
asFunctionCall,
|
|
asFunctionCallStmt,
|
|
asFunctionDef,
|
|
asFunctionStmt,
|
|
asGoto,
|
|
asIf,
|
|
asLabel,
|
|
asLocal,
|
|
asLocalFunction,
|
|
asLValuePropertyAccess,
|
|
asLValueTableAccess,
|
|
asLValueVariable,
|
|
asParenthesized,
|
|
asPropertyAccess,
|
|
asQueryExpr,
|
|
asRepeat,
|
|
asReturn,
|
|
asTableAccess,
|
|
asTableConstructor,
|
|
asUnary,
|
|
asVariable,
|
|
asWhile,
|
|
} from "./ast_narrow.ts";
|
|
import { getBlockGotoMeta } from "./labels.ts";
|
|
|
|
type GotoSignal = { ctrl: "goto"; target: string };
|
|
function isGotoSignal(v: any): v is GotoSignal {
|
|
return !!v && typeof v === "object" && v.ctrl === "goto";
|
|
}
|
|
|
|
function consumeGotoInBlock(
|
|
res: any,
|
|
labels: Map<string, number>,
|
|
): number | any | undefined {
|
|
if (res === undefined) {
|
|
return undefined;
|
|
}
|
|
if (isGotoSignal(res)) {
|
|
const labelIdx = labels.get(res.target);
|
|
if (labelIdx !== undefined) {
|
|
return labelIdx + 1; // next statement
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function blockMetaOrThrow(
|
|
block: LuaBlock,
|
|
sf: LuaStackFrame,
|
|
): ReturnType<typeof getBlockGotoMeta> {
|
|
try {
|
|
return getBlockGotoMeta(block);
|
|
} catch (e: any) {
|
|
if (e && typeof e === "object" && "astCtx" in e) {
|
|
throw new LuaRuntimeError(e.message, sf.withCtx((e as any).astCtx));
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Queryable guard to avoid `(collection as any).query` usage
|
|
type Queryable = {
|
|
query: (
|
|
q: LuaCollectionQuery,
|
|
env: LuaEnv,
|
|
sf: LuaStackFrame,
|
|
) => Promise<any>;
|
|
};
|
|
function isQueryable(x: unknown): x is Queryable {
|
|
return !!x && typeof (x as any).query === "function";
|
|
}
|
|
|
|
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": {
|
|
const b = asBinary(e);
|
|
if (b.operator === "or") {
|
|
// Special case: eagerly evaluate left before even attempting right
|
|
return evalLogical("or", b.left, b.right, env, sf);
|
|
} else if (b.operator === "and") {
|
|
// Special case: eagerly evaluate left before even attempting right
|
|
return evalLogical("and", b.left, b.right, env, sf);
|
|
}
|
|
// Enforce left-to-right evaluation
|
|
const hints: OpHints = opHintsFromBinary(b);
|
|
return evalBinaryWithLR(
|
|
b.operator,
|
|
b.left,
|
|
b.right,
|
|
b.ctx,
|
|
env,
|
|
sf,
|
|
hints,
|
|
);
|
|
}
|
|
case "Unary": {
|
|
const u = asUnary(e);
|
|
const value = evalExpression(u.argument, env, sf);
|
|
if (isPromise(value)) {
|
|
return value.then((value) => {
|
|
switch (u.operator) {
|
|
case "-": {
|
|
const arg = singleResult(value);
|
|
return unaryWithMeta(
|
|
arg,
|
|
"__unm",
|
|
u.ctx,
|
|
sf,
|
|
() => luaUnaryMinus(arg),
|
|
);
|
|
}
|
|
case "+": {
|
|
return +singleResult(value);
|
|
}
|
|
case "not": {
|
|
return !luaTruthy(value);
|
|
}
|
|
case "~": {
|
|
const arg = singleResult(value);
|
|
return unaryWithMeta(
|
|
arg,
|
|
"__bnot",
|
|
u.ctx,
|
|
sf,
|
|
() => ~exactInt(arg, u.ctx, sf),
|
|
);
|
|
}
|
|
case "#": {
|
|
return luaLengthOp(singleResult(value), u.ctx, sf);
|
|
}
|
|
default: {
|
|
throw new LuaRuntimeError(
|
|
`Unknown unary operator ${u.operator}`,
|
|
sf.withCtx(u.ctx),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
switch (u.operator) {
|
|
case "-": {
|
|
const arg = singleResult(value);
|
|
return unaryWithMeta(
|
|
arg,
|
|
"__unm",
|
|
u.ctx,
|
|
sf,
|
|
() => luaUnaryMinus(arg),
|
|
);
|
|
}
|
|
case "+": {
|
|
return +singleResult(value);
|
|
}
|
|
case "not": {
|
|
return !luaTruthy(value);
|
|
}
|
|
case "~": {
|
|
const arg = singleResult(value);
|
|
return unaryWithMeta(
|
|
arg,
|
|
"__bnot",
|
|
u.ctx,
|
|
sf,
|
|
() => ~exactInt(arg, u.ctx, sf),
|
|
);
|
|
}
|
|
case "#": {
|
|
return luaLengthOp(singleResult(value), u.ctx, sf);
|
|
}
|
|
default: {
|
|
throw new LuaRuntimeError(
|
|
`Unknown unary operator ${u.operator}`,
|
|
sf.withCtx(u.ctx),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
case "Variable":
|
|
case "FunctionCall":
|
|
case "TableAccess":
|
|
case "PropertyAccess": {
|
|
return evalPrefixExpression(e, env, sf);
|
|
}
|
|
case "TableConstructor": {
|
|
const tc = asTableConstructor(e);
|
|
return Promise.resolve().then(async () => {
|
|
const table = new LuaTable();
|
|
|
|
for (const field of tc.fields) {
|
|
await handleTableFieldSync(table, field, env, sf);
|
|
}
|
|
|
|
return table;
|
|
});
|
|
}
|
|
case "FunctionDefinition": {
|
|
const fd = asFunctionDef(e);
|
|
return new LuaFunction(fd.body, env);
|
|
}
|
|
case "Query": {
|
|
const q = asQueryExpr(e);
|
|
const findFromClause = q.clauses.find((c) => c.type === "From");
|
|
if (!findFromClause) {
|
|
throw new LuaRuntimeError(
|
|
"No from clause found",
|
|
sf.withCtx(q.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(q.ctx),
|
|
);
|
|
}
|
|
if (collection instanceof LuaTable && collection.empty()) {
|
|
// Make sure we're converting an empty result to an array to "query"
|
|
collection = [];
|
|
} else {
|
|
collection = luaValueToJS(collection, sf);
|
|
}
|
|
// Check if collection is a queryable collection
|
|
if (!isQueryable(collection)) {
|
|
if (!Array.isArray(collection)) {
|
|
throw new LuaRuntimeError(
|
|
"Collection does not support query",
|
|
sf.withCtx(q.ctx),
|
|
);
|
|
}
|
|
collection = new ArrayQueryCollection(collection);
|
|
}
|
|
// Build up query object
|
|
const query: LuaCollectionQuery = {
|
|
objectVariable,
|
|
distinct: true,
|
|
};
|
|
|
|
// Map clauses to query parameters
|
|
for (const clause of q.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 as Queryable).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 v = asVariable(e);
|
|
const value = env.get(v.name);
|
|
if (value === undefined) {
|
|
return null;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
case "Parenthesized": {
|
|
const p = asParenthesized(e);
|
|
return evalExpression(p.expression, env, sf);
|
|
}
|
|
// <<expr>>[<<expr>>]
|
|
case "TableAccess": {
|
|
const ta = asTableAccess(e);
|
|
// Sync-first: evaluate object and key without allocating Promise when both are sync.
|
|
const objV = evalPrefixExpression(ta.object, env, sf);
|
|
const keyV = evalExpression(ta.key, env, sf);
|
|
|
|
if (!isPromise(objV) && !isPromise(keyV)) {
|
|
const table = singleResult(objV);
|
|
const key = singleResult(keyV);
|
|
return luaGet(table, key, ta.ctx, sf);
|
|
}
|
|
|
|
return rpThen(
|
|
objV,
|
|
(obj) =>
|
|
rpThen(
|
|
keyV,
|
|
(key) => luaGet(singleResult(obj), singleResult(key), ta.ctx, sf),
|
|
),
|
|
);
|
|
}
|
|
// <expr>.property
|
|
case "PropertyAccess": {
|
|
const pa = asPropertyAccess(e);
|
|
// Sync-first: evaluate object; avoid Promise when object is sync.
|
|
const objV = evalPrefixExpression(pa.object, env, sf);
|
|
if (!isPromise(objV)) {
|
|
return luaGet(objV, pa.property, pa.ctx, sf);
|
|
}
|
|
return rpThen(objV, (obj) => luaGet(obj, pa.property, pa.ctx, sf));
|
|
}
|
|
case "FunctionCall": {
|
|
const fc = asFunctionCall(e);
|
|
const prefixValue = evalPrefixExpression(fc.prefix, env, sf);
|
|
if (prefixValue === null || prefixValue === undefined) {
|
|
const nilMsg = fc.prefix.type === "Variable"
|
|
? `attempt to call a nil value (global '${
|
|
asVariable(fc.prefix).name
|
|
}')`
|
|
: `attempt to call a nil value`;
|
|
throw new LuaRuntimeError(
|
|
nilMsg,
|
|
sf.withCtx(fc.prefix.ctx),
|
|
);
|
|
}
|
|
|
|
let selfArgs: LuaValue[] = [];
|
|
|
|
const handleFunctionCall = (
|
|
calleeVal: LuaValue,
|
|
): LuaValue | Promise<LuaValue> => {
|
|
// Normal argument handling for hello:there(a, b, c) type calls
|
|
if (fc.name) {
|
|
selfArgs = [calleeVal];
|
|
calleeVal = luaIndexValue(calleeVal, fc.name, sf);
|
|
|
|
if (isPromise(calleeVal)) {
|
|
return (calleeVal as Promise<any>).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 parts = fc.args.map((arg: LuaExpression) =>
|
|
evalExpression(arg, env, sf)
|
|
);
|
|
const argsVal = rpAll(parts);
|
|
|
|
const thenCall = (args: LuaValue[]) =>
|
|
luaCall(calleeVal, [...selfArgs, ...args], fc.ctx, sf);
|
|
|
|
if (isPromise(argsVal)) {
|
|
return (argsVal as Promise<any[]>).then((argsResolved: any[]) => {
|
|
if (argsResolved.length === 0) return thenCall([]);
|
|
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 thenCall(out);
|
|
});
|
|
} else {
|
|
const argsResolved = argsVal as LuaValue[];
|
|
if (argsResolved.length === 0) return thenCall([]);
|
|
const out: LuaValue[] = [];
|
|
for (let i = 0; i < argsResolved.length - 1; i++) {
|
|
out.push(singleResult(argsResolved[i]));
|
|
}
|
|
const last = argsResolved[argsResolved.length - 1];
|
|
if (last instanceof LuaMultiRes) {
|
|
out.push(...last.flatten().values);
|
|
} else {
|
|
out.push(singleResult(last));
|
|
}
|
|
return thenCall(out);
|
|
}
|
|
};
|
|
if (isPromise(prefixValue)) {
|
|
return (prefixValue as Promise<any>).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 isPromise(mm)
|
|
? (mm as Promise<any>).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 isPromise(rv)
|
|
? (rv as Promise<any>).then(singleResult)
|
|
: singleResult(rv);
|
|
} else {
|
|
if (!luaTruthy(lv)) {
|
|
return singleResult(lv);
|
|
}
|
|
const rv = evalExpression(rightExpr, env, sf);
|
|
return isPromise(rv)
|
|
? (rv as Promise<any>).then(singleResult)
|
|
: singleResult(rv);
|
|
}
|
|
};
|
|
|
|
if (isPromise(left)) {
|
|
return (left as Promise<any>).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);
|
|
|
|
if (!isPromise(leftVal)) {
|
|
const rightVal = evalExpression(rightExpr, env, sf);
|
|
if (!isPromise(rightVal)) {
|
|
return luaOp(
|
|
op,
|
|
singleResult(leftVal),
|
|
singleResult(rightVal),
|
|
ctx,
|
|
sf,
|
|
hints,
|
|
);
|
|
}
|
|
return rpThen(rightVal, (rv) =>
|
|
luaOp(
|
|
op,
|
|
singleResult(leftVal),
|
|
singleResult(rv),
|
|
ctx,
|
|
sf,
|
|
hints,
|
|
));
|
|
}
|
|
|
|
return rpThen(leftVal, (lv) => {
|
|
const rightVal = evalExpression(rightExpr, env, sf);
|
|
if (!isPromise(rightVal)) {
|
|
return luaOp(
|
|
op,
|
|
singleResult(lv),
|
|
singleResult(rightVal),
|
|
ctx,
|
|
sf,
|
|
hints,
|
|
);
|
|
}
|
|
return rpThen(rightVal, (rv) =>
|
|
luaOp(
|
|
op,
|
|
singleResult(lv),
|
|
singleResult(rv),
|
|
ctx,
|
|
sf,
|
|
hints,
|
|
));
|
|
});
|
|
}
|
|
|
|
// 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 };
|
|
}
|
|
|
|
// 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[] {
|
|
// Evaluate all arguments first (sync-first); do not allocate a Promise if all are sync.
|
|
const parts = es.map((arg) => evalExpression(arg, env, sf));
|
|
const argsVal = rpAll(parts);
|
|
|
|
// 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;
|
|
};
|
|
|
|
return isPromise(argsVal)
|
|
? (argsVal as Promise<any[]>).then(finalize)
|
|
: finalize(argsVal as LuaValue[]);
|
|
}
|
|
|
|
/**
|
|
* Evaluates a statement 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.
|
|
*
|
|
* May also return `{ctrl:"goto", target}` for goto.
|
|
*/
|
|
export function evalStatement(
|
|
s: LuaStatement,
|
|
env: LuaEnv,
|
|
sf: LuaStackFrame,
|
|
returnOnReturn = false,
|
|
): void | LuaValue[] | GotoSignal | Promise<void | LuaValue[] | GotoSignal> {
|
|
switch (s.type) {
|
|
case "Assignment": {
|
|
const a = asAssignment(s);
|
|
const valuesRP = evalExpressions(a.expressions, env, sf);
|
|
const lvaluesRP = evalPromiseValues(a.variables
|
|
.map((lval) => evalLValue(lval, env, sf)));
|
|
|
|
const apply = (values: LuaValue[], lvalues: { env: any; key: any }[]) => {
|
|
const ps: Promise<any>[] = [];
|
|
for (let i = 0; i < lvalues.length; i++) {
|
|
const r = luaSet(
|
|
lvalues[i].env,
|
|
lvalues[i].key,
|
|
values[i],
|
|
sf.withCtx(a.ctx),
|
|
);
|
|
if (isPromise(r)) {
|
|
ps.push(r);
|
|
}
|
|
}
|
|
if (ps.length) {
|
|
return Promise.all(ps).then(() => undefined);
|
|
}
|
|
return;
|
|
};
|
|
|
|
if (!isPromise(valuesRP) && !isPromise(lvaluesRP)) {
|
|
return apply(
|
|
valuesRP as LuaValue[],
|
|
lvaluesRP as LuaLValueContainer[],
|
|
);
|
|
} else if (
|
|
isPromise(valuesRP) && !isPromise(lvaluesRP)
|
|
) {
|
|
return (valuesRP as Promise<LuaValue[]>).then((values: LuaValue[]) =>
|
|
apply(values, lvaluesRP as LuaLValueContainer[])
|
|
);
|
|
} else if (
|
|
!isPromise(valuesRP) && isPromise(lvaluesRP)
|
|
) {
|
|
return (lvaluesRP as Promise<any[]>).then((lvalues: any[]) =>
|
|
apply(valuesRP as LuaValue[], lvalues)
|
|
);
|
|
} else {
|
|
return (valuesRP as Promise<LuaValue[]>).then((values: LuaValue[]) =>
|
|
(lvaluesRP as Promise<any[]>).then((lvalues: any[]) =>
|
|
apply(values, lvalues)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
case "Local": {
|
|
const l = asLocal(s);
|
|
if (l.expressions) {
|
|
const valuesRP = evalExpressions(l.expressions, env, sf);
|
|
const setAll = (values: LuaValue[]) => {
|
|
for (let i = 0; i < l.names.length; i++) {
|
|
env.setLocal(l.names[i].name, values[i]);
|
|
}
|
|
return;
|
|
};
|
|
if (isPromise(valuesRP)) {
|
|
return (valuesRP as Promise<LuaValue[]>).then(setAll);
|
|
} else {
|
|
return setAll(valuesRP);
|
|
}
|
|
} else {
|
|
for (let i = 0; i < l.names.length; i++) {
|
|
env.setLocal(l.names[i].name, null);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
case "Semicolon": {
|
|
return;
|
|
}
|
|
case "Label": {
|
|
const _lab = asLabel(s); // No-op!
|
|
return;
|
|
}
|
|
case "Goto": {
|
|
const g = asGoto(s);
|
|
return { ctrl: "goto", target: g.name };
|
|
}
|
|
case "Block": {
|
|
const b = asBlock(s);
|
|
const hasGotoFlag = b.hasGoto === true;
|
|
const hasLabelFlag = b.hasLabel === true;
|
|
const hasLabelHere = b.hasLabelHere === true;
|
|
|
|
const curFn = (sf as any).currentFunction as LuaFunction | undefined;
|
|
const fnHasGotos = curFn?.funcHasGotos;
|
|
|
|
// Fast path: function known to have no gotos, run without meta
|
|
if (fnHasGotos === false || (!hasGotoFlag && !hasLabelFlag)) {
|
|
const dup = b.dupLabelError;
|
|
if (dup) {
|
|
// Duplicated labels detected by parser.
|
|
throw new LuaRuntimeError(
|
|
`label '${dup.name}' already defined`,
|
|
sf.withCtx(dup.ctx),
|
|
);
|
|
}
|
|
|
|
// Sync-first execution: iterate statements in a simple loop; if
|
|
// a statement returns a Promise, immediately switch to async by
|
|
// returning a continuation that resumes execution from the next
|
|
// statement (`i + 1`).
|
|
const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
|
|
const stmts = b.statements;
|
|
|
|
const processFrom = (
|
|
idx: number,
|
|
): void | LuaValue[] | Promise<void | LuaValue[]> => {
|
|
for (let i = idx; i < stmts.length; i++) {
|
|
const result = evalStatement(
|
|
stmts[i],
|
|
execEnv,
|
|
sf,
|
|
returnOnReturn,
|
|
);
|
|
if (isPromise(result)) {
|
|
return (result as Promise<any>).then((res) => {
|
|
if (res !== undefined && !isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (isGotoSignal(res)) {
|
|
// Should not happen in fast path
|
|
throw new LuaRuntimeError(
|
|
"unexpected goto signal",
|
|
sf.withCtx(stmts[i].ctx),
|
|
);
|
|
}
|
|
return processFrom(i + 1);
|
|
});
|
|
}
|
|
// Will only happen with `return` statement
|
|
if (result !== undefined) {
|
|
if (isGotoSignal(result)) {
|
|
throw new LuaRuntimeError(
|
|
"unexpected goto signal",
|
|
sf.withCtx(stmts[i].ctx),
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
return processFrom(0);
|
|
}
|
|
|
|
// If function has gotos, but this block itself has no labels,
|
|
// avoid computing metadata for this block.
|
|
if (fnHasGotos === true && !hasLabelHere && !hasGotoFlag) {
|
|
const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
|
|
const stmts = b.statements;
|
|
const runFrom = (
|
|
i: number,
|
|
):
|
|
| void
|
|
| LuaValue[]
|
|
| GotoSignal
|
|
| Promise<void | LuaValue[] | GotoSignal> => {
|
|
for (; i < stmts.length; i++) {
|
|
const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) return res;
|
|
if (res !== undefined) return res;
|
|
return runFrom(i + 1);
|
|
});
|
|
} else {
|
|
if (isGotoSignal(r)) return r;
|
|
if (r !== undefined) return r;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
return runFrom(0);
|
|
}
|
|
|
|
// Need metadata (function or block has label/goto)
|
|
let meta: ReturnType<typeof getBlockGotoMeta> | undefined;
|
|
if (fnHasGotos === undefined && (hasGotoFlag || hasLabelFlag)) {
|
|
meta = blockMetaOrThrow(b, sf);
|
|
if (curFn) {
|
|
curFn.funcHasGotos = !!meta?.funcHasGotos;
|
|
}
|
|
} else if (fnHasGotos === true) {
|
|
// Only fetch metadata for blocks that actually have label/goto
|
|
meta = hasLabelFlag || hasGotoFlag
|
|
? blockMetaOrThrow(b, sf)
|
|
: undefined;
|
|
} else {
|
|
meta = undefined;
|
|
}
|
|
|
|
if (!meta || !meta.funcHasGotos) {
|
|
const dup = b.dupLabelError;
|
|
if (dup) {
|
|
throw new LuaRuntimeError(
|
|
`label '${dup.name}' already defined`,
|
|
sf.withCtx(dup.ctx),
|
|
);
|
|
}
|
|
const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
|
|
const stmts = b.statements;
|
|
|
|
const processFrom = (
|
|
idx: number,
|
|
): void | LuaValue[] | Promise<void | LuaValue[]> => {
|
|
for (let i = idx; i < stmts.length; i++) {
|
|
const result = evalStatement(
|
|
stmts[i],
|
|
execEnv,
|
|
sf,
|
|
returnOnReturn,
|
|
);
|
|
if (isPromise(result)) {
|
|
return (result as Promise<any>).then((res) => {
|
|
if (res !== undefined && !isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (isGotoSignal(res)) {
|
|
throw new LuaRuntimeError(
|
|
"unexpected goto signal",
|
|
sf.withCtx(stmts[i].ctx),
|
|
);
|
|
}
|
|
return processFrom(i + 1);
|
|
});
|
|
}
|
|
if (result !== undefined) {
|
|
if (isGotoSignal(result)) {
|
|
throw new LuaRuntimeError(
|
|
"unexpected goto signal",
|
|
sf.withCtx(stmts[i].ctx),
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
return processFrom(0);
|
|
} else {
|
|
const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
|
|
const stmts = b.statements;
|
|
|
|
const runFrom = (
|
|
i: number,
|
|
):
|
|
| void
|
|
| LuaValue[]
|
|
| GotoSignal
|
|
| Promise<void | LuaValue[] | GotoSignal> => {
|
|
for (; i < stmts.length; i++) {
|
|
const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
const consumed = consumeGotoInBlock(res, meta!.labels);
|
|
if (typeof consumed === "number") {
|
|
return runFrom(consumed);
|
|
}
|
|
if (consumed !== undefined) {
|
|
return consumed;
|
|
}
|
|
return runFrom(i + 1);
|
|
});
|
|
}
|
|
const consumed = consumeGotoInBlock(r, meta.labels);
|
|
if (typeof consumed === "number") {
|
|
// consumed is the next statement index; adjust for for-loop increment
|
|
i = consumed - 1;
|
|
continue;
|
|
}
|
|
if (consumed !== undefined) {
|
|
return consumed;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
return runFrom(0);
|
|
}
|
|
}
|
|
case "If": {
|
|
const iff = asIf(s);
|
|
// Evaluate conditions in order; avoid awaiting when not necessary
|
|
const conds = iff.conditions;
|
|
|
|
const runFrom = (
|
|
i: number,
|
|
):
|
|
| void
|
|
| LuaValue[]
|
|
| GotoSignal
|
|
| Promise<void | LuaValue[] | GotoSignal> => {
|
|
if (i >= conds.length) {
|
|
if (iff.elseBlock) {
|
|
return evalStatement(iff.elseBlock, env, sf, returnOnReturn);
|
|
}
|
|
return;
|
|
}
|
|
const cv = evalExpression(conds[i].condition, env, sf);
|
|
if (isPromise(cv)) {
|
|
return (cv as Promise<any>).then((val) => {
|
|
if (luaTruthy(val)) {
|
|
return evalStatement(conds[i].block, env, sf, returnOnReturn);
|
|
}
|
|
return runFrom(i + 1);
|
|
});
|
|
} else {
|
|
if (luaTruthy(cv)) {
|
|
return evalStatement(conds[i].block, env, sf, returnOnReturn);
|
|
}
|
|
return runFrom(i + 1);
|
|
}
|
|
};
|
|
|
|
return runFrom(0);
|
|
}
|
|
case "While": {
|
|
const w = asWhile(s);
|
|
const runAsync = async (): Promise<void | LuaValue[] | GotoSignal> => {
|
|
while (true) {
|
|
const c = await evalExpression(w.condition, env, sf);
|
|
if (!luaTruthy(c)) {
|
|
break;
|
|
}
|
|
try {
|
|
const r = evalStatement(w.block, env, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
const res = await r;
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
} else if (isGotoSignal(r)) {
|
|
return r;
|
|
} else if (r !== undefined) {
|
|
return r;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
while (true) {
|
|
const c = evalExpression(w.condition, env, sf);
|
|
if (isPromise(c)) {
|
|
return (c as Promise<any>).then((cv) => {
|
|
if (!luaTruthy(cv)) {
|
|
return;
|
|
}
|
|
try {
|
|
const r = evalStatement(w.block, env, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync();
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else {
|
|
if (isGotoSignal(r)) {
|
|
return r;
|
|
}
|
|
if (r !== undefined) {
|
|
return r;
|
|
}
|
|
return runAsync();
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
if (!luaTruthy(c)) {
|
|
break;
|
|
}
|
|
try {
|
|
const r = evalStatement(w.block, env, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync();
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else {
|
|
if (isGotoSignal(r)) {
|
|
return r;
|
|
}
|
|
if (r !== undefined) {
|
|
return r;
|
|
}
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case "Repeat": {
|
|
const r = asRepeat(s);
|
|
const runAsync = async (): Promise<void | LuaValue[] | GotoSignal> => {
|
|
while (true) {
|
|
try {
|
|
const rr = evalStatement(r.block, env, sf, returnOnReturn);
|
|
if (isPromise(rr)) {
|
|
const res = await rr;
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
} else {
|
|
if (isGotoSignal(rr)) {
|
|
return rr;
|
|
}
|
|
if (rr !== undefined) {
|
|
return rr;
|
|
}
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
const c = await evalExpression(r.condition, env, sf);
|
|
if (luaTruthy(c)) {
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
while (true) {
|
|
try {
|
|
const rr = evalStatement(r.block, env, sf, returnOnReturn);
|
|
if (isPromise(rr)) {
|
|
return (rr as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync();
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else {
|
|
if (isGotoSignal(rr)) {
|
|
return rr;
|
|
}
|
|
if (rr !== undefined) return rr;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
const c = evalExpression(r.condition, env, sf);
|
|
if (isPromise(c)) {
|
|
return (c as Promise<any>).then((
|
|
cv,
|
|
) => (luaTruthy(cv) ? undefined : runAsync()));
|
|
} else {
|
|
if (luaTruthy(c)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case "Break": {
|
|
throw new LuaBreak();
|
|
}
|
|
case "FunctionCallStatement": {
|
|
const fcs = asFunctionCallStmt(s);
|
|
const r = evalExpression(fcs.call, env, sf);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then(() => undefined);
|
|
}
|
|
return;
|
|
}
|
|
case "Function": {
|
|
const fn = asFunctionStmt(s);
|
|
let body = fn.body;
|
|
let propNames = fn.name.propNames;
|
|
if (fn.name.colonName) {
|
|
// function hello:there() -> function hello.there(self) transformation
|
|
body = {
|
|
...(fn.body),
|
|
parameters: ["self", ...fn.body.parameters],
|
|
};
|
|
propNames = [...fn.name.propNames, fn.name.colonName];
|
|
}
|
|
let settable: ILuaSettable & ILuaGettable = env;
|
|
for (let i = 0; i < propNames.length - 1; i++) {
|
|
settable = (settable as any).get(propNames[i]);
|
|
if (!settable) {
|
|
throw new LuaRuntimeError(
|
|
`Cannot find property ${propNames[i]}`,
|
|
sf.withCtx(fn.name.ctx),
|
|
);
|
|
}
|
|
}
|
|
(settable as any).set(
|
|
propNames[propNames.length - 1],
|
|
new LuaFunction(body, env),
|
|
);
|
|
return;
|
|
}
|
|
case "LocalFunction": {
|
|
const lf = asLocalFunction(s);
|
|
env.setLocal(
|
|
lf.name,
|
|
new LuaFunction(lf.body, env),
|
|
);
|
|
return;
|
|
}
|
|
case "Return": {
|
|
const ret = asReturn(s);
|
|
// Sync-first collection of return expressions, no extra Promise
|
|
// if all are sync.
|
|
const parts = ret.expressions.map((value: LuaExpression) =>
|
|
evalExpression(value, env, sf)
|
|
);
|
|
const valuesRP = rpAll(parts);
|
|
if (returnOnReturn) {
|
|
return isPromise(valuesRP) ? valuesRP : valuesRP;
|
|
} else {
|
|
if (isPromise(valuesRP)) {
|
|
return (valuesRP as Promise<any[]>).then((vals) => {
|
|
throw new LuaReturn(vals);
|
|
});
|
|
} else {
|
|
throw new LuaReturn(valuesRP as LuaValue[]);
|
|
}
|
|
}
|
|
}
|
|
case "For": {
|
|
const fr = asFor(s);
|
|
// Evaluate bounds and step once (sync-first)
|
|
const startV = evalExpression(fr.start, env, sf);
|
|
const endV = evalExpression(fr.end, env, sf);
|
|
const stepV = fr.step ? evalExpression(fr.step, env, sf) : 1;
|
|
|
|
const runAsync = async (start: any, end: any, step: any) => {
|
|
for (
|
|
let i = start;
|
|
step > 0 ? i <= end : i >= end;
|
|
i += step
|
|
) {
|
|
const localEnv = new LuaEnv(env);
|
|
localEnv.setLocal(fr.name, i);
|
|
try {
|
|
const r = evalStatement(fr.block, localEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
const res = await r;
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
} else if (isGotoSignal(r)) {
|
|
return r;
|
|
} else if (r !== undefined) {
|
|
return r;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
const runSyncFirst = (
|
|
start: any,
|
|
end: any,
|
|
step: any,
|
|
):
|
|
| void
|
|
| LuaValue[]
|
|
| GotoSignal
|
|
| Promise<void | LuaValue[] | GotoSignal> => {
|
|
for (
|
|
let i = start;
|
|
step > 0 ? i <= end : i >= end;
|
|
i += step
|
|
) {
|
|
const localEnv = new LuaEnv(env);
|
|
localEnv.setLocal(fr.name, i);
|
|
try {
|
|
const r = evalStatement(fr.block, localEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync(i + step, end, step);
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else if (isGotoSignal(r)) {
|
|
return r;
|
|
} else if (r !== undefined) {
|
|
return r;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
if (
|
|
!isPromise(startV) &&
|
|
!isPromise(endV) &&
|
|
!isPromise(stepV)
|
|
) {
|
|
return runSyncFirst(startV, endV, (stepV as number) ?? 1);
|
|
} else {
|
|
return Promise.all([
|
|
isPromise(startV) ? startV : Promise.resolve(startV),
|
|
isPromise(endV) ? endV : Promise.resolve(endV),
|
|
isPromise(stepV) ? stepV : Promise.resolve(stepV),
|
|
]).then(([start, end, step]) => runSyncFirst(start, end, step ?? 1));
|
|
}
|
|
}
|
|
case "ForIn": {
|
|
const fi = asForIn(s);
|
|
const exprVals = rpAll(
|
|
fi.expressions.map((e: LuaExpression) => evalExpression(e, env, sf)),
|
|
);
|
|
|
|
const afterExprs = (resolved: any[]) => {
|
|
const iteratorMultiRes = new LuaMultiRes(resolved).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") as ILuaFunction).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(fi.ctx),
|
|
);
|
|
}
|
|
|
|
const state: LuaValue = iteratorMultiRes.values[1] || null;
|
|
const control: LuaValue = iteratorMultiRes.values[2] || null;
|
|
|
|
const runAsync = async () => {
|
|
while (true) {
|
|
const callRes = luaCall(
|
|
iteratorValue,
|
|
[state, control],
|
|
fi.ctx,
|
|
sf,
|
|
);
|
|
const iterResult = new LuaMultiRes(
|
|
isPromise(callRes) ? await callRes : callRes,
|
|
).flatten();
|
|
if (
|
|
iterResult.values[0] === null ||
|
|
iterResult.values[0] === undefined
|
|
) {
|
|
break;
|
|
}
|
|
const localEnv = new LuaEnv(env);
|
|
for (let i = 0; i < fi.names.length; i++) {
|
|
localEnv.setLocal(fi.names[i], iterResult.values[i]);
|
|
}
|
|
try {
|
|
const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
const res = await r;
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
} else if (isGotoSignal(r)) {
|
|
return r;
|
|
} else if (r !== undefined) {
|
|
return r;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
while (true) {
|
|
const iterCall = luaCall(iteratorValue, [state, control], fi.ctx, sf);
|
|
if (isPromise(iterCall)) {
|
|
return (iterCall as Promise<any>).then((itv) => {
|
|
const iterResult = new LuaMultiRes(itv).flatten();
|
|
if (
|
|
iterResult.values[0] === null ||
|
|
iterResult.values[0] === undefined
|
|
) {
|
|
return;
|
|
}
|
|
const localEnv = new LuaEnv(env);
|
|
for (let i = 0; i < fi.names.length; i++) {
|
|
localEnv.setLocal(fi.names[i], iterResult.values[i]);
|
|
}
|
|
const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync();
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else {
|
|
if (isGotoSignal(r)) {
|
|
return r;
|
|
}
|
|
if (r !== undefined) {
|
|
return r;
|
|
} else {
|
|
return runAsync();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const iterResult = new LuaMultiRes(iterCall).flatten();
|
|
if (
|
|
iterResult.values[0] === null || iterResult.values[0] === undefined
|
|
) {
|
|
break;
|
|
}
|
|
const localEnv = new LuaEnv(env);
|
|
for (let i = 0; i < fi.names.length; i++) {
|
|
localEnv.setLocal(fi.names[i], iterResult.values[i]);
|
|
}
|
|
try {
|
|
const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
|
|
if (isPromise(r)) {
|
|
return (r as Promise<any>).then((res) => {
|
|
if (isGotoSignal(res)) {
|
|
return res;
|
|
}
|
|
if (res !== undefined) {
|
|
return res;
|
|
}
|
|
return runAsync();
|
|
}).catch((e: any) => {
|
|
if (e instanceof LuaBreak) {
|
|
return;
|
|
}
|
|
throw e;
|
|
});
|
|
} else if (isGotoSignal(r)) {
|
|
return r;
|
|
} else if (r !== undefined) {
|
|
return r;
|
|
}
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
if (isPromise(exprVals)) {
|
|
return (exprVals as Promise<any[]>).then(afterExprs);
|
|
} else {
|
|
return afterExprs(exprVals as any[]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function evalLValue(
|
|
lval: LuaLValue,
|
|
env: LuaEnv,
|
|
sf: LuaStackFrame,
|
|
): LuaLValueContainer | Promise<LuaLValueContainer> {
|
|
switch (lval.type) {
|
|
case "Variable": {
|
|
const v = asLValueVariable(lval);
|
|
return {
|
|
env,
|
|
key: v.name,
|
|
};
|
|
}
|
|
case "TableAccess": {
|
|
const ta = asLValueTableAccess(lval);
|
|
const objValue = evalExpression(
|
|
ta.object,
|
|
env,
|
|
sf,
|
|
);
|
|
const keyValue = evalExpression(ta.key, env, sf);
|
|
if (
|
|
isPromise(objValue) ||
|
|
isPromise(keyValue)
|
|
) {
|
|
return Promise.all([
|
|
isPromise(objValue) ? objValue : Promise.resolve(objValue),
|
|
isPromise(keyValue) ? keyValue : Promise.resolve(keyValue),
|
|
]).then(([objValue, keyValue]) => ({
|
|
env: singleResult(objValue),
|
|
key: singleResult(keyValue),
|
|
}));
|
|
} else {
|
|
return {
|
|
env: singleResult(objValue),
|
|
key: singleResult(keyValue),
|
|
};
|
|
}
|
|
}
|
|
case "PropertyAccess": {
|
|
const pa = asLValuePropertyAccess(lval);
|
|
const objValue = evalExpression(
|
|
pa.object,
|
|
env,
|
|
sf,
|
|
);
|
|
if (isPromise(objValue)) {
|
|
return (objValue as Promise<any>).then((ov) => {
|
|
return {
|
|
env: ov,
|
|
key: pa.property,
|
|
};
|
|
});
|
|
} else {
|
|
return {
|
|
env: objValue,
|
|
key: pa.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;
|
|
}
|