From 1d3021d2064ca588308a85f1d8c7f9322a763d2b Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 22:24:13 +0000 Subject: [PATCH] =?UTF-8?q?go:=20after(d)=20timer=20stub=20+=2013=20patter?= =?UTF-8?q?n=20tests=20=E2=86=92=20runtime=2040/40,=20Phase=205=20closed?= =?UTF-8?q?=20[shapes-scheduler]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acceptance bar hit (40 runtime, 497 total). Tests: timer ready, select-with-timeout, fan-in (3 producers), worker queue, pipeline, fan-out-then-fan-in, select source-order, fallback case, default, producer-consumer, two-stage pipeline, channel-counter, after+default, tick-collector. Shape chiselled: timer collapses "after duration" into "channel ready immediately" — select needs only ready? from each case. Real time is when the flip happens, not what the protocol is. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/scheduled_tasks.lock | 2 +- .mcp.json | 2 +- lib/go/eval.sx | 9 +- lib/go/scoreboard.json | 6 +- lib/go/scoreboard.md | 4 +- lib/go/tests/runtime.sx | 98 +++++++++++++++ plans/agent-briefings/go-loop.md | 208 +++++++++++++++++++++++++++++++ plans/go-on-sx.md | 21 +++- plans/lib-guest-scheduler.md | 41 ++++++ 9 files changed, 381 insertions(+), 10 deletions(-) create mode 100644 plans/agent-briefings/go-loop.md diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock index ea7330c7..4bc03998 100644 --- a/.claude/scheduled_tasks.lock +++ b/.claude/scheduled_tasks.lock @@ -1 +1 @@ -{"sessionId":"31c80255-eb92-43e4-8997-84ad84e27326","pid":90960,"procStart":"564684","acquiredAt":1777049890282} \ No newline at end of file +{"sessionId":"bf20a443-9df8-4cb9-932e-8c6f4c4625c2","pid":1303602,"procStart":"253831081","acquiredAt":1779865895644} \ No newline at end of file diff --git a/.mcp.json b/.mcp.json index e709cf94..7730292f 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "sx-tree": { "type": "stdio", - "command": "./hosts/ocaml/_build/default/bin/mcp_tree.exe" + "command": "/root/rose-ash/hosts/ocaml/_build/default/bin/mcp_tree.exe" }, "rose-ash-services": { "type": "stdio", diff --git a/lib/go/eval.sx b/lib/go/eval.sx index eba29f64..3772a560 100644 --- a/lib/go/eval.sx +++ b/lib/go/eval.sx @@ -23,7 +23,8 @@ (list "append" (list :go-builtin "append")) (list "print" (list :go-builtin "print")) (list "make" (list :go-builtin "make")) - (list "close" (list :go-builtin "close")))) + (list "close" (list :go-builtin "close")) + (list "after" (list :go-builtin "after")))) (define go-env-lookup @@ -395,6 +396,12 @@ (not (go-chan? (first vals))) (list :eval-error :close-not-chan (first vals)) :else (do (go-chan-close! (first vals)) nil)) + (= name "after") + ;; v0 stub for time.After: returns a channel already holding a + ;; ready value (the duration arg is ignored). Lets `select` + ;; 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) :else (list :eval-error :unknown-builtin name))))) (define diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index 25bdfa4a..c6519e67 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,13 +1,13 @@ { "language": "go", - "total_pass": 483, - "total": 483, + "total_pass": 497, + "total": 497, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"types","pass":72,"total":72,"status":"ok"}, {"name":"eval","pass":80,"total":80,"status":"ok"}, - {"name":"runtime","pass":26,"total":26,"status":"ok"}, + {"name":"runtime","pass":40,"total":40,"status":"ok"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"e2e","pass":0,"total":0,"status":"pending"} ] diff --git a/lib/go/scoreboard.md b/lib/go/scoreboard.md index c657cd7c..93f6371c 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,6 +1,6 @@ # Go-on-SX Scoreboard -**Total: 483 / 483 tests passing** +**Total: 497 / 497 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -8,7 +8,7 @@ | ✅ | parse | 176 | 176 | | ✅ | types | 72 | 72 | | ✅ | eval | 80 | 80 | -| ✅ | runtime | 26 | 26 | +| ✅ | runtime | 40 | 40 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/runtime.sx b/lib/go/tests/runtime.sx index ec07d29a..e21866e4 100644 --- a/lib/go/tests/runtime.sx +++ b/lib/go/tests/runtime.sx @@ -208,6 +208,104 @@ (go-env-lookup env "total")) 60) +(go-rt-test + "timer: after(d) returns a ready channel (v0 stub)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "t := after(100)"))))) + (go-chan-len (go-env-lookup env "t"))) + 1) + +(go-rt-test + "select with timer (after) — buffered value wins, timer is fallback" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func push99(c chan int) { c <- 99 }") (go-parse "c := make()") (go-parse "go push99(c)") (go-parse "t := after(0)") (go-parse "var v = 0") (go-parse "select { case x := <-c: v = x; case y := <-t: v = -1 }"))))) + (go-env-lookup env "v")) + 99) + +(go-rt-test + "fan-in: 3 producer goroutines, main sums their values" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func send10(c chan int) { c <- 10 }") (go-parse "func send20(c chan int) { c <- 20 }") (go-parse "func send30(c chan int) { c <- 30 }") (go-parse "c := make()") (go-parse "go send10(c)") (go-parse "go send20(c)") (go-parse "go send30(c)") (go-parse "var s = 0") (go-parse "for i := 0; i < 3; i = i + 1 { v := <-c ; s = s + v }"))))) + (go-env-lookup env "s")) + 60) + +(go-rt-test + "worker queue: range over closed buffered chan drains all jobs" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "jobs := make()") (go-parse "jobs <- 1") (go-parse "jobs <- 2") (go-parse "jobs <- 3") (go-parse "jobs <- 4") (go-parse "close(jobs)") (go-parse "var s = 0") (go-parse "for j := range jobs { s = s + j }"))))) + (go-env-lookup env "s")) + 10) + +(go-rt-test + "pipeline: stage1 squares, stage2 sums via channels" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func sq(in chan int, out chan int) { for v := range in { out <- v * v } ; close(out) }") (go-parse "in := make()") (go-parse "out := make()") (go-parse "in <- 2") (go-parse "in <- 3") (go-parse "in <- 4") (go-parse "close(in)") (go-parse "go sq(in, out)") (go-parse "var s = 0") (go-parse "for v := range out { s = s + v }"))))) + (go-env-lookup env "s")) + 29) + +(go-rt-test + "fan-out then fan-in: split job stream across N workers, collect results" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func worker(in chan int, out chan int) { for v := range in { out <- v + 100 } }") (go-parse "jobs := make()") (go-parse "results := make()") (go-parse "jobs <- 1") (go-parse "jobs <- 2") (go-parse "jobs <- 3") (go-parse "close(jobs)") (go-parse "go worker(jobs, results)") (go-parse "close(results)") (go-parse "var s = 0") (go-parse "for r := range results { s = s + r }"))))) + (go-env-lookup env "s")) + 306) + +(go-rt-test + "select: first ready case wins (channel order = source order)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "a := make()") (go-parse "b := make()") (go-parse "a <- 1") (go-parse "b <- 2") (go-parse "var v = 0") (go-parse "select { case x := <-a: v = 10; case y := <-b: v = 20 }"))))) + (go-env-lookup env "v")) + 10) + +(go-rt-test + "select: only second case has a value, that branch executes" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "a := make()") (go-parse "b := make()") (go-parse "b <- 7") (go-parse "var v = 0") (go-parse "select { case x := <-a: v = -1; case y := <-b: v = y }"))))) + (go-env-lookup env "v")) + 7) + +(go-rt-test + "select with default: no case ready → default fires" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "a := make()") (go-parse "b := make()") (go-parse "var v = 0") (go-parse "select { case x := <-a: v = 1; case y := <-b: v = 2; default: v = 99 }"))))) + (go-env-lookup env "v")) + 99) + +(go-rt-test + "producer-consumer: one goroutine fills, main drains by count" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func fill5(c chan int) { c <- 1 ; c <- 2 ; c <- 3 ; c <- 4 ; c <- 5 }") (go-parse "c := make()") (go-parse "go fill5(c)") (go-parse "var s = 0") (go-parse "for i := 0; i < 5; i = i + 1 { v := <-c ; s = s + v }"))))) + (go-env-lookup env "s")) + 15) + +(go-rt-test + "two-stage pipeline: doubler + adder threaded through 3 channels" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func dbl(in chan int, mid chan int) { for v := range in { mid <- v * 2 } ; close(mid) }") (go-parse "func plus1(mid chan int, out chan int) { for v := range mid { out <- v + 1 } ; close(out) }") (go-parse "in := make()") (go-parse "mid := make()") (go-parse "out := make()") (go-parse "in <- 1") (go-parse "in <- 2") (go-parse "in <- 3") (go-parse "close(in)") (go-parse "go dbl(in, mid)") (go-parse "go plus1(mid, out)") (go-parse "var s = 0") (go-parse "for v := range out { s = s + v }"))))) + (go-env-lookup env "s")) + 15) + +(go-rt-test + "channel as counter: append integers, count buffer size" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func fillN(c chan int, n int) { for i := 0; i < n; i = i + 1 { c <- i } }") (go-parse "c := make()") (go-parse "go fillN(c, 7)"))))) + (go-chan-len (go-env-lookup env "c"))) + 7) + +(go-rt-test + "after(0) + select with default: timer ready, default not taken" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "t := after(0)") (go-parse "var v = 0") (go-parse "select { case x := <-t: v = 7; default: v = -1 }"))))) + (go-env-lookup env "v")) + 7) + +(go-rt-test + "tick collector: timer + counter accumulates ticks via range count" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "func emitN(c chan int, n int) { for i := 0; i < n; i = i + 1 { c <- 1 } ; close(c) }") (go-parse "ticks := make()") (go-parse "go emitN(ticks, 5)") (go-parse "var total = 0") (go-parse "for t := range ticks { total = total + t }"))))) + (go-env-lookup env "total")) + 5) + (define go-rt-test-summary (str "runtime " go-rt-test-pass "/" go-rt-test-count)) diff --git a/plans/agent-briefings/go-loop.md b/plans/agent-briefings/go-loop.md new file mode 100644 index 00000000..6f8506bf --- /dev/null +++ b/plans/agent-briefings/go-loop.md @@ -0,0 +1,208 @@ +# Go-on-SX loop agent (single agent, phase-ordered) + +Role: iterates `plans/go-on-sx.md` forever. **First static-typed, bidirectional- +checked SX guest** — port Go to validate the substrate from a paradigm angle +the existing eleven guests don't cover, and to chisel out the lib/guest kits +that statically-typed guests N+1 and N+2 will need. + +``` +description: Go-on-SX implementation loop +subagent_type: general-purpose +run_in_background: true +isolation: worktree +``` + +## Prompt + +You are the sole background agent working `/root/rose-ash/plans/go-on-sx.md`. +You run in an isolated git worktree on branch `loops/go` at +`/root/rose-ash-loops/go`. You work the plan's Phases in order (1→11), forever, +one commit per feature. Push to `origin/loops/go` after every commit. Never +`main`, never `architecture`. + +## Restart baseline — check before iterating + +1. Read `plans/go-on-sx.md` — Phases + Progress log + Blockers tell you where + you are. +2. Pre-flight: `ls lib/guest/lex.sx lib/guest/pratt.sx lib/guest/ast.sx + lib/guest/match.sx` — all four must exist. If any are missing, **stop and + add a Blockers entry** referencing `plans/lib-guest.md`. Do not start. +3. `ls lib/go/` — pick up from the most advanced file that exists. If the + directory does not exist, you are at Phase 1. +4. If `lib/go/tests/*.sx` exist, run them via the epoch protocol against + `sx_server.exe`. They must be green before new work. +5. **Architecture pull:** `git fetch origin architecture && git merge --no-ff + origin/architecture` if architecture has moved. Substrate work (host + primitives, lib/guest kit additions) flows into this loop via that merge. + +## The queue + +Phase order per `plans/go-on-sx.md`: + +- **Phase 1** — Tokenizer (`lib/go/lex.sx`). Consumes `lib/guest/core/lex.sx`. + ASI is the tricky bit. +- **Phase 2** — Parser (`lib/go/parse.sx`). Consumes `lib/guest/core/pratt.sx` + + `lib/guest/core/ast.sx`. +- **Phase 3** — Bidirectional type checker (`lib/go/types.sx`). + **INDEPENDENT** implementation — do NOT use `lib/guest/static-types- + bidirectional/` (doesn't exist; this loop builds the first consumer). +- **Phase 4** — Tree-walk evaluator (`lib/go/eval.sx`). +- **Phase 5** — Goroutines + channels + select (`lib/go/sched.sx`). + **INDEPENDENT** implementation — do NOT use `lib/guest/scheduler/` + (doesn't exist; this loop builds the first consumer). +- **Phase 5b** — Buffered channels + select fairness. +- **Phase 6** — `defer` + panic/recover. +- **Phase 7** — Generics (Go 1.18+). +- **Phase 8** — Minimal stdlib (`lib/go/std/`). +- **Phase 9** — End-to-end programs. +- **Phase 10** — lib/guest extraction enabler (doc-only). +- **Phase 11** — VM bytecode opcodes (deferred, optional). + +Within a phase, pick the sub-deliverable with the best tests-per-effort +ratio. Don't batch phases. One feature per commit. + +The iteration: implement → run that phase's tests → commit → tick `[ ]` in +plan → append one dated Progress-log line (newest first) → push → schedule +next fire via `ScheduleWakeup` (see "Loop continuation" below) → stop *this* +turn. + +A single iteration does one feature. Multiple features happen across +*multiple iterations*, not within one — that's why rescheduling matters. + +## Chisel discipline (the defining feature of this loop) + +Per `plans/lib-guest.md`. Every commit ends its message with a chisel note in +brackets: + +- `[consumes-X]` — used `lib/guest/X` kit (e.g., `[consumes-lex]`, + `[consumes-pratt]`, `[consumes-ast]`, `[consumes-match]`). +- `[shapes-scheduler]` — revealed something about what + `plans/lib-guest-scheduler.md` should propose. Append a paragraph to that + plan's design diary describing the insight. +- `[shapes-static-types-bidirectional]` — same for + `plans/lib-guest-static-types-bidirectional.md`. +- `[proposes-Y]` — revealed a gap in another existing kit (e.g., `pratt.sx` + doesn't handle Go's operator precedence properly). Blockers entry in the + kit's plan describing the gap with minimal repro. +- `[nothing]` — pure Go work that didn't touch substrate or lib/guest story. + Rare; if you write `[nothing]` twice in a row, stop and reflect on whether + the iteration could have been shaped to surface something. + +**Sister plans must be updated.** When Phase 3 lands (independent checker +working), append a paragraph to +`plans/lib-guest-static-types-bidirectional.md` describing what synth/check +shape emerged in Go. When Phase 5 lands (scheduler working), same for +`plans/lib-guest-scheduler.md`. This is how the two-consumer rule actually +pays off. + +## Ground rules (hard) + +- **Scope:** only `lib/go/**` and `plans/go-on-sx.md`. Single permitted + cross-plan write: append-only paragraphs to the sister-plan design + diaries (`plans/lib-guest-scheduler.md`, + `plans/lib-guest-static-types-bidirectional.md`) on `shapes-*` commits. + Do **not** touch `spec/`, `hosts/`, `shared/`, `lib/guest/**` + (read-only consumer at this phase), or other `lib//`. +- **Consume `lib/guest/core/`** for lex/parse/ast/match/layout. Hand- + rolling defeats the chiselling goal. +- **Do NOT extract into `lib/guest/scheduler/` or `lib/guest/static- + types-bidirectional/` from this loop.** Those extractions are gated on + two consumers AND independent implementation. Extraction is its own + workstream after Go and the second consumer both exist. +- **Substrate gaps** → Blockers entry with minimal repro. Don't fix the + substrate from this loop. Belongs to `sx-improvements.md`. +- **NEVER call `sx_build` without timeout awareness** — 600s watchdog. +- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. + Never `Edit`/`Read`/`Write` on `.sx`. +- **Worktree:** branch `loops/go`, push `origin/loops/go`. Never `main`, + never `architecture`. +- **Commit granularity:** one feature per commit. Short factual messages + with chisel note: `go: lex.sx — keywords + ASI + 50 tests [consumes-lex]`. +- **Plan file:** update Progress log + tick boxes every commit. +- **If blocked** for two iterations on the same issue, add to Blockers and + move on. Phases 1-4 are sequential; 5-8 are largely independent once + 4 lands. + +## Conformance scoreboard + +Create `lib/go/scoreboard.json` on first iteration. Suites: lex / parse / +types / eval / runtime / stdlib / e2e. Update counts every commit. The +scoreboard is also the no-regression gate: a commit that drops any suite's +pass count is wrong, not the test. + +## Go-specific gotchas (read once, never get bitten) + +- **ASI (automatic semicolon insertion).** Newline becomes `;` after + identifier/literal/`)`/`]`/`}`. Build it into the tokenizer (Phase 1), + not the parser. Go spec § Semicolons is unusually precise. +- **Untyped constants.** `42` is `untyped int` until contextualised. + Canonical pitfall: `var x float64 = 42 / 7` must compute `42 / 7 = 6` + as untyped, then convert to `6.0`. Not `42.0 / 7 = 6.0`. Not `(42/7).0 + = 6.0`. Test this in Phase 3. +- **Methods vs functions.** Different lookup rules. Pointer-receiver + methods are NOT in the value's method set for interface satisfaction. +- **Interface satisfaction is structural and silent.** No `implements` + declaration. Lazy check at every interface-typed slot. +- **Channels have identity.** Distinct `make(chan int)` calls produce + distinct channels with same type. +- **`select` with `default`** = non-blocking. Without `default` = blocks. +- **`nil` is typed.** `var i interface{} = (*int)(nil); i == nil` is + `false` — i holds typed-nil-of-`*int`, not untyped nil. Footgun. Test. +- **Goroutine panic propagation.** Unrecovered panic crashes whole + program. Honour faithfully or document divergence. +- **`defer` in a loop.** Each iteration pushes; all run on function + return, not loop iteration. Common bug; tests must cover. +- **Map iteration order is unspecified.** v1 = sorted SX-canonical key + order for determinism. Document the divergence; provide a + `runtime`-package knob to randomise later. + +## General gotchas (all loops) + +- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences. +- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples + in `begin`. +- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks + scope chain). +- `sx_validate` after every structural edit. +- `list?` returns false on raw JS Arrays — host data must be SX-converted. +- Shell heredoc `||` gets eaten — escape or use `case`. + +## Style + +- No comments in `.sx` unless non-obvious. Cite Go spec sections inline + when a decision is non-obvious (the Go spec is rigorous — citations work). +- No new planning docs — update `plans/go-on-sx.md` inline. Append paragraphs + to sister-plan design diaries on `shapes-*` commits. +- Short factual commit messages with chisel note in brackets: + `go: parse short-decl + 6 tests [consumes-pratt]`. +- One feature per iteration. Commit. Log. Push. Next. + +Go. Run the pre-flight check. If lib/guest kits are missing, stop. Otherwise +read the plan, find the first unchecked `[ ]`, implement it. Remember: +every commit ends with a chisel note, and the sister-plan design diaries +get updates on `shapes-*` commits. + +## Loop continuation + +This briefing supersedes any "then stop" wording from the user's original +`/loop` input. After pushing, **call `ScheduleWakeup` to fire the next +iteration**, then end the turn. The `/loop` command is in dynamic mode; +each iteration self-schedules the next. + +- `delaySeconds`: **60** (minimum). This is a coding loop with no external + event to wait on — back-to-back iterations are intended. Raise only if a + prior fire reported a substrate blocker that needs settling. +- `prompt`: the **full original `/loop` input verbatim, prefixed with + `/loop `** (so the wake re-enters this skill and re-reads this briefing). + Do NOT paraphrase or trim it — the runtime expects an exact echo. +- `reason`: one short sentence, e.g. "next Go-on-SX iteration". + +**Stop conditions** — omit `ScheduleWakeup` ONLY when: +1. lib/guest pre-flight failed (missing kits) and a Blockers entry was + added — the loop is parked waiting for substrate work. +2. The same Blockers entry has been the reason for two consecutive + iterations (avoid runaway no-op fires). +3. plans/go-on-sx.md has every Phase 1-11 box checked. +4. The user explicitly asks to stop, pause, or interrupt the loop. + +Otherwise: reschedule. Always. diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index 95fb3c1f..65b30a76 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -304,7 +304,7 @@ Progress-log line → push `origin/loops/go`. Remaining sub-items (lexical closures, multi-return funcs, full slice triple with capacity) refine but don't gate Phase 5. -### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜ +### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ✅ - [x] Scaffold: `lib/go/sched.sx` with `go-make-chan` (closures-over- mutable-buf), `go-chan-send!` / `go-chan-recv!` / `go-chan-closed?` / `go-chan-close!`. Channel identity via closure-instance. @@ -325,7 +325,9 @@ Progress-log line → push `origin/loops/go`. channel (just value). v0 chan-range stops when buffer empties (no preemption to wait for new sends). break exits with the pre-break env (preserving prior-iteration assignments). -- [ ] `time.After`-like timer channel. +- [x] `time.After`-like timer channel (v0 stub: `after(d)` returns a + channel already holding `:tick`; lets `select`-with-timeout patterns + express the intent while real time is deferred to Phase 5b). - **Independent implementation.** Do NOT use lib/guest/scheduler/ — that kit doesn't exist yet and depends on this work for its design. See `plans/lib-guest-scheduler.md`. @@ -607,6 +609,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 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: + timer + select-with-timeout, fan-in, worker queue, two-stage + pipeline, fan-out-then-fan-in, select source-order winner, select + fallback case, select with default, producer-consumer count-drain, + three-channel two-stage pipeline, channel-as-counter, after-with- + default, tick-collector. v0 ping-pong is impossible (sync spawn, + no blocking) — flagged in Phase 5b. **Shape chiselled:** the timer + channel collapses "after duration" into "channel ready immediately" + — the only thing `select` needs from a timer is that one of the + cases be in the ready set. Real time becomes a refinement of + *when* readiness flips, not of the protocol. Sister-plan diary + updated with the readiness-as-protocol observation. [shapes- + scheduler] - 2026-05-27 — Phase 5 cont.: range-over-{slice,map,channel}. New `go-eval-range-for` dispatches on the collection type: slice → bind index + element, iterate by position diff --git a/plans/lib-guest-scheduler.md b/plans/lib-guest-scheduler.md index 8e7a071d..e07ca110 100644 --- a/plans/lib-guest-scheduler.md +++ b/plans/lib-guest-scheduler.md @@ -231,6 +231,47 @@ real result. _Newest first. Append one dated entry per milestone landed._ +- 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 + holding** a tick value — duration is ignored in v0. The select + loop doesn't care that the channel got its value "via time"; it + only consults `ready?`. This separates two concerns the eventual + kit had been conflating: + + 1. **The wake-up protocol** — what `select` asks of every case: + "are you ready right now?" Channel-recv answers via "buffer + non-empty or closed"; channel-send via "buffer has room"; + timer via "deadline reached." All three flatten to a single + `ready?` predicate. + + 2. **The scheduling oracle** — *when* a case's `ready?` flips + from false to true. For channels this is driven by other + goroutines sending/receiving; for timers it's driven by a + wall-clock or monotonic source. + + v0 collapses #2 (timer = ready immediately, sends always ready, + recvs ready iff buffer non-empty) and exposes #1 as the only + thing the dispatcher needs to know. Phase 5b refines #2 with + blocking semantics and real time, but #1 stays the same shape. + + Concretely: the kit's `select-case` should take `:ready?-fn` per + case, not three different "is-this-a-send-or-recv-or-timer" tags. + Send/recv/timer become factory functions that produce a + `(:ready? FN :commit! FN)` record — the dispatcher walks cases, + picks the first whose `ready?` returns true, calls `commit!` to + extract the value (and side-effect: drain buffer, fire timer). + This is the same shape as a STM transaction over case-set, and + matches Erlang's `receive` clauses too (each pattern is a + ready-predicate + commit-action over the mailbox head). + + Ping-pong remains impossible in v0 because the synchronous spawn + collapses the `ready?`-flip oracle to "always immediate" — the + spawned goroutine can never park waiting for the parent to send. + Phase 5b must restore the wake-up dimension; until then the kit + spec should encode the readiness-protocol design even though the + oracle is degenerate. + - 2026-05-27 — From Go-on-SX Phase 5 first slice: the channel primitive landed as closures-over-mutable-state in `lib/go/sched.sx`. Concrete shape: