diff --git a/jolt-core/jolt/analyzer.clj b/jolt-core/jolt/analyzer.clj index 8840292..7d2a91c 100644 --- a/jolt-core/jolt/analyzer.clj +++ b/jolt-core/jolt/analyzer.clj @@ -39,11 +39,21 @@ (swap! gensym-counter inc) (str "_r$" prefix n))) -(defn- empty-env [] {:locals #{}}) +(defn- empty-env [] {:locals #{} :hints {}}) (defn- local? [env nm] (contains? (:locals env) nm)) (defn- add-locals [env names] (update env :locals #(reduce conj % names))) (defn- with-recur [env name] (assoc env :recur name)) +;; Type hints (jolt-dad). The reader keeps ^hint metadata on the binding symbol; +;; we recognize ^:struct, which asserts the value is a plain struct/record map so +;; a constant-keyword lookup can skip the :jolt/type guard and emit a bare get. +;; Other hints parse and are ignored, as before. +(defn- hint-of [sym] + (let [m (form-sym-meta sym)] + (when (and m (get m :struct)) :struct))) +(defn- add-hint [env nm h] + (if h (assoc env :hints (assoc (:hints env) nm h)) env)) + (defn- analyze-seq [ctx forms env] (let [v (mapv #(analyze ctx % env) forms) n (count v)] @@ -59,20 +69,25 @@ (when-not (form-sym? bsym) (uncompilable "destructuring binding")) (let [nm (form-sym-name bsym) init (analyze ctx (nth bvec (inc i)) env)] - (recur (+ i 2) (add-locals env [nm]) (conj pairs [nm init])))) + (recur (+ i 2) (add-hint (add-locals env [nm]) nm (hint-of bsym)) + (conj pairs [nm init])))) [pairs env]))) (defn- parse-params [pvec] - (loop [i 0 fixed [] rest-name nil] + ;; :hints is a vector of [name hint] pairs (vector, not a map, so the caller + ;; folds it with a plain reduce — no reduce-over-map in the kernel subset). + (loop [i 0 fixed [] rest-name nil hints []] (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))) - (recur (inc i) (conj fixed (form-sym-name p)) rest-name))) - {:fixed fixed :rest rest-name}))) + (recur (+ i 2) fixed (form-sym-name r) hints)) + (let [nm (form-sym-name p) h (hint-of p)] + (recur (inc i) (conj fixed nm) rest-name + (if h (conj hints [nm h]) hints))))) + {:fixed fixed :rest rest-name :hints hints}))) (defn- analyze-arity [ctx pvec body env fn-name] (let [pp (parse-params (vec (form-vec-items pvec))) @@ -88,7 +103,8 @@ ;; keeps recur targets unique per compilation unit. rname (gen-name (str (compile-ns ctx) "/" (or fn-name "fn") "--")) names (cond-> (vec fixed) rst (conj rst) fn-name (conj fn-name)) - env* (-> (add-locals env names) (with-recur rname)) + env0 (-> (add-locals env names) (with-recur rname)) + env* (reduce (fn [e pr] (add-hint e (nth pr 0) (nth pr 1))) env0 (:hints pp)) arity {:params fixed :recur-name rname :body (analyze-seq ctx body env*)}] ;; :rest only when variadic — an absent :rest reads back nil, same as before, @@ -190,7 +206,8 @@ (defn- analyze-symbol [ctx form env] (let [nm (form-sym-name form) ns (form-sym-ns form)] (cond - (and (nil? ns) (local? env nm)) (local nm) + (and (nil? ns) (local? env nm)) + (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)) diff --git a/jolt-core/jolt/passes.clj b/jolt-core/jolt/passes.clj index 1173088..76a7656 100644 --- a/jolt-core/jolt/passes.clj +++ b/jolt-core/jolt/passes.clj @@ -178,7 +178,15 @@ (let [op (get node :op)] (cond (= op :local) (let [r (get env (get node :name))] - (if r r node)) + ;; carry the param's ^:struct hint onto a let-bound fresh + ;; local, so lookups inside the inlined body keep the bare + ;; (no-guard) path (jolt-dad). The param hint asserts the + ;; arg is a struct; inlining doesn't change that contract. + (if r + (if (and (= :local (get r :op)) (get node :hint) (not (get r :hint))) + (assoc r :hint (get node :hint)) + r) + node)) (= op :if) (assoc node :test (subst (get node :test) env) :then (subst (get node :then) env) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index ab2bd62..a2b9aae 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -331,6 +331,13 @@ (>= 2 (length args) 1)) (let [k (fnode :val) m-expr (in args 0) + # ^:struct hint (jolt-dad): the subject IR local asserts a plain + # struct/record map, so skip the :jolt/type guard entirely and emit a + # bare get (~20ns vs ~36ns guarded). Programmer-asserted, like a + # Clojure type hint — a lie just makes the raw get return the wrong + # thing, same contract as ^String. Only for a :local subject. + subj (norm-node (in (vview (node :args)) 0)) + hinted (and (= :local (subj :op)) (= :struct (subj :hint))) # when the subject is already a janet symbol (a local), read it # directly — the guard + lookup both reference it, and locals are # immutable reads, so no rebinding let is needed (saves a binding @@ -338,13 +345,13 @@ m (if (symbol? m-expr) m-expr (jsym)) wrap (fn [body] (if (symbol? m-expr) body ['let [m m-expr] body]))] (if (= 1 (length args)) - (wrap ['if ['get m :jolt/type] (tuple core-get m k) ['get m k]]) + (let [fast ['get m k]] + (wrap (if hinted fast ['if ['get m :jolt/type] (tuple core-get m k) fast]))) (let [d-expr (in args 1) d (if (symbol? d-expr) d-expr (jsym)) v (jsym) - body ['if ['get m :jolt/type] - (tuple core-get m k d) - ['let [v ['get m k]] ['if ['nil? v] d v]]] + fast ['let [v ['get m k]] ['if ['nil? v] d v]] + body (if hinted fast ['if ['get m :jolt/type] (tuple core-get m k d) fast]) body (if (symbol? d-expr) body ['let [d d-expr] body])] (wrap body)))) (direct-call? ctx fnode) (tuple (emit ctx fnode) ;args) diff --git a/test/integration/struct-hint-test.janet b/test/integration/struct-hint-test.janet new file mode 100644 index 0000000..bf433ce --- /dev/null +++ b/test/integration/struct-hint-test.janet @@ -0,0 +1,44 @@ +# ^:struct type hint (jolt-dad). A constant-keyword lookup on a local hinted +# ^:struct skips the :jolt/type guard and emits a bare get (~20ns vs ~36ns), +# the way Clojure type hints let the compiler specialize. The hint is a +# programmer assertion (a lie just makes the raw get return the wrong thing, +# same contract as ^String); these tests pin that an ACCURATE hint is +# correctness-preserving, that it drops the guard, and that it survives inlining. +(import ../../src/jolt/api :as api) +(import ../../src/jolt/backend :as backend) +(import ../../src/jolt/reader :as reader) + +(print "Struct hint (jolt-dad)...") + +(os/setenv "JOLT_DIRECT_LINK" "1") # inline on, so hint-through-inline is exercised +(def ctx (api/init {:compile? true})) +(api/eval-string ctx "(ns sh)") +(each s ["(defn v3 [r g b] {:r r :g g :b b})" + "(defn dot [^:struct l ^:struct r] (+ (+ (* (:r l) (:r r)) (* (:g l) (:g r))) (* (:b l) (:b r))))" + "(defn sub [^:struct l ^:struct r] {:r (- (:r l) (:r r)) :g (- (:g l) (:g r)) :b (- (:b l) (:b r))})" + "(defn lensq [^:struct v] (dot v v))"] + (api/eval-string ctx s)) + +(defn guards [src] + (def code (string/format "%p" (backend/emit-ir ctx (backend/analyze-form ctx (reader/parse-string src))))) + (length (string/find-all ":jolt/type" code))) + +# the guard is dropped for hinted subjects, kept for unhinted ones +(assert (= 1 (guards "(fn [v] (:r v))")) "unhinted (:r v) keeps the guard") +(assert (= 0 (guards "(fn [^:struct v] (:r v))")) "hinted (:r v) drops the guard") +(assert (= 0 (guards "(fn [^:struct v] (+ (+ (:r v) (:g v)) (:b v)))")) "all three hinted lookups bare") +(assert (= 0 (guards "(fn [^:struct v] (lensq v))")) "hint survives through an inlined call") + +# accurate hints are correctness-preserving (value identical to the guarded path) +(assert (= 32 (api/eval-string ctx "(dot (v3 1 2 3) (v3 4 5 6))")) "hinted dot value") +(assert (= 14 (api/eval-string ctx "(lensq (v3 1 2 3))")) "hinted lensq (inline-flow) value") +(assert (= 7 (api/eval-string ctx "(:r (sub (v3 9 8 7) (v3 2 0 0)))")) "hinted sub field") +# a hinted value flowing through an inlined call still reads correctly +(api/eval-string ctx "(defn hit [^:struct ray ^:struct c] (lensq (sub (:origin ray) c)))") +(assert (= 48 (api/eval-string ctx "(hit {:origin (v3 5 5 5) :direction (v3 0 0 0)} (v3 1 1 1))")) + "hinted value through nested inline reads correctly") + +# a missing key on a hinted struct still reads nil (struct miss), like a guarded get +(assert (= nil (api/eval-string ctx "((fn [^:struct m] (:absent m)) (v3 1 2 3))")) "hinted struct miss -> nil") + +(print "Struct hint passed!")