The closed-world fixpoint (#226) flowed record types across fn boundaries; this adds a numeric refinement so a hintless fn whose every call site passes a flonum has its param unboxed to fl-ops, no ^double hint needed. Lattice gains :double, a flonum refinement of :num: two doubles join to :double, a double joined with anything else widens to :num — so a param is :double only when every contributing value is a flonum, which is what makes the fl-op sound. infer types a flonum literal and flonum arithmetic (+ - * / min max inc dec over double/int-literal operands) as :double, and the fixpoint joins those across call sites and return types like any other lattice value. The bridge to the existing hint-directed pass is a synthetic [param :double] nhint: wp-infer! stashes the :double params separately from the structural seeds, and run-passes injects them as nhints before numeric/annotate, so the fl-op emission and the exact->inexact entry coercion (a no-op on a proven flonum) apply unchanged. Sound subset only: :double, never :long — an untyped integer can be a bignum and fx-ops would overflow/diverge from jolt's arbitrary precision. So an integer caller leaves a param generic; an escaped fn (unknown callers) keeps :any. run-numwp.ss gate: cross-fn :double propagation incl. through a flonum-returning helper, the integer-caller and escape negatives, and the full run-passes path emitting fl* + entry coercion. make test / shakesmoke green, selfhost holds, 0 new divergences. Co-authored-by: Yogthos <yogthos@gmail.com>
97 lines
5.6 KiB
Clojure
97 lines
5.6 KiB
Clojure
(ns jolt.passes
|
|
"IR optimization passes (nanopass-lite) + the inference/checking
|
|
driver. Façade over three weakly-coupled namespaces, loaded with the compiler:
|
|
|
|
jolt.passes.fold — const-fold (always-on) + the shared const-shape predicate.
|
|
jolt.passes.inline — inline + flatten-lets + scalar-replace (direct-link only).
|
|
jolt.passes.types — collection-type inference + success-type checking
|
|
(RFC 0006) + the inter-procedural driver API.
|
|
|
|
run-passes (below) is the single entry the back end applies to every analyzed
|
|
form. The driver/checker fns the back end looks up by name (check-form,
|
|
infer-body, reinfer-def, set-rtenv!, take-diags!, …) are re-exported here via
|
|
:refer, so jolt.passes stays the only namespace the back end imports.
|
|
|
|
Portable Clojure: kernel-tier fns + seed primitives only."
|
|
(:require [jolt.host :refer [inline-enabled? record-shapes protocol-methods stash-inline!]]
|
|
[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
|
|
set-rtenv! set-vtypes! join-types
|
|
set-record-shapes! set-map-shapes! set-protocol-methods!
|
|
reset-escapes! collected-escapes
|
|
wp-infer! param-seeds-for param-num-seeds-for
|
|
set-check-mode! take-diags!]]))
|
|
|
|
;; Cap on inline -> flatten -> scalar-replace -> const-fold iterations. Each pass
|
|
;; sets `dirty` when it rewrote something; the loop stops at a clean pass or here.
|
|
(def ^:private inline-fixpoint-cap 8)
|
|
|
|
;; A top-level defn the inline pass may splice: a single fixed arity (no rest). The
|
|
;; pass itself checks body size + closedness, so any such fn is stashable.
|
|
(defn- inline-eligible? [node]
|
|
(and (= :def (:op node)) (:init node) (= :fn (:op (:init node)))
|
|
(= 1 (count (:arities (:init node))))
|
|
(not (:rest (first (:arities (:init node)))))))
|
|
|
|
(defn- stash-of [node]
|
|
(let [a (first (:arities (:init node)))]
|
|
{:params (:params a) :body (:body a) :nhints (:nhints a) :ret (:ret-nhint a)}))
|
|
|
|
(defn inject-wp-nhints
|
|
"Merge the whole-program :double param seeds into a def's arity :nhints as
|
|
synthetic ^double hints, so the numeric pass unboxes a hintless fn whose callers
|
|
all pass flonums (the entry coercion exact->inexact is a no-op on a proven
|
|
flonum). Only un-hinted params are added — an explicit hint wins. A no-op unless
|
|
the closed-world fixpoint typed a param :double (param-num-seeds-for)."
|
|
[node]
|
|
(let [seeds (when (= :def (:op node)) (param-num-seeds-for (str (:ns node) "/" (:name node))))
|
|
f (:init node)]
|
|
(if (and seeds (= :fn (:op f)) (= 1 (count (:arities f))))
|
|
(let [a (first (:arities f))
|
|
have (into #{} (map first (:nhints a)))
|
|
add (for [[p k] seeds :when (not (have p))] [p k])]
|
|
(assoc node :init (assoc f :arities [(assoc a :nhints (vec (concat (:nhints a) add)))])))
|
|
node)))
|
|
|
|
(defn run-passes
|
|
"All passes, in order. The back end applies this to every analyzed form. When
|
|
inlining is enabled for the unit (user code under direct-linking),
|
|
run inline + flatten + scalar-replace + const-fold to a capped fixpoint —
|
|
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.
|
|
|
|
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]
|
|
;; stash an inline-eligible defn so later call sites can splice it (closed-world
|
|
;; optimization only). Done before optimizing, from the analyzed node.
|
|
(when (and (inline-enabled? ctx) (inline-eligible? node))
|
|
(stash-inline! ctx (:ns node) (:name node) (stash-of 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))
|
|
_ (set-protocol-methods! (protocol-methods ctx)) ;; devirtualization
|
|
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 top-level def whose params the whole-program fixpoint typed gets
|
|
;; reinferred with those seeds (record types flow in from its callers);
|
|
;; everything else takes the ordinary per-form inference.
|
|
seeds (when (= :def (:op opt)) (param-seeds-for (str (:ns opt) "/" (:name opt))))]
|
|
;; a final const-fold after inference propagates any predicate folded to a
|
|
;; constant, collapsing the `if` it gates to the taken branch; then inject
|
|
;; any whole-program :double param hints for the numeric pass that follows.
|
|
(inject-wp-nhints (const-fold (if seeds (reinfer-def opt seeds) (run-inference opt)))))
|
|
(const-fold node))))
|