;; 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)))))
