Hint-directed fast arithmetic: fl*/fx* from ^double/^long (round 1)

A ^double/^long param hint (or a float literal) now drives Chez flonum/fixnum
ops instead of generic arithmetic — JVM-style primitive hints, available in every
build and at -e (not gated on direct-linking or whole-program inference).

New pass jolt.passes.numeric: a local forward type-flow seeded from ^double/^long
fn-param hints (analyzer attaches :nhints per arity) and float literals,
propagated through let inits / arithmetic / if / do. It tags an arithmetic invoke
:num-kind :double|:long when every operand is that kind (an integer literal is a
wildcard, coerced to a flonum in a double op). The back end lowers a tagged node
to fl+/fl-/fl*/fl//fl<?/... or fx+/fx*/fx1+/fxquotient/... (unchecked-add etc.
join the fixnum path; == too). Runs last in run-passes, both branches.

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, no corpus divergence. :double comes from ^double hints
and float literals (flonum arithmetic is always flonum, matching the generic
result). A ^long hint is a promise the value is a fixnum: fx+ raises on overflow,
like a JVM fixed-width long.

Numeric-hinted params coerce at fn entry (exact->inexact / jolt->fx), the way the
JVM coerces a primitive parameter — so the body's fl*/fx* ops can rely on the
type even when a caller passes an exact int (e.g. Chez's (* 0 1.0) => exact 0).

Round 1 specializes hinted straight-line / fn-body arithmetic. fl-ops are ~4x
generic in a tight Chez loop, but realizing that on loop-carried accumulators
needs loop-var typing — round 2. Sound foundation, gated by test/chez/numeric-test.ss.
This commit is contained in:
Yogthos 2026-06-23 16:43:55 -04:00
parent 2c18fcdc61
commit 59905a71fd
9 changed files with 567 additions and 236 deletions

View file

@ -80,6 +80,15 @@
(let [m (form-sym-meta sym)]
(when m (let [t (get m :tag)] (when t (record-ctor-key ctx t))))))
;; A primitive numeric hint (^long / ^double) on a binding symbol -> :long /
;; :double, else nil. Drives the fl*/fx* fast path (jolt.passes.numeric). The tag
;; is a string on the data reader; tolerate a symbol from macroexpansion too.
(defn- nhint-of [ctx sym]
(let [m (form-sym-meta sym)
t (when m (get m :tag))
s (cond (form-sym? t) (form-sym-name t) (string? t) t :else nil)]
(cond (= s "double") :double (= s "long") :long :else nil)))
(defn- analyze-seq [ctx forms env]
(let [v (mapv #(analyze ctx % env) forms)
n (count v)]
@ -104,19 +113,20 @@
;; folds it with a plain reduce — no reduce-over-map in the kernel subset).
;; :phints is the parallel vector of [name ctor-key] for record param hints,
;; carrying the specific type for the inference to seed.
(loop [i 0 fixed [] rest-name nil hints [] phints []]
(loop [i 0 fixed [] rest-name nil hints [] phints [] nhints []]
(if (< i (count pvec))
(let [p (nth pvec i)]
(when-not (form-sym? p) (uncompilable "destructuring fn param"))
(if (= "&" (form-sym-name p))
(let [r (nth pvec (inc i))]
(when-not (form-sym? r) (uncompilable "destructuring fn rest"))
(recur (+ i 2) fixed (form-sym-name r) hints phints))
(let [nm (form-sym-name p) h (hint-of ctx p) ph (phint-of ctx p)]
(recur (+ i 2) fixed (form-sym-name r) hints phints nhints))
(let [nm (form-sym-name p) h (hint-of ctx p) ph (phint-of ctx p) nh (nhint-of ctx p)]
(recur (inc i) (conj fixed nm) rest-name
(if h (conj hints [nm h]) hints)
(if ph (conj phints [nm ph]) phints)))))
{:fixed fixed :rest rest-name :hints hints :phints phints})))
(if ph (conj phints [nm ph]) phints)
(if nh (conj nhints [nm nh]) nhints)))))
{:fixed fixed :rest rest-name :hints hints :phints phints :nhints nhints})))
;; Clojure lets a later param shadow an earlier same-named one (a macro expander
;; uses _ for both its &form and &env slots, so its param list is (_ _ …)); the
@ -154,7 +164,9 @@
:body (analyze-seq ctx body env*)}
;; carry record param hints (name -> ctor-key) for the inference to seed
;; the param type; only when present so a hintless arity stays a struct.
arity (if (seq (:phints pp)) (assoc arity :phints (:phints pp)) arity)]
arity (if (seq (:phints pp)) (assoc arity :phints (:phints pp)) arity)
;; numeric param hints (name -> :long/:double) for jolt.passes.numeric.
arity (if (seq (:nhints pp)) (assoc arity :nhints (:nhints pp)) arity)]
;; :rest only when variadic — an absent :rest reads back nil, same as before,
;; but keeps a fixed arity a nil-free struct rather than a phm.
(if rst (assoc arity :rest rst) arity)))