go: panic + recover → eval 92/92, total 509/509, Phase 6 closed [shapes-scheduler]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
Panic/recover builtins + per-frame __go-panic-cell of shape (STATE V). Body panic flips cell :none→:raised BEFORE defers drain so recover() can find it. recover() walks env chain past shadowing cells to the outermost :raised one — flips it :recovered, returns V. Frame exit checks cell: :recovered → return clean; :raised → propagate (:go-panic V). 6 tests: uncaught-from-program, panic-from-fn, defer-recover-swallow, recover-captures-via-channel, propagation-through-no-defer-chain, middle-frame-catches-deeper-panic. Shape: panic cell is a frame-attached out-of-band channel that survives function boundaries via env-chain walk. Same primitive slots into the scheduler kit's termination-record + cleanup-with- error-context hook. Maps cleanly to Erlang try/catch/after. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
106
lib/go/eval.sx
106
lib/go/eval.sx
@@ -24,7 +24,9 @@
|
|||||||
(list "print" (list :go-builtin "print"))
|
(list "print" (list :go-builtin "print"))
|
||||||
(list "make" (list :go-builtin "make"))
|
(list "make" (list :go-builtin "make"))
|
||||||
(list "close" (list :go-builtin "close"))
|
(list "close" (list :go-builtin "close"))
|
||||||
(list "after" (list :go-builtin "after"))))
|
(list "after" (list :go-builtin "after"))
|
||||||
|
(list "panic" (list :go-builtin "panic"))
|
||||||
|
(list "recover" (list :go-builtin "recover"))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
go-env-lookup
|
go-env-lookup
|
||||||
@@ -48,6 +50,27 @@
|
|||||||
(not (= (len x) 0))
|
(not (= (len x) 0))
|
||||||
(= (first x) :eval-error))))
|
(= (first x) :eval-error))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-panic?
|
||||||
|
(fn (x)
|
||||||
|
(and (list? x) (not (= (len x) 0)) (= (first x) :go-panic))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
go-find-raised-panic-cell
|
||||||
|
;; Env is a list of (NAME VALUE) pairs. Find the first one whose
|
||||||
|
;; name is "__go-panic-cell" AND whose state slot is :raised.
|
||||||
|
;; Returns the cell (so recover() can mutate it) or nil.
|
||||||
|
(fn (env)
|
||||||
|
(cond
|
||||||
|
(or (= env nil) (= (len env) 0)) nil
|
||||||
|
:else
|
||||||
|
(let ((b (first env)))
|
||||||
|
(cond
|
||||||
|
(and (= (first b) "__go-panic-cell")
|
||||||
|
(= (nth (nth b 1) 0) :raised))
|
||||||
|
(nth b 1)
|
||||||
|
:else (go-find-raised-panic-cell (rest env)))))))
|
||||||
|
|
||||||
;; ── literal parsing ──────────────────────────────────────────────
|
;; ── literal parsing ──────────────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -402,6 +425,27 @@
|
|||||||
;; with-timeout patterns express the intent even though we
|
;; with-timeout patterns express the intent even though we
|
||||||
;; don't model real time yet.
|
;; don't model real time yet.
|
||||||
(let ((ch (go-make-chan))) (go-chan-send! ch :tick) ch)
|
(let ((ch (go-make-chan))) (go-chan-send! ch :tick) ch)
|
||||||
|
(= name "panic")
|
||||||
|
;; Returns a panic sentinel — propagated like :return-value
|
||||||
|
;; through statements/blocks; trapped by the enclosing frame
|
||||||
|
;; to drain defers, then either consumed by recover() or
|
||||||
|
;; re-raised. nil panic value is the implicit "nil panic".
|
||||||
|
(cond
|
||||||
|
(not (= (len vals) 1))
|
||||||
|
(list :eval-error :builtin-arity name 1 (len vals))
|
||||||
|
:else (list :go-panic (first vals)))
|
||||||
|
(= name "recover")
|
||||||
|
;; Walks env chain for the *outermost* panic cell currently
|
||||||
|
;; in :raised state — this is the panicking frame's cell,
|
||||||
|
;; reached through the deferred-call invocation chain.
|
||||||
|
;; Flips it to :recovered, returns V. Returns nil if no
|
||||||
|
;; panic is in flight.
|
||||||
|
(let ((cell (go-find-raised-panic-cell caller-env)))
|
||||||
|
(cond
|
||||||
|
(= cell nil) nil
|
||||||
|
:else
|
||||||
|
(let ((v (nth cell 1)))
|
||||||
|
(do (set-nth! cell 0 :recovered) v))))
|
||||||
:else (list :eval-error :unknown-builtin name)))))
|
:else (list :eval-error :unknown-builtin name)))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -562,22 +606,37 @@
|
|||||||
:else
|
:else
|
||||||
(let ((call-env
|
(let ((call-env
|
||||||
(go-bind-names caller-env param-names arg-vals)))
|
(go-bind-names caller-env param-names arg-vals)))
|
||||||
;; Install a fresh defer stack for this call frame.
|
;; Install a fresh defer stack + panic cell for this
|
||||||
;; Mutated by go-eval-defer-stmt via append!; drained
|
;; frame. Panic cell is (list STATE VALUE): :none if
|
||||||
;; LIFO before the call returns. Replaces any outer
|
;; nothing happened, :raised V if body panicked,
|
||||||
;; frame's stack (defers are frame-local).
|
;; :recovered if a defer called recover() to swallow.
|
||||||
(let ((defer-stack (list)))
|
(let ((defer-stack (list))
|
||||||
|
(panic-cell (list :none nil)))
|
||||||
(let ((frame-env
|
(let ((frame-env
|
||||||
(go-env-extend
|
(go-env-extend
|
||||||
call-env "__go-defer-stack" defer-stack)))
|
(go-env-extend
|
||||||
|
call-env "__go-defer-stack" defer-stack)
|
||||||
|
"__go-panic-cell" panic-cell)))
|
||||||
(cond
|
(cond
|
||||||
(= body nil)
|
(= body nil)
|
||||||
(do (go-run-defers! frame-env defer-stack) nil)
|
(do (go-run-defers! frame-env defer-stack) nil)
|
||||||
(and (list? body) (= (first body) :block))
|
(and (list? body) (= (first body) :block))
|
||||||
(let ((r (go-eval-block frame-env (nth body 1))))
|
(let ((r (go-eval-block frame-env (nth body 1))))
|
||||||
(do
|
(do
|
||||||
|
;; If body panicked, stash value before
|
||||||
|
;; defers run so recover() can see it.
|
||||||
|
(cond
|
||||||
|
(go-panic? r)
|
||||||
|
(do (set-nth! panic-cell 0 :raised)
|
||||||
|
(set-nth! panic-cell 1 (nth r 1)))
|
||||||
|
:else nil)
|
||||||
(go-run-defers! frame-env defer-stack)
|
(go-run-defers! frame-env defer-stack)
|
||||||
(cond
|
(cond
|
||||||
|
;; Recover called during defers — swallow.
|
||||||
|
(= (nth panic-cell 0) :recovered) nil
|
||||||
|
;; Still raised after defers — propagate.
|
||||||
|
(= (nth panic-cell 0) :raised)
|
||||||
|
(list :go-panic (nth panic-cell 1))
|
||||||
(and (list? r) (= (first r) :return-value))
|
(and (list? r) (= (first r) :return-value))
|
||||||
(nth r 1)
|
(nth r 1)
|
||||||
(go-eval-error? r) r
|
(go-eval-error? r) r
|
||||||
@@ -931,7 +990,7 @@
|
|||||||
:else r))
|
:else r))
|
||||||
(and (list? stmt) (= (first stmt) :range-for))
|
(and (list? stmt) (= (first stmt) :range-for))
|
||||||
(go-eval-range-for env stmt)
|
(go-eval-range-for env stmt)
|
||||||
:else (let ((v (go-eval env stmt))) (cond (go-eval-error? v) v :else env)))))
|
:else (let ((v (go-eval env stmt))) (cond (go-eval-error? v) v (go-panic? v) v :else env)))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
go-select-try-case
|
go-select-try-case
|
||||||
@@ -1313,21 +1372,36 @@
|
|||||||
r
|
r
|
||||||
(go-eval-error? r)
|
(go-eval-error? r)
|
||||||
r
|
r
|
||||||
|
(go-panic? r)
|
||||||
|
r
|
||||||
:else (go-eval-block r (rest stmts)))))))
|
:else (go-eval-block r (rest stmts)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
go-eval-program
|
go-eval-program
|
||||||
;; Top-level driver. The "implicit main frame" gets its own defer
|
;; Top-level driver = implicit main frame. Gets its own defer stack
|
||||||
;; stack so `defer` at top level (which is what most runtime tests
|
;; and panic cell so `defer` and `recover()` at top level behave
|
||||||
;; use) behaves like deferring in main. The stack is drained after
|
;; like inside main(). Panic that escapes top-level surfaces as
|
||||||
;; all forms run.
|
;; the program's return value (tests use this to assert uncaught
|
||||||
|
;; panics).
|
||||||
(fn (env forms)
|
(fn (env forms)
|
||||||
(let ((defer-stack (list)))
|
(let ((defer-stack (list))
|
||||||
(let ((env (go-env-extend env "__go-defer-stack" defer-stack)))
|
(panic-cell (list :none nil)))
|
||||||
|
(let ((env (go-env-extend
|
||||||
|
(go-env-extend env "__go-defer-stack" defer-stack)
|
||||||
|
"__go-panic-cell" panic-cell)))
|
||||||
(let ((r (go-eval-program-loop env forms)))
|
(let ((r (go-eval-program-loop env forms)))
|
||||||
(do
|
(do
|
||||||
|
(cond
|
||||||
|
(go-panic? r)
|
||||||
|
(do (set-nth! panic-cell 0 :raised)
|
||||||
|
(set-nth! panic-cell 1 (nth r 1)))
|
||||||
|
:else nil)
|
||||||
(go-run-defers! env defer-stack)
|
(go-run-defers! env defer-stack)
|
||||||
r))))))
|
(cond
|
||||||
|
(= (nth panic-cell 0) :recovered) env
|
||||||
|
(= (nth panic-cell 0) :raised)
|
||||||
|
(list :go-panic (nth panic-cell 1))
|
||||||
|
:else r)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
go-eval-program-loop
|
go-eval-program-loop
|
||||||
@@ -1343,6 +1417,8 @@
|
|||||||
r
|
r
|
||||||
(go-eval-error? r)
|
(go-eval-error? r)
|
||||||
r
|
r
|
||||||
|
(go-panic? r)
|
||||||
|
r
|
||||||
:else (go-eval-program-loop r (rest forms)))))))
|
:else (go-eval-program-loop r (rest forms)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"language": "go",
|
"language": "go",
|
||||||
"total_pass": 503,
|
"total_pass": 509,
|
||||||
"total": 503,
|
"total": 509,
|
||||||
"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":176,"total":176,"status":"ok"},
|
||||||
{"name":"types","pass":72,"total":72,"status":"ok"},
|
{"name":"types","pass":72,"total":72,"status":"ok"},
|
||||||
{"name":"eval","pass":86,"total":86,"status":"ok"},
|
{"name":"eval","pass":92,"total":92,"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"}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Go-on-SX Scoreboard
|
# Go-on-SX Scoreboard
|
||||||
|
|
||||||
**Total: 503 / 503 tests passing**
|
**Total: 509 / 509 tests passing**
|
||||||
|
|
||||||
| | Suite | Pass | Total |
|
| | Suite | Pass | Total |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ✅ | lex | 129 | 129 |
|
| ✅ | lex | 129 | 129 |
|
||||||
| ✅ | parse | 176 | 176 |
|
| ✅ | parse | 176 | 176 |
|
||||||
| ✅ | types | 72 | 72 |
|
| ✅ | types | 72 | 72 |
|
||||||
| ✅ | eval | 86 | 86 |
|
| ✅ | eval | 92 | 92 |
|
||||||
| ✅ | runtime | 40 | 40 |
|
| ✅ | runtime | 40 | 40 |
|
||||||
| ⬜ | stdlib | 0 | 0 |
|
| ⬜ | stdlib | 0 | 0 |
|
||||||
| ⬜ | e2e | 0 | 0 |
|
| ⬜ | e2e | 0 | 0 |
|
||||||
|
|||||||
@@ -516,6 +516,48 @@
|
|||||||
(go-env-lookup env "e")))
|
(go-env-lookup env "e")))
|
||||||
(list 3 2 1 0))
|
(list 3 2 1 0))
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"panic: uncaught panic surfaces as (:go-panic V) from program"
|
||||||
|
(let
|
||||||
|
((r (go-eval-program go-env-builtins (list (go-parse "panic(\"boom\")")))))
|
||||||
|
r)
|
||||||
|
(list :go-panic "boom"))
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"panic inside fn: surfaces from fn call too"
|
||||||
|
(let
|
||||||
|
((r (go-eval-program go-env-builtins (list (go-parse "func boom() { panic(\"oops\") }") (go-parse "boom()")))))
|
||||||
|
r)
|
||||||
|
(list :go-panic "oops"))
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"recover: deferred recover swallows panic, fn returns normally"
|
||||||
|
(let
|
||||||
|
((env (go-eval-program go-env-builtins (list (go-parse "func safe() { defer recover() ; panic(\"x\") }") (go-parse "safe()") (go-parse "after := 42")))))
|
||||||
|
(go-env-lookup env "after"))
|
||||||
|
42)
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"recover: deferred recover captures the panic value"
|
||||||
|
(let
|
||||||
|
((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func grab(c chan int) { r := recover() ; c <- r }") (go-parse "func safe(c chan int) { defer grab(c) ; panic(99) }") (go-parse "safe(ch)") (go-parse "got := <-ch")))))
|
||||||
|
(go-env-lookup env "got"))
|
||||||
|
99)
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"panic: propagates through intermediate frames without defers"
|
||||||
|
(let
|
||||||
|
((r (go-eval-program go-env-builtins (list (go-parse "func inner() { panic(\"deep\") }") (go-parse "func middle() { inner() }") (go-parse "func outer() { middle() }") (go-parse "outer()")))))
|
||||||
|
r)
|
||||||
|
(list :go-panic "deep"))
|
||||||
|
|
||||||
|
(go-eval-test
|
||||||
|
"recover: middle-frame defer catches panic from deeper frame"
|
||||||
|
(let
|
||||||
|
((env (go-eval-program go-env-builtins (list (go-parse "func inner() { panic(\"deep\") }") (go-parse "func middle() { inner() }") (go-parse "func outer() { defer recover() ; middle() }") (go-parse "outer()") (go-parse "after := 7")))))
|
||||||
|
(go-env-lookup env "after"))
|
||||||
|
7)
|
||||||
|
|
||||||
(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))
|
||||||
|
|||||||
@@ -359,8 +359,12 @@ Progress-log line → push `origin/loops/go`.
|
|||||||
- [x] Defer stack per function frame; runs LIFO on normal return.
|
- [x] Defer stack per function frame; runs LIFO on normal return.
|
||||||
Args eager at defer-time; frame-local (inner defers don't run
|
Args eager at defer-time; frame-local (inner defers don't run
|
||||||
outer ones); defer-in-loop pushes each iteration. 6 tests.
|
outer ones); defer-in-loop pushes each iteration. 6 tests.
|
||||||
- [ ] `panic(v)` unwinds frames running deferreds; `recover()` inside a
|
- [x] `panic(v)` unwinds frames running deferreds; `recover()` inside a
|
||||||
deferred fn captures the panic value and stops unwinding.
|
deferred fn captures the panic value and stops unwinding. Panic
|
||||||
|
sentinel `(:go-panic V)` propagates through go-eval-block /
|
||||||
|
go-eval-stmt / go-eval-program-loop. Per-frame panic cell
|
||||||
|
`(STATE V)` flips :none → :raised → :recovered; recover walks
|
||||||
|
env chain finding the outermost :raised cell. 6 tests on eval/.
|
||||||
- Goroutine panic propagation: a panicking goroutine that doesn't recover
|
- Goroutine panic propagation: a panicking goroutine that doesn't recover
|
||||||
crashes the whole program (honour Go spec, or document divergence).
|
crashes the whole program (honour Go spec, or document divergence).
|
||||||
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
|
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
|
||||||
@@ -611,6 +615,24 @@ 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-27 — **Phase 6: panic + recover.** `panic` and `recover`
|
||||||
|
builtins. Panic sentinel `(:go-panic V)` propagates like
|
||||||
|
`:return-value` through stmt/block/program-loop. Each call frame
|
||||||
|
gets its own `__go-panic-cell` of shape `(STATE V)`; on body panic
|
||||||
|
the cell flips to `:raised V` BEFORE defers drain so `recover()`
|
||||||
|
can find it. `recover` walks the env chain looking for the
|
||||||
|
outermost `:raised` cell (so deferred calls invoked from a
|
||||||
|
panicking frame still see that frame's cell despite their own
|
||||||
|
nested cell shadowing it). Flips to `:recovered` and returns V;
|
||||||
|
the panicking frame then returns normally instead of propagating.
|
||||||
|
6 tests: uncaught surfaces from program, panic from fn surfaces,
|
||||||
|
defer-recover swallows, defer-recover captures value via channel,
|
||||||
|
propagation through 3-deep no-defer chain, middle frame catches
|
||||||
|
panic from deeper. 509/509 total. **Shape:** the panic cell is
|
||||||
|
a *frame-attached out-of-band channel* that survives across the
|
||||||
|
function boundary because env-chain lookup can walk past
|
||||||
|
shadowed bindings to find it. Same primitive will serve as the
|
||||||
|
scheduler kit's "cleanup-with-error-context" hook. [shapes-scheduler]
|
||||||
- 2026-05-27 — **Phase 6 first slice: defer + LIFO.** Added
|
- 2026-05-27 — **Phase 6 first slice: defer + LIFO.** Added
|
||||||
`go-eval-defer-stmt`, `go-run-defers!`, `go-run-defers-prefix!`,
|
`go-eval-defer-stmt`, `go-run-defers!`, `go-run-defers-prefix!`,
|
||||||
plus new `:quoted-value` AST node so deferred calls can be
|
plus new `:quoted-value` AST node so deferred calls can be
|
||||||
|
|||||||
@@ -231,6 +231,40 @@ real result.
|
|||||||
|
|
||||||
_Newest first. Append one dated entry per milestone landed._
|
_Newest first. Append one dated entry per milestone landed._
|
||||||
|
|
||||||
|
- 2026-05-27 — **Phase 6: panic/recover shape lands.** The panic
|
||||||
|
cell is the missing piece. It's a per-frame mutable record of
|
||||||
|
shape `(STATE VALUE)` carrying one of `:none` / `:raised` /
|
||||||
|
`:recovered`. Three properties matter for the scheduler kit:
|
||||||
|
|
||||||
|
1. **It survives the function boundary** via env-chain lookup —
|
||||||
|
when a deferred call's own frame creates a shadowing cell,
|
||||||
|
`recover()` walks past it to find the OUTER frame's cell (the
|
||||||
|
one that's `:raised`). This is the same mechanism the
|
||||||
|
scheduler will need when a panic-unwinding goroutine has
|
||||||
|
multiple frames each carrying their own state, and the
|
||||||
|
"current panic" must be locatable from any depth.
|
||||||
|
|
||||||
|
2. **It flips state in place** (`set-nth!`) so that the change
|
||||||
|
made by `recover()` deep in a defer chain is visible to the
|
||||||
|
enclosing frame's exit check. The scheduler kit needs the
|
||||||
|
same pattern: a goroutine's "termination reason" must be
|
||||||
|
writable by any frame in its stack.
|
||||||
|
|
||||||
|
3. **It's distinct from the return-value channel.** A frame can
|
||||||
|
carry both `(:go-panic V)` from its body AND a recovery
|
||||||
|
commitment in its panic cell; they're checked in sequence.
|
||||||
|
For the scheduler this maps to: a goroutine carries both its
|
||||||
|
running-state (channel-blocked, ready, sleeping) AND its
|
||||||
|
termination-record (panic V / clean exit / killed) — two
|
||||||
|
orthogonal slots, not one tag.
|
||||||
|
|
||||||
|
Concrete kit hint: every frame record should expose
|
||||||
|
`frame-panic-cell` alongside `frame-defer-queue`. The scheduler's
|
||||||
|
exit-path becomes: drain defers (cell may flip :raised→:recovered)
|
||||||
|
→ consult cell → either propagate or return clean. Erlang's
|
||||||
|
`try/catch/after` decomposes identically: `after` is the defer
|
||||||
|
queue, `catch` is the recover-via-cell mechanism.
|
||||||
|
|
||||||
- 2026-05-27 — **Phase 6 first slice: defer + LIFO observation.**
|
- 2026-05-27 — **Phase 6 first slice: defer + LIFO observation.**
|
||||||
Go's defer is a *frame-local cleanup queue* — a list of (callee,
|
Go's defer is a *frame-local cleanup queue* — a list of (callee,
|
||||||
pre-evaluated-args) records appended on `defer`, drained LIFO at
|
pre-evaluated-args) records appended on `defer`, drained LIFO at
|
||||||
|
|||||||
Reference in New Issue
Block a user