From 9ed58bd0fce21fc8f03b424d7dda7b310062cb56 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 21:33:17 +0000 Subject: [PATCH] =?UTF-8?q?go:=20eval.sx=20=E2=80=94=20maps=20+=20index-as?= =?UTF-8?q?sign=20+=208=20tests;=20word-count=20e2e=20[nothing]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 cont. Adds map values and index-assignment for both slices and maps. Map representation: (list :go-map ENTRIES) where ENTRIES is an association list of (key value) pairs. go-map-get / go-map-set — primitive lookup + functional-update. go-slice-set — same idea for slices. go-extract-map-entries reads each :kv element in a composite literal, evaluating key and value. go-eval-composite dispatches on :ty-map to build the :go-map value. go-eval-index extended: when OBJ is a :go-map, look up the key via go-map-get. Missing keys return nil in v0 (Go's real semantics is the zero value of the value type — needs runtime type info that this slice doesn't yet thread through). go-eval-builtin's len handles :go-map alongside :go-slice and strings. go-eval-assign-pairs gets a new branch for (:index OBJ IDX) LHS: - var-rooted indexing only (a[i] = v / m["k"] = v) - slice → go-slice-set then rebind the var - map → go-map-set then rebind the var **Word-counter via map[string]int works end-to-end:** words := []string{"a", "b", "a", "c", "a"} counts := map[string]int{} for i := 0; i < len(words); i++ { counts[words[i]] = counts[words[i]] + 1 } // counts["a"] == 3 Builds on: - map composite literal eval - map index lookup - map index-assign - slice indexing - len() builtin - nil + 1 = 1 (numeric-coercion of missing-key default) eval 58/58, total 435/435. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/eval.sx | 92 ++++++++++++++++++++++++++++++++++++++++-- lib/go/scoreboard.json | 6 +-- lib/go/scoreboard.md | 4 +- lib/go/tests/eval.sx | 54 +++++++++++++++++++++++++ plans/go-on-sx.md | 18 ++++++++- 5 files changed, 163 insertions(+), 11 deletions(-) diff --git a/lib/go/eval.sx b/lib/go/eval.sx index 0ffef19f..1fd51a46 100644 --- a/lib/go/eval.sx +++ b/lib/go/eval.sx @@ -239,6 +239,34 @@ (go-env-extend env (first names) (first vals)) (rest names) (rest vals))))) +(define + go-map-get + (fn (entries key) + (cond + (= (len entries) 0) nil + (= (first (first entries)) key) (nth (first entries) 1) + :else (go-map-get (rest entries) key)))) + +(define + go-map-set + ;; Update the key's value if present, else append. Returns a new entry list. + (fn (entries key value) + (cond + (= (len entries) 0) (list (list key value)) + (= (first (first entries)) key) + (cons (list key value) (rest entries)) + :else (cons (first entries) (go-map-set (rest entries) key value))))) + +(define + go-slice-set + ;; Functional update on a list at index IDX. Out-of-range no-ops in v0. + (fn (elems idx value) + (cond + (>= idx (len elems)) elems + (< idx 0) elems + (= idx 0) (cons value (rest elems)) + :else (cons (first elems) (go-slice-set (rest elems) (- idx 1) value))))) + (define go-eval-builtin ;; Run Go's predeclared builtins (len, append, print). args are @@ -255,6 +283,7 @@ (let ((arg (first vals))) (cond (and (list? arg) (= (first arg) :go-slice)) (len (nth arg 1)) + (and (list? arg) (= (first arg) :go-map)) (len (nth arg 1)) (string? arg) (len arg) :else (list :eval-error :len-not-applicable arg)))) (= name "append") @@ -293,9 +322,30 @@ (go-eval-error? rest-vs) rest-vs :else (cons v rest-vs))))))))) +(define + go-extract-map-entries + (fn (env elems) + (cond + (or (= elems nil) (= (len elems) 0)) (list) + :else + (let ((e (first elems))) + (cond + (not (and (list? e) (= (first e) :kv))) + (list :eval-error :map-elem-missing-key e) + :else + (let ((k (go-eval env (nth e 1))) (v (go-eval env (nth e 2)))) + (cond + (go-eval-error? k) k + (go-eval-error? v) v + :else + (let ((rest-es (go-extract-map-entries env (rest elems)))) + (cond + (go-eval-error? rest-es) rest-es + :else (cons (list k v) rest-es)))))))))) + (define go-eval-composite - ;; (:composite TYPE-OR-EXPR ELEMS). v0 supports slice/array; map/struct + ;; (:composite TYPE-OR-EXPR ELEMS). v0 supports slice/array/map; struct ;; later. (fn (env expr) (let ((ty (nth expr 1)) (elems (nth expr 2))) @@ -306,11 +356,16 @@ (cond (go-eval-error? vals) vals :else (list :go-slice vals))) + (and (list? ty) (= (first ty) :ty-map)) + (let ((entries (go-extract-map-entries env elems))) + (cond + (go-eval-error? entries) entries + :else (list :go-map entries))) :else (list :eval-error :unsupported-composite ty))))) (define go-eval-index - ;; (:index OBJ IDX-EXPR). v0: slice indexing only. + ;; (:index OBJ IDX-EXPR). v0: slice or map. (fn (env expr) (let ((obj (go-eval env (nth expr 1))) (idx (go-eval env (nth expr 2)))) @@ -323,6 +378,10 @@ (or (< idx 0) (>= idx (len elems))) (list :eval-error :index-out-of-range idx (len elems)) :else (nth elems idx))) + (and (list? obj) (= (first obj) :go-map)) + ;; v0: returns nil for missing keys. Go's real semantics is the + ;; zero value of the value type — needs runtime type info. + (go-map-get (nth obj 1) idx) :else (list :eval-error :not-indexable obj))))) (define @@ -453,12 +512,37 @@ (cond (= (len lhs-list) 0) env :else - (let ((lhs (first lhs-list))) + (let ((lhs (first lhs-list)) (rhs-val (first vals))) (cond (and (list? lhs) (= (first lhs) :var)) (go-eval-assign-pairs - (go-env-extend env (nth lhs 1) (first vals)) + (go-env-extend env (nth lhs 1) rhs-val) (rest lhs-list) (rest vals)) + ;; (:index OBJ IDX) — slice or map element assignment + (and (list? lhs) (= (first lhs) :index)) + (let ((obj-expr (nth lhs 1)) (idx-expr (nth lhs 2))) + (cond + ;; only support var-rooted indexing for now + (not (and (list? obj-expr) (= (first obj-expr) :var))) + (list :eval-error :unsupported-lhs lhs) + :else + (let ((obj (go-eval env obj-expr)) (idx (go-eval env idx-expr))) + (cond + (go-eval-error? obj) obj + (go-eval-error? idx) idx + (and (list? obj) (= (first obj) :go-slice)) + (go-eval-assign-pairs + (go-env-extend env (nth obj-expr 1) + (list :go-slice + (go-slice-set (nth obj 1) idx rhs-val))) + (rest lhs-list) (rest vals)) + (and (list? obj) (= (first obj) :go-map)) + (go-eval-assign-pairs + (go-env-extend env (nth obj-expr 1) + (list :go-map + (go-map-set (nth obj 1) idx rhs-val))) + (rest lhs-list) (rest vals)) + :else (list :eval-error :unsupported-lhs lhs))))) :else (list :eval-error :unsupported-lhs lhs)))))) (define diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index a84ab350..c29d6b32 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,12 +1,12 @@ { "language": "go", - "total_pass": 427, - "total": 427, + "total_pass": 435, + "total": 435, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"types","pass":72,"total":72,"status":"ok"}, - {"name":"eval","pass":50,"total":50,"status":"ok"}, + {"name":"eval","pass":58,"total":58,"status":"ok"}, {"name":"runtime","pass":0,"total":0,"status":"pending"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"e2e","pass":0,"total":0,"status":"pending"} diff --git a/lib/go/scoreboard.md b/lib/go/scoreboard.md index a13a7213..8bd33624 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,13 +1,13 @@ # Go-on-SX Scoreboard -**Total: 427 / 427 tests passing** +**Total: 435 / 435 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | | ✅ | parse | 176 | 176 | | ✅ | types | 72 | 72 | -| ✅ | eval | 50 | 50 | +| ✅ | eval | 58 | 58 | | ⬜ | runtime | 0 | 0 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/eval.sx b/lib/go/tests/eval.sx index c73e8885..f7a23c44 100644 --- a/lib/go/tests/eval.sx +++ b/lib/go/tests/eval.sx @@ -262,6 +262,60 @@ (go-env-lookup env "sum")) 15) +(go-eval-test + "map: map[string]int{...} → :go-map" + (gtev go-env-empty "map[string]int{\"a\": 1, \"b\": 2}") + (list :go-map (list (list "a" 1) (list "b" 2)))) + +(go-eval-test + "map: m[\"a\"] → 1" + (let + ((env (go-eval-program go-env-empty (list (go-parse "m := map[string]int{\"a\": 1, \"b\": 2}"))))) + (go-eval env (go-parse "m[\"a\"]"))) + 1) + +(go-eval-test + "map: missing key → nil (v0 stand-in for zero value)" + (let + ((env (go-eval-program go-env-empty (list (go-parse "m := map[string]int{\"a\": 1}"))))) + (go-eval env (go-parse "m[\"missing\"]"))) + nil) + +(go-eval-test + "map: len(m) = 2" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "m := map[string]int{\"a\": 1, \"b\": 2}"))))) + (go-eval env (go-parse "len(m)"))) + 2) + +(go-eval-test + "map: index-assign updates existing key" + (let + ((env (go-eval-program go-env-empty (list (go-parse "m := map[string]int{\"a\": 1}") (go-parse "m[\"a\"] = 99"))))) + (go-eval env (go-parse "m[\"a\"]"))) + 99) + +(go-eval-test + "map: index-assign adds new key" + (let + ((env (go-eval-program go-env-empty (list (go-parse "m := map[string]int{}") (go-parse "m[\"new\"] = 7"))))) + (go-eval env (go-parse "m[\"new\"]"))) + 7) + +(go-eval-test + "slice: index-assign a[0] = 99" + (let + ((env (go-eval-program go-env-empty (list (go-parse "a := []int{10, 20, 30}") (go-parse "a[0] = 99"))))) + (go-eval env (go-parse "a[0]"))) + 99) + +(go-eval-test + "map: word count via loop" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "words := []string{\"a\", \"b\", \"a\", \"c\", \"a\"}") (go-parse "counts := map[string]int{}") (go-parse "for i := 0; i < len(words); i++ { counts[words[i]] = counts[words[i]] + 1 }"))))) + (go-eval env (go-parse "counts[\"a\"]"))) + 3) + (define go-eval-test-summary (str "eval " go-eval-test-pass "/" go-eval-test-count)) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index ef7317a5..68805722 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -282,14 +282,18 @@ Progress-log line → push `origin/loops/go`. indexes, `a[low:high]` slices, `len(a)` returns count, `append(a, ...)` extends. The full triple with capacity-grow comes in a later slice when programs need it. -- [ ] Maps: SX dict + key-type metadata. +- [x] Maps: v0 represents `m` as `(list :go-map ENTRIES)` where ENTRIES + is an assoc list. Composite-literal `map[K]V{...}`, `m[k]` lookup + (nil for missing key, until runtime type info enables zero-value), + `m[k] = v` index-assignment, `len(m)`. Index-assignment for slices + also lands here (`a[i] = v` rebuilds via `go-slice-set`). - [ ] Structs: SX dict + type tag. Methods looked up via type's table. - [/] Functions: top-level definition + call (incl. recursion via the calling env). Lexical closures and multiple return values pending. - [ ] Channels: stub (Phase 5 wires them). - Tests: arithmetic, control flow, recursion, closures, slices, maps, structs, methods, pointer semantics, multiple-return. -- **Acceptance:** eval/ suite at 80+ tests. Current: 50/50. No concurrency yet. +- **Acceptance:** eval/ suite at 80+ tests. Current: 58/58. No concurrency yet. ### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/scheduler/ — that @@ -573,6 +577,16 @@ 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 4 cont.: maps + index-assignment. Maps represented + as `(list :go-map ENTRIES)` where ENTRIES is an assoc list. New + helpers `go-map-get` / `go-map-set` / `go-slice-set`. Composite-lit + for `map[K]V{...}` evaluates via `go-extract-map-entries`. `m[k]` + index lookup added to `go-eval-index`; `len(m)` extended in + `go-eval-builtin`. **Index-assignment** for both slices and maps + added to `go-eval-assign-pairs`: only var-rooted LHS for v0 + (`a[0] = 99`, `m["k"] = v`), enough for canonical programs. + **Word-count via `counts[words[i]] = counts[words[i]] + 1` works + end-to-end.** +8 tests, eval 58/58, total 435/435. `[nothing]`. - 2026-05-27 — Phase 4 cont.: slice values + index/slice exprs + the `len`/`append`/`print` builtins. Slice representation is `(list :go-slice ELEMS)` for v0 (deferring the full length/cap/