jolt/test/spec/map-fastpath-spec.janet
Yogthos 8b2be06b68 perf: inline keyword lookup + map literals, clojure.math, indexed reduce
jank's ray tracer benchmark (examples/ray-tracer) drops from 165.6s to
15.8s per render (10.5x) — from 118x JVM Clojure to 11x. The changes
mirror the optimizations in jank's June 2026 post, adapted to the
janet backend (jolt-4vr, jolt-h79):

- (:kw m) emits an inline lookup instead of variadic jolt-call ->
  core-get's predicate chain. The guard is (get m :jolt/type): janet
  compiles get to an opcode (~17ns) where a struct? cfunction call
  costs ~85ns/lookup. :jolt/type is reserved (the reader rejects it in
  map literals) and every table rep that must not be raw-indexed
  carries it — phm tables now tagged too — so tagged values route to
  core-get and everything else gets janet get, which matches core-get
  for keyword keys on structs/records/nil/arrays. 929ns -> 90ns.
- {:k v ...} literals with scalar const keys emit let-bound values, an
  `and` truthiness test (pure branch opcodes), and a native (struct ...)
  call instead of variadic build-map-literal + runtime kv re-scan. nil
  or false values fall back to build-map-literal, which keeps Clojure's
  nil-entry semantics via the phm rep. 890ns -> 246ns.
- native-op additions: min/max (janet's are variadic with the same
  numeric semantics), nil?/some? lowered to janet's fastfun = / not=
  against nil, and not.
- clojure.math (Clojure 1.11) installed as a namespace whose vars hold
  janet's math natives directly, so calls direct-link. Math/sqrt-style
  interop stays in the frozen interpret-only punt set (~5us/call); this
  is the compiled route (~30ns).
- reduce over pvec/tuple/array iterates indexed in place — it was
  copying the whole pvec into a fresh array on every reduce call — and
  stops at `reduced` instead of scanning the tail.
- interpreter coll-lookup gains the sorted-coll arm: (:k (sorted-map ..))
  was nil in interpret mode (compiled mode had it right).

map-fastpath-spec pins keyword-invoke/map-literal semantics (16+15
rows incl. nil-value maps, records, sorted, vectors-of-internals) and
clojure.math (10 rows). Two pre-existing bugs found and filed while
writing it: map literals evaluate entries in reader-hash order
(jolt-p3c), and an attempted local-callee call inline that breaks
overlay lazy self-recursion is parked with a repro (jolt-507).

Gate green, conformance 335/335 x3, suite 4718 steady, core bench even
with main back-to-back.
2026-06-12 13:15:03 -04:00

57 lines
3.3 KiB
Text

# Specification: keyword-invoke and map-literal semantics that the compiled
# fast paths (jolt-4vr: inlined keyword lookup, inlined struct construction)
# must preserve. Written BEFORE the optimization; every row passes on the
# generic jolt-call/build-map-literal paths and must keep passing on the
# inlined ones.
(use ../support/harness)
(defspec "maps / keyword invoke"
["hit" "1" "(:a {:a 1 :b 2})"]
["miss" "nil" "(:z {:a 1})"]
["miss with default" ":d" "(:z {:a 1} :d)"]
["hit with default" "1" "(:a {:a 1} :d)"]
["on nil" "nil" "(:a nil)"]
["on nil with default" ":d" "(:a nil :d)"]
["nil value is present" "nil" "(:a {:a nil} :d)"]
["false value is present" "false" "(:a {:a false} :d)"]
["on a vector" "nil" "(:a [1 2 3])"]
["on a number" "nil" "(:a 42)"]
["on a sorted map" "2" "(:b (sorted-map :a 1 :b 2))"]
["on assoc result" "3" "(:c (assoc {:a 1} :c 3))"]
["on a record field" "5" "(do (defrecord KFP [x]) (:x (->KFP 5)))"]
["qualified keyword" "1" "(:n/a {:n/a 1})"]
["nested in expr" "6" "(+ (:a {:a 1}) (:b {:b 2}) (:c {:c 3}))"]
["evaluates map expr once" "[2 1]"
"(do (def cnt (atom 0)) (let [v (:a (do (swap! cnt inc) {:a 2}))] [v @cnt]))"])
(defspec "maps / literal construction"
["basic" "{:a 1, :b 2}" "{:a 1 :b 2}"]
["empty" "{}" "{}"]
["computed values" "{:a 3}" "{:a (+ 1 2)}"]
["nil value kept" "true" "(contains? {:a nil} :a)"]
["nil value lookup" "nil" "(get {:a nil} :a :d)"]
["string key" "1" "(get {\"k\" 1} \"k\")"]
["number key" ":one" "(get {1 :one} 1)"]
["collection key" ":v" "(get {[1 2] :v} [1 2])"]
["collection value-equal key" ":v" "(get {[1 2] :v} (vector 1 2))"]
["computed key" "1" "(get {(keyword \"a\") 1} :a)"]
# NOTE: entry ORDER is reader-hash order today, not source order (Clojure
# evaluates array-map literals left-to-right) — pinned loosely; see jolt-p3c
["values evaluate exactly once each" "[1 2 3]"
"(do (def log (atom [])) {:a (swap! log conj 1) :b (swap! log conj 2) :c (swap! log conj 3)} (vec (sort (deref log))))"]
["count" "3" "(count {:a 1 :b 2 :c 3})"]
["equality with phm" "true" "(= {:a 1 :b 2} (assoc {:a 1} :b 2))"]
["keys work after assoc" "2" "(:b (assoc {:a 1 :b 2} :c 3))"]
["literal in fn body" "12" "(do (defn mfp-mk [x] {:v (* x 2)}) (:v (mfp-mk 6)))"])
(defspec "clojure.math (jolt-h79)"
["sqrt" "true" "(< 1.4142 (clojure.math/sqrt 2) 1.4143)"]
["pow" "1024" "(long (clojure.math/pow 2 10))"]
["tan of 0" "0" "(long (clojure.math/tan 0))"]
["round" "3" "(clojure.math/round 2.6)"]
["floor" "2" "(clojure.math/floor 2.9)"]
["signum" "-1" "(clojure.math/signum -7.2)"]
["to-radians" "true" "(< 3.14 (clojure.math/to-radians 180) 3.15)"]
["PI" "true" "(< 3.14 clojure.math/PI 3.15)"]
["require + alias" "5" "(do (require '[clojure.math :as m]) (long (m/hypot 3 4)))"]
["as a value" "[1 2]" "(mapv (comp long clojure.math/sqrt) [1 4])"])