git » blacktide.git » commit 7034d1f

Infer numeric values only after structured recovery succeeds

author Alan Dipert
2026-11-11 04:32:05 UTC
committer Alan Dipert
2026-11-11 04:32:05 UTC
parent ddf183af2f4a1b935aa1666fe5c8eaba65bb36b9

Infer numeric values only after structured recovery succeeds

doc/compiler.md +2 -2
doc/tutorial.md +2 -2
src/com/tailrecursion/blacktide/compiler.clj +4 -7
src/com/tailrecursion/blacktide/compiler/languages.clj +8 -10
src/com/tailrecursion/blacktide/compiler/passes/joins.clj +7 -7
src/com/tailrecursion/blacktide/compiler/passes/structure.clj +4 -4
test/com/tailrecursion/blacktide/backend_test.clj +7 -3

diff --git a/doc/compiler.md b/doc/compiler.md
index 6bf5e9d..21b0ba8 100644
--- a/doc/compiler.md
+++ b/doc/compiler.md
@@ -36,11 +36,11 @@ The outer bindings enter as one lexical scope. `Native` contains analyzed subtre
 
 ```text
 Dispatcher:  CFG → Machine → Code
-Structured:  CFG → Machine → Numeric → Analysis → Planned → Joined
+Structured:  CFG → Machine → Numeric → Planned → Joined
                      → Represented → Lexical → Compact → Code
 ```
 
-`numeric` recognizes numeric runtime calls and implicit Java numeric conversions, retaining their original signatures. `value-facts` proves scalar values across assignments and backedges; a boxed result can still be known to contain a long. `storage` chooses representations after accounting for consumers, copies and observable identity. `lexical` makes loop arguments explicit. `numeric-lower` implements the proven equivalences using host operations and ordinary control flow. `compact` simplifies safe intermediates. Emission applies the Object result boundary at public exits and leaves join values internal.
+`numeric` recognizes numeric runtime calls and implicit Java numeric conversions, retaining their original signatures. Control recovery uses that graph without numeric value proofs. Once it succeeds, `value-facts` analyzes the `Numeric` graph to produce `Analysis`: scalar facts across assignments and backedges, including long values held in boxes. `storage` combines those facts with the recovered tree to choose representations after accounting for consumers, copies and observable identity. Dispatcher fallback skips value inference. `lexical` makes loop arguments explicit. `numeric-lower` implements the proven equivalences using host operations and ordinary control flow. `compact` simplifies safe intermediates. Emission applies the Object result boundary at public exits and leaves join values internal.
 
 Inference and lowering share `numeric-rules`. Knowing the operands' values is insufficient: their original overload determines overflow, conversion and reference-return behavior.
 
diff --git a/doc/tutorial.md b/doc/tutorial.md
index a1cd658..eb86858 100644
--- a/doc/tutorial.md
+++ b/doc/tutorial.md
@@ -491,9 +491,9 @@ Two terms help here: a _join_ is a point where control-flow paths meet, and a _l
 
 | Pass group | Purpose |
 | --- | --- |
-| Feasibility, numeric recognition and value facts | Select known branches, recognize audited numeric operations and prove scalar values. |
+| Feasibility and numeric recognition | Select known branches and recognize audited numeric operations. |
 | Join planning and structured recovery | Arrange shared paths and recover conditionals and loops. |
-| Invariants and storage | Remove loop parameters that only forward unchanged outer values; choose storage without changing object identity. |
+| Value facts, invariants and storage | Prove scalar values, remove loop parameters that only forward unchanged outer values, and choose storage without changing object identity. |
 | Numeric lowering | Select proven primitive overloads; preserve other calls unchanged. |
 | Compaction and emission | Remove safe intermediates and emit Clojure with an Object boundary at public exits. |
 
