Build.cachedir sat under TMPDIR, which dune makes private per run, so no test run ever reused an object and every build in the suite was cold. It moves to $XDG_CACHE_HOME/flan/objcache (FLAN_CACHE_DIR overrides), which is safe because the keys are total: compile_c digests the source text, the compiler's stamp and every flag; wasm_resource_dir digests the builtins archive; compiler_object digests flan.cmxa and flan.a. Writes were already .tmp-then-rename, so concurrent dune jobs are fine. Macro.key was the one key that was not total -- prelude text plus the call's forms, and nothing about the compiler whose codegen produced the .so it names, which is dlopened straight back into this binary. Under a per-run TMPDIR that never showed; under a durable cache it is a stale expander that crashes rather than a compile error. It carries the compiler's stamp now, handed across start_merged's exec in FLAN_COMPILER_STAMP because a merged dev binary lives at a per-session path and keying on that rebuilt a macro module every dev start. Measured on dev-repl.flan, launch to bound socket: 2.0s cold against 0.48s warm. Whole-program flan build: 1.44s against 0.06s. Full dune test 25.7s/30.1s before, 24.0s after, user CPU ~50s down to ~34s. And the await: one timer covered two waits, a build then a bind, so 'the daemon never listened' was a wrong diagnosis of a build that had not finished. listening now polls the process alongside the socket and says which -- exited with a status, or still running and therefore still building. A daemon that dies fails in milliseconds instead of costing the whole timeout. Thirty seconds, down from a minute, because the build it waits on is warm now.
Description
Languages
OCaml
67.2%
Emacs Lisp
15.2%
C
10.4%
HTML
2.9%
Standard ML
2.8%
Other
1.5%