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

@@ -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