From 44fb231391ce169792807e021819be268f2cca9f Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 20:29:37 +0000 Subject: [PATCH] =?UTF-8?q?go:=20parse.sx=20=E2=80=94=20switch=20+=20selec?= =?UTF-8?q?t=20+=208=20tests;=20stmts=20done=20[shapes-scheduler]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Go's switch and select statements: switch TAG { case V1, V2: a; case V3: b; default: c } switch { case cond: ... } — tagless select { case x := <-ch: a; case ch <- v: b; default: c } AST shapes: (list :switch TAG CASES) — TAG nil for tagless (list :case VALUES BODY) — VALUES is expr-list (list :select CASES) (list :select-case COMM-STMT BODY) — COMM-STMT is send/recv-assign/bare-recv (list :default BODY) gp-parse-case-body reads stmts until the next case/default/}/eof without consuming the terminator — used by both switch and select. select-case parsing reuses gp-parse-stmt for the comm-stmt, so all four shapes (send, x := <-ch, x = <-ch, bare <-ch) fall out from the existing stmt parser. Composite-lit suppression is engaged for the switch tag expression. Type-switch (`switch v := x.(type) { case int: ... }`) is the one deferred shape; needs the `.(type)` pseudo-syntax recognised in the expression layer. Phase 2 statement coverage is otherwise complete. This is also a chiselling iteration for scheduler sister kit. Diary updated with select-case design insights: * All four select-case shapes share (list :select-case STMT BODY) — kit primitive sched-select accepts a uniform list of cases. * Default vs no-default determines blocking semantics. Erlang's `receive ... after Timeout -> ...` is the analogue — both fit "non-blocking fallback case" in the kit API. parse 169/169, total 298/298. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/parse.sx | 124 +++++++++++++++++++++++++++++++++++ lib/go/scoreboard.json | 6 +- lib/go/scoreboard.md | 4 +- lib/go/tests/parse.sx | 80 ++++++++++++++++++++++ plans/go-on-sx.md | 33 ++++++++-- plans/lib-guest-scheduler.md | 18 +++++ 6 files changed, 253 insertions(+), 12 deletions(-) diff --git a/lib/go/parse.sx b/lib/go/parse.sx index 78b0faf1..4e5a64ee 100644 --- a/lib/go/parse.sx +++ b/lib/go/parse.sx @@ -728,6 +728,126 @@ :else (list :method-decl recv name params results body)))))) :else nil)))) + (define + gp-parse-case-body + ;; Stmts inside a switch/select case clause. Reads until the next + ;; 'case'/'default'/'}'/eof without consuming those terminators. + (fn + () + (let ((stmts (list))) + (define + gp-cb-loop + (fn + () + (cond + (= (gp-tok-type) "eof") nil + (and (= (gp-tok-type) "op") (= (gp-tok-value) "}")) nil + (and (= (gp-tok-type) "keyword") + (or (= (gp-tok-value) "case") + (= (gp-tok-value) "default"))) + nil + (= (gp-tok-type) "semi") (do (gp-advance!) (gp-cb-loop)) + :else + (do + (let ((saved-idx gp-idx)) + (let ((s (gp-parse-stmt))) + (when (not (= s nil)) (append! stmts s))) + (when (= gp-idx saved-idx) (gp-advance!))) + (gp-cb-loop))))) + (gp-cb-loop) + stmts))) + (define + gp-parse-switch + ;; Caller has consumed 'switch'. Two shapes: + ;; switch { ...cases... } — tagless (each case is a bool) + ;; switch TAG { ...cases... } — tagged (match against TAG) + ;; AST: (list :switch TAG CASES) — TAG may be nil. + ;; Each case: (list :case VALUES BODY) or (list :default BODY). + ;; Type-switch (`switch v := x.(type)`) deferred to a follow-up. + (fn + () + (set! gp-no-comp-lit (+ gp-no-comp-lit 1)) + (let ((tag nil) (cases (list))) + (when (not (and (= (gp-tok-type) "op") (= (gp-tok-value) "{"))) + (set! tag (gp-parse-expr 1))) + (set! gp-no-comp-lit (- gp-no-comp-lit 1)) + (when (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) + (gp-advance!)) + (define + gp-sw-loop + (fn + () + (cond + (= (gp-tok-type) "semi") + (do (gp-advance!) (gp-sw-loop)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) "}")) + (gp-advance!) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "case")) + (do + (gp-advance!) + (let ((vals (gp-parse-expr-list))) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) ":")) + (gp-advance!)) + (append! cases + (list :case vals (gp-parse-case-body)))) + (gp-sw-loop)) + (and (= (gp-tok-type) "keyword") + (= (gp-tok-value) "default")) + (do + (gp-advance!) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) ":")) + (gp-advance!)) + (append! cases + (list :default (gp-parse-case-body))) + (gp-sw-loop)) + :else (do (gp-advance!) (gp-sw-loop))))) + (gp-sw-loop) + (list :switch tag cases)))) + (define + gp-parse-select + ;; Caller has consumed 'select'. Each case is a communication stmt + ;; (send / recv) or a recv-assignment. + ;; AST: (list :select CASES). + ;; Each case: (list :select-case COMM-STMT BODY) or (list :default BODY). + (fn + () + (let ((cases (list))) + (when (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) + (gp-advance!)) + (define + gp-sel-loop + (fn + () + (cond + (= (gp-tok-type) "semi") + (do (gp-advance!) (gp-sel-loop)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) "}")) + (gp-advance!) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "case")) + (do + (gp-advance!) + (let ((comm (gp-parse-stmt))) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) ":")) + (gp-advance!)) + (append! cases + (list :select-case comm (gp-parse-case-body)))) + (gp-sel-loop)) + (and (= (gp-tok-type) "keyword") + (= (gp-tok-value) "default")) + (do + (gp-advance!) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) ":")) + (gp-advance!)) + (append! cases + (list :default (gp-parse-case-body))) + (gp-sel-loop)) + :else (do (gp-advance!) (gp-sel-loop))))) + (gp-sel-loop) + (list :select cases)))) (define gp-parse-control-cond ;; Parses an expression as a control-flow condition with the @@ -915,6 +1035,10 @@ (do (gp-advance!) (gp-parse-if)) (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "for")) (do (gp-advance!) (gp-parse-for)) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "switch")) + (do (gp-advance!) (gp-parse-switch)) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "select")) + (do (gp-advance!) (gp-parse-select)) (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) (do (gp-advance!) (gp-parse-block-body)) :else diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index 1094dc4e..190918da 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,10 +1,10 @@ { "language": "go", - "total_pass": 290, - "total": 290, + "total_pass": 298, + "total": 298, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, - {"name":"parse","pass":161,"total":161,"status":"ok"}, + {"name":"parse","pass":169,"total":169,"status":"ok"}, {"name":"types","pass":0,"total":0,"status":"pending"}, {"name":"eval","pass":0,"total":0,"status":"pending"}, {"name":"runtime","pass":0,"total":0,"status":"pending"}, diff --git a/lib/go/scoreboard.md b/lib/go/scoreboard.md index 229e569b..b7e046fc 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,11 +1,11 @@ # Go-on-SX Scoreboard -**Total: 290 / 290 tests passing** +**Total: 298 / 298 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | -| ✅ | parse | 161 | 161 | +| ✅ | parse | 169 | 169 | | ⬜ | types | 0 | 0 | | ⬜ | eval | 0 | 0 | | ⬜ | runtime | 0 | 0 | diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 922e7361..91529d0a 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -1047,6 +1047,86 @@ (list :defer (ast-app (ast-var "cleanup") (list))) (list :go (ast-app (ast-var "worker") (list))))))) +(go-parse-test + "switch: tagged with two cases" + (go-parse "switch x { case 1: a; case 2: b }") + (list + :switch (ast-var "x") + (list + (list :case (list (ast-literal "1")) (list (ast-var "a"))) + (list :case (list (ast-literal "2")) (list (ast-var "b")))))) + +(go-parse-test + "switch: multi-value case" + (go-parse "switch x { case 1, 2: a; case 3: b }") + (list + :switch (ast-var "x") + (list + (list + :case (list (ast-literal "1") (ast-literal "2")) + (list (ast-var "a"))) + (list :case (list (ast-literal "3")) (list (ast-var "b")))))) + +(go-parse-test + "switch: tagless (if-else chain)" + (go-parse "switch { case x > 0: a; case x < 0: b; default: c }") + (list + :switch nil + (list + (list + :case (list + (ast-app (ast-var ">") (list (ast-var "x") (ast-literal "0")))) + (list (ast-var "a"))) + (list + :case (list + (ast-app (ast-var "<") (list (ast-var "x") (ast-literal "0")))) + (list (ast-var "b"))) + (list :default (list (ast-var "c")))))) + +(go-parse-test + "switch: with default only" + (go-parse "switch x { default: y }") + (list :switch (ast-var "x") (list (list :default (list (ast-var "y")))))) + +(go-parse-test + "select: recv-into-var case" + (go-parse "select { case x := <-ch: a }") + (list + :select (list + (list + :select-case (list + :short-decl (list (ast-var "x")) + (list (ast-app (ast-var "<-") (list (ast-var "ch"))))) + (list (ast-var "a")))))) + +(go-parse-test + "select: send case" + (go-parse "select { case ch <- v: done() }") + (list + :select (list + (list + :select-case (list :send (ast-var "ch") (ast-var "v")) + (list (ast-app (ast-var "done") (list))))))) + +(go-parse-test + "select: recv (discard) case" + (go-parse "select { case <-ch: a }") + (list + :select (list + (list + :select-case (ast-app (ast-var "<-") (list (ast-var "ch"))) + (list (ast-var "a")))))) + +(go-parse-test + "select: with default (non-blocking)" + (go-parse "select { case <-ch: a; default: b }") + (list + :select (list + (list + :select-case (ast-app (ast-var "<-") (list (ast-var "ch"))) + (list (ast-var "a"))) + (list :default (list (ast-var "b")))))) + (go-parse-test "non-primary: '+'" (go-parse "+") nil) (go-parse-test "non-primary: empty" (go-parse "") nil) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index 7dea3dc1..a895426c 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -196,17 +196,18 @@ Progress-log line → push `origin/loops/go`. canonical AST kit. Grouped/parenthesized decls (`var (...)`, etc.) and variadic params deferred. Anonymous param-list disambiguation (`func(int, string)`) is a known parser-greedy limitation, flagged. -- [/] Statements: return, short-decl, assign, compound assign, expr stmt, +- [x] Statements: return, short-decl, assign, compound assign, expr stmt, block, if/else (chained), for (4 shapes incl. range), break, - continue, inc-dec, **`go EXPR`, `defer EXPR`, send `ch <- v`, - `for k, v := range coll`** all done. Composite-literal `{` - suppression active in control-flow conditions. `switch` and - `select` deferred. + continue, inc-dec, go, defer, send, **switch (tagged / tagless, + multi-value cases, default), select (recv-into-var / send / + bare-recv / default)** all done. Composite-literal `{` suppression + active in control-flow conditions. Type-switch (`switch v := + x.(type)`) deferred to a follow-up. - [ ] End-to-end: hello-world, fibonacci, FizzBuzz, goroutine ping-pong, struct + method. - **Acceptance:** parse/ suite at 80+ tests. **Acceptance bar crossed: - 161/161.** Remaining sub-items (switch/select, end-to-end programs) - keep Phase 2 open ⬜. + 169/169.** Remaining sub-item (end-to-end programs) keeps Phase 2 + open ⬜. ### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/static-types- @@ -523,6 +524,24 @@ 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 2 cont.: `switch` and `select` statements. + Tagged + tagless switch, multi-value cases, `default`, and select + with recv-into-var / send / bare-recv / default cases. New + `gp-parse-case-body` reads stmts until the next `case`/`default`/`}` + without consuming the terminator. AST shapes: + `(list :switch TAG CASES)`, + `(list :case VALUES BODY)`, + `(list :select CASES)`, + `(list :select-case COMM-STMT BODY)`, + `(list :default BODY)`. With this, **Phase 2 statement coverage + is complete** — type-switch is the one remaining shape (deferred). + +8 tests, parse 169/169, total 298/298. `[shapes-scheduler]` — + sister-plan diary updated with the `:select-case` uniform shape + insight (single kit primitive covers all four Go case kinds; default + vs no-default determines blocking semantics; cross-references to + Erlang's `receive ... after`). + + Sister-plan diary update follows. - 2026-05-27 — Phase 2 cont.: concurrency + iteration statements. `go EXPR`, `defer EXPR`, channel send `ch <- v`, and the four `for ... range` shapes (no-kv / k-only / k,v / assign-form). New diff --git a/plans/lib-guest-scheduler.md b/plans/lib-guest-scheduler.md index b1f600eb..5fe537e4 100644 --- a/plans/lib-guest-scheduler.md +++ b/plans/lib-guest-scheduler.md @@ -231,6 +231,24 @@ real result. _Newest first. Append one dated entry per milestone landed._ +- 2026-05-27 — Follow-up from same Phase 2 work: **`select` AST shape** + landed. Each case is `(list :select-case COMM-STMT BODY)` where + COMM-STMT is one of `:send`, `:short-decl` (recv into new var), + `:assign` (recv into existing var), or a bare receive expression + `(:app (:var "<-") [chan])`. The shape is uniform across all four + comm-stmt kinds — the kit's `sched-select` primitive should accept a + list of cases each described by `(direction chan value-target?)` and + let the kit's runtime pick a ready case. That uniformity is what + makes a single kit primitive cover all four Go case shapes. + + Also: Go's `select` with `default` makes the multiplexer non-blocking; + without default it blocks until a case is ready. The kit primitive + should mirror this — present-or-absent default determines blocking + semantics. Erlang's `receive ... after Timeout -> ...` is a similar + pattern with a timeout case rather than default; the kit primitive + should handle both as instances of "non-blocking-fallback case." + Source: Go-on-SX commit `parse.sx — switch + select`. + - 2026-05-27 — From Go-on-SX Phase 2 (parser side, ahead of scheduler implementation): the **parsed AST shapes** for Go's concurrency primitives have landed and are worth recording before Phase 5 builds