go: parse.sx — go/defer/send/for-range + 9 tests [shapes-scheduler]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 28s

Adds Go's concurrency + iteration primitives to the statement parser:

  go EXPR                     →  (list :go EXPR)
  defer EXPR                  →  (list :defer EXPR)
  ch <- v                     →  (list :send CHAN VALUE)
  for range COLL { ... }      →  (list :range-for nil nil nil COLL BODY)
  for k := range C { ... }    →  (list :range-for :short-decl KEY nil COLL BODY)
  for k, v := range C { }     →  (list :range-for :short-decl KEY VAL COLL BODY)
  for k, v = range C { ... }  →  (list :range-for :assign KEY VAL COLL BODY)

gp-for-find-range pre-scans the for-header (to '{' or eof) looking
for the 'range' keyword; if present, dispatches to gp-parse-for-range
which handles the four range shapes. C-style and while-like and
infinite are now in gp-parse-for-c-style — gp-parse-for is just a
dispatcher.

Send statement detection lives in the LHS-list branch of gp-parse-stmt:
after parsing a single LHS expression, '<-' triggers (:send LHS RHS).
Channel-recv (`<-ch`) was already parsed as unary `<-` in the expression
layer, so both directions cover.

This is the **chiselling-relevant iteration** for the scheduler sister
kit: the AST shapes Go-on-SX will eventually feed into the kit's
scheduler primitives (sched-spawn, sched-defer, chan-op) have landed.
Sister-plan diary updated with three design insights:

  * :go / :defer both wrap a single expr — kit's sched-spawn should
    accept a thunk uniformly across Erlang's spawn(M,F,A) and Go's
    go fn().
  * :send carries CHAN+VALUE symmetrically with the unary <- recv —
    both reduce to (chan-op direction chan value) in the kit.
  * `for v := range ch` uses the same :range-for shape as range-over-
    slice; the scheduler kit's range dispatch is where chan-recv ⇄
    iteration polymorphism lives.

parse 161/161, total 290/290.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 20:24:23 +00:00
parent ba41f8a580
commit 171a08a2f8
6 changed files with 233 additions and 40 deletions

View File

@@ -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

View File

@@ -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"},

View File

@@ -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 |

View File

@@ -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)