diff --git a/plans/agent-briefings/smalltalk-loop.md b/plans/agent-briefings/smalltalk-loop.md new file mode 100644 index 00000000..c971fdd1 --- /dev/null +++ b/plans/agent-briefings/smalltalk-loop.md @@ -0,0 +1,77 @@ +# smalltalk-on-sx loop agent (single agent, queue-driven) + +Role: iterates `plans/smalltalk-on-sx.md` forever. Message-passing OO + **blocks with non-local return** on delimited continuations. Non-local return is the headline showcase — every other Smalltalk reinvents it on the host stack; on SX it falls out of the captured method-return continuation. + +``` +description: smalltalk-on-sx queue loop +subagent_type: general-purpose +run_in_background: true +isolation: worktree +``` + +## Prompt + +You are the sole background agent working `/root/rose-ash/plans/smalltalk-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push. + +## Restart baseline — check before iterating + +1. Read `plans/smalltalk-on-sx.md` — roadmap + Progress log. +2. `ls lib/smalltalk/` — pick up from the most advanced file. +3. If `lib/smalltalk/tests/*.sx` exist, run them. Green before new work. +4. If `lib/smalltalk/scoreboard.md` exists, that's your baseline. + +## The queue + +Phase order per `plans/smalltalk-on-sx.md`: + +- **Phase 1** — tokenizer + parser (chunk format, identifiers, keywords `foo:`, binary selectors, `#sym`, `#(…)`, `$c`, blocks `[:a | …]`, cascades, message precedence) +- **Phase 2** — object model + sequential eval (class table bootstrap, message dispatch, `super`, `doesNotUnderstand:`, instance variables) +- **Phase 3** — **THE SHOWCASE**: blocks with non-local return via captured method-return continuation. `whileTrue:` / `ifTrue:ifFalse:` as block sends. 5 classic programs (eight-queens, quicksort, mandelbrot, life, fibonacci) green. +- **Phase 4** — reflection + MOP: `perform:`, `respondsTo:`, runtime method addition, `becomeForward:`, `Exception` / `on:do:` / `ensure:` on top of `handler-bind`/`raise` +- **Phase 5** — collections + numeric tower + streams +- **Phase 6** — port SUnit, vendor Pharo Kernel-Tests slice, drive corpus to 200+ +- **Phase 7** — speed (optional): inline caching, block intrinsification + +Within a phase, pick the checkbox that unlocks the most tests per effort. + +Every iteration: implement → test → commit → tick `[ ]` → Progress log → next. + +## Ground rules (hard) + +- **Scope:** only `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib//` dirs, `lib/stdlib.sx`, or `lib/` root. Smalltalk primitives go in `lib/smalltalk/runtime.sx`. +- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop. +- **Shared-file issues** → plan's Blockers with minimal repro. +- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines. +- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits. +- **Worktree:** commit locally. Never push. Never touch `main`. +- **Commit granularity:** one feature per commit. +- **Plan file:** update Progress log + tick boxes every commit. + +## Smalltalk-specific gotchas + +- **Method invocation captures `^k`** — the return continuation. Bind it as the block's escape token. `^expr` from inside any nested block invokes that captured `^k`. Escape past method return raises `BlockContext>>cannotReturn:`. +- **Blocks are lambdas + escape token**, not bare lambdas. `value`/`value:`/… invoke the lambda; `^` invokes the escape. +- **`ifTrue:` / `ifFalse:` / `whileTrue:` are ordinary block sends** — no special form. The runtime intrinsifies them in the JIT path (Tier 1 of bytecode expansion already covers this pattern). +- **Cascade** `r m1; m2; m3` desugars to `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`. Result is the cascade's last send (or first, depending on parser variant — pick one and document). +- **`super` send** looks up starting from the *defining* class's superclass, not the receiver class. Stash the defining class on the method record. +- **Selectors are interned symbols.** Use SX symbols. +- **Receiver dispatch:** tagged ints / floats / strings / symbols / `nil` / `true` / `false` aren't boxed. Their classes (`SmallInteger`, `Float`, `String`, `Symbol`, `UndefinedObject`, `True`, `False`) are looked up by SX type-of, not by an `:class` field. +- **Method precedence:** unary > binary > keyword. `3 + 4 factorial` is `3 + (4 factorial)`. `a foo: b bar` is `a foo: (b bar)` (keyword absorbs trailing unary). +- **Image / fileIn / become: between sessions** = out of scope. One-way `becomeForward:` only. +- **Test corpus:** ~200 hand-written + a slice of Pharo Kernel-Tests. Place programs in `lib/smalltalk/tests/programs/`. + +## General gotchas (all loops) + +- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences. +- `cond`/`when`/`let` clauses evaluate only the last expr. +- `type-of` on user fn returns `"lambda"`. +- Shell heredoc `||` gets eaten — escape or use `case`. + +## Style + +- No comments in `.sx` unless non-obvious. +- No new planning docs — update `plans/smalltalk-on-sx.md` inline. +- Short, factual commit messages (`smalltalk: tokenizer + 56 tests`). +- One feature per iteration. Commit. Log. Next. + +Go. Read the plan; find first `[ ]`; implement. diff --git a/plans/smalltalk-on-sx.md b/plans/smalltalk-on-sx.md new file mode 100644 index 00000000..2d4f47f1 --- /dev/null +++ b/plans/smalltalk-on-sx.md @@ -0,0 +1,116 @@ +# Smalltalk-on-SX: blocks with non-local return on delimited continuations + +The headline showcase is **blocks** — Smalltalk's closures with non-local return (`^expr` aborts the enclosing *method*, not the block). Every other Smalltalk on top of a host VM (RSqueak on PyPy, GemStone on C, Maxine on Java) reinvents non-local return on whatever stack discipline the host gives them. On SX it's a one-liner: a block holds a captured continuation; `^` just invokes it. Message-passing OO falls out cheaply on top of the existing component / dispatch machinery. + +End-state goal: ANSI-ish Smalltalk-80 subset, SUnit working, ~200 hand-written tests + a vendored slice of the Pharo kernel tests, classic corpus (eight queens, quicksort, mandelbrot, Conway's Life). + +## Scope decisions (defaults — override by editing before we spawn) + +- **Syntax:** Pharo / Squeak chunk format (`!` separators, `Object subclass: #Foo …`). No fileIn/fileOut images — text source only. +- **Conformance:** ANSI X3J20 *as a target*, not bug-for-bug Squeak. "Reads like Smalltalk, runs like Smalltalk." +- **Test corpus:** SUnit ported to SX-Smalltalk + custom programs + a curated slice of Pharo `Kernel-Tests` / `Collections-Tests`. +- **Image:** out of scope. Source-only. No `become:` between sessions, no snapshotting. +- **Reflection:** `class`, `respondsTo:`, `perform:`, `doesNotUnderstand:` in. `become:` (object-identity swap) **in** — it's a good CEK exercise. Method modification at runtime in. +- **GUI / Morphic / threads:** out entirely. + +## Ground rules + +- **Scope:** only touch `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib//**`. Smalltalk primitives go in `lib/smalltalk/runtime.sx`. +- **SX files:** use `sx-tree` MCP tools only. +- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes. + +## Architecture sketch + +``` +Smalltalk source + │ + ▼ +lib/smalltalk/tokenizer.sx — selectors, keywords, literals, $c, #sym, #(…), $'…' + │ + ▼ +lib/smalltalk/parser.sx — AST: classes, methods, blocks, cascades, sends + │ + ▼ +lib/smalltalk/transpile.sx — AST → SX AST (entry: smalltalk-eval-ast) + │ + ▼ +lib/smalltalk/runtime.sx — class table, MOP, dispatch, primitives +``` + +Core mapping: +- **Class** = SX dict `{:name :superclass :ivars :methods :class-methods :metaclass}`. Class table is a flat dict keyed by class name. +- **Object** = SX dict `{:class :ivars}` — `ivars` keyed by symbol. Tagged ints / floats / strings / symbols are not boxed; their class is looked up by SX type. +- **Method** = SX lambda closing over a `self` binding + temps. Body wrapped in a delimited continuation so `^` can escape. +- **Message send** = `(st-send receiver selector args)` — does class-table lookup, walks superclass chain, falls back to `doesNotUnderstand:` with a `Message` object. +- **Block** `[:x | … ^v … ]` = lambda + captured `^k` (the method-return continuation). Invoking `^` calls `k`; outer block invocation past method return raises `BlockContext>>cannotReturn:`. +- **Cascade** `r m1; m2; m3` = `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`. +- **`ifTrue:ifFalse:` / `whileTrue:`** = ordinary block sends; the runtime intrinsifies them in the JIT path so they compile to native branches (Tier 1 of bytecode expansion already covers this pattern). +- **`become:`** = swap two object identities everywhere — in SX this is a heap walk, but we restrict to `oneWayBecome:` (cheap: rewrite class field) by default. + +## Roadmap + +### Phase 1 — tokenizer + parser +- [ ] Tokenizer: identifiers, keywords (`foo:`), binary selectors (`+`, `==`, `,`, `->`, `~=` etc.), numbers (radix `16r1F`, scaled `1.5s2`), strings `'…''…'`, characters `$c`, symbols `#foo` `#'foo bar'` `#+`, byte arrays `#[1 2 3]`, literal arrays `#(1 #foo 'x')`, comments `"…"` +- [ ] Parser: chunk format (`! !` separators), class definitions (`Object subclass: #X instanceVariableNames: '…' classVariableNames: '…' …`), method definitions (`extend: #Foo with: 'bar ^self'`), pragmas ``, blocks `[:a :b | | t1 t2 | …]`, cascades, message precedence (unary > binary > keyword) +- [ ] Unit tests in `lib/smalltalk/tests/parse.sx` + +### Phase 2 — object model + sequential eval +- [ ] Class table + bootstrap: `Object`, `Behavior`, `Class`, `Metaclass`, `UndefinedObject`, `Boolean`/`True`/`False`, `Number`/`Integer`/`Float`, `String`, `Symbol`, `Array`, `Block` +- [ ] `smalltalk-eval-ast`: literals, variable reference, assignment, message send, cascade, sequence, return +- [ ] Method lookup: walk class → superclass; cache hit-class on `(class, selector)` +- [ ] `doesNotUnderstand:` fallback constructing `Message` object +- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class) +- [ ] 30+ tests in `lib/smalltalk/tests/eval.sx` + +### Phase 3 — blocks + non-local return (THE SHOWCASE) +- [ ] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape +- [ ] `^expr` from inside a block invokes that captured `^k` +- [ ] `BlockContext>>value`, `value:`, `value:value:`, …, `valueWithArguments:` +- [ ] `whileTrue:` / `whileTrue` / `whileFalse:` / `whileFalse` as ordinary block sends — runtime intrinsifies the loop in the bytecode JIT +- [ ] `ifTrue:` / `ifFalse:` / `ifTrue:ifFalse:` as block sends, similarly intrinsified +- [ ] Escape past returned-from method raises `BlockContext>>cannotReturn:` +- [ ] Classic programs in `lib/smalltalk/tests/programs/`: + - [ ] `eight-queens.st` + - [ ] `quicksort.st` + - [ ] `mandelbrot.st` + - [ ] `life.st` (Conway's Life, glider gun) + - [ ] `fibonacci.st` (recursive + memoised) +- [ ] `lib/smalltalk/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` + +### Phase 4 — reflection + MOP +- [ ] `Object>>class`, `class>>name`, `class>>superclass`, `class>>methodDict`, `class>>selectors` +- [ ] `Object>>perform:` / `perform:with:` / `perform:withArguments:` +- [ ] `Object>>respondsTo:`, `Object>>isKindOf:`, `Object>>isMemberOf:` +- [ ] `Behavior>>compile:` — runtime method addition +- [ ] `Object>>becomeForward:` (one-way become; rewrites the class field of `aReceiver`) +- [ ] Exceptions: `Exception`, `Error`, `signal`, `signal:`, `on:do:`, `ensure:`, `ifCurtailed:` — built on top of SX `handler-bind`/`raise` + +### Phase 5 — collections + numeric tower +- [ ] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol` +- [ ] `HashedCollection`/`Set`/`Dictionary`/`IdentityDictionary` +- [ ] `Stream` hierarchy: `ReadStream`/`WriteStream`/`ReadWriteStream` +- [ ] `Number` tower: `SmallInteger`/`LargePositiveInteger`/`Float`/`Fraction` +- [ ] `String>>format:`, `printOn:` for everything + +### Phase 6 — SUnit + corpus to 200+ +- [ ] Port SUnit (TestCase, TestSuite, TestResult) — written in SX-Smalltalk, runs in itself +- [ ] Vendor a slice of Pharo `Kernel-Tests` and `Collections-Tests` +- [ ] Drive the scoreboard up: aim for 200+ green tests +- [ ] Stretch: ANSI Smalltalk validator subset + +### Phase 7 — speed (optional) +- [ ] Method-dictionary inline caching (already in CEK as a primitive; just wire selector cache) +- [ ] Block intrinsification beyond `whileTrue:` / `ifTrue:` +- [ ] Compare against GNU Smalltalk on the corpus + +## Progress log + +_Newest first. Agent appends on every commit._ + +- _(none yet)_ + +## Blockers + +_Shared-file issues that need someone else to fix. Minimal repro only._ + +- _(none yet)_ diff --git a/scripts/sx-loops-down.sh b/scripts/sx-loops-down.sh index f9c9fdc0..fca473ec 100755 --- a/scripts/sx-loops-down.sh +++ b/scripts/sx-loops-down.sh @@ -30,7 +30,7 @@ fi if [ "$CLEAN" = "1" ]; then cd "$(dirname "$0")/.." - for lang in lua prolog forth erlang haskell js hs; do + for lang in lua prolog forth erlang haskell js hs smalltalk; do wt="$WORKTREE_BASE/$lang" if [ -d "$wt" ]; then git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt" @@ -39,5 +39,5 @@ if [ "$CLEAN" = "1" ]; then done git worktree prune echo "Worktree branches (loops/) are preserved. Delete manually if desired:" - echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs" + echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs loops/smalltalk" fi diff --git a/scripts/sx-loops-up.sh b/scripts/sx-loops-up.sh index 6a517aca..3a93a3b1 100755 --- a/scripts/sx-loops-up.sh +++ b/scripts/sx-loops-up.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Spawn 7 claude sessions in tmux, one per language loop. +# Spawn 8 claude sessions in tmux, one per language loop. # Each runs in its own git worktree rooted at /root/rose-ash-loops/, # on branch loops/. No two loops share a working tree, so there's # zero risk of file collisions between languages. @@ -9,7 +9,7 @@ # # After the script prints done: # tmux a -t sx-loops -# Ctrl-B + to switch (0=lua ... 6=hs) +# Ctrl-B + to switch (0=lua ... 7=smalltalk) # Ctrl-B + d to detach (loops keep running, SSH-safe) # # Stop: ./scripts/sx-loops-down.sh @@ -38,8 +38,9 @@ declare -A BRIEFING=( [haskell]=haskell-loop.md [js]=loop.md [hs]=hs-loop.md + [smalltalk]=smalltalk-loop.md ) -ORDER=(lua prolog forth erlang haskell js hs) +ORDER=(lua prolog forth erlang haskell js hs smalltalk) mkdir -p "$WORKTREE_BASE" @@ -66,7 +67,7 @@ for lang in "${ORDER[@]:1}"; do tmux new-window -t "$SESSION" -n "$lang" -c "$WORKTREE_BASE/$lang" done -echo "Starting 7 claude sessions..." +echo "Starting 8 claude sessions..." for lang in "${ORDER[@]}"; do tmux send-keys -t "$SESSION:$lang" "claude" C-m done @@ -89,10 +90,10 @@ for lang in "${ORDER[@]}"; do done echo "" -echo "Done. 7 loops started in tmux session '$SESSION', each in its own worktree." +echo "Done. 8 loops started in tmux session '$SESSION', each in its own worktree." echo "" echo " Attach: tmux a -t $SESSION" -echo " Switch: Ctrl-B <0..6> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs)" +echo " Switch: Ctrl-B <0..7> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs 7=smalltalk)" echo " List: Ctrl-B w" echo " Detach: Ctrl-B d" echo " Stop: ./scripts/sx-loops-down.sh"