From 674d8115b81d20a8f1a77ee30da45d28d917d607 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 21:47:07 +0000 Subject: [PATCH] =?UTF-8?q?go:=20eval.sx=20=E2=80=94=20method=20dispatch?= =?UTF-8?q?=20+=20unary=20+=20e2e=20programs=20+=2014=20tests;=20Phase=204?= =?UTF-8?q?=20bar=20crossed=20[nothing]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 cont. The crossings: * Method dispatch — Methods record under #method/TYPE/NAME (same mangled-key scheme the type checker uses, intentionally so eval and type checker can converge on a shared method-table protocol later). go-eval-method-call: lookup the receiver type's method, bind receiver param to the struct value, evaluate body. Value and pointer receivers treated the same in v0 (pointer semantics not modelled yet). * Method-call dispatch — In go-eval's :app branch, head=:select routes to go-eval-method-call. If the receiver is not a struct, falls back to the field-as-callable path. * Unary prefix ops — go-eval's :app branch checks for 1-arg :var head with op name "-" / "+" / "!". (Other unary ops like *p / &v / <-ch / ^x deferred until pointer / channel / bitwise semantics arrive.) End-to-end programs verified: * recursive fib(10) = 55 * struct + method + iterative loop (counter bump 7 times) * linear search (returns index or -1) * factorial via method on Counter (= 120) * count odd numbers in 1..10 = 5 **Phase 4 acceptance bar (80+) crossed: eval 80/80, total 457/457.** Remaining Phase 4 work (closures, multi-return, full slice triple, pointer semantics) refines but doesn't gate Phase 5 (goroutines). Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/go/eval.sx | 87 ++++++++++++++++++++++++++++++++++++++++++ lib/go/scoreboard.json | 6 +-- lib/go/scoreboard.md | 4 +- lib/go/tests/eval.sx | 87 ++++++++++++++++++++++++++++++++++++++++++ plans/go-on-sx.md | 30 +++++++++++---- 5 files changed, 201 insertions(+), 13 deletions(-) diff --git a/lib/go/eval.sx b/lib/go/eval.sx index e0da6002..64de809b 100644 --- a/lib/go/eval.sx +++ b/lib/go/eval.sx @@ -779,6 +779,8 @@ (go-eval-inc-dec env stmt) (and (list? stmt) (= (first stmt) :func-decl)) (go-eval-func-decl env stmt) + (and (list? stmt) (= (first stmt) :method-decl)) + (go-eval-method-decl env stmt) (and (list? stmt) (= (first stmt) :type-decl)) (go-eval-type-decl env stmt) :else @@ -787,6 +789,77 @@ (go-eval-error? v) v :else env))))) +(define + go-eval-method-decl + ;; (:method-decl RECV NAME PARAMS RESULTS BODY) — register the method + ;; under #method/RECV-TYPE-NAME/METHOD-NAME, value is a :go-method. + (fn (env stmt) + (let ((recv (nth stmt 1)) (name (nth stmt 2)) + (params (nth stmt 3)) (body (nth stmt 5))) + (let ((recv-names (nth recv 1)) (recv-ty (nth recv 2))) + (let ((recv-name + (cond + (= (len recv-names) 0) "_" + :else (first recv-names)))) + (let ((type-name (go-extract-recv-ty-name recv-ty))) + (cond + (= type-name nil) env + :else + (go-env-extend env + (str "#method/" type-name "/" name) + (list :go-method recv-name params body))))))))) + +(define + go-eval-method-call + ;; Method dispatch: lookup #method/TYPE/NAME in env, bind receiver + ;; to OBJ-value and params to ARGS, run body. + (fn (env obj-expr method-name args) + (let ((obj (go-eval env obj-expr))) + (cond + (go-eval-error? obj) obj + (not (and (list? obj) (= (first obj) :go-struct))) + ;; Not a struct: maybe it's a callable field access? Try the + ;; normal select-then-call path. + (let ((callee (go-eval env (list :select obj-expr method-name)))) + (cond + (go-eval-error? callee) callee + :else (go-eval-call env callee args))) + :else + (let ((type-name (nth obj 1))) + (let ((method-val (go-env-lookup env + (str "#method/" type-name "/" method-name)))) + (cond + (= method-val nil) + (list :eval-error :no-such-method type-name method-name) + :else + (let ((recv-name (nth method-val 1)) + (params (nth method-val 2)) + (body (nth method-val 3))) + (let ((arg-vals (go-eval-args env args))) + (cond + (go-eval-error? arg-vals) arg-vals + :else + (let ((param-names (go-flatten-param-names params))) + (cond + (not (= (len param-names) (len arg-vals))) + (list :eval-error :arity-mismatch + (len param-names) (len arg-vals)) + :else + (let ((call-env + (go-env-extend + (go-bind-names env param-names arg-vals) + recv-name obj))) + (cond + (= body nil) nil + (and (list? body) (= (first body) :block)) + (let ((r (go-eval-block call-env (nth body 1)))) + (cond + (and (list? r) (= (first r) :return-value)) + (nth r 1) + (go-eval-error? r) r + :else nil)) + :else nil)))))))))))))) + (define go-eval-type-decl ;; (:type-decl NAME TYPE). For struct types we register the field-name @@ -864,6 +937,20 @@ (go-eval-error? lv) lv (go-eval-error? rv) rv :else (go-eval-binop op lv rv)))) + ;; Unary prefix op: head is :var with op name + 1 arg. + (and (list? head) (= (first head) :var) (= (len args) 1) + (some (fn (o) (= o (nth head 1))) + (list "-" "+" "!"))) + (let ((op (nth head 1)) (v (go-eval env (first args)))) + (cond + (go-eval-error? v) v + (= op "-") (- 0 v) + (= op "+") v + (= op "!") (not v) + :else (list :eval-error :unsupported-unary op))) + ;; Method-call shape: head is (:select OBJ METHOD-NAME). + (and (list? head) (= (first head) :select)) + (go-eval-method-call env (nth head 1) (nth head 2) args) :else (let ((callee (go-eval env head))) (cond diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index e4b7b5e2..70edd9d7 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,12 +1,12 @@ { "language": "go", - "total_pass": 443, - "total": 443, + "total_pass": 457, + "total": 457, "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":66,"total":66,"status":"ok"}, + {"name":"eval","pass":80,"total":80,"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 b57a6ff5..cf307cbd 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,13 +1,13 @@ # Go-on-SX Scoreboard -**Total: 443 / 443 tests passing** +**Total: 457 / 457 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | | ✅ | parse | 176 | 176 | | ✅ | types | 72 | 72 | -| ✅ | eval | 66 | 66 | +| ✅ | eval | 80 | 80 | | ⬜ | runtime | 0 | 0 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/eval.sx b/lib/go/tests/eval.sx index 6e8a2afb..2e6f12e3 100644 --- a/lib/go/tests/eval.sx +++ b/lib/go/tests/eval.sx @@ -380,6 +380,93 @@ :go-struct "Point" (list (list "x" 4) (list "y" 6)))) +(go-eval-test + "method: p.Sum() = 3" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Point struct { x, y int }") (go-parse "func (p Point) Sum() int { return p.x + p.y }") (go-parse "p := Point{1, 2}"))))) + (go-eval env (go-parse "p.Sum()"))) + 3) + +(go-eval-test + "method: p.Add(5) = 6 (with arg)" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Point struct { x, y int }") (go-parse "func (p Point) Add(d int) int { return p.x + d }") (go-parse "p := Point{1, 2}"))))) + (go-eval env (go-parse "p.Add(5)"))) + 6) + +(go-eval-test + "method: pointer receiver works value-style in v0" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Point struct { x, y int }") (go-parse "func (p *Point) GetX() int { return p.x }") (go-parse "p := Point{1, 2}"))))) + (go-eval env (go-parse "p.GetX()"))) + 1) + +(go-eval-test + "method: missing method → :no-such-method" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Point struct { x, y int }") (go-parse "p := Point{1, 2}"))))) + (go-eval env (go-parse "p.Ghost()"))) + (list :eval-error :no-such-method "Point" "Ghost")) + +(go-eval-test + "unary: -x" + (go-eval (go-env-extend go-env-empty "x" 5) (go-parse "-x")) + -5) + +(go-eval-test "unary: !true → false" (gtev go-env-empty "!true") false) + +(go-eval-test "unary: !false → true" (gtev go-env-empty "!false") true) + +(go-eval-test + "unary: -3 + 5 = 2 (unary binds tighter)" + (gtev go-env-empty "-3 + 5") + 2) + +(go-eval-test + "e2e: count odd numbers in 1..10 = 5" + (let + ((env (go-eval-program go-env-empty + (list (go-parse "odds := 0") + (go-parse "i := 1") + (go-parse "for i <= 10 { odds = odds + 1; i = i + 2 }"))))) + (go-env-lookup env "odds")) + 5) + +(go-eval-test + "e2e: factorial via method on Counter" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Acc struct { v int }") (go-parse "func (a Acc) Mul(x int) Acc { return Acc{a.v * x} }") (go-parse "a := Acc{1}") (go-parse "for i := 1; i <= 5; i++ { a = a.Mul(i) }"))))) + (go-eval env (go-parse "a.v"))) + 120) + +(go-eval-test + "e2e: recursive fibonacci fib(10) = 55" + (let + ((env (go-eval-program go-env-empty (list (go-parse "func fib(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) }"))))) + (go-eval env (go-parse "fib(10)"))) + 55) + +(go-eval-test + "e2e: struct + method + iterative loop" + (let + ((env (go-eval-program go-env-empty (list (go-parse "type Counter struct { n int }") (go-parse "func (c Counter) Bump() Counter { return Counter{c.n + 1} }") (go-parse "c := Counter{0}") (go-parse "for i := 0; i < 7; i++ { c = c.Bump() }"))))) + (go-eval env (go-parse "c.n"))) + 7) + +(go-eval-test + "e2e: linear search returns index" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func find(a []int, x int) int { for i := 0; i < len(a); i++ { if a[i] == x { return i } } ; return -1 }") (go-parse "nums := []int{10, 20, 30, 40}"))))) + (go-eval env (go-parse "find(nums, 30)"))) + 2) + +(go-eval-test + "e2e: linear search returns -1 when missing" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func find(a []int, x int) int { for i := 0; i < len(a); i++ { if a[i] == x { return i } } ; return -1 }") (go-parse "nums := []int{10, 20, 30}"))))) + (go-eval env (go-parse "find(nums, 99)"))) + -1) + (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 81541b73..e66afade 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -287,19 +287,22 @@ Progress-log line → push `origin/loops/go`. (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: `(list :go-struct TYPE-NAME FIELDS)` where FIELDS is an - assoc list. `type Point struct {...}` registers field names in - env via `(:go-struct-type FIELD-NAMES)`; positional and keyed - composite literals build struct values; `p.field` selector and - `p.field = v` selector-assignment work. Methods lookup-by-receiver - pending — depends on threading the type checker's method-key - scheme into eval. +- [x] Structs + method dispatch. `(list :go-struct TYPE-NAME FIELDS)` + assoc-list. `type T struct {...}` registers `:go-struct-type` + with field names. Positional + keyed composite literals; `p.f` + / `p.f = v` selectors. Methods bind under `#method/T/N` mangled + keys — same scheme as the type checker. `p.M(...)` dispatches via + receiver type lookup, binds the receiver param to the struct + value, runs body. Both value and pointer receivers work in v0 + (treated the same since pointer semantics aren't modelled yet). - [/] 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: 66/66. No concurrency yet. +- **Acceptance:** eval/ suite at 80+ tests. **Bar crossed: 80/80.** + Remaining sub-items (lexical closures, multi-return funcs, full + slice triple with capacity) refine but don't gate Phase 5. ### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜ - **Independent implementation.** Do NOT use lib/guest/scheduler/ — that @@ -583,6 +586,17 @@ 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.: **method dispatch + unary ops + e2e + programs. Acceptance bar (80+) crossed.** Methods register under + `#method/TYPE/NAME` (same scheme the type checker uses). When `p.M(...)` + is called, `go-eval-method-call` looks up the receiver type's method, + binds the receiver param to the struct value, runs the body. Both + value and pointer receivers work in v0 (treated the same — no + pointer semantics yet). Unary `-x` / `+x` / `!x` in `go-eval`. + E2E programs evaluating end-to-end now include: counter-via-method + (factorial), linear search returning index or -1, recursive + fibonacci(10) = 55, and the counter-bump-N-times pattern. +14 tests, + eval 80/80, total 457/457. `[nothing]`. - 2026-05-27 — Phase 4 cont.: structs + selector access + selector-assignment. `(:go-struct TYPE-NAME FIELDS)` value, with FIELDS an assoc list. `type T struct {...}` is now significant at