From e64d72f554974c255d9a7c924c61a10906a92bcb Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 07:53:10 +0000 Subject: [PATCH] =?UTF-8?q?go:=20parse.sx=20=E2=80=94=20index=20x[i]=20+?= =?UTF-8?q?=20slice=20x[a:b]/x[a:b:c]=20+=2012=20tests=20[proposes-ast]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the bracket postfix branch: a[0] / a[i] / a[i+1] / m["key"] → (list :index OBJ IDX) a[:] / a[1:] / a[:2] / a[1:2] / a[1:2:3] → (list :slice OBJ LOW HIGH MAX) LOW/HIGH/MAX are AST nodes or nil for omitted indices. The 4th MAX slot is only populated by the three-index full-slice form. Two new lib/guest/ast.sx kit gaps surfaced (logged in plans/go-on-sx.md Blockers): * No :index node — universal across guests with arrays/maps. * No :slice node — Python/Rust/Swift/JS/Ruby all need at minimum the two-index form. Go's three-index variant is more specialised but fits in the same shape with an optional fourth slot. Parser is permissive on a[1::3] (strict Go rejects, but the type phase can enforce the grammar; lexer/parser stays loose). Chained (a[0][1]) and mixed-with-selector (a[0].field) cases work via the existing left-associative postfix loop. parse 61/61, total 190/190. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/parse.sx | 52 ++++++++++++++++++++++++++++++-- lib/go/scoreboard.json | 6 ++-- lib/go/scoreboard.md | 4 +-- lib/go/tests/parse.sx | 68 ++++++++++++++++++++++++++++++++++++++++++ plans/go-on-sx.md | 29 ++++++++++++++++-- 5 files changed, 148 insertions(+), 11 deletions(-) diff --git a/lib/go/parse.sx b/lib/go/parse.sx index 7a026b34..df398b2e 100644 --- a/lib/go/parse.sx +++ b/lib/go/parse.sx @@ -95,12 +95,54 @@ :else nil))) (gp-args-rest) args))))) + (define + gp-parse-bracket-expr + ;; Optional expression inside brackets — returns nil if next token + ;; is ':' or ']' (the slice "omitted" cases). + (fn + () + (cond + (and (= (gp-tok-type) "op") + (or (= (gp-tok-value) ":") (= (gp-tok-value) "]"))) + nil + :else (gp-parse-expr 1)))) + (define + gp-parse-bracket + ;; Caller has consumed '['. Parses index or slice and ']'. + ;; x[i] → (list :index BASE i) + ;; x[a:b] → (list :slice BASE LOW HIGH nil) (LOW/HIGH may be nil) + ;; x[a:b:c] → (list :slice BASE LOW HIGH MAX) + ;; Returns the AST node based on BASE. + (fn + (base) + (let ((low (gp-parse-bracket-expr))) + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) "]")) + (do (gp-advance!) (list :index base low)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) ":")) + (do + (gp-advance!) + (let ((high (gp-parse-bracket-expr))) + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) "]")) + (do (gp-advance!) (list :slice base low high nil)) + (and (= (gp-tok-type) "op") (= (gp-tok-value) ":")) + (do + (gp-advance!) + (let ((maxe (gp-parse-bracket-expr))) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) "]")) + (gp-advance!)) + (list :slice base low high maxe))) + :else (list :slice base low high nil)))) + :else base)))) (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 + ;; x.field → (list :select x "field") — Go-specific + ;; f(args...) → (ast-app f args) — canonical + ;; x[i] → (list :index x i) — Go-specific + ;; x[a:b] → (list :slice x low high max) — Go-specific (fn () (let ((base (gp-parse-primary))) @@ -129,6 +171,10 @@ (do (gp-advance!) (gp-postfix-loop (ast-app base (gp-parse-call-args)))) + (and (= (get tok :type) "op") (= (get tok :value) "[")) + (do + (gp-advance!) + (gp-postfix-loop (gp-parse-bracket base))) :else base))))) (define gp-unary-ops diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index dabaca6f..be68fedc 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,10 +1,10 @@ { "language": "go", - "total_pass": 178, - "total": 178, + "total_pass": 190, + "total": 190, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, - {"name":"parse","pass":49,"total":49,"status":"ok"}, + {"name":"parse","pass":61,"total":61,"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 19c2a5bb..e08026c3 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,11 +1,11 @@ # Go-on-SX Scoreboard -**Total: 178 / 178 tests passing** +**Total: 190 / 190 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | -| ✅ | parse | 49 | 49 | +| ✅ | parse | 61 | 61 | | ⬜ | types | 0 | 0 | | ⬜ | eval | 0 | 0 | | ⬜ | runtime | 0 | 0 | diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 28c9e1fa..18c9e560 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -242,6 +242,74 @@ (ast-var "+") (list (ast-app (ast-var "f") (list (ast-var "x"))) (ast-literal "1")))) +(go-parse-test + "index: a[0]" + (go-parse "a[0]") + (list :index (ast-var "a") (ast-literal "0"))) + +(go-parse-test + "index: a[i]" + (go-parse "a[i]") + (list :index (ast-var "a") (ast-var "i"))) + +(go-parse-test + "index: a[i + 1] (expr index)" + (go-parse "a[i + 1]") + (list + :index (ast-var "a") + (ast-app (ast-var "+") (list (ast-var "i") (ast-literal "1"))))) + +(go-parse-test + "index: m[\"key\"] (string index)" + (go-parse "m[\"key\"]") + (list :index (ast-var "m") (ast-literal "key"))) + +(go-parse-test + "index: a[0][1] (chained)" + (go-parse "a[0][1]") + (list + :index (list :index (ast-var "a") (ast-literal "0")) + (ast-literal "1"))) + +(go-parse-test + "index: a[0].field (mixed with selector)" + (go-parse "a[0].field") + (list :select (list :index (ast-var "a") (ast-literal "0")) "field")) + +(go-parse-test + "slice: a[:]" + (go-parse "a[:]") + (list :slice (ast-var "a") nil nil nil)) + +(go-parse-test + "slice: a[1:]" + (go-parse "a[1:]") + (list :slice (ast-var "a") (ast-literal "1") nil nil)) + +(go-parse-test + "slice: a[:2]" + (go-parse "a[:2]") + (list :slice (ast-var "a") nil (ast-literal "2") nil)) + +(go-parse-test + "slice: a[1:2]" + (go-parse "a[1:2]") + (list :slice (ast-var "a") (ast-literal "1") (ast-literal "2") nil)) + +(go-parse-test + "slice: a[1:2:3] (full slice)" + (go-parse "a[1:2:3]") + (list + :slice (ast-var "a") + (ast-literal "1") + (ast-literal "2") + (ast-literal "3"))) + +(go-parse-test + "slice: a[i:j] (var bounds)" + (go-parse "a[i:j]") + (list :slice (ast-var "a") (ast-var "i") (ast-var "j") nil)) + (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 4ddcfe30..96f72ffb 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -167,7 +167,10 @@ Progress-log line → push `origin/loops/go`. `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]`. +- [x] Index `x[i]` and slice `x[a:b]`/`x[a:b:c]`. Go-specific + `(list :index OBJ IDX)` and `(list :slice OBJ LOW HIGH MAX)` + (LOW/HIGH/MAX may be nil) — kit lacks both. Permissive parser + accepts `a[1::3]` (strict Go rejects, but type phase can enforce). - [ ] Type assertion `v.(T)`. - [ ] Type expressions: basic, slice `[]T`, array `[N]T`, map `map[K]V`, chan `chan T` / `chan<- T` / `<-chan T`, func, struct, interface, @@ -181,7 +184,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: 49/49. +- **Acceptance:** parse/ suite at 80+ tests. Current: 61/61. ### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/static-types- @@ -428,7 +431,18 @@ Observed from building the Go parser: 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). +2. **No index / subscript node.** `x[i]` is universal across nearly every + guest with arrays/maps. Rolled `(list :index OBJ IDX)` locally. + +3. **No slice node.** Go's two- and three-index slice expressions are + distinctive but the basic two-index `x[a:b]` shape covers Python, + Rust, Swift, JS, Ruby slicing too. Rolled + `(list :slice OBJ LOW HIGH MAX)` (LOW/HIGH/MAX may be nil for + omitted indices). MAX-as-fourth-field is Go-specific; the canonical + kit shape could ship as `(list :slice OBJ LOW HIGH)` for the common + case and a separate `:slice3` or `:full-slice` for the Go variant. + +Minimal repro: see `lib/go/parse.sx#gp-parse-postfix` + `gp-parse-bracket`. ### Kit-gap proposals against `lib/guest/lex.sx` @@ -453,6 +467,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.: index `x[i]` and slice `x[a:b]` / + `x[a:b:c]` postfix forms. New `gp-parse-bracket` + `gp-parse-bracket-expr` + branch off the same postfix loop as calls/selectors. AST: Go-specific + `(list :index OBJ IDX)` and `(list :slice OBJ LOW HIGH MAX)` — + LOW/HIGH/MAX may be nil for omitted indices. Two more kit gaps logged + (no `:index`, no `:slice` in canonical AST). Permissive on `a[1::3]`. + Covers: literal idx, var idx, expr idx, string idx, chained `a[0][1]`, + mixed `a[0].field`, full slice with three indices. +12 tests, parse + 61/61, total 190/190. `[proposes-ast]`. - 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;