From b363bd7e593c85fcc5e0157b8b8b72768c5360e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Jan=20Fialka?= Date: Mon, 20 Oct 2025 17:34:45 +0200 Subject: [PATCH] Fix serious truthiness evaluation flaws. (#1644) In Lua, only the last expression in an argument list preserves multiple results and all earlier expressions yield exactly one value (their first). For example: ``` f(a(), b(), c()) ``` where ``` a -> (1,2) b -> (3,4) c -> (5,6) ``` becomes ``` f(1, 3, 5, 6) ``` which matches Lua behaviour (tested in version 5.4). Changes: * Function argument lists keep multiple results only from the last argument. * Fixes failing truthiness tests (`true and ret_one_two()`). Sidenote: the `truthiness_test.lua` was not called at all because of my obvious copy/paste typo (the `arithmetical_test.lua` was called twice instead; now fixed too and both test suites pass ok). --- client/space_lua/eval.ts | 44 +++++++++++++++++++++------- client/space_lua/lua.test.ts | 2 +- client/space_lua/stdlib/load.ts | 2 +- client/space_lua/truthiness_test.lua | 26 ++++++++++++++++ 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/client/space_lua/eval.ts b/client/space_lua/eval.ts index b600b6c1..48b92b98 100644 --- a/client/space_lua/eval.ts +++ b/client/space_lua/eval.ts @@ -268,7 +268,7 @@ export function evalExpression( return +singleResult(value); } case "not": { - return !singleResult(value); + return !luaTruthy(singleResult(value)); } case "~": { const arg = singleResult(value); @@ -307,7 +307,7 @@ export function evalExpression( return +singleResult(value); } case "not": { - return !singleResult(value); + return !luaTruthy(singleResult(value)); } case "~": { const arg = singleResult(value); @@ -493,7 +493,7 @@ function evalPrefixExpression( } case "FunctionCall": { const prefixValue = evalPrefixExpression(e.prefix, env, sf); - if (!prefixValue) { + if (prefixValue === null || prefixValue === undefined) { throw new LuaRuntimeError( `Attempting to call nil as a function`, sf.withCtx(e.prefix.ctx), @@ -604,14 +604,16 @@ function evalLogical( const decide = (lv: any) => { if (op === "or") { if (luaTruthy(lv)) { - return lv; + return singleResult(lv); } - return evalExpression(rightExpr, env, sf); + const rv = evalExpression(rightExpr, env, sf); + return rv instanceof Promise ? rv.then(singleResult) : singleResult(rv); } else { if (!luaTruthy(lv)) { - return lv; + return singleResult(lv); } - return evalExpression(rightExpr, env, sf); + const rv = evalExpression(rightExpr, env, sf); + return rv instanceof Promise ? rv.then(singleResult) : singleResult(rv); } }; @@ -901,12 +903,32 @@ function evalExpressions( 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((argsResolved) => - new LuaMultiRes(argsResolved).flatten().values - ); + return argsVal.then(finalize); } else { - return new LuaMultiRes(argsVal).flatten().values; + return finalize(argsVal as any[]); } } diff --git a/client/space_lua/lua.test.ts b/client/space_lua/lua.test.ts index cf6a4706..ae8d8c82 100644 --- a/client/space_lua/lua.test.ts +++ b/client/space_lua/lua.test.ts @@ -10,7 +10,7 @@ Deno.test("[Lua] Core language", async () => { }); Deno.test("[Lua] Core language (truthiness)", async () => { - await runLuaTest("./arithmetic_test.lua"); + await runLuaTest("./truthiness_test.lua"); }); Deno.test("[Lua] Core language (arithmetic)", async () => { diff --git a/client/space_lua/stdlib/load.ts b/client/space_lua/stdlib/load.ts index 7f0d88f4..89655264 100644 --- a/client/space_lua/stdlib/load.ts +++ b/client/space_lua/stdlib/load.ts @@ -1,6 +1,6 @@ import { LuaBuiltinFunction, - LuaEnv, + type LuaEnv, LuaMultiRes, type LuaStackFrame, type LuaValue, diff --git a/client/space_lua/truthiness_test.lua b/client/space_lua/truthiness_test.lua index 2afdc72c..2570fd42 100644 --- a/client/space_lua/truthiness_test.lua +++ b/client/space_lua/truthiness_test.lua @@ -226,3 +226,29 @@ assert_eq(s1, 5, "or should see falsy first result and evaluate RHS") local s2 = (truthy_pair() or 5) assert_eq(s2, 1, "or should return first result of LHS when truthy") + +-- 12. Only the last expression expands + +local function ret() return "A", "B" end +local function a() return "A1", "A2" end +local function b() return "B1", "B2" end +local function c() return "C1", "C2" end + +assert_eq(string.format("%s-%s", ret()), "A-B", + "last-only: single arg expands") + +assert_eq(string.format("%s-%s", ret(), "Z"), "A-Z", + "last-only: earlier arg is single") + +assert_eq(string.format("%s-%s-%s-%s", a(), b(), c()), "A1-B1-C1-C2", + "last-only: only last expands, earlier args single") + +-- 13. Only false and `nil` are falsey + +assert_false(not 0, "not 0 must be false") +assert_false(not -0.0, "not -0.0 must be false") +assert_false(not "", "not '' must be false") +assert_false(not {}, "not {} must be false") + +assert_true(not nil, "not nil must be true") +assert_true(not false, "not false must be true")