git » blacktide.git » commit 31924cf

Make emission eager and reuse compaction read summaries

author Alan Dipert
2026-11-07 05:33:42 UTC
committer Alan Dipert
2026-11-07 05:33:42 UTC
parent 4e4f90f39d28a74068dd5a8e0a4bb3450bdd1052

Make emission eager and reuse compaction read summaries

Complete dispatcher emission before returning generated forms, use eager
collection transformations where order permits, and carry free-read counts
through compaction instead of repeatedly traversing retained bodies. Process
sequential bindings iteratively to avoid growing the simplifier's call stack.

Cover emission timing, lexical read counts and long binding chains, and
assert the intentional boxed-math warnings without leaking them into test output.

src/com/tailrecursion/blacktide/compiler/passes/compact.clj +79 -37
src/com/tailrecursion/blacktide/compiler/passes/emit.clj +4 -4
src/com/tailrecursion/blacktide/compiler/passes/feasible.clj +2 -6
src/com/tailrecursion/blacktide/compiler/passes/graph.clj +3 -4
src/com/tailrecursion/blacktide/compiler/passes/native.clj +2 -2
src/com/tailrecursion/blacktide/compiler/passes/structured_emit.clj +2 -2
src/com/tailrecursion/blacktide/compiler/passes/transfers.clj +2 -6
test/com/tailrecursion/blacktide/backend_test.clj +32 -0
test/com/tailrecursion/blacktide/jvm_test.clj +10 -6
test/com/tailrecursion/blacktide/machine_test.clj +20 -0

diff --git a/src/com/tailrecursion/blacktide/compiler/passes/compact.clj b/src/com/tailrecursion/blacktide/compiler/passes/compact.clj
index 372317c..65824fd 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/compact.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/compact.clj
@@ -37,28 +37,30 @@
     :host (into [] (mapcat #(atom-reads (:value %))) (:args node))
     :invoke (into (atom-reads (:function node)) (mapcat atom-reads) (:args node))))
 
