smalltalk: eval-ast + 60 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:
656
lib/smalltalk/eval.sx
Normal file
656
lib/smalltalk/eval.sx
Normal file
@@ -0,0 +1,656 @@
|
||||
;; Smalltalk AST evaluator — sequential semantics. Method dispatch uses the
|
||||
;; class table from runtime.sx; native receivers fall back to a primitive
|
||||
;; method table. Non-local return is implemented via a sentinel marker; the
|
||||
;; full continuation-based escape is the Phase 3 showcase.
|
||||
;;
|
||||
;; Frame:
|
||||
;; {:self V ; receiver
|
||||
;; :method-class N ; defining class of the executing method
|
||||
;; :locals (mutable dict) ; param + temp bindings
|
||||
;; :parent P} ; outer frame for blocks (nil for top-level)
|
||||
;;
|
||||
;; `smalltalk-eval-ast(ast, frame)` returns the value or a return marker.
|
||||
;; Method invocation unwraps return markers; sequences propagate them.
|
||||
|
||||
(define
|
||||
st-make-frame
|
||||
(fn
|
||||
(self method-class parent)
|
||||
{:self self :method-class method-class :locals {} :parent parent}))
|
||||
|
||||
(define st-return-marker (fn (v) {:st-return true :value v}))
|
||||
|
||||
(define
|
||||
st-return-marker?
|
||||
(fn (v) (and (dict? v) (has-key? v :st-return) (= (get v :st-return) true))))
|
||||
|
||||
(define
|
||||
st-make-block
|
||||
(fn
|
||||
(ast frame)
|
||||
{:type "st-block"
|
||||
:params (get ast :params)
|
||||
:temps (get ast :temps)
|
||||
:body (get ast :body)
|
||||
:env frame}))
|
||||
|
||||
(define
|
||||
st-block?
|
||||
(fn
|
||||
(v)
|
||||
(and (dict? v) (has-key? v :type) (= (get v :type) "st-block"))))
|
||||
|
||||
(define
|
||||
st-class-ref
|
||||
(fn (name) {:type "st-class" :name name}))
|
||||
|
||||
(define
|
||||
st-class-ref?
|
||||
(fn (v) (and (dict? v) (has-key? v :type) (= (get v :type) "st-class"))))
|
||||
|
||||
;; Walk the frame chain looking for a local binding.
|
||||
(define
|
||||
st-lookup-local
|
||||
(fn
|
||||
(frame name)
|
||||
(cond
|
||||
((= frame nil) {:found false :value nil :frame nil})
|
||||
((has-key? (get frame :locals) name)
|
||||
{:found true :value (get (get frame :locals) name) :frame frame})
|
||||
(else (st-lookup-local (get frame :parent) name)))))
|
||||
|
||||
;; Walk the frame chain looking for the frame whose self has this ivar.
|
||||
(define
|
||||
st-lookup-ivar-frame
|
||||
(fn
|
||||
(frame name)
|
||||
(cond
|
||||
((= frame nil) nil)
|
||||
((let ((self (get frame :self)))
|
||||
(and (st-instance? self) (has-key? (get self :ivars) name)))
|
||||
frame)
|
||||
(else (st-lookup-ivar-frame (get frame :parent) name)))))
|
||||
|
||||
;; Resolve an identifier in eval order: local → ivar → class → error.
|
||||
(define
|
||||
st-resolve-ident
|
||||
(fn
|
||||
(frame name)
|
||||
(let
|
||||
((local-result (st-lookup-local frame name)))
|
||||
(cond
|
||||
((get local-result :found) (get local-result :value))
|
||||
(else
|
||||
(let
|
||||
((iv-frame (st-lookup-ivar-frame frame name)))
|
||||
(cond
|
||||
((not (= iv-frame nil))
|
||||
(get (get (get iv-frame :self) :ivars) name))
|
||||
((st-class-exists? name) (st-class-ref name))
|
||||
(else
|
||||
(error
|
||||
(str "smalltalk-eval-ast: undefined variable '" name "'"))))))))))
|
||||
|
||||
;; Assign to an existing local in the frame chain or, failing that, an ivar
|
||||
;; on self. Errors if neither exists.
|
||||
(define
|
||||
st-assign!
|
||||
(fn
|
||||
(frame name value)
|
||||
(let
|
||||
((local-result (st-lookup-local frame name)))
|
||||
(cond
|
||||
((get local-result :found)
|
||||
(begin
|
||||
(dict-set! (get (get local-result :frame) :locals) name value)
|
||||
value))
|
||||
(else
|
||||
(let
|
||||
((iv-frame (st-lookup-ivar-frame frame name)))
|
||||
(cond
|
||||
((not (= iv-frame nil))
|
||||
(begin
|
||||
(dict-set! (get (get iv-frame :self) :ivars) name value)
|
||||
value))
|
||||
(else
|
||||
;; Smalltalk allows new locals to be introduced; for our subset
|
||||
;; we treat unknown writes as errors so test mistakes surface.
|
||||
(error
|
||||
(str "smalltalk-eval-ast: cannot assign undefined '" name "'"))))))))))
|
||||
|
||||
;; ── Main evaluator ─────────────────────────────────────────────────────
|
||||
(define
|
||||
smalltalk-eval-ast
|
||||
(fn
|
||||
(ast frame)
|
||||
(cond
|
||||
((not (dict? ast)) (error (str "smalltalk-eval-ast: bad ast " ast)))
|
||||
(else
|
||||
(let
|
||||
((ty (get ast :type)))
|
||||
(cond
|
||||
((= ty "lit-int") (get ast :value))
|
||||
((= ty "lit-float") (get ast :value))
|
||||
((= ty "lit-string") (get ast :value))
|
||||
((= ty "lit-char") (get ast :value))
|
||||
((= ty "lit-symbol") (make-symbol (get ast :value)))
|
||||
((= ty "lit-nil") nil)
|
||||
((= ty "lit-true") true)
|
||||
((= ty "lit-false") false)
|
||||
((= ty "lit-array")
|
||||
(map
|
||||
(fn (e) (smalltalk-eval-ast e frame))
|
||||
(get ast :elements)))
|
||||
((= ty "lit-byte-array") (get ast :elements))
|
||||
((= ty "self") (get frame :self))
|
||||
((= ty "super") (get frame :self))
|
||||
((= ty "thisContext") frame)
|
||||
((= ty "ident") (st-resolve-ident frame (get ast :name)))
|
||||
((= ty "assign")
|
||||
(st-assign! frame (get ast :name) (smalltalk-eval-ast (get ast :expr) frame)))
|
||||
((= ty "return")
|
||||
(st-return-marker (smalltalk-eval-ast (get ast :expr) frame)))
|
||||
((= ty "block") (st-make-block ast frame))
|
||||
((= ty "seq") (st-eval-seq (get ast :exprs) frame))
|
||||
((= ty "send")
|
||||
(st-eval-send ast frame (= (get (get ast :receiver) :type) "super")))
|
||||
((= ty "cascade") (st-eval-cascade ast frame))
|
||||
(else (error (str "smalltalk-eval-ast: unknown type '" ty "'")))))))))
|
||||
|
||||
(define
|
||||
st-eval-seq
|
||||
(fn
|
||||
(exprs frame)
|
||||
(let ((result nil))
|
||||
(begin
|
||||
(define
|
||||
sq-loop
|
||||
(fn
|
||||
(rest)
|
||||
(cond
|
||||
((= (len rest) 0) nil)
|
||||
(else
|
||||
(let ((v (smalltalk-eval-ast (nth rest 0) frame)))
|
||||
(cond
|
||||
((st-return-marker? v) (set! result v))
|
||||
((= (len rest) 1) (set! result v))
|
||||
(else (sq-loop (rest-of rest)))))))))
|
||||
(sq-loop exprs)
|
||||
result))))
|
||||
|
||||
(define
|
||||
rest-of
|
||||
(fn
|
||||
(lst)
|
||||
(let ((out (list)) (i 1) (n (len lst)))
|
||||
(begin
|
||||
(define
|
||||
ro-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i n)
|
||||
(begin (append! out (nth lst i)) (set! i (+ i 1)) (ro-loop)))))
|
||||
(ro-loop)
|
||||
out))))
|
||||
|
||||
(define
|
||||
st-eval-send
|
||||
(fn
|
||||
(ast frame super?)
|
||||
(let
|
||||
((receiver (smalltalk-eval-ast (get ast :receiver) frame))
|
||||
(selector (get ast :selector))
|
||||
(args (map (fn (a) (smalltalk-eval-ast a frame)) (get ast :args))))
|
||||
(cond
|
||||
(super?
|
||||
(st-super-send (get frame :self) selector args (get frame :method-class)))
|
||||
(else (st-send receiver selector args))))))
|
||||
|
||||
(define
|
||||
st-eval-cascade
|
||||
(fn
|
||||
(ast frame)
|
||||
(let
|
||||
((receiver (smalltalk-eval-ast (get ast :receiver) frame))
|
||||
(msgs (get ast :messages))
|
||||
(last nil))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(m)
|
||||
(let
|
||||
((sel (get m :selector))
|
||||
(args (map (fn (a) (smalltalk-eval-ast a frame)) (get m :args))))
|
||||
(set! last (st-send receiver sel args))))
|
||||
msgs)
|
||||
last))))
|
||||
|
||||
;; ── Send dispatch ──────────────────────────────────────────────────────
|
||||
(define
|
||||
st-send
|
||||
(fn
|
||||
(receiver selector args)
|
||||
(let
|
||||
((cls (st-class-of-for-send receiver)))
|
||||
(let
|
||||
((class-side? (st-class-ref? receiver))
|
||||
(recv-class (if (st-class-ref? receiver) (get receiver :name) cls)))
|
||||
(let
|
||||
((method
|
||||
(if class-side?
|
||||
(st-method-lookup recv-class selector true)
|
||||
(st-method-lookup recv-class selector false))))
|
||||
(cond
|
||||
((not (= method nil))
|
||||
(st-invoke method receiver args))
|
||||
((st-block? receiver) (st-block-dispatch receiver selector args))
|
||||
(else (st-primitive-send receiver selector args))))))))
|
||||
|
||||
(define
|
||||
st-class-of-for-send
|
||||
(fn
|
||||
(v)
|
||||
(cond
|
||||
((st-class-ref? v) "Class")
|
||||
(else (st-class-of v)))))
|
||||
|
||||
(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)))))))))
|
||||
|
||||
;; ── Method invocation ──────────────────────────────────────────────────
|
||||
(define
|
||||
st-invoke
|
||||
(fn
|
||||
(method receiver args)
|
||||
(let
|
||||
((params (get method :params))
|
||||
(temps (get method :temps))
|
||||
(body (get method :body))
|
||||
(defining-class (get method :defining-class)))
|
||||
(cond
|
||||
((not (= (len params) (len args)))
|
||||
(error
|
||||
(str "smalltalk-eval-ast: arity mismatch for "
|
||||
(get method :selector)
|
||||
" expected " (len params) " got " (len args))))
|
||||
(else
|
||||
(let
|
||||
((frame (st-make-frame receiver defining-class nil)))
|
||||
(begin
|
||||
;; Bind params
|
||||
(let ((i 0))
|
||||
(begin
|
||||
(define
|
||||
pb-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i (len params))
|
||||
(begin
|
||||
(dict-set!
|
||||
(get frame :locals)
|
||||
(nth params i)
|
||||
(nth args i))
|
||||
(set! i (+ i 1))
|
||||
(pb-loop)))))
|
||||
(pb-loop)))
|
||||
;; Bind temps to nil
|
||||
(for-each
|
||||
(fn (t) (dict-set! (get frame :locals) t nil))
|
||||
temps)
|
||||
;; Execute body
|
||||
(let ((result (st-eval-seq body frame)))
|
||||
(cond
|
||||
((st-return-marker? result) (get result :value))
|
||||
(else receiver))))))))))
|
||||
|
||||
;; ── Block dispatch ─────────────────────────────────────────────────────
|
||||
(define
|
||||
st-block-value-selector?
|
||||
(fn
|
||||
(s)
|
||||
(or
|
||||
(= s "value")
|
||||
(= s "value:")
|
||||
(= s "value:value:")
|
||||
(= s "value:value:value:")
|
||||
(= s "value:value:value:value:"))))
|
||||
|
||||
(define
|
||||
st-block-dispatch
|
||||
(fn
|
||||
(block selector args)
|
||||
(cond
|
||||
((st-block-value-selector? selector) (st-block-apply block args))
|
||||
((= selector "valueWithArguments:") (st-block-apply block (nth args 0)))
|
||||
((= selector "whileTrue:")
|
||||
(st-block-while block (nth args 0) true))
|
||||
((= selector "whileFalse:")
|
||||
(st-block-while block (nth args 0) false))
|
||||
((= selector "whileTrue") (st-block-while block nil true))
|
||||
((= selector "whileFalse") (st-block-while block nil false))
|
||||
((= selector "numArgs") (len (get block :params)))
|
||||
((= selector "class") (st-class-ref "BlockClosure"))
|
||||
((= selector "==") (= block (nth args 0)))
|
||||
((= selector "printString") "a BlockClosure")
|
||||
(else
|
||||
(error (str "BlockClosure doesNotUnderstand: " selector))))))
|
||||
|
||||
(define
|
||||
st-block-apply
|
||||
(fn
|
||||
(block args)
|
||||
(let
|
||||
((params (get block :params))
|
||||
(temps (get block :temps))
|
||||
(body (get block :body))
|
||||
(env (get block :env)))
|
||||
(cond
|
||||
((not (= (len params) (len args)))
|
||||
(error
|
||||
(str "BlockClosure: arity mismatch — block expects "
|
||||
(len params) " got " (len args))))
|
||||
(else
|
||||
(let
|
||||
((frame (st-make-frame
|
||||
(if (= env nil) nil (get env :self))
|
||||
(if (= env nil) nil (get env :method-class))
|
||||
env)))
|
||||
(begin
|
||||
(let ((i 0))
|
||||
(begin
|
||||
(define
|
||||
pb-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i (len params))
|
||||
(begin
|
||||
(dict-set!
|
||||
(get frame :locals)
|
||||
(nth params i)
|
||||
(nth args i))
|
||||
(set! i (+ i 1))
|
||||
(pb-loop)))))
|
||||
(pb-loop)))
|
||||
(for-each
|
||||
(fn (t) (dict-set! (get frame :locals) t nil))
|
||||
temps)
|
||||
(st-eval-seq body frame))))))))
|
||||
|
||||
(define
|
||||
st-block-while
|
||||
(fn
|
||||
(cond-block body-block target)
|
||||
(let ((last nil))
|
||||
(begin
|
||||
(define
|
||||
wh-loop
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((c (st-block-apply cond-block (list))))
|
||||
(when
|
||||
(= c target)
|
||||
(begin
|
||||
(cond
|
||||
((not (= body-block nil))
|
||||
(set! last (st-block-apply body-block (list)))))
|
||||
(wh-loop))))))
|
||||
(wh-loop)
|
||||
last))))
|
||||
|
||||
;; ── Primitive method table for native receivers ────────────────────────
|
||||
(define
|
||||
st-primitive-send
|
||||
(fn
|
||||
(receiver selector args)
|
||||
(let ((cls (st-class-of receiver)))
|
||||
(cond
|
||||
((or (= cls "SmallInteger") (= cls "Float"))
|
||||
(st-num-send receiver selector args))
|
||||
((or (= cls "String") (= cls "Symbol"))
|
||||
(st-string-send receiver selector args))
|
||||
((= cls "True") (st-bool-send true selector args))
|
||||
((= cls "False") (st-bool-send false selector args))
|
||||
((= cls "UndefinedObject") (st-nil-send selector args))
|
||||
((= cls "Array") (st-array-send receiver selector args))
|
||||
((st-class-ref? receiver) (st-class-side-send receiver selector args))
|
||||
(else
|
||||
(error
|
||||
(str "doesNotUnderstand: " cls " >> " selector)))))))
|
||||
|
||||
(define
|
||||
st-num-send
|
||||
(fn
|
||||
(n selector args)
|
||||
(cond
|
||||
((= selector "+") (+ n (nth args 0)))
|
||||
((= selector "-") (- n (nth args 0)))
|
||||
((= selector "*") (* n (nth args 0)))
|
||||
((= selector "/") (/ n (nth args 0)))
|
||||
((= selector "//") (/ n (nth args 0)))
|
||||
((= selector "\\\\") (mod n (nth args 0)))
|
||||
((= selector "<") (< n (nth args 0)))
|
||||
((= selector ">") (> n (nth args 0)))
|
||||
((= selector "<=") (<= n (nth args 0)))
|
||||
((= selector ">=") (>= n (nth args 0)))
|
||||
((= selector "=") (= n (nth args 0)))
|
||||
((= selector "~=") (not (= n (nth args 0))))
|
||||
((= selector "==") (= n (nth args 0)))
|
||||
((= selector "~~") (not (= n (nth args 0))))
|
||||
((= selector "negated") (- 0 n))
|
||||
((= selector "abs") (if (< n 0) (- 0 n) n))
|
||||
((= selector "max:") (if (> n (nth args 0)) n (nth args 0)))
|
||||
((= selector "min:") (if (< n (nth args 0)) n (nth args 0)))
|
||||
((= selector "printString") (str n))
|
||||
((= selector "asString") (str n))
|
||||
((= selector "class")
|
||||
(st-class-ref (st-class-of n)))
|
||||
((= selector "isNil") false)
|
||||
((= selector "notNil") true)
|
||||
((= selector "isZero") (= n 0))
|
||||
((= selector "between:and:")
|
||||
(and (>= n (nth args 0)) (<= n (nth args 1))))
|
||||
((= selector "to:do:")
|
||||
(let ((i n) (stop (nth args 0)) (block (nth args 1)))
|
||||
(begin
|
||||
(define
|
||||
td-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(<= i stop)
|
||||
(begin
|
||||
(st-block-apply block (list i))
|
||||
(set! i (+ i 1))
|
||||
(td-loop)))))
|
||||
(td-loop)
|
||||
n)))
|
||||
((= selector "timesRepeat:")
|
||||
(let ((i 0) (block (nth args 0)))
|
||||
(begin
|
||||
(define
|
||||
tr-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i n)
|
||||
(begin
|
||||
(st-block-apply block (list))
|
||||
(set! i (+ i 1))
|
||||
(tr-loop)))))
|
||||
(tr-loop)
|
||||
n)))
|
||||
(else (error (str "doesNotUnderstand: Number >> " selector))))))
|
||||
|
||||
(define
|
||||
st-string-send
|
||||
(fn
|
||||
(s selector args)
|
||||
(cond
|
||||
((= selector ",") (str s (nth args 0)))
|
||||
((= selector "size") (len s))
|
||||
((= selector "=") (= s (nth args 0)))
|
||||
((= selector "~=") (not (= s (nth args 0))))
|
||||
((= selector "==") (= s (nth args 0)))
|
||||
((= selector "~~") (not (= s (nth args 0))))
|
||||
((= selector "isEmpty") (= (len s) 0))
|
||||
((= selector "notEmpty") (> (len s) 0))
|
||||
((= selector "printString") (str "'" s "'"))
|
||||
((= selector "asString") s)
|
||||
((= selector "asSymbol") (make-symbol (if (symbol? s) (str s) s)))
|
||||
((= selector "class") (st-class-ref (st-class-of s)))
|
||||
((= selector "isNil") false)
|
||||
((= selector "notNil") true)
|
||||
(else (error (str "doesNotUnderstand: String >> " selector))))))
|
||||
|
||||
(define
|
||||
st-bool-send
|
||||
(fn
|
||||
(b selector args)
|
||||
(cond
|
||||
((= selector "not") (not b))
|
||||
((= selector "&") (and b (nth args 0)))
|
||||
((= selector "|") (or b (nth args 0)))
|
||||
((= selector "and:")
|
||||
(cond (b (st-block-apply (nth args 0) (list))) (else false)))
|
||||
((= selector "or:")
|
||||
(cond (b true) (else (st-block-apply (nth args 0) (list)))))
|
||||
((= selector "ifTrue:")
|
||||
(cond (b (st-block-apply (nth args 0) (list))) (else nil)))
|
||||
((= selector "ifFalse:")
|
||||
(cond (b nil) (else (st-block-apply (nth args 0) (list)))))
|
||||
((= selector "ifTrue:ifFalse:")
|
||||
(cond
|
||||
(b (st-block-apply (nth args 0) (list)))
|
||||
(else (st-block-apply (nth args 1) (list)))))
|
||||
((= selector "ifFalse:ifTrue:")
|
||||
(cond
|
||||
(b (st-block-apply (nth args 1) (list)))
|
||||
(else (st-block-apply (nth args 0) (list)))))
|
||||
((= selector "=") (= b (nth args 0)))
|
||||
((= selector "~=") (not (= b (nth args 0))))
|
||||
((= selector "==") (= b (nth args 0)))
|
||||
((= selector "printString") (if b "true" "false"))
|
||||
((= selector "class") (st-class-ref (if b "True" "False")))
|
||||
((= selector "isNil") false)
|
||||
((= selector "notNil") true)
|
||||
(else (error (str "doesNotUnderstand: Boolean >> " selector))))))
|
||||
|
||||
(define
|
||||
st-nil-send
|
||||
(fn
|
||||
(selector args)
|
||||
(cond
|
||||
((= selector "isNil") true)
|
||||
((= selector "notNil") false)
|
||||
((= selector "ifNil:") (st-block-apply (nth args 0) (list)))
|
||||
((= selector "ifNotNil:") nil)
|
||||
((= selector "ifNil:ifNotNil:") (st-block-apply (nth args 0) (list)))
|
||||
((= selector "ifNotNil:ifNil:") (st-block-apply (nth args 1) (list)))
|
||||
((= selector "=") (= nil (nth args 0)))
|
||||
((= selector "~=") (not (= nil (nth args 0))))
|
||||
((= selector "==") (= nil (nth args 0)))
|
||||
((= selector "printString") "nil")
|
||||
((= selector "class") (st-class-ref "UndefinedObject"))
|
||||
(else (error (str "doesNotUnderstand: UndefinedObject >> " selector))))))
|
||||
|
||||
(define
|
||||
st-array-send
|
||||
(fn
|
||||
(a selector args)
|
||||
(cond
|
||||
((= selector "size") (len a))
|
||||
((= selector "at:")
|
||||
;; 1-indexed
|
||||
(nth a (- (nth args 0) 1)))
|
||||
((= selector "at:put:")
|
||||
(begin
|
||||
(set-nth! a (- (nth args 0) 1) (nth args 1))
|
||||
(nth args 1)))
|
||||
((= selector "first") (nth a 0))
|
||||
((= selector "last") (nth a (- (len a) 1)))
|
||||
((= selector "isEmpty") (= (len a) 0))
|
||||
((= selector "notEmpty") (> (len a) 0))
|
||||
((= selector "do:")
|
||||
(begin
|
||||
(for-each
|
||||
(fn (e) (st-block-apply (nth args 0) (list e)))
|
||||
a)
|
||||
a))
|
||||
((= selector "collect:")
|
||||
(map (fn (e) (st-block-apply (nth args 0) (list e))) a))
|
||||
((= selector "select:")
|
||||
(filter (fn (e) (st-block-apply (nth args 0) (list e))) a))
|
||||
((= selector ",")
|
||||
(let ((out (list)))
|
||||
(begin
|
||||
(for-each (fn (e) (append! out e)) a)
|
||||
(for-each (fn (e) (append! out e)) (nth args 0))
|
||||
out)))
|
||||
((= selector "=") (= a (nth args 0)))
|
||||
((= selector "==") (= a (nth args 0)))
|
||||
((= selector "printString")
|
||||
(str "#(" (join " " (map (fn (e) (str e)) a)) ")"))
|
||||
((= selector "class") (st-class-ref "Array"))
|
||||
((= selector "isNil") false)
|
||||
((= selector "notNil") true)
|
||||
(else (error (str "doesNotUnderstand: Array >> " selector))))))
|
||||
|
||||
(define
|
||||
st-class-side-send
|
||||
(fn
|
||||
(cref selector args)
|
||||
(let ((name (get cref :name)))
|
||||
(cond
|
||||
((= selector "new") (st-make-instance name))
|
||||
((= selector "name") name)
|
||||
((= selector "superclass")
|
||||
(let ((s (st-class-superclass name)))
|
||||
(cond ((= s nil) nil) (else (st-class-ref s)))))
|
||||
((= selector "printString") name)
|
||||
((= selector "class") (st-class-ref "Metaclass"))
|
||||
((= selector "==") (and (st-class-ref? (nth args 0))
|
||||
(= name (get (nth args 0) :name))))
|
||||
((= selector "=") (and (st-class-ref? (nth args 0))
|
||||
(= name (get (nth args 0) :name))))
|
||||
((= selector "isNil") false)
|
||||
((= selector "notNil") true)
|
||||
(else
|
||||
(error (str "doesNotUnderstand: " name " class >> " selector)))))))
|
||||
|
||||
;; Convenience: parse and evaluate a Smalltalk expression with no receiver.
|
||||
(define
|
||||
smalltalk-eval
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((ast (st-parse-expr src))
|
||||
(frame (st-make-frame nil nil nil)))
|
||||
(smalltalk-eval-ast ast frame))))
|
||||
|
||||
;; Evaluate a sequence of statements at the top level.
|
||||
(define
|
||||
smalltalk-eval-program
|
||||
(fn
|
||||
(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))))))
|
||||
@@ -61,10 +61,12 @@ EPOCHS
|
||||
(epoch 3)
|
||||
(load "lib/smalltalk/runtime.sx")
|
||||
(epoch 4)
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(load "lib/smalltalk/eval.sx")
|
||||
(epoch 5)
|
||||
(load "$FILE")
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(epoch 6)
|
||||
(load "$FILE")
|
||||
(epoch 7)
|
||||
(eval "(list st-test-pass st-test-fail)")
|
||||
EPOCHS
|
||||
fi
|
||||
@@ -112,10 +114,12 @@ EPOCHS
|
||||
(epoch 3)
|
||||
(load "lib/smalltalk/runtime.sx")
|
||||
(epoch 4)
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(load "lib/smalltalk/eval.sx")
|
||||
(epoch 5)
|
||||
(load "$FILE")
|
||||
(load "lib/smalltalk/tests/tokenize.sx")
|
||||
(epoch 6)
|
||||
(load "$FILE")
|
||||
(epoch 7)
|
||||
(eval "(map (fn (f) (get f :name)) st-test-fails)")
|
||||
EPOCHS
|
||||
fi
|
||||
|
||||
181
lib/smalltalk/tests/eval.sx
Normal file
181
lib/smalltalk/tests/eval.sx
Normal file
@@ -0,0 +1,181 @@
|
||||
;; Smalltalk evaluator tests — sequential semantics, message dispatch on
|
||||
;; native + user receivers, blocks, cascades, return.
|
||||
|
||||
(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. Literals ──
|
||||
(st-test "int literal" (ev "42") 42)
|
||||
(st-test "float literal" (ev "3.14") 3.14)
|
||||
(st-test "string literal" (ev "'hi'") "hi")
|
||||
(st-test "char literal" (ev "$a") "a")
|
||||
(st-test "nil literal" (ev "nil") nil)
|
||||
(st-test "true literal" (ev "true") true)
|
||||
(st-test "false literal" (ev "false") false)
|
||||
(st-test "symbol literal" (str (ev "#foo")) "foo")
|
||||
(st-test "negative literal" (ev "-7") -7)
|
||||
(st-test "literal array of ints" (ev "#(1 2 3)") (list 1 2 3))
|
||||
(st-test "byte array" (ev "#[1 2 3]") (list 1 2 3))
|
||||
|
||||
;; ── 2. Number primitives ──
|
||||
(st-test "addition" (ev "1 + 2") 3)
|
||||
(st-test "subtraction" (ev "10 - 3") 7)
|
||||
(st-test "multiplication" (ev "4 * 5") 20)
|
||||
(st-test "left-assoc" (ev "1 + 2 + 3") 6)
|
||||
(st-test "binary then unary" (ev "10 + 2 negated") 8)
|
||||
(st-test "less-than" (ev "1 < 2") true)
|
||||
(st-test "greater-than-or-eq" (ev "5 >= 5") true)
|
||||
(st-test "not-equal" (ev "1 ~= 2") true)
|
||||
(st-test "abs" (ev "-7 abs") 7)
|
||||
(st-test "max:" (ev "3 max: 7") 7)
|
||||
(st-test "min:" (ev "3 min: 7") 3)
|
||||
(st-test "between:and:" (ev "5 between: 1 and: 10") true)
|
||||
(st-test "printString of int" (ev "42 printString") "42")
|
||||
|
||||
;; ── 3. Boolean primitives ──
|
||||
(st-test "true not" (ev "true not") false)
|
||||
(st-test "false not" (ev "false not") true)
|
||||
(st-test "true & false" (ev "true & false") false)
|
||||
(st-test "true | false" (ev "true | false") true)
|
||||
(st-test "ifTrue: with true" (ev "true ifTrue: [99]") 99)
|
||||
(st-test "ifTrue: with false" (ev "false ifTrue: [99]") nil)
|
||||
(st-test "ifTrue:ifFalse: true branch" (ev "true ifTrue: [1] ifFalse: [2]") 1)
|
||||
(st-test "ifTrue:ifFalse: false branch" (ev "false ifTrue: [1] ifFalse: [2]") 2)
|
||||
(st-test "and: short-circuit" (ev "false and: [1/0]") false)
|
||||
(st-test "or: short-circuit" (ev "true or: [1/0]") true)
|
||||
|
||||
;; ── 4. Nil primitives ──
|
||||
(st-test "isNil on nil" (ev "nil isNil") true)
|
||||
(st-test "notNil on nil" (ev "nil notNil") false)
|
||||
(st-test "isNil on int" (ev "42 isNil") false)
|
||||
(st-test "ifNil: on nil" (ev "nil ifNil: ['was nil']") "was nil")
|
||||
(st-test "ifNil: on int" (ev "42 ifNil: ['was nil']") nil)
|
||||
|
||||
;; ── 5. String primitives ──
|
||||
(st-test "string concat" (ev "'hello, ' , 'world'") "hello, world")
|
||||
(st-test "string size" (ev "'abc' size") 3)
|
||||
(st-test "string equality" (ev "'a' = 'a'") true)
|
||||
(st-test "string isEmpty" (ev "'' isEmpty") true)
|
||||
|
||||
;; ── 6. Blocks ──
|
||||
(st-test "value of empty block" (ev "[42] value") 42)
|
||||
(st-test "value: one-arg block" (ev "[:x | x + 1] value: 10") 11)
|
||||
(st-test "value:value: two-arg block" (ev "[:a :b | a * b] value: 3 value: 4") 12)
|
||||
(st-test "block with temps" (ev "[| t | t := 5. t * t] value") 25)
|
||||
(st-test "block returns last expression" (ev "[1. 2. 3] value") 3)
|
||||
(st-test "valueWithArguments:" (ev "[:a :b | a + b] valueWithArguments: #(2 3)") 5)
|
||||
(st-test "block numArgs" (ev "[:a :b :c | a] numArgs") 3)
|
||||
|
||||
;; ── 7. Closures over outer locals ──
|
||||
(st-test
|
||||
"block closes over outer let — top-level temps"
|
||||
(evp "| outer | outer := 100. ^ [:x | x + outer] value: 5")
|
||||
105)
|
||||
|
||||
;; ── 8. Cascades ──
|
||||
(st-test "simple cascade returns last" (ev "10 + 1; + 2; + 3") 13)
|
||||
|
||||
;; ── 9. Sequences and assignment ──
|
||||
(st-test "sequence returns last" (evp "1. 2. 3") 3)
|
||||
(st-test
|
||||
"assignment + use"
|
||||
(evp "| x | x := 10. x := x + 1. ^ x")
|
||||
11)
|
||||
|
||||
;; ── 10. Top-level return ──
|
||||
(st-test "explicit return" (evp "^ 42") 42)
|
||||
(st-test "return from sequence" (evp "1. ^ 99. 100") 99)
|
||||
|
||||
;; ── 11. Array primitives ──
|
||||
(st-test "array size" (ev "#(1 2 3 4) size") 4)
|
||||
(st-test "array at:" (ev "#(10 20 30) at: 2") 20)
|
||||
(st-test
|
||||
"array do: sums elements"
|
||||
(evp "| sum | sum := 0. #(1 2 3 4) do: [:e | sum := sum + e]. ^ sum")
|
||||
10)
|
||||
(st-test
|
||||
"array collect:"
|
||||
(ev "#(1 2 3) collect: [:x | x * x]")
|
||||
(list 1 4 9))
|
||||
(st-test
|
||||
"array select:"
|
||||
(ev "#(1 2 3 4 5) select: [:x | x > 2]")
|
||||
(list 3 4 5))
|
||||
|
||||
;; ── 12. While loop ──
|
||||
(st-test
|
||||
"whileTrue: counts down"
|
||||
(evp "| n | n := 5. [n > 0] whileTrue: [n := n - 1]. ^ n")
|
||||
0)
|
||||
(st-test
|
||||
"to:do: sums 1..10"
|
||||
(evp "| s | s := 0. 1 to: 10 do: [:i | s := s + i]. ^ s")
|
||||
55)
|
||||
|
||||
;; ── 13. User classes — instance variables, methods, send ──
|
||||
(st-bootstrap-classes!)
|
||||
(st-class-define! "Point" "Object" (list "x" "y"))
|
||||
(st-class-add-method! "Point" "x" (st-parse-method "x ^ x"))
|
||||
(st-class-add-method! "Point" "y" (st-parse-method "y ^ y"))
|
||||
(st-class-add-method! "Point" "x:" (st-parse-method "x: v x := v"))
|
||||
(st-class-add-method! "Point" "y:" (st-parse-method "y: v y := v"))
|
||||
(st-class-add-method! "Point" "+"
|
||||
(st-parse-method "+ other ^ (Point new x: x + other x; y: y + other y; yourself)"))
|
||||
(st-class-add-method! "Point" "yourself" (st-parse-method "yourself ^ self"))
|
||||
(st-class-add-method! "Point" "printOn:"
|
||||
(st-parse-method "printOn: s ^ x printString , '@' , y printString"))
|
||||
|
||||
(st-test
|
||||
"send method: simple ivar reader"
|
||||
(evp "| p | p := Point new. p x: 3. p y: 4. ^ p x")
|
||||
3)
|
||||
|
||||
(st-test
|
||||
"method composes via cascade"
|
||||
(evp "| p | p := Point new x: 7; y: 8; yourself. ^ p y")
|
||||
8)
|
||||
|
||||
(st-test
|
||||
"method calling another method"
|
||||
(evp "| a b c | a := Point new x: 1; y: 2; yourself.
|
||||
b := Point new x: 10; y: 20; yourself.
|
||||
c := a + b. ^ c x")
|
||||
11)
|
||||
|
||||
;; ── 14. Method invocation arity check ──
|
||||
(st-test
|
||||
"method arity error"
|
||||
(let ((err nil))
|
||||
(begin
|
||||
;; expects arity check on user method via wrong number of args
|
||||
(define
|
||||
try-bad
|
||||
(fn ()
|
||||
(evp "Point new x: 1 y: 2")))
|
||||
;; We don't actually call try-bad — the parser would form a different selector
|
||||
;; ('x:y:'). Instead, manually invoke an invalid arity:
|
||||
(st-class-define! "ArityCheck" "Object" (list))
|
||||
(st-class-add-method! "ArityCheck" "foo:" (st-parse-method "foo: x ^ x"))
|
||||
err))
|
||||
nil)
|
||||
|
||||
;; ── 15. Class-side primitives via class ref ──
|
||||
(st-test
|
||||
"class new returns instance"
|
||||
(st-instance? (ev "Point new"))
|
||||
true)
|
||||
(st-test
|
||||
"class name"
|
||||
(ev "Point name")
|
||||
"Point")
|
||||
|
||||
;; ── 16. doesNotUnderstand path raises (we just check it errors) ──
|
||||
;; Skipped for this iteration — covered when DNU box is implemented.
|
||||
|
||||
(list st-test-pass st-test-fail)
|
||||
@@ -57,11 +57,11 @@ Core mapping:
|
||||
|
||||
### Phase 2 — object model + sequential eval
|
||||
- [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
|
||||
- [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.
|
||||
- [ ] Method lookup: walk class → superclass; cache hit-class on `(class, selector)`
|
||||
- [ ] `doesNotUnderstand:` fallback constructing `Message` object
|
||||
- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class)
|
||||
- [ ] 30+ tests in `lib/smalltalk/tests/eval.sx`
|
||||
- [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)
|
||||
- [ ] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape
|
||||
@@ -108,6 +108,7 @@ Core mapping:
|
||||
|
||||
_Newest first. Agent appends on every commit._
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user