silverbullet/common/space_lua/eval.ts

712 lines
20 KiB
TypeScript
Raw Normal View History

2024-09-27 15:11:03 +08:00
import type {
2024-10-20 21:06:23 +08:00
ASTCtx,
LuaExpression,
LuaLValue,
LuaStatement,
2024-09-27 15:11:03 +08:00
} from "$common/space_lua/ast.ts";
2024-09-12 23:01:54 +08:00
import { evalPromiseValues } from "$common/space_lua/util.ts";
2024-10-20 21:06:23 +08:00
import {
luaCall,
luaSet,
type LuaStackFrame,
} from "$common/space_lua/runtime.ts";
2024-09-24 16:15:22 +08:00
import {
type ILuaFunction,
type ILuaGettable,
type ILuaSettable,
LuaBreak,
LuaEnv,
LuaFunction,
luaGet,
luaLen,
type LuaLValueContainer,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
LuaTable,
luaToString,
luaTruthy,
type LuaValue,
singleResult,
2024-09-24 16:15:22 +08:00
} from "./runtime.ts";
2024-09-12 23:01:54 +08:00
export function evalExpression(
e: LuaExpression,
env: LuaEnv,
2024-10-20 21:06:23 +08:00
sf: LuaStackFrame,
2024-09-27 15:11:03 +08:00
): Promise<LuaValue> | LuaValue {
try {
switch (e.type) {
case "String":
return e.value;
case "Number":
return e.value;
case "Boolean":
return e.value;
case "Nil":
return null;
case "Binary": {
const values = evalPromiseValues([
2024-10-20 21:06:23 +08:00
evalExpression(e.left, env, sf),
evalExpression(e.right, env, sf),
]);
if (values instanceof Promise) {
return values.then(([left, right]) =>
2024-10-20 21:06:23 +08:00
luaOp(
e.operator,
singleResult(left),
singleResult(right),
e.ctx,
sf,
)
);
} else {
return luaOp(
e.operator,
singleResult(values[0]),
singleResult(values[1]),
2024-10-20 21:06:23 +08:00
e.ctx,
sf,
);
}
}
case "Unary": {
2024-10-20 21:06:23 +08:00
const value = evalExpression(e.argument, env, sf);
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}`,
);
}
2024-09-12 23:01:54 +08:00
}
}
2024-10-09 01:53:09 +08:00
case "Variable":
case "FunctionCall":
2024-10-09 01:53:09 +08:00
case "TableAccess":
case "PropertyAccess":
2024-10-20 21:06:23 +08:00
return evalPrefixExpression(e, env, sf);
case "TableConstructor": {
const table = new LuaTable();
const promises: Promise<void>[] = [];
for (const field of e.fields) {
switch (field.type) {
case "PropField": {
2024-10-20 21:06:23 +08:00
const value = evalExpression(field.value, env, sf);
if (value instanceof Promise) {
promises.push(value.then((value) => {
table.set(
field.key,
singleResult(value),
2024-10-20 21:06:23 +08:00
sf,
);
}));
} else {
2024-10-20 21:06:23 +08:00
table.set(field.key, singleResult(value), sf);
}
break;
}
case "DynamicField": {
2024-10-20 21:06:23 +08:00
const key = evalExpression(field.key, env, sf);
const value = evalExpression(field.value, env, sf);
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),
2024-10-20 21:06:23 +08:00
sf,
);
}),
);
} else {
table.set(
singleResult(key),
singleResult(value),
2024-10-20 21:06:23 +08:00
sf,
2024-09-12 23:01:54 +08:00
);
}
break;
2024-09-12 23:01:54 +08:00
}
case "ExpressionField": {
2024-10-20 21:06:23 +08:00
const value = evalExpression(field.value, env, sf);
if (value instanceof Promise) {
promises.push(value.then((value) => {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
2024-10-20 21:06:23 +08:00
sf,
);
}));
} else {
// +1 because Lua tables are 1-indexed
table.set(
table.length + 1,
singleResult(value),
2024-10-20 21:06:23 +08:00
sf,
);
}
break;
2024-09-24 16:15:22 +08:00
}
}
2024-09-24 16:15:22 +08:00
}
if (promises.length > 0) {
return Promise.all(promises).then(() => table);
} else {
return table;
}
}
case "FunctionDefinition": {
return new LuaFunction(e.body, env);
}
default:
throw new Error(`Unknown expression type ${e.type}`);
}
} catch (err: any) {
// Repackage any non Lua-specific exceptions with some position information
if (!err.constructor.name.startsWith("Lua")) {
2024-10-20 21:06:23 +08:00
throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
} else {
throw err;
2024-09-12 23:01:54 +08:00
}
}
2024-09-12 23:01:54 +08:00
}
function evalPrefixExpression(
e: LuaExpression,
env: LuaEnv,
2024-10-20 21:06:23 +08:00
sf: LuaStackFrame,
2024-09-27 15:11:03 +08:00
): Promise<LuaValue> | LuaValue {
switch (e.type) {
case "Variable": {
const value = env.get(e.name);
if (value === undefined) {
return null;
} else {
return value;
}
}
case "Parenthesized":
2024-10-20 21:06:23 +08:00
return evalExpression(e.expression, env, sf);
2024-10-09 01:53:09 +08:00
// <<expr>>[<<expr>>]
case "TableAccess": {
const values = evalPromiseValues([
2024-10-20 21:06:23 +08:00
evalPrefixExpression(e.object, env, sf),
evalExpression(e.key, env, sf),
2024-10-09 01:53:09 +08:00
]);
if (values instanceof Promise) {
return values.then(([table, key]) => {
table = singleResult(table);
key = singleResult(key);
2024-10-10 02:35:07 +08:00
2024-10-20 21:06:23 +08:00
return luaGet(table, key, sf.withCtx(e.ctx));
2024-10-09 01:53:09 +08:00
});
} else {
const table = singleResult(values[0]);
const key = singleResult(values[1]);
2024-10-20 21:06:23 +08:00
return luaGet(table, singleResult(key), sf.withCtx(e.ctx));
2024-10-09 01:53:09 +08:00
}
}
// <expr>.property
case "PropertyAccess": {
2024-10-20 21:06:23 +08:00
const obj = evalPrefixExpression(e.object, env, sf);
if (obj instanceof Promise) {
return obj.then((obj) => {
2024-10-20 21:06:23 +08:00
return luaGet(obj, e.property, sf.withCtx(e.ctx));
});
} else {
2024-10-20 21:06:23 +08:00
return luaGet(obj, e.property, sf.withCtx(e.ctx));
}
}
case "FunctionCall": {
2024-10-20 21:06:23 +08:00
let prefixValue = evalPrefixExpression(e.prefix, env, sf);
if (!prefixValue) {
throw new LuaRuntimeError(
`Attempting to call nil as a function`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
}
if (prefixValue instanceof Promise) {
return prefixValue.then((prefixValue) => {
if (!prefixValue) {
throw new LuaRuntimeError(
`Attempting to call a nil value`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
}
let selfArgs: LuaValue[] = [];
// Handling a:b() syntax (b is kept in .name)
if (e.name && !prefixValue.get) {
throw new LuaRuntimeError(
`Attempting to index a non-table: ${prefixValue}`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
} else if (e.name) {
// Two things need to happen: the actual function be called needs to be looked up in the table, and the table itself needs to be passed as the first argument
selfArgs = [prefixValue];
prefixValue = prefixValue.get(e.name);
}
if (!prefixValue.call) {
throw new LuaRuntimeError(
`Attempting to call ${prefixValue} as a function`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
}
const args = evalPromiseValues(
2024-10-20 21:06:23 +08:00
e.args.map((arg) => evalExpression(arg, env, sf)),
);
if (args instanceof Promise) {
2024-10-10 02:35:07 +08:00
return args.then((args) =>
2024-10-20 21:06:23 +08:00
luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf)
2024-10-10 02:35:07 +08:00
);
} else {
2024-10-20 21:06:23 +08:00
return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf);
}
});
} else {
let selfArgs: LuaValue[] = [];
// Handling a:b() syntax (b is kept in .name)
if (e.name && !prefixValue.get) {
throw new LuaRuntimeError(
`Attempting to index a non-table: ${prefixValue}`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
} else if (e.name) {
// Two things need to happen: the actual function be called needs to be looked up in the table, and the table itself needs to be passed as the first argument
selfArgs = [prefixValue];
prefixValue = prefixValue.get(e.name);
}
if (!prefixValue.call) {
throw new LuaRuntimeError(
`Attempting to call ${prefixValue} as a function`,
2024-10-20 21:06:23 +08:00
sf.withCtx(e.prefix.ctx),
);
2024-09-12 23:01:54 +08:00
}
const args = evalPromiseValues(
2024-10-20 21:06:23 +08:00
e.args.map((arg) => evalExpression(arg, env, sf)),
);
if (args instanceof Promise) {
2024-10-10 02:35:07 +08:00
return args.then((args) =>
2024-10-20 21:06:23 +08:00
luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf)
2024-10-10 02:35:07 +08:00
);
} else {
2024-10-20 21:06:23 +08:00
return luaCall(prefixValue, [...selfArgs, ...args], e.ctx, sf);
}
}
2024-09-12 23:01:54 +08:00
}
default:
throw new Error(`Unknown prefix expression type ${e.type}`);
}
2024-09-12 23:01:54 +08:00
}
// Mapping table of operators meta-methods to their corresponding operator
type LuaMetaMethod = Record<string, {
metaMethod?: string;
2024-10-20 21:06:23 +08:00
nativeImplementation: (
a: LuaValue,
b: LuaValue,
ctx: ASTCtx,
sf: LuaStackFrame,
) => LuaValue;
}>;
const operatorsMetaMethods: LuaMetaMethod = {
"+": {
metaMethod: "__add",
nativeImplementation: (a, b) => a + b,
},
"-": {
metaMethod: "__sub",
nativeImplementation: (a, b) => a - b,
},
"*": {
metaMethod: "__mul",
nativeImplementation: (a, b) => a * b,
},
"/": {
metaMethod: "__div",
nativeImplementation: (a, b) => a / b,
},
"//": {
metaMethod: "__idiv",
nativeImplementation: (a, b) => Math.floor(a / b),
},
"%": {
metaMethod: "__mod",
nativeImplementation: (a, b) => a % b,
},
"^": {
metaMethod: "__pow",
nativeImplementation: (a, b) => a ** b,
},
"..": {
metaMethod: "__concat",
2024-10-26 22:02:37 +08:00
nativeImplementation: (a, b) => {
const aString = luaToString(a);
const bString = luaToString(b);
if (aString instanceof Promise || bString instanceof Promise) {
return Promise.all([aString, bString]).then(([aString, bString]) =>
aString + bString
);
} else {
return aString + bString;
}
},
},
"==": {
metaMethod: "__eq",
nativeImplementation: (a, b) => a === b,
},
"~=": {
metaMethod: "__ne",
nativeImplementation: (a, b) => a !== b,
},
"!=": {
metaMethod: "__ne",
nativeImplementation: (a, b) => a !== b,
},
"<": {
metaMethod: "__lt",
nativeImplementation: (a, b) => a < b,
},
"<=": {
metaMethod: "__le",
nativeImplementation: (a, b) => a <= b,
},
">": {
2024-10-20 21:06:23 +08:00
nativeImplementation: (a, b, ctx, sf) => !luaOp("<=", a, b, ctx, sf),
},
">=": {
2024-10-20 21:06:23 +08:00
nativeImplementation: (a, b, ctx, cf) => !luaOp("<", a, b, ctx, cf),
},
and: {
metaMethod: "__and",
nativeImplementation: (a, b) => a && b,
},
or: {
metaMethod: "__or",
nativeImplementation: (a, b) => a || b,
},
};
2024-10-20 21:06:23 +08:00
function luaOp(
op: string,
left: any,
right: any,
ctx: ASTCtx,
sf: LuaStackFrame,
): any {
const operatorHandler = operatorsMetaMethods[op];
if (!operatorHandler) {
2024-10-20 21:06:23 +08:00
throw new LuaRuntimeError(`Unknown operator ${op}`, sf.withCtx(ctx));
}
if (operatorHandler.metaMethod) {
if (left?.metatable?.has(operatorHandler.metaMethod)) {
const fn = left.metatable.get(operatorHandler.metaMethod);
2024-10-20 21:06:23 +08:00
return luaCall(fn, [left, right], ctx, sf);
} else if (right?.metatable?.has(operatorHandler.metaMethod)) {
const fn = right.metatable.get(operatorHandler.metaMethod);
2024-10-20 21:06:23 +08:00
return luaCall(fn, [left, right], ctx, sf);
}
}
2024-10-20 21:06:23 +08:00
return operatorHandler.nativeImplementation(left, right, ctx, sf);
}
async function evalExpressions(
es: LuaExpression[],
env: LuaEnv,
2024-10-20 21:06:23 +08:00
sf: LuaStackFrame,
): Promise<LuaValue[]> {
return new LuaMultiRes(
2024-10-20 21:06:23 +08:00
await Promise.all(es.map((e) => evalExpression(e, env, sf))),
).flatten().values;
2024-09-12 23:01:54 +08:00
}
2024-09-27 15:11:03 +08:00
export async function evalStatement(
s: LuaStatement,
env: LuaEnv,
2024-10-20 21:06:23 +08:00
sf: LuaStackFrame,
2024-09-27 15:11:03 +08:00
): Promise<void> {
switch (s.type) {
case "Assignment": {
2024-10-20 21:06:23 +08:00
const values = await evalExpressions(s.expressions, env, sf);
const lvalues = await evalPromiseValues(s.variables
2024-10-20 21:06:23 +08:00
.map((lval) => evalLValue(lval, env, sf)));
2024-09-27 15:11:03 +08:00
for (let i = 0; i < lvalues.length; i++) {
2024-10-20 21:06:23 +08:00
luaSet(lvalues[i].env, lvalues[i].key, values[i], sf.withCtx(s.ctx));
}
2024-09-27 15:11:03 +08:00
break;
}
case "Local": {
if (s.expressions) {
2024-10-20 21:06:23 +08:00
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);
2024-09-27 15:11:03 +08:00
}
}
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) {
2024-10-20 21:06:23 +08:00
await evalStatement(statement, newEnv, sf);
}
break;
}
case "If": {
for (const cond of s.conditions) {
2024-10-20 21:06:23 +08:00
if (luaTruthy(await evalExpression(cond.condition, env, sf))) {
return evalStatement(cond.block, env, sf);
2024-09-27 15:11:03 +08:00
}
}
if (s.elseBlock) {
2024-10-20 21:06:23 +08:00
return evalStatement(s.elseBlock, env, sf);
}
break;
}
case "While": {
2024-10-20 21:06:23 +08:00
while (luaTruthy(await evalExpression(s.condition, env, sf))) {
try {
2024-10-20 21:06:23 +08:00
await evalStatement(s.block, env, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
2024-09-27 15:11:03 +08:00
break;
} else {
throw e;
}
2024-09-27 15:11:03 +08:00
}
}
break;
}
case "Repeat": {
do {
try {
2024-10-20 21:06:23 +08:00
await evalStatement(s.block, env, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
2024-09-27 15:11:03 +08:00
break;
} else {
throw e;
}
2024-09-27 15:11:03 +08:00
}
2024-10-20 21:06:23 +08:00
} while (!luaTruthy(await evalExpression(s.condition, env, sf)));
break;
}
case "Break":
throw new LuaBreak();
case "FunctionCallStatement": {
2024-10-20 21:06:23 +08:00
return evalExpression(s.call, env, sf);
}
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) {
2024-10-09 01:53:09 +08:00
throw new LuaRuntimeError(
`Cannot find property ${propNames[i]}`,
2024-10-20 21:06:23 +08:00
sf.withCtx(s.name.ctx),
);
2024-09-27 15:11:03 +08:00
}
}
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": {
// A return statement for now is implemented by throwing the value as an exception, this should
// be optimized for the common case later
throw new LuaReturn(
await evalPromiseValues(
2024-10-20 21:06:23 +08:00
s.expressions.map((value) => evalExpression(value, env, sf)),
),
);
}
case "For": {
2024-10-20 21:06:23 +08:00
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;
const localEnv = new LuaEnv(env);
for (
let i = start;
step > 0 ? i <= end : i >= end;
i += step
) {
localEnv.setLocal(s.name, i);
try {
2024-10-20 21:06:23 +08:00
await evalStatement(s.block, localEnv, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
2024-09-27 15:11:03 +08:00
break;
} else {
throw e;
}
2024-09-27 15:11:03 +08:00
}
}
break;
}
case "ForIn": {
const iteratorMultiRes = new LuaMultiRes(
await evalPromiseValues(
2024-10-20 21:06:23 +08:00
s.expressions.map((e) => evalExpression(e, env, sf)),
),
).flatten();
const iteratorFunction: ILuaFunction | undefined =
iteratorMultiRes.values[0];
if (!iteratorFunction?.call) {
console.error("Cannot iterate over", iteratorMultiRes.values[0]);
throw new LuaRuntimeError(
`Cannot iterate over ${iteratorMultiRes.values[0]}`,
2024-10-20 21:06:23 +08:00
sf.withCtx(s.ctx),
);
}
const state: LuaValue = iteratorMultiRes.values[1] || null;
const control: LuaValue = iteratorMultiRes.values[2] || null;
while (true) {
const iterResult = new LuaMultiRes(
2024-10-20 21:06:23 +08:00
await luaCall(iteratorFunction, [state, control], s.ctx, sf),
).flatten();
if (
iterResult.values[0] === null || iterResult.values[0] === undefined
) {
break;
2024-09-27 15:11:03 +08:00
}
const localEnv = new LuaEnv(env);
for (let i = 0; i < s.names.length; i++) {
localEnv.setLocal(s.names[i], iterResult.values[i]);
2024-09-27 23:09:25 +08:00
}
try {
2024-10-20 21:06:23 +08:00
await evalStatement(s.block, localEnv, sf);
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
}
2024-09-27 23:09:25 +08:00
}
}
break;
2024-09-27 15:11:03 +08:00
}
}
2024-09-27 15:11:03 +08:00
}
function evalLValue(
lval: LuaLValue,
env: LuaEnv,
2024-10-20 21:06:23 +08:00
sf: LuaStackFrame,
2024-09-27 15:11:03 +08:00
): LuaLValueContainer | Promise<LuaLValueContainer> {
switch (lval.type) {
case "Variable":
return { env, key: lval.name };
case "TableAccess": {
const objValue = evalExpression(
lval.object,
env,
2024-10-20 21:06:23 +08:00
sf,
);
2024-10-20 21:06:23 +08:00
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,
2024-10-20 21:06:23 +08:00
sf,
);
if (objValue instanceof Promise) {
return objValue.then((objValue) => {
return {
env: objValue,
key: lval.property,
};
});
} else {
return {
env: objValue,
key: lval.property,
};
}
2024-09-27 15:11:03 +08:00
}
}
2024-09-27 15:11:03 +08:00
}