BigDecimal call-position arithmetic via :bigdec type (Phase 2)

A direct (+ 1.5M 2.5M) emits a raw Chez + that rejects the bigdec record. Rather
than guard every arithmetic call site (measured 2-4x on unhinted fixnum loops),
let the analyzer dispatch where it can prove the type.

jolt.passes.numeric seeds a :bigdec kind from the M-literal and flows it through
let/loop/if like the existing :double/:long kinds; an arithmetic/comparison invoke
whose operands are all bigdec (integer literals allowed) gets :num-kind :bigdec.
The back end (bd-ops + emit-numeric) lowers those to the bigdec.ss engine
(jbd-add/-sub/-mul/-div, jbd-lt?/…, jbd-zero?/-pos?/-neg?, jbd-quot/-rem).

Zero cost on non-bigdec code: with no bigdec literals present the kind never
arises, so emission is byte-identical — the re-mint leaves prelude.ss unchanged,
only image.ss (the compiler) moves. Gaps (filed): a bigdec mixed with a flonum in
call position, and a bigdec the analyzer types :any, still hit the raw op and
throw; use value position or a literal-typed let.

Re-mint (numeric/backend are seed sources). 16 JVM-certified corpus rows.
This commit is contained in:
Yogthos 2026-06-25 19:49:17 -04:00
parent bd7b75fb5d
commit 6fcc9fa8e6
5 changed files with 240 additions and 180 deletions

View file

@ -94,6 +94,17 @@
"quot" "fxquotient" "rem" "fxremainder" "mod" "fxmodulo"
"<" "fx<?" ">" "fx>?" "<=" "fx<=?" ">=" "fx>=?" "=" "fx=?" "==" "fx=?"})
;; BigDecimal ops. jolt.passes.numeric tags an arithmetic/comparison invoke
;; :num-kind :bigdec when every operand is a bigdec (or an integer literal); these
;; are the bigdec.ss engine procedures it lowers to. Variadic where the source op
;; is; an integer-literal operand is coerced to a bigdec at runtime, so unlike the
;; flonum path no literal rewrite is needed.
(def ^:private bd-ops
{"+" "jbd-add" "-" "jbd-sub" "*" "jbd-mul" "/" "jbd-div"
"quot" "jbd-quot" "rem" "jbd-rem"
"<" "jbd-lt?" ">" "jbd-gt?" "<=" "jbd-le?" ">=" "jbd-ge?"
"zero?" "jbd-zero?" "pos?" "jbd-pos?" "neg?" "jbd-neg?"})
;; PRELUDE MODE. The default (subset) mode rejects any clojure.core ref
;; that isn't a native-op — a clean "out of subset" signal for user-facing `-e`.
;; When emitting clojure.core ITSELF as a prelude, core fns reference each other
@ -461,7 +472,7 @@
(and (= kind :long) (or (= nm "inc") (= nm "unchecked-inc"))) (str "(fx1+ " (first args) ")")
(and (= kind :long) (or (= nm "dec") (= nm "unchecked-dec"))) (str "(fx1- " (first args) ")")
:else
(let [op (if (= kind :double) (dbl-ops nm) (lng-ops nm))]
(let [op (case kind :double (dbl-ops nm) :long (lng-ops nm) :bigdec (bd-ops nm))]
(order-args (fn [as] (str "(" op " " (str/join " " as) ")"))))))
(defn- emit-invoke [node]

View file

@ -49,6 +49,18 @@
(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? #{"+" "-" "*" "/"} 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))
@ -150,11 +162,12 @@
(get fnode :num-ret) [(get fnode :num-ret) node1]
(nil? nm) [nil node1]
:else
(let [;; per-operand class: :double / :long (typed), :wild (integer literal,
;; usable in either), or :no (anything else — blocks specialization).
(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)
@ -163,7 +176,8 @@
(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)]
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.
@ -172,6 +186,11 @@
[(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.
@ -179,6 +198,9 @@
(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)