From 459427512dbfe7603abbba63440978bb29ffbb79 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 28 May 2026 00:31:28 +0000 Subject: [PATCH] =?UTF-8?q?go:=20Phase=207=20foundation=20=E2=80=94=20gene?= =?UTF-8?q?rics=20syntax=20through=20parser/typer/eval=20[shapes-static-ty?= =?UTF-8?q?pes-bidirectional]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gp-parse-type-params consumes the optional [NAMES CONSTRAINT, ...] clause after a func name. AST stays backward-compatible: 5-slot func-decl when no [...] is present, 6-slot when it is. Typer binds each type-param name as (:ty-param NAME CONSTRAINT) so body's (:ty-name "T") references resolve. Eval is type-erasing — ignores type info, dispatches by name + arity. 10 new tests: parse (3), types (5), eval (2). Total 527/527. Shape: the field binding-group from the canonical kit now feeds 6 consumers (struct fields, var-decls, const-decls, params, receivers, type-params). Confirms it as a TRUE cross-deliverable shape — sister-plan diary documents the 5 roles binding-groups take and why the kit should expose ONE parser + pluggable validators. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/parse.sx | 73 ++++++++++++++++--- lib/go/scoreboard.json | 10 +-- lib/go/scoreboard.md | 8 +- lib/go/tests/eval.sx | 14 ++++ lib/go/tests/parse.sx | 41 ++++++++++- lib/go/tests/types.sx | 35 +++++++++ lib/go/types.sx | 46 +++++++++++- plans/go-on-sx.md | 33 +++++++-- plans/lib-guest-static-types-bidirectional.md | 38 ++++++++++ 9 files changed, 264 insertions(+), 34 deletions(-) diff --git a/lib/go/parse.sx b/lib/go/parse.sx index 280ae216..6b6ebec9 100644 --- a/lib/go/parse.sx +++ b/lib/go/parse.sx @@ -694,6 +694,42 @@ (when (> depth 0) (gp-block-loop))) :else (do (gp-advance!) (gp-block-loop))))) (gp-block-loop)))) + (define + gp-parse-type-params + ;; Optional [...] preceding a func/type decl's regular params. + ;; Each group is `NAMES constraint-type` (re-uses the regular + ;; param-group parser). Returns a list of (:field NAMES TY) + ;; records, or nil if no `[` is present. Type-set constraints + ;; (`T int | float64`) deferred. + (fn () + (cond + (not (and (= (gp-tok-type) "op") (= (gp-tok-value) "["))) + nil + :else + (do + (gp-advance!) + (let ((groups (list))) + (define + gp-tp-loop + (fn () + (cond + (and (= (gp-tok-type) "op") (= (gp-tok-value) "]")) + (gp-advance!) + :else + (let ((group (gp-parse-decl-param-group))) + (cond + (= group nil) + (do (gp-advance!) (gp-tp-loop)) + :else + (do + (append! groups group) + (cond + (and (= (gp-tok-type) "op") + (= (gp-tok-value) ",")) + (do (gp-advance!) (gp-tp-loop)) + :else (gp-tp-loop)))))))) + (gp-tp-loop) + groups))))) (define gp-parse-func-decl ;; Caller has consumed 'func'. @@ -715,18 +751,31 @@ (= (gp-tok-type) "ident") (let ((name (gp-tok-value))) (gp-advance!) - (let ((params (gp-parse-func-decl-params))) - (let ((results (gp-parse-func-type-results))) - (let ((body nil)) - (when (and (= (gp-tok-type) "op") - (= (gp-tok-value) "{")) - (gp-advance!) - (set! body (gp-parse-block-body))) - (cond - (= recv nil) - (list :func-decl name params results body) - :else - (list :method-decl recv name params results body)))))) + ;; Type parameters: [T any] / [T, U any] / [T any, U comparable]. + ;; Same shape as a regular param group — (:field NAMES TY). + ;; Type-set constraints (T int | float64) deferred. + (let ((type-params (gp-parse-type-params))) + (let ((params (gp-parse-func-decl-params))) + (let ((results (gp-parse-func-type-results))) + (let ((body nil)) + (when (and (= (gp-tok-type) "op") + (= (gp-tok-value) "{")) + (gp-advance!) + (set! body (gp-parse-block-body))) + ;; Keep the legacy 5-slot shape when there are + ;; no type params so existing AST consumers + ;; (parse tests, types/eval pattern matchers) + ;; stay compatible. Only add the 6th slot when + ;; a `[...]` clause was actually present. + (cond + (and (= recv nil) (= type-params nil)) + (list :func-decl name params results body) + (= recv nil) + (list :func-decl name params results body type-params) + (= type-params nil) + (list :method-decl recv name params results body) + :else + (list :method-decl recv name params results body type-params))))))) :else nil)))) (define gp-parse-case-body diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index 1a1bfe23..b6b4bc60 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,12 +1,12 @@ { "language": "go", - "total_pass": 517, - "total": 517, + "total_pass": 527, + "total": 527, "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":100,"total":100,"status":"ok"}, + {"name":"parse","pass":179,"total":179,"status":"ok"}, + {"name":"types","pass":77,"total":77,"status":"ok"}, + {"name":"eval","pass":102,"total":102,"status":"ok"}, {"name":"runtime","pass":40,"total":40,"status":"ok"}, {"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 b94a4086..f38ee61a 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,13 +1,13 @@ # Go-on-SX Scoreboard -**Total: 517 / 517 tests passing** +**Total: 527 / 527 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | -| ✅ | parse | 176 | 176 | -| ✅ | types | 72 | 72 | -| ✅ | eval | 100 | 100 | +| ✅ | parse | 179 | 179 | +| ✅ | types | 77 | 77 | +| ✅ | eval | 102 | 102 | | ✅ | runtime | 40 | 40 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/eval.sx b/lib/go/tests/eval.sx index c832b48d..b57af0a2 100644 --- a/lib/go/tests/eval.sx +++ b/lib/go/tests/eval.sx @@ -614,6 +614,20 @@ (go-env-lookup env "got")) 5) +(go-eval-test + "generic: identity Id[T any](x) returns x at runtime" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func Id[T any](x T) T { return x }") (go-parse "r := Id(42)"))))) + (go-env-lookup env "r")) + 42) + +(go-eval-test + "generic: Id works with strings (type erasure)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func Id[T any](x T) T { return x }") (go-parse "r := Id(\"hi\")"))))) + (go-env-lookup env "r")) + "hi") + (define go-eval-test-summary (str "eval " go-eval-test-pass "/" go-eval-test-count)) diff --git a/lib/go/tests/parse.sx b/lib/go/tests/parse.sx index 593e0ba4..1a68ab41 100644 --- a/lib/go/tests/parse.sx +++ b/lib/go/tests/parse.sx @@ -793,6 +793,38 @@ (list (ast-app (ast-var "+") (list (ast-var "x") (ast-var "y"))))))))) +(go-parse-test + "fdecl: generic identity func with one type param [T any]" + (go-parse "func Id[T any](x T) T { return x }") + (list + :func-decl "Id" + (list (list :field (list "x") (list :ty-name "T"))) + (list (list :ty-name "T")) + (list :block (list (list :return (list (list :var "x"))))) + (list (list :field (list "T") (list :ty-name "any"))))) + +(go-parse-test + "fdecl: generic with two type params [T, U any]" + (go-parse "func Map[T, U any](x T) U { return x }") + (list + :func-decl "Map" + (list (list :field (list "x") (list :ty-name "T"))) + (list (list :ty-name "U")) + (list :block (list (list :return (list (list :var "x"))))) + (list (list :field (list "T" "U") (list :ty-name "any"))))) + +(go-parse-test + "fdecl: generic with multi-group type params" + (go-parse "func F[T any, U comparable]() {}") + (list + :func-decl "F" + (list) + (list) + (list :block (list)) + (list + (list :field (list "T") (list :ty-name "any")) + (list :field (list "U") (list :ty-name "comparable"))))) + (go-parse-test "fdecl: func with multi-group params" (go-parse "func mix(x int, y string) {}") @@ -830,8 +862,8 @@ "String" (list) (list (list :ty-name "string")) - (list :block - (list (list :return (list (list :select (ast-var "p") "x"))))))) + (list + :block (list (list :return (list (list :select (ast-var "p") "x"))))))) (go-parse-test "mdecl: method on value receiver" @@ -846,7 +878,10 @@ (go-parse-test "fdecl: body with return" (go-parse "func ret() { return 42 }") - (list :func-decl "ret" (list) (list) + (list + :func-decl "ret" + (list) + (list) (list :block (list (list :return (list (ast-literal "42"))))))) (go-parse-test diff --git a/lib/go/tests/types.sx b/lib/go/tests/types.sx index b1002aab..13399196 100644 --- a/lib/go/tests/types.sx +++ b/lib/go/tests/types.sx @@ -563,6 +563,41 @@ (list :method "Close" (list) (list (list :ty-name "error"))))))) false) +(go-types-test + "generic: identity func [T any] checks (body uses x of type T)" + (let + ((ctx (go-check-decl go-ctx-empty (go-parse "func Id[T any](x T) T { return x }")))) + (go-type-error? ctx)) + false) + +(go-types-test + "generic: two type params [T, U any] checks" + (let + ((ctx (go-check-decl go-ctx-empty (go-parse "func Pair[T, U any](x T, y U) T { return x }")))) + (go-type-error? ctx)) + false) + +(go-types-test + "generic: multi-group type params [T any, U comparable] checks" + (let + ((ctx (go-check-decl go-ctx-empty (go-parse "func F[T any, U comparable](x T, y U) T { return x }")))) + (go-type-error? ctx)) + false) + +(go-types-test + "generic: empty body with type params still checks" + (let + ((ctx (go-check-decl go-ctx-empty (go-parse "func Noop[T any]() {}")))) + (go-type-error? ctx)) + false) + +(go-types-test + "generic: multiple uses of same type param check (x T, y T)" + (let + ((ctx (go-check-decl go-ctx-empty (go-parse "func H[T any](x T, y T) T { return x }")))) + (go-type-error? ctx)) + false) + (define go-types-test-summary (str "types " go-types-test-pass "/" go-types-test-count)) diff --git a/lib/go/types.sx b/lib/go/types.sx index 8f6ac37f..3cb0e974 100644 --- a/lib/go/types.sx +++ b/lib/go/types.sx @@ -749,11 +749,19 @@ (define go-check-func-decl ;; Bind the function in the outer ctx (so recursion works), extend - ;; ctx with params, check the body. Returns the outer ctx with the - ;; function bound, or :type-error. + ;; ctx with type params + value params, check the body. Returns the + ;; outer ctx with the function bound, or :type-error. + ;; + ;; Type parameters become opaque type variables in the body's ctx: + ;; each name `T` is bound as a type alias to (:ty-param "T") so the + ;; checker treats references to T as "this type", not "unknown". + ;; Constraint enforcement (T satisfies `comparable` etc.) is a + ;; later refinement; v0 just allows any operation that's polymorphic + ;; under the constraint `any`. (fn (ctx decl) (let ((name (nth decl 1)) (params (nth decl 2)) - (results (nth decl 3)) (body (nth decl 4))) + (results (nth decl 3)) (body (nth decl 4)) + (type-params (cond (> (len decl) 5) (nth decl 5) :else nil))) (let ((fn-ty (list :ty-func (go-decl-params-to-ty-list params) results))) @@ -762,10 +770,40 @@ (= body nil) ctx-with-fn (and (list? body) (= (first body) :block)) (let ((body-ctx - (go-extend-with-params ctx-with-fn params))) + (go-extend-with-type-params + (go-extend-with-params ctx-with-fn params) + type-params))) (let ((err (go-check-block body-ctx (nth body 1) results))) (cond (go-type-error? err) err :else ctx-with-fn))) :else ctx-with-fn)))))) + +(define + go-extend-with-type-params + ;; Each (:field NAMES CONSTRAINT) field contributes opaque type + ;; vars: bind each NAME as a type alias to (:ty-param NAME). The + ;; constraint type is stored alongside so future "constraint + ;; satisfaction" checks can find it; for v0 it's informational. + (fn (ctx type-params) + (cond + (or (= type-params nil) (= (len type-params) 0)) ctx + :else + (let ((field (first type-params))) + (let ((names (nth field 1)) (constraint (nth field 2))) + (go-extend-with-type-params + (go-extend-with-type-param-names ctx names constraint) + (rest type-params))))))) + +(define + go-extend-with-type-param-names + (fn (ctx names constraint) + (cond + (= (len names) 0) ctx + :else + (let ((nm (first names))) + (go-extend-with-type-param-names + (go-ctx-extend ctx nm + (list :ty-param nm constraint)) + (rest names) constraint))))) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index 28432ba7..7fbd0d5f 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -380,14 +380,20 @@ Progress-log line → push `origin/loops/go`. - **Acceptance:** eval/ +20 tests — **20/20 cleared.** ### Phase 7 — Generics (Go 1.18+) ⬜ -- Type parameters with constraints (type sets: `interface{ int | float64 - }`, `comparable`, `any`). -- Type inference at call sites — basic; the full Go inference algorithm - is notoriously complex. Implement enough for common cases; document - limitations in a Blockers section below. +- [x] **Foundation: parser + typer + eval handle `[T any]` syntax.** + `gp-parse-type-params` reads `[NAMES CONSTRAINT, ...]` after the + func name; AST gets optional 6th slot (legacy 5-slot preserved + when no `[...]`). Typer binds each name as `(:ty-param NAME + CONSTRAINT)` in the body ctx via `go-extend-with-type-params`. + Eval is type-erasing: ignores type info, dispatches by name + + arg count. 10 tests: parse (3), types (5), eval (2). +- [ ] Type parameters with type-set constraints (`int | float64`, + `~int`). Deferred — needs constraint-satisfaction predicate. +- [ ] Type inference at call sites — basic. Currently calls must use + explicit type args OR rely on type erasure at eval. - Tests: generic function (`func Map[T, U any](xs []T, f func(T) U) []U`), generic data structure (linked list), constrained type param. -- **Acceptance:** types/ +30 tests. +- **Acceptance:** types/ +30 tests. Currently +5. ### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜ - Implement just what's needed for representative programs: @@ -622,6 +628,21 @@ Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`. _Newest first. Append one dated entry per commit._ +- 2026-05-28 — **Phase 7 foundation: generics syntax wired through + parser + typer + eval.** New `gp-parse-type-params` consumes the + optional `[NAMES CONSTRAINT, ...]` clause after a func name, + reusing `gp-parse-decl-param-group` so the same field shape that + recurs in struct fields / var-decls / func params / receivers + now also feeds type-parameter lists (6th cross-deliverable use). + AST stays backward-compatible: 5 slots when no `[...]` was + present, 6 slots when it was. Typer binds each name as + `(:ty-param NAME CONSTRAINT)` so body's `(:ty-name "T")` + references resolve. Eval ignores type info entirely (type + erasure) — generic calls just dispatch by name + arity. 10 new + tests (3 parse, 5 types, 2 eval). Total 527/527. **Shape:** the + field-binding-group from canonical kit now feeds 6 consumers, + validating it as a TRUE cross-deliverable shape (not just a + Go-internal artifact). [shapes-static-types-bidirectional] - 2026-05-27 — **Phase 6 closed (eval 100/100, +20 cleared, total 517/517).** Wired panic propagation through `:go` stmt (v0 sync surfaces the panic back to the spawner — same end-effect as real diff --git a/plans/lib-guest-static-types-bidirectional.md b/plans/lib-guest-static-types-bidirectional.md index 00bacfa0..ef2327ef 100644 --- a/plans/lib-guest-static-types-bidirectional.md +++ b/plans/lib-guest-static-types-bidirectional.md @@ -282,6 +282,44 @@ The kits compose; design accordingly. _Newest first. Append one dated entry per milestone landed._ +- 2026-05-28 — From Go-on-SX Phase 7 foundation — **the field + binding-group is a cross-deliverable shape, confirmed by its 6th + consumer (type-parameter lists).** Previously documented uses: + struct fields, var-decls, const-decls, func params, method + receivers. Now type-parameters re-use the EXACT same parser + (`gp-parse-decl-param-group`) and the same `(list :field NAMES TY)` + shape — only the meaning of TY differs (it's a *constraint* type, + not a value type). + + This is the strongest evidence yet that the kit's primary shape + should be a generic `binding-group` parametric over the + role TY plays. Five roles emerge: + + 1. **value-typing** (struct fields, var-decls, params, receivers): + TY is the type of values that bind to NAMES. + 2. **value-pinning** (const-decls): TY is the type of compile- + time-known values. + 3. **constraint-binding** (type-parameter lists): TY is a + constraint that the type-variables NAMES must satisfy. + 4. **kind-binding** (anticipated for higher-kinded types): + TY would be a kind that type-constructors NAMES inhabit. + 5. **trait-binding** (anticipated for Rust-style impl blocks): + TY would be the trait the implementations NAMES provide. + + All five share parser + AST shape; they differ in (a) which + predicate validates the relationship between NAMES and TY, and + (b) what scope NAMES are visible in. The kit should expose a + single `parse-binding-group` consumer and let each role plug in + its own validator. This is the same lesson the assignable? + + constraint-satisfies? pluggable-predicate work surfaced — kit + primitives are SHAPES, validators are PLUGINS. + + Concretely: when the kit extracts, the bidirectional checker + exposes `extend-ctx-with-binding-group(role, group)` where role + selects the validator. Go's type-params bind via role= + "constraint-binding"; struct fields bind via "value-typing". + Erlang's pattern bindings will bind via something else again. + - 2026-05-27 — From Go-on-SX Phase 3 — **interface satisfaction** is the third pluggable predicate the kit should ship, alongside `assignable?` and the synth/check skeleton. Go's structural-and-silent