lua: coroutines (create/resume/yield/status/wrap) via call/cc +8 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 18:36:41 +00:00
parent 0934c4bd28
commit a5947e1295
3 changed files with 152 additions and 1 deletions

View File

@@ -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)

View File

@@ -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"

View File

@@ -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.