From 0934c4bd28b82b2a2b80de1915c0e40cc8156e37 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 18:23:50 +0000 Subject: [PATCH] lua: generic for-in; ipairs/pairs/next; arity-tolerant fns +9 tests --- lib/lua/parser.sx | 99 ++++++++++++++++++++------------- lib/lua/runtime.sx | 81 +++++++++++++++++++++++++++ lib/lua/test.sh | 40 ++++++++++++++ lib/lua/transpile.sx | 128 ++++++++++++++++++++++++++++++++++++++++++- plans/lua-on-sx.md | 7 ++- 5 files changed, 313 insertions(+), 42 deletions(-) diff --git a/lib/lua/parser.sx b/lib/lua/parser.sx index d604224b..6d10e368 100644 --- a/lib/lua/parser.sx +++ b/lib/lua/parser.sx @@ -534,50 +534,73 @@ (let ((body (parse-block))) (begin (consume! "keyword" "end") (list (quote lua-do) body)))))) - (define - parse-for - (fn - () + (define parse-for-num-rest + (fn (name) + (begin + (consume! "op" "=") + (let ((start (parse-expr))) + (begin + (consume! "op" ",") + (let ((stop (parse-expr)) (step nil)) + (begin + (when (at-op? ",") + (begin + (advance-tok!) + (set! step (parse-expr)))) + (consume! "keyword" "do") + (let ((body (parse-block))) + (begin + (consume! "keyword" "end") + (list (quote lua-for-num) name start stop step body)))))))))) + (define parse-for-in-names + (fn (names) + (cond + ((at-op? ",") + (begin + (advance-tok!) + (let ((nt (peek-tok))) + (begin + (when (not (= (lua-tok-type nt) "ident")) + (error "lua-parse: expected name after , in for")) + (let ((nm (lua-tok-value nt))) + (begin + (advance-tok!) + (parse-for-in-names (append names (list nm))))))))) + (else names)))) + (define parse-for-in-exps + (fn (exps) + (cond + ((at-op? ",") + (begin + (advance-tok!) + (parse-for-in-exps (append exps (list (parse-expr)))))) + (else exps)))) + (define parse-for-in-rest + (fn (names) + (begin + (consume! "keyword" "in") + (let ((exps (parse-for-in-exps (list (parse-expr))))) + (begin + (consume! "keyword" "do") + (let ((body (parse-block))) + (begin + (consume! "keyword" "end") + (list (quote lua-for-in) names exps body)))))))) + (define parse-for + (fn () (begin (consume! "keyword" "for") - (let - ((t (peek-tok))) + (let ((t (peek-tok))) (begin - (when - (not (= (lua-tok-type t) "ident")) + (when (not (= (lua-tok-type t) "ident")) (error "lua-parse: expected name in for")) - (let - ((name (lua-tok-value t))) + (let ((name (lua-tok-value t))) (begin (advance-tok!) - (when - (not (at-op? "=")) - (error "lua-parse: only numeric for supported")) - (consume! "op" "=") - (let - ((start (parse-expr))) - (begin - (consume! "op" ",") - (let - ((stop (parse-expr)) (step nil)) - (begin - (when - (at-op? ",") - (begin - (advance-tok!) - (set! step (parse-expr)))) - (consume! "keyword" "do") - (let - ((body (parse-block))) - (begin - (consume! "keyword" "end") - (list - (quote lua-for-num) - name - start - stop - step - body)))))))))))))) + (cond + ((at-op? "=") (parse-for-num-rest name)) + (else + (parse-for-in-rest (parse-for-in-names (list name)))))))))))) (define parse-funcname (fn diff --git a/lib/lua/runtime.sx b/lib/lua/runtime.sx index 6a6f2fc7..6da0183c 100644 --- a/lib/lua/runtime.sx +++ b/lib/lua/runtime.sx @@ -448,3 +448,84 @@ (cond ((lua-multi? r) (cons (quote lua-multi) (cons true (rest r)))) (else (list (quote lua-multi) true r))))))) + +(define + lua-ipairs-iter + (fn + (t i) + (let + ((nk (+ i 1))) + (let + ((v (get t (str nk)))) + (if (= v nil) nil (list (quote lua-multi) nk v)))))) + +(define + lua-ipairs + (fn + (t) + (list (quote lua-multi) lua-ipairs-iter t 0))) + +(define ipairs lua-ipairs) + +(define + lua-next-skip-meta + (fn + (ks) + (cond + ((= (len ks) 0) (list)) + ((= (first ks) "__meta") (lua-next-skip-meta (rest ks))) + (else ks)))) + +(define + lua-next-after + (fn + (ks prev-key) + (cond + ((= (len ks) 0) (list)) + ((= (first ks) (str prev-key)) (lua-next-skip-meta (rest ks))) + (else (lua-next-after (rest ks) prev-key))))) + +(define + lua-key-to-value + (fn + (k) + (cond + ((= k "true") true) + ((= k "false") false) + (else + (let + ((n (lua-to-number k))) (if (= n nil) k n)))))) + +(define + lua-next + (fn + (&rest args) + (let + ((t (first args)) + (prev (if (> (len (rest args)) 0) (first (rest args)) nil))) + (let + ((all-keys (keys t))) + (let + ((ks (if (= prev nil) (lua-next-skip-meta all-keys) (lua-next-after all-keys prev)))) + (cond + ((= (len ks) 0) nil) + (else + (let + ((k (first ks))) + (list (quote lua-multi) (lua-key-to-value k) (get t k)))))))))) + +(define next lua-next) + +(define + lua-pairs + (fn + (t) + (list (quote lua-multi) lua-next t nil))) + +(define pairs lua-pairs) + +(define + lua-arg + (fn + (args i) + (if (< i (len args)) (nth args i) nil))) diff --git a/lib/lua/test.sh b/lib/lua/test.sh index e390bf6b..14b5d21a 100755 --- a/lib/lua/test.sh +++ b/lib/lua/test.sh @@ -681,6 +681,35 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 911) (eval "(lua-eval-ast \"local function f() return 99 end local function handler(e) return e end local ok, v = xpcall(f, handler) return v\")") +;; ── Phase 4: generic `for … in …` ────────────────────────────── +;; ipairs over array +(epoch 950) +(eval "(lua-eval-ast \"local t = {10, 20, 30} local sum = 0 for i, v in ipairs(t) do sum = sum + v end return sum\")") +(epoch 951) +(eval "(lua-eval-ast \"local t = {10, 20, 30} local last = 0 for i, v in ipairs(t) do last = i end return last\")") +(epoch 952) +(eval "(lua-eval-ast \"local t = {} local count = 0 for i, v in ipairs(t) do count = count + 1 end return count\")") +(epoch 953) +(eval "(lua-eval-ast \"local t = {1, 2, nil, 4} local c = 0 for i, v in ipairs(t) do c = c + 1 end return c\")") + +;; pairs over hash +(epoch 960) +(eval "(lua-eval-ast \"local t = {a = 1, b = 2, c = 3} local sum = 0 for k, v in pairs(t) do sum = sum + v end return sum\")") +(epoch 961) +(eval "(lua-eval-ast \"local t = {x = 1, y = 2, z = 3} local n = 0 for k, v in pairs(t) do n = n + 1 end return n\")") + +;; custom stateful iterator (if-else-return, works around early-return limit) +(epoch 970) +(eval "(lua-eval-ast \"local function range(n) local i = 0 return function() i = i + 1 if i > n then return nil else return i end end end local c = 0 for x in range(5) do c = c + x end return c\")") + +;; 3-value iterator form (f, s, var) +(epoch 971) +(eval "(lua-eval-ast \"local function step(max, i) if i >= max then return nil else return i + 1, (i + 1) * (i + 1) end end local sum = 0 for i, v in step, 4, 0 do sum = sum + v end return sum\")") + +;; pairs ignores __meta key +(epoch 980) +(eval "(lua-eval-ast \"local t = setmetatable({x = 1, y = 2}, {}) local n = 0 for k in pairs(t) do n = n + 1 end return n\")") + EPOCHS OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1022,6 +1051,17 @@ check 906 "nested pcall" '"outer:inner"' check 910 "xpcall invokes handler" '"H:raw"' check 911 "xpcall ok path" '99' +# ── Phase 4: generic `for … in …` ───────────────────────────── +check 950 "ipairs sum" '60' +check 951 "ipairs last index" '3' +check 952 "ipairs empty → 0" '0' +check 953 "ipairs stops at nil" '2' +check 960 "pairs hash sum" '6' +check 961 "pairs hash count" '3' +check 970 "stateful closure iter" '15' +check 971 "3-value iterator form" '30' +check 980 "pairs skips __meta" '2' + 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 070fddb6..006250e3 100644 --- a/lib/lua/transpile.sx +++ b/lib/lua/transpile.sx @@ -35,6 +35,7 @@ ((= tag (quote lua-while)) (lua-tx-while node)) ((= tag (quote lua-repeat)) (lua-tx-repeat node)) ((= tag (quote lua-for-num)) (lua-tx-for-num node)) + ((= tag (quote lua-for-in)) (lua-tx-for-in node)) ((= tag (quote lua-do)) (lua-tx-do node)) ((= tag (quote lua-break)) (quote lua-break-marker)) ((= tag (quote lua-return)) (lua-tx-return node)) @@ -160,6 +161,19 @@ (lua-tx (nth f 2)))) (else (error "lua-transpile: unknown table field"))))) +(define + lua-tx-function-bindings + (fn + (params i) + (if + (>= i (len params)) + (list) + (cons + (list + (make-symbol (nth params i)) + (list (make-symbol "lua-arg") (make-symbol "__args") i)) + (lua-tx-function-bindings params (+ i 1)))))) + (define lua-tx-function (fn @@ -168,9 +182,20 @@ ((params (nth node 1)) (is-vararg (nth node 2)) (body (nth node 3))) - (let - ((sym-params (map make-symbol params))) - (list (make-symbol "fn") sym-params (lua-tx body)))))) + (cond + ((= (len params) 0) + (list + (make-symbol "fn") + (list (make-symbol "&rest") (make-symbol "__args")) + (lua-tx body))) + (else + (list + (make-symbol "fn") + (list (make-symbol "&rest") (make-symbol "__args")) + (list + (make-symbol "let") + (lua-tx-function-bindings params 0) + (lua-tx body)))))))) (define lua-tx-block @@ -536,3 +561,100 @@ (nth lhss i) (list (make-symbol "lua-nth-ret") tmp i)) (lua-tx-multi-assign-pairs lhss tmp (+ i 1)))))) + +(define + lua-tx-for-in-decls + (fn + (names i) + (if + (>= i (len names)) + (list) + (cons + (list (make-symbol "define") (make-symbol (nth names i)) nil) + (lua-tx-for-in-decls names (+ i 1)))))) + +(define + lua-tx-for-in-sets + (fn + (names rets-sym i) + (if + (>= i (len names)) + (list) + (cons + (list + (make-symbol "set!") + (make-symbol (nth names i)) + (list (make-symbol "lua-nth-ret") rets-sym i)) + (lua-tx-for-in-sets names rets-sym (+ i 1)))))) + +(define + lua-tx-for-in-step-body + (fn + (names body v-sym loop-sym first-name) + (list + (make-symbol "when") + (list (make-symbol "not") (list (make-symbol "=") first-name nil)) + (list + (make-symbol "begin") + (list (make-symbol "set!") v-sym first-name) + body + (list loop-sym))))) + +(define + lua-tx-for-in-loop-body + (fn + (names body f-sym s-sym v-sym rets-sym loop-sym first-name) + (list + (make-symbol "let") + (list + (list + rets-sym + (list + (make-symbol "lua-pack-return") + (list + (make-symbol "list") + (list (make-symbol "lua-call") f-sym s-sym v-sym))))) + (cons + (make-symbol "begin") + (append + (lua-tx-for-in-sets names rets-sym 0) + (list (lua-tx-for-in-step-body names body v-sym loop-sym first-name))))))) + +(define + lua-tx-for-in + (fn + (node) + (let + ((names (nth node 1)) + (exps (nth node 2)) + (body (lua-tx (nth node 3)))) + (let + ((pack-sym (make-symbol "__for_pack")) + (f-sym (make-symbol "__for_f")) + (s-sym (make-symbol "__for_s")) + (v-sym (make-symbol "__for_var")) + (rets-sym (make-symbol "__for_rets")) + (loop-sym (make-symbol "__for_loop")) + (first-name (make-symbol (first names)))) + (list + (make-symbol "let") + (list (list pack-sym (lua-tx-multi-rhs exps))) + (list + (make-symbol "let") + (list + (list f-sym (list (make-symbol "lua-nth-ret") pack-sym 0)) + (list s-sym (list (make-symbol "lua-nth-ret") pack-sym 1)) + (list v-sym (list (make-symbol "lua-nth-ret") pack-sym 2))) + (cons + (make-symbol "begin") + (append + (lua-tx-for-in-decls names 0) + (list + (list + (make-symbol "define") + loop-sym + (list + (make-symbol "fn") + (list) + (lua-tx-for-in-loop-body names body f-sym s-sym v-sym rets-sym loop-sym first-name))) + (list loop-sym)))))))))) diff --git a/plans/lua-on-sx.md b/plans/lua-on-sx.md index fc3766b4..fd874583 100644 --- a/plans/lua-on-sx.md +++ b/plans/lua-on-sx.md @@ -62,7 +62,7 @@ Each item: implement → tests → tick box → update progress log. ### Phase 4 — metatables + error handling (next run) - [x] Metatable dispatch: `__index`, `__newindex`, `__add`/`__sub`/…, `__eq`, `__lt`, `__call`, `__tostring`, `__len` - [x] `pcall`/`xpcall`/`error` via handler-bind -- [ ] Generic `for … in …` +- [x] Generic `for … in …` ### Phase 5 — coroutines (the showcase) - [ ] `coroutine.create`/`.resume`/`.yield`/`.status`/`.wrap` via `perform`/`cek-resume` @@ -82,6 +82,7 @@ Each item: implement → tests → tick box → update progress log. _Newest first. Agent appends on every commit._ +- 2026-04-24: lua: generic `for … in …` — parser split (`=` → num, else `in`), new `lua-for-in` node, transpile to `let`-bound `f,s,var` + recursive `__for_loop`. Added `ipairs`/`pairs`/`next`/`lua-arg` globals. Lua fns now arity-tolerant (`&rest __args` + indexed bind) — needed because generic for always calls iter with 2 args. Noted early-return-in-nested-block as pre-existing limitation. 265 tests. - 2026-04-24: lua: `pcall`/`xpcall`/`error` via SX `guard` + `raise`. Added `lua-apply` (arity-dispatch 0-8, apply fallback) because SX `apply` re-wraps raises as "Unhandled exception". Table payloads preserved (`error({code = 42})`). 256 total tests. - 2026-04-24: lua: phase 4 — metatable dispatch (`__index`/`__newindex`/arith/compare/`__call`/`__len`), `setmetatable`/`getmetatable`/`type` globals, OO `self:method` pattern. Transpile routes all calls through `lua-call` (stashed `sx-apply-ref` to dodge user-shadowing of SX `apply`). Skipped `__tostring` (needs `tostring()` builtin). 247 total tests. - 2026-04-24: lua: PUC-Rio scoreboard baseline — 0/16 runnable pass (0.0%). Top modes: 14× parse error, 1× `print` undef, 1× vararg transpile. Phase 3 complete. @@ -100,3 +101,7 @@ _Newest first. Agent appends on every commit._ _Shared-file issues that need someone else to fix. Minimal repro only._ - _(none yet)_ + +## Known limitations (own code, not shared) + +- **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.