Numeric return-type hints: ^double/^long on a defn (round 3)

A ^double/^long return hint on a fn's name now (a) coerces the fn's value on the
way out — exact->inexact / jolt->fx, like a JVM primitive return — and (b) types a
call to it, so an accumulator over the result specializes:

  (defn ^double work [^double x ^double y] (+ (* x x) (* y y)))
  (loop [acc 0.0] (recur (+ acc (work a b))))   ; (+ acc (work ..)) -> fl+

The analyzer pushes the name's numeric tag onto each arity (:ret-nhint) for the
back-end coercion, and resolve-global surfaces the callee's declared return
(:num-ret, read from var meta) onto the :var node so jolt.passes.numeric types the
call. defn carries the name hint through.

This unblocks the accumulator-over-fn-result pattern that round 2 had to demote.
The win is bounded by call overhead in an open/dispatched build (~1.15x on a hot
loop whose body is a helper call); it compounds with direct-linking and, later,
inlining. A numeric return hint is a contract, like ^long — redefining the var to
return another type in an open build breaks it.

Not yet: per-arity arglist return hints, (defn f (^double [x] ..)). Gate:
test/chez/numeric-test.ss 39/39; full make test green, 0 new corpus divergences.
This commit is contained in:
Yogthos 2026-06-23 17:21:53 -04:00
parent 0db417a491
commit 5a9acd3cf4
6 changed files with 227 additions and 164 deletions

View file

@ -80,15 +80,26 @@
(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)]
;; A ^long / ^double tag -> :long / :double, else nil. The tag is a string on the
;; data reader; tolerate a symbol from macroexpansion too.
(defn- tag->nkind [t]
(let [s (cond (form-sym? t) (form-sym-name t) (string? t) t :else nil)]
(cond (= s "double") :double (= s "long") :long :else nil)))
;; A primitive numeric hint (^long / ^double) on a binding symbol. Drives the
;; fl*/fx* fast path (jolt.passes.numeric).
(defn- nhint-of [ctx sym]
(let [m (form-sym-meta sym)] (when m (tag->nkind (get m :tag)))))
;; Push a numeric return hint (from ^double/^long on a defn's name) onto each arity
;; of its fn, so the back end coerces the body's value to that kind on return —
;; making the hint a contract a caller's arithmetic can trust.
(defn- with-ret-nhint [node kind]
(if (and kind (= :fn (:op node)))
(assoc node :arities (mapv (fn [a] (if (:ret-nhint a) a (assoc a :ret-nhint kind)))
(:arities node)))
node))
(defn- analyze-seq [ctx forms env]
(let [v (mapv #(analyze ctx % env) forms)
n (count v)]
@ -304,7 +315,8 @@
base0)
node-meta (if has-doc (assoc base-meta :doc (nth items 2)) base-meta)]
(host-intern! ctx cur nm)
(def-node cur nm (analyze ctx val-form env) node-meta)))))
;; a ^double/^long return hint on the name applies to all arities of the fn.
(def-node cur nm (with-ret-nhint (analyze ctx val-form env) (tag->nkind tag)) node-meta)))))
;; (set! (.-field obj) v) mutates a deftype instance field in place; (set! *var* v)
;; sets the var's innermost thread binding, else its root. A local target (jolt
@ -493,14 +505,16 @@
(let [h (get (:hints env) nm)] (if h (assoc (local nm) :hint h) (local nm)))
ns (let [r (resolve-global ctx form)]
(if (= :var (:kind r))
(var-ref (:ns r) (:name r))
(cond-> (var-ref (:ns r) (:name r)) (:num-ret r) (assoc :num-ret (:num-ret r)))
;; A non-var qualified ref `Class/member` is a host class static
;; (Math/sqrt, Long/MAX_VALUE, System/getenv). The Chez back end
;; lowers it to a runtime static dispatch.
(host-static ns nm)))
:else (let [r (resolve-global ctx form)]
(case (:kind r)
:var (var-ref (:ns r) (:name r))
;; :num-ret (a ^double/^long declared return) rides on the var node so
;; jolt.passes.numeric types a call to it (an accumulator over the result).
:var (cond-> (var-ref (:ns r) (:name r)) (:num-ret r) (assoc :num-ret (:num-ret r)))
:host (host-ref (:name r))
;; :unresolved — emitting a var-ref here would auto-intern an
;; UNBOUND var, so a typo'd symbol would die later as 'Cannot call