diff --git a/lib/lua/runtime.sx b/lib/lua/runtime.sx index 6da0183c..13cd6ad1 100644 --- a/lib/lua/runtime.sx +++ b/lib/lua/runtime.sx @@ -529,3 +529,125 @@ (fn (args i) (if (< i (len args)) (nth args i) nil))) + +;; ── Coroutines (call/cc based) ──────────────────────────────── +(define __current-co nil) + +(define + lua-coroutine-create + (fn + (f) + (let ((co {})) + (begin + (dict-set! co "__co" true) + (dict-set! co "status" "suspended") + (dict-set! co "body" f) + (dict-set! co "resume-k" nil) + (dict-set! co "caller-k" nil) + co)))) + +(define + lua-coroutine-status + (fn + (co) + (if (and (= (type-of co) "dict") (has-key? co "__co")) + (get co "status") + (error "lua: not a coroutine")))) + +(define + lua-co-wrap-result + (fn (r) + (cond + ((lua-multi? r) (cons (quote lua-multi) (cons true (rest r)))) + (else (list (quote lua-multi) true r))))) + +(define + lua-co-first-call + (fn (co rvals prev) + (let ((r (lua-apply (get co "body") rvals))) + (begin + (dict-set! co "status" "dead") + (set! __current-co prev) + ((get co "caller-k") (lua-co-wrap-result r)))))) + +(define + lua-co-continue-call + (fn (co rvals) + (let ((rk (get co "resume-k"))) + (begin + (dict-set! co "resume-k" nil) + (rk (if (> (len rvals) 0) (first rvals) nil)))))) + +(define + lua-coroutine-resume + (fn + (&rest args) + (let ((co (first args)) (rvals (rest args))) + (cond + ((not (and (= (type-of co) "dict") (has-key? co "__co"))) + (list (quote lua-multi) false "not a coroutine")) + ((= (get co "status") "dead") + (list (quote lua-multi) false "cannot resume dead coroutine")) + ((= (get co "status") "running") + (list (quote lua-multi) false "cannot resume running coroutine")) + (else + (call/cc + (fn (k) + (let ((prev __current-co)) + (begin + (dict-set! co "caller-k" k) + (dict-set! co "status" "running") + (set! __current-co co) + (guard + (e (true + (begin + (dict-set! co "status" "dead") + (set! __current-co prev) + (list (quote lua-multi) false e)))) + (cond + ((= (get co "resume-k") nil) (lua-co-first-call co rvals prev)) + (else (lua-co-continue-call co rvals))))))))))))) + +(define + lua-coroutine-yield + (fn + (&rest yvals) + (cond + ((= __current-co nil) (error "lua: attempt to yield from outside a coroutine")) + (else + (call/cc + (fn (k) + (let ((co __current-co)) + (begin + (dict-set! co "resume-k" k) + (dict-set! co "status" "suspended") + (set! __current-co nil) + ((get co "caller-k") (cons (quote lua-multi) (cons true yvals))))))))))) + +(define + lua-co-wrap-caller + (fn (co args) + (let ((r (sx-apply-ref lua-coroutine-resume (cons co args)))) + (cond + ((and (lua-multi? r) (> (len r) 1) (= (nth r 1) true)) + (cond + ((<= (len r) 2) nil) + ((= (len r) 3) (nth r 2)) + (else (cons (quote lua-multi) (rest (rest r)))))) + ((and (lua-multi? r) (> (len r) 1)) + (error (if (> (len r) 2) (nth r 2) "coroutine error"))) + (else nil))))) + +(define + lua-coroutine-wrap + (fn (f) + (let ((co (lua-coroutine-create f))) + (fn (&rest args) (lua-co-wrap-caller co args))))) + +(define coroutine {}) + +(dict-set! coroutine "create" lua-coroutine-create) +(dict-set! coroutine "resume" lua-coroutine-resume) +(dict-set! coroutine "yield" lua-coroutine-yield) +(dict-set! coroutine "status" lua-coroutine-status) +(dict-set! coroutine "wrap" lua-coroutine-wrap) diff --git a/lib/lua/test.sh b/lib/lua/test.sh index 14b5d21a..faff8f4d 100755 --- a/lib/lua/test.sh +++ b/lib/lua/test.sh @@ -710,6 +710,24 @@ cat > "$TMPFILE" << 'EPOCHS' (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\")") +;; ── Phase 5: coroutines ──────────────────────────────────────── +(epoch 1000) +(eval "(lua-eval-ast \"local co = coroutine.create(function() end) return coroutine.status(co)\")") +(epoch 1001) +(eval "(lua-eval-ast \"local co = coroutine.create(function() return 42 end) coroutine.resume(co) return coroutine.status(co)\")") +(epoch 1010) +(eval "(lua-eval-ast \"local co = coroutine.create(function() coroutine.yield(1) coroutine.yield(2) return 3 end) local ok1, v1 = coroutine.resume(co) local ok2, v2 = coroutine.resume(co) local ok3, v3 = coroutine.resume(co) return v1 * 100 + v2 * 10 + v3\")") +(epoch 1011) +(eval "(lua-eval-ast \"local co = coroutine.create(function(a, b) return a + b end) local ok, v = coroutine.resume(co, 10, 20) return v\")") +(epoch 1012) +(eval "(lua-eval-ast \"local co = coroutine.create(function() local x = coroutine.yield() return x + 100 end) coroutine.resume(co) local ok, v = coroutine.resume(co, 42) return v\")") +(epoch 1020) +(eval "(lua-eval-ast \"local co = coroutine.create(function() return 42 end) coroutine.resume(co) local ok, err = coroutine.resume(co) if ok then return \\\"no\\\" else return err end\")") +(epoch 1030) +(eval "(lua-eval-ast \"local gen = coroutine.wrap(function() coroutine.yield(1) coroutine.yield(2) coroutine.yield(3) end) return gen() + gen() + gen()\")") +(epoch 1040) +(eval "(lua-eval-ast \"local function iter() coroutine.yield(10) coroutine.yield(20) coroutine.yield(30) end local co = coroutine.create(iter) local sum = 0 for i = 1, 3 do local ok, v = coroutine.resume(co) sum = sum + v end return sum\")") + EPOCHS OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1062,6 +1080,16 @@ check 970 "stateful closure iter" '15' check 971 "3-value iterator form" '30' check 980 "pairs skips __meta" '2' +# ── Phase 5: coroutines ──────────────────────────────────────── +check 1000 "coroutine.status initial" '"suspended"' +check 1001 "coroutine.status after done" '"dead"' +check 1010 "yield/resume × 3 sequence" '123' +check 1011 "resume passes args to body" '30' +check 1012 "resume passes args via yield" '142' +check 1020 "resume dead returns error" '"cannot resume dead coroutine"' +check 1030 "coroutine.wrap" '6' +check 1040 "iterator via coroutine" '60' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL Lua-on-SX tests passed" diff --git a/plans/lua-on-sx.md b/plans/lua-on-sx.md index fd874583..a59360be 100644 --- a/plans/lua-on-sx.md +++ b/plans/lua-on-sx.md @@ -65,7 +65,7 @@ Each item: implement → tests → tick box → update progress log. - [x] Generic `for … in …` ### Phase 5 — coroutines (the showcase) -- [ ] `coroutine.create`/`.resume`/`.yield`/`.status`/`.wrap` via `perform`/`cek-resume` +- [x] `coroutine.create`/`.resume`/`.yield`/`.status`/`.wrap` via `perform`/`cek-resume` ### Phase 6 — standard library - [ ] `string` — `format`, `sub`, `find`, `match`, `gmatch`, `gsub`, `len`, `rep`, `upper`, `lower`, `byte`, `char` @@ -82,6 +82,7 @@ Each item: implement → tests → tick box → update progress log. _Newest first. Agent appends on every commit._ +- 2026-04-24: lua: phase 5 — coroutines (create/resume/yield/status/wrap) via `call/cc` (perform/cek-resume not exposed to SX userland). Handles multi-yield + final return + arg passthrough. Fix: body's final return must jump via `caller-k` to the **current** resume's caller, not unwind through the stale first-call continuation. 273 tests. - 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.