diff --git a/src/com/tailrecursion/blacktide/compiler.clj b/src/com/tailrecursion/blacktide/compiler.clj
index 0e02da3..b7ea436 100644
--- a/src/com/tailrecursion/blacktide/compiler.clj
+++ b/src/com/tailrecursion/blacktide/compiler.clj
@@ -31,14 +31,11 @@
 (np/defpass compile-program
   {:from [l/CFG Program] :to [l/Machine Code] :body true}
   [cfg]
-  (let [analysis (-> cfg
-                     feasible/program
-                     graph/allocate
-                     numeric/program
-                     value-facts/analyze)
-        decision (-> analysis joins/plan structure/recover)]
+  (let [machine (-> cfg feasible/program graph/allocate numeric/program)
+        decision (-> machine joins/plan structure/recover)]
     (if (= :structured (:op decision))
-      (let [program (->> (:body decision)
+      (let [analysis (value-facts/analyze machine)
+            program (->> (:body decision)
                          (invariants/expression #{})
                          (storage/select-storage analysis)
                          lexical/program
diff --git a/src/com/tailrecursion/blacktide/compiler/languages.clj b/src/com/tailrecursion/blacktide/compiler/languages.clj
index a613837..5113cf1 100644
--- a/src/com/tailrecursion/blacktide/compiler/languages.clj
+++ b/src/com/tailrecursion/blacktide/compiler/languages.clj
@@ -186,7 +186,7 @@
              Graph (t/HMap :mandatory {:entry Label :blocks (t/Map Label Block)}
                            :optional {:slots (t/Vec t/Sym) :live (t/Map Label (t/Set t/Sym))} :complete? true)}})
 
-;; Facts and plans used by structured control recovery.
+;; Numeric value facts and control plans are independent backend analyses.
 (np/deflanguage Analysis
   {Type (t/U ':bottom ':boxed java.lang.Class)
    Types (t/Map t/Sym Type)
@@ -194,15 +194,13 @@
    Program (t/HMap :mandatory {:machine l/Numeric-Program :types Types
                                :reachable (t/Set l/Machine-Label)} :complete? true)})
 
-(np/derive-language Planned Analysis
-  {:replace
-   {Postdominators (t/Map l/Machine-Label (t/Set l/Machine-Label))
-    Region (t/HMap :mandatory {:definite (t/Set t/Sym) :changed (t/Set t/Sym)} :complete? true)
-    Regions (t/Map l/Machine-Label Region)
-    Join (t/HMap :mandatory {:target l/Machine-Label :result t/Sym} :complete? true)
-    Joins (t/Map l/Machine-Label Join)
-    Program (t/HMap :mandatory {:machine l/Numeric-Program :types Types
-                                :reachable (t/Set l/Machine-Label) :joins Joins} :complete? true)}})
+(np/deflanguage Planned
+  {Postdominators (t/Map l/Machine-Label (t/Set l/Machine-Label))
+   Region (t/HMap :mandatory {:definite (t/Set t/Sym) :changed (t/Set t/Sym)} :complete? true)
+   Regions (t/Map l/Machine-Label Region)
+   Join (t/HMap :mandatory {:target l/Machine-Label :result t/Sym} :complete? true)
+   Joins (t/Map l/Machine-Label Join)
+   Program (t/HMap :mandatory {:machine l/Numeric-Program :joins Joins} :complete? true)})
 
 ;; Structured recovery produces Joined expressions. Representation selection
 ;; produces Represented; lexical and numeric lowering follow it.
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/joins.clj b/src/com/tailrecursion/blacktide/compiler/passes/joins.clj
index 673a392..a864565 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/joins.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/joins.clj
@@ -61,10 +61,10 @@
                 (when (and (<= (count outputs) 1) (symbol? result) (contains? (:definite proof) result))
                   {:target target :result result})))))))))
 
