go: Phase 7 foundation — generics syntax through parser/typer/eval [shapes-static-types-bidirectional]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 31s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 00:31:28 +00:00
parent c50f5d5155
commit 459427512d
9 changed files with 264 additions and 34 deletions

View File

@@ -694,6 +694,42 @@
(when (> depth 0) (gp-block-loop))) (when (> depth 0) (gp-block-loop)))
:else (do (gp-advance!) (gp-block-loop))))) :else (do (gp-advance!) (gp-block-loop)))))
(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 (define
gp-parse-func-decl gp-parse-func-decl
;; Caller has consumed 'func'. ;; Caller has consumed 'func'.
@@ -715,6 +751,10 @@
(= (gp-tok-type) "ident") (= (gp-tok-type) "ident")
(let ((name (gp-tok-value))) (let ((name (gp-tok-value)))
(gp-advance!) (gp-advance!)
;; 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 ((params (gp-parse-func-decl-params)))
(let ((results (gp-parse-func-type-results))) (let ((results (gp-parse-func-type-results)))
(let ((body nil)) (let ((body nil))
@@ -722,11 +762,20 @@
(= (gp-tok-value) "{")) (= (gp-tok-value) "{"))
(gp-advance!) (gp-advance!)
(set! body (gp-parse-block-body))) (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 (cond
(= recv nil) (and (= recv nil) (= type-params nil))
(list :func-decl name params results body) (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 :else
(list :method-decl recv name params results body)))))) (list :method-decl recv name params results body type-params)))))))
:else nil)))) :else nil))))
(define (define
gp-parse-case-body gp-parse-case-body

View File

@@ -1,12 +1,12 @@
{ {
"language": "go", "language": "go",
"total_pass": 517, "total_pass": 527,
"total": 517, "total": 527,
"suites": [ "suites": [
{"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"lex","pass":129,"total":129,"status":"ok"},
{"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"parse","pass":179,"total":179,"status":"ok"},
{"name":"types","pass":72,"total":72,"status":"ok"}, {"name":"types","pass":77,"total":77,"status":"ok"},
{"name":"eval","pass":100,"total":100,"status":"ok"}, {"name":"eval","pass":102,"total":102,"status":"ok"},
{"name":"runtime","pass":40,"total":40,"status":"ok"}, {"name":"runtime","pass":40,"total":40,"status":"ok"},
{"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"},
{"name":"e2e","pass":0,"total":0,"status":"pending"} {"name":"e2e","pass":0,"total":0,"status":"pending"}

View File

@@ -1,13 +1,13 @@
# Go-on-SX Scoreboard # Go-on-SX Scoreboard
**Total: 517 / 517 tests passing** **Total: 527 / 527 tests passing**
| | Suite | Pass | Total | | | Suite | Pass | Total |
|---|---|---|---| |---|---|---|---|
| ✅ | lex | 129 | 129 | | ✅ | lex | 129 | 129 |
| ✅ | parse | 176 | 176 | | ✅ | parse | 179 | 179 |
| ✅ | types | 72 | 72 | | ✅ | types | 77 | 77 |
| ✅ | eval | 100 | 100 | | ✅ | eval | 102 | 102 |
| ✅ | runtime | 40 | 40 | | ✅ | runtime | 40 | 40 |
| ⬜ | stdlib | 0 | 0 | | ⬜ | stdlib | 0 | 0 |
| ⬜ | e2e | 0 | 0 | | ⬜ | e2e | 0 | 0 |

View File

@@ -614,6 +614,20 @@
(go-env-lookup env "got")) (go-env-lookup env "got"))
5) 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 (define
go-eval-test-summary go-eval-test-summary
(str "eval " go-eval-test-pass "/" go-eval-test-count)) (str "eval " go-eval-test-pass "/" go-eval-test-count))

View File

@@ -793,6 +793,38 @@
(list (list
(ast-app (ast-var "+") (list (ast-var "x") (ast-var "y"))))))))) (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 (go-parse-test
"fdecl: func with multi-group params" "fdecl: func with multi-group params"
(go-parse "func mix(x int, y string) {}") (go-parse "func mix(x int, y string) {}")
@@ -830,8 +862,8 @@
"String" "String"
(list) (list)
(list (list :ty-name "string")) (list (list :ty-name "string"))
(list :block (list
(list (list :return (list (list :select (ast-var "p") "x"))))))) :block (list (list :return (list (list :select (ast-var "p") "x")))))))
(go-parse-test (go-parse-test
"mdecl: method on value receiver" "mdecl: method on value receiver"
@@ -846,7 +878,10 @@
(go-parse-test (go-parse-test
"fdecl: body with return" "fdecl: body with return"
(go-parse "func ret() { return 42 }") (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"))))))) (list :block (list (list :return (list (ast-literal "42")))))))
(go-parse-test (go-parse-test

View File

@@ -563,6 +563,41 @@
(list :method "Close" (list) (list (list :ty-name "error"))))))) (list :method "Close" (list) (list (list :ty-name "error")))))))
false) 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 (define
go-types-test-summary go-types-test-summary
(str "types " go-types-test-pass "/" go-types-test-count)) (str "types " go-types-test-pass "/" go-types-test-count))

