fix(repl): make the stdlib typecheck cache actually save - #337
Merged
Conversation
`save_cached_tc_env` had never once succeeded. The env's `import_tracker` entries carry an `ie_matches : string -> bool` closure, so `Marshal.to_channel` raised `Invalid_argument "output_value: functional value"` on every REPL start; the surrounding `with _ -> ()` swallowed it and the pid-suffixed temp it had already created was never unlinked. Net effect: one 0-byte orphan per launch (1,132 had piled up in ~/.cache/march), a full stdlib typecheck on every start, and a cache-hit path that had never run outside tests. - `marshalable_tc_env` strips `import_tracker`/`import_idx` before the write, the same way `Lsp.Typecheck_cache.derive` already does — both fields only drive unused-import warnings for the decls that filled them, and nothing reports those for stdlib. - `Fun.protect` unlinks the staging temp on failure, so a future unmarshalable field costs a slow start rather than an orphan per launch. - The swallowed failure is now a stderr warning. - `sweep_stale_cache_tmps` removes `<name>.<pid>.tmp` files whose owning pid is gone, run on every REPL start — same liveness check as `Repl_jit.create`'s per-pid tmp dir sweep. `bin/main.ml`'s `get_stdlib_tc_env` shares the directory and the staging discipline, so it gets all three; its own writes were fine but it had left orphans from killed processes. Verified: fresh HOME, first start `load_decls 0.701s` with no temp left, second start `[timing] tc_env cache hit: 0.083s`; hit and miss paths produce byte-identical REPL output; `march warm-cache` now actually warms. New `repl_cache` suite fails on the pre-fix code.
…al-6f1f49 # Conflicts: # CHANGELOG.md
…al-6f1f49 # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
~/.cache/marchon my machine held 1,132 zero-bytestdlib_tcenv_<build>_<hash>.bin.<pid>.tmpfiles, oldest dating to 2026-08-17. They were staging files fromRepl.save_cached_tc_env, which stages through a pid-suffixed temp and renames.The save had never once succeeded.
Typecheck.import_entrycarriesie_matches : string -> bool, so any env that has folded a decl with ause/import/alias — i.e. every post-stdlib env — makesMarshal.to_channelraiseInvalid_argument "output_value: functional value". The enclosingtry ... with _ -> ()swallowed it, and the already-created temp was never unlinked.Three consequences, in descending order of visibility:
[timing] tc_env cache hithad never been printed in a real session, andmarch warm-cachereported "built + cached" every single run while warming nothing.The fix
In
lib/repl/repl.ml:marshalable_tc_envstripsimport_trackerandimport_idxbefore the write. Sound for the same reasonLsp.Typecheck_cache.derivealready does it: both fields exist only to drive unused-import warnings for the decls that populated them, and nothing reports those for stdlib. Imports typed at the REPL register on the live env either way.Fun.protectaround the staged write unlinks the temp on any failure, so a future unmarshalable field costs a slow start, not an orphan per launch.with _ -> ()is now a loud stderr warning. A cache whose only failure mode is "silently slower forever" is exactly how this survived.sweep_stale_cache_tmpsremoves<name>.<pid>.tmpfiles whose owning pid is gone, run on every REPL start. Same liveness-based shape asRepl_jit.create's per-pid tmp-dir sweep, and deliberately conservative — a recycled pid held by an unrelated live process keeps its file.bin/main.ml'sget_stdlib_tc_envwrites into the same directory with the same staging discipline, so it gets all three. Its own writes were succeeding, but it had left five orphans from killed processes.Verification
test/test_repl_cache.ml(suiterepl_cache, carried byrun_compiler.exe): a save of an env with a populated import tracker must leave a non-empty blob thatload_cached_tc_envreads back, and no.tmp; a dead-pid temp must be swept while a live-pid one survives. Both fail on the pre-fix code — the first with exactly the observed orphan filename.HOME: first startload_decls 0.701s, no temp left; second start[timing] tc_env cache hit: 0.083s+eval_decls 0.111s.typedecl, a userfn,:type) gives byte-identical output on both paths — worth checking precisely because the hit path had never executed before.march warm-cache:tc_env 0.799s (built + cached)cold,0.089s (cached)on the second run.scripts/run-tests.shall five suites green (compiler 934, eval 262, codegen 587, stdlib 868, stdlib_march 61);scripts/check-docs.shpasses.~/.cache/march: 1132 → 6 temps in one REPL start; the six survivors are pid-reuse false negatives, confirmed byps.specs/progress/2026-08-24-repl-tcenv-cache-never-saved.mdand aCHANGELOG.mdentry land with the fix.