diff --git a/lib/lua/runtime.sx b/lib/lua/runtime.sx index 14bf4aab..c88dbcce 100644 --- a/lib/lua/runtime.sx +++ b/lib/lua/runtime.sx @@ -1503,9 +1503,15 @@ (guard (e (true (list (quote lua-multi) nil (str e)))) (let ((compiled (lua-transpile src))) - (let ((wrapped (list (make-symbol "fn") (list (make-symbol "&rest") (make-symbol "__args")) - (list (make-symbol "let") (list) compiled)))) - (eval-expr wrapped)))))) + (let ((guarded (list + (make-symbol "guard") + (list (make-symbol "e") + (list + (list (make-symbol "lua-return-sentinel?") (make-symbol "e")) + (list (make-symbol "lua-return-value") (make-symbol "e")))) + (list (make-symbol "let") (list) compiled)))) + (let ((wrapped (list (make-symbol "fn") (list (make-symbol "&rest") (make-symbol "__args")) guarded))) + (eval-expr wrapped))))))) (define loadstring lua-loadstring) (define load lua-loadstring) @@ -1630,3 +1636,13 @@ (va-build skip) (dict-set! t "n" n) t)))) + +;; Return-sentinel: wrap function bodies so mid-block `return` can escape. +(define + lua-return-sentinel? + (fn (e) + (and (= (type-of e) "list") (> (len e) 0) (= (first e) (quote lua-ret))))) + +(define + lua-return-value + (fn (e) (if (> (len e) 1) (nth e 1) nil))) diff --git a/lib/lua/scoreboard.json b/lib/lua/scoreboard.json index a9d54092..4bbeb235 100644 --- a/lib/lua/scoreboard.json +++ b/lib/lua/scoreboard.json @@ -11,7 +11,7 @@ "top_failure_modes": [ [ "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - 9 + 10 ], [ "timeout", @@ -22,11 +22,7 @@ 2 ], [ - "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: attempt to compare incompat", - 1 - ], - [ - "other: Unhandled exception: \\\"Not callable: nil (kont=10 frames)\\", + "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: concat on list and string\\\\", 1 ] ], @@ -47,19 +43,19 @@ "name": "attrib.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 6000 + "ms": 6225 }, { "name": "big.lua", "status": "timeout", "reason": "per-test timeout", - "ms": 8006 + "ms": 8008 }, { "name": "calls.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 4946 + "ms": 5175 }, { "name": "checktable.lua", @@ -71,7 +67,7 @@ "name": "closure.lua", "status": "timeout", "reason": "per-test timeout", - "ms": 8003 + "ms": 8007 }, { "name": "code.lua", @@ -82,8 +78,8 @@ { "name": "constructs.lua", "status": "fail", - "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: attempt to compare incompat", - "ms": 4533 + "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: concat on list and string\\\\", + "ms": 4874 }, { "name": "db.lua", @@ -94,14 +90,14 @@ { "name": "errors.lua", "status": "fail", - "reason": "other: Unhandled exception: \\\"Not callable: nil (kont=10 frames)\\", - "ms": 3217 + "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", + "ms": 3289 }, { "name": "events.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 7215 + "ms": 7546 }, { "name": "files.lua", @@ -119,13 +115,13 @@ "name": "literals.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 1901 + "ms": 1888 }, { "name": "locals.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: attempt to call non-functio", - "ms": 1693 + "ms": 1721 }, { "name": "main.lua", @@ -137,43 +133,43 @@ "name": "math.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 4095 + "ms": 4231 }, { "name": "nextvar.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 7177 + "ms": 7290 }, { "name": "pm.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 6048 + "ms": 6155 }, { "name": "sort.lua", "status": "timeout", "reason": "per-test timeout", - "ms": 8007 + "ms": 8006 }, { "name": "strings.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 3990 + "ms": 4215 }, { "name": "vararg.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"assertion failed!\\\\\\\"\\", - "ms": 2249 + "ms": 2288 }, { "name": "verybig.lua", "status": "fail", "reason": "other: Unhandled exception: \\\"Unhandled exception: \\\\\\\"lua: attempt to call non-functio", - "ms": 572 + "ms": 622 } ] } \ No newline at end of file diff --git a/lib/lua/scoreboard.md b/lib/lua/scoreboard.md index eb5d76a2..f04e00bb 100644 --- a/lib/lua/scoreboard.md +++ b/lib/lua/scoreboard.md @@ -5,11 +5,10 @@ fail=13 timeout=3 skip=8 total=24 ## Top failure modes -- **9x** other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ +- **10x** other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ - **3x** timeout - **2x** other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to call non-functio -- **1x** other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to compare incompat -- **1x** other: Unhandled exception: \"Not callable: nil (kont=10 frames)\ +- **1x** other: Unhandled exception: \"Unhandled exception: \\\"lua: concat on list and string\\ ## Per-test results @@ -17,25 +16,25 @@ fail=13 timeout=3 skip=8 total=24 |---|---|---|---:| | all.lua | skip | driver uses dofile to chain other tests | 0 | | api.lua | skip | requires testC (C debug library) | 0 | -| attrib.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 6000 | -| big.lua | timeout | per-test timeout | 8006 | -| calls.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 4946 | +| attrib.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 6225 | +| big.lua | timeout | per-test timeout | 8008 | +| calls.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 5175 | | checktable.lua | skip | internal debug helpers | 0 | -| closure.lua | timeout | per-test timeout | 8003 | +| closure.lua | timeout | per-test timeout | 8007 | | code.lua | skip | bytecode inspection via debug library | 0 | -| constructs.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to compare incompat | 4533 | +| constructs.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: concat on list and string\\ | 4874 | | db.lua | skip | debug library | 0 | -| errors.lua | fail | other: Unhandled exception: \"Not callable: nil (kont=10 frames)\ | 3217 | -| events.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 7215 | +| errors.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 3289 | +| events.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 7546 | | files.lua | skip | io library | 0 | | gc.lua | skip | collectgarbage / finalisers | 0 | -| literals.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 1901 | -| locals.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to call non-functio | 1693 | +| literals.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 1888 | +| locals.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to call non-functio | 1721 | | main.lua | skip | standalone interpreter driver | 0 | -| math.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 4095 | -| nextvar.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 7177 | -| pm.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 6048 | -| sort.lua | timeout | per-test timeout | 8007 | -| strings.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 3990 | -| vararg.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 2249 | -| verybig.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to call non-functio | 572 | +| math.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 4231 | +| nextvar.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 7290 | +| pm.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 6155 | +| sort.lua | timeout | per-test timeout | 8006 | +| strings.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 4215 | +| vararg.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"assertion failed!\\\"\ | 2288 | +| verybig.lua | fail | other: Unhandled exception: \"Unhandled exception: \\\"lua: attempt to call non-functio | 622 | diff --git a/lib/lua/test.sh b/lib/lua/test.sh index c9f8908c..d4f58133 100755 --- a/lib/lua/test.sh +++ b/lib/lua/test.sh @@ -936,6 +936,14 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1842) (eval "(lua-eval-ast \"if -2^2 == -4 then return 1 else return 0 end\")") +;; ── Early-return inside nested block (guard+raise sentinel) ── +(epoch 1850) +(eval "(lua-eval-ast \"local function f(n) if n < 0 then return -1 end return n * 2 end return f(-5)\")") +(epoch 1851) +(eval "(lua-eval-ast \"local function f(n) if n < 0 then return -1 end return n * 2 end return f(7)\")") +(epoch 1852) +(eval "(lua-eval-ast \"function f(i) if type(i) ~= \\\"number\\\" then return i, \\\"jojo\\\" end if i > 0 then return i, f(i-1) end end local a, b = f(3) return a\")") + EPOCHS OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1414,6 +1422,11 @@ check 1840 "-2^2 = -4" '-4' check 1841 "2^3^2 = 512 (right-assoc)" '512' check 1842 "-2^2 == -4 true" '1' +# ── Early-return inside nested block ───────────────────────── +check 1850 "early return negative path" '-1' +check 1851 "non-early return path" '14' +check 1852 "nested early-return recursion" '3' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL Lua-on-SX tests passed" diff --git a/lib/lua/transpile.sx b/lib/lua/transpile.sx index 8c5e39a4..c06824e1 100644 --- a/lib/lua/transpile.sx +++ b/lib/lua/transpile.sx @@ -187,6 +187,17 @@ (make-symbol "arg") (list (make-symbol "lua-varargs-arg-table") (make-symbol "__args") n)))) +(define + lua-tx-function-guard + (fn (body-sx) + (list + (make-symbol "guard") + (list (make-symbol "e") + (list + (list (make-symbol "lua-return-sentinel?") (make-symbol "e")) + (list (make-symbol "lua-return-value") (make-symbol "e")))) + body-sx))) + (define lua-tx-function (fn @@ -200,7 +211,7 @@ (list (make-symbol "fn") (list (make-symbol "&rest") (make-symbol "__args")) - (lua-tx body))) + (lua-tx-function-guard (lua-tx body)))) (else (let ((bindings (lua-tx-function-bindings params 0))) @@ -215,10 +226,11 @@ (list (make-symbol "fn") (list (make-symbol "&rest") (make-symbol "__args")) - (list - (make-symbol "let") - all-bindings - (lua-tx body)))))))))) + (lua-tx-function-guard + (list + (make-symbol "let") + all-bindings + (lua-tx body))))))))))) (define lua-tx-block @@ -460,13 +472,18 @@ (node) (let ((exps (nth node 1))) - (cond - ((= (len exps) 0) nil) - ((= (len exps) 1) (lua-tx (first exps))) - (else - (list - (make-symbol "lua-pack-return") - (cons (make-symbol "list") (lua-tx-multi-args exps 0)))))))) + (let + ((val + (cond + ((= (len exps) 0) nil) + ((= (len exps) 1) (lua-tx (first exps))) + (else + (list + (make-symbol "lua-pack-return") + (cons (make-symbol "list") (lua-tx-multi-args exps 0))))))) + (list + (make-symbol "raise") + (list (make-symbol "list") (list (make-symbol "quote") (make-symbol "lua-ret")) val)))))) (define lua-tx-local-function @@ -500,7 +517,9 @@ (define lua-eval-ast - (fn (src) (let ((sx (lua-transpile src))) (eval-expr sx)))) + (fn (src) + (let ((sx (lua-transpile src))) + (eval-expr (lua-tx-function-guard sx))))) (define lua-tx-multi-args diff --git a/plans/lua-on-sx.md b/plans/lua-on-sx.md index 29a28e5d..4a6da9fd 100644 --- a/plans/lua-on-sx.md +++ b/plans/lua-on-sx.md @@ -82,6 +82,7 @@ Each item: implement → tests → tick box → update progress log. _Newest first. Agent appends on every commit._ +- 2026-04-24: lua: scoreboard iteration — **proper early-return via guard+raise sentinel**. Fixes long-logged limitation: `if cond then return X end ...rest` now exits the enclosing function; `rest` is skipped. `lua-tx-return` raises `(list 'lua-ret value)`; every function body and the top-level chunk + loadstring'd chunks wrap in a guard that catches the sentinel and returns its value. Eliminates "compare incompatible types" from constructs.lua (past line 40). 368/368 green (+3 early-return tests). - 2026-04-24: lua: scoreboard iteration — **unary-minus / `^` precedence fix**. Per Lua spec, `^` binds tighter than unary `-`, so `-2^2` should parse as `-(2^2) = -4`, not `(-2)^2 = 4`. My parser recursed into `parse-unary` and then let `^` bind to the already-negated operand. Added `parse-pow-chain` helper and changed the `else` branch of `parse-unary` to parse a primary + `^`-chain before returning; unary operators now wrap the full `^`-chain. Fixed `constructs.lua` past assert #3 (moved to compare-incompatible). 365/365 green (+3 precedence tests). - 2026-04-24: lua: scoreboard iteration — `lua-byte-to-char` regression fix. My previous change returned 2-char strings (`"\a"` etc.) for bytes that SX string literals can't express (0, 7, 8, 11, 12, 14–31, 127+), breaking `'a\0a'` length from 3 → 4. Now only 9/10/13 and printable 32-126 produce real bytes; others use a single `"?"` placeholder so `string.len` stays correct. literals.lua back to failing at assert #4 (was regressed to #2). - 2026-04-24: lua: scoreboard iteration — **decimal string escapes** `\ddd` (1-3 digits). Tokenizer `read-string` previously fell through to literal for digits, so `"\65"` came out as `"65"` not `"A"`. Added `read-decimal-escape!` consuming up to 3 digits while keeping value ≤255, plus `\a`/`\b`/`\f`/`\v` control escapes and `lua-byte-to-char` ASCII lookup. 362 tests (+2 escape tests). @@ -133,4 +134,4 @@ _Shared-file issues that need someone else to fix. Minimal repro only._ - **`string.find`/`match`/`gmatch`/`gsub` patterns are LITERAL only** — no `%d`/`%a`/`.`/`*`/`+`/etc. Implementing Lua patterns is a separate work item; literal search covers the common case. - **`string.format`** supports only `%s`, `%d`, `%f`, `%%`. No width/precision flags (`%.2f`, `%5d`). - **`string.char`** supports printable ASCII 32–126 plus `\t`/`\n`/`\r`; other codes error. -- **Early `return` inside nested block** — `if cond then return nil end ...rest` doesn't exit the enclosing function; `rest` runs anyway. Use `if cond then return X else return Y end` instead. Likely needs guard+raise sentinel for proper fix. +- ~~Early `return` inside nested block~~ — **FIXED 2026-04-24** via guard+raise sentinel (`lua-ret`). All function bodies and the top-level chunk wrap in a guard that catches the return-sentinel; `return` statements raise it.