The globals section attributed a frame by its slot fingerprint, which is the wrong cut for it: a redefined body can name entirely different globals while binding identical locals, so the check saw no change and the new body's reference set went into the union under the old body's frame, with the frame numbers beside an entry saying so. So a second fingerprint. Reach.ref_fingerprint hashes the set of globals a body names — sorted and deduplicated, because a reference set is not ordered, where slot indices make the slot fingerprint order-sensitive on purpose — and it travels the path the first one already cut: %fninfo, flan_dev_frame_refsig, the agent's snapshot, the backtrace line, Dev.globals_op. Different means the frame is skipped by name with its reason, and the rest of the stack still contributes. Two numbers rather than one, because they are two facts. A frame whose slots match and whose globals do not has locals that are perfectly readable and attribution that is not, and a combined hash would make locals refuse a frame with nothing wrong with it. locals still checks the slot fingerprint alone. It lives in reach.ml because expr_refs is already the walk that answers what a body refers to, and is the walk the union itself is built from. One consequence: emit now reaches reach, which closes a cycle through Load if cimport calls Build.cachedir, so the header cache spells the object cache directory itself. test_dev.ml drives the exact case — a body that binds identical locals and names untouched where the stopped frame names pressure. With the check disabled it fails twice: the missing refusal, and untouched appearing under frame 0.
Description
Languages
OCaml
67.2%
Emacs Lisp
15.2%
C
10.4%
HTML
2.9%
Standard ML
2.8%
Other
1.5%