Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s
lib/guest/reflective/env.sx — added refl-env-find-frame-with (returns
the scope where NAME is bound, or nil). Needed by consumers like
Smalltalk that mutate variables at the source frame rather than
shadowing at the current one. Also added refl-env-find-frame for
the canonical shape.
lib/smalltalk/eval.sx — new st-frame-cfg adapter for the kit.
st-lookup-local now delegates parent-walk to refl-env-find-frame-with
while preserving its Smalltalk-flavoured {:found :value :frame}
return shape (which is used to mutate at the binding's source
frame, not the current one).
lib/smalltalk/test.sh + compare.sh — load lib/guest/reflective/env.sx
before lib/smalltalk/eval.sx.
Three genuinely different wire shapes now share the parent-walk:
- Kernel: {:refl-tag :env :bindings :parent} mutable bindings
- Tcl: {:level :locals :parent} functional update
- Smalltalk: {:self :method-class :locals :parent mutable bindings,
:return-k :active-cell} rich metadata
All three consumers' full test suites unchanged: Smalltalk 847/847,
Kernel 322/322, Tcl 427/427. The cfg adapter pattern (modelled after
lib/guest/match.sx) cleanly handles all three.
1478 lines
54 KiB
Plaintext
1478 lines
54 KiB
Plaintext
;; 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 captured continuations:
|
|
;; each method invocation wraps its body in `call/cc`, the captured k is
|
|
;; stored on the frame as `:return-k`, and `^expr` invokes that k. Blocks
|
|
;; capture their creating method's k so `^` from inside a block returns
|
|
;; from the *creating* method, not the invoking one — this is Smalltalk's
|
|
;; non-local return, the headline of Phase 3.
|
|
;;
|
|
;; 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)
|
|
;; :return-k K} ; the ^k that ^expr should invoke
|
|
|
|
(define
|
|
st-make-frame
|
|
(fn
|
|
(self method-class parent return-k active-cell)
|
|
{:self self
|
|
:method-class method-class
|
|
:locals {}
|
|
:parent parent
|
|
:return-k return-k
|
|
;; A small mutable dict shared between the method-frame and any
|
|
;; block created in its scope. While the method is on the stack
|
|
;; :active is true; once st-invoke finishes (normally or via the
|
|
;; captured ^k) it flips to false. ^expr from a block whose
|
|
;; active-cell is dead raises cannotReturn:.
|
|
:active-cell active-cell}))
|
|
|
|
(define
|
|
st-make-block
|
|
(fn
|
|
(ast frame)
|
|
{:type "st-block"
|
|
:params (get ast :params)
|
|
:temps (get ast :temps)
|
|
:body (get ast :body)
|
|
:env frame
|
|
;; capture the creating method's return continuation so that `^expr`
|
|
;; from inside this block always returns from that method
|
|
:return-k (if (= frame nil) nil (get frame :return-k))
|
|
;; Pair the captured ^k with the active-cell — invoking ^k after
|
|
;; the originating method has returned must raise cannotReturn:.
|
|
:active-cell (if (= frame nil) nil (get frame :active-cell))}))
|
|
|
|
(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"))))
|
|
|
|
;; Smalltalk-side adapter for lib/guest/reflective/env.sx. The
|
|
;; Smalltalk frame carries language-specific metadata (:self,
|
|
;; :method-class, :return-k, :active-cell) but the parent-walk for
|
|
;; local-binding lookup is the same algorithm Kernel and Tcl use.
|
|
;; Third consumer of the env kit; cfg routes through :locals and
|
|
;; :parent and uses mutable dict-set! for binding.
|
|
(define st-frame-cfg
|
|
{:bindings-of (fn (f) (get f :locals))
|
|
:parent-of (fn (f) (get f :parent))
|
|
:extend (fn (f) (st-make-frame nil nil f nil nil))
|
|
:bind! (fn (f n v)
|
|
(dict-set! (get f :locals) n v) f)
|
|
:env? (fn (v) (and (dict? v) (dict? (get v :locals))))})
|
|
|
|
;; Walk the frame chain looking for a local binding. Returns the
|
|
;; Smalltalk-flavoured {:found :value :frame} shape callers expect;
|
|
;; the parent-walk delegates to refl-env-find-frame-with.
|
|
(define
|
|
st-lookup-local
|
|
(fn
|
|
(frame name)
|
|
(let ((src (refl-env-find-frame-with st-frame-cfg frame name)))
|
|
(cond
|
|
((nil? src) {:found false :value nil :frame nil})
|
|
(:else
|
|
{:found true
|
|
:value (get (get src :locals) name)
|
|
:frame src})))))
|
|
|
|
;; 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 returns an immutable list — Smalltalk arrays must be
|
|
;; mutable so that `at:put:` works. Build via append! so each
|
|
;; literal yields a fresh mutable list.
|
|
(let ((out (list)))
|
|
(begin
|
|
(for-each
|
|
(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))
|
|
((= 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")
|
|
(let ((v (smalltalk-eval-ast (get ast :expr) frame)))
|
|
(let
|
|
((k (get frame :return-k))
|
|
(cell (get frame :active-cell)))
|
|
(cond
|
|
((= k nil)
|
|
(error "smalltalk-eval-ast: return outside method context"))
|
|
((and (not (= cell nil))
|
|
(not (get cell :active)))
|
|
(error
|
|
(str
|
|
"BlockContext>>cannotReturn: — ^expr after the "
|
|
"creating method has already returned (value was "
|
|
v ")")))
|
|
(else (k v))))))
|
|
((= 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 "'")))))))))
|
|
|
|
;; Evaluate a sequence; return the last expression's value. `^expr`
|
|
;; mid-sequence transfers control via the frame's :return-k and never
|
|
;; returns to this loop, so we don't need any return-marker plumbing.
|
|
(define
|
|
st-eval-seq
|
|
(fn
|
|
(exprs frame)
|
|
(let ((result nil))
|
|
(begin
|
|
(for-each
|
|
(fn (e) (set! result (smalltalk-eval-ast e frame)))
|
|
exprs)
|
|
result))))
|
|
|
|
;; Per-call-site monomorphic inline cache: each `send` AST node stores
|
|
;; the receiver class and method record from the last dispatch. When the
|
|
;; next dispatch sees the same class AND the runtime's IC generation
|
|
;; hasn't changed, we skip the global method-lookup. Mutations to the
|
|
;; class table bump `st-ic-generation` (defined in runtime.sx) so stale
|
|
;; method records can't fire.
|
|
(define st-ic-hits 0)
|
|
(define st-ic-misses 0)
|
|
|
|
(define
|
|
st-ic-reset-stats!
|
|
(fn () (begin (set! st-ic-hits 0) (set! st-ic-misses 0))))
|
|
|
|
(define
|
|
st-ic-stats
|
|
(fn () {:hits st-ic-hits :misses st-ic-misses :gen st-ic-generation}))
|
|
|
|
;; Counter for intrinsified block sends — incremented when a known
|
|
;; control-flow idiom fires inline instead of going through dispatch.
|
|
(define st-intrinsic-hits 0)
|
|
(define
|
|
st-intrinsic-stats
|
|
(fn () {:hits st-intrinsic-hits}))
|
|
(define
|
|
st-intrinsic-reset!
|
|
(fn () (set! st-intrinsic-hits 0)))
|
|
|
|
(define
|
|
st-simple-block-ast?
|
|
(fn
|
|
(a)
|
|
(and (dict? a)
|
|
(= (get a :type) "block")
|
|
(= (len (get a :params)) 0)
|
|
(= (len (get a :temps)) 0))))
|
|
|
|
;; AST-level recognition of control-flow idioms. When the call site looks
|
|
;; like `cond ifTrue: [body]`, `cond ifTrue:ifFalse:`, or
|
|
;; `[cond] whileTrue: [body]` and the block arguments are simple
|
|
;; (no params, no temps), short-circuit the entire dispatch chain and
|
|
;; evaluate the bodies inline in the current frame. ^expr inside an
|
|
;; inlined body still escapes correctly because the frame's :return-k
|
|
;; is unchanged.
|
|
(define
|
|
st-try-intrinsify
|
|
(fn
|
|
(ast frame)
|
|
(let
|
|
((selector (get ast :selector))
|
|
(args-ast (get ast :args)))
|
|
(cond
|
|
((and (= selector "ifTrue:")
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c true) (st-eval-seq (get (nth args-ast 0) :body) frame))
|
|
(else nil)))))
|
|
((and (= selector "ifFalse:")
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c false) (st-eval-seq (get (nth args-ast 0) :body) frame))
|
|
(else nil)))))
|
|
((and (= selector "ifTrue:ifFalse:")
|
|
(= (len args-ast) 2)
|
|
(st-simple-block-ast? (nth args-ast 0))
|
|
(st-simple-block-ast? (nth args-ast 1)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c true) (st-eval-seq (get (nth args-ast 0) :body) frame))
|
|
(else (st-eval-seq (get (nth args-ast 1) :body) frame))))))
|
|
((and (= selector "ifFalse:ifTrue:")
|
|
(= (len args-ast) 2)
|
|
(st-simple-block-ast? (nth args-ast 0))
|
|
(st-simple-block-ast? (nth args-ast 1)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c true) (st-eval-seq (get (nth args-ast 1) :body) frame))
|
|
(else (st-eval-seq (get (nth args-ast 0) :body) frame))))))
|
|
((and (= selector "and:")
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c true) (st-eval-seq (get (nth args-ast 0) :body) frame))
|
|
(else false)))))
|
|
((and (= selector "or:")
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let ((c (smalltalk-eval-ast (get ast :receiver) frame)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(cond
|
|
((= c true) true)
|
|
(else (st-eval-seq (get (nth args-ast 0) :body) frame))))))
|
|
((and (= selector "whileTrue:")
|
|
(st-simple-block-ast? (get ast :receiver))
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let
|
|
((cond-body (get (get ast :receiver) :body))
|
|
(body-body (get (nth args-ast 0) :body)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(define
|
|
wt-loop
|
|
(fn
|
|
()
|
|
(let
|
|
((c (st-eval-seq cond-body frame)))
|
|
(when
|
|
(= c true)
|
|
(begin (st-eval-seq body-body frame) (wt-loop))))))
|
|
(wt-loop)
|
|
nil)))
|
|
((and (= selector "whileFalse:")
|
|
(st-simple-block-ast? (get ast :receiver))
|
|
(= (len args-ast) 1)
|
|
(st-simple-block-ast? (nth args-ast 0)))
|
|
(let
|
|
((cond-body (get (get ast :receiver) :body))
|
|
(body-body (get (nth args-ast 0) :body)))
|
|
(begin
|
|
(set! st-intrinsic-hits (+ st-intrinsic-hits 1))
|
|
(define
|
|
wf-loop
|
|
(fn
|
|
()
|
|
(let
|
|
((c (st-eval-seq cond-body frame)))
|
|
(when
|
|
(= c false)
|
|
(begin (st-eval-seq body-body frame) (wf-loop))))))
|
|
(wf-loop)
|
|
nil)))
|
|
(else :no-intrinsic)))))
|
|
|
|
(define
|
|
st-eval-send
|
|
(fn
|
|
(ast frame super?)
|
|
(cond
|
|
(super?
|
|
(let
|
|
((selector (get ast :selector))
|
|
(args (map (fn (a) (smalltalk-eval-ast a frame)) (get ast :args))))
|
|
(st-super-send (get frame :self) selector args (get frame :method-class))))
|
|
(else
|
|
(let ((intrinsified (st-try-intrinsify ast frame)))
|
|
(cond
|
|
((not (= intrinsified :no-intrinsic)) intrinsified)
|
|
(else (st-eval-send-dispatch ast frame))))))))
|
|
|
|
(define
|
|
st-eval-send-dispatch
|
|
(fn
|
|
(ast frame)
|
|
(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))))
|
|
(let ((cls (st-class-of-for-send receiver)))
|
|
(cond
|
|
;; Inline-cache hit: same receiver class, same generation.
|
|
((and (has-key? ast :ic-class)
|
|
(= (get ast :ic-class) cls)
|
|
(has-key? ast :ic-gen)
|
|
(= (get ast :ic-gen) st-ic-generation)
|
|
(has-key? ast :ic-method))
|
|
(begin
|
|
(set! st-ic-hits (+ st-ic-hits 1))
|
|
(st-invoke (get ast :ic-method) receiver args)))
|
|
(else
|
|
(begin
|
|
(set! st-ic-misses (+ st-ic-misses 1))
|
|
(let
|
|
((class-side? (st-class-ref? receiver))
|
|
(recv-class (if (st-class-ref? receiver)
|
|
(get receiver :name)
|
|
cls)))
|
|
(let ((method (st-method-lookup recv-class selector class-side?)))
|
|
(cond
|
|
((not (= method nil))
|
|
(begin
|
|
(dict-set! ast :ic-class cls)
|
|
(dict-set! ast :ic-method method)
|
|
(dict-set! ast :ic-gen st-ic-generation)
|
|
(st-invoke method receiver args)))
|
|
(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)
|
|
(let ((bd (st-block-dispatch receiver selector args)))
|
|
(cond
|
|
((= bd :unhandled) (st-dnu receiver selector args))
|
|
(else bd))))
|
|
(else
|
|
(let ((primitive-result (st-primitive-send receiver selector args)))
|
|
(cond
|
|
((= primitive-result :unhandled)
|
|
(st-dnu receiver selector args))
|
|
(else primitive-result))))))))))
|
|
|
|
;; Construct a Message object for doesNotUnderstand:.
|
|
(define
|
|
st-make-message
|
|
(fn
|
|
(selector args)
|
|
(let ((msg (st-make-instance "Message")))
|
|
(begin
|
|
(dict-set! (get msg :ivars) "selector" (make-symbol selector))
|
|
(dict-set! (get msg :ivars) "arguments" args)
|
|
msg))))
|
|
|
|
;; Trigger doesNotUnderstand:. If the receiver's class chain defines an
|
|
;; override, invoke it with a freshly-built Message; otherwise raise.
|
|
(define
|
|
st-dnu
|
|
(fn
|
|
(receiver selector args)
|
|
(let
|
|
((cls (st-class-of-for-send receiver))
|
|
(class-side? (st-class-ref? receiver)))
|
|
(let
|
|
((recv-class (if class-side? (get receiver :name) cls)))
|
|
(let
|
|
((method (st-method-lookup recv-class "doesNotUnderstand:" class-side?)))
|
|
(cond
|
|
((not (= method nil))
|
|
(let ((msg (st-make-message selector args)))
|
|
(st-invoke method receiver (list msg))))
|
|
(else
|
|
(error
|
|
(str "doesNotUnderstand: " recv-class " >> " selector)))))))))
|
|
|
|
(define
|
|
st-class-of-for-send
|
|
(fn
|
|
(v)
|
|
(cond
|
|
((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)
|
|
(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 ──────────────────────────────────────────────────
|
|
;;
|
|
;; Method body is wrapped in (call/cc (fn (k) ...)). The k is bound on the
|
|
;; method's frame as :return-k. `^expr` invokes k, which abandons the body
|
|
;; and resumes call/cc with v. Blocks that escape with `^` capture the
|
|
;; *creating* method's k, so non-local return reaches back through any
|
|
;; number of nested block.value calls.
|
|
(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 ((cell {:active true}))
|
|
(let
|
|
((result
|
|
(call/cc
|
|
(fn (k)
|
|
(let ((frame (st-make-frame receiver defining-class nil k cell)))
|
|
(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. If body finishes without ^, the implicit
|
|
;; return value in Smalltalk is `self` — match that.
|
|
(st-eval-seq body frame)
|
|
receiver))))))
|
|
(begin
|
|
;; Method invocation is finished — flip the cell so any block
|
|
;; that captured this method's ^k can no longer return.
|
|
(dict-set! cell :active false)
|
|
result))))))))
|
|
|
|
;; ── 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")
|
|
;; Smalltalk exception machinery on top of SX guard/raise.
|
|
((= selector "on:do:")
|
|
(st-block-on-do block (nth args 0) (nth args 1)))
|
|
((= selector "ensure:")
|
|
(st-block-ensure block (nth args 0)))
|
|
((= selector "ifCurtailed:")
|
|
(st-block-if-curtailed block (nth args 0)))
|
|
(else :unhandled))))
|
|
|
|
;; on: ExceptionClass do: aHandler — run the receiver block, catching
|
|
;; raised st-instances whose class isKindOf: the given Exception class.
|
|
;; Other raises propagate. The handler receives the caught exception.
|
|
(define
|
|
st-block-on-do
|
|
(fn
|
|
(block exc-class-ref handler)
|
|
(let
|
|
((target-name
|
|
(cond
|
|
((st-class-ref? exc-class-ref) (get exc-class-ref :name))
|
|
(else "Exception"))))
|
|
(guard
|
|
(caught
|
|
((and (st-instance? caught)
|
|
(st-class-inherits-from? (get caught :class) target-name))
|
|
(st-block-apply handler (list caught))))
|
|
(st-block-apply block (list))))))
|
|
|
|
;; ensure: cleanup — run the receiver block, then run cleanup whether the
|
|
;; receiver completed normally or raised. On raise, cleanup runs and the
|
|
;; exception propagates. The side-effect predicate pattern lets cleanup
|
|
;; run inside the guard clause without us needing to call (raise c)
|
|
;; explicitly (which has issues in nested handlers).
|
|
(define
|
|
st-block-ensure
|
|
(fn
|
|
(block cleanup)
|
|
(let ((result nil) (raised false))
|
|
(begin
|
|
(guard
|
|
(caught
|
|
((begin
|
|
(set! raised true)
|
|
(st-block-apply cleanup (list))
|
|
false)
|
|
nil))
|
|
(set! result (st-block-apply block (list))))
|
|
(when (not raised) (st-block-apply cleanup (list)))
|
|
result))))
|
|
|
|
;; ifCurtailed: cleanup — run cleanup ONLY if the receiver block raises.
|
|
(define
|
|
st-block-if-curtailed
|
|
(fn
|
|
(block cleanup)
|
|
(guard
|
|
(caught
|
|
((begin (st-block-apply cleanup (list)) false) nil))
|
|
(st-block-apply block (list)))))
|
|
|
|
(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
|
|
;; Use the block's captured ^k so `^expr` returns from
|
|
;; the *creating* method, not whoever invoked the block.
|
|
(get block :return-k)
|
|
;; Same active-cell as the creating method's frame; if
|
|
;; the method has returned, ^expr through this frame
|
|
;; raises cannotReturn:.
|
|
(get block :active-cell))))
|
|
(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))))))))
|
|
|
|
;; whileTrue: / whileTrue / whileFalse: / whileFalse — the receiver is the
|
|
;; condition block; the optional argument is the body block. Per ANSI / Pharo
|
|
;; convention, the loop returns nil regardless of how many iterations ran.
|
|
(define
|
|
st-block-while
|
|
(fn
|
|
(cond-block body-block target)
|
|
(begin
|
|
(define
|
|
wh-loop
|
|
(fn
|
|
()
|
|
(let
|
|
((c (st-block-apply cond-block (list))))
|
|
(when
|
|
(= c target)
|
|
(begin
|
|
(cond
|
|
((not (= body-block nil))
|
|
(st-block-apply body-block (list))))
|
|
(wh-loop))))))
|
|
(wh-loop)
|
|
nil)))
|
|
|
|
;; ── Primitive method table for native receivers ────────────────────────
|
|
;; Returns the result, or the sentinel :unhandled if no primitive matches —
|
|
;; in which case st-send falls back to doesNotUnderstand:.
|
|
(define
|
|
st-primitive-send
|
|
(fn
|
|
(receiver selector args)
|
|
(let ((cls (st-class-of receiver)))
|
|
;; Universal Object messages — work on any receiver type.
|
|
(cond
|
|
((= selector "class")
|
|
(cond
|
|
((st-class-ref? receiver) (st-class-ref "Metaclass"))
|
|
(else (st-class-ref cls))))
|
|
;; perform: / perform:with: / perform:withArguments:
|
|
((= selector "perform:")
|
|
(st-send receiver (str (nth args 0)) (list)))
|
|
((= selector "perform:withArguments:")
|
|
(st-send receiver (str (nth args 0)) (nth args 1)))
|
|
((or (= selector "perform:with:")
|
|
(= selector "perform:with:with:")
|
|
(= selector "perform:with:with:with:")
|
|
(= selector "perform:with:with:with:with:"))
|
|
(st-send receiver (str (nth args 0)) (slice args 1 (len args))))
|
|
;; respondsTo: aSymbol — searches user method dicts only. Native
|
|
;; primitive selectors aren't enumerated, so e.g. `42 respondsTo:
|
|
;; #+` returns false. (The send still works because dispatch falls
|
|
;; through to st-num-send.) Documented limitation.
|
|
((= selector "respondsTo:")
|
|
(let
|
|
((sel-str (str (nth args 0)))
|
|
(target-cls (if (st-class-ref? receiver) (get receiver :name) cls))
|
|
(class-side? (st-class-ref? receiver)))
|
|
(not (= (st-method-lookup target-cls sel-str class-side?) nil))))
|
|
;; isKindOf: aClass — true iff the receiver's class chain reaches it.
|
|
((= selector "isKindOf:")
|
|
(let
|
|
((arg (nth args 0))
|
|
(target-cls (if (st-class-ref? receiver) "Metaclass" cls)))
|
|
(cond
|
|
((not (st-class-ref? arg)) false)
|
|
(else (st-class-inherits-from? target-cls (get arg :name))))))
|
|
;; Universal printOn: — send `printString` (so user overrides win)
|
|
;; and write the result to the stream argument. Coerce the
|
|
;; printString result via SX `str` so it's an iterable String —
|
|
;; if a user method returns a Symbol, the stream's nextPutAll:
|
|
;; (which loops with `do:`) needs a String to walk character by
|
|
;; character.
|
|
((= selector "printOn:")
|
|
(let
|
|
((stream (nth args 0))
|
|
(s (str (st-send receiver "printString" (list)))))
|
|
(begin
|
|
(st-send stream "nextPutAll:" (list s))
|
|
receiver)))
|
|
;; Universal printString fallback for receivers no primitive table
|
|
;; handles (notably user st-instances). Native types implement
|
|
;; their own printString in the primitive senders below.
|
|
((and (= selector "printString")
|
|
(or (st-instance? receiver) (st-class-ref? receiver)))
|
|
(st-printable-string receiver))
|
|
;; isMemberOf: aClass — exact class match.
|
|
((= selector "isMemberOf:")
|
|
(let
|
|
((arg (nth args 0))
|
|
(target-cls (if (st-class-ref? receiver) "Metaclass" cls)))
|
|
(cond
|
|
((not (st-class-ref? arg)) false)
|
|
(else (= target-cls (get arg :name))))))
|
|
;; Smalltalk Exception system — `signal` raises the receiver via
|
|
;; SX raise. The argument to signal: sets messageText.
|
|
;; on:do: / ensure: / ifCurtailed: are implemented on BlockClosure
|
|
;; in `st-block-dispatch`.
|
|
((and (= selector "signal")
|
|
(st-instance? receiver)
|
|
(st-class-inherits-from? cls "Exception"))
|
|
(raise receiver))
|
|
((and (= selector "signal:")
|
|
(st-instance? receiver)
|
|
(st-class-inherits-from? cls "Exception"))
|
|
(begin
|
|
(dict-set! (get receiver :ivars) "messageText" (nth args 0))
|
|
(raise receiver)))
|
|
((and (= selector "signal")
|
|
(st-class-ref? receiver)
|
|
(st-class-inherits-from? (get receiver :name) "Exception"))
|
|
(raise (st-make-instance (get receiver :name))))
|
|
((and (= selector "signal:")
|
|
(st-class-ref? receiver)
|
|
(st-class-inherits-from? (get receiver :name) "Exception"))
|
|
(let ((inst (st-make-instance (get receiver :name))))
|
|
(begin
|
|
(dict-set! (get inst :ivars) "messageText" (nth args 0))
|
|
(raise inst))))
|
|
;; Object>>becomeForward: aReceiver — one-way become. The receiver's
|
|
;; class and ivars are mutated in place to match the target. Every
|
|
;; existing reference to the receiver dict sees the new identity.
|
|
;; Note: receiver and target remain distinct dicts (not == in the
|
|
;; SX-identity sense), but receiver behaves as though it were the
|
|
;; target — which is the practical Pharo guarantee.
|
|
((= selector "becomeForward:")
|
|
(let ((other (nth args 0)))
|
|
(cond
|
|
((not (st-instance? receiver))
|
|
(error "becomeForward: only supported on user instances"))
|
|
((not (st-instance? other))
|
|
(error "becomeForward: target must be a user instance"))
|
|
(else
|
|
(begin
|
|
(dict-set! receiver :class (get other :class))
|
|
(dict-set! receiver :ivars (get other :ivars))
|
|
receiver)))))
|
|
((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 :unhandled)))))
|
|
|
|
;; Default printable representation. User instances render as
|
|
;; "an X" (or "a X" for vowel-initial classes); class-refs render as
|
|
;; their name. Native types are handled by their primitive senders.
|
|
(define
|
|
st-printable-string
|
|
(fn
|
|
(v)
|
|
(cond
|
|
((st-class-ref? v) (get v :name))
|
|
((st-instance? v)
|
|
(let ((cls (get v :class)))
|
|
(let ((article (if (st-vowel-initial? cls) "an " "a ")))
|
|
(str article cls))))
|
|
(else (str v)))))
|
|
|
|
(define
|
|
st-vowel-initial?
|
|
(fn
|
|
(s)
|
|
(cond
|
|
((= (len s) 0) false)
|
|
(else
|
|
(let ((c (nth s 0)))
|
|
(or (= c "A") (= c "E") (= c "I") (= c "O") (= c "U")
|
|
(= c "a") (= c "e") (= c "i") (= c "o") (= c "u")))))))
|
|
|
|
;; Pharo-style {N}-substitution. Walks the source, when a '{' starts a
|
|
;; valid numeric index, substitutes the corresponding (1-indexed) item
|
|
;; from the args collection. Unmatched braces are preserved.
|
|
(define
|
|
st-format-step
|
|
(fn
|
|
(src args out i n)
|
|
(let ((c (nth src i)))
|
|
(cond
|
|
((not (= c "{"))
|
|
{:emit c :advance 1})
|
|
(else
|
|
(let ((close (st-find-close-brace src i)))
|
|
(cond
|
|
((= close -1) {:emit c :advance 1})
|
|
(else
|
|
(let ((idx (parse-number (slice src (+ i 1) close))))
|
|
(cond
|
|
((and (number? idx)
|
|
(integer? idx)
|
|
(> idx 0)
|
|
(<= idx (len args)))
|
|
{:emit (str (nth args (- idx 1)))
|
|
:advance (- (+ close 1) i)})
|
|
(else
|
|
{:emit c :advance 1})))))))))))
|
|
|
|
(define
|
|
st-format-string
|
|
(fn
|
|
(src args)
|
|
(let ((out (list)) (i 0) (n (len src)))
|
|
(begin
|
|
(define
|
|
fmt-loop
|
|
(fn
|
|
()
|
|
(when
|
|
(< i n)
|
|
(let ((step (st-format-step src args out i n)))
|
|
(begin
|
|
(append! out (get step :emit))
|
|
(set! i (+ i (get step :advance)))
|
|
(fmt-loop))))))
|
|
(fmt-loop)
|
|
(join "" out)))))
|
|
|
|
(define
|
|
st-find-close-brace
|
|
(fn
|
|
(src start)
|
|
(let ((i (+ start 1)) (n (len src)) (found -1))
|
|
(begin
|
|
(define
|
|
fc-loop
|
|
(fn
|
|
()
|
|
(when
|
|
(and (< i n) (= found -1))
|
|
(cond
|
|
((= (nth src i) "}") (set! found i))
|
|
(else (begin (set! i (+ i 1)) (fc-loop)))))))
|
|
(fc-loop)
|
|
found))))
|
|
|
|
(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 "floor") (floor n))
|
|
((= selector "ceiling")
|
|
;; ceiling(x) = -floor(-x); fast for both signs.
|
|
(- 0 (floor (- 0 n))))
|
|
((= selector "truncated") (truncate n))
|
|
((= selector "rounded") (round n))
|
|
((= selector "sqrt") (sqrt n))
|
|
((= selector "squared") (* n n))
|
|
((= selector "raisedTo:")
|
|
(let ((p (nth args 0)) (acc 1) (i 0))
|
|
(begin
|
|
(define
|
|
rt-loop
|
|
(fn ()
|
|
(when (< i p)
|
|
(begin (set! acc (* acc n)) (set! i (+ i 1)) (rt-loop)))))
|
|
(rt-loop)
|
|
acc)))
|
|
((= selector "factorial")
|
|
(let ((acc 1) (i 2))
|
|
(begin
|
|
(define
|
|
ft-loop
|
|
(fn ()
|
|
(when (<= i n)
|
|
(begin (set! acc (* acc i)) (set! i (+ i 1)) (ft-loop)))))
|
|
(ft-loop)
|
|
acc)))
|
|
((= selector "even") (= (mod n 2) 0))
|
|
((= selector "odd") (= (mod n 2) 1))
|
|
((= selector "isInteger") (integer? n))
|
|
((= selector "isFloat") (and (number? n) (not (integer? n))))
|
|
((= selector "isNumber") true)
|
|
((= selector "gcd:")
|
|
(let ((a (if (< n 0) (- 0 n) n))
|
|
(b (if (< (nth args 0) 0) (- 0 (nth args 0)) (nth args 0))))
|
|
(begin
|
|
(define
|
|
gcd-loop
|
|
(fn ()
|
|
(cond
|
|
((= b 0) a)
|
|
(else
|
|
(let ((t (mod a b)))
|
|
(begin (set! a b) (set! b t) (gcd-loop)))))))
|
|
(gcd-loop))))
|
|
((= selector "lcm:")
|
|
(let ((g (st-num-send n "gcd:" args)))
|
|
(cond ((= g 0) 0)
|
|
(else (* (/ n g) (nth args 0))))))
|
|
((= 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 :unhandled))))
|
|
|
|
(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)))
|
|
;; 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)))
|
|
;; String>>format: — Pharo-style {N}-substitution.
|
|
;; '{1} loves {2}' format: #('Alice' 'Bob') → 'Alice loves Bob'
|
|
;; Indexes are 1-based. Unmatched braces are kept literally.
|
|
((= selector "format:")
|
|
(st-format-string s (nth args 0)))
|
|
((= selector "class") (st-class-ref (st-class-of s)))
|
|
((= selector "isNil") false)
|
|
((= selector "notNil") true)
|
|
(else :unhandled))))
|
|
|
|
(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 :unhandled))))
|
|
|
|
(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 :unhandled))))
|
|
|
|
(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 "add:")
|
|
(begin (append! a (nth args 0)) (nth args 0)))
|
|
((= 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 :unhandled))))
|
|
|
|
;; Split a Smalltalk-style "x y z" instance-variable string into a list of
|
|
;; ivar names. Whitespace-delimited.
|
|
(define
|
|
st-split-ivars
|
|
(fn
|
|
(s)
|
|
(let ((out (list)) (n (len s)) (i 0) (start nil))
|
|
(begin
|
|
(define
|
|
flush!
|
|
(fn ()
|
|
(when
|
|
(not (= start nil))
|
|
(begin (append! out (slice s start i)) (set! start nil)))))
|
|
(define
|
|
si-loop
|
|
(fn ()
|
|
(when
|
|
(< i n)
|
|
(let ((c (nth s i)))
|
|
(cond
|
|
((or (= c " ") (= c "\t") (= c "\n") (= c "\r"))
|
|
(begin (flush!) (set! i (+ i 1)) (si-loop)))
|
|
(else
|
|
(begin
|
|
(when (= start nil) (set! start i))
|
|
(set! i (+ i 1))
|
|
(si-loop))))))))
|
|
(si-loop)
|
|
(flush!)
|
|
out))))
|
|
|
|
(define
|
|
st-class-side-send
|
|
(fn
|
|
(cref selector args)
|
|
(let ((name (get cref :name)))
|
|
(cond
|
|
((= selector "new")
|
|
(cond
|
|
((= name "Array") (list))
|
|
(else (st-make-instance name))))
|
|
((= selector "new:")
|
|
(cond
|
|
((= name "Array")
|
|
(let ((size (nth args 0)) (out (list)))
|
|
(begin
|
|
(let ((i 0))
|
|
(begin
|
|
(define
|
|
an-loop
|
|
(fn ()
|
|
(when
|
|
(< i size)
|
|
(begin
|
|
(append! out nil)
|
|
(set! i (+ i 1))
|
|
(an-loop)))))
|
|
(an-loop)))
|
|
out)))
|
|
(else (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 "methodDict")
|
|
;; The class's own method dictionary (instance side).
|
|
(get (st-class-get name) :methods))
|
|
((= selector "classMethodDict")
|
|
(get (st-class-get name) :class-methods))
|
|
((= selector "selectors")
|
|
;; Own instance-side selectors as an Array of symbols.
|
|
(let ((out (list)))
|
|
(begin
|
|
(for-each
|
|
(fn (k) (append! out (make-symbol k)))
|
|
(keys (get (st-class-get name) :methods)))
|
|
out)))
|
|
((= selector "classSelectors")
|
|
(let ((out (list)))
|
|
(begin
|
|
(for-each
|
|
(fn (k) (append! out (make-symbol k)))
|
|
(keys (get (st-class-get name) :class-methods)))
|
|
out)))
|
|
((= selector "instanceVariableNames")
|
|
;; Own ivars as an Array of strings (matches Pharo).
|
|
(get (st-class-get name) :ivars))
|
|
((= selector "allInstVarNames")
|
|
;; Inherited + own ivars in declaration order (root first).
|
|
(st-class-all-ivars name))
|
|
;; Class definition: `Object subclass: #Foo instanceVariableNames: 'x y'`.
|
|
;; Supports the short `subclass:` and the full
|
|
;; `subclass:instanceVariableNames:classVariableNames:package:` form.
|
|
((or (= selector "subclass:")
|
|
(= selector "subclass:instanceVariableNames:")
|
|
(= selector "subclass:instanceVariableNames:classVariableNames:")
|
|
(= selector "subclass:instanceVariableNames:classVariableNames:package:")
|
|
(= selector "subclass:instanceVariableNames:classVariableNames:poolDictionaries:category:"))
|
|
(let
|
|
((sub-sym (nth args 0))
|
|
(iv-string (if (> (len args) 1) (nth args 1) "")))
|
|
(let
|
|
((sub-name (str sub-sym)))
|
|
(begin
|
|
(st-class-define!
|
|
sub-name
|
|
name
|
|
(st-split-ivars (if (string? iv-string) iv-string (str iv-string))))
|
|
(st-class-ref sub-name)))))
|
|
;; methodsFor: / methodsFor:stamp: are Pharo file-in markers — at
|
|
;; the expression level they just return the class for further
|
|
;; cascades. Method bodies are loaded by the chunk-stream loader.
|
|
((or (= selector "methodsFor:")
|
|
(= selector "methodsFor:stamp:")
|
|
(= selector "category:")
|
|
(= selector "comment:"))
|
|
cref)
|
|
;; Behavior>>compile: parses the source string as a method and
|
|
;; installs it. Returns the selector as a symbol.
|
|
;; Sister forms: compile:classified: and compile:notifying:
|
|
;; ignore the extra arg, mirroring Pharo's tolerant behaviour.
|
|
((or (= selector "compile:")
|
|
(= selector "compile:classified:")
|
|
(= selector "compile:notifying:"))
|
|
(let ((src (nth args 0)))
|
|
(let ((method-ast (st-parse-method (str src))))
|
|
(st-class-add-method!
|
|
name (get method-ast :selector) method-ast)
|
|
(make-symbol (get method-ast :selector)))))
|
|
((or (= selector "addSelector:withMethod:")
|
|
(= selector "addSelector:method:"))
|
|
(let
|
|
((sel (str (nth args 0)))
|
|
(method-ast (nth args 1)))
|
|
(begin
|
|
(st-class-add-method! name sel method-ast)
|
|
(make-symbol sel))))
|
|
((= selector "removeSelector:")
|
|
(let ((sel (str (nth args 0))))
|
|
(st-class-remove-method! name sel)))
|
|
((= 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 :unhandled)))))
|
|
|
|
;; Run a chunk-format Smalltalk program. Do-it expressions execute in a
|
|
;; fresh top-level frame (with an active-cell so ^expr works). Method
|
|
;; chunks register on the named class.
|
|
(define
|
|
smalltalk-load
|
|
(fn
|
|
(src)
|
|
(let ((entries (st-parse-chunks src)) (last-result nil))
|
|
(begin
|
|
(for-each
|
|
(fn (entry)
|
|
(let ((kind (get entry :kind)))
|
|
(cond
|
|
((= kind "expr")
|
|
(let ((cell {:active true}))
|
|
(set!
|
|
last-result
|
|
(call/cc
|
|
(fn (k)
|
|
(smalltalk-eval-ast
|
|
(get entry :ast)
|
|
(st-make-frame nil nil nil k cell)))))
|
|
(dict-set! cell :active false)))
|
|
((= kind "method")
|
|
(cond
|
|
((get entry :class-side?)
|
|
(st-class-add-class-method!
|
|
(get entry :class)
|
|
(get (get entry :ast) :selector)
|
|
(get entry :ast)))
|
|
(else
|
|
(st-class-add-method!
|
|
(get entry :class)
|
|
(get (get entry :ast) :selector)
|
|
(get entry :ast)))))
|
|
(else nil))))
|
|
entries)
|
|
last-result))))
|
|
|
|
;; Convenience: parse and evaluate a Smalltalk expression with no receiver.
|
|
(define
|
|
smalltalk-eval
|
|
(fn
|
|
(src)
|
|
(let ((cell {:active true}))
|
|
(let
|
|
((result
|
|
(call/cc
|
|
(fn (k)
|
|
(let
|
|
((ast (st-parse-expr src))
|
|
(frame (st-make-frame nil nil nil k cell)))
|
|
(smalltalk-eval-ast ast frame))))))
|
|
(begin (dict-set! cell :active false) result)))))
|
|
|
|
;; Evaluate a sequence of statements at the top level.
|
|
(define
|
|
smalltalk-eval-program
|
|
(fn
|
|
(src)
|
|
(let ((cell {:active true}))
|
|
(let
|
|
((result
|
|
(call/cc
|
|
(fn (k)
|
|
(let
|
|
((ast (st-parse src))
|
|
(frame (st-make-frame nil nil nil k cell)))
|
|
(begin
|
|
(when
|
|
(and (dict? ast) (has-key? ast :temps))
|
|
(for-each
|
|
(fn (t) (dict-set! (get frame :locals) t nil))
|
|
(get ast :temps)))
|
|
(smalltalk-eval-ast ast frame)))))))
|
|
(begin (dict-set! cell :active false) result)))))
|