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