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

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:
2026-05-27 23:20:46 +00:00
parent 219e2fcfe7
commit f52ad1fac6
6 changed files with 196 additions and 22 deletions

View File

@@ -24,7 +24,9 @@
(list "print" (list :go-builtin "print"))
(list "make" (list :go-builtin "make"))
(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
go-env-lookup
@@ -48,6 +50,27 @@
(not (= (len x) 0))
(= (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 ──────────────────────────────────────────────
(define
@@ -402,6 +425,27 @@
;; with-timeout patterns express the intent even though we
;; don't model real time yet.
(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)))))
(define
@@ -562,22 +606,37 @@
:else
(let ((call-env
(go-bind-names caller-env param-names arg-vals)))
;; Install a fresh defer stack for this call frame.
;; Mutated by go-eval-defer-stmt via append!; drained
;; LIFO before the call returns. Replaces any outer
;; frame's stack (defers are frame-local).
(let ((defer-stack (list)))
;; Install a fresh defer stack + panic cell for this
;; frame. Panic cell is (list STATE VALUE): :none if
;; nothing happened, :raised V if body panicked,
;; :recovered if a defer called recover() to swallow.
(let ((defer-stack (list))
(panic-cell (list :none nil)))
(let ((frame-env
(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
(= body nil)
(do (go-run-defers! frame-env defer-stack) nil)
(and (list? body) (= (first body) :block))
(let ((r (go-eval-block frame-env (nth body 1))))
(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)
(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))
(nth r 1)
(go-eval-error? r) r
@@ -931,7 +990,7 @@
:else r))
(and (list? stmt) (= (first stmt) :range-for))
(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
go-select-try-case
@@ -1313,21 +1372,36 @@
r
(go-eval-error? r)
r
(go-panic? r)
r
:else (go-eval-block r (rest stmts)))))))
(define
go-eval-program
;; Top-level driver. The "implicit main frame" gets its own defer
;; stack so `defer` at top level (which is what most runtime tests
;; use) behaves like deferring in main. The stack is drained after
;; all forms run.
;; Top-level driver = implicit main frame. Gets its own defer stack
;; and panic cell so `defer` and `recover()` at top level behave
;; like inside main(). Panic that escapes top-level surfaces as
;; the program's return value (tests use this to assert uncaught
;; panics).
(fn (env forms)
(let ((defer-stack (list)))
(let ((env (go-env-extend env "__go-defer-stack" defer-stack)))
(let ((defer-stack (list))
(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)))
(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)
r))))))
(cond
(= (nth panic-cell 0) :recovered) env
(= (nth panic-cell 0) :raised)
(list :go-panic (nth panic-cell 1))
:else r)))))))
(define
go-eval-program-loop
@@ -1343,6 +1417,8 @@
r
(go-eval-error? r)
r
(go-panic? r)
r
:else (go-eval-program-loop r (rest forms)))))))
(define