467 lines
15 KiB
TypeScript
467 lines
15 KiB
TypeScript
import type {
|
|
LuaExpression,
|
|
LuaLValue,
|
|
LuaStatement,
|
|
} from "$common/space_lua/ast.ts";
|
|
import { evalPromiseValues } from "$common/space_lua/util.ts";
|
|
import {
|
|
type ILuaFunction,
|
|
type ILuaGettable,
|
|
type ILuaSettable,
|
|
LuaBreak,
|
|
LuaEnv,
|
|
LuaFunction,
|
|
luaGet,
|
|
luaLen,
|
|
type LuaLValueContainer,
|
|
LuaReturn,
|
|
LuaTable,
|
|
luaTruthy,
|
|
type LuaValue,
|
|
singleResult,
|
|
} from "./runtime.ts";
|
|
|
|
export function evalExpression(
|
|
e: LuaExpression,
|
|
env: LuaEnv,
|
|
): Promise<LuaValue> | LuaValue {
|
|
switch (e.type) {
|
|
case "String":
|
|
// TODO: Deal with escape sequences
|
|
return e.value;
|
|
case "Number":
|
|
return e.value;
|
|
case "Boolean":
|
|
return e.value;
|
|
case "Nil":
|
|
return null;
|
|
case "Binary": {
|
|
const values = evalPromiseValues([
|
|
evalExpression(e.left, env),
|
|
evalExpression(e.right, env),
|
|
]);
|
|
if (values instanceof Promise) {
|
|
return values.then(([left, right]) =>
|
|
luaOp(e.operator, singleResult(left), singleResult(right))
|
|
);
|
|
} else {
|
|
return luaOp(
|
|
e.operator,
|
|
singleResult(values[0]),
|
|
singleResult(values[1]),
|
|
);
|
|
}
|
|
}
|
|
case "Unary": {
|
|
const value = evalExpression(e.argument, env);
|
|
if (value instanceof Promise) {
|
|
return value.then((value) => {
|
|
switch (e.operator) {
|
|
case "-":
|
|
return -singleResult(value);
|
|
case "+":
|
|
return +singleResult(value);
|
|
case "not":
|
|
return !singleResult(value);
|
|
case "#":
|
|
return luaLen(singleResult(value));
|
|
default:
|
|
throw new Error(
|
|
`Unknown unary operator ${e.operator}`,
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
switch (e.operator) {
|
|
case "-":
|
|
return -singleResult(value);
|
|
case "+":
|
|
return +singleResult(value);
|
|
case "not":
|
|
return !singleResult(value);
|
|
case "#":
|
|
return luaLen(singleResult(value));
|
|
default:
|
|
throw new Error(
|
|
`Unknown unary operator ${e.operator}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
case "TableAccess": {
|
|
const values = evalPromiseValues([
|
|
evalPrefixExpression(e.object, env),
|
|
evalExpression(e.key, env),
|
|
]);
|
|
if (values instanceof Promise) {
|
|
return values.then(([table, key]) =>
|
|
luaGet(singleResult(table), singleResult(key))
|
|
);
|
|
} else {
|
|
return luaGet(singleResult(values[0]), singleResult(values[1]));
|
|
}
|
|
}
|
|
case "Variable":
|
|
case "FunctionCall":
|
|
return evalPrefixExpression(e, env);
|
|
case "TableConstructor": {
|
|
const table = new LuaTable();
|
|
const promises: Promise<void>[] = [];
|
|
for (const field of e.fields) {
|
|
switch (field.type) {
|
|
case "PropField": {
|
|
const value = evalExpression(field.value, env);
|
|
if (value instanceof Promise) {
|
|
promises.push(value.then((value) => {
|
|
table.set(
|
|
field.key,
|
|
singleResult(value),
|
|
);
|
|
}));
|
|
} else {
|
|
table.set(field.key, singleResult(value));
|
|
}
|
|
break;
|
|
}
|
|
case "DynamicField": {
|
|
const key = evalExpression(field.key, env);
|
|
const value = evalExpression(field.value, env);
|
|
if (
|
|
key instanceof Promise || value instanceof Promise
|
|
) {
|
|
promises.push(
|
|
Promise.all([
|
|
key instanceof Promise
|
|
? key
|
|
: Promise.resolve(key),
|
|
value instanceof Promise
|
|
? value
|
|
: Promise.resolve(value),
|
|
]).then(([key, value]) => {
|
|
table.set(
|
|
singleResult(key),
|
|
singleResult(value),
|
|
);
|
|
}),
|
|
);
|
|
} else {
|
|
table.set(
|
|
singleResult(key),
|
|
singleResult(value),
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "ExpressionField": {
|
|
const value = evalExpression(field.value, env);
|
|
if (value instanceof Promise) {
|
|
promises.push(value.then((value) => {
|
|
// +1 because Lua tables are 1-indexed
|
|
table.set(
|
|
table.length + 1,
|
|
singleResult(value),
|
|
);
|
|
}));
|
|
} else {
|
|
// +1 because Lua tables are 1-indexed
|
|
table.set(
|
|
table.length + 1,
|
|
singleResult(value),
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (promises.length > 0) {
|
|
return Promise.all(promises).then(() => table);
|
|
} else {
|
|
return table;
|
|
}
|
|
}
|
|
default:
|
|
throw new Error(`Unknown expression type ${e.type}`);
|
|
}
|
|
}
|
|
|
|
function evalPrefixExpression(
|
|
e: LuaExpression,
|
|
env: LuaEnv,
|
|
): Promise<LuaValue> | LuaValue {
|
|
switch (e.type) {
|
|
case "Variable": {
|
|
const value = env.get(e.name);
|
|
if (value === undefined) {
|
|
throw new Error(`Undefined variable ${e.name}`);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
case "Parenthesized":
|
|
return evalExpression(e.expression, env);
|
|
case "FunctionCall": {
|
|
const fn = evalPrefixExpression(e.prefix, env);
|
|
if (fn instanceof Promise) {
|
|
return fn.then((fn: ILuaFunction) => {
|
|
if (!fn.call) {
|
|
throw new Error(`Not a function: ${fn}`);
|
|
}
|
|
const args = evalPromiseValues(
|
|
e.args.map((arg) => evalExpression(arg, env)),
|
|
);
|
|
if (args instanceof Promise) {
|
|
return args.then((args) => fn.call(...args));
|
|
} else {
|
|
return fn.call(...args);
|
|
}
|
|
});
|
|
} else {
|
|
if (!fn.call) {
|
|
throw new Error(`Not a function: ${fn}`);
|
|
}
|
|
const args = evalPromiseValues(
|
|
e.args.map((arg) => evalExpression(arg, env)),
|
|
);
|
|
if (args instanceof Promise) {
|
|
return args.then((args) => fn.call(...args));
|
|
} else {
|
|
return fn.call(...args);
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
throw new Error(`Unknown prefix expression type ${e.type}`);
|
|
}
|
|
}
|
|
|
|
function luaOp(op: string, left: any, right: any): any {
|
|
switch (op) {
|
|
case "+":
|
|
return left + right;
|
|
case "-":
|
|
return left - right;
|
|
case "*":
|
|
return left * right;
|
|
case "/":
|
|
return left / right;
|
|
case "//":
|
|
return Math.floor(left / right);
|
|
case "%":
|
|
return left % right;
|
|
case "^":
|
|
return left ** right;
|
|
case "..":
|
|
return left + right;
|
|
case "==":
|
|
return left === right;
|
|
case "~=":
|
|
case "!=":
|
|
case "/=":
|
|
return left !== right;
|
|
case "<":
|
|
return left < right;
|
|
case "<=":
|
|
return left <= right;
|
|
case ">":
|
|
return left > right;
|
|
case ">=":
|
|
return left >= right;
|
|
case "and":
|
|
return left && right;
|
|
case "or":
|
|
return left || right;
|
|
default:
|
|
throw new Error(`Unknown operator ${op}`);
|
|
}
|
|
}
|
|
|
|
export async function evalStatement(
|
|
s: LuaStatement,
|
|
env: LuaEnv,
|
|
): Promise<void> {
|
|
switch (s.type) {
|
|
case "Assignment": {
|
|
const values = await evalPromiseValues(
|
|
s.expressions.map((value) => evalExpression(value, env)),
|
|
);
|
|
const lvalues = await evalPromiseValues(s.variables
|
|
.map((lval) => evalLValue(lval, env)));
|
|
|
|
for (let i = 0; i < lvalues.length; i++) {
|
|
lvalues[i].env.set(lvalues[i].key, values[i]);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Local": {
|
|
for (let i = 0; i < s.names.length; i++) {
|
|
if (!s.expressions || s.expressions[i] === undefined) {
|
|
env.setLocal(s.names[i].name, null);
|
|
} else {
|
|
const value = await evalExpression(s.expressions[i], env);
|
|
env.setLocal(s.names[i].name, value);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "Semicolon":
|
|
break;
|
|
case "Label":
|
|
case "Goto":
|
|
throw new Error("Labels and gotos are not supported yet");
|
|
case "Block": {
|
|
const newEnv = new LuaEnv(env);
|
|
for (const statement of s.statements) {
|
|
await evalStatement(statement, newEnv);
|
|
}
|
|
break;
|
|
}
|
|
case "If": {
|
|
for (const cond of s.conditions) {
|
|
if (luaTruthy(await evalExpression(cond.condition, env))) {
|
|
return evalStatement(cond.block, env);
|
|
}
|
|
}
|
|
if (s.elseBlock) {
|
|
return evalStatement(s.elseBlock, env);
|
|
}
|
|
break;
|
|
}
|
|
case "While": {
|
|
while (luaTruthy(await evalExpression(s.condition, env))) {
|
|
try {
|
|
await evalStatement(s.block, env);
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "Repeat": {
|
|
do {
|
|
try {
|
|
await evalStatement(s.block, env);
|
|
} catch (e: any) {
|
|
if (e instanceof LuaBreak) {
|
|
break;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
} while (!luaTruthy(await evalExpression(s.condition, env)));
|
|
break;
|
|
}
|
|
case "Break":
|
|
throw new LuaBreak();
|
|
case "FunctionCallStatement": {
|
|
return evalExpression(s.call, env);
|
|
}
|
|
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 Error(
|
|
`Cannot find property ${propNames[i]}`,
|
|
);
|
|
}
|
|
}
|
|
settable.set(
|
|
propNames[propNames.length - 1],
|
|
new LuaFunction(body, env),
|
|
);
|
|
break;
|
|
}
|
|
case "Return": {
|
|
throw new LuaReturn(
|
|
await evalPromiseValues(
|
|
s.expressions.map((value) => evalExpression(value, env)),
|
|
),
|
|
);
|
|
}
|
|
default:
|
|
throw new Error(`Unknown statement type ${s.type}`);
|
|
}
|
|
}
|
|
|
|
function evalLValue(
|
|
lval: LuaLValue,
|
|
env: LuaEnv,
|
|
): LuaLValueContainer | Promise<LuaLValueContainer> {
|
|
switch (lval.type) {
|
|
case "Variable":
|
|
return { env, key: lval.name };
|
|
case "TableAccess": {
|
|
const objValue = evalExpression(
|
|
lval.object,
|
|
env,
|
|
);
|
|
const keyValue = evalExpression(lval.key, env);
|
|
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,
|
|
);
|
|
if (objValue instanceof Promise) {
|
|
return objValue.then((objValue) => {
|
|
if (!objValue.set) {
|
|
throw new Error(
|
|
`Not a settable object: ${objValue}`,
|
|
);
|
|
}
|
|
return {
|
|
env: objValue,
|
|
key: lval.property,
|
|
};
|
|
});
|
|
} else {
|
|
if (!objValue.set) {
|
|
throw new Error(
|
|
`Not a settable object: ${objValue}`,
|
|
);
|
|
}
|
|
return {
|
|
env: objValue,
|
|
key: lval.property,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|