diff --git a/lib/smalltalk/runtime.sx b/lib/smalltalk/runtime.sx new file mode 100644 index 00000000..1f66e5ec --- /dev/null +++ b/lib/smalltalk/runtime.sx @@ -0,0 +1,291 @@ +;; Smalltalk runtime — class table, bootstrap hierarchy, type→class mapping, +;; instance construction. Method dispatch / eval-ast live in a later layer. +;; +;; Class record shape: +;; {:name "Foo" +;; :superclass "Object" ; or nil for Object itself +;; :ivars (list "x" "y") ; instance variable names declared on this class +;; :methods (dict selector→method-record) +;; :class-methods (dict selector→method-record)} +;; +;; A method record is the AST returned by st-parse-method, plus a :defining-class +;; field so super-sends can resolve from the right place. (Methods are registered +;; via runtime helpers that fill the field.) +;; +;; The class table is a single dict keyed by class name. Bootstrap installs the +;; canonical hierarchy. Test code resets it via (st-bootstrap-classes!). + +(define st-class-table {}) + +(define st-class-table-clear! (fn () (set! st-class-table {}))) + +(define + st-class-define! + (fn + (name superclass ivars) + (begin + (set! + st-class-table + (assoc + st-class-table + name + {:name name + :superclass superclass + :ivars ivars + :methods {} + :class-methods {}})) + name))) + +(define + st-class-get + (fn (name) (if (has-key? st-class-table name) (get st-class-table name) nil))) + +(define + st-class-exists? + (fn (name) (has-key? st-class-table name))) + +(define + st-class-superclass + (fn + (name) + (let + ((c (st-class-get name))) + (cond ((= c nil) nil) (else (get c :superclass)))))) + +;; Walk class chain root-to-leaf? No, follow superclass chain leaf-to-root. +;; Returns list of class names starting at `name` and ending with the root. +(define + st-class-chain + (fn + (name) + (let ((acc (list)) (cur name)) + (begin + (define + ch-loop + (fn + () + (when + (and (not (= cur nil)) (st-class-exists? cur)) + (begin + (append! acc cur) + (set! cur (st-class-superclass cur)) + (ch-loop))))) + (ch-loop) + acc)))) + +;; Inherited + own ivars in declaration order from root to leaf. +(define + st-class-all-ivars + (fn + (name) + (let ((chain (reverse (st-class-chain name))) (out (list))) + (begin + (for-each + (fn + (cn) + (let + ((c (st-class-get cn))) + (when + (not (= c nil)) + (for-each (fn (iv) (append! out iv)) (get c :ivars))))) + chain) + out)))) + +;; Method install. The defining-class field is stamped on the method record +;; so super-sends look up from the right point in the chain. +(define + st-class-add-method! + (fn + (cls-name selector method-ast) + (let + ((cls (st-class-get cls-name))) + (cond + ((= cls nil) (error (str "st-class-add-method!: unknown class " cls-name))) + (else + (let + ((m (assoc method-ast :defining-class cls-name))) + (begin + (set! + st-class-table + (assoc + st-class-table + cls-name + (assoc + cls + :methods + (assoc (get cls :methods) selector m)))) + selector))))))) + +(define + st-class-add-class-method! + (fn + (cls-name selector method-ast) + (let + ((cls (st-class-get cls-name))) + (cond + ((= cls nil) (error (str "st-class-add-class-method!: unknown class " cls-name))) + (else + (let + ((m (assoc method-ast :defining-class cls-name))) + (begin + (set! + st-class-table + (assoc + st-class-table + cls-name + (assoc + cls + :class-methods + (assoc (get cls :class-methods) selector m)))) + selector))))))) + +;; Method lookup: walk superclass chain starting at `cls-name`. +;; class-side? = true searches :class-methods, false searches :methods. +;; Returns the method record (with :defining-class) or nil. +(define + st-method-lookup + (fn + (cls-name selector class-side?) + (let + ((found nil)) + (begin + (define + ml-loop + (fn + (cur) + (when + (and (= found nil) (not (= cur nil)) (st-class-exists? cur)) + (let + ((c (st-class-get cur))) + (let + ((dict (if class-side? (get c :class-methods) (get c :methods)))) + (cond + ((has-key? dict selector) (set! found (get dict selector))) + (else (ml-loop (get c :superclass))))))))) + (ml-loop cls-name) + found)))) + +;; SX value → Smalltalk class name. Native types are not boxed. +(define + st-class-of + (fn + (v) + (cond + ((= v nil) "UndefinedObject") + ((= v true) "True") + ((= v false) "False") + ((integer? v) "SmallInteger") + ((number? v) "Float") + ((string? v) "String") + ((symbol? v) "Symbol") + ((list? v) "Array") + ((and (dict? v) (has-key? v :type) (= (get v :type) "st-instance")) + (get v :class)) + ((and (dict? v) (has-key? v :type) (= (get v :type) "block")) + "BlockClosure") + ((and (dict? v) (has-key? v :st-block?) (get v :st-block?)) + "BlockClosure") + ((dict? v) "Dictionary") + ((lambda? v) "BlockClosure") + (else "Object")))) + +;; Construct a fresh instance of cls-name. Ivars (own + inherited) start as nil. +(define + st-make-instance + (fn + (cls-name) + (cond + ((not (st-class-exists? cls-name)) + (error (str "st-make-instance: unknown class " cls-name))) + (else + (let + ((iv-names (st-class-all-ivars cls-name)) (ivars {})) + (begin + (for-each (fn (n) (set! ivars (assoc ivars n nil))) iv-names) + {:type "st-instance" :class cls-name :ivars ivars})))))) + +(define + st-instance? + (fn + (v) + (and (dict? v) (has-key? v :type) (= (get v :type) "st-instance")))) + +(define + st-iv-get + (fn + (inst name) + (let ((ivs (get inst :ivars))) + (if (has-key? ivs name) (get ivs name) nil)))) + +(define + st-iv-set! + (fn + (inst name value) + (let + ((new-ivars (assoc (get inst :ivars) name value))) + (assoc inst :ivars new-ivars)))) + +;; Inherits-from check: is `descendant` either equal to `ancestor` or a subclass? +(define + st-class-inherits-from? + (fn + (descendant ancestor) + (let ((found false) (cur descendant)) + (begin + (define + ih-loop + (fn + () + (when + (and (not found) (not (= cur nil)) (st-class-exists? cur)) + (cond + ((= cur ancestor) (set! found true)) + (else + (begin + (set! cur (st-class-superclass cur)) + (ih-loop))))))) + (ih-loop) + found)))) + +;; Bootstrap the canonical class hierarchy. Reset and rebuild. +(define + st-bootstrap-classes! + (fn + () + (begin + (st-class-table-clear!) + ;; Root + (st-class-define! "Object" nil (list)) + ;; Class side machinery + (st-class-define! "Behavior" "Object" (list "superclass" "methodDict" "format")) + (st-class-define! "ClassDescription" "Behavior" (list "instanceVariables" "organization")) + (st-class-define! "Class" "ClassDescription" (list "name" "subclasses")) + (st-class-define! "Metaclass" "ClassDescription" (list "thisClass")) + ;; Pseudo-variable types + (st-class-define! "UndefinedObject" "Object" (list)) + (st-class-define! "Boolean" "Object" (list)) + (st-class-define! "True" "Boolean" (list)) + (st-class-define! "False" "Boolean" (list)) + ;; Magnitudes + (st-class-define! "Magnitude" "Object" (list)) + (st-class-define! "Number" "Magnitude" (list)) + (st-class-define! "Integer" "Number" (list)) + (st-class-define! "SmallInteger" "Integer" (list)) + (st-class-define! "LargePositiveInteger" "Integer" (list)) + (st-class-define! "Float" "Number" (list)) + (st-class-define! "Character" "Magnitude" (list "value")) + ;; Collections + (st-class-define! "Collection" "Object" (list)) + (st-class-define! "SequenceableCollection" "Collection" (list)) + (st-class-define! "ArrayedCollection" "SequenceableCollection" (list)) + (st-class-define! "Array" "ArrayedCollection" (list)) + (st-class-define! "String" "ArrayedCollection" (list)) + (st-class-define! "Symbol" "String" (list)) + (st-class-define! "OrderedCollection" "SequenceableCollection" (list "array" "firstIndex" "lastIndex")) + (st-class-define! "Dictionary" "Collection" (list)) + ;; Blocks / contexts + (st-class-define! "BlockClosure" "Object" (list)) + "ok"))) + +;; Initialise on load. Tests can re-bootstrap to reset state. +(st-bootstrap-classes!) diff --git a/lib/smalltalk/test.sh b/lib/smalltalk/test.sh index 90c01854..afe6ef3b 100755 --- a/lib/smalltalk/test.sh +++ b/lib/smalltalk/test.sh @@ -59,10 +59,12 @@ EPOCHS (epoch 2) (load "lib/smalltalk/parser.sx") (epoch 3) -(load "lib/smalltalk/tests/tokenize.sx") +(load "lib/smalltalk/runtime.sx") (epoch 4) -(load "$FILE") +(load "lib/smalltalk/tests/tokenize.sx") (epoch 5) +(load "$FILE") +(epoch 6) (eval "(list st-test-pass st-test-fail)") EPOCHS fi @@ -108,10 +110,12 @@ EPOCHS (epoch 2) (load "lib/smalltalk/parser.sx") (epoch 3) -(load "lib/smalltalk/tests/tokenize.sx") +(load "lib/smalltalk/runtime.sx") (epoch 4) -(load "$FILE") +(load "lib/smalltalk/tests/tokenize.sx") (epoch 5) +(load "$FILE") +(epoch 6) (eval "(map (fn (f) (get f :name)) st-test-fails)") EPOCHS fi diff --git a/lib/smalltalk/tests/runtime.sx b/lib/smalltalk/tests/runtime.sx new file mode 100644 index 00000000..3561132e --- /dev/null +++ b/lib/smalltalk/tests/runtime.sx @@ -0,0 +1,182 @@ +;; Smalltalk runtime tests — class table, type→class mapping, instances. +;; +;; Reuses helpers (st-test, st-deep=?) from tokenize.sx. Counters reset +;; here so this file's summary covers runtime tests only. + +(set! st-test-pass 0) +(set! st-test-fail 0) +(set! st-test-fails (list)) + +;; Fresh hierarchy for every test file. +(st-bootstrap-classes!) + +;; ── 1. Bootstrap installed expected classes ── +(st-test "Object exists" (st-class-exists? "Object") true) +(st-test "Behavior exists" (st-class-exists? "Behavior") true) +(st-test "Metaclass exists" (st-class-exists? "Metaclass") true) +(st-test "True/False/UndefinedObject" + (and + (st-class-exists? "True") + (st-class-exists? "False") + (st-class-exists? "UndefinedObject")) + true) +(st-test "SmallInteger / Float / Symbol exist" + (and + (st-class-exists? "SmallInteger") + (st-class-exists? "Float") + (st-class-exists? "Symbol")) + true) +(st-test "BlockClosure exists" (st-class-exists? "BlockClosure") true) + +;; ── 2. Superclass chain ── +(st-test "Object has no superclass" (st-class-superclass "Object") nil) +(st-test "Behavior super = Object" (st-class-superclass "Behavior") "Object") +(st-test "True super = Boolean" (st-class-superclass "True") "Boolean") +(st-test "Symbol super = String" (st-class-superclass "Symbol") "String") +(st-test + "String chain" + (st-class-chain "String") + (list "String" "ArrayedCollection" "SequenceableCollection" "Collection" "Object")) +(st-test + "SmallInteger chain" + (st-class-chain "SmallInteger") + (list "SmallInteger" "Integer" "Number" "Magnitude" "Object")) + +;; ── 3. inherits-from? ── +(st-test "True inherits from Boolean" (st-class-inherits-from? "True" "Boolean") true) +(st-test "True inherits from Object" (st-class-inherits-from? "True" "Object") true) +(st-test "True inherits from True" (st-class-inherits-from? "True" "True") true) +(st-test + "True does not inherit from Number" + (st-class-inherits-from? "True" "Number") + false) +(st-test + "Object does not inherit from Number" + (st-class-inherits-from? "Object" "Number") + false) + +;; ── 4. type→class mapping ── +(st-test "class-of nil" (st-class-of nil) "UndefinedObject") +(st-test "class-of true" (st-class-of true) "True") +(st-test "class-of false" (st-class-of false) "False") +(st-test "class-of int" (st-class-of 42) "SmallInteger") +(st-test "class-of zero" (st-class-of 0) "SmallInteger") +(st-test "class-of negative int" (st-class-of -3) "SmallInteger") +(st-test "class-of float" (st-class-of 3.14) "Float") +(st-test "class-of string" (st-class-of "hi") "String") +(st-test "class-of symbol" (st-class-of (quote foo)) "Symbol") +(st-test "class-of list" (st-class-of (list 1 2)) "Array") +(st-test "class-of empty list" (st-class-of (list)) "Array") +(st-test "class-of lambda" (st-class-of (fn (x) x)) "BlockClosure") +(st-test "class-of dict" (st-class-of {:a 1}) "Dictionary") + +;; ── 5. User class definition ── +(st-class-define! "Account" "Object" (list "balance" "owner")) +(st-class-define! "SavingsAccount" "Account" (list "rate")) + +(st-test "Account exists" (st-class-exists? "Account") true) +(st-test "Account super = Object" (st-class-superclass "Account") "Object") +(st-test + "SavingsAccount chain" + (st-class-chain "SavingsAccount") + (list "SavingsAccount" "Account" "Object")) +(st-test + "SavingsAccount own ivars" + (get (st-class-get "SavingsAccount") :ivars) + (list "rate")) +(st-test + "SavingsAccount inherited+own ivars" + (st-class-all-ivars "SavingsAccount") + (list "balance" "owner" "rate")) + +;; ── 6. Instance construction ── +(define a1 (st-make-instance "Account")) +(st-test "instance is st-instance" (st-instance? a1) true) +(st-test "instance class" (get a1 :class) "Account") +(st-test "instance ivars start nil" (st-iv-get a1 "balance") nil) +(st-test + "instance has all expected ivars" + (sort (keys (get a1 :ivars))) + (sort (list "balance" "owner"))) +(define a2 (st-iv-set! a1 "balance" 100)) +(st-test "iv-set! returns updated copy" (st-iv-get a2 "balance") 100) +(st-test "iv-set! does not mutate original" (st-iv-get a1 "balance") nil) +(st-test "class-of instance" (st-class-of a1) "Account") + +(define s1 (st-make-instance "SavingsAccount")) +(st-test + "subclass instance has all inherited ivars" + (sort (keys (get s1 :ivars))) + (sort (list "balance" "owner" "rate"))) + +;; ── 7. Method install + lookup ── +(st-class-add-method! + "Account" + "balance" + (st-parse-method "balance ^ balance")) +(st-class-add-method! + "Account" + "deposit:" + (st-parse-method "deposit: amount balance := balance + amount. ^ self")) + +(st-test + "method registered" + (has-key? (get (st-class-get "Account") :methods) "balance") + true) + +(st-test + "method lookup direct" + (= (st-method-lookup "Account" "balance" false) nil) + false) + +(st-test + "method lookup walks superclass" + (= (st-method-lookup "SavingsAccount" "deposit:" false) nil) + false) + +(st-test + "method lookup unknown selector" + (st-method-lookup "Account" "frobnicate" false) + nil) + +(st-test + "method lookup records defining class" + (get (st-method-lookup "SavingsAccount" "balance" false) :defining-class) + "Account") + +;; SavingsAccount overrides deposit: +(st-class-add-method! + "SavingsAccount" + "deposit:" + (st-parse-method "deposit: amount ^ super deposit: amount + 1")) + +(st-test + "subclass override picked first" + (get (st-method-lookup "SavingsAccount" "deposit:" false) :defining-class) + "SavingsAccount") + +(st-test + "Account still finds its own deposit:" + (get (st-method-lookup "Account" "deposit:" false) :defining-class) + "Account") + +;; ── 8. Class-side methods ── +(st-class-add-class-method! + "Account" + "new" + (st-parse-method "new ^ super new")) +(st-test + "class-side lookup" + (= (st-method-lookup "Account" "new" true) nil) + false) +(st-test + "instance-side does not find class method" + (st-method-lookup "Account" "new" false) + nil) + +;; ── 9. Re-bootstrap resets table ── +(st-bootstrap-classes!) +(st-test "after re-bootstrap Account gone" (st-class-exists? "Account") false) +(st-test "after re-bootstrap Object stays" (st-class-exists? "Object") true) + +(list st-test-pass st-test-fail) diff --git a/plans/smalltalk-on-sx.md b/plans/smalltalk-on-sx.md index dec0cd01..e6f32b20 100644 --- a/plans/smalltalk-on-sx.md +++ b/plans/smalltalk-on-sx.md @@ -56,7 +56,7 @@ Core mapping: - [x] 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` +- [x] Class table + bootstrap (`lib/smalltalk/runtime.sx`): canonical hierarchy installed (`Object`, `Behavior`, `ClassDescription`, `Class`, `Metaclass`, `UndefinedObject`, `Boolean`/`True`/`False`, `Magnitude`/`Number`/`Integer`/`SmallInteger`/`Float`/`Character`, `Collection`/`SequenceableCollection`/`ArrayedCollection`/`Array`/`String`/`Symbol`/`OrderedCollection`/`Dictionary`, `BlockClosure`). User class definition via `st-class-define!`, methods via `st-class-add-method!` (stamps `:defining-class` for super), method lookup walks chain, ivars accumulated through superclass chain, native SX value types map to Smalltalk classes via `st-class-of`. - [ ] `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 @@ -108,6 +108,7 @@ Core mapping: _Newest first. Agent appends on every commit._ +- 2026-04-25: class table + bootstrap (`lib/smalltalk/runtime.sx`, `lib/smalltalk/tests/runtime.sx`). Canonical hierarchy, type→class mapping for native SX values, instance construction, ivar inheritance, method install with `:defining-class` stamp, instance- and class-side method lookup walking the superclass chain. 54 new tests, 185/185 total. - 2026-04-25: chunk-stream parser + pragmas + 21 chunk/pragma tests (`lib/smalltalk/tests/parse_chunks.sx`). `st-read-chunks` (with `!!` doubling), `st-parse-chunks` state machine for `methodsFor:` batches incl. class-side. Pragmas with multiple keyword pairs, signed numeric / string / symbol args, in either pragma-then-temps or temps-then-pragma order. 131/131 tests pass. - 2026-04-25: expression-level parser + 47 parse tests (`lib/smalltalk/parser.sx`, `lib/smalltalk/tests/parse.sx`). Full message precedence (unary > binary > keyword), cascades, blocks with params/temps, literal/byte arrays, assignment chain, method headers (unary/binary/keyword). Chunk-format `! !` driver deferred to a follow-up box. 110/110 tests pass. - 2026-04-25: tokenizer + 63 tests (`lib/smalltalk/tokenizer.sx`, `lib/smalltalk/tests/tokenize.sx`, `lib/smalltalk/test.sh`). All token types covered except scaled decimals `1.5s2` (deferred). `#(` and `#[` emit open tokens; literal-array contents lexed as ordinary tokens for the parser to interpret.