From 0e3584884fd8247c8bf1410f9ac1a0e6dbf613dc Mon Sep 17 00:00:00 2001 From: Yogthos Date: Wed, 10 Jun 2026 13:57:37 -0400 Subject: [PATCH] =?UTF-8?q?core:=20AOT=20context=20image=20=E2=80=94=20ini?= =?UTF-8?q?t-cached=20recovers=20the=20bootstrap=20cost=20across=20process?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro recompilation), paid by every process that builds a ctx from source — each jpm-test file, embedders, workers. init-cached marshals the built ctx to a disk image (same root-env dicts as snapshot/fork) and later processes unmarshal it in ~5 ms, any process: nothing from the baking process is needed at load. The cache key fingerprints the embedded .clj stdlib (which covers jolt-core: analyzer, IR, core tiers), the .janet seed sources next to the module, the janet version, the init opts, and the env knobs that shape a ctx (JOLT_PATH/ MUTABLE/AOT_CORE/FEATURES) — any change rebuilds. Corrupt or non-ctx images fall back to a rebuild (unmarshal of garbage can 'succeed' with a scalar, so the shape is checked, not just the throw). Writes are atomic (tmp + rename) so racing cold starts never publish a torn image. JOLT_NO_IMAGE_CACHE=1 opts out; JOLT_IMAGE_CACHE_DIR overrides the location (default TMPDIR). Test consumers switch to init-cached (harness, suite-worker, conformance, the behavioral unit/integration tests); tests that validate the bootstrap itself (bootstrap-fixpoint, staged-bootstrap, aot round-trip, direct-linking) and the deps tests (tmp-dir :paths would fragment the key) keep real init. Full jpm test: 2:46 -> 1:58 (~29%). New ctx-image-test covers cold/warm, cross-process load (subprocess runs defn/redef/macros/protocols/multimethods off the baked image), per-opts keying, and corrupt-image fallback. --- src/jolt/api.janet | 80 +++++++++++++++++++ test/integration/api-test.janet | 10 +-- test/integration/compile-mode-test.janet | 6 +- test/integration/conformance-test.janet | 2 +- test/integration/ctx-image-test.janet | 78 ++++++++++++++++++ test/integration/dispatch-cache-test.janet | 4 +- test/integration/embedded-stdlib-test.janet | 2 +- test/integration/fallback-zero-test.janet | 2 +- test/integration/features-test.janet | 4 +- test/integration/jank-conformance-test.janet | 2 +- test/integration/nrepl-test.janet | 2 +- test/integration/sci-bootstrap-test.janet | 2 +- test/integration/sci-runtime-test.janet | 2 +- test/integration/self-host-test.janet | 8 +- test/integration/suite-worker.janet | 2 +- .../systematic-coverage-test.janet | 20 ++--- test/support/harness.janet | 2 +- test/support/lazy-eval.janet | 2 +- test/unit/compile-fallback-test.janet | 2 +- test/unit/eval-test.janet | 2 +- test/unit/evaluator-test.janet | 14 ++-- test/unit/interop-test.janet | 12 +-- test/unit/lazy-seq-test.janet | 16 ++-- test/unit/persistent-map-test.janet | 10 +-- 24 files changed, 222 insertions(+), 64 deletions(-) create mode 100644 test/integration/ctx-image-test.janet diff --git a/src/jolt/api.janet b/src/jolt/api.janet index bf243a1..4899453 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -194,6 +194,86 @@ [snap] (unmarshal snap image-load-dict)) +# --- Disk-cached init (AOT context image) ------------------------------------ +# +# init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro +# recompilation) — paid by every PROCESS that builds a ctx from source, e.g. +# each `jpm test` file. init-cached pays it once: the built ctx is snapshotted +# to an image file and later processes unmarshal it (~tens of ms). Marshaling +# is against root-env (same dicts as snapshot/fork), so core cfunctions ride by +# name and everything jolt-level rides by value — a loaded image needs nothing +# from the baking process. The cache key fingerprints everything a fresh build +# would read: the embedded .clj stdlib (which includes jolt-core — the +# analyzer, IR, and core tiers), the .janet seed sources next to this module, +# the janet version, and the init opts. Any change rebuilds; a corrupt or +# unreadable image silently rebuilds. JOLT_NO_IMAGE_CACHE=1 disables. + +# Captured at module load: in source mode this is .../src/jolt/api.janet, so +# the seed sources can be fingerprinted; nil or stale in a built binary, where +# the baked-at-build-time ctx makes init-cached pointless anyway. +(def- api-module-file (dyn :current-file)) + +(defn- src-dir [] + (when api-module-file + (let [idxs (string/find-all "/" api-module-file)] + (when (not (empty? idxs)) + (string/slice api-module-file 0 (last idxs)))))) + +(defn- source-fingerprint + "Hash + total length of every source a fresh init depends on. Two numbers, so + a 32-bit hash collision alone can't alias two different source trees." + [] + (def buf @"") + (each k (sorted (keys stdlib-embed/sources)) + (buffer/push buf k "\x00" (get stdlib-embed/sources k) "\x00")) + (def dir (src-dir)) + (when dir + (each f (sorted (os/dir dir)) + (when (string/has-suffix? ".janet" f) + (buffer/push buf f "\x00" (slurp (string dir "/" f)) "\x00")))) + [(hash (string buf)) (length buf)]) + +(defn- image-cache-path [opts] + (def dir (or (os/getenv "JOLT_IMAGE_CACHE_DIR") + (os/getenv "TMPDIR") + "/tmp")) + (def [h len] (source-fingerprint)) + # 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" + (string janet/version "-" janet/build) + opts + (os/getenv "JOLT_PATH") + (os/getenv "JOLT_MUTABLE") + (os/getenv "JOLT_AOT_CORE") + (os/getenv "JOLT_FEATURES"))) + (string dir "/jolt-ctx-" (band h 0x7FFFFFFF) "-" len "-" (band (hash key) 0x7FFFFFFF) ".jimg")) + +(defn init-cached + "init, but disk-cached: the first call builds the context and writes a + bytecode image; later calls (any process, same sources) load the image + instead of rebuilding. Same opts as init. JOLT_NO_IMAGE_CACHE=1 disables; + JOLT_IMAGE_CACHE_DIR overrides the cache directory (default TMPDIR)." + [&opt opts] + (default opts {}) + (if (or (= "1" (os/getenv "JOLT_NO_IMAGE_CACHE")) (nil? (src-dir))) + (init opts) + (let [path (image-cache-path opts) + loaded (when (os/stat path) + (let [r (protect (fork (slurp path)))] + # unmarshal of a corrupt image can also "succeed" with a + # non-ctx value, so check the shape, not just the throw. + (when (and (r 0) (ctx? (r 1))) + (r 1))))] + (or loaded + (let [ctx (init opts) + tmp (string path "." (os/getpid) ".tmp")] + # Atomic publish so concurrent cold starts never see a torn image. + (when (protect (spit tmp (snapshot ctx))) + (protect (os/rename tmp path))) + ctx))))) + (defn eval-one "Evaluate a single already-parsed form. Routing (compile when :compile? is set, stateful forms interpret, interpreter fallback for forms the compiler can't diff --git a/test/integration/api-test.janet b/test/integration/api-test.janet index 25a7d6e..3029a1f 100644 --- a/test/integration/api-test.janet +++ b/test/integration/api-test.janet @@ -2,7 +2,7 @@ (use ../../src/jolt/types) (print "1: init creates context...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (ctx? ctx) "init returns context") (let [ns (ctx-find-ns ctx "clojure.core")] (assert (ns? ns) "clojure.core namespace exists") @@ -11,27 +11,27 @@ (print " passed") (print "2: eval-string basics...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 42 (eval-string ctx "42")) "eval integer") (assert (= true (eval-string ctx "true")) "eval bool") (assert (= 3 (eval-string ctx "(+ 1 2)")) "eval list")) (print " passed") (print "3: eval-string with core fns...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (eval-string ctx "(nil? nil)")) "nil?") (assert (deep= [2 3 4] (normalize-pvecs (eval-string ctx "(map inc [1 2 3])"))) "map+inc") (assert (= 6 (eval-string ctx "(reduce + [1 2 3])")) "reduce")) (print " passed") (print "4: eval-string with def...") -(let [ctx (init)] +(let [ctx (init-cached)] (eval-string ctx "(def x 42)") (assert (= 42 (eval-string ctx "x")) "def then resolve")) (print " passed") (print "5: eval-string* with bindings...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 99 (eval-string* ctx "y" @{"y" 99})) "bound variable")) (print " passed") diff --git a/test/integration/compile-mode-test.janet b/test/integration/compile-mode-test.janet index b663a42..236b1ce 100644 --- a/test/integration/compile-mode-test.janet +++ b/test/integration/compile-mode-test.janet @@ -3,7 +3,7 @@ (defn ct-eval [ctx s] (normalize-pvecs (eval-string ctx s))) (print "Phase 6: comprehensive compile-mode tests...") -(let [ctx (init {:compile? true})] +(let [ctx (init-cached {:compile? true})] (print " collections...") (assert (= :a (ct-eval ctx "(nth [:a :b :c :d] 0)")) "nth") @@ -126,13 +126,13 @@ # Context isolation: a def in one compiled context is invisible in another. With # var-indirection each context has its own var cells, so b's `secret` is a # distinct, unbound var (nil) rather than a's 7. -(let [a (init {:compile? true}) b (init {:compile? true})] +(let [a (init-cached {:compile? true}) b (init-cached {:compile? true})] (eval-string a "(def secret 7)") (assert (= 7 (ct-eval a "secret")) "def visible in its own ctx") (assert (nil? (ct-eval b "secret")) "def isolated to its ctx")) # Redefinition is visible to already-compiled callers (var-indirection). -(let [c (init {:compile? true})] +(let [c (init-cached {:compile? true})] (eval-string c "(defn g [] 1)") (eval-string c "(defn calls-g [] (g))") (eval-string c "(defn g [] 2)") diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index 13d495c..b9a63d4 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -464,7 +464,7 @@ # instead of its own init (~50 ms interpreted / ~900 ms compiled). Isolation is # preserved — a fork shares nothing mutable with its siblings. For self-host # mode, compile one form first so the lazily-built analyzer is in the snapshot. - (def base (init init-opts)) + (def base (init-cached init-opts)) (when selfhost? (selfhost/compile-and-eval base (parse-string "1"))) (def snap (snapshot base)) (def fails @[]) diff --git a/test/integration/ctx-image-test.janet b/test/integration/ctx-image-test.janet new file mode 100644 index 0000000..67e3917 --- /dev/null +++ b/test/integration/ctx-image-test.janet @@ -0,0 +1,78 @@ +# init-cached: disk-cached AOT image of the fully-built context. +# +# init in compile mode costs ~2.4 s (tier loading, analyzer self-compile, macro +# recompilation). init-cached pays that once, marshals the built ctx to an image +# file (the same machinery as api/snapshot), and every later process unmarshals +# it instead of rebuilding. The cache key fingerprints the embedded .clj stdlib, +# the .janet seed sources, and the init opts, so any source change invalidates. +# +# The cross-process case is the one that matters (each `jpm test` file is its +# own janet process), so the warm-load checks run in a SUBPROCESS against the +# image this process bakes. +(use ../../src/jolt/api) + +(print "ctx image cache...") + +(def dir (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-img-test-" (os/getpid))) +(os/mkdir dir) +(os/setenv "JOLT_IMAGE_CACHE_DIR" dir) +# The cache is the test subject — undo an ambient opt-out (and for the subprocess). +(os/setenv "JOLT_NO_IMAGE_CACHE" nil) +(defer (do (each f (os/dir dir) (os/rm (string dir "/" f))) (os/rmdir dir)) + + # 1. Cold init: builds the ctx, writes an image, and is fully functional. + (def t0 (os/clock)) + (def ctx1 (init-cached {:compile? true})) + (def cold-s (- (os/clock) t0)) + (assert (= 3 (eval-string ctx1 "(+ 1 2)")) "cold ctx evaluates") + (def files (filter |(string/has-suffix? ".jimg" $) (os/dir dir))) + (assert (= 1 (length files)) (string "one image written, got " (length files))) + + # 2. Warm init in THIS process: loads the image, functional, and much faster. + (def t1 (os/clock)) + (def ctx2 (init-cached {:compile? true})) + (def warm-s (- (os/clock) t1)) + (assert (= 10 (eval-string ctx2 "(do (defn f [x] (* 2 x)) (f 5))")) "warm ctx compiles defns") + (assert (< warm-s (/ cold-s 4)) + (string/format "warm load (%.0f ms) at least 4x faster than cold (%.0f ms)" + (* 1000 warm-s) (* 1000 cold-s))) + + # 3. Different opts get a different image (no false sharing between modes). + (init-cached {}) + (def files2 (filter |(string/has-suffix? ".jimg" $) (os/dir dir))) + (assert (= 2 (length files2)) "interpret-mode image is keyed separately") + + # 4. Cross-process warm load: a fresh janet process loads the image this + # process baked and runs real work — compiled defns, redefinition, + # macros, lazy seqs, protocols, multimethods, stdlib require. + (def checks + ``(use ./src/jolt/api) + (def t0 (os/clock)) + (def ctx (init-cached {:compile? true})) + (def warm-ms (* 1000 (- (os/clock) t0))) + (def img-files (filter |(string/has-suffix? ".jimg" $) + (os/dir (os/getenv "JOLT_IMAGE_CACHE_DIR")))) + (assert (= 2 (length img-files)) "subprocess hit the cache (no new image)") + (assert (= 3 (eval-string ctx "(+ 1 2)")) "arith") + (assert (= 120 (eval-string ctx "(do (defn fact [n] (if (zero? n) 1 (* n (fact (dec n))))) (fact 5))")) "compiled defn") + (assert (= 7 (eval-string ctx "(do (def a 3) (def a 7) a)")) "redefinition") + (assert (= 6 (eval-string ctx "(-> 1 inc (* 3))")) "macros expand") + (assert (= 9 (eval-string ctx "(do (defmacro tw [x] `(* 3 ~x)) (tw 3))")) "defmacro works post-load") + (assert (= [2 4 6] (normalize-pvecs (eval-string ctx "(vec (map #(* 2 %) [1 2 3]))"))) "lazy/HOF") + (assert (= "a-b" (eval-string ctx "(do (require '[clojure.string :as str]) (str/join \"-\" [\"a\" \"b\"]))")) "stdlib require") + (assert (= 42 (eval-string ctx "(do (defprotocol P (pf [x])) (defrecord R [] P (pf [x] 42)) (pf (->R)))")) "protocols") + (assert (= :big (eval-string ctx "(do (defmulti m (fn [x] (if (> x 5) :big :small))) (defmethod m :big [_] :big) (m 10))")) "multimethods") + (print "subprocess warm load ok in " (math/round warm-ms) " ms")``) + (def code (os/execute ["janet" "-e" checks] :p)) + (assert (= 0 code) "cross-process warm load passes") + + # 5. A source change invalidates: poisoning the fingerprint env knob is not + # possible from here, but a corrupted image must fall back to a rebuild + # rather than crash. + (each f (os/dir dir) + (when (string/has-suffix? ".jimg" f) (spit (string dir "/" f) "garbage"))) + (def ctx3 (init-cached {:compile? true})) + (assert (= 3 (eval-string ctx3 "(+ 1 2)")) "corrupted image falls back to rebuild") + + (printf "ctx image cache passed! (cold %.0f ms, warm %.0f ms)" + (* 1000 cold-s) (* 1000 warm-s))) diff --git a/test/integration/dispatch-cache-test.janet b/test/integration/dispatch-cache-test.janet index 680a777..334bd3a 100644 --- a/test/integration/dispatch-cache-test.janet +++ b/test/integration/dispatch-cache-test.janet @@ -15,7 +15,7 @@ (printf "FAIL [%s] got %q want %q" label got want))) (each mode [{:compile? true} {} {:aot-core? false}] - (def ctx (init mode)) + (def ctx (init-cached mode)) (eval-string ctx "(defprotocol P (m [x]))") (eval-string ctx "(extend-protocol P Number (m [x] (* x 2)))") (check (string mode " host dispatch") (eval-string ctx "(m 5)") 10) @@ -31,7 +31,7 @@ # Multimethod hierarchy-fallback cache (jolt-g8w): the isa? walk result for a # dispatch value is cached; defmethod/remove-method must invalidate it. (each mode [{:compile? true} {} {:aot-core? false}] - (def ctx (init mode)) + (def ctx (init-cached mode)) (eval-string ctx "(derive ::circle ::shape)") (eval-string ctx "(derive ::square ::shape)") (eval-string ctx "(defmulti area identity)") diff --git a/test/integration/embedded-stdlib-test.janet b/test/integration/embedded-stdlib-test.janet index d11540f..20ce338 100644 --- a/test/integration/embedded-stdlib-test.janet +++ b/test/integration/embedded-stdlib-test.janet @@ -6,7 +6,7 @@ (use ../../src/jolt/api) (use ../../src/jolt/types) -(def ctx (init)) +(def ctx (init-cached)) (ctx-set-current-ns ctx "user") (put (ctx :env) :source-paths @[]) # no FS roots — embedded fallback only diff --git a/test/integration/fallback-zero-test.janet b/test/integration/fallback-zero-test.janet index dd6a7ee..3095c58 100644 --- a/test/integration/fallback-zero-test.janet +++ b/test/integration/fallback-zero-test.janet @@ -18,7 +18,7 @@ (use ../../src/jolt/api) (use ../../src/jolt/reader) -(def ctx (init)) +(def ctx (init-cached)) (defn- analyzes? [s] # true if the analyzer produced IR (compiled), false if it punted/uncompilable. diff --git a/test/integration/features-test.janet b/test/integration/features-test.janet index 2548956..318009d 100644 --- a/test/integration/features-test.janet +++ b/test/integration/features-test.janet @@ -9,14 +9,14 @@ (defn check [label expected actual] # evaluate (= expected actual) in a fresh ctx; expects boolean true - (def ctx (init)) + (def ctx (init-cached)) (def res (protect (eval-string ctx (string "(= " expected " " actual ")")))) (cond (not= (res 0) true) (array/push fails [label "ERROR" (string (res 1))]) (= (res 1) true) (++ pass) - (let [got (protect (eval-string (init) actual))] + (let [got (protect (eval-string (init-cached) actual))] (array/push fails [label "NEQ" (string "want=" expected " got=" (if (= (got 0) true) (string/format "%q" (got 1)) (string "ERR:" (got 1))))])))) diff --git a/test/integration/jank-conformance-test.janet b/test/integration/jank-conformance-test.janet index cb53ccd..470bc80 100644 --- a/test/integration/jank-conformance-test.janet +++ b/test/integration/jank-conformance-test.janet @@ -42,7 +42,7 @@ (def fails @[]) (each f files # A pass-test passes when no assertion throws (assert now errors on failure). - (def res (protect (load-string (init) (slurp f)))) + (def res (protect (load-string (init-cached) (slurp f)))) (if (= (res 0) true) (++ pass) (array/push fails (string/slice f (+ 1 (length jank-dir)))))) diff --git a/test/integration/nrepl-test.janet b/test/integration/nrepl-test.janet index fba05b8..9a4d434 100644 --- a/test/integration/nrepl-test.janet +++ b/test/integration/nrepl-test.janet @@ -25,7 +25,7 @@ (do (ev/sleep 0.1) (++ tries))))) (assert ready "nREPL server did not start") -(def ctx (init)) +(def ctx (init-cached)) (ctx-set-current-ns ctx "user") (load-string ctx "(require '[jolt.nrepl])") (load-string ctx (string "(def c (jolt.nrepl/connect {:port " port "}))")) diff --git a/test/integration/sci-bootstrap-test.janet b/test/integration/sci-bootstrap-test.janet index 96f1e21..7bc44a4 100644 --- a/test/integration/sci-bootstrap-test.janet +++ b/test/integration/sci-bootstrap-test.janet @@ -9,7 +9,7 @@ # context; the portable default is #{:jolt :default}). (reader-features-set! ["jolt" "clj" "default"]) -(def ctx (init)) +(def ctx (init-cached)) (printf "Loading SCI stubs...\n") (defn load-stubs [ctx filepath] diff --git a/test/integration/sci-runtime-test.janet b/test/integration/sci-runtime-test.janet index ebe7266..c647ca1 100644 --- a/test/integration/sci-runtime-test.janet +++ b/test/integration/sci-runtime-test.janet @@ -31,7 +31,7 @@ # Run from project root so paths resolve (def root (if (has-value? (dyn :syspath) 0) (first (dyn :syspath)) ".")) -(def ctx (init)) +(def ctx (init-cached)) (load-stubs ctx (string root "/src/jolt/clojure/sci/lang_stubs.clj")) (load-stubs ctx (string root "/src/jolt/clojure/sci/io_stubs.clj")) diff --git a/test/integration/self-host-test.janet b/test/integration/self-host-test.janet index 3e0518d..d545b1b 100644 --- a/test/integration/self-host-test.janet +++ b/test/integration/self-host-test.janet @@ -10,7 +10,7 @@ (defn ce [ctx s] (normalize-pvecs (backend/compile-and-eval ctx (parse-string s)))) (print "self-host pipeline (Clojure analyzer -> IR -> Janet)...") -(let [ctx (init)] +(let [ctx (init-cached)] # primitives + control flow (assert (= 3 (ce ctx "(+ 1 2)")) "+") (assert (= 6 (ce ctx "(* 2 3)")) "*") @@ -58,7 +58,7 @@ # compile path. Forms the analyzer can't handle (stateful / destructuring) fall # back to the interpreter, with the same observable results. (print "self-host via eval-toplevel routing...") -(let [ctx (init {:compile? true})] +(let [ctx (init-cached {:compile? true})] (defn ev [s] (normalize-pvecs (eval-one ctx (parse-string s)))) (assert (= 3 (ev "(+ 1 2)")) "tl +") (ev "(defn sq [x] (* x x))") # def via self-host @@ -70,7 +70,7 @@ # Proof the self-hosted pipeline actually ran: only backend/ensure-analyzer # populates jolt.analyzer. An interpret-only ctx never loads it. (assert (pos? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings))) "analyzer loaded under :compile?")) -(let [ctx (init {})] +(let [ctx (init-cached {})] (eval-one ctx (parse-string "(+ 1 2)")) (assert (zero? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings))) "analyzer NOT loaded when interpreting")) @@ -78,7 +78,7 @@ # load into clojure.core at init and work the same compiled or interpreted. (print "clojure.core overlay (Clojure-defined core fns)...") (each opts [{:compile? true} {}] - (let [ctx (init opts)] + (let [ctx (init-cached opts)] (defn ev [s] (normalize-pvecs (eval-one ctx (parse-string s)))) (assert (= 1 (ev "(ffirst [[1 2] [3 4]])")) "ffirst") (assert (= [2] (ev "(nfirst [[1 2] [3 4]])")) "nfirst") diff --git a/test/integration/suite-worker.janet b/test/integration/suite-worker.janet index 8a515e5..63c7a36 100644 --- a/test/integration/suite-worker.janet +++ b/test/integration/suite-worker.janet @@ -28,7 +28,7 @@ # portable Clojure analyzer + Janet back end, hybrid with interpreter fallback) # so the whole battery validates the self-hosted compiler against the baseline. (def selfhost? (= "1" (os/getenv "JOLT_SELFHOST"))) - (def ctx (init (if compile? {:compile? true} {}))) + (def ctx (init-cached (if compile? {:compile? true} {}))) (defn run-form [f] (cond selfhost? (selfhost/compile-and-eval ctx f) diff --git a/test/integration/systematic-coverage-test.janet b/test/integration/systematic-coverage-test.janet index 14fd485..0644430 100644 --- a/test/integration/systematic-coverage-test.janet +++ b/test/integration/systematic-coverage-test.janet @@ -6,7 +6,7 @@ # --- Type Predicates --- (print "1: type predicates...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(integer? 0)")) "integer? 0") (assert (= true (ct-eval ctx "(integer? 1)")) "integer? 1") (assert (= true (ct-eval ctx "(integer? -5)")) "integer? negative") @@ -56,7 +56,7 @@ # --- Number Predicates --- (print "2: number predicates...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(zero? 0)")) "zero? 0") (assert (= false (ct-eval ctx "(zero? 1)")) "zero? 1") # zero?/pos?/neg? now reject non-numbers (Clojure-strict), like the JVM. @@ -77,7 +77,7 @@ # --- Math --- (print "3: math operations...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 0 (ct-eval ctx "(+)")) "+ 0 args") (assert (= 5 (ct-eval ctx "(+ 2 3)")) "+ 2 args") (assert (= 10 (ct-eval ctx "(+ 1 2 3 4)")) "+ varargs") @@ -104,7 +104,7 @@ # --- Comparison --- (print "4: comparison...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= 1 1)")) "= same") (assert (= true (ct-eval ctx "(= 1 1 1)")) "= multi same") (assert (= false (ct-eval ctx "(= 1 2)")) "= different") @@ -119,7 +119,7 @@ # --- Boolean logic --- (print "5: boolean logic...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(and)")) "and empty") (assert (= true (ct-eval ctx "(and true)")) "and true") (assert (= nil (ct-eval ctx "(and nil)")) "and nil") @@ -139,7 +139,7 @@ # --- Collections --- (print "6: collections...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(empty? nil)")) "empty? nil") (assert (= true (ct-eval ctx "(empty? [])")) "empty? []") (assert (= true (ct-eval ctx "(empty? ())")) "empty? ()") @@ -157,7 +157,7 @@ # --- Sequence Operations --- (print "7: sequence operations...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 1 (ct-eval ctx "(first [1 2 3])")) "first") (assert (= nil (ct-eval ctx "(first [])")) "first empty") (assert (= nil (ct-eval ctx "(first nil)")) "first nil") @@ -178,7 +178,7 @@ # --- Collections: conj, assoc, dissoc, get --- (print "8: collection mutation...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [1 2 3] (conj [1 2] 3))")) "conj vector") (assert (= true (ct-eval ctx "(= (quote (0 1 2)) (conj (quote (1 2)) 0))")) "conj list prepend") (assert (= true (ct-eval ctx "(= {:a 1 :b 2} (assoc {:a 1} :b 2))")) "assoc") @@ -194,7 +194,7 @@ # --- Higher-order functions --- (print "9: higher-order functions...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 3 (ct-eval ctx "((comp inc inc) 1)")) "comp") (assert (= 42 (ct-eval ctx "(identity 42)")) "identity") (assert (= 99 (ct-eval ctx "((constantly 99) :anything)")) "constantly") @@ -206,7 +206,7 @@ # --- Destructuring --- (print "10: destructuring...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 1 (ct-eval ctx "(let [[x] [1 2 3]] x)")) "seq destructure first") (assert (= 2 (ct-eval ctx "(let [[_ y] [1 2 3]] y)")) "seq destructure second")) (print " ok") diff --git a/test/support/harness.janet b/test/support/harness.janet index 483e79d..7675c87 100644 --- a/test/support/harness.janet +++ b/test/support/harness.janet @@ -30,7 +30,7 @@ (defn fresh-ctx "A fresh, fully-isolated interpret-mode context (cheap fork of one shared init)." [] - (when (nil? base-snap) (set base-snap (snapshot (init)))) + (when (nil? base-snap) (set base-snap (snapshot (init-cached)))) (fork base-snap)) (defn jeval diff --git a/test/support/lazy-eval.janet b/test/support/lazy-eval.janet index f656ce6..e4d4eea 100644 --- a/test/support/lazy-eval.janet +++ b/test/support/lazy-eval.janet @@ -8,7 +8,7 @@ (def actual (get (dyn :args) 2)) (when (and expected actual) - (def ctx (init {})) + (def ctx (init-cached {})) (def prog (string "(= " expected " " actual ")")) (def [ok val] (protect (eval-string ctx prog))) (if ok diff --git a/test/unit/compile-fallback-test.janet b/test/unit/compile-fallback-test.janet index 1248128..10abe80 100644 --- a/test/unit/compile-fallback-test.janet +++ b/test/unit/compile-fallback-test.janet @@ -12,7 +12,7 @@ (use ../../src/jolt/reader) (import ../../src/jolt/backend :as backend) -(def ctx (init)) +(def ctx (init-cached)) # 1. A deliberate punt (letfn needs letrec IR) falls back and evaluates correctly. (assert (= 3 (backend/compile-and-eval ctx (parse-string "(letfn [(f [n] (+ n 1))] (f 2))"))) diff --git a/test/unit/eval-test.janet b/test/unit/eval-test.janet index 2e566ce..8b2ac87 100644 --- a/test/unit/eval-test.janet +++ b/test/unit/eval-test.janet @@ -3,7 +3,7 @@ (print "Eval Tests") (print "1: eval literal...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 42 (ct-eval ctx "(eval 42)")) "eval literal") (assert (= 3 (ct-eval ctx "(eval '(+ 1 2))")) "eval quoted form") (assert (= 3 (ct-eval ctx "(eval (eval '(+ 1 2)))")) "eval nested") diff --git a/test/unit/evaluator-test.janet b/test/unit/evaluator-test.janet index e407327..48f347b 100644 --- a/test/unit/evaluator-test.janet +++ b/test/unit/evaluator-test.janet @@ -14,7 +14,7 @@ # Helper: parse and eval # init, not bare make-ctx: with the implicit Janet root-env fallback removed # (Stage 3), a context without clojure.core interned can't resolve inc/+/etc. -(def- shared-ctx (init)) +(def- shared-ctx (init-cached)) (defn eval-str [s] (eval-form shared-ctx @{} (parse-string s))) @@ -103,13 +103,13 @@ (print "13: locking...") # locking/instance? are overlay macros now (Stage 2 tier 6c) — they need the # full env (init loads the overlay), not a bare make-ctx. -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 42 (eval-string ctx "(locking :lock 42)")) "locking returns body result")) (print " passed") (print "14: instance?...") # instance? checks type -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (eval-string ctx "(instance? Number 42)")) "instance? Number matches number") (assert (= false (eval-string ctx "(instance? Number \"hello\")")) "instance? Number doesn't match string")) (print " passed") @@ -117,7 +117,7 @@ (print "15: defmulti/defmethod...") # defmulti/defmethod are overlay macros now (Stage 2 jolt-eaa), so this needs the # full env (init loads the overlay + installs the *-setup fns), not a bare make-ctx. -(let [ctx (init)] +(let [ctx (init-cached)] (eval-form ctx @{} (parse-string "(defmulti my-dispatch (fn* [x] (x :type)))")) (eval-form ctx @{} (parse-string "(defmethod my-dispatch :foo [_] :got-foo)")) (eval-form ctx @{} (parse-string "(defmethod my-dispatch :bar [_] :got-bar)")) @@ -126,8 +126,8 @@ (print " passed") (print "16: deftype...") -# deftype is an overlay macro now (Stage 2 jolt-eaa) — needs the full env (init). -(let [ctx (init) +# deftype is an overlay macro now (Stage 2 jolt-eaa) — needs the full env (init-cached). +(let [ctx (init-cached) _ (eval-form ctx @{} (parse-string "(deftype Point [x y])")) _ (eval-form ctx @{} (parse-string "(def p (Point. 10 20))")) p-val (eval-form ctx @{} (parse-string "p")) @@ -143,7 +143,7 @@ (print "17: defmacro...") # define a macro using defmacro special form # init loads clojure.core so `list` is available -(let [ctx (init) +(let [ctx (init-cached) _ (eval-form ctx @{} (parse-string "(defmacro my-when [test body] (list 'if test body nil))")) result (eval-form ctx @{} (parse-string "(my-when true 2)"))] (assert (= 2 result) "defmacro defines callable macro")) diff --git a/test/unit/interop-test.janet b/test/unit/interop-test.janet index a06d8f3..83d89fc 100644 --- a/test/unit/interop-test.janet +++ b/test/unit/interop-test.janet @@ -3,7 +3,7 @@ (print "Janet Interop Tests") (print "1: field access on tables...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(def t {:a 1 :b 2})") (assert (= 1 (ct-eval ctx "(. t :a)")) ". field access table") (assert (= 2 (ct-eval ctx "(. t :b)")) ". field access table 2") @@ -11,33 +11,33 @@ (print " ok") (print "2: field access on structs...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(def s {:x 10 :y 20})") (assert (= 10 (ct-eval ctx "(. s :x)")) ". field access struct") (assert (= 20 (ct-eval ctx "(. s :y)")) ". field access struct 2")) (print " ok") (print "3: method calls on tables...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(def obj {:greet (fn [self name] (str \"Hello \" name))})") (assert (= "Hello Alice" (ct-eval ctx "(. obj greet \"Alice\")")) ". method call on table")) (print " ok") (print "4: field access via .- reader sugar...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(def t {:x 42 :len 5})") (assert (= 42 (ct-eval ctx "(.-x t)")) ".-x reader sugar") (assert (= 5 (ct-eval ctx "(.-len t)")) ".-len reader sugar")) (print " ok") (print "5: . field access still works on deftypes...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(deftype Point [x y])") (assert (= 3 (ct-eval ctx "(. (Point. 3 4) x)")) ". field access deftype")) (print " ok") (print "6: method call with multiple args...") -(let [ctx (init)] +(let [ctx (init-cached)] (ct-eval ctx "(def calc {:add (fn [_ a b] (+ a b)) :mul (fn [_ a b] (* a b))})") (assert (= 7 (ct-eval ctx "(. calc add 3 4)")) ". method add") (assert (= 12 (ct-eval ctx "(. calc mul 3 4)")) ". method mul")) diff --git a/test/unit/lazy-seq-test.janet b/test/unit/lazy-seq-test.janet index 4911642..dda9bfe 100644 --- a/test/unit/lazy-seq-test.janet +++ b/test/unit/lazy-seq-test.janet @@ -3,47 +3,47 @@ (print "LazySeq Tests") (print "1: lazy-seq from list...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [1 2 3] (take 10 (lazy-seq [1 2 3])))")) "lazy-seq list") (assert (= true (ct-eval ctx "(= 3 (count (lazy-seq [1 2 3])))")) "count lazy-seq")) (print " ok") (print "2: lazy-cat concatenation...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [1 2 3 4] (take 10 (lazy-cat [1 2] [3 4])))")) "lazy-cat concat") (assert (= true (ct-eval ctx "(= 4 (count (lazy-cat [1 2] [3 4])))")) "lazy-cat count")) (print " ok") (print "3: first/rest on lazy-seqs...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= 1 (first (lazy-seq [1 2 3])))")) "first lazy") (assert (= true (ct-eval ctx "(= 2 (first (rest (lazy-seq [1 2 3]))))")) "first rest lazy")) (print " ok") (print "4: drop/nth on lazy-seqs...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [3 4 5] (take 10 (drop 2 (lazy-seq [1 2 3 4 5]))))")) "drop 2 take 10") (assert (= true (ct-eval ctx "(= 3 (nth (lazy-seq [1 2 3 4 5]) 2))")) "nth lazy")) (print " ok") (print "5: concat on lazy-seqs...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= 5 (count (concat (lazy-seq [1 2]) (lazy-seq [3 4 5]))))")) "concat lazy")) (print " ok") (print "6: reverse/sort on lazy-seqs...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [3 2 1] (reverse (lazy-seq [1 2 3])))")) "reverse lazy") (assert (= true (ct-eval ctx "(= [1 2 3] (sort (lazy-seq [3 1 2])))")) "sort lazy")) (print " ok") (print "7: distinct on lazy-seqs...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= [1 2 3] (distinct (lazy-seq [1 2 1 3 2])))")) "distinct lazy")) (print " ok") (print "8: fib-seq (deferred — eager core-map needs lazy upgrade)...") -(let [ctx (init)] +(let [ctx (init-cached)] (print " The cell-by-cell lazy-seq architecture is correct. concat, take,") (print " drop, nth, first, rest all work with self-referencing patterns.") (print " core-map still eagerly realizes all lazy-seq arguments, which") diff --git a/test/unit/persistent-map-test.janet b/test/unit/persistent-map-test.janet index 9ccba03..a3df3a3 100644 --- a/test/unit/persistent-map-test.janet +++ b/test/unit/persistent-map-test.janet @@ -13,7 +13,7 @@ # 1. Basic hash-map construction and access # ============================================================ (print "1: hash-map construction...") -(let [ctx (init)] +(let [ctx (init-cached)] (def m1 (ct-eval ctx "(hash-map :a 1)")) (assert (not (nil? m1)) "hash-map returns non-nil") (assert (= true (ct-eval ctx "(map? (hash-map :a 1))")) "map? returns true for PHM") @@ -29,7 +29,7 @@ # 2. assoc and dissoc # ============================================================ (print "2: assoc/dissoc...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(= (assoc (hash-map :a 1) :b 2) (hash-map :a 1 :b 2))")) "assoc add") (assert (= true (ct-eval ctx "(= (assoc (hash-map :a 1) :a 99) (hash-map :a 99))")) "assoc replace") (assert (= true (ct-eval ctx "(= (dissoc (hash-map :a 1 :b 2) :a) (hash-map :b 2))")) "dissoc") @@ -41,7 +41,7 @@ # 3. keys, vals, merge # ============================================================ (print "3: keys/vals/merge...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= 2 (ct-eval ctx "(count (keys (hash-map :a 1 :b 2)))")) "keys count") (assert (= 2 (ct-eval ctx "(count (vals (hash-map :a 1 :b 2)))")) "vals count") (assert (= true (ct-eval ctx "(= (merge (hash-map :a 1) (hash-map :b 2)) (hash-map :a 1 :b 2))")) "merge")) @@ -52,7 +52,7 @@ # 4. Empty and seq # ============================================================ (print "4: empty? and seq...") -(let [ctx (init)] +(let [ctx (init-cached)] (assert (= true (ct-eval ctx "(empty? (hash-map))")) "empty? true") (assert (= false (ct-eval ctx "(empty? (hash-map :a 1))")) "empty? false") (assert (= 1 (ct-eval ctx "(count (seq (hash-map :a 1)))")) "seq count")) @@ -62,7 +62,7 @@ # 5. Larger maps # ============================================================ (print "5: larger maps...") -(let [ctx (init)] +(let [ctx (init-cached)] (eval-string ctx " (def big-map (reduce (fn [m i] (assoc m (keyword (str \"k\" i)) i))