Fix -m arg drop under whole-program cache (jolt-4mui) + RFC 0003 sync (#138)

* Bind *command-line-args* after the deps-image cache swap (jolt-4mui)

Under whole-program (deps-image cache active), `jolt -m NS ARG` dropped ARG:
run-main set *command-line-args* on the current ctx, but a cache HIT then
replaced ctx with the saved image (via `set ctx cached`), whose *command-line-
args* was whatever got baked when the image was saved. The stale binding won at
`(apply NS/-main *command-line-args*)`, so -main ran with the wrong (usually
default) args — silently, for any optimized -m program.

Move set-command-line-args to AFTER the cache swap so it binds on the final ctx.
Repro/regression in deps-cache-args-test.janet: first run builds the image
(arg "first"), second run (cache hit) must echo "second", not the baked "first".

* docs: RFC 0003 — phm is a HAMT, sorted colls a red-black tree

The transients RFC described phm as "bucket-based copy-on-write" and mused about
"if it ever becomes a HAMT" — it is one now (jolt-684u), and sorted maps/sets are
a red-black tree (jolt-0hbr). Update the deviation/future-work notes accordingly.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-16 13:34:08 +00:00 committed by GitHub
parent 82525b6a81
commit 307b65b45b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 4 deletions

View file

@ -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.

View file

@ -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))))

View file

@ -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!"))