smalltalk: super send + top-level temps + 9 super tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -296,20 +296,41 @@
|
||||
((st-class-ref? v) "Class")
|
||||
(else (st-class-of v)))))
|
||||
|
||||
;; super send: lookup starts at the *defining* class's superclass, not the
|
||||
;; receiver class. This is what makes inherited methods compose correctly
|
||||
;; under refinement — a method on Foo that calls `super bar` resolves to
|
||||
;; Foo's superclass's `bar` regardless of the dynamic receiver class.
|
||||
(define
|
||||
st-super-send
|
||||
(fn
|
||||
(receiver selector args defining-class)
|
||||
(let
|
||||
((super (st-class-superclass defining-class)))
|
||||
(cond
|
||||
((= super nil)
|
||||
(error (str "super send past Object: " selector)))
|
||||
(else
|
||||
(let ((method (st-method-lookup super selector false)))
|
||||
(cond
|
||||
((not (= method nil)) (st-invoke method receiver args))
|
||||
(else (st-primitive-send receiver selector args)))))))))
|
||||
(cond
|
||||
((= defining-class nil)
|
||||
(error (str "super send outside method context: " selector)))
|
||||
(else
|
||||
(let
|
||||
((super (st-class-superclass defining-class))
|
||||
(class-side? (st-class-ref? receiver)))
|
||||
(cond
|
||||
((= super nil)
|
||||
(error (str "super send past root: " selector " in " defining-class)))
|
||||
(else
|
||||
(let ((method (st-method-lookup super selector class-side?)))
|
||||
(cond
|
||||
((not (= method nil))
|
||||
(st-invoke method receiver args))
|
||||
(else
|
||||
;; Try primitives starting from super's perspective too —
|
||||
;; for native receivers the primitive table is global, so
|
||||
;; super basically reaches the same primitives. The point
|
||||
;; of super is to skip user overrides on the receiver's
|
||||
;; class chain below `super`, which method-lookup above
|
||||
;; already enforces.
|
||||
(let ((p (st-primitive-send receiver selector args)))
|
||||
(cond
|
||||
((= p :unhandled)
|
||||
(st-dnu receiver selector args))
|
||||
(else p)))))))))))))
|
||||
|
||||
;; ── Method invocation ──────────────────────────────────────────────────
|
||||
(define
|
||||
@@ -689,7 +710,13 @@
|
||||
(src)
|
||||
(let
|
||||
((ast (st-parse src)) (frame (st-make-frame nil nil nil)))
|
||||
(let ((result (smalltalk-eval-ast ast frame)))
|
||||
(cond
|
||||
((st-return-marker? result) (get result :value))
|
||||
(else result))))))
|
||||
(begin
|
||||
(when
|
||||
(and (dict? ast) (has-key? ast :temps))
|
||||
(for-each
|
||||
(fn (t) (dict-set! (get frame :locals) t nil))
|
||||
(get ast :temps)))
|
||||
(let ((result (smalltalk-eval-ast ast frame)))
|
||||
(cond
|
||||
((st-return-marker? result) (get result :value))
|
||||
(else result)))))))
|
||||
|
||||
@@ -883,9 +883,30 @@
|
||||
:pragmas pragmas
|
||||
:body body}))))
|
||||
|
||||
;; Top-level program: statements separated by '.'
|
||||
;; Top-level program: optional temp declaration, then statements
|
||||
;; separated by '.'. Pharo workspace-style scripts allow
|
||||
;; `| temps | body...` at the top level.
|
||||
(cond
|
||||
((= mode "expr") (parse-expression))
|
||||
((= mode "method") (parse-method))
|
||||
(else
|
||||
{:type "seq" :exprs (parse-statements "eof")}))))))
|
||||
(let ((temps (list)))
|
||||
(begin
|
||||
(when
|
||||
(at? "bar" nil)
|
||||
(begin
|
||||
(advance-tok!)
|
||||
(define
|
||||
tt-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(at-type? "ident")
|
||||
(let ((t (peek-tok)))
|
||||
(begin
|
||||
(advance-tok!)
|
||||
(append! temps (st-tok-value t))
|
||||
(tt-loop))))))
|
||||
(tt-loop)
|
||||
(consume! "bar" nil)))
|
||||
{:type "seq" :temps temps :exprs (parse-statements "eof")}))))))))
|
||||
|
||||
@@ -289,13 +289,13 @@
|
||||
(st-test
|
||||
"return statement at top level"
|
||||
(st-parse "^ 1")
|
||||
{:type "seq"
|
||||
{:type "seq" :temps (list)
|
||||
:exprs (list {:type "return" :expr {:type "lit-int" :value 1}})})
|
||||
|
||||
(st-test
|
||||
"two statements"
|
||||
(st-parse "x := 1. y := 2")
|
||||
{:type "seq"
|
||||
{:type "seq" :temps (list)
|
||||
:exprs (list
|
||||
{:type "assign" :name "x" :expr {:type "lit-int" :value 1}}
|
||||
{:type "assign" :name "y" :expr {:type "lit-int" :value 2}})})
|
||||
@@ -303,7 +303,7 @@
|
||||
(st-test
|
||||
"trailing dot allowed"
|
||||
(st-parse "1. 2.")
|
||||
{:type "seq"
|
||||
{:type "seq" :temps (list)
|
||||
:exprs (list {:type "lit-int" :value 1} {:type "lit-int" :value 2})})
|
||||
|
||||
;; ── 12. Method headers ──
|
||||
|
||||
149
lib/smalltalk/tests/super.sx
Normal file
149
lib/smalltalk/tests/super.sx
Normal file
@@ -0,0 +1,149 @@
|
||||
;; super-send tests.
|
||||
;;
|
||||
;; super looks up methods starting at the *defining class*'s superclass —
|
||||
;; not the receiver's class. This means an inherited method that uses
|
||||
;; `super` always reaches the same parent regardless of where in the
|
||||
;; subclass chain the receiver actually sits.
|
||||
|
||||
(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. Basic super: subclass override calls parent ──
|
||||
(st-class-define! "Animal" "Object" (list))
|
||||
(st-class-add-method! "Animal" "speak"
|
||||
(st-parse-method "speak ^ #generic"))
|
||||
|
||||
(st-class-define! "Dog" "Animal" (list))
|
||||
(st-class-add-method! "Dog" "speak"
|
||||
(st-parse-method "speak ^ super speak"))
|
||||
|
||||
(st-test
|
||||
"super reaches parent's speak"
|
||||
(str (evp "^ Dog new speak"))
|
||||
"generic")
|
||||
|
||||
(st-class-add-method! "Dog" "loud"
|
||||
(st-parse-method "loud ^ super speak , #'!' asString"))
|
||||
;; The above tries to use `, #'!' asString` which won't quite work with my
|
||||
;; primitives. Replace with a simpler test.
|
||||
(st-class-add-method! "Dog" "loud"
|
||||
(st-parse-method "loud | s | s := super speak. ^ s"))
|
||||
|
||||
(st-test
|
||||
"method calls super and returns same"
|
||||
(str (evp "^ Dog new loud"))
|
||||
"generic")
|
||||
|
||||
;; ── 2. Super with argument ──
|
||||
(st-class-add-method! "Animal" "greet:"
|
||||
(st-parse-method "greet: name ^ name , ' (animal)'"))
|
||||
(st-class-add-method! "Dog" "greet:"
|
||||
(st-parse-method "greet: name ^ super greet: name"))
|
||||
|
||||
(st-test
|
||||
"super with arg reaches parent and threads value"
|
||||
(evp "^ Dog new greet: 'Rex'")
|
||||
"Rex (animal)")
|
||||
|
||||
;; ── 3. Inherited method uses *defining* class for super ──
|
||||
;; A defines speak ^ 'A'
|
||||
;; A defines speakLog: which sends `super speak`. super starts at Object → no
|
||||
;; speak there → DNU. So invoke speakLog from A subclass to test that super
|
||||
;; resolves to A's parent (Object), not the subclass's parent.
|
||||
(st-class-define! "RootSpeaker" "Object" (list))
|
||||
(st-class-add-method! "RootSpeaker" "speak"
|
||||
(st-parse-method "speak ^ #root"))
|
||||
(st-class-add-method! "RootSpeaker" "speakDelegate"
|
||||
(st-parse-method "speakDelegate ^ super speak"))
|
||||
;; Object has no speak (and we add a temporary DNU for testing).
|
||||
(st-class-add-method! "Object" "doesNotUnderstand:"
|
||||
(st-parse-method "doesNotUnderstand: aMessage ^ #dnu"))
|
||||
|
||||
(st-class-define! "ChildSpeaker" "RootSpeaker" (list))
|
||||
(st-class-add-method! "ChildSpeaker" "speak"
|
||||
(st-parse-method "speak ^ #child"))
|
||||
|
||||
(st-test
|
||||
"inherited speakDelegate uses RootSpeaker's super, not ChildSpeaker's"
|
||||
(str (evp "^ ChildSpeaker new speakDelegate"))
|
||||
"dnu")
|
||||
|
||||
;; A non-inherited path: ChildSpeaker overrides speak, but speakDelegate is
|
||||
;; inherited from RootSpeaker. The super inside speakDelegate must resolve to
|
||||
;; *Object* (RootSpeaker's parent), not to RootSpeaker (ChildSpeaker's parent).
|
||||
(st-test
|
||||
"inherited method's super does not call subclass override"
|
||||
(str (evp "^ ChildSpeaker new speak"))
|
||||
"child")
|
||||
|
||||
;; Remove the Object DNU shim now that those tests are done.
|
||||
(st-class-remove-method! "Object" "doesNotUnderstand:")
|
||||
|
||||
;; ── 4. Multi-level: A → B → C ──
|
||||
(st-class-define! "GA" "Object" (list))
|
||||
(st-class-add-method! "GA" "level"
|
||||
(st-parse-method "level ^ #ga"))
|
||||
|
||||
(st-class-define! "GB" "GA" (list))
|
||||
(st-class-add-method! "GB" "level"
|
||||
(st-parse-method "level ^ super level"))
|
||||
|
||||
(st-class-define! "GC" "GB" (list))
|
||||
(st-class-add-method! "GC" "level"
|
||||
(st-parse-method "level ^ super level"))
|
||||
|
||||
(st-test
|
||||
"super chains to grandparent"
|
||||
(str (evp "^ GC new level"))
|
||||
"ga")
|
||||
|
||||
;; ── 5. Super inside a block ──
|
||||
(st-class-add-method! "Dog" "delayed"
|
||||
(st-parse-method "delayed ^ [super speak] value"))
|
||||
(st-test
|
||||
"super inside a block resolves correctly"
|
||||
(str (evp "^ Dog new delayed"))
|
||||
"generic")
|
||||
|
||||
;; ── 6. Super send keeps receiver as self ──
|
||||
(st-class-define! "Counter" "Object" (list "count"))
|
||||
(st-class-add-method! "Counter" "init"
|
||||
(st-parse-method "init count := 0. ^ self"))
|
||||
(st-class-add-method! "Counter" "incr"
|
||||
(st-parse-method "incr count := count + 1. ^ self"))
|
||||
(st-class-add-method! "Counter" "count"
|
||||
(st-parse-method "count ^ count"))
|
||||
|
||||
(st-class-define! "DoubleCounter" "Counter" (list))
|
||||
(st-class-add-method! "DoubleCounter" "incr"
|
||||
(st-parse-method "incr super incr. super incr. ^ self"))
|
||||
|
||||
(st-test
|
||||
"super uses same receiver — ivars on self update"
|
||||
(evp "| c | c := DoubleCounter new init. c incr. ^ c count")
|
||||
2)
|
||||
|
||||
;; ── 7. Super on a class without an immediate parent definition ──
|
||||
;; Mid-chain class with no override at this level: super resolves correctly
|
||||
;; through the missing rung.
|
||||
(st-class-define! "Mid" "Animal" (list))
|
||||
(st-class-define! "Pup" "Mid" (list))
|
||||
(st-class-add-method! "Pup" "speak"
|
||||
(st-parse-method "speak ^ super speak"))
|
||||
|
||||
(st-test
|
||||
"super walks past intermediate class with no override"
|
||||
(str (evp "^ Pup new speak"))
|
||||
"generic")
|
||||
|
||||
;; ── 8. Super outside any method errors ──
|
||||
;; (We don't have try/catch in SX from here; skip the negative test —
|
||||
;; documented behaviour is that st-super-send errors when method-class is nil.)
|
||||
|
||||
(list st-test-pass st-test-fail)
|
||||
@@ -60,7 +60,7 @@ Core mapping:
|
||||
- [x] `smalltalk-eval-ast` (`lib/smalltalk/eval.sx`): all literal kinds, ident resolution (locals → ivars → class refs), self/super/thisContext, assignment (locals or ivars, mutating), message send, cascade, sequence, and ^return via a sentinel marker (proper continuation-based escape is the Phase 3 showcase). Frames carry a parent chain so blocks close over outer locals. Primitive method tables for SmallInteger/Float, String/Symbol, Boolean, UndefinedObject, Array, BlockClosure (value/value:/whileTrue:/etc.), and class-side `new`/`name`/etc. Also satisfies "30+ tests" — 60 eval tests.
|
||||
- [x] Method lookup: walk class → superclass already in `st-method-lookup-walk`; new cached wrapper `st-method-lookup` keys on `(class, selector, side)` and stores `:not-found` for negative results so DNU paths don't re-walk. Cache invalidates on `st-class-define!`, `st-class-add-method!`, `st-class-add-class-method!`, `st-class-remove-method!`, and full bootstrap. Stats helpers `st-method-cache-stats` / `st-method-cache-reset-stats!` for tests + later debugging.
|
||||
- [x] `doesNotUnderstand:` fallback. `Message` class added at bootstrap with `selector`/`arguments` ivars and accessor methods. Primitive senders (Number/String/Boolean/Nil/Array/BlockClosure/class-side) now return the `:unhandled` sentinel for unknown selectors; `st-send` builds a `Message` via `st-make-message` and routes through `st-dnu`, which looks up `doesNotUnderstand:` on the receiver's class chain (instance- or class-side as appropriate). User overrides intercept unknowns and see the symbol selector + arguments array in the Message.
|
||||
- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class)
|
||||
- [x] `super` send. Method invocation captures the defining class on the frame; `st-super-send` walks from `(st-class-superclass defining-class)` (instance- or class-side as appropriate). Falls through primitives → DNU when no method is found. Receiver is preserved as `self`, so ivar mutations stick. Verified for: subclass override calls parent, inherited `super` resolves to *defining* class's parent (not receiver's), multi-level `A→B→C` chain, super inside a block, super walks past an intermediate class with no local override.
|
||||
- [x] 30+ tests in `lib/smalltalk/tests/eval.sx` (60 tests, covering literals through user-class method dispatch with cascades and closures)
|
||||
|
||||
### Phase 3 — blocks + non-local return (THE SHOWCASE)
|
||||
@@ -108,6 +108,7 @@ Core mapping:
|
||||
|
||||
_Newest first. Agent appends on every commit._
|
||||
|
||||
- 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.
|
||||
- 2026-04-25: method-lookup cache (`st-method-cache` keyed by `class|selector|side`, stores `:not-found` for misses). Invalidation on define/add/remove + bootstrap. `st-class-remove-method!` added. Stats helpers + 10 cache tests; 255/255 total.
|
||||
- 2026-04-25: `smalltalk-eval-ast` + 60 eval tests (`lib/smalltalk/eval.sx`, `lib/smalltalk/tests/eval.sx`). Frame chain with mutable locals/ivars (via `dict-set!`), full literal eval, send dispatch (user methods + native primitive tables for Number/String/Boolean/Nil/Array/Block/Class), block closures, while/to:do:, cascades returning last, sentinel-based `^return`. User Point class round-trip works including `+` returning a fresh point. 245/245 total.
|
||||
|
||||
Reference in New Issue
Block a user