From 3be722d5b64751720405a3a28ea46d9b3cf18f72 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 10:58:08 +0000 Subject: [PATCH] smalltalk: SequenceableCollection methods (13) + String at:/copyFrom:to: + 28 tests --- lib/smalltalk/eval.sx | 19 +++++ lib/smalltalk/runtime.sx | 76 +++++++++++++++++++ lib/smalltalk/tests/collections.sx | 115 +++++++++++++++++++++++++++++ plans/smalltalk-on-sx.md | 3 +- 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 lib/smalltalk/tests/collections.sx diff --git a/lib/smalltalk/eval.sx b/lib/smalltalk/eval.sx index 52b6c539..f576a295 100644 --- a/lib/smalltalk/eval.sx +++ b/lib/smalltalk/eval.sx @@ -757,6 +757,25 @@ ((= selector "printString") (str "'" s "'")) ((= selector "asString") s) ((= selector "asSymbol") (make-symbol (if (symbol? s) (str s) s))) + ;; 1-indexed character access; returns the character (a 1-char string). + ((= selector "at:") (nth s (- (nth args 0) 1))) + ((= selector "do:") + (let ((i 0) (n (len s)) (block (nth args 0))) + (begin + (define + sd-loop + (fn () + (when (< i n) + (begin + (st-block-apply block (list (nth s i))) + (set! i (+ i 1)) + (sd-loop))))) + (sd-loop) + s))) + ((= selector "first") (nth s 0)) + ((= selector "last") (nth s (- (len s) 1))) + ((= selector "copyFrom:to:") + (slice s (- (nth args 0) 1) (nth args 1))) ((= selector "class") (st-class-ref (st-class-of s))) ((= selector "isNil") false) ((= selector "notNil") true) diff --git a/lib/smalltalk/runtime.sx b/lib/smalltalk/runtime.sx index 8d27a9a5..8f499c2c 100644 --- a/lib/smalltalk/runtime.sx +++ b/lib/smalltalk/runtime.sx @@ -402,6 +402,82 @@ (st-class-define! "Error" "Exception" (list)) (st-class-define! "ZeroDivide" "Error" (list)) (st-class-define! "MessageNotUnderstood" "Error" (list)) + ;; SequenceableCollection — shared iteration / inspection methods. + ;; Defined on the parent class so Array, String, Symbol, and + ;; OrderedCollection all inherit. Each method calls `self do:`, + ;; which dispatches to the receiver's primitive do: implementation. + (st-class-add-method! "SequenceableCollection" "inject:into:" + (st-parse-method + "inject: initial into: aBlock + | acc | + acc := initial. + self do: [:e | acc := aBlock value: acc value: e]. + ^ acc")) + (st-class-add-method! "SequenceableCollection" "detect:" + (st-parse-method + "detect: aBlock + self do: [:e | (aBlock value: e) ifTrue: [^ e]]. + ^ nil")) + (st-class-add-method! "SequenceableCollection" "detect:ifNone:" + (st-parse-method + "detect: aBlock ifNone: noneBlock + self do: [:e | (aBlock value: e) ifTrue: [^ e]]. + ^ noneBlock value")) + (st-class-add-method! "SequenceableCollection" "count:" + (st-parse-method + "count: aBlock + | n | + n := 0. + self do: [:e | (aBlock value: e) ifTrue: [n := n + 1]]. + ^ n")) + (st-class-add-method! "SequenceableCollection" "allSatisfy:" + (st-parse-method + "allSatisfy: aBlock + self do: [:e | (aBlock value: e) ifFalse: [^ false]]. + ^ true")) + (st-class-add-method! "SequenceableCollection" "anySatisfy:" + (st-parse-method + "anySatisfy: aBlock + self do: [:e | (aBlock value: e) ifTrue: [^ true]]. + ^ false")) + (st-class-add-method! "SequenceableCollection" "includes:" + (st-parse-method + "includes: target + self do: [:e | e = target ifTrue: [^ true]]. + ^ false")) + (st-class-add-method! "SequenceableCollection" "do:separatedBy:" + (st-parse-method + "do: aBlock separatedBy: sepBlock + | first | + first := true. + self do: [:e | + first ifFalse: [sepBlock value]. + first := false. + aBlock value: e]. + ^ self")) + (st-class-add-method! "SequenceableCollection" "indexOf:" + (st-parse-method + "indexOf: target + | idx | + idx := 1. + self do: [:e | e = target ifTrue: [^ idx]. idx := idx + 1]. + ^ 0")) + (st-class-add-method! "SequenceableCollection" "indexOf:ifAbsent:" + (st-parse-method + "indexOf: target ifAbsent: noneBlock + | idx | + idx := 1. + self do: [:e | e = target ifTrue: [^ idx]. idx := idx + 1]. + ^ noneBlock value")) + (st-class-add-method! "SequenceableCollection" "reject:" + (st-parse-method + "reject: aBlock ^ self select: [:e | (aBlock value: e) not]")) + (st-class-add-method! "SequenceableCollection" "isEmpty" + (st-parse-method "isEmpty ^ self size = 0")) + (st-class-add-method! "SequenceableCollection" "notEmpty" + (st-parse-method "notEmpty ^ self size > 0")) + (st-class-add-method! "SequenceableCollection" "asString" + (st-parse-method "asString ^ self printString")) "ok"))) ;; Initialise on load. Tests can re-bootstrap to reset state. diff --git a/lib/smalltalk/tests/collections.sx b/lib/smalltalk/tests/collections.sx new file mode 100644 index 00000000..c4d5259b --- /dev/null +++ b/lib/smalltalk/tests/collections.sx @@ -0,0 +1,115 @@ +;; Phase 5 collection tests — methods on SequenceableCollection / Array / +;; String / Symbol. Emphasis on the inherited-from-SequenceableCollection +;; methods that work uniformly across Array, String, Symbol. + +(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. inject:into: (fold) ── +(st-test "Array inject:into: sum" + (ev "#(1 2 3 4) inject: 0 into: [:a :b | a + b]") 10) + +(st-test "Array inject:into: product" + (ev "#(2 3 4) inject: 1 into: [:a :b | a * b]") 24) + +(st-test "Array inject:into: empty array → initial" + (ev "#() inject: 99 into: [:a :b | a + b]") 99) + +;; ── 2. detect: / detect:ifNone: ── +(st-test "detect: finds first match" + (ev "#(1 3 5 7) detect: [:x | x > 4]") 5) + +(st-test "detect: returns nil if no match" + (ev "#(1 2 3) detect: [:x | x > 10]") nil) + +(st-test "detect:ifNone: invokes block on miss" + (ev "#(1 2 3) detect: [:x | x > 10] ifNone: [#none]") + (make-symbol "none")) + +;; ── 3. count: ── +(st-test "count: matches" + (ev "#(1 2 3 4 5 6) count: [:x | x > 3]") 3) + +(st-test "count: zero matches" + (ev "#(1 2 3) count: [:x | x > 100]") 0) + +;; ── 4. allSatisfy: / anySatisfy: ── +(st-test "allSatisfy: when all match" + (ev "#(2 4 6) allSatisfy: [:x | x > 0]") true) + +(st-test "allSatisfy: when one fails" + (ev "#(2 4 -1) allSatisfy: [:x | x > 0]") false) + +(st-test "anySatisfy: when at least one matches" + (ev "#(1 2 3) anySatisfy: [:x | x > 2]") true) + +(st-test "anySatisfy: when none match" + (ev "#(1 2 3) anySatisfy: [:x | x > 100]") false) + +;; ── 5. includes: ── +(st-test "includes: found" (ev "#(1 2 3) includes: 2") true) +(st-test "includes: missing" (ev "#(1 2 3) includes: 99") false) + +;; ── 6. indexOf: / indexOf:ifAbsent: ── +(st-test "indexOf: returns 1-based index" + (ev "#(10 20 30 40) indexOf: 30") 3) + +(st-test "indexOf: missing returns 0" + (ev "#(1 2 3) indexOf: 99") 0) + +(st-test "indexOf:ifAbsent: invokes block" + (ev "#(1 2 3) indexOf: 99 ifAbsent: [-1]") -1) + +;; ── 7. reject: (complement of select:) ── +(st-test "reject: removes matching" + (ev "#(1 2 3 4 5) reject: [:x | x > 3]") + (list 1 2 3)) + +;; ── 8. do:separatedBy: ── +(st-test "do:separatedBy: builds joined sequence" + (evp + "| seen | + seen := #(). + #(1 2 3) do: [:e | seen := seen , (Array with: e)] + separatedBy: [seen := seen , #(0)]. + ^ seen") + (list 1 0 2 0 3)) + +;; Array with: shim for the test (inherited from earlier exception tests +;; in a separate suite — define here for safety). +(st-class-add-class-method! "Array" "with:" + (st-parse-method "with: x | a | a := Array new: 1. a at: 1 put: x. ^ a")) + +;; ── 9. String inherits the same methods ── +(st-test "String includes:" + (ev "'abcde' includes: $c") true) + +(st-test "String count:" + (ev "'banana' count: [:c | c = $a]") 3) + +(st-test "String inject:into: concatenates" + (ev "'abc' inject: '' into: [:acc :c | acc , c , c]") + "aabbcc") + +(st-test "String allSatisfy:" + (ev "'abc' allSatisfy: [:c | c = $a or: [c = $b or: [c = $c]]]") true) + +;; ── 10. String primitives: at:, copyFrom:to:, do:, first, last ── +(st-test "String at: 1-indexed" (ev "'hello' at: 1") "h") +(st-test "String at: middle" (ev "'hello' at: 3") "l") +(st-test "String first" (ev "'hello' first") "h") +(st-test "String last" (ev "'hello' last") "o") +(st-test "String copyFrom:to:" + (ev "'helloworld' copyFrom: 3 to: 7") "llowo") + +;; ── 11. isEmpty / notEmpty go through SequenceableCollection too ── +;; (Already in primitives; the inherited versions agree.) +(st-test "Array isEmpty" (ev "#() isEmpty") true) +(st-test "Array notEmpty" (ev "#(1) notEmpty") true) + +(list st-test-pass st-test-fail) diff --git a/plans/smalltalk-on-sx.md b/plans/smalltalk-on-sx.md index 962bb7b0..29328842 100644 --- a/plans/smalltalk-on-sx.md +++ b/plans/smalltalk-on-sx.md @@ -87,7 +87,7 @@ Core mapping: - [x] Exceptions: `Exception`, `Error`, `ZeroDivide`, `MessageNotUnderstood` in bootstrap. `signal` raises the receiver via SX `raise`; `signal:` sets `messageText` first. `on:do:` / `ensure:` / `ifCurtailed:` on BlockClosure use SX `guard`. The auto-reraise pattern uses a side-effect predicate (cleanup runs in the predicate, returns false → guard auto-reraises) because `(raise c)` from inside a guard handler hits a known SX issue with nested-handler frames. 15 tests in `lib/smalltalk/tests/exceptions.sx`. Phase 4 complete. ### Phase 5 — collections + numeric tower -- [ ] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol` +- [x] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol`. Bootstrap installs shared methods on `SequenceableCollection`: `inject:into:`, `detect:`/`detect:ifNone:`, `count:`, `allSatisfy:`/`anySatisfy:`, `includes:`, `do:separatedBy:`, `indexOf:`/`indexOf:ifAbsent:`, `reject:`, `isEmpty`/`notEmpty`, `asString`. They each call `self do:`, which dispatches to the receiver's primitive `do:` — so Array, String, and Symbol inherit them uniformly. String/Symbol primitives gained `at:` (1-indexed), `copyFrom:to:`, `first`/`last`, `do:`. OrderedCollection class is in the bootstrap hierarchy; its instance shape will fill out alongside Set/Dictionary in the next box. 28 tests in `lib/smalltalk/tests/collections.sx`. - [ ] `HashedCollection`/`Set`/`Dictionary`/`IdentityDictionary` - [ ] `Stream` hierarchy: `ReadStream`/`WriteStream`/`ReadWriteStream` - [ ] `Number` tower: `SmallInteger`/`LargePositiveInteger`/`Float`/`Fraction` @@ -108,6 +108,7 @@ Core mapping: _Newest first. Agent appends on every commit._ +- 2026-04-25: Phase 5 sequenceable-collection methods + 28 tests (`lib/smalltalk/tests/collections.sx`). 13 shared methods on `SequenceableCollection` (inject:into:, detect:, count:, …), inherited by Array/String/Symbol via `self do:`. String primitives at:/copyFrom:to:/first/last/do:. 523/523 total. - 2026-04-25: Exception system + 15 tests (`lib/smalltalk/tests/exceptions.sx`). Exception/Error/ZeroDivide/MessageNotUnderstood in bootstrap; signal/signal: raise via SX `raise`; on:do:/ensure:/ifCurtailed: on BlockClosure via SX `guard`. Phase 4 complete. 495/495 total. - 2026-04-25: `Object>>becomeForward:` + 6 tests. In-place mutation of `:class` and `:ivars` via `dict-set!`; aliases see the new identity. 480/480 total. - 2026-04-25: `Behavior>>compile:` + sisters + 9 tests. Parses source via `st-parse-method`, installs via runtime helpers; also added `addSelector:withMethod:` and `removeSelector:`. 474/474 total.