go: goroutine-panic propagation + 8 corner tests → eval 100/100, Phase 6 acceptance cleared [shapes-scheduler]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 20s

Wired panic through :go stmt (v0 sync surfaces back to spawner —
matches real Go's "crash whole program" end-effect) and through
go-eval-for (was swallowing panic at the loop boundary).

8 tests added: goroutine-panic-surfaces, goroutine-recover-via-
spawner-defer, multi-defer-LIFO-with-recover, defer-fires-on-panic-
path, panic(nil), panic-in-loop, defer-still-runs-in-panicking-fn,
args-eager-on-panic-path. 20 Phase-6 tests total; +20 acceptance
bar cleared (eval/ 80 → 100).

Shape: 4 control-flow sites now repeat the same sentinel dispatch
arm (return-value, break, continue, eval-error, go-panic). The
scheduler kit should bake in a single propagates? helper rather
than have each guest evaluator list every sentinel inline — diary
documents the cross-cutting abstraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 23:54:56 +00:00
parent f52ad1fac6
commit c50f5d5155
6 changed files with 137 additions and 13 deletions

View File

@@ -914,6 +914,8 @@
:else (go-for-loop env1 cnd post body)))
(go-eval-error? r)
r
(go-panic? r)
r
:else (let
((env1 (cond (= post nil) r :else (go-eval-stmt r post))))
(cond
@@ -972,9 +974,16 @@
(and (list? stmt) (= (first stmt) :defer))
(go-eval-defer-stmt env stmt)
(and (list? stmt) (= (first stmt) :go))
;; v0: synchronous spawn. A panic from the spawned expression
;; that the goroutine didn't recover propagates here — real
;; Go would crash the whole program; the sync model surfaces
;; it back to the spawner which has the same end-effect.
(let
((v (go-eval env (nth stmt 1))))
(cond (go-eval-error? v) v :else env))
(cond
(go-eval-error? v) v
(go-panic? v) v
:else env))
(and (list? stmt) (= (first stmt) :select))
(let
((r (go-eval-select-stmt env stmt)))

View File

@@ -1,12 +1,12 @@
{
"language": "go",
"total_pass": 509,
"total": 509,
"total_pass": 517,
"total": 517,
"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":92,"total":92,"status":"ok"},
{"name":"eval","pass":100,"total":100,"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"}

View File

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

View File

@@ -558,6 +558,62 @@
(go-env-lookup env "after"))
7)
(go-eval-test
"goroutine panic: surfaces synchronously back to spawner (v0)"
(let
((r (go-eval-program go-env-builtins (list (go-parse "func boom() { panic(\"goroutine\") }") (go-parse "go boom()")))))
r)
(list :go-panic "goroutine"))
(go-eval-test
"goroutine panic + spawner-defer-recover catches it (v0 sync)"
(let
((env (go-eval-program go-env-builtins (list (go-parse "func boom() { panic(\"g\") }") (go-parse "func main() { defer recover() ; go boom() }") (go-parse "main()") (go-parse "after := 11")))))
(go-env-lookup env "after"))
11)
(go-eval-test
"defer order with recover: all defers run, recover catches"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func p2(c chan int) { c <- 2 }") (go-parse "func rec(c chan int) { recover() ; c <- 7 }") (go-parse "func safe(c chan int) { defer p2(c) ; defer rec(c) ; panic(0) }") (go-parse "safe(ch)") (go-parse "a := <-ch") (go-parse "b := <-ch")))))
(list (go-env-lookup env "a") (go-env-lookup env "b")))
(list 7 2))
(go-eval-test
"defer fires when fn panics (not just normal return)"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func note(c chan int) { c <- 5 }") (go-parse "func safe(c chan int) { defer note(c) ; defer recover() ; panic(\"!\") }") (go-parse "safe(ch)") (go-parse "got := <-ch")))))
(go-env-lookup env "got"))
5)
(go-eval-test
"panic with nil value: still surfaces as (:go-panic nil)"
(let
((r (go-eval-program go-env-builtins (list (go-parse "panic(nil)")))))
r)
(list :go-panic nil))
(go-eval-test
"panic inside loop body: aborts loop + propagates"
(let
((r (go-eval-program go-env-builtins (list (go-parse "func find(x int) { for i := 0; i < 10; i = i + 1 { if i == x { panic(i) } } }") (go-parse "find(3)")))))
r)
(list :go-panic 3))
(go-eval-test
"defer in panicking fn: still runs even though no return reached"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func mark(c chan int) { c <- 8 }") (go-parse "func inner(c chan int) { defer mark(c) ; panic(\"!\") }") (go-parse "func outer(c chan int) { defer recover() ; inner(c) }") (go-parse "outer(ch)") (go-parse "got := <-ch")))))
(go-env-lookup env "got"))
8)
(go-eval-test
"defer fn captures args by value, not reference (re-confirm)"
(let
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func pushN(c chan int, v int) { c <- v }") (go-parse "func run(c chan int) { defer recover() ; x := 5 ; defer pushN(c, x) ; x = 999 ; panic(\"k\") }") (go-parse "run(ch)") (go-parse "got := <-ch")))))
(go-env-lookup env "got"))
5)
(define
go-eval-test-summary
(str "eval " go-eval-test-pass "/" go-eval-test-count))