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