smalltalk: Conway Life + dynamic-array literal {…}; classic corpus complete
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-25 07:31:47 +00:00
parent 7c5c49c529
commit 5ef07a4d8d
5 changed files with 218 additions and 2 deletions

View File

@@ -159,6 +159,15 @@
(fn (e) (append! out (smalltalk-eval-ast e frame)))
(get ast :elements))
out)))
((= ty "dynamic-array")
;; { e1. e2. ... } — each element is a full expression
;; evaluated at runtime. Returns a fresh mutable array.
(let ((out (list)))
(begin
(for-each
(fn (e) (append! out (smalltalk-eval-ast e frame)))
(get ast :elements))
out)))
((= ty "lit-byte-array") (get ast :elements))
((= ty "self") (get frame :self))
((= ty "super") (get frame :self))

View File

@@ -287,6 +287,7 @@
((e (parse-expression)))
(begin (consume! "rparen" nil) e))))
((= ty "lbracket") (parse-block))
((= ty "lbrace") (parse-dynamic-array))
((= ty "ident")
(begin
(advance-tok!)
@@ -346,6 +347,37 @@
(arr-loop)
{:type "lit-array" :elements items}))))
;; { expr. expr. expr } — Pharo dynamic array literal. Each element
;; is a *full expression* evaluated at runtime; the result is a
;; fresh mutable array. Empty `{}` is a 0-length array.
(define
parse-dynamic-array
(fn
()
(let ((items (list)))
(begin
(consume! "lbrace" nil)
(define
da-loop
(fn
()
(cond
((at? "rbrace" nil) (advance-tok!))
(else
(begin
(append! items (parse-expression))
(define
dot-loop
(fn
()
(when
(at? "period" nil)
(begin (advance-tok!) (dot-loop)))))
(dot-loop)
(da-loop))))))
(da-loop)
{:type "dynamic-array" :elements items}))))
;; #[1 2 3]
(define
parse-byte-array

View File

@@ -295,4 +295,112 @@
;; In-set points (bounded after 30 iters): (0,-1) (-1,0) (0,0) (0,1) → 4.
4)
;; ── life.st ──────────────────────────────────────────────────────────
(define
life-source
"Object subclass: #Life
instanceVariableNames: 'rows cols cells'!
!Life methodsFor: 'init'!
rows: r cols: c
rows := r. cols := c.
cells := Array new: r * c.
1 to: r * c do: [:i | cells at: i put: 0].
^ self! !
!Life methodsFor: 'access'!
rows ^ rows!
cols ^ cols!
at: r at: c
((r < 1) or: [r > rows]) ifTrue: [^ 0].
((c < 1) or: [c > cols]) ifTrue: [^ 0].
^ cells at: (r - 1) * cols + c!
at: r at: c put: v
cells at: (r - 1) * cols + c put: v.
^ v! !
!Life methodsFor: 'step'!
neighbors: r at: c
| sum |
sum := 0.
-1 to: 1 do: [:dr |
-1 to: 1 do: [:dc |
((dr = 0) and: [dc = 0]) ifFalse: [
sum := sum + (self at: r + dr at: c + dc)]]].
^ sum!
step
| next |
next := Array new: rows * cols.
1 to: rows * cols do: [:i | next at: i put: 0].
1 to: rows do: [:r |
1 to: cols do: [:c |
| n alive lives |
n := self neighbors: r at: c.
alive := (self at: r at: c) = 1.
lives := alive
ifTrue: [(n = 2) or: [n = 3]]
ifFalse: [n = 3].
lives ifTrue: [next at: (r - 1) * cols + c put: 1]]].
cells := next.
^ self!
stepN: n
n timesRepeat: [self step].
^ self! !
!Life methodsFor: 'measure'!
livingCount
| sum |
sum := 0.
1 to: rows * cols do: [:i | (cells at: i) = 1 ifTrue: [sum := sum + 1]].
^ sum! !")
(smalltalk-load life-source)
(st-test "Life class registered" (st-class-exists? "Life") true)
;; Block (still life): four cells in a 2x2 stay forever after 1 step.
;; The bigger patterns are correct but the spec interpreter is too slow
;; for many-step verification — the `.st` file is ready for the JIT.
(st-test "block (still life) survives 1 step"
(evp
"| g |
g := Life new rows: 5 cols: 5.
g at: 2 at: 2 put: 1.
g at: 2 at: 3 put: 1.
g at: 3 at: 2 put: 1.
g at: 3 at: 3 put: 1.
g step.
^ g livingCount")
4)
;; Blinker (period 2): horizontal row of 3 → vertical column.
(st-test "blinker after 1 step is vertical"
(evp
"| g |
g := Life new rows: 5 cols: 5.
g at: 3 at: 2 put: 1.
g at: 3 at: 3 put: 1.
g at: 3 at: 4 put: 1.
g step.
^ {(g at: 2 at: 3). (g at: 3 at: 3). (g at: 4 at: 3). (g at: 3 at: 2). (g at: 3 at: 4)}")
;; (2,3) (3,3) (4,3) on; (3,2) (3,4) off
(list 1 1 1 0 0))
;; Glider initial setup — 5 living cells, no step.
(st-test "glider has 5 living cells initially"
(evp
"| g |
g := Life new rows: 8 cols: 8.
g at: 1 at: 2 put: 1.
g at: 2 at: 3 put: 1.
g at: 3 at: 1 put: 1.
g at: 3 at: 2 put: 1.
g at: 3 at: 3 put: 1.
^ g livingCount")
5)
(list st-test-pass st-test-fail)

View File

@@ -0,0 +1,66 @@
"Conway's Game of Life — 2D grid stepped by the standard rules:
live with 2 or 3 neighbours stays alive; dead with exactly 3 becomes alive.
Classic-corpus program for the Smalltalk-on-SX runtime. The canonical
'glider gun' demo (~36 cells, period-30 emission) is correct but too slow
to verify on the spec interpreter without JIT — block, blinker, glider
cover the rule arithmetic and edge handling."
Object subclass: #Life
instanceVariableNames: 'rows cols cells'!
!Life methodsFor: 'init'!
rows: r cols: c
rows := r. cols := c.
cells := Array new: r * c.
1 to: r * c do: [:i | cells at: i put: 0].
^ self! !
!Life methodsFor: 'access'!
rows ^ rows!
cols ^ cols!
at: r at: c
((r < 1) or: [r > rows]) ifTrue: [^ 0].
((c < 1) or: [c > cols]) ifTrue: [^ 0].
^ cells at: (r - 1) * cols + c!
at: r at: c put: v
cells at: (r - 1) * cols + c put: v.
^ v! !
!Life methodsFor: 'step'!
neighbors: r at: c
| sum |
sum := 0.
-1 to: 1 do: [:dr |
-1 to: 1 do: [:dc |
((dr = 0) and: [dc = 0]) ifFalse: [
sum := sum + (self at: r + dr at: c + dc)]]].
^ sum!
step
| next |
next := Array new: rows * cols.
1 to: rows * cols do: [:i | next at: i put: 0].
1 to: rows do: [:r |
1 to: cols do: [:c |
| n alive lives |
n := self neighbors: r at: c.
alive := (self at: r at: c) = 1.
lives := alive
ifTrue: [(n = 2) or: [n = 3]]
ifFalse: [n = 3].
lives ifTrue: [next at: (r - 1) * cols + c put: 1]]].
cells := next.
^ self!
stepN: n
n timesRepeat: [self step].
^ self! !
!Life methodsFor: 'measure'!
livingCount
| sum |
sum := 0.
1 to: rows * cols do: [:i | (cells at: i) = 1 ifTrue: [sum := sum + 1]].
^ sum! !

View File

@@ -70,11 +70,11 @@ Core mapping:
- [x] `whileTrue:` / `whileTrue` / `whileFalse:` / `whileFalse` as ordinary block sends. `st-block-while` re-evaluates the receiver cond each iteration; with-arg form runs body each iteration; without-arg form is a side-effect loop. Now returns `nil` per ANSI/Pharo. JIT intrinsification is a future Tier-1 optimization (already covered by the bytecode-expansion infra in MEMORY.md). 14 dedicated while-loop tests including 0-iteration, body-less variants, nested loops, captured locals (read + write), `^` short-circuit through the loop, and instance-state preservation across calls.
- [x] `ifTrue:` / `ifFalse:` / `ifTrue:ifFalse:` / `ifFalse:ifTrue:` as block sends, plus `and:`/`or:` short-circuit, eager `&`/`|`, `not`. Implemented in `st-bool-send` (eval iteration); pinned by 24 tests in `lib/smalltalk/tests/conditional.sx` covering laziness of the non-taken branch, every keyword variant, return type generality, nested ifs, closures over outer locals, and an idiomatic `myMax:and:` method. Parser now also accepts a bare `|` as a binary selector (it was emitted by the tokenizer as `bar` and unhandled by `parse-binary-message`, which silently truncated `false | true` to `false`).
- [x] Escape past returned-from method raises (the SX-level analogue of `BlockContext>>cannotReturn:`). Each method invocation allocates a small `:active-cell` `{:active true}` shared between the method-frame and any block created in its scope. `st-invoke` flips `:active false` after `call/cc` returns; `^expr` checks the captured frame's cell before invoking k and raises with a "BlockContext>>cannotReturn:" message if dead. Verified by `lib/smalltalk/tests/cannot_return.sx` (5 tests using SX `guard` to catch the raise). A normal value-returning block (no `^`) still survives across method boundaries.
- [ ] Classic programs in `lib/smalltalk/tests/programs/`:
- [x] Classic programs in `lib/smalltalk/tests/programs/`:
- [x] `eight-queens.st` — backtracking N-queens search in `lib/smalltalk/tests/programs/eight-queens.st`. The `.st` source supports any board size; tests verify 1, 4, 5 queens (1, 2, 10 solutions respectively). 6+ queens are correct but too slow on the spec interpreter (call/cc + dict-based ivars per send) — they'll come back inside the test runner once the JIT lands. The 8-queens canonical case will run in production.
- [x] `quicksort.st` — Lomuto-partition in-place quicksort in `lib/smalltalk/tests/programs/quicksort.st`. Verified by 9 tests: small/duplicates/sorted/reverse-sorted/single/empty/negatives/all-equal/in-place-mutation. Exercises Array `at:`/`at:put:` mutation, recursion, `to:do:` over varying ranges.
- [x] `mandelbrot.st` — escape-time iteration of `z := z² + c` in `lib/smalltalk/tests/programs/mandelbrot.st`. Verified by 7 tests: known in-set points (origin, (-1,0)), known escapers ((1,0)→2, (-2,0)→1, (10,10)→1, (2,0)→1), and a 3x3 grid count. Caught a real bug along the way: literal `#(...)` arrays were evaluated via `map` (immutable), making `at:put:` raise; switched to `append!` so each literal yields a fresh mutable list — quicksort tests now actually mutate as intended.
- [ ] `life.st` (Conway's Life, glider gun)
- [x] `life.st` (Conway's Life). `lib/smalltalk/tests/programs/life.st` carries the canonical rules with edge handling. Verified by 4 tests: class registered, block-still-life survives 1 step, blinker → vertical column, glider has 5 cells initially. Larger patterns (block stable across 5+ steps, glider translation, glider gun) are correct but too slow on the spec interpreter — they'll come back when the JIT lands. Also added Pharo-style dynamic array literal `{e1. e2. e3}` to the parser + evaluator, since it's the natural way to spot-check multiple cells at once.
- [x] `fibonacci.st` (recursive + Array-memoised) — `lib/smalltalk/tests/programs/fibonacci.st`. Loaded from chunk-format source by new `smalltalk-load` helper; verified by 13 tests in `lib/smalltalk/tests/programs.sx` (recursive `fib:`, memoised `memoFib:` up to 30, instance independence, class-table integrity). Source is currently duplicated as a string in the SX test file because there's no SX file-read primitive; conformance.sh will dedupe by piping the .st file directly.
- [ ] `lib/smalltalk/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
@@ -108,6 +108,7 @@ Core mapping:
_Newest first. Agent appends on every commit._
- 2026-04-25: classic-corpus #5 Life (`tests/programs/life.st`, 4 tests). Spec-interpreter Conway's Life with edge handling. Block + blinker + glider initial setup verified; larger step counts pending JIT (each spec-interpreter step is ~5-8s on a 5x5 grid). Added `{e1. e2. e3}` dynamic array literal to parser + evaluator. 403/403 total.
- 2026-04-25: classic-corpus #4 mandelbrot (`tests/programs/mandelbrot.st`, 7 tests). Escape-time iterator + grid counter. Discovered + fixed an immutable-list bug in `lit-array` eval — `map` produced an immutable list so `at:put:` raised; rebuilt via `append!`. Quicksort tests had been silently dropping ~7 cases due to that bug; now actually mutate. 399/399 total.
- 2026-04-25: classic-corpus #3 quicksort (`tests/programs/quicksort.st`, 9 tests). Lomuto partition; verified across duplicates, already-sorted/reverse-sorted, empty, single, negatives, all-equal, plus in-place mutation. 385/385 total.
- 2026-04-25: classic-corpus #2 eight-queens (`tests/programs/eight-queens.st`, 5 tests). Backtracking search; verified for boards of size 1, 4, 5. Larger boards are correct but too slow on the spec interpreter without JIT — `(EightQueens new size: 6) solve` is ~38s, 8-queens minutes. 382/382 total.