go: defer + LIFO drain → eval 86/86, total 503/503 [shapes-scheduler]
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s

Phase 6 first slice. New :defer stmt dispatch, go-eval-defer-stmt
captures (callee, eagerly-evaluated args) onto a frame-local
__go-defer-stack mutable list. go-eval-call installs the stack and
drains LIFO before returning; go-eval-program does the same for
the implicit main frame. New :quoted-value AST node lets defer
re-invoke calls with the frozen arg values.

6 eval tests: single defer, multi-LIFO, args-eager-at-defer-time,
fires-on-early-return, frame-local (no bleed to outer), defer-in-loop.

Shape: defer is a per-frame cleanup queue (LIFO on frame exit) that
the scheduler kit will reuse for panic-unwind + clean-exit + select-
case-rollback paths. Distinct from the scheduler's ready-queue —
diary updated to keep that distinction explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 23:00:37 +00:00
parent 1d3021d206
commit 219e2fcfe7
6 changed files with 701 additions and 394 deletions

View File

@@ -356,9 +356,11 @@ Progress-log line → push `origin/loops/go`.
- **Acceptance:** runtime/ +20 tests.
### Phase 6 — `defer` + panic/recover ⬜
- Defer stack per function frame; runs LIFO on return (normal or panic).
- `panic(v)` unwinds frames running deferreds; `recover()` inside a
deferred fn captures the panic value and stops unwinding.
- [x] Defer stack per function frame; runs LIFO on normal return.
Args eager at defer-time; frame-local (inner defers don't run
outer ones); defer-in-loop pushes each iteration. 6 tests.
- [ ] `panic(v)` unwinds frames running deferreds; `recover()` inside a
deferred fn captures the panic value and stops unwinding.
- Goroutine panic propagation: a panicking goroutine that doesn't recover
crashes the whole program (honour Go spec, or document divergence).
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
@@ -609,6 +611,21 @@ 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 6 first slice: defer + LIFO.** Added
`go-eval-defer-stmt`, `go-run-defers!`, `go-run-defers-prefix!`,
plus new `:quoted-value` AST node so deferred calls can be
re-invoked with values captured at defer-time. Frame: `go-eval-call`
installs a fresh `__go-defer-stack` (mutable list) in the call env,
drains LIFO before returning. `go-eval-program` does the same for
the implicit main frame. 6 tests on eval/: single defer,
multi-defer LIFO, args eager at defer-time, defer fires on early
return, frame-local stack (inner defers don't bleed to outer),
defer-in-loop (all iterations defer to fn return). 503/503 total.
**Shape:** SX assignment shadows rather than mutates, so the
natural defer side-effect channel is the *channel buffer* — shared
via closure identity. Drove the test design and matches the eventual
panic/recover shape (errors will need to escape through a similar
out-of-band mechanism, not through env mutation). [shapes-scheduler]
- 2026-05-27 — **Phase 5 acceptance bar hit (40/40 runtime, 497/497
total).** Added `after(d)` builtin (v0 timer stub: returns a channel
already buffered with `:tick`) and 13 canonical-pattern tests:

View File

@@ -231,6 +231,42 @@ real result.
_Newest first. Append one dated entry per milestone landed._
- 2026-05-27 — **Phase 6 first slice: defer + LIFO observation.**
Go's defer is a *frame-local cleanup queue* — a list of (callee,
pre-evaluated-args) records appended on `defer`, drained LIFO at
frame exit. The scheduler kit needs the same shape because: (a) a
panicking goroutine must run its frame's defers before unwinding to
the next frame; (b) a goroutine that exits cleanly still runs them;
(c) `select` cases that own resources (an acquired send slot, a
buffer reservation) need a cleanup hook on the case-not-taken path.
All three reduce to the same primitive: **"hand the frame a list
of thunks; call them LIFO before the frame is gone."**
Concretely the kit should expose `frame-defer!` (push) and an
internal `frame-teardown!` (drained by the scheduler on exit / by
the panic unwinder on abort). The scheduler's exit-path becomes:
1. Mark frame done.
2. Call `frame-teardown!` — run defers LIFO. A defer that itself
panics: capture the new panic, continue running the rest
(matches Go spec).
3. Release frame slot.
Crucially the defer queue is *not* the same as the scheduler's
ready-queue — confusing the two was an early temptation. The defer
queue is per-frame and synchronous-on-exit; the ready-queue is
global and async. Phase 5b will need to keep these distinct when
real preemption lands.
Test signal that drove the shape: SX assignment shadows rather than
mutates, so the only observable side-effect channel for deferred
calls is `(append! buf ...)` on a value with stable identity (e.g.
a channel). That maps cleanly to "deferred work emits its effects
through capabilities the frame held, not through enclosing-env
mutation" — which is also how the scheduler kit's deferred work
should communicate with the rest of the system. No magic; just
capabilities the frame can hand to its defers.
- 2026-05-27 — **Phase 5 acceptance crossed (40 runtime tests).**
Final shape observation: *time-as-readiness-flip*. The Go side
added an `after(d)` builtin that returns a channel **already