Arithmetic and comparisons lowered to raw Chez ops, so an operand outside Chez's tower (BigDecimal) crashed with a raw condition, and Chez contagion leaked: (* 1.0 0) gave exact 0 where the JVM gives 0.0, (* ##Inf 0) gave 0 instead of ##NaN, (/ 1 0) raised an untyped error. One seam now (host/chez/seq.ss): call position emits jolt-n* macros with the both-Chez-numbers fast path open-coded; value position folds through the same binary ops. Anything outside the tower falls to per-op slow hooks that java/bigdec.ss extends, so bigdec arithmetic works in every position (the old static-only :bigdec typing limitation is gone). JVM rules patched into the fast path: a double operand wins, an exact zero divisor throws ArithmeticException while a double zero divisor yields Inf/NaN, quot/rem/mod cover ratios and doubles, min/max return the original operand with NaN winning, a nil operand is NPE and a non-number CCE, zero-arg -// throw ArityException at runtime instead of failing expansion. Also: with-precision now binds *math-context* and bigdec results round with real RoundingMode semantics (UNNECESSARY throws; division rounds to precision instead of throwing); rationalize goes through the shortest decimal print like BigDecimal.valueOf (the identity stub is gone); ratios coerce to bigdec like Numbers.toBigDecimal; min/max int-literal operands no longer coerce to flonum in the numeric pass. Perf neutral: fib and seq benches unchanged (the fast path is two type checks the optimizer folds); hinted fl/fx paths untouched. 19 JVM-certified corpus rows; cts baseline 5614->5730 pass, 192->88 errors, 84->79 baselined namespaces.
275 lines
14 KiB
Clojure
275 lines
14 KiB
Clojure
(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 <int-literal>)? 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)
|
|
;; min/max return the ORIGINAL operand (Numbers.min: an integer
|
|
;; literal stays exact), so an int-literal operand blocks the
|
|
;; flonum lowering there — flmin would coerce it.
|
|
(or (not (contains? #{"min" "max"} nm))
|
|
(every? (fn [c] (= c :double)) cls)))
|
|
;; 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))
|