View File

@@ -749,11 +749,19 @@
(define (define
go-check-func-decl go-check-func-decl
;; Bind the function in the outer ctx (so recursion works), extend ;; Bind the function in the outer ctx (so recursion works), extend
;; ctx with params, check the body. Returns the outer ctx with the ;; ctx with type params + value params, check the body. Returns the
;; function bound, or :type-error. ;; 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) (fn (ctx decl)
(let ((name (nth decl 1)) (params (nth decl 2)) (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 (let ((fn-ty
(list :ty-func (list :ty-func
(go-decl-params-to-ty-list params) results))) (go-decl-params-to-ty-list params) results)))
@@ -762,10 +770,40 @@
(= body nil) ctx-with-fn (= body nil) ctx-with-fn
(and (list? body) (= (first body) :block)) (and (list? body) (= (first body) :block))
(let ((body-ctx (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 (let ((err
(go-check-block body-ctx (nth body 1) results))) (go-check-block body-ctx (nth body 1) results)))
(cond (cond
(go-type-error? err) err (go-type-error? err) err
:else ctx-with-fn))) :else ctx-with-fn)))
: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)))))

View File

@@ -380,14 +380,20 @@ Progress-log line → push `origin/loops/go`.
- **Acceptance:** eval/ +20 tests — **20/20 cleared.** - **Acceptance:** eval/ +20 tests — **20/20 cleared.**
### Phase 7 — Generics (Go 1.18+) ⬜ ### Phase 7 — Generics (Go 1.18+) ⬜
- Type parameters with constraints (type sets: `interface{ int | float64 - [x] **Foundation: parser + typer + eval handle `[T any]` syntax.**
}`, `comparable`, `any`). `gp-parse-type-params` reads `[NAMES CONSTRAINT, ...]` after the
- Type inference at call sites — basic; the full Go inference algorithm func name; AST gets optional 6th slot (legacy 5-slot preserved
is notoriously complex. Implement enough for common cases; document when no `[...]`). Typer binds each name as `(:ty-param NAME
limitations in a Blockers section below. 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`), - Tests: generic function (`func Map[T, U any](xs []T, f func(T) U) []U`),
generic data structure (linked list), constrained type param. 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/`) ⬜ ### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜
- Implement just what's needed for representative programs: - 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._ _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 - 2026-05-27 — **Phase 6 closed (eval 100/100, +20 cleared, total
517/517).** Wired panic propagation through `:go` stmt (v0 sync 517/517).** Wired panic propagation through `:go` stmt (v0 sync
surfaces the panic back to the spawner — same end-effect as real surfaces the panic back to the spawner — same end-effect as real

View File

@@ -282,6 +282,44 @@ The kits compose; design accordingly.
_Newest first. Append one dated entry per milestone landed._ _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<TyKind>` 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 - 2026-05-27 — From Go-on-SX Phase 3 — **interface satisfaction** is the
third pluggable predicate the kit should ship, alongside `assignable?` third pluggable predicate the kit should ship, alongside `assignable?`
and the synth/check skeleton. Go's structural-and-silent and the synth/check skeleton. Go's structural-and-silent