diff --git a/lib/go/eval.sx b/lib/go/eval.sx index c772c63c..eba29f64 100644 --- a/lib/go/eval.sx +++ b/lib/go/eval.sx @@ -823,6 +823,8 @@ ;; Otherwise r is the env after the selected body ran; ;; propagate so assignments inside cases stick. :else r)) + (and (list? stmt) (= (first stmt) :range-for)) + (go-eval-range-for env stmt) :else (let ((v (go-eval env stmt))) (cond @@ -926,6 +928,120 @@ (fn (env stmt) (go-select-pick env (nth stmt 1) nil))) +(define + go-ast-name + ;; Extract a name from a (:var NAME) ast, else nil. + (fn (ast) + (cond + (and (list? ast) (= (first ast) :var)) (nth ast 1) + :else nil))) + +(define + go-range-extend + (fn (env key-name value-name k v) + (cond + (and (not (= key-name nil)) (not (= value-name nil))) + (go-env-extend (go-env-extend env key-name k) value-name v) + (not (= key-name nil)) (go-env-extend env key-name k) + :else env))) + +(define + go-range-body + ;; Evaluate body in env. Returns env-or-sentinel. + (fn (env body) + (cond + (and (list? body) (= (first body) :block)) + (go-eval-block env (nth body 1)) + :else env))) + +(define + go-range-slice-loop + (fn (env elems i key-name value-name body original-env) + (cond + (>= i (len elems)) env + :else + (let ((env2 (go-range-extend env key-name value-name i + (nth elems i)))) + (let ((r (go-range-body env2 body))) + (cond + (and (list? r) (= (first r) :return-value)) r + (= r :break) env + (= r :continue) + (go-range-slice-loop env elems (+ i 1) + key-name value-name body original-env) + (go-eval-error? r) r + :else + (go-range-slice-loop r elems (+ i 1) + key-name value-name body original-env))))))) + +(define + go-range-map-loop + (fn (env entries key-name value-name body original-env) + (cond + (or (= entries nil) (= (len entries) 0)) env + :else + (let ((entry (first entries))) + (let ((k (first entry)) (v (nth entry 1))) + (let ((env2 (go-range-extend env key-name value-name k v))) + (let ((r (go-range-body env2 body))) + (cond + (and (list? r) (= (first r) :return-value)) r + (= r :break) env + (= r :continue) + (go-range-map-loop env (rest entries) + key-name value-name body original-env) + (go-eval-error? r) r + :else + (go-range-map-loop r (rest entries) + key-name value-name body original-env))))))))) + +(define + go-range-chan-loop + ;; For chan: KEY-NAME receives each value. v0 stops when chan is + ;; empty (no preemption to wait for new values). Real Go waits on + ;; the chan until closed AND empty. + (fn (env coll key-name body original-env) + (cond + (= (go-chan-len coll) 0) env + :else + (let ((v (go-chan-recv! coll))) + (let ((env2 + (cond + (not (= key-name nil)) (go-env-extend env key-name v) + :else env))) + (let ((r (go-range-body env2 body))) + (cond + (and (list? r) (= (first r) :return-value)) r + (= r :break) env + (= r :continue) + (go-range-chan-loop env coll key-name body original-env) + (go-eval-error? r) r + :else + (go-range-chan-loop r coll key-name body original-env)))))))) + +(define + go-eval-range-for + ;; (:range-for DECL-KIND KEY VALUE COLL BODY) + ;; KEY/VALUE: (:var NAME) or nil + ;; COLL: an expression evaluating to slice / map / chan + (fn (env stmt) + (let ((key-name (go-ast-name (nth stmt 2))) + (value-name (go-ast-name (nth stmt 3))) + (coll-expr (nth stmt 4)) + (body (nth stmt 5))) + (let ((coll (go-eval env coll-expr))) + (cond + (go-eval-error? coll) coll + (and (list? coll) (= (first coll) :go-slice)) + (go-range-slice-loop env (nth coll 1) 0 + key-name value-name body env) + (and (list? coll) (= (first coll) :go-map)) + (go-range-map-loop env (nth coll 1) + key-name value-name body env) + (and (list? coll) (= (first coll) :go-chan)) + (go-range-chan-loop env coll key-name body env) + :else (list :eval-error :not-rangeable coll)))))) + (define go-eval-method-decl ;; (:method-decl RECV NAME PARAMS RESULTS BODY) — register the method diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index 4f910e8b..25bdfa4a 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,13 +1,13 @@ { "language": "go", - "total_pass": 475, - "total": 475, + "total_pass": 483, + "total": 483, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"types","pass":72,"total":72,"status":"ok"}, {"name":"eval","pass":80,"total":80,"status":"ok"}, - {"name":"runtime","pass":18,"total":18,"status":"ok"}, + {"name":"runtime","pass":26,"total":26,"status":"ok"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"e2e","pass":0,"total":0,"status":"pending"} ] diff --git a/lib/go/scoreboard.md b/lib/go/scoreboard.md index 777c11ba..c657cd7c 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,6 +1,6 @@ # Go-on-SX Scoreboard -**Total: 475 / 475 tests passing** +**Total: 483 / 483 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -8,7 +8,7 @@ | ✅ | parse | 176 | 176 | | ✅ | types | 72 | 72 | | ✅ | eval | 80 | 80 | -| ✅ | runtime | 18 | 18 | +| ✅ | runtime | 26 | 26 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/runtime.sx b/lib/go/tests/runtime.sx index 56f6cf56..ec07d29a 100644 --- a/lib/go/tests/runtime.sx +++ b/lib/go/tests/runtime.sx @@ -152,6 +152,62 @@ (go-env-lookup env "result")) 7) +(go-rt-test + "range: slice — sum of 1..5" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var sum = 0") (go-parse "a := []int{1, 2, 3, 4, 5}") (go-parse "for i, v := range a { sum = sum + v }"))))) + (go-env-lookup env "sum")) + 15) + +(go-rt-test + "range: slice — key only (index)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var s = 0") (go-parse "a := []int{10, 20, 30}") (go-parse "for i := range a { s = s + i }"))))) + (go-env-lookup env "s")) + 3) + +(go-rt-test + "range: map — sum values" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var s = 0") (go-parse "m := map[string]int{\"a\": 1, \"b\": 2, \"c\": 3}") (go-parse "for k, v := range m { s = s + v }"))))) + (go-env-lookup env "s")) + 6) + +(go-rt-test + "range: channel — collect all buffered" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "ch <- 1") (go-parse "ch <- 2") (go-parse "ch <- 3") (go-parse "var sum = 0") (go-parse "for v := range ch { sum = sum + v }"))))) + (go-env-lookup env "sum")) + 6) + +(go-rt-test + "range: slice with break exits early" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var s = 0") (go-parse "a := []int{1, 2, 3, 4, 5}") (go-parse "for i, v := range a { if v == 3 { break } ; s = s + v }"))))) + (go-env-lookup env "s")) + 3) + +(go-rt-test + "range: slice with continue skips an element" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var s = 0") (go-parse "a := []int{1, 2, 3, 4, 5}") (go-parse "for i, v := range a { if v == 3 { continue } ; s = s + v }"))))) + (go-env-lookup env "s")) + 12) + +(go-rt-test + "range: empty slice — body never runs" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "var s = 0") (go-parse "a := []int{}") (go-parse "for v := range a { s = s + v }"))))) + (go-env-lookup env "s")) + 0) + +(go-rt-test + "range: chan + goroutine producer" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func emit(c chan int) { c <- 10 ; c <- 20 ; c <- 30 }") (go-parse "ch := make()") (go-parse "go emit(ch)") (go-parse "var total = 0") (go-parse "for v := range ch { total = total + v }"))))) + (go-env-lookup env "total")) + 60) + (define go-rt-test-summary (str "runtime " go-rt-test-pass "/" go-rt-test-count)) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index 91d89f47..95fb3c1f 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -320,7 +320,11 @@ Progress-log line → push `origin/loops/go`. 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. +- [x] `range` over slices / maps / channels. New `go-eval-range-for` + dispatches on collection type: slice (index+elem), map (key+val), + channel (just value). v0 chan-range stops when buffer empties + (no preemption to wait for new sends). break exits with the + pre-break env (preserving prior-iteration assignments). - [ ] `time.After`-like timer channel. - **Independent implementation.** Do NOT use lib/guest/scheduler/ — that kit doesn't exist yet and depends on this work for its design. See @@ -603,6 +607,20 @@ Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`. _Newest first. Append one dated entry per commit._ +- 2026-05-27 — Phase 5 cont.: range-over-{slice,map,channel}. New + `go-eval-range-for` dispatches on the collection type: + slice → bind index + element, iterate by position + map → bind key + value, iterate over entries assoc list + chan → bind value, drain until empty (v0: no preemption to wait + for new sends; real Go blocks until close + empty) + break/continue propagate via the existing sentinel scheme. + Subtle fix in break-from-loop: was returning the pre-loop env + (clobbering prior-iteration assignments); now returns the current + iteration's input env so successful iterations stick. Patched for + range-slice, range-map, range-chan in one go. +7 tests, runtime + 26/26, total 483/483. `[nothing]` — collection-iteration semantics + are Go-specific; the cross-language scheduler insights are already + in the sister-plan diary. - 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