| author | Alan Dipert
<alan@dipert.org> 2026-11-07 05:33:42 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2026-11-07 05:33:42 UTC |
| parent | 4e4f90f39d28a74068dd5a8e0a4bb3450bdd1052 |
| 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 #{}}