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
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user