go: eval.sx + sched.sx — select stmt evaluation + 6 tests [nothing]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 28s

Phase 5 cont. Adds `select` statement evaluation:

  go-select-try-case env COMM →
    :not-ready / extended-env / :eval-error
  go-select-pick env CASES DEFAULT-OR-NIL →
    body-result / blocked-error
  go-eval-select-stmt env STMT  — public entry

Walks cases in declared order:
  * :send case — always ready in v0 (unbounded buffer). Sends value
    via go-chan-send! and returns env unchanged.
  * :short-decl / :assign case — RHS expected to be unary <- on a
    channel. Ready iff go-chan-len > 0; on success, recv-into-var
    binds the new value in env.
  * Bare recv (:app (:var "<-") [CHAN]) — ready iff len > 0; consumes
    the value (discarded).
  * :default — deferred until end of walk. Runs if no other case
    ready. Absence + no ready case → (:eval-error :select-blocked-
    no-default).

New `go-chan-len` accessor on the channel closure-bundle so the
select can peek without consuming.

Subtle bug fix: the :select stmt branch in go-eval-stmt was returning
the old env instead of the env returned by the case body. Assignments
inside select cases (`select { case <-ch: x = 1 ; default: x = 99 }`)
now stick.

Tests (6):
  default fires when no case ready
  recv case fires when ready
  recv-into-var binds the value
  send case always ready
  picks first ready case (deterministic order in v0)
  no default + nothing ready → blocked error
  combined with goroutine fan-in

runtime 18/18, total 475/475.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 22:03:17 +00:00
parent b693854dc4
commit 4807bc9c58
6 changed files with 183 additions and 7 deletions

View File

