Skip to content

fix(repl): make the stdlib typecheck cache actually save - #337

Merged
Ch4s3 merged 3 commits into
mainfrom
claude/agitated-elgamal-6f1f49
Aug 24, 2026
Merged

fix(repl): make the stdlib typecheck cache actually save#337
Ch4s3 merged 3 commits into
mainfrom
claude/agitated-elgamal-6f1f49

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The bug

~/.cache/march on my machine held 1,132 zero-byte stdlib_tcenv_<build>_<hash>.bin.<pid>.tmp files, oldest dating to 2026-08-17. They were staging files from Repl.save_cached_tc_env, which stages through a pid-suffixed temp and renames.

The save had never once succeeded. Typecheck.import_entry carries ie_matches : string -> bool, so any env that has folded a decl with a use/import/alias — i.e. every post-stdlib env — makes Marshal.to_channel raise Invalid_argument "output_value: functional value". The enclosing try ... with _ -> () swallowed it, and the already-created temp was never unlinked.

Three consequences, in descending order of visibility:

  1. one 0-byte orphan per REPL launch, forever;
  2. every REPL start paid the full stdlib typecheck (~0.7s here);
  3. the entire cache-hit path was dead code — [timing] tc_env cache hit had never been printed in a real session, and march warm-cache reported "built + cached" every single run while warming nothing.

The fix

In lib/repl/repl.ml:

  • marshalable_tc_env strips import_tracker and import_idx before the write. Sound for the same reason Lsp.Typecheck_cache.derive already 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.protect around the staged write unlinks the temp on any failure, so a future unmarshalable field costs a slow start, not an orphan per launch.
  • The 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_tmps removes <name>.<pid>.tmp files whose owning pid is gone, run on every REPL start. Same liveness-based shape as Repl_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's get_stdlib_tc_env writes 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

  • New test/test_repl_cache.ml (suite repl_cache, carried by run_compiler.exe): a save of an env with a populated import tracker must leave a non-empty blob that load_cached_tc_env reads 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.
  • Real REPL, fresh HOME: first start load_decls 0.701s, no temp left; second start [timing] tc_env cache hit: 0.083s + eval_decls 0.111s.
  • Hit/miss parity: a 12-line REPL script (list/enum/option/result ops, a type decl, a user fn, :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.sh all five suites green (compiler 934, eval 262, codegen 587, stdlib 868, stdlib_march 61); scripts/check-docs.sh passes.
  • Sweep against the real ~/.cache/march: 1132 → 6 temps in one REPL start; the six survivors are pid-reuse false negatives, confirmed by ps.

specs/progress/2026-08-24-repl-tcenv-cache-never-saved.md and a CHANGELOG.md entry land with the fix.

Ch4s3 added 3 commits August 24, 2026 15:42
`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.
@Ch4s3
Ch4s3 merged commit 2dff9d8 into main Aug 24, 2026
25 checks passed
@Ch4s3
Ch4s3 deleted the claude/agitated-elgamal-6f1f49 branch August 24, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant