(ns jolt.passes.numeric "Hint-directed numeric specialization. A local forward type-flow that seeds local kinds from `^double`/`^long` fn-param hints and float literals, propagates them through let inits, arithmetic results, and if/do, and tags an arithmetic `:invoke` node with `:num-kind :double` or `:long` when every operand is that kind (an integer literal is a wildcard, valid in either). The back end then emits Chez `fl*`/`fx*` ops instead of generic arithmetic. Soundness: `:long` is seeded ONLY from an explicit `^long` hint — never a bare integer literal — so un-hinted integer code keeps jolt's arbitrary-precision numbers (no fixnum overflow surprise). `:double` is seeded from `^double` hints and float literals; flonum arithmetic is always flonum, so this matches the generic result. A `^long` hint is a promise the value is a fixnum: `fx+` raises on overflow rather than promoting, exactly as a JVM primitive long is fixed-width. Runs in every build and at `-e`/repl, but not the seed mint (which compiles with the passes off), so it stays out of the self-host fixpoint and benefits open and closed builds alike." (:require [jolt.ir :refer [map-ir-children]])) ;; --- operand classification ------------------------------------------------- (defn- int-lit? [n] (and (= :const (get n :op)) (let [v (get n :val)] (and (number? v) (integer? v))))) (defn- float-lit? [n] (and (= :const (get n :op)) (let [v (get n :val)] (and (number? v) (float? v))))) ;; result kind of a double-specialized op at this name/arity, or nil if N/A. ;; arithmetic -> :double; comparison -> :bool (operands specialized, result not numeric). ;; Every op name dbl-spec / lng-spec returns non-nil for must have a Chez op in ;; jolt.backend-scheme/dbl-ops resp. lng-ops, or emit-numeric splices a nil op. (defn- dbl-spec [nm n] (cond (and (>= n 1) (contains? #{"+" "-" "*" "/" "min" "max"} nm)) :double (and (= n 1) (contains? #{"inc" "dec"} nm)) :double (and (>= n 2) (contains? #{"<" ">" "<=" ">=" "=" "=="} nm)) :bool :else nil)) ;; result kind of a long-specialized op, or nil. `/` is absent on purpose: ;; (/ long long) is a Ratio in Clojure, not a long. unchecked-* join the fast path ;; (they aren't native ops otherwise). (defn- lng-spec [nm n] (cond (and (>= n 1) (contains? #{"+" "-" "*" "min" "max" "unchecked-add" "unchecked-subtract" "unchecked-multiply"} nm)) :long (and (= n 1) (contains? #{"inc" "dec" "unchecked-inc" "unchecked-dec"} nm)) :long (and (= n 2) (contains? #{"quot" "rem" "mod"} nm)) :long (and (>= n 2) (contains? #{"<" ">" "<=" ">=" "=" "=="} nm)) :bool :else nil)) ;; result kind of a bigdec-specialized op, or nil. Arithmetic / quot / rem yield a ;; bigdec; the comparisons and zero?/pos?/neg? yield a bool. `=` is left to the ;; generic jolt= (already bigdec-aware), and `/` can throw (non-terminating) but is ;; still a bigdec op. Each non-nil name must have an entry in backend bd-ops. (defn- bd-spec [nm n] (cond (and (>= n 1) (contains? #{"+" "-" "*" "/" "min" "max"} nm)) :bigdec (and (= n 2) (contains? #{"quot" "rem"} nm)) :bigdec (and (= n 1) (contains? #{"zero?" "pos?" "neg?"} nm)) :bool (and (>= n 2) (contains? #{"<" ">" "<=" ">="} nm)) :bool :else nil)) ;; A non-numeric result (a comparison) doesn't propagate a numeric kind. (defn- propagate [spec] (if (= spec :bool) nil spec)) (declare an) ;; The recur-arg kinds for the recurs targeting THIS loop level. recur only appears ;; in tail position (an if branch, a do's ret, a let body), so descend only those; ;; a nested loop/fn (and any non-tail child) owns its own recur and is skipped. (defn- recur-kinds [node tenv] (let [op (get node :op)] (cond (= op :recur) [(mapv (fn [a] (nth (an a tenv) 0)) (get node :args))] (= op :let) (recur-kinds (get node :body) (reduce (fn [te b] (assoc te (nth b 0) (nth (an (nth b 1) te) 0))) tenv (get node :bindings))) (= op :if) (concat (recur-kinds (get node :then) tenv) (recur-kinds (get node :else) tenv)) (= op :do) (recur-kinds (get node :ret) tenv) :else []))) ;; The recur-arg NODE lists for the recurs at THIS loop level (structural, no env), ;; parallel to recur-kinds. Used to recognise a counter. (defn- recur-arg-lists [node] (let [op (get node :op)] (cond (= op :recur) [(get node :args)] (= op :let) (recur-arg-lists (get node :body)) (= op :if) (concat (recur-arg-lists (get node :then)) (recur-arg-lists (get node :else))) (= op :do) (recur-arg-lists (get node :ret)) :else []))) ;; Is `arg` an increment-style step of loop var `vname`: the var unchanged, or ;; inc/dec/unchecked-inc/dec, or (+/- var )? Bounded growth that a ;; fixnum-range counter can sustain for any realistic loop — unlike (* acc x), which ;; overflows fast, so a multiplicative accumulator never qualifies and stays ;; arbitrary-precision. (defn- counter-step? [arg vname] (cond (and (= :local (get arg :op)) (= vname (get arg :name))) true (= :invoke (get arg :op)) (let [f (get arg :fn) as (get arg :args)] (and (= :var (get f :op)) (= "clojure.core" (get f :ns)) (let [nm (get f :name) v? (fn [n] (and (= :local (get n :op)) (= vname (get n :name))))] (cond (and (contains? #{"inc" "dec" "unchecked-inc" "unchecked-dec"} nm) (= 1 (count as))) (v? (nth as 0)) (and (contains? #{"+" "unchecked-add"} nm) (= 2 (count as))) (or (and (v? (nth as 0)) (int-lit? (nth as 1))) (and (v? (nth as 1)) (int-lit? (nth as 0)))) (and (contains? #{"-" "unchecked-subtract"} nm) (= 2 (count as))) (and (v? (nth as 0)) (int-lit? (nth as 1))) :else false)))) :else false)) ;; Loop-var kinds by bounded fixpoint. A var keeps its init kind (:double or :long) ;; only if every recur arg in that slot is the same kind (under the current ;; assumption) — a monotone demotion that stops at a fixpoint, bounded by the var ;; count. An integer-literal init has kind nil and stays generic, so a bignum loop ;; keeps arbitrary precision (no :long from a bare literal). A typed loop var's init ;; and recur args are all flonums/fixnums (a :long init flows from a coerced ^long ;; value or an fx op), so no entry coercion is needed here, unlike a fn param. (defn- loop-kinds [names seed body tenv] (loop [cur seed iter 0] (if (> iter (count names)) cur (let [te (reduce (fn [t i] (assoc t (nth names i) (nth cur i))) tenv (range (count names))) rks (recur-kinds body te) nxt (mapv (fn [j] (let [k (nth cur j)] (if (and k (every? (fn [rk] (= k (nth rk j))) rks)) k nil))) (range (count names)))] (if (= nxt cur) cur (recur nxt (inc iter))))))) ;; Seed a fn arity's local env from its numeric param hints; an unhinted param ;; shadows any same-named outer local to nil. (defn- arity-env [tenv a] (let [nh (into {} (get a :nhints)) pe (reduce (fn [e p] (assoc e p (get nh p))) tenv (get a :params))] (if (get a :rest) (assoc pe (get a :rest) nil) pe))) (defn- an-invoke "Annotate an :invoke with its numeric kind. An arithmetic core op specializes to the Chez fl*/fx* op only when every operand is the same kind (:double or :long), except an integer literal is :wild — valid in either — so (+ ^double x 2) stays double. A call to a ^double/^long-returning var yields that kind without lowering the call (its body already coerces the return)." [node tenv] (let [fnode (get node :fn) nm (when (and (= :var (get fnode :op)) (= "clojure.core" (get fnode :ns))) (get fnode :name)) ars (mapv (fn [a] (an a tenv)) (get node :args)) argnodes (mapv (fn [r] (nth r 1)) ars) node1 (assoc node :args argnodes) n (count ars)] (cond ;; a field read the structural inference proved is a flonum (a ^double record ;; field) is a :double operand — so (* (:x v) (:x v)) unboxes. The read itself ;; isn't lowered here; it keeps its keyword/jrec-field-at emit. (= :double (get node :num-read)) [:double node1] ;; a call to a var with a declared numeric return (^double/^long) yields that ;; kind, so an accumulator over the result types. The call itself isn't an ;; arithmetic op to lower — its body already coerces the return. (get fnode :num-ret) [(get fnode :num-ret) node1] (nil? nm) [nil node1] :else (let [;; per-operand class: :double / :long / :bigdec (typed), :wild (integer ;; literal, usable in any), or :no (anything else — blocks specialization). cls (mapv (fn [r] (let [k (nth r 0) nd (nth r 1)] (cond (= k :double) :double (= k :long) :long (= k :bigdec) :bigdec (int-lit? nd) :wild :else :no))) ars) ok? (fn [allowed need] (and (pos? n) (every? (fn [c] (or (= c :wild) (= c allowed))) cls) (some (fn [c] (= c need)) cls))) ds (dbl-spec nm n) ls (lng-spec nm n) bs (bd-spec nm n)] (cond (and ds (ok? :double :double)) ;; coerce integer-literal operands to flonum so fl-ops never see an exact int. (let [args' (mapv (fn [nd] (if (int-lit? nd) (assoc nd :val (double (get nd :val))) nd)) argnodes)] [(propagate ds) (assoc node1 :args args' :num-kind :double)]) (and ls (ok? :long :long)) [(propagate ls) (assoc node1 :num-kind :long)] ;; bigdec: every operand a bigdec (integer literals allowed, coerced at ;; runtime). A flonum operand blocks this (double contagion) and falls ;; through to the generic op. (and bs (ok? :bigdec :bigdec)) [(propagate bs) (assoc node1 :num-kind :bigdec)] :else [nil node1]))))) ;; Returns [kind node'] — kind is :double, :long, or nil. (defn- an [node tenv] (let [op (get node :op)] (cond (= op :const) [(if (float-lit? node) :double nil) node] ;; a bigdec (M) literal seeds the :bigdec kind so call-position arithmetic ;; over it (and let-bound copies of it) dispatches to the bigdec engine. (= op :bigdec) [:bigdec node] (= op :local) [(get tenv (get node :name)) node] (= op :coerce) [(get node :kind) (assoc node :expr (nth (an (get node :expr) tenv) 1))] (= op :invoke) (an-invoke node tenv) (= op :let) (let [res (reduce (fn [acc b] (let [te (nth acc 0) binds (nth acc 1) ir (an (nth b 1) te)] [(assoc te (nth b 0) (nth ir 0)) (conj binds [(nth b 0) (nth ir 1)])])) [tenv []] (get node :bindings)) br (an (get node :body) (nth res 0))] [(nth br 0) (assoc node :bindings (nth res 1) :body (nth br 1))]) (= op :loop) ;; inits evaluate in the OUTER env; loop vars get their fixpoint kinds for the body. (let [binds (get node :bindings) names (mapv (fn [b] (nth b 0)) binds) ik (mapv (fn [b] (nth (an (nth b 1) tenv) 0)) binds) rlists (recur-arg-lists (get node :body)) ;; seed each var: an already-typed init keeps its kind; an integer-literal ;; init whose recur args are all counter steps is a fixnum counter (:long). seed (mapv (fn [j] (let [k (nth ik j) b (nth binds j)] (cond k k ;; an int-literal var is a fixnum counter only in a real ;; iterating loop (>= 1 recur) whose every step is bounded. ;; A recur-less loop is a let — its int literal stays ;; generic (arbitrary precision), like a let binding. (and (seq rlists) (int-lit? (nth b 1)) (every? (fn [args] (counter-step? (nth args j) (nth b 0))) rlists)) :long :else nil))) (range (count names))) lk (loop-kinds names seed (get node :body) tenv) te (reduce (fn [t i] (assoc t (nth names i) (nth lk i))) tenv (range (count names)))] [nil (assoc node :bindings (mapv (fn [b] [(nth b 0) (nth (an (nth b 1) tenv) 1)]) binds) :body (nth (an (get node :body) te) 1))]) (= op :if) (let [tr (an (get node :test) tenv) thn (an (get node :then) tenv) els (an (get node :else) tenv) tk (nth thn 0) ek (nth els 0)] [(if (= tk ek) tk nil) (assoc node :test (nth tr 1) :then (nth thn 1) :else (nth els 1))]) (= op :do) (let [stmts (mapv (fn [s] (nth (an s tenv) 1)) (get node :statements)) r (an (get node :ret) tenv)] [(nth r 0) (assoc node :statements stmts :ret (nth r 1))]) (= op :fn) [nil (assoc node :arities (mapv (fn [a] (assoc a :body (nth (an (get a :body) (arity-env tenv a)) 1))) (get node :arities)))] (= op :def) [nil (assoc node :init (nth (an (get node :init) tenv) 1))] ;; every other op introduces no bindings and isn't numeric: descend with the ;; same env to specialize nested arithmetic, no kind. :else [nil (map-ir-children (fn [c] (nth (an c tenv) 1)) node)]))) (defn annotate "Tag arithmetic nodes with :num-kind from local numeric type-flow. Returns the rewritten IR (no kind escapes to the caller)." [node] (nth (an node {}) 1))