-(np/defpass plan {:from [l/Analysis Program] :to [l/Planned Program] :body true}
-  [analysis]
-  (let [machine (:machine analysis) known (postdominators machine)]
-    (assoc analysis :joins
-      (reduce (fn [^{::t/- l/Planned-Joins} out ^{::t/- l/Machine-Label} id]
-                (if-let [join (candidate machine known id)] (assoc out id join) out))
-              {} (keys (:blocks machine))))))
+(np/defpass plan {:from [l/Numeric Program] :to [l/Planned Program] :body true}
+  [machine]
+  (let [known (postdominators machine)]
+    {:machine machine
+     :joins (reduce (fn [^{::t/- l/Planned-Joins} out ^{::t/- l/Machine-Label} id]
+                      (if-let [join (candidate machine known id)] (assoc out id join) out))
+                    {} (keys (:blocks machine)))}))
diff --git a/src/com/tailrecursion/blacktide/compiler/passes/structure.clj b/src/com/tailrecursion/blacktide/compiler/passes/structure.clj
index 284249d..dae9c77 100644
--- a/src/com/tailrecursion/blacktide/compiler/passes/structure.clj
+++ b/src/com/tailrecursion/blacktide/compiler/passes/structure.clj
@@ -141,9 +141,9 @@
       :native-sites)))
 
 (np/defpass recover {:from [l/Planned Program] :to [l/Joined Decision] :body true}
-  [analysis]
-  (let [machine (:machine analysis) reachable (:reachable analysis)
-        [_ headers] (scan machine #{} [#{} #{}] (:entry machine))
+  [plan]
+  (let [machine (:machine plan)
+        [reachable headers] (scan machine #{} [#{} #{}] (:entry machine))
         dead (apply dissoc (:blocks machine) reachable)
         header (first headers)]
     (cond
@@ -151,7 +151,7 @@
       (not (every? harmless-block? (vals dead))) {:op :fallback :reason :validation}
       :else
       (let [slots (if header (vec (sort (get (:live machine) header #{}))) [])
-            ctx {:machine machine :header header :slots slots :joins (:joins analysis) :stop nil}
+            ctx {:machine machine :header header :slots slots :joins (:joins plan) :stop nil}
             budget (* 4 (reduce + 0 (map #(inc (count (:ops %))) (vals (select-keys (:blocks machine) reachable)))))
             result (edge ctx false budget (:entry machine))]
         (if (= :built (:op result))
diff --git a/test/com/tailrecursion/blacktide/backend_test.clj b/test/com/tailrecursion/blacktide/backend_test.clj
index 0afcb6a..c5fdd1c 100644
--- a/test/com/tailrecursion/blacktide/backend_test.clj
+++ b/test/com/tailrecursion/blacktide/backend_test.clj
@@ -182,13 +182,17 @@
                   (do (psetq a b b (+ a b)) (setq i (inc i)) (goto again))
                   [a b]))]
     (is (= :structured (:op (capture #'structure/recover form))))
+    (is (some? (capture #'facts/analyze form)) "Structured emission needs numeric value proofs")
     (doseq [mode [:default :dispatcher]]
       (let [events (atom [])]
         (is (= [8 13] ((compile-program form mode) events)))
         (is (= [[0 0 1] [1 1 1] [2 1 2] [3 2 3] [4 3 5] [5 5 8] [6 8 13]] @events)))))
-  (is (= :cycles (:reason (capture #'structure/recover '(loophole [i 0 j 0]
-                                       (while (< i 2) (setq i (inc i)))
-                                       (while (< j 3) (setq j (inc j))) [i j])))))
+  (let [form '(loophole [i 0 j 0]
+                (while (< i 2) (setq i (inc i)))
+                (while (< j 3) (setq j (inc j))) [i j])]
+    (is (= :cycles (:reason (capture #'structure/recover form))))
+    (is (nil? (capture #'facts/analyze form)) "Dispatcher fallback skips unused numeric value proofs")
+    (is (= [2 3] ((compile-program form :default) nil))))
   (let [step '(if (odd? i) (setq x (inc x) y (inc y)) (setq x (+ x 2) y (+ y 2)))
         form (list 'loophole '[i 0 x 0 y 0]
                    (list* 'while '(< i 3) (concat (repeat 8 step) ['(setq i (inc i))])) '[x y])]