From e5b4c8feb22a44cb4b22b3a77f9f2ed21dd09297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Jan=20Fialka?= Date: Thu, 29 Jan 2026 08:07:26 +0100 Subject: [PATCH] Space Lua: Refactor control flow signals (#1794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Space Lua: Refactor control flow signals Removes "return throws an exception" failure mode. @zef :) Signed-off-by: Matouš Jan Fialka * Remove unused function Signed-off-by: Matouš Jan Fialka * Remove another unused function Signed-off-by: Matouš Jan Fialka * Remove unused `LuaBreak`/`LuaReturn` exception control flow Signed-off-by: Matouš Jan Fialka --------- Signed-off-by: Matouš Jan Fialka --- client/space_lua/eval.ts | 397 +++++++++++--------------------- client/space_lua/runtime.ts | 31 ++- client/space_lua/stdlib/load.ts | 11 +- 3 files changed, 168 insertions(+), 271 deletions(-) diff --git a/client/space_lua/eval.ts b/client/space_lua/eval.ts index 3e172148..63e0f2bf 100644 --- a/client/space_lua/eval.ts +++ b/client/space_lua/eval.ts @@ -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, @@ -1124,9 +1130,8 @@ function evalExpressions( type EvalBlockResult = | void - | LuaValue[] - | GotoSignal - | Promise; + | ControlSignal + | Promise; function runStatementsNoGoto( stmts: LuaStatement[], @@ -1134,10 +1139,10 @@ function runStatementsNoGoto( sf: LuaStackFrame, returnOnReturn: boolean, startIdx: number, -): void | LuaValue[] | Promise { +): void | ControlSignal | Promise { const processFrom = ( idx: number, - ): void | LuaValue[] | Promise => { + ): void | ControlSignal | Promise => { 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).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 | ControlSignal | Promise { switch (s.type) { case "Assignment": { const a = asAssignment(s); @@ -1536,9 +1541,8 @@ export function evalStatement( i: number, ): | void - | LuaValue[] - | GotoSignal - | Promise => { + | ControlSignal + | Promise => { 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 => { + + const runAsync = async (): Promise => { 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).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).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).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 => { + + const runAsync = async (): Promise => { 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).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).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).then(( - cv, - ) => (luaTruthy(cv) ? undefined : runAsync())); + return (c as Promise).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).then((vals) => ({ + ctrl: "return", + values: vals as LuaValue[], + })); } else { - if (isPromise(valuesRP)) { - return (valuesRP as Promise).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 => { + | ControlSignal + | Promise => { 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).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).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).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).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).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).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) { diff --git a/client/space_lua/runtime.ts b/client/space_lua/runtime.ts index 2ebaf338..48f37129 100644 --- a/client/space_lua/runtime.ts +++ b/client/space_lua/runtime.ts @@ -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 { } } -// 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, diff --git a/client/space_lua/stdlib/load.ts b/client/space_lua/stdlib/load.ts index 89655264..76783f4a 100644 --- a/client/space_lua/stdlib/load.ts +++ b/client/space_lua/stdlib/load.ts @@ -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; } });