diff --git a/docs/rfc/0003-transients.md b/docs/rfc/0003-transients.md index a2a62c1..8c451db 100644 --- a/docs/rfc/0003-transients.md +++ b/docs/rfc/0003-transients.md @@ -53,7 +53,7 @@ are identical (O(n) total either way) with a better constant in the loop and a worse constant at the two edges. The pattern transients exist for — batch construction — is fully served. What is NOT served is transient-editing a *large* collection to change a few keys: that's O(n) in Jolt vs O(log n) in -Clojure, because `transient` flattens the pvec trie / phm buckets into a +Clojure, because `transient` flattens the pvec trie / phm HAMT into a native array/table and `persistent!` rebuilds them. **No thread-ownership check.** JVM Clojure ≥1.7 also dropped the owner-thread @@ -110,7 +110,9 @@ tiers as ordinary migration batches. - pvec is already a 32-way trie with structural sharing (pv.janet), so Clojure-style O(1) `transient`/`persistent!` via editable nodes is a real option for vectors — an internal change behind the same surface, not a - semantics change. phm is bucket-based copy-on-write; the same trick applies - if it ever becomes a HAMT. + semantics change. phm is now a HAMT with structural sharing too (jolt-684u), + and sorted maps/sets are a red-black tree (jolt-0hbr), so the same editable- + node trick is open for those as well — the transient surface here is still the + copy-to-native-table flatten. - `transient?` (Jolt extension, useful in tests) stays; Clojure has no public predicate, so it must not leak into portability-sensitive code. diff --git a/src/jolt/main.janet b/src/jolt/main.janet index 6e93e63..83d0fca 100644 --- a/src/jolt/main.janet +++ b/src/jolt/main.janet @@ -477,7 +477,6 @@ (defn- run-main [ns-name argv] (when (nil? ns-name) (eprint "Error: -m/--main requires a namespace") (os/exit 1)) - (set-command-line-args argv) (try (do (def path (deps-image-path ns-name)) @@ -495,6 +494,10 @@ (when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx))) (put (ctx :env) :infer-program-done? true)) (save-deps-image ctx path))) + # Bind *command-line-args* on the FINAL ctx, AFTER any cache swap: a cache + # hit replaces ctx with the saved image, which carries the args baked when + # it was saved — the current run's argv must win (jolt-4mui). + (set-command-line-args argv) (load-string ctx (string "(apply " ns-name "/-main *command-line-args*)"))) ([err fib] (report-error err fib) (os/exit 1)))) diff --git a/test/integration/deps-cache-args-test.janet b/test/integration/deps-cache-args-test.janet new file mode 100644 index 0000000..502fdf1 --- /dev/null +++ b/test/integration/deps-cache-args-test.janet @@ -0,0 +1,41 @@ +# The deps-image cache must not drop -main's command-line args under +# whole-program (jolt-4mui). A cache HIT swaps the running ctx for the saved +# image; *command-line-args* must be (re)bound from the CURRENT invocation's +# argv, not the stale value baked into the image. Drives the built binary: +# first run builds the cache (arg "first"), second run (cache hit) passes "second" +# — both must echo their own arg. Skips if build/jolt is absent. +(def repo (os/cwd)) +(def jolt (string repo "/build/jolt")) +(if-not (os/stat jolt) + (do (print "deps-cache-args: SKIP (no build/jolt)") (os/exit 0))) + +(def dir (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-dca-" (os/time))) +(os/mkdir dir) +(spit (string dir "/echoargs.clj") + "(ns echoargs)\n(defn -main [& args] (println \"GOT\" (pr-str (vec args))))\n") +# fresh per-run image cache dir so the first run is a guaranteed miss +(def imgdir (string dir "/imgcache")) (os/mkdir imgdir) + +(each [k v] [["JOLT_DIRECT_LINK" "1"] ["JOLT_WHOLE_PROGRAM" "1"] + ["JOLT_PATH" dir] ["JOLT_APP_PATHS" dir] ["JOLT_IMAGE_CACHE_DIR" imgdir]] + (os/setenv k v)) +(defn- run [arg] + (def p (os/spawn [jolt "-m" "echoargs" arg] :p {:out :pipe :err :pipe})) + (def out (:read (p :out) :all)) + (os/proc-wait p) + (string (or out ""))) + +(var fails 0) +(defn check [label got want] + (if (string/find want got) (print " ok " label) + (do (++ fails) (printf " FAIL %s: want %p in %p" label want got)))) + +(def r1 (run "first")) # cache MISS — builds + saves the image +(check "first run passes its arg" r1 `GOT ["first"]`) +(def r2 (run "second")) # cache HIT — must use the NEW arg, not the baked one +(check "second run (cache hit) passes its arg, not the baked one" r2 `GOT ["second"]`) + +(defn rmrf [p] (when (os/stat p) (if (= :directory (os/stat p :mode)) (do (each e (os/dir p) (rmrf (string p "/" e))) (os/rmdir p)) (os/rm p)))) +(rmrf dir) +(if (> fails 0) (do (printf "deps-cache-args: %d FAILED" fails) (os/exit 1)) + (print "deps-cache-args (jolt-4mui) passed!"))