diff --git a/lib/go/parse.sx b/lib/go/parse.sx index 8c87b780..78b0faf1 100644 --- a/lib/go/parse.sx +++ b/lib/go/parse.sx @@ -765,42 +765,111 @@ (do (gp-advance!) (set! els (gp-parse-block-body))))) (list :if cnd then els)))) (define - gp-parse-for - ;; Caller has consumed 'for'. - ;; for { BODY } — infinite - ;; for COND { BODY } — while-like - ;; for INIT; COND; POST { BODY } — C-style - ;; for k, v := range coll { BODY } — deferred (range) - ;; AST: (list :for INIT COND POST BODY); any of INIT/COND/POST may be nil. + gp-for-find-range + ;; Scan tokens from current idx looking for the `range` keyword; + ;; stops at '{' or eof. Restores idx before returning. Returns + ;; true iff the for-header contains a range clause. (fn () - (set! gp-no-comp-lit (+ gp-no-comp-lit 1)) - (let ((init nil) (cnd nil) (post nil) (body nil)) + (let ((saved-idx gp-idx) (found false)) + (define + gp-scan-rng + (fn + () + (cond + (or (= (gp-tok-type) "eof") + (and (= (gp-tok-type) "op") + (= (gp-tok-value) "{"))) + nil + (and (= (gp-tok-type) "keyword") + (= (gp-tok-value) "range")) + (set! found true) + :else (do (gp-advance!) (gp-scan-rng))))) + (gp-scan-rng) + (set! gp-idx saved-idx) + found))) + (define + gp-parse-for-range + ;; Range form: + ;; for range COLL { ... } + ;; for k := range COLL { ... } + ;; for k, v := range COLL { ... } + ;; for k, v = range COLL { ... } (reuse existing vars) + ;; AST: (list :range-for DECL-KIND KEY VALUE COLL BODY) + ;; DECL-KIND : :short-decl | :assign | nil (no kv) + ;; KEY/VALUE : ast-var nodes or nil + (fn + () + (let ((decl-kind nil) (key nil) (value nil) + (coll nil) (body nil)) (cond - (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "range")) nil :else - (let ((first (gp-parse-stmt))) + (do + (when (= (gp-tok-type) "ident") + (set! key (ast-var (gp-tok-value))) + (gp-advance!)) + (when (and (= (gp-tok-type) "op") (= (gp-tok-value) ",")) + (gp-advance!) + (when (= (gp-tok-type) "ident") + (set! value (ast-var (gp-tok-value))) + (gp-advance!))) (cond - (= (gp-tok-type) "semi") - (do - (set! init first) - (gp-advance!) - (when (not (and (= (gp-tok-type) "op") - (= (gp-tok-value) ";"))) - (cond - (= (gp-tok-type) "semi") nil - :else (set! cnd (gp-parse-expr 1)))) - (when (= (gp-tok-type) "semi") (gp-advance!)) - (when (not (and (= (gp-tok-type) "op") - (= (gp-tok-value) "{"))) - (set! post (gp-parse-stmt)))) - :else (set! cnd first)))) + (and (= (gp-tok-type) "op") (= (gp-tok-value) ":=")) + (do (gp-advance!) (set! decl-kind :short-decl)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) "=")) + (do (gp-advance!) (set! decl-kind :assign))))) + (when (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "range")) + (gp-advance!)) + (set! coll (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!) + (set! body (gp-parse-block-body))) + (list :range-for decl-kind key value coll body)))) + (define + gp-parse-for-c-style + ;; for COND { ... } OR for INIT; COND; POST { ... } + ;; AST: (list :for INIT COND POST BODY). + (fn + () + (let ((init nil) (cnd nil) (post nil) (body nil)) + (let ((first (gp-parse-stmt))) + (cond + (= (gp-tok-type) "semi") + (do + (set! init first) + (gp-advance!) + (when (not (and (= (gp-tok-type) "op") + (= (gp-tok-value) ";"))) + (cond + (= (gp-tok-type) "semi") nil + :else (set! cnd (gp-parse-expr 1)))) + (when (= (gp-tok-type) "semi") (gp-advance!)) + (when (not (and (= (gp-tok-type) "op") + (= (gp-tok-value) "{"))) + (set! post (gp-parse-stmt)))) + :else (set! cnd first))) (set! gp-no-comp-lit (- gp-no-comp-lit 1)) (when (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) (gp-advance!) (set! body (gp-parse-block-body))) (list :for init cnd post body)))) + (define + gp-parse-for + ;; Caller has consumed 'for'. Dispatches on header shape. + (fn + () + (set! gp-no-comp-lit (+ gp-no-comp-lit 1)) + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) "{")) + (do + (set! gp-no-comp-lit (- gp-no-comp-lit 1)) + (gp-advance!) + (list :for nil nil nil (gp-parse-block-body))) + (gp-for-find-range) (gp-parse-for-range) + :else (gp-parse-for-c-style)))) (define gp-stmt-assign-ops ;; Compound assignment operators per Go spec § Assignment operations. @@ -838,6 +907,10 @@ (do (gp-advance!) (list :break nil)) (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "continue")) (do (gp-advance!) (list :continue nil)) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "go")) + (do (gp-advance!) (list :go (gp-parse-expr 1))) + (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "defer")) + (do (gp-advance!) (list :defer (gp-parse-expr 1))) (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "if")) (do (gp-advance!) (gp-parse-if)) (and (= (gp-tok-type) "keyword") (= (gp-tok-value) "for")) @@ -881,6 +954,13 @@ (let ((op (gp-tok-value))) (gp-advance!) (list :inc-dec op lhs)) + ;; Channel send statement: ch <- v (Go spec § Send + ;; statements). Only valid when LHS is a single expr. + (and (= (gp-tok-type) "op") (= (gp-tok-value) "<-") + (= (len lhs-list) 1)) + (do + (gp-advance!) + (list :send lhs (gp-parse-expr 1))) :else ;; Plain expression statement — return the single expr. ;; (If somehow there was a comma chain without =/:=, just diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index cb8cf568..1094dc4e 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,10 +1,10 @@ { "language": "go", - "total_pass": 281, - "total": 281, + "total_pass": 290, + "total": 290, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, - {"name":"parse","pass":152,"total":152,"status":"ok"}, + {"name":"parse","pass":161,"total":161,"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 7d66514e..229e569b 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,11 +1,11 @@ # Go-on-SX Scoreboard -**Total: 281 / 281 tests passing** +**Total: 290 / 290 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | -| ✅ | parse | 152 | 152 | +| ✅ | parse | 161 | 161 | | ⬜ | types | 0 | 0 | | ⬜ | eval | 0 | 0 | | ⬜ | runtime | 0 | 0 | diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 80ed3e44..922e7361 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -977,6 +977,76 @@ (go-parse "if Foo {}") (list :if (ast-var "Foo") (list :block (list)) nil)) +(go-parse-test + "stmt: go f()" + (go-parse "go f()") + (list :go (ast-app (ast-var "f") (list)))) + +(go-parse-test + "stmt: go method(x, y)" + (go-parse "go obj.method(x, y)") + (list + :go (ast-app + (list :select (ast-var "obj") "method") + (list (ast-var "x") (ast-var "y"))))) + +(go-parse-test + "stmt: defer cleanup()" + (go-parse "defer cleanup()") + (list :defer (ast-app (ast-var "cleanup") (list)))) + +(go-parse-test + "stmt: send ch <- v" + (go-parse "ch <- 42") + (list :send (ast-var "ch") (ast-literal "42"))) + +(go-parse-test + "for-range: no kv — for range coll { }" + (go-parse "for range coll { }") + (list :range-for nil nil nil (ast-var "coll") (list :block (list)))) + +(go-parse-test + "for-range: key only — for k := range m { }" + (go-parse "for k := range m { }") + (list + :range-for :short-decl + (ast-var "k") + nil + (ast-var "m") + (list :block (list)))) + +(go-parse-test + "for-range: k, v := range m" + (go-parse "for k, v := range m { }") + (list + :range-for :short-decl + (ast-var "k") + (ast-var "v") + (ast-var "m") + (list :block (list)))) + +(go-parse-test + "for-range: assign form k = range coll" + (go-parse "for k = range coll { }") + (list + :range-for :assign + (ast-var "k") + nil + (ast-var "coll") + (list :block (list)))) + +(go-parse-test + "concurrency: defer + go in func body" + (go-parse "func main() { defer cleanup(); go worker() }") + (list + :func-decl "main" + (list) + (list) + (list + :block (list + (list :defer (ast-app (ast-var "cleanup") (list))) + (list :go (ast-app (ast-var "worker") (list))))))) + (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 9a6ad086..7dea3dc1 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -196,19 +196,17 @@ 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 - (`+=` etc.), expression stmt, block `{...}`, **if/else (with chained - else-if), for (infinite / while-like / C-style), break, continue, - inc-dec (`x++` / `x--`)** all done. **Composite-literal `{` - suppression** active in control-flow conditions via - `gp-no-comp-lit` counter — closes the parser-mode caveat flagged - when composite literals landed. `switch`/`select`/`defer`/`go`/ - `for...range`/send `ch<-v` deferred. +- [/] 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. - [ ] End-to-end: hello-world, fibonacci, FizzBuzz, goroutine ping-pong, struct + method. - **Acceptance:** parse/ suite at 80+ tests. **Acceptance bar crossed: - 152/152.** Remaining sub-items (switch/select/defer/go/range, - end-to-end programs) keep Phase 2 open ⬜. + 161/161.** Remaining sub-items (switch/select, end-to-end programs) + keep Phase 2 open ⬜. ### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/static-types- @@ -525,6 +523,19 @@ 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.: 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 + `gp-for-find-range` pre-scans the for-header to dispatch between + range and C-style/while forms cleanly. Send-stmt detection added to + the LHS-list branch (after lhs, `<-` → send). +9 tests, parse + 161/161, total 290/290. `[shapes-scheduler]` — Go's concurrency- + primitive AST shapes (`:go`, `:defer`, `:send`, `:range-for`) all + landed; sister-plan diary updated with the corresponding kit-API + insights (uniform spawn-thunk shape, channel-recv ⇄ iteration + polymorphism at the range-coll dispatch). + + Sister-plan diary update follows. - 2026-05-27 — Phase 2 cont.: control-flow statements. `if cond { } [else { }]` with chained else-if, `for { }` (infinite), `for cond { }` (while-like), `for init; cond; post { }` (C-style), `break`, diff --git a/plans/lib-guest-scheduler.md b/plans/lib-guest-scheduler.md index 51e4cbf6..b1f600eb 100644 --- a/plans/lib-guest-scheduler.md +++ b/plans/lib-guest-scheduler.md @@ -231,5 +231,37 @@ real result. _Newest first. Append one dated entry per milestone landed._ +- 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 + the scheduler. + + ``` + go EXPR → (list :go EXPR) + defer EXPR → (list :defer EXPR) + ch <- v → (list :send CHAN VALUE) + <-ch → (list :app (:var "<-") [CHAN]) ; unary recv + for range COLL { } → (list :range-for nil nil nil COLL BODY) + for k, v := range C → (list :range-for :short-decl KEY VAL COLL BODY) + ``` + + **Design insight for the kit**: the `:go` and `:defer` shapes are + pleasingly minimal — both wrap a single expression. Erlang's + `spawn(Mod, Fun, Args)` will produce something more elaborate; the + scheduler kit primitive `(sched-spawn task)` should accept a thunk so + both languages reduce to a uniform spawn API. + + The `:send` shape carries CHAN + VALUE — symmetric with channel-recv + as the unary `<-` form. Once the scheduler has channel primitives, + both shapes thunk-down to a single `(chan-op direction chan value)` + abstraction. + + Range over channels (`for v := range ch`) is currently parsed as + range-for with `coll = ch`; the scheduler kit will dispatch on the + type of `coll` at execution time (channels yield via receive, + collections via iteration). This dispatch is the right place for the + scheduler kit to express the channel-receive ⇄ iteration polymorphism. + Source: Go-on-SX commit `parse.sx — go/defer/send/range`. + - 2026-05-26 — Plan drafted. Phase 0 unstarted. Awaiting Go-on-SX to begin Phase 1.