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
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user