@@ -813,12 +813,119 @@
(cond (cond
(go-eval-error? v) v (go-eval-error? v) v
:else env)) :else env))
(and (list? stmt) (= (first stmt) :select))
(let ((r (go-eval-select-stmt env stmt)))
(cond
(go-eval-error? r) r
(and (list? r) (= (first r) :return-value)) r
(= r :break) r
(= r :continue) r
;; Otherwise r is the env after the selected body ran;
;; propagate so assignments inside cases stick.
:else r))
:else :else
(let ((v (go-eval env stmt))) (let ((v (go-eval env stmt)))
(cond (cond
(go-eval-error? v) v (go-eval-error? v) v
:else env))))) :else env)))))
(define
go-select-try-case
;; Returns:
;; :not-ready — case can't proceed (recv on empty channel)
;; env-or-extended-env — case ran; for recv-into-decl/assign, env
;; carries the new binding
;; :eval-error sentinel
(fn (env comm)
(cond
;; Send case (always ready in v0 with unbounded buffer)
(and (list? comm) (= (first comm) :send))
(let ((ch (go-eval env (nth comm 1)))
(v (go-eval env (nth comm 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)))
;; Recv-into-var: x := <-ch / x = <-ch
(and (list? comm)
(or (= (first comm) :short-decl) (= (first comm) :assign)))
(let ((lhs-list (nth comm 1)) (exprs (nth comm 2)))
(cond
(not (= (len exprs) 1)) :not-ready
:else
(let ((rhs (first exprs)))
(cond
(not (and (list? rhs) (= (first rhs) :app)
(list? (nth rhs 1)) (= (first (nth rhs 1)) :var)
(= (nth (nth rhs 1) 1) "<-")
(= (len (nth rhs 2)) 1)))
:not-ready
:else
(let ((ch (go-eval env (first (nth rhs 2)))))
(cond
(go-eval-error? ch) ch
(not (go-chan? ch)) (list :eval-error :recv-not-chan ch)
(= (go-chan-len ch) 0) :not-ready
:else
(let ((v (go-chan-recv! ch)))
(cond
(= v :empty) :not-ready
:else
(let ((names (map (fn (lhs)
(cond
(and (list? lhs)
(= (first lhs) :var))
(nth lhs 1)
:else :unknown))
lhs-list)))
(cond
(= (len names) 0) env
:else
(go-env-extend env (first names) v)))))))))))
;; Bare recv: (:app (:var "<-") [CHAN])
(and (list? comm) (= (first comm) :app)
(list? (nth comm 1)) (= (first (nth comm 1)) :var)
(= (nth (nth comm 1) 1) "<-")
(= (len (nth comm 2)) 1))
(let ((ch (go-eval env (first (nth comm 2)))))
(cond
(go-eval-error? ch) ch
(not (go-chan? ch)) (list :eval-error :recv-not-chan ch)
(= (go-chan-len ch) 0) :not-ready
:else (do (go-chan-recv! ch) env)))
:else :not-ready)))
(define
go-select-pick
;; Walk cases in order. First :select-case whose comm-stmt is ready
;; wins. If none ready and a :default was seen, run it. Otherwise
;; :select-blocked-no-default.
(fn (env cases default-case)
(cond
(or (= cases nil) (= (len cases) 0))
(cond
(= default-case nil) (list :eval-error :select-blocked-no-default)
:else (go-eval-block env (nth default-case 1)))
:else
(let ((c (first cases)))
(cond
(and (list? c) (= (first c) :default))
(go-select-pick env (rest cases) c)
(and (list? c) (= (first c) :select-case))
(let ((maybe-env (go-select-try-case env (nth c 1))))
(cond
(= maybe-env :not-ready)
(go-select-pick env (rest cases) default-case)
(go-eval-error? maybe-env) maybe-env
:else (go-eval-block maybe-env (nth c 2))))
:else (go-select-pick env (rest cases) default-case))))))
(define
go-eval-select-stmt
(fn (env stmt)
(go-select-pick env (nth stmt 1) nil)))
(define (define
go-eval-method-decl go-eval-method-decl
;; (:method-decl RECV NAME PARAMS RESULTS BODY) — register the method ;; (:method-decl RECV NAME PARAMS RESULTS BODY) — register the method

View File

@@ -50,7 +50,8 @@
:empty :else :empty :else
(let ((v (first buf))) (set! buf (rest buf)) v))) (let ((v (first buf))) (set! buf (rest buf)) v)))
(fn () closed) (fn () closed)
(fn () (set! closed true) nil))))) (fn () (set! closed true) nil)
(fn () (len buf))))))
(define (define
go-chan? go-chan?
@@ -62,3 +63,4 @@
(define go-chan-recv! (fn (ch) ((nth ch 2)))) (define go-chan-recv! (fn (ch) ((nth ch 2))))
(define go-chan-closed? (fn (ch) ((nth ch 3)))) (define go-chan-closed? (fn (ch) ((nth ch 3))))
(define go-chan-close! (fn (ch) ((nth ch 4)))) (define go-chan-close! (fn (ch) ((nth ch 4))))
(define go-chan-len (fn (ch) ((nth ch 5))))

View File

@@ -1,13 +1,13 @@
{ {
"language": "go", "language": "go",
"total_pass": 469, "total_pass": 475,
"total": 469, "total": 475,
"suites": [ "suites": [
{"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"lex","pass":129,"total":129,"status":"ok"},
{"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"parse","pass":176,"total":176,"status":"ok"},
{"name":"types","pass":72,"total":72,"status":"ok"}, {"name":"types","pass":72,"total":72,"status":"ok"},
{"name":"eval","pass":80,"total":80,"status":"ok"}, {"name":"eval","pass":80,"total":80,"status":"ok"},
{"name":"runtime","pass":12,"total":12,"status":"ok"}, {"name":"runtime","pass":18,"total":18,"status":"ok"},
{"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"},
{"name":"e2e","pass":0,"total":0,"status":"pending"} {"name":"e2e","pass":0,"total":0,"status":"pending"}
] ]

View File

@@ -1,6 +1,6 @@
# Go-on-SX Scoreboard # Go-on-SX Scoreboard
**Total: 469 / 469 tests passing** **Total: 475 / 475 tests passing**
| | Suite | Pass | Total | | | Suite | Pass | Total |
|---|---|---|---| |---|---|---|---|
@@ -8,7 +8,7 @@
| ✅ | parse | 176 | 176 | | ✅ | parse | 176 | 176 |
| ✅ | types | 72 | 72 | | ✅ | types | 72 | 72 |
| ✅ | eval | 80 | 80 | | ✅ | eval | 80 | 80 |
| ✅ | runtime | 12 | 12 | | ✅ | runtime | 18 | 18 |
| ⬜ | stdlib | 0 | 0 | | ⬜ | stdlib | 0 | 0 |
| ⬜ | e2e | 0 | 0 | | ⬜ | e2e | 0 | 0 |

View File

@@ -103,6 +103,55 @@
20) 20)
;; ── report ───────────────────────────────────────────────────── ;; ── report ─────────────────────────────────────────────────────
(go-rt-test
"select: default runs when no case is ready"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "x := 0") (go-parse "select { case <-ch: x = 1 ; default: x = 99 }")))))
(go-env-lookup env "x"))
99)
(go-rt-test
"select: recv case fires when ready"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "ch <- 7") (go-parse "x := 0") (go-parse "select { case <-ch: x = 1 ; default: x = 99 }")))))
(go-env-lookup env "x"))
1)
(go-rt-test
"select: recv-into-var binds the value"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "ch <- 42") (go-parse "select { case v := <-ch: v }")))))
(go-env-lookup env "v"))
42)
(go-rt-test
"select: send case (always ready in v0)"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "select { case ch <- 5: }")))))
(go-chan-len (go-env-lookup env "ch")))
1)
(go-rt-test
"select: picks first ready case"
(let
((env (go-eval-program go-env-builtins (list (go-parse "a := make()") (go-parse "b := make()") (go-parse "b <- 100") (go-parse "x := 0") (go-parse "select { case <-a: x = 1 ; case <-b: x = 2 ; default: x = 99 }")))))
(go-env-lookup env "x"))
2)
(go-rt-test
"select: no default + nothing ready → blocked error"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()")))))
(go-eval-stmt env (go-parse "select { case <-ch: }") (list)))
(list :eval-error :select-blocked-no-default))
(go-rt-test
"select: combined with goroutine fan-in"
(let
((env (go-eval-program go-env-builtins (list (go-parse "func push(c chan int, v int) { c <- v }") (go-parse "ch := make()") (go-parse "go push(ch, 7)") (go-parse "result := 0") (go-parse "select { case v := <-ch: result = v ; default: result = -1 }")))))
(go-env-lookup env "result"))
7)
(define (define
go-rt-test-summary go-rt-test-summary
(str "runtime " go-rt-test-pass "/" go-rt-test-count)) (str "runtime " go-rt-test-pass "/" go-rt-test-count))

