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

@ -15,6 +15,7 @@
Portable Clojure: kernel-tier fns + seed primitives only."
(:require [jolt.host :refer [inline-enabled? record-shapes]]
[jolt.passes.fold :refer [const-fold]]
[jolt.passes.numeric :as numeric]
[jolt.passes.inline :refer [inline-node flatten-lets scalar-replace dirty set-rec-shapes!]]
[jolt.passes.types :refer [run-inference
check-form infer-body reinfer-def phint-seed
@ -34,21 +35,25 @@
inlining exposes map literals to lookups, scalar-replace collapses them, which
may expose more then a collection-type inference pass (optionally
also emitting success diagnostics) that auto-drops the lookup guard where the
type is proven. Otherwise (core + bootstrap) just const-fold, as before."
type is proven. Otherwise (core + bootstrap) just const-fold, as before.
numeric/annotate runs last in both branches (hint-directed fl*/fx* arithmetic);
it benefits open builds too, so it is not gated on inlining."
[node ctx]
(if (inline-enabled? ctx)
(let [_ (set-rec-shapes! (record-shapes ctx)) ;; record ctor fold
;; resolve ^Record param hints (incl. defrecord/extend-type method
;; `this`) to bare field reads per-form, not only under whole-program.
;; Same shapes the inline pass uses.
_ (set-record-shapes! (record-shapes ctx))
opt (loop [i 0 n (const-fold node)]
(reset! dirty false)
(let [n2 (const-fold (scalar-replace (flatten-lets (inline-node n ctx))))]
(if (and @dirty (< i inline-fixpoint-cap))
(recur (inc i) n2)
n2)))]
;; a final const-fold after inference propagates any predicate folded to a
;; constant, collapsing the `if` it gates to the taken branch.
(const-fold (run-inference opt)))
(const-fold node)))
(numeric/annotate
(if (inline-enabled? ctx)
(let [_ (set-rec-shapes! (record-shapes ctx)) ;; record ctor fold
;; resolve ^Record param hints (incl. defrecord/extend-type method
;; `this`) to bare field reads per-form, not only under whole-program.
;; Same shapes the inline pass uses.
_ (set-record-shapes! (record-shapes ctx))
opt (loop [i 0 n (const-fold node)]
(reset! dirty false)
(let [n2 (const-fold (scalar-replace (flatten-lets (inline-node n ctx))))]
(if (and @dirty (< i inline-fixpoint-cap))
(recur (inc i) n2)
n2)))]
;; a final const-fold after inference propagates any predicate folded to a
;; constant, collapsing the `if` it gates to the taken branch.
(const-fold (run-inference opt)))
(const-fold node))))