diff --git a/docs/rfc/0004-type-hints.md b/docs/rfc/0004-type-hints.md new file mode 100644 index 0000000..4c84837 --- /dev/null +++ b/docs/rfc/0004-type-hints.md @@ -0,0 +1,128 @@ +# RFC 0004: Type hints and keyword-lookup specialization + +Status: accepted (design note) + +This note describes how Jolt treats Clojure type hints, and the one place it +uses them: a `^:struct` or `^Record` hint on a local lets a constant-keyword +lookup skip its runtime representation guard. It records the rationale, the +soundness contract, the checked mode for catching inaccurate hints, and the +measured effect, so later work does not relitigate it. + +## Background: why the lookup carries a guard + +A Jolt map value has several runtime representations (see RFC on collections and +`src/jolt/core.janet`): a Janet struct for a small all-scalar-key literal map, a +persistent hash map (a table tagged `:jolt/type :jolt/phm`) when a key is a +collection or a value is nil, plus sorted maps, transients, and record/deftype +instances. A record instance is a Janet table tagged `:jolt/deftype` but, like a +struct, it carries no `:jolt/type`, so a raw Janet `(get inst :field)` reads its +fields directly. + +A constant-keyword lookup `(:k m)` compiles to a guarded form: + +```janet +(if (get m :jolt/type) (core-get m k) (get m k)) +``` + +The guard is one opcode. A non-nil `:jolt/type` routes phm/sorted/transient/ +lazy-seq values to `core-get`'s full semantics; everything else (structs, +records, nil, scalars) takes the bare Janet `get`, which matches `core-get` for +keyword keys. The guard is correct and cheap, but on a struct it is a second +`get`: profiling the ray tracer (a naive all-maps program) found keyword lookups +are about half of a render, and the guard is the only avoidable part of each +one. A bare get is roughly 20ns where the guarded form is roughly 36ns. + +Dropping the guard is only safe when the subject is known to be a plain +struct/record rather than a tagged collection. Jolt does not infer that +inter-procedurally (it would be unsound across a dynamic language's call +boundaries). A type hint supplies the same fact soundly, as a programmer +assertion. + +## What the hints mean + +Two hints on a local resolve to the "plain struct/record" assertion, which we +call the `:struct` hint internally: + +- `^:struct` — the value is a plain struct or record map. There is no Clojure + keyword with this meaning (Clojure's type hints are class names), so this is a + Jolt-specific metadata flag, analogous to `^:dynamic`. +- `^Name` where `Name` is a `defrecord`/`deftype`. Both forms define a `->Name` + positional constructor, so the analyzer treats a tag whose `->Name` resolves + as a record type. Record instances are raw-get-safe, so the lookup drops the + guard. A `^String`, `^long`, or any other non-record tag is not a record and + is ignored, exactly as before. + +Every other hint parses and is inert, matching Clojure (S12b in the reader +spec). A hint never changes a program's result; it only permits an +optimization. + +## How it flows + +The reader already keeps `^hint` metadata on the binding symbol and is otherwise +transparent (`reader.janet`, `meta-form->map`). The change threads that fact to +the lookup site: + +1. The analyzer (`jolt-core/jolt/analyzer.clj`) records a `:struct` hint per + local in its env when a param or `let` binding carries `^:struct` or a + record-type tag, and attaches `:hint :struct` to that local's `:local` IR + node. Resolving a record-type tag uses a new host contract function + `record-type?` (`src/jolt/host_iface.janet`), which checks for the `->Name` + constructor. +2. The back end (`emit-kw-lookup` in `src/jolt/backend.janet`) emits the bare get + when the lookup subject is a `:local` carrying the hint, and the guarded form + otherwise. The unhinted path is byte-identical to before. +3. The inline pass (`jolt-core/jolt/passes.clj`) propagates the hint: when it + binds a non-trivial call argument to a fresh local, it carries the called + function's parameter hint onto that local, so lookups inside the spliced body + keep the bare path. Without this, inlining a hinted function would erase the + benefit, because the hinted parameter is replaced by an unhinted temporary. + +The same machinery covers both `(:k m)` and `(get m :k [default])` when the key +is a constant keyword. A `get` with a variable, numeric, or string key falls +through to `core-get` unchanged. + +## Soundness and the checked mode + +An accurate hint is correctness-preserving by construction: for a struct or +record the bare get equals the guarded result. An inaccurate hint (asserting +`^:struct` for a value that is actually a phm) makes the raw get return the wrong +thing. This is the same contract as a wrong Clojure `^String`, except that a +wrong Jolt hint fails silently rather than throwing. + +To make a lie visible without taxing the fast path, `JOLT_CHECK_HINTS=1` keeps +the guard but throws on the tagged arm with a message naming the local and key: + +``` +type hint violated on `m`: (:a m) — value carries :jolt/type +(a phm/sorted/transient/lazy-seq), not the plain struct/record the +^:struct/^Record hint asserts +``` + +This is a development aid, off by default, with zero cost to normal builds (the +flag is read when the lookup is compiled, and the bare get is emitted when it is +off). The flag is part of the image-cache fingerprint. + +## Coverage + +Type hints parse in every position Clojure accepts them and are inert except for +the optimization above. This matches Clojure's "parse and otherwise do nothing" +model, with the difference that Clojure additionally uses hints to avoid +reflection and select primitive arithmetic, which do not apply to a Janet host. + +## Measured effect + +On the ray tracer (`~/src/examples/ray-tracer`, all values are `{:r :g :b}`-style +maps), with inlining on and the hot parameters hinted, a render goes from 13.3s +to 10.9s, about 1.22x, taking it to roughly 7.8x the JVM from 9.4x after the +inline pass. A seeded render produces an identical pixel checksum hinted and +unhinted, confirming the hints are correctness-preserving on the full pipeline. + +## Status and non-goals + +Implemented. Not pursued: inter-procedural shape inference (unsound in a dynamic +language without a guard, which costs as much as the one being removed) and a +shape-based "hidden class" representation (profiling showed allocation is about +1% of the workload, so a cheaper allocation would not help, and an escaping-map +lookup through a runtime shape check costs about the same as the guard it would +replace). The hint is the sound, opt-in lever on the part of the cost that can +move. diff --git a/docs/spec/02-reader.md b/docs/spec/02-reader.md index 67e4500..7d9962b 100644 --- a/docs/spec/02-reader.md +++ b/docs/spec/02-reader.md @@ -83,6 +83,16 @@ checks → UNVERIFIED (rows to add). - S12a. `^:kw form` ≡ `^{:kw true} form`; `^Sym form` ≡ `^{:tag Sym} form`; `^"str"` ≡ `^{:tag "str"} form`. Multiple `^` stack, rightmost innermost, merged left-over-right. +- S12b. Type hints are semantically transparent: a hint MUST NOT change a + program's result. Hints parse in every position they do in Clojure (params, + `let` bindings, `def` names, return position, arbitrary forms) and are + otherwise inert. As a non-normative optimization, jolt recognizes two hints + on a local as an assertion that a constant-keyword lookup may skip its + runtime representation guard: `^:struct` (a plain struct/record map) and + `^Name` where `Name` is a `defrecord`/`deftype`. The assertion is the + programmer's (an inaccurate hint yields a wrong lookup, like a wrong Clojure + `^String`); `JOLT_CHECK_HINTS=1` turns a violated hint into an error at no + cost to unchecked builds. See RFC 0004. - S13a. `#'ns/sym` MUST denote the same var as `(var ns/sym)`: `(= (var clojure.core/str) #'clojure.core/str)` is true. diff --git a/jolt-core/jolt/analyzer.clj b/jolt-core/jolt/analyzer.clj index 7d2a91c..2ce9f44 100644 --- a/jolt-core/jolt/analyzer.clj +++ b/jolt-core/jolt/analyzer.clj @@ -22,7 +22,8 @@ form-literal? form-elements form-vec-items form-map-pairs form-set-items form-special? compile-ns form-macro? form-expand-1 resolve-global - form-sym-meta host-intern! form-syntax-quote-lower]])) + form-sym-meta host-intern! form-syntax-quote-lower + record-type?]])) (declare analyze) @@ -44,13 +45,19 @@ (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] +;; Type hints (jolt-94n). The reader keeps ^hint metadata on the binding symbol. +;; Two hints resolve to the :struct fast path (a constant-keyword lookup skips +;; the :jolt/type guard and emits a bare get): ^:struct (a plain struct/record +;; map) and ^TypeName where TypeName is a defrecord/deftype (its instances are +;; tagged :jolt/deftype, not :jolt/type, so a raw get is correct). Every other +;; hint (^String, ^long, ...) parses and is ignored, as before. +(defn- hint-of [ctx sym] (let [m (form-sym-meta sym)] - (when (and m (get m :struct)) :struct))) + (cond + (nil? m) nil + (get m :struct) :struct + :else (let [t (get m :tag)] + (when (and t (record-type? ctx t)) :struct))))) (defn- add-hint [env nm h] (if h (assoc env :hints (assoc (:hints env) nm h)) env)) @@ -69,11 +76,11 @@ (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-hint (add-locals env [nm]) nm (hint-of bsym)) + (recur (+ i 2) (add-hint (add-locals env [nm]) nm (hint-of ctx bsym)) (conj pairs [nm init])))) [pairs env]))) -(defn- parse-params [pvec] +(defn- parse-params [ctx pvec] ;; :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 []] @@ -84,13 +91,13 @@ (let [r (nth pvec (inc i))] (when-not (form-sym? r) (uncompilable "destructuring fn rest")) (recur (+ i 2) fixed (form-sym-name r) hints)) - (let [nm (form-sym-name p) h (hint-of p)] + (let [nm (form-sym-name p) h (hint-of ctx 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))) + (let [pp (parse-params ctx (vec (form-vec-items pvec))) fixed (:fixed pp) rst (:rest pp) ;; Always a recur target, variadic included: the back end gives the rest diff --git a/src/jolt/api.janet b/src/jolt/api.janet index b4c9e8f..cb84cb0 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -296,7 +296,7 @@ # Opts land in the key via their printed form; an opt that prints unstably # (e.g. a closure in :namespaces) just degrades to a cache miss, never to a # wrong hit. Runtime knobs that shape the ctx outside opts ride along too. - (def key (string/format "%q|%q|%q|%q|%q|%q|%q|%q|%q" + (def key (string/format "%q|%q|%q|%q|%q|%q|%q|%q|%q|%q" (string janet/version "-" janet/build) opts (os/getenv "JOLT_PATH") @@ -305,7 +305,8 @@ (os/getenv "JOLT_FEATURES") (os/getenv "JOLT_INTERPRET_MACROS") (os/getenv "JOLT_DIRECT_LINK") - (os/getenv "JOLT_NO_IR_PASSES"))) + (os/getenv "JOLT_NO_IR_PASSES") + (os/getenv "JOLT_CHECK_HINTS"))) (string dir "/jolt-ctx-" (band h 0x7FFFFFFF) "-" len "-" (band (hash key) 0x7FFFFFFF) ".jimg")) (defn init-cached diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index a2b9aae..04c324f 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -304,10 +304,55 @@ (var- fp-counter 0) (defn- jsym [] (symbol "_fp$" (++ fp-counter))) +# Is fnode a reference to clojure.core/get (or a host `get`)? Used to give +# (get m :kw [d]) the same inlined keyword-lookup treatment as (:kw m [d]). +(defn- get-head? [fnode] + (case (fnode :op) + :var (and (= "clojure.core" (fnode :ns)) (= "get" (fnode :name))) + :host (= "get" (fnode :name)) + false)) + +# Shared emit for a constant-keyword map lookup — both (:kw m [d]) and +# (get m :kw [d]). subj-node is the subject's IR node (carries the type hint), +# m-expr its emitted form, k the keyword, d-expr the emitted default or nil. +# - unhinted: GUARDED — (if (get m :jolt/type) (core-get …) (bare get)). The +# guard (one opcode) routes tagged reps (phm/sorted/transient/lazy-seq) to +# core-get; a plain struct/record (no :jolt/type) takes the bare get, which +# matches core-get for keyword keys. +# - ^:struct / ^Record hinted subject: skip the guard, bare get (~20 vs ~36ns). +# - hinted + JOLT_CHECK_HINTS: keep the guard but THROW on the tagged arm, so a +# lying hint surfaces a clear error (dev aid; off by default, no perf cost). +(defn- emit-kw-lookup [subj-node m-expr k d-expr] + (def hinted (and subj-node (= :local (subj-node :op)) (= :struct (subj-node :hint)))) + (def checked (and hinted (os/getenv "JOLT_CHECK_HINTS"))) + (def m (if (symbol? m-expr) m-expr (jsym))) + (def wrap (fn [body] (if (symbol? m-expr) body ['let [m m-expr] body]))) + (def err (when checked + ['error (string "type hint violated on `" (subj-node :name) "`: (" + k " " (subj-node :name) ") — value carries :jolt/type " + "(a phm/sorted/transient/lazy-seq), not the plain " + "struct/record the ^:struct/^Record hint asserts")])) + (if (nil? d-expr) + (let [fast ['get m k]] + (wrap (cond + checked ['if ['get m :jolt/type] err fast] + hinted fast + ['if ['get m :jolt/type] (tuple core-get m k) fast]))) + (let [d (if (symbol? d-expr) d-expr (jsym)) + v (jsym) + fast ['let [v ['get m k]] ['if ['nil? v] d v]] + body (cond + checked ['if ['get m :jolt/type] err fast] + 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)))) + (defn- emit-invoke [ctx node] (def fnode (norm-node (node :fn))) (def args (map |(emit ctx $) (vview (node :args)))) (def nop (native-op fnode (length args))) + (def argnodes (vview (node :args))) (cond nop (case nop '++ ['+ (in args 0) 1] @@ -326,34 +371,21 @@ # records with direct field keys, nil, janet arrays, scalars) gets janet # `get` semantics, which match core-get for keyword keys. Structs never # store nil values (nil values force the phm rep), so present-but-nil - # can't be confused with missing on the fast arm. + # can't be confused with missing on the fast arm. A ^:struct/^Record hint on + # the subject skips the guard entirely (jolt-94n; see emit-kw-lookup). (and (= :const (fnode :op)) (keyword? (fnode :val)) (>= 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 - # per lookup in exactly the hottest shape, (:k local)) - 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)) - (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) - 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)))) + (emit-kw-lookup (norm-node (in argnodes 0)) (in args 0) (fnode :val) + (when (= 2 (length args)) (in args 1))) + # (get m :kw) / (get m :kw default) — same inlined keyword lookup as (:kw m), + # so an explicit get with a constant keyword gets the guard fast path and the + # ^:struct/^Record hint (jolt-94n). Only when the key is a constant keyword; + # a variable/number/string key falls through to core-get below. + (and (get-head? fnode) (>= (length args) 2) (<= (length args) 3) + (let [a1 (norm-node (in argnodes 1))] (and (= :const (a1 :op)) (keyword? (a1 :val))))) + (emit-kw-lookup (norm-node (in argnodes 0)) (in args 0) + ((norm-node (in argnodes 1)) :val) + (when (= 3 (length args)) (in args 2))) (direct-call? ctx fnode) (tuple (emit ctx fnode) ;args) # Local callee (closure param, let-bound fn, defn self-name): inline the # function check so the overwhelmingly-common function case is a direct diff --git a/src/jolt/host_iface.janet b/src/jolt/host_iface.janet index 55a05d3..aed1a76 100644 --- a/src/jolt/host_iface.janet +++ b/src/jolt/host_iface.janet @@ -219,6 +219,18 @@ (not (let [m (cell :meta)] (and m (get m :redef))))) (cell :inline-ir)))) +# Is `name` (a bare type-name string, e.g. "Vec3") a defrecord/deftype? Both +# expand to define a ->Name positional constructor var (30-macros.clj), so its +# presence is the marker. Lets the analyzer resolve a ^Record type hint to the +# struct fast path: record instances are tables tagged :jolt/deftype (NOT +# :jolt/type), so a raw keyword get is correct for them (jolt-94n). +(defn h-record-type? [ctx name] + (def ctor (string "->" name)) + (def cns (ctx-find-ns ctx (h-current-ns ctx))) + (if (or (and cns (ns-find cns ctor)) + (ns-find (ctx-find-ns ctx "clojure.core") ctor)) + true false)) + (def- exports {"form-sym?" h-sym? "form-sym-name" h-sym-name "form-sym-ns" h-sym-ns "ref-put!" h-ref-put! @@ -233,7 +245,8 @@ "form-expand-1" h-expand-1 "resolve-global" h-resolve-global "form-syntax-quote-lower" h-syntax-quote-lower "host-intern!" h-intern! - "inline-enabled?" h-inline-enabled? "inline-ir" h-inline-ir}) + "inline-enabled?" h-inline-enabled? "inline-ir" h-inline-ir + "record-type?" h-record-type?}) (defn install! [ctx] (def ns (ctx-find-ns ctx "jolt.host")) diff --git a/test/integration/struct-hint-test.janet b/test/integration/struct-hint-test.janet index bf433ce..a13cf9d 100644 --- a/test/integration/struct-hint-test.janet +++ b/test/integration/struct-hint-test.janet @@ -1,18 +1,21 @@ -# ^: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. +# Type hints driving keyword-lookup specialization (jolt-94n). A local hinted +# ^:struct (a plain struct/record map) or ^Record (a defrecord/deftype) lets a +# constant-keyword lookup skip the :jolt/type guard and emit a bare get +# (~20ns vs ~36ns), the way Clojure type hints let the compiler specialize. +# Covers both (:k m) and (get m :k), hint propagation through inlining, the +# ^Record path, the JOLT_CHECK_HINTS dev aid, and that accurate hints preserve +# results. An inaccurate hint is a programmer error (like a wrong ^String): the +# raw get returns the wrong value, surfaced only under JOLT_CHECK_HINTS. (import ../../src/jolt/api :as api) (import ../../src/jolt/backend :as backend) (import ../../src/jolt/reader :as reader) -(print "Struct hint (jolt-dad)...") +(print "Type hints (jolt-94n)...") (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)") +(api/eval-string ctx "(defrecord Vec3r [r g b])") (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))})" @@ -23,22 +26,50 @@ (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 +# --- guard removal ---------------------------------------------------------- (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))")) "^:struct (:r v) drops the guard") +(assert (= 0 (guards "(fn [^Vec3r v] (:r v))")) "^Record (:r v) drops the guard") +(assert (= 1 (guards "(fn [^String v] (:r v))")) "^String (not a record) still guards") (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") +# (get m :k) gets the same treatment as (:k m) +(assert (= 1 (guards "(fn [m] (get m :k))")) "unhinted (get m :k) is guarded-inline") +(assert (= 0 (guards "(fn [^:struct m] (get m :k))")) "^:struct (get m :k) drops the guard") +(assert (= 0 (guards "(fn [^Vec3r m] (get m :k 0))")) "^Record (get m :k d) drops the guard") +# a variable (non-constant) key isn't a keyword literal, so the inline doesn't +# fire — it falls through to core-get, which still indexes correctly. +(assert (= 2 (api/eval-string ctx "((fn [m kk] (get m kk)) {:a 2} :a)")) "variable-key get via core-get") +(assert (= 10 (api/eval-string ctx "((fn [m i] (get m i)) [10 20] 0)")) "variable-key get indexes a vector") -# accurate hints are correctness-preserving (value identical to the guarded path) +# --- correctness (accurate hints preserve results) -------------------------- (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") +(assert (= 9 (api/eval-string ctx "((fn [^:struct m] (get m :absent 9)) (v3 1 2 3))")) "hinted get default") +# field access on a real record instance through a ^Record hint +(api/eval-string ctx "(defn vr-x [^Vec3r v] (:r v))") +(assert (= 5 (api/eval-string ctx "(vr-x (->Vec3r 5 6 7))")) "record field via ^Record hint") +# (get m :k) on assorted reps still matches core-get semantics (unhinted path) +(assert (= 2 (api/eval-string ctx "(get {:a 2} :a)")) "get struct present") +(assert (= nil (api/eval-string ctx "(get {:a 2} :z)")) "get struct miss") +(assert (= 1 (api/eval-string ctx "(get (hash-map :a 1 :x nil) :a)")) "get phm present") +(assert (= nil (api/eval-string ctx "(get (hash-map :a 1 :x nil) :x)")) "get phm nil value") +(assert (= 7 (api/eval-string ctx "(get (sorted-map :a 7) :a)")) "get sorted present") -(print "Struct hint passed!") +# --- checked mode: a lying hint throws (separate ctx with the flag on) ------- +(os/setenv "JOLT_CHECK_HINTS" "1") +(def cctx (api/init {:compile? true})) +(api/eval-string cctx "(ns ck)") +(api/eval-string cctx "(defn rd [^:struct m] (:a m))") +(assert (= 1 (api/eval-string cctx "(rd {:a 1 :b 2})")) "checked mode: accurate hint still works") +(let [r (protect (api/eval-string cctx "(rd (hash-map :a 1 :x nil))"))] + (assert (not (r 0)) "checked mode: lying ^:struct hint throws") + (assert (string/find "type hint violated" (string (r 1))) "checked-mode error is meaningful")) +(os/setenv "JOLT_CHECK_HINTS" nil) + +(print "Type hints passed!")