Space Lua: Refactor control flow signals (#1794)

* Space Lua: Refactor control flow signals

Removes "return throws an exception" failure mode. @zef :)

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Remove unused function

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Remove another unused function

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Remove unused `LuaBreak`/`LuaReturn` exception control flow

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

---------

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
This commit is contained in:
Matouš Jan Fialka
2026-01-29 08:07:26 +01:00
committed by GitHub
parent 5a20a5f8f4
commit e5b4c8feb2
3 changed files with 168 additions and 271 deletions
+139 -258
View File
@@ -14,7 +14,6 @@ import {
type ILuaGettable,
type ILuaSettable,
jsToLuaValue,
LuaBreak,
luaCall,
luaCloseFromMark,
luaEnsureCloseStack,
@@ -26,7 +25,6 @@ import {
type LuaLValueContainer,
luaMarkToBeClosed,
LuaMultiRes,
LuaReturn,
LuaRuntimeError,
luaSet,
type LuaStackFrame,
@@ -83,10 +81,18 @@ import {
import { getBlockGotoMeta } from "./labels.ts";
type GotoSignal = { ctrl: "goto"; target: string };
type ReturnSignal = { ctrl: "return"; values: LuaValue[] };
type BreakSignal = { ctrl: "break" };
type ControlSignal = GotoSignal | ReturnSignal | BreakSignal;
function isGotoSignal(v: any): v is GotoSignal {
return !!v && typeof v === "object" && v.ctrl === "goto";
}
function isBreakSignal(v: any): v is BreakSignal {
return !!v && typeof v === "object" && v.ctrl === "break";
}
function consumeGotoInBlock(
res: any,
labels: Map<string, number>,
@@ -1124,9 +1130,8 @@ function evalExpressions(
type EvalBlockResult =
| void
| LuaValue[]
| GotoSignal
| Promise<void | LuaValue[] | GotoSignal>;
| ControlSignal
| Promise<void | ControlSignal>;
function runStatementsNoGoto(
stmts: LuaStatement[],
@@ -1134,10 +1139,10 @@ function runStatementsNoGoto(
sf: LuaStackFrame,
returnOnReturn: boolean,
startIdx: number,
): void | LuaValue[] | Promise<void | LuaValue[]> {
): void | ControlSignal | Promise<void | ControlSignal> {
const processFrom = (
idx: number,
): void | LuaValue[] | Promise<void | LuaValue[]> => {
): void | ControlSignal | Promise<void | ControlSignal> => {
for (let i = idx; i < stmts.length; i++) {
const result = evalStatement(
stmts[i],
@@ -1147,15 +1152,15 @@ function runStatementsNoGoto(
);
if (isPromise(result)) {
return (result as Promise<any>).then((res) => {
if (res !== undefined && !isGotoSignal(res)) {
if (res !== undefined) {
if (isGotoSignal(res)) {
throw new LuaRuntimeError(
"unexpected goto signal",
sf.withCtx(stmts[i].ctx),
);
}
return res;
}
if (isGotoSignal(res)) {
throw new LuaRuntimeError(
"unexpected goto signal",
sf.withCtx(stmts[i].ctx),
);
}
return processFrom(i + 1);
});
}
@@ -1336,7 +1341,7 @@ export function evalStatement(
env: LuaEnv,
sf: LuaStackFrame,
returnOnReturn = false,
): void | LuaValue[] | GotoSignal | Promise<void | LuaValue[] | GotoSignal> {
): void | ControlSignal | Promise<void | ControlSignal> {
switch (s.type) {
case "Assignment": {
const a = asAssignment(s);
@@ -1536,9 +1541,8 @@ export function evalStatement(
i: number,
):
| void
| LuaValue[]
| GotoSignal
| Promise<void | LuaValue[] | GotoSignal> => {
| ControlSignal
| Promise<void | ControlSignal> => {
if (i >= conds.length) {
if (iff.elseBlock) {
return evalStatement(iff.elseBlock, env, sf, returnOnReturn);
@@ -1565,33 +1569,20 @@ export function evalStatement(
}
case "While": {
const w = asWhile(s);
const runAsync = async (): Promise<void | LuaValue[] | GotoSignal> => {
const runAsync = async (): Promise<void | ControlSignal> => {
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) {
const r = evalStatement(w.block, env, sf, returnOnReturn);
const res = isPromise(r) ? await r : r;
if (res !== undefined) {
if (isBreakSignal(res)) {
break;
} else {
throw e;
}
return res;
}
}
return;
@@ -1608,32 +1599,24 @@ export function evalStatement(
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) {
if (isBreakSignal(res)) {
return;
}
return res;
}
return runAsync();
}).catch((e: any) => {
if (e instanceof LuaBreak) {
return;
}
throw e;
});
} else {
if (isGotoSignal(r)) {
return r;
}
if (r !== undefined) {
if (isBreakSignal(r)) {
return;
}
return r;
}
return runAsync();
}
} catch (e: any) {
if (e instanceof LuaBreak) {
return;
}
throw e;
}
});
@@ -1641,36 +1624,23 @@ export function evalStatement(
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) {
const r = evalStatement(w.block, env, sf, returnOnReturn);
if (isPromise(r)) {
return (r as Promise<any>).then((res) => {
if (res !== undefined) {
if (isBreakSignal(res)) {
return;
}
throw e;
});
} else {
if (isGotoSignal(r)) {
return r;
return res;
}
if (r !== undefined) {
return r;
return runAsync();
});
} else {
if (r !== undefined) {
if (isBreakSignal(r)) {
break;
}
}
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
return r;
}
}
}
@@ -1678,32 +1648,16 @@ export function evalStatement(
}
case "Repeat": {
const r = asRepeat(s);
const runAsync = async (): Promise<void | LuaValue[] | GotoSignal> => {
const runAsync = async (): Promise<void | ControlSignal> => {
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) {
const rr = evalStatement(r.block, env, sf, returnOnReturn);
const res = isPromise(rr) ? await rr : rr;
if (res !== undefined) {
if (isBreakSignal(res)) {
break;
} else {
throw e;
}
return res;
}
const c = await evalExpression(r.condition, env, sf);
if (luaTruthy(c)) {
@@ -1714,41 +1668,31 @@ export function evalStatement(
};
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) {
const rr = evalStatement(r.block, env, sf, returnOnReturn);
if (isPromise(rr)) {
return (rr as Promise<any>).then((res) => {
if (res !== undefined) {
if (isBreakSignal(res)) {
return;
}
throw e;
});
} else {
if (isGotoSignal(rr)) {
return rr;
return res;
}
if (rr !== undefined) return rr;
}
} catch (e: any) {
if (e instanceof LuaBreak) {
break;
} else {
throw e;
return runAsync();
});
} else {
if (rr !== undefined) {
if (isBreakSignal(rr)) {
return;
}
return rr;
}
}
const c = evalExpression(r.condition, env, sf);
if (isPromise(c)) {
return (c as Promise<any>).then((
cv,
) => (luaTruthy(cv) ? undefined : runAsync()));
return (c as Promise<any>).then((cv) =>
luaTruthy(cv) ? undefined : runAsync()
);
} else {
if (luaTruthy(c)) {
break;
@@ -1758,7 +1702,7 @@ export function evalStatement(
return;
}
case "Break": {
throw new LuaBreak();
return { ctrl: "break" };
}
case "FunctionCallStatement": {
const fcs = asFunctionCallStmt(s);
@@ -1812,16 +1756,17 @@ export function evalStatement(
evalExpression(value, env, sf)
);
const valuesRP = rpAll(parts);
if (returnOnReturn) {
return isPromise(valuesRP) ? valuesRP : valuesRP;
if (isPromise(valuesRP)) {
return (valuesRP as Promise<any[]>).then((vals) => ({
ctrl: "return",
values: vals as LuaValue[],
}));
} else {
if (isPromise(valuesRP)) {
return (valuesRP as Promise<any[]>).then((vals) => {
throw new LuaReturn(vals);
});
} else {
throw new LuaReturn(valuesRP as LuaValue[]);
}
return {
ctrl: "return",
values: valuesRP as LuaValue[],
};
}
}
case "For": {
@@ -1838,27 +1783,13 @@ export function evalStatement(
) {
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) {
const r = evalStatement(fr.block, localEnv, sf, returnOnReturn);
const res = isPromise(r) ? await r : r;
if (res !== undefined) {
if (isBreakSignal(res)) {
break;
} else {
throw e;
}
return res;
}
}
return;
@@ -1870,9 +1801,8 @@ export function evalStatement(
step: any,
):
| void
| LuaValue[]
| GotoSignal
| Promise<void | LuaValue[] | GotoSignal> => {
| ControlSignal
| Promise<void | ControlSignal> => {
for (
let i = start;
step > 0 ? i <= end : i >= end;
@@ -1880,34 +1810,22 @@ export function evalStatement(
) {
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) {
const r = evalStatement(fr.block, localEnv, sf, returnOnReturn);
if (isPromise(r)) {
return (r as Promise<any>).then((res) => {
if (res !== undefined) {
if (isBreakSignal(res)) {
return;
}
throw e;
});
} else if (isGotoSignal(r)) {
return r;
} else if (r !== undefined) {
return r;
}
} catch (e: any) {
if (e instanceof LuaBreak) {
return res;
}
return runAsync(i + step, end, step);
});
} else if (r !== undefined) {
if (isBreakSignal(r)) {
break;
} else {
throw e;
}
return r;
}
}
return;
@@ -2010,27 +1928,13 @@ export function evalStatement(
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 await finish(res);
}
if (res !== undefined) {
return await finish(res);
}
} else if (isGotoSignal(r)) {
return await finish(r);
} else if (r !== undefined) {
return await finish(r);
}
} catch (e: any) {
if (e instanceof LuaBreak) {
const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
const res = isPromise(r) ? await r : r;
if (res !== undefined) {
if (isBreakSignal(res)) {
break;
} else {
throw e;
}
return await finish(res);
}
}
return await finish(undefined);
@@ -2059,42 +1963,30 @@ export function evalStatement(
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 rpThen(finish(undefined), () => res);
}
if (res !== undefined) {
return rpThen(finish(undefined), () => res);
}
return runAsync();
}).catch((e: any) => {
if (e instanceof LuaBreak) {
const r = evalStatement(
fi.block,
localEnv,
sf,
returnOnReturn,
);
if (isPromise(r)) {
return (r as Promise<any>).then((res) => {
if (res !== undefined) {
if (isBreakSignal(res)) {
return finish(undefined);
}
throw e;
});
} else {
if (isGotoSignal(r)) {
return rpThen(finish(undefined), () => r);
}
if (r !== undefined) {
return rpThen(finish(undefined), () => r);
return rpThen(finish(undefined), () => res);
}
return runAsync();
});
} else {
if (r !== undefined) {
if (isBreakSignal(r)) {
return finish(undefined);
}
return rpThen(finish(undefined), () => r);
}
} catch (e: any) {
if (e instanceof LuaBreak) {
return finish(undefined);
}
throw e;
return runAsync();
}
}).catch((e: any) => finishErr(e));
}
@@ -2115,33 +2007,22 @@ export function evalStatement(
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 rpThen(finish(undefined), () => res);
}
if (res !== undefined) {
return rpThen(finish(undefined), () => res);
}
return runAsync();
}).catch((e: any) => {
if (e instanceof LuaBreak) {
const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
if (isPromise(r)) {
return (r as Promise<any>).then((res) => {
if (res !== undefined) {
if (isBreakSignal(res)) {
return finish(undefined);
}
throw e;
}).catch((e: any) => finishErr(e));
} else if (isGotoSignal(r)) {
return rpThen(finish(undefined), () => r);
} else if (r !== undefined) {
return rpThen(finish(undefined), () => r);
}
} catch (e: any) {
if (e instanceof LuaBreak) {
return rpThen(finish(undefined), () => res);
}
return runAsync();
}).catch((e: any) => finishErr(e));
} else if (r !== undefined) {
if (isBreakSignal(r)) {
return finish(undefined);
}
return finishErr(e);
return rpThen(finish(undefined), () => r);
}
}
} catch (e: any) {
+19 -12
View File
@@ -445,11 +445,28 @@ export class LuaFunction implements ILuaFunction {
// Evaluate the function body with returnOnReturn set to true
const r = evalStatement(this.body.block, env, sfWithFn, true);
const map = (val: any) => {
if (val !== undefined) {
return mapFunctionReturnValue(val);
if (val === undefined) {
return;
}
if (val && typeof val === "object" && val.ctrl === "return") {
return mapFunctionReturnValue(val.values);
}
if (val && typeof val === "object" && val.ctrl === "break") {
throw new LuaRuntimeError(
"break outside loop",
sfWithFn.withCtx(this.body.block.ctx),
);
}
if (val && typeof val === "object" && val.ctrl === "goto") {
throw new LuaRuntimeError(
"unexpected goto signal",
sfWithFn.withCtx(this.body.block.ctx),
);
}
};
if (isPromise(r)) {
return r.then(map);
} else {
@@ -1042,16 +1059,6 @@ export function luaTypeOf(val: any): LuaType | Promise<LuaType> {
}
}
// Both `break` and `return` are implemented by exception throwing
export class LuaBreak extends Error {
}
export class LuaReturn extends Error {
constructor(readonly values: LuaValue[]) {
super();
}
}
export class LuaRuntimeError extends Error {
constructor(
override readonly message: string,
+10 -1
View File
@@ -31,7 +31,16 @@ export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue {
if (res === undefined) {
return null;
} else {
return new LuaMultiRes(res);
if (res && typeof res === "object" && (res as any).ctrl === "return") {
return new LuaMultiRes((res as any).values);
}
if (res && typeof res === "object" && (res as any).ctrl === "break") {
throw new Error("break outside loop");
}
if (res && typeof res === "object" && (res as any).ctrl === "goto") {
throw new Error("unexpected goto signal");
}
return null;
}
});