-(np/defpass reads {:from [l/Compact Expr] :to (t/Vec t/Sym)}
+(np/defpass reads {:from [l/Compact Expr] :to (t/Map t/Sym t/Int)}
   [node]
-  :bind (into (rhs-reads (:value (:binding node))) (filterv #(not= % (:target (:binding node))) (reads (:body node))))
-  :effect (into (rhs-reads (:value (:binding node))) (reads (:body node)))
-  :join (into (reads (:value node)) (filterv #(not= % (:target node)) (reads (:body node))))
-  :branch (into (into (atom-reads (:test node)) (reads (:yes node))) (reads (:no node)))
-  :switch (into (atom-reads (:test node)) (mapcat reads) (:arms node))
-  :loop (reduce (fn [^{::t/- (t/Vec t/Sym)} refs ^{::t/- l/Compact-Binding} binding]
-                  (into (atom-reads (:value binding)) (filterv #(not= % (:target binding)) refs)))
+  :bind (merge-with + (frequencies (rhs-reads (:value (:binding node))))
+                      (dissoc (reads (:body node)) (:target (:binding node))))
+  :effect (merge-with + (frequencies (rhs-reads (:value (:binding node)))) (reads (:body node)))
+  :join (merge-with + (reads (:value node)) (dissoc (reads (:body node)) (:target node)))
+  :branch (merge-with + (frequencies (atom-reads (:test node))) (reads (:yes node)) (reads (:no node)))
+  :switch (reduce (fn [^{::t/- (t/Map t/Sym t/Int)} refs ^{::t/- l/Compact-Expr} arm]
+                    (merge-with + refs (reads arm))) (frequencies (atom-reads (:test node))) (:arms node))
+  :loop (reduce (fn [^{::t/- (t/Map t/Sym t/Int)} refs ^{::t/- l/Compact-Binding} binding]
+                  (merge-with + (frequencies (atom-reads (:value binding))) (dissoc refs (:target binding))))
                 (reads (:body node)) (rseq (:bindings node)))
-  :recur (into [] (mapcat atom-reads) (:args node))
-  :return (atom-reads (:value node)) :throw (atom-reads (:value node)))
+  :recur (frequencies (into [] (mapcat atom-reads) (:args node)))
+  :return (frequencies (atom-reads (:value node))) :throw (frequencies (atom-reads (:value node))))
 
 (np/defpass stable?
   {:from [l/Compact Expr] :to t/Bool :args [t/Sym (t/Set t/Sym)]}
   [id dependencies node]
   :bind (if (contains? dependencies (:target (:binding node)))
-          (not (some #{id} (reads (:body node)))) (stable? id dependencies (:body node)))
+          (not (contains? (reads (:body node)) id)) (stable? id dependencies (:body node)))
   :effect (stable? id dependencies (:body node))
   :join (and (stable? id dependencies (:value node))
              (if (contains? dependencies (:target node))
-               (not (some #{id} (reads (:body node)))) (stable? id dependencies (:body node))))
+               (not (contains? (reads (:body node)) id)) (stable? id dependencies (:body node))))
   :branch (and (stable? id dependencies (:yes node)) (stable? id dependencies (:no node)))
   :switch (every? #(stable? id dependencies %) (:arms node))
   ;; Loop binders also rebind on the backedge. Do not move an outer snapshot
@@ -166,31 +168,71 @@
        (let [value (:value rhs)]
          (or (= :local (:op value)) (and (= :literal (:op value)) (nil? (:value value)))))))
 
-(np/defpass simplify {:from [l/Compact Expr] :to [l/Compact Expr] :args [l/Compact-Types]}
-  [types node]
-  :bind (let [binding (:binding node) id (:target binding) body (simplify types (:body node))
-              uses (count (filter #{id} (reads body)))
-              value (replacement types uses binding)]
-          (cond
-            (and (= :native (:op (:value binding))) (not (contains? types id))
-                 (= body {:op :return :value {:op :local :id id}}))
-            {:op :return :value {:op :inline :value (:value binding) :type nil}}
-            (zero? uses) (if (discardable? (:value binding)) body {:op :effect :binding binding :body body})
-            (and value (stable? id (set (atom-reads value)) body))
-            (let [out (substitute id value body)]
-              ;; Substitution deliberately leaves native code alone.
-              (if (some #{id} (reads out)) (assoc node :body body) out))
-            :else (assoc node :body body)))
-  :effect (let [body (simplify types (:body node))]
-            (if (discardable? (:value (:binding node))) body (assoc node :body body)))
-  :join (let [value (simplify types (:value node)) body (simplify types (:body node)) id (:target node)]
-          (if (and (not (contains? types id)) (= body {:op :return :value {:op :local :id id}}))
-            value (assoc node :value value :body body)))
-  :branch (assoc node :yes (simplify types (:yes node)) :no (simplify types (:no node)))
-  :switch (assoc node :arms (mapv #(simplify types %) (:arms node)))
-  :loop (assoc node :body (simplify types (:body node)))
-  :recur node :return node :throw node)
+(np/defpasses
+  (simplify-bindings
+    {:from [l/Compact Expr] :to (t/HVec [l/Compact-Expr (t/Map t/Sym t/Int)])
+     :args [l/Compact-Types] :body true}
+    [types node]
+    ;; Process sequential bindings inside out without growing the JVM stack.
+    ;; Each result carries free-read counts for the next enclosing binding.
+    ((fn ^{::t/- [(t/Vec l/Compact-Expr) l/Compact-Expr
+                   :-> (t/HVec [l/Compact-Expr (t/Map t/Sym t/Int)])]} collect [parents node]
+      (case (:op node)
+        (:bind :effect) (recur (conj parents node) (:body node))
+        (reduce
+          (fn [^{::t/- (t/HVec [l/Compact-Expr (t/Map t/Sym t/Int)])} result
+               ^{::t/- l/Compact-Expr} parent]
+            (let [[body refs] result]
+              (case (:op parent)
+                :bind
+                (let [binding (:binding parent) id (:target binding)
+                      uses (get refs id 0) value (replacement types uses binding)
+                      keep-binding (fn [] [(assoc parent :body body)
+                                           (merge-with + (frequencies (rhs-reads (:value binding))) (dissoc refs id))])]
+                  (cond
+                    (and (= :native (:op (:value binding))) (not (contains? types id))
+                         (= body {:op :return :value {:op :local :id id}}))
+                    [{:op :return :value {:op :inline :value (:value binding) :type nil}}
+                     (frequencies (rhs-reads (:value binding)))]
+                    (zero? uses) (if (discardable? (:value binding)) [body refs]
+                                  [{:op :effect :binding binding :body body}
+                                   (merge-with + (frequencies (rhs-reads (:value binding))) refs)])
+                    (and value (stable? id (set (atom-reads value)) body))
+                    (let [out (substitute id value body) refs (reads out)]
+                      ;; Substitution deliberately leaves native code alone.
+                      (if (contains? refs id) (keep-binding) [out refs]))
+                    :else (keep-binding)))
+                :effect
+                (let [rhs (:value (:binding parent))]
+                  (if (discardable? rhs) [body refs]
+                    [(assoc parent :body body) (merge-with + (frequencies (rhs-reads rhs)) refs)])))))
+          (simplify types node) (rseq parents)))) [] node))
+
+  (simplify
+    {:from [l/Compact Expr] :to (t/HVec [l/Compact-Expr (t/Map t/Sym t/Int)]) :args [l/Compact-Types]}
+    [types node]
+    :bind (simplify-bindings types node)
+    :effect (simplify-bindings types node)
+    :join (let [[value value-refs] (simplify types (:value node))
+                [body body-refs] (simplify types (:body node)) id (:target node)]
+            (if (and (not (contains? types id)) (= body {:op :return :value {:op :local :id id}}))
+              [value value-refs]
+              [(assoc node :value value :body body) (merge-with + value-refs (dissoc body-refs id))]))
+    :branch (let [[yes yes-refs] (simplify types (:yes node)) [no no-refs] (simplify types (:no node))]
+              [(assoc node :yes yes :no no)
+               (merge-with + (frequencies (atom-reads (:test node))) yes-refs no-refs)])
+    :switch (let [arms (mapv #(simplify types %) (:arms node))]
+              [(assoc node :arms (mapv first arms))
+               (reduce (fn [^{::t/- (t/Map t/Sym t/Int)} refs
+                            ^{::t/- (t/HVec [l/Compact-Expr (t/Map t/Sym t/Int)])} arm]
+                         (merge-with + refs (second arm))) (frequencies (atom-reads (:test node))) arms)])
+    :loop (let [[body refs] (simplify types (:body node))]
+            [(assoc node :body body)
+             (reduce (fn [^{::t/- (t/Map t/Sym t/Int)} refs ^{::t/- l/Compact-Binding} binding]
+                       (merge-with + (frequencies (atom-reads (:value binding))) (dissoc refs (:target binding))))
+                     refs (rseq (:bindings node)))])
+    :recur [node (reads node)] :return [node (reads node)] :throw [node (reads node)]))
 
 (np/defpass program {:from [l/Stored Program] :to [l/Compact Program] :body true}
   [program]
-  {:types (:types program) :body (simplify (:types program) (introduce (:body program)))})
+  {:types (:types program) :body (first (simplify (:types program) (introduce (:body program))))})
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/emit.clj b/src/com/tailrecursion/blacktide/compiler/passes/emit.clj
index 138f12b..f9bb232 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/emit.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/emit.clj
@@ -61,7 +61,7 @@
   [slots live block]
   (let [end (term-form slots live (:term block))]
     (if (seq (:ops block))
-      (list 'let* (vec (mapcat identity (mapv operation-form (:ops block)))) end)
+      (list 'let* (into [] (mapcat operation-form) (:ops block)) end)
       end)))
 
 (np/defpass program-form {:from [l/Machine Program] :to [l/Machine Code] :body true}
@@ -70,8 +70,8 @@
         slots (mapv #(with-meta % nil) (:slots program))
         bindings (into [pc (host/box-form (list 'quote (:entry program)))]
                        (mapcat #(vector % nil)) slots)
-        arms (mapcat (fn [[label block]] [label (block-form slots (:live program) block)])
-                     (sort-by key (:blocks program)))]
+        arms (into [] (mapcat (fn [[label block]] [label (block-form slots (:live program) block)]))
+                   (sort-by key (:blocks program)))]
     (list 'loop* bindings
           (list* 'clojure.core/case pc
-                 (concat arms [(list 'throw (list 'clojure.core/ex-info "Invalid machine PC" {:pc pc}))])))))
+                 (conj arms (list 'throw (list 'clojure.core/ex-info "Invalid machine PC" {:pc pc})))))))
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/feasible.clj b/src/com/tailrecursion/blacktide/compiler/passes/feasible.clj
index c5e0999..282f6fe 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/feasible.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/feasible.clj
@@ -2,8 +2,7 @@
   "Fold literal truth tests after source validation. Keep every block so dead
   source still receives JVM validation."
   (:require [com.tailrecursion.nanopass :as np]
-            [com.tailrecursion.blacktide.compiler.languages :as l]
-            [typed.clojure :as-alias t]))
+            [com.tailrecursion.blacktide.compiler.languages :as l]))
 
 (np/defpass terminator {:from [l/CFG Term] :to [l/CFG Term] :body true}
   [term]
@@ -16,7 +15,4 @@
 
 (np/defpass program {:from [l/CFG Program] :to [l/CFG Program] :body true}
   [program]
-  (assoc program :blocks
-    (reduce-kv (fn [^{::t/- (t/Map l/CFG-Label l/CFG-Block)} out
-                     ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-Block} node]
-                 (assoc out id (block node))) {} (:blocks program))))
+  (update program :blocks update-vals block))
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/graph.clj b/src/com/tailrecursion/blacktide/compiler/passes/graph.clj
index ba56c9b..381e5e3 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/graph.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/graph.clj
@@ -20,11 +20,11 @@
   :var #{}
   :native (native/reads (:value node))
   :dispatch (atom-reads (:value (:value node)))
-  :construct (reduce set/union #{} (mapv atom-reads (:items node)))
+  :construct (into #{} (mapcat atom-reads) (:items node))
   :metadata (set/union (atom-reads (:value node)) (atom-reads (:metadata node)))
   :convert (atom-reads (:value (:value node)))
   :host (into #{} (mapcat #(atom-reads (:value %))) (:args node))
-  :invoke (reduce set/union (atom-reads (:function node)) (mapv atom-reads (:args node))))
+  :invoke (into (atom-reads (:function node)) (mapcat atom-reads) (:args node)))
 
 (np/defpass term-reads {:from [l/CFG Term] :to (t/Set t/Sym)}
   [term]
@@ -56,8 +56,7 @@
 
 (np/defpass collect-facts {:from [l/CFG Program] :to [l/CFG Facts] :body true}
   [program]
-  (reduce-kv (fn [^{::t/- l/CFG-Facts} facts ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-Block} block]
-               (assoc facts id (block-facts block))) {} (:blocks program)))
+  (update-vals (:blocks program) block-facts))
 
 (np/defpass fact-at {:from [l/CFG Label] :to [l/CFG BlockFacts] :args [l/CFG-Facts] :body true}
   [facts id]
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/native.clj b/src/com/tailrecursion/blacktide/compiler/passes/native.clj
index 08c5c00..b23496f 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/native.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/native.clj
@@ -27,9 +27,9 @@
   (bindings-form
     {:from [l/Native Bindings] :to (t/Vec t/Any) :args [(t/Set t/Sym)] :body true}
     [bound bindings]
-    (vec (mapcat (fn [^{::t/- l/Native-Binding} binding]
+    (into [] (mapcat (fn [^{::t/- l/Native-Binding} binding]
                    [(with-meta (:id (:local binding)) (meta (:name (:local binding))))
-                    (form bound (:init binding))]) bindings)))
+                    (form bound (:init binding))])) bindings))
 
   (node-form
     {:from [l/Native Node] :to [l/Machine Code] :args [(t/Set t/Sym) l/Native-Expr]}
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/structured_emit.clj b/src/com/tailrecursion/blacktide/compiler/passes/structured_emit.clj
index 135d033..6b9ed9d 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/structured_emit.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/structured_emit.clj
@@ -95,10 +95,10 @@
     :join (let-form types node)
     :branch (list 'if (atom-form types (:test node)) (expression types (:yes node)) (expression types (:no node)))
     :switch (cases/arms-form (mapv #(expression types %) (:arms node)) (atom-form types (:test node)))
-    :loop (list 'loop* (vec (mapcat (fn [binding]
+    :loop (list 'loop* (into [] (mapcat (fn [binding]
                                     [(:target binding) (binding-form types (:target binding)
                                                          (compact/atom-type types (:value binding))
-                                                         (atom-form types (:value binding)))]) (:bindings node)))
+                                                         (atom-form types (:value binding)))])) (:bindings node))
                 (expression types (:body node)))
     :recur (list* 'recur (mapv #(atom-form types %) (:args node)))
     :return (let [value (:value node) form (atom-form types value)]
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/transfers.clj b/src/com/tailrecursion/blacktide/compiler/passes/transfers.clj
index a64bc28..c4a95f1 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/transfers.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/transfers.clj
@@ -3,9 +3,7 @@
   Source analysis admits only outer-body labels and statement-position jumps."
   (:require [com.tailrecursion.blacktide.compiler.errors :as errors]
             [com.tailrecursion.nanopass :as np]
-            [com.tailrecursion.blacktide.compiler.languages :as l]
-            [typed.clojure :as-alias t]))
-
+            [com.tailrecursion.blacktide.compiler.languages :as l]))
 
 (np/defpass resolve-term
   {:from [l/Symbolic Term] :to [l/CFG Term] :args [l/Symbolic-LabelTable]}
@@ -29,6 +27,4 @@
   {:from [l/Symbolic Program] :to [l/CFG Program] :body true}
   [program]
   {:entry (:entry program)
-   :blocks (reduce-kv (fn [^{::t/- (t/Map l/CFG-Label l/CFG-Block)} out
-                           ^{::t/- l/Symbolic-Label} id ^{::t/- l/Symbolic-Block} block]
-                        (assoc out id (resolve-block (:labels program) block))) {} (:blocks program))})
+   :blocks (update-vals (:blocks program) #(resolve-block (:labels program) %))})
diff --git a/test/com/tailrecursion/blacktide/backend_test.clj b/test/com/tailrecursion/blacktide/backend_test.clj
index 19e0309..6b1cd5b 100644
--- a/test/com/tailrecursion/blacktide/backend_test.clj
+++ b/test/com/tailrecursion/blacktide/backend_test.clj
@@ -52,6 +52,38 @@
 (defmacro local-class [name]
   (list 'quote (.getJavaClass ^clojure.lang.Compiler$LocalBinding (get &env name))))
 
+(deftest compaction-carries-free-read-counts
+  (let [x (m/local 'x) y (m/local 'y) result {:op :return :value x}
+        binding (m/call 'x 'f x)]
+    (doseq [[tree expected]
+            [[{:op :bind :binding binding :body result} {'f 1 'x 1}]
+             [{:op :effect :binding binding :body result} {'f 1 'x 2}]
+             [{:op :branch :test (m/local 'test) :yes result :no result} {'test 1 'x 2}]
+             [{:op :switch :test (m/local 'index) :arms [result {:op :throw :value y}]} {'index 1 'x 1 'y 1}]
+             [{:op :loop :bindings [{:target 'x :value x} {:target 'y :value x}]
+               :body {:op :return :value y}} {'x 1}]
+             [{:op :recur :args [x y x]} {'x 2 'y 1}]
+             [{:op :join :target 'x :value result
+               :body {:op :bind :binding (m/call 'z 'f x) :body {:op :return :value (m/local 'z)}}}
+              {'x 1 'f 1}]
+             [{:op :bind :binding (m/assign 'x {:op :value :value y})
+               :body {:op :bind :binding binding :body result}} {'y 1 'f 1}]]]
+      (let [[out refs] (compact/simplify {} tree)]
+        (is (= expected refs) (pr-str tree))
+        (is (= expected (compact/reads out)) (pr-str out)))))
+  (let [n 1024
+        tree (reduce (fn [body i]
+                       {:op :bind :binding (m/call (symbol (str "x" (inc i))) 'f (m/local (symbol (str "x" i))))
+                        :body body})
+                     {:op :return :value (m/local (symbol (str "x" n)))} (reverse (range n)))
+        visits (atom 0) reads compact/reads
+        [out refs] (with-redefs [compact/reads (fn [node] (swap! visits inc) (reads node))]
+                     (compact/simplify {} tree))]
+    (is (= n (count (take-while #(= :bind (:op %)) (iterate :body out))))
+        "Long binding sequences must not grow the simplifier's call stack")
+    (is (= {'f n 'x0 1} refs))
+    (is (= 1 @visits) "Retained bindings reuse the child's summary instead of walking it again")))
+
 (deftest result-type-boundary
   (doseq [body '[1 2.5 nil false "text" (loop [] 1) (do (goto end) :dead (label end) 42)]
           mode [:default :dispatcher]]
diff --git a/test/com/tailrecursion/blacktide/jvm_test.clj b/test/com/tailrecursion/blacktide/jvm_test.clj
index dfa2bdc..2a73e6d 100644
--- a/test/com/tailrecursion/blacktide/jvm_test.clj
+++ b/test/com/tailrecursion/blacktide/jvm_test.clj
@@ -98,12 +98,16 @@
   (is (= 30 (loophole [x 0] (+ (do (setq x 10) x) (do (setq x 20) x)))))
   (is (= [1 2] (loophole [+ vector] (+ 1 2))))
   (doseq [unchecked [false true :warn-on-boxed]
-          form '[(+ Long/MAX_VALUE 1) (inc Long/MAX_VALUE)
-                 (* Long/MAX_VALUE 2) (int Long/MAX_VALUE)
-                 (let [x 4] (+ x (if true 2 3)))
-                 (let [x (identity Long/MAX_VALUE)] (+ x 1))]]
-    (binding [*unchecked-math* unchecked]
-      (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str [unchecked form])))))
+          [boxed? form] '[[false (+ Long/MAX_VALUE 1)] [false (inc Long/MAX_VALUE)]
+                          [false (* Long/MAX_VALUE 2)] [false (int Long/MAX_VALUE)]
+                          [false (let [x 4] (+ x (if true 2 3)))]
+                          [true (let [x (identity Long/MAX_VALUE)] (+ x 1))]]]
+    (let [warnings (java.io.StringWriter.)]
+      (binding [*unchecked-math* unchecked *err* warnings]
+        (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str [unchecked form])))
+      ;; Only the deliberately Object-typed addition warns, once per compiler.
+      (is (= (if (and boxed? (= :warn-on-boxed unchecked)) 2 0)
+             (count (re-seq #"Boxed math warning" (str warnings)))) (pr-str [unchecked form])))))
 
 (defn numeric-expression [depth locals]
   (let [leaf (gen/elements (into [-13 -1 0 1 7 2.5 1/2] locals))]
diff --git a/test/com/tailrecursion/blacktide/machine_test.clj b/test/com/tailrecursion/blacktide/machine_test.clj
index f003755..9e515aa 100644
--- a/test/com/tailrecursion/blacktide/machine_test.clj
+++ b/test/com/tailrecursion/blacktide/machine_test.clj
@@ -2,6 +2,7 @@
   "Compile allocated machines with the real Clojure compiler and execute them."
   (:require [clojure.test :refer [deftest is]]
             [com.tailrecursion.blacktide.compiler.jvm :as jvm]
+            [com.tailrecursion.blacktide.compiler.passes.host-emit :as host]
             [com.tailrecursion.blacktide.compiler.passes.emit :as emit]))
 
 (defn literal [value] {:op :literal :value value})
@@ -14,6 +15,25 @@
   (binding [*ns* (the-ns 'com.tailrecursion.blacktide.machine-test)]
     (eval (emit/program-form machine))))
 
+(deftest emission-is-complete-before-returning
+  (let [calls (atom []) original host/global-form
+        machine {:entry 0 :slots [] :live {}
+                 :blocks (into {} (for [i (range 128)]
+                                    [i (block [(assign 'f {:op :global :name 'clojure.core/identity})]
+                                              {:op :return :value (literal i)})]))}
+        form (with-redefs [host/global-form (fn [name]
+                                             (swap! calls conj name)
+                                             (original name))]
+               (emit/program-form machine))]
+    (is (= (vec (repeat 128 'clojure.core/identity)) @calls))
+    (with-redefs [host/global-form (fn [_] (throw (ex-info "Deferred emission" {})))]
+      (is (string? (pr-str form)))
+      (is (zero? (eval form)))))
+  ;; Emitting a quote must not realize its payload.
+  (let [payload (lazy-seq (throw (ex-info "Forced quoted data" {})))
+        form (emit/atom-form (literal payload))]
+    (is (identical? payload (second form)))))
+
 (deftest values-and-clojure-truth
   (doseq [value [nil false true 0 1N 1/2 2.5 2M "text" :keyword 'symbol [1 2] {:a 3} #{4}]]
     (is (= value (run-machine {:entry 0 :slots [] :live {0 #{}}