From f09a712666f28c1f8f1c3c83543672bcdb2a5795 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 04:02:00 +0000 Subject: [PATCH] smalltalk: BlockContext value family + 19 tests --- lib/smalltalk/tests/blocks.sx | 92 +++++++++++++++++++++++++++++++++++ plans/smalltalk-on-sx.md | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 lib/smalltalk/tests/blocks.sx diff --git a/lib/smalltalk/tests/blocks.sx b/lib/smalltalk/tests/blocks.sx new file mode 100644 index 00000000..7f7a323b --- /dev/null +++ b/lib/smalltalk/tests/blocks.sx @@ -0,0 +1,92 @@ +;; BlockContext>>value family tests. +;; +;; The runtime already implements value, value:, value:value:, value:value:value:, +;; value:value:value:value:, and valueWithArguments: in st-block-dispatch. +;; This file pins each variant down with explicit tests + closure semantics. + +(set! st-test-pass 0) +(set! st-test-fail 0) +(set! st-test-fails (list)) + +(st-bootstrap-classes!) +(define ev (fn (src) (smalltalk-eval src))) +(define evp (fn (src) (smalltalk-eval-program src))) + +;; ── 1. The value/valueN family ── +(st-test "value: zero-arg block" (ev "[42] value") 42) +(st-test "value: one-arg block" (ev "[:a | a + 1] value: 10") 11) +(st-test "value:value: two-arg" (ev "[:a :b | a * b] value: 3 value: 4") 12) +(st-test "value:value:value: three" (ev "[:a :b :c | a + b + c] value: 1 value: 2 value: 3") 6) +(st-test "value:value:value:value: four" + (ev "[:a :b :c :d | a + b + c + d] value: 1 value: 2 value: 3 value: 4") 10) + +;; ── 2. valueWithArguments: ── +(st-test "valueWithArguments: zero-arg" + (ev "[99] valueWithArguments: #()") 99) +(st-test "valueWithArguments: one-arg" + (ev "[:x | x * x] valueWithArguments: #(7)") 49) +(st-test "valueWithArguments: many" + (ev "[:a :b :c | a , b , c] valueWithArguments: #('foo' '-' 'bar')") "foo-bar") + +;; ── 3. Block returns last expression ── +(st-test "block last-expression result" (ev "[1. 2. 3] value") 3) +(st-test "block with temps initial state" + (ev "[| t u | t := 5. u := t * 2. u] value") 10) + +;; ── 4. Closure over outer locals ── +(st-test + "block reads outer let temps" + (evp "| n | n := 5. ^ [n * n] value") + 25) +(st-test + "block writes outer locals (mutating)" + (evp "| n | n := 10. [:x | n := n + x] value: 5. ^ n") + 15) + +;; ── 5. Block sees later mutation of captured local ── +(st-test + "block re-reads outer local on each invocation" + (evp + "| n b r1 r2 | + n := 1. b := [n]. + r1 := b value. + n := 99. + r2 := b value. + ^ r1 + r2") + 100) + +;; ── 6. Re-entrant invocations ── +(st-test + "calling same block twice independent results" + (evp + "| sq | + sq := [:x | x * x]. + ^ (sq value: 3) + (sq value: 4)") + 25) + +;; ── 7. Nested blocks ── +(st-test + "nested block closes over both scopes" + (evp + "| a | + a := [:x | [:y | x + y]]. + ^ ((a value: 10) value: 5)") + 15) + +;; ── 8. Block as method argument ── +(st-class-define! "BlockUser" "Object" (list)) +(st-class-add-method! "BlockUser" "apply:to:" + (st-parse-method "apply: aBlock to: x ^ aBlock value: x")) + +(st-test + "method invokes block argument" + (evp "^ BlockUser new apply: [:n | n * n] to: 9") + 81) + +;; ── 9. numArgs + class ── +(st-test "numArgs zero" (ev "[] numArgs") 0) +(st-test "numArgs three" (ev "[:a :b :c | a] numArgs") 3) +(st-test "block class is BlockClosure" + (str (ev "[1] class name")) "BlockClosure") + +(list st-test-pass st-test-fail) diff --git a/plans/smalltalk-on-sx.md b/plans/smalltalk-on-sx.md index d9817d24..5a0021a6 100644 --- a/plans/smalltalk-on-sx.md +++ b/plans/smalltalk-on-sx.md @@ -66,7 +66,7 @@ Core mapping: ### Phase 3 — blocks + non-local return (THE SHOWCASE) - [x] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape. `st-invoke` wraps body in `(call/cc (fn (k) ...))`; the frame's `:return-k` is set to k. Block creation copies `(get frame :return-k)` onto the block. Block invocation sets the new frame's `:return-k` to the block's saved one — so non-local return reaches *back through* any number of intermediate block invocations. - [x] `^expr` from inside a block invokes that captured `^k`. The "return" AST type evaluates the expression then calls `(k v)` on the frame's :return-k. Verified: `detect:in:` style early-exit, multi-level nested blocks, ^ from inside `to:do:`/`whileTrue:`, ^ from a block passed to a *different* method (Caller→Helper) returns from Caller. -- [ ] `BlockContext>>value`, `value:`, `value:value:`, …, `valueWithArguments:` +- [x] `BlockContext>>value`, `value:`, `value:value:`, `value:value:value:`, `value:value:value:value:`, `valueWithArguments:`. Implemented in `st-block-dispatch` + `st-block-apply` (eval iteration); pinned by 19 dedicated tests in `lib/smalltalk/tests/blocks.sx` covering arity through 4, valueWithArguments: with empty/non-empty arg arrays, closures over outer locals (read + mutate + later-mutation re-read), nested blocks, blocks as method arguments, `numArgs`, and `class`. - [ ] `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:` @@ -108,6 +108,7 @@ Core mapping: _Newest first. Agent appends on every commit._ +- 2026-04-25: BlockContext value family pinned (`lib/smalltalk/tests/blocks.sx`, 19 tests). Each value/valueN/valueWithArguments: variant verified plus closure semantics (read, write, later-mutation re-read), nested blocks, and block-as-arg. 320/320 total. - 2026-04-25: **THE SHOWCASE** — non-local return via captured method-return continuations + 14 NLR tests (`lib/smalltalk/tests/nlr.sx`). `st-invoke` wraps body in `call/cc`; blocks copy creating method's `^k`; `^expr` invokes that k. Verified across nested blocks, `to:do:` / `whileTrue:`, blocks passed to different methods (Caller→Helper escapes back to Caller), inner-vs-outer method nesting. Sentinel-based return removed. 301/301 total. - 2026-04-25: `super` send + 9 tests (`lib/smalltalk/tests/super.sx`). `st-super-send` walks from defining-class's superclass; class-side aware; primitives → DNU fallback. Also fixed top-level `| temps |` parsing in `st-parse` (the absence of which was silently aborting earlier eval/dnu tests — counts go from 274 → 287, with previously-skipped tests now actually running). - 2026-04-25: `doesNotUnderstand:` + 12 DNU tests (`lib/smalltalk/tests/dnu.sx`). Bootstrap installs `Message` (with selector/arguments accessors). Primitives signal `:unhandled` instead of erroring; `st-dnu` builds a Message and walks `doesNotUnderstand:` lookup. User Object DNU intercepts unknown sends to native receivers (Number, String, Block) too. 267/267 total.