go: sched.sx — channels + goroutines (v0 synchronous) + 12 tests; Phase 5 starts [shapes-scheduler]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 26s

Phase 5 (goroutines + channels) opens.

lib/go/sched.sx is the **independent implementation** referenced by
plans/lib-guest-scheduler.md — the first-consumer cut whose realised
shape will inform the eventual sister kit.

Channel representation:
  (list :go-chan SEND-FN RECV-FN CLOSED?-FN CLOSE!-FN)
Each closure shares a mutable `buf` (a list mutated via append! and
set!) and a `closed` flag. Channel identity is closure-instance —
two `make()` calls produce distinct values per Go spec § Channel types.

Primitive API in sched.sx:
  go-make-chan / go-chan? / go-chan-send! / go-chan-recv! /
  go-chan-closed? / go-chan-close!

Eval integration in eval.sx:
  * `make` and `close` added as builtins. v0 `make()` takes no args
    and returns an unbounded-buffer channel.
  * `:send` stmt → go-chan-send! on the channel.
  * Unary `<-` recv on channel values → go-chan-recv!. `:empty`
    sentinel converted to nil (stand-in for blocking semantics).
  * `:go expr` → synchronous eval (v0 limitation, see sched.sx
    header).

**v0 concurrency model — synchronous goroutines.** SX doesn't expose
first-class continuations to guest code, so v0 runs `go f()`
immediately and depends on the spawned goroutine running to
completion before the main goroutine receives. This is the right
semantics for the simple producer/consumer patterns covered here.
True preemption with blocking send/recv is Phase 5b — requires either
a CEK-style trampolining eval rewrite or kit-level continuation
support. Logged in sched.sx header and in the sister-plan diary.

Runtime suite (12 tests):
  * 6 direct API tests: identity, FIFO order, closed-flag
  * 6 source-level: make + send + recv, go ping-pong, close,
    multi-goroutine fan-in, worker-with-result

Sister-plan scheduler diary updated with the channel-as-closure-
bundle insight and the v0 synchronous-spawn caveat.

runtime 12/12, total 469/469.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 21:55:41 +00:00
parent 674d8115b8
commit b693854dc4
8 changed files with 282 additions and 11 deletions

View File

@@ -17,11 +17,13 @@
go-env-builtins
;; A starter env containing the Go builtins eval understands.
;; Tests can call (go-env-builtins) instead of go-env-empty when they
;; need len/append/print.
;; need len/append/print/make/close.
(list
(list "len" (list :go-builtin "len"))
(list "append" (list :go-builtin "append"))
(list "print" (list :go-builtin "print"))))
(list "print" (list :go-builtin "print"))
(list "make" (list :go-builtin "make"))
(list "close" (list :go-builtin "close"))))
(define
go-env-lookup
@@ -381,6 +383,18 @@
:else (list :eval-error :append-not-slice slc))))
(= name "print")
nil ;; v0: silent. Real impl would write to stdout.
(= name "make")
;; v0: ignore args, always return a fresh channel. Real Go is
;; make(chan T) / make(chan T, n) / make([]T, n) / make(map[K]V) —
;; v0 channel-buffer is unbounded so cap arg is a no-op.
(go-make-chan)
(= name "close")
(cond
(not (= (len vals) 1))
(list :eval-error :builtin-arity name 1 (len vals))
(not (go-chan? (first vals)))
(list :eval-error :close-not-chan (first vals))
:else (do (go-chan-close! (first vals)) nil))
:else (list :eval-error :unknown-builtin name)))))
(define
@@ -783,6 +797,22 @@
(go-eval-method-decl env stmt)
(and (list? stmt) (= (first stmt) :type-decl))
(go-eval-type-decl env stmt)
(and (list? stmt) (= (first stmt) :send))
(let ((ch (go-eval env (nth stmt 1)))
(v (go-eval env (nth stmt 2))))
(cond
(go-eval-error? ch) ch
(go-eval-error? v) v
(not (go-chan? ch)) (list :eval-error :send-not-chan ch)
:else (do (go-chan-send! ch v) env)))
(and (list? stmt) (= (first stmt) :go))
;; v0: synchronous evaluation — no real preemption. The spawned
;; expression's value is dropped. See sched.sx header for
;; semantic notes.
(let ((v (go-eval env (nth stmt 1))))
(cond
(go-eval-error? v) v
:else env))
:else
(let ((v (go-eval env stmt)))
(cond
@@ -940,13 +970,21 @@
;; Unary prefix op: head is :var with op name + 1 arg.
(and (list? head) (= (first head) :var) (= (len args) 1)
(some (fn (o) (= o (nth head 1)))
(list "-" "+" "!")))
(list "-" "+" "!" "<-")))
(let ((op (nth head 1)) (v (go-eval env (first args))))
(cond
(go-eval-error? v) v
(= op "-") (- 0 v)
(= op "+") v
(= op "!") (not v)
(= op "<-")
(cond
(not (go-chan? v)) (list :eval-error :recv-not-chan v)
:else
(let ((r (go-chan-recv! v)))
;; :empty in v0 means "no value yet" — Go would block.
;; We return nil as a stand-in for the zero value.
(cond (= r :empty) nil :else r)))
:else (list :eval-error :unsupported-unary op)))
;; Method-call shape: head is (:select OBJ METHOD-NAME).
(and (list? head) (= (first head) :select))