View File

@@ -313,7 +313,13 @@ Progress-log line → push `origin/loops/go`.
sched.sx header). sched.sx header).
- [ ] Real preemption (suspending sends on full buffer / recvs on empty). - [ ] Real preemption (suspending sends on full buffer / recvs on empty).
Requires reified execution state; deferred to Phase 5b. Requires reified execution state; deferred to Phase 5b.
- [ ] `select { case ... }` multiplexing. - [x] `select { case ... }` multiplexing. v0 tries cases in declared
order, picks first ready (sends always ready; recv ready iff
channel has buffered values). Default runs if nothing else
ready; no default and nothing ready → typed error.
Recv-into-var (`case v := <-ch`) binds; recv-into-assign
(`case v = <-ch`) re-binds. Real-Go random selection among
ready cases deferred (v0 deterministic).
- [ ] `range` over channels. - [ ] `range` over channels.
- [ ] `time.After`-like timer channel. - [ ] `time.After`-like timer channel.
- **Independent implementation.** Do NOT use lib/guest/scheduler/ — that - **Independent implementation.** Do NOT use lib/guest/scheduler/ — that
@@ -597,6 +603,18 @@ Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`.
_Newest first. Append one dated entry per commit._ _Newest first. Append one dated entry per commit._
- 2026-05-27 — Phase 5 cont.: `select` statement evaluation. New
`go-eval-select-stmt` + `go-select-pick` + `go-select-try-case`.
Walks cases in declared order: send always ready in v0; recv ready
iff `(go-chan-len ch) > 0` (new accessor added to sched.sx);
recv-into-decl/assign binds the value into env. Default deferred
until end of walk; if nothing ready and no default, returns
`(:eval-error :select-blocked-no-default)`. Subtle bug fixed where
`:select` stmt was returning the old env instead of the
case-body-extended env — assignments inside cases now stick. +6
tests, runtime 18/18, total 475/475. `[nothing]` — `:select-case`
uniform shape was already chiselled into the sister-plan diary when
the parser landed.
- 2026-05-27 — **Phase 5 first slice.** `lib/go/sched.sx` lands with - 2026-05-27 — **Phase 5 first slice.** `lib/go/sched.sx` lands with
the v0 channel primitive: `go-make-chan` returns a closures-over- the v0 channel primitive: `go-make-chan` returns a closures-over-
mutable-buf channel. Send appends, recv pops first, close flips a mutable-buf channel. Send appends, recv pops first, close flips a