From e1c5fdae53b3e63657443b766c5845c2f178f351 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 07:48:21 +0000 Subject: [PATCH] =?UTF-8?q?go:=20parse.sx=20=E2=80=94=20function=20calls?= =?UTF-8?q?=20+=20member=20access=20+=2012=20tests=20[consumes-ast=20propo?= =?UTF-8?q?ses-ast]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds postfix expression forms per Go spec: f() f(x) f(x, y, z) — function calls x.y x.y.z obj.method(x) — selector / member access gp-parse-postfix sits between gp-parse-unary and gp-parse-primary, so calls and selectors bind tighter than any unary prefix — `-f(x)` parses as `-(f(x))`, not `(-f)(x)`. Postfix is left-associative (`x.y.z` = `(x.y).z`), so the loop iterates rather than recurses on the LHS. AST shapes: Call: (ast-app FN ARGS) — canonical Selector: (list :select OBJ "field") — Go-specific tag The selector shape is a kit gap — lib/guest/ast.sx ships ast-app but no ast-select, despite `obj.field` being universal across Go, Rust, Swift, TS, JS, Python, Ruby, Java, C#. Logged in Blockers; tagging [proposes-ast]. Worth promoting on the next nominally-typed guest. parse 49/49, total 178/178. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/parse.sx | 68 +++++++++++++++++++++++++++++++++++++++- lib/go/scoreboard.json | 6 ++-- lib/go/scoreboard.md | 4 +-- lib/go/tests/parse.sx | 70 ++++++++++++++++++++++++++++++++++++++++++ plans/go-on-sx.md | 29 +++++++++++++++-- 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/lib/go/parse.sx b/lib/go/parse.sx index db19823a..7a026b34 100644 --- a/lib/go/parse.sx +++ b/lib/go/parse.sx @@ -64,6 +64,72 @@ (= ty "ident") (do (gp-advance!) (ast-var v)) :else nil)))) + (define + gp-parse-call-args + ;; Parse comma-separated args inside (...). Caller has already + ;; consumed the opening "(". Consumes the closing ")". + ;; Returns a list of argument AST nodes. + (fn + () + (let ((args (list))) + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) ")")) + (do (gp-advance!) args) + :else + (do + (let ((first (gp-parse-expr 1))) + (when (not (= first nil)) (append! args first))) + (define + gp-args-rest + (fn + () + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) ",")) + (do + (gp-advance!) + (let ((arg (gp-parse-expr 1))) + (when (not (= arg nil)) (append! args arg))) + (gp-args-rest)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) ")")) + (gp-advance!) + :else nil))) + (gp-args-rest) + args))))) + (define + gp-parse-postfix + ;; Left-associative postfix loop on top of gp-parse-primary: + ;; x.field → (list :select x "field") — Go-specific node, + ;; no kit shape covers selector access + ;; f(args...) → (ast-app f args) — canonical + (fn + () + (let ((base (gp-parse-primary))) + (gp-postfix-loop base)))) + (define + gp-postfix-loop + (fn + (base) + (cond + (= base nil) nil + :else + (let ((tok (gp-cur))) + (cond + (and (= (get tok :type) "op") (= (get tok :value) ".")) + (do + (gp-advance!) + (let ((field-tok (gp-cur))) + (cond + (= (get field-tok :type) "ident") + (do + (gp-advance!) + (gp-postfix-loop + (list :select base (get field-tok :value)))) + :else base))) + (and (= (get tok :type) "op") (= (get tok :value) "(")) + (do + (gp-advance!) + (gp-postfix-loop (ast-app base (gp-parse-call-args)))) + :else base))))) (define gp-unary-ops ;; Go spec § Operators: prefix unary, all higher precedence than @@ -84,7 +150,7 @@ (cond (= operand nil) nil :else (ast-app (ast-var (get tok :value)) (list operand))))) - :else (gp-parse-primary))))) + :else (gp-parse-postfix))))) (define gp-parse-expr (fn diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index 2bd75d6d..dabaca6f 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,10 +1,10 @@ { "language": "go", - "total_pass": 166, - "total": 166, + "total_pass": 178, + "total": 178, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, - {"name":"parse","pass":37,"total":37,"status":"ok"}, + {"name":"parse","pass":49,"total":49,"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 cd6f620f..19c2a5bb 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,11 +1,11 @@ # Go-on-SX Scoreboard -**Total: 166 / 166 tests passing** +**Total: 178 / 178 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | -| ✅ | parse | 37 | 37 | +| ✅ | parse | 49 | 49 | | ⬜ | types | 0 | 0 | | ⬜ | eval | 0 | 0 | | ⬜ | runtime | 0 | 0 | diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 17cc99c4..28c9e1fa 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -172,6 +172,76 @@ (ast-var "+") (list (ast-var "a") (ast-app (ast-var "-") (list (ast-var "b")))))) +(go-parse-test + "call: f() (no args)" + (go-parse "f()") + (ast-app (ast-var "f") (list))) + +(go-parse-test + "call: f(x)" + (go-parse "f(x)") + (ast-app (ast-var "f") (list (ast-var "x")))) + +(go-parse-test + "call: f(x, y, z)" + (go-parse "f(x, y, z)") + (ast-app (ast-var "f") (list (ast-var "x") (ast-var "y") (ast-var "z")))) + +(go-parse-test + "call: f(1, 2)" + (go-parse "f(1, 2)") + (ast-app (ast-var "f") (list (ast-literal "1") (ast-literal "2")))) + +(go-parse-test + "call: f(a + b) — arg can be a binary expr" + (go-parse "f(a + b)") + (ast-app + (ast-var "f") + (list (ast-app (ast-var "+") (list (ast-var "a") (ast-var "b")))))) + +(go-parse-test + "call: f(g(x)) — nested" + (go-parse "f(g(x))") + (ast-app + (ast-var "f") + (list (ast-app (ast-var "g") (list (ast-var "x")))))) + +(go-parse-test + "select: x.y" + (go-parse "x.y") + (list :select (ast-var "x") "y")) + +(go-parse-test + "select: x.y.z (chained left-assoc)" + (go-parse "x.y.z") + (list :select (list :select (ast-var "x") "y") "z")) + +(go-parse-test + "method: obj.method()" + (go-parse "obj.method()") + (ast-app (list :select (ast-var "obj") "method") (list))) + +(go-parse-test + "method: obj.method(x, y)" + (go-parse "obj.method(x, y)") + (ast-app + (list :select (ast-var "obj") "method") + (list (ast-var "x") (ast-var "y")))) + +(go-parse-test + "postfix: -f(x) → unary applies after call" + (go-parse "-f(x)") + (ast-app + (ast-var "-") + (list (ast-app (ast-var "f") (list (ast-var "x")))))) + +(go-parse-test + "postfix: f(x) + 1 → call binds tighter than binary +" + (go-parse "f(x) + 1") + (ast-app + (ast-var "+") + (list (ast-app (ast-var "f") (list (ast-var "x"))) (ast-literal "1")))) + (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 5d7215b5..4ddcfe30 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -163,7 +163,10 @@ Progress-log line → push `origin/loops/go`. - [x] Unary operators (`+x`, `-x`, `!x`, `^x`, `*p`, `&v`, `<-ch`). `gp-parse-unary` recursive, sits between `gp-parse-expr` and `gp-parse-primary`; right-associative chains (`!!x`). -- [ ] Function calls `f(a, b)` and member access `x.field`. +- [x] Function calls `f(a, b)` (canonical `ast-app`) and member access + `x.field` (Go-specific `(list :select OBJ "field")` — the AST kit + doesn't ship a selector node; this is a sister-plan-static-types + data point about what the canonical AST is missing). - [ ] Index `x[i]` and slice `x[a:b]`/`x[a:b:c]`. - [ ] Type assertion `v.(T)`. - [ ] Type expressions: basic, slice `[]T`, array `[N]T`, map `map[K]V`, @@ -178,7 +181,7 @@ Progress-log line → push `origin/loops/go`. assign, short-decl `:=`, send `ch <- v`, recv `<-ch`. - [ ] End-to-end: hello-world, fibonacci, FizzBuzz, goroutine ping-pong, struct + method. -- **Acceptance:** parse/ suite at 80+ tests. Current: 37/37. +- **Acceptance:** parse/ suite at 80+ tests. Current: 49/49. ### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/static-types- @@ -414,6 +417,19 @@ Every commit ends its message with a chisel note in brackets: ## Blockers +### Kit-gap proposals against `lib/guest/ast.sx` + +Observed from building the Go parser: + +1. **No selector / field-access node.** `obj.field` is a universal shape + across nominally-typed languages — Go, Rust, Swift, TS, JS, Python, + Ruby, Java, C#. The kit ships `ast-app` (function application) but + not `ast-select`. We rolled `(list :select OBJ "field")` locally as + a Go-specific tag. Worth promoting once a second consumer hits the + same need (likely immediately — almost every guest needs it). + +Minimal repro: see `lib/go/parse.sx#gp-parse-postfix` (`.` branch). + ### Kit-gap proposals against `lib/guest/lex.sx` Observed from building the Go tokenizer. Not blocking Phase 2; surfaced @@ -437,6 +453,15 @@ 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.: postfix forms — function calls `f(a, b)` + via canonical `ast-app`, and member access `x.field` via Go-specific + `(list :select OBJ "field")`. The AST kit has no selector node; + logged in Blockers as `[proposes-ast]` — every nominally-typed guest + will hit the same gap, worth promoting on the next consumer. Postfix + loop sits between unary and primary so calls bind tighter than unary + (`-f(x)` = `-(f(x))`). Covers nested calls, chained selectors, + methods `obj.m(x)`, mixed precedence. +12 tests, parse 49/49, total + 178/178. `[consumes-ast proposes-ast]`. - 2026-05-27 — Phase 2 cont.: unary prefix operators (`+`, `-`, `!`, `^`, `*`, `&`, `<-`). `gp-parse-unary` is recursive (`!!x`) and sits between `gp-parse-expr` and `gp-parse-primary` so unary always binds tighter