fix(trace): tape-record file_exists / ls / mkdir / getcwd / exe_path (#585)#668
Merged
Conversation
…585) These five fs builtins predated the trace tape and were neither recorded nor replay-blocked, so under EIGS_REPLAY they ran LIVE: a program branching on `file_exists of p` replayed against the current filesystem, not the recorded one — a tape recorded on one machine silently diverged on another. Same closed-world hole #471 closed for `args` and #148 covered for proc_*. The inconsistency was visible next to the already-taped is_dir (#576) and read_line (#558) in the same BUILTINS.md table. Each is now wrapped with the TAKE/RECORD pair (their live paths do real work — fopen, readdir bulk-list construction, a syscall — before the return exists, so TRACE_NONDET_RET alone would run the live work under replay). Under EIGS_REPLAY the early TAKE short-circuits before any fs access and serves the recorded value. mkdir is the design call: a WRITE whose return (a success bit) is filesystem-dependent. It is Recorded rather than #148-non-replayable because that bit IS pinnable by the tape (unlike a subprocess fd), and under replay the TAKE short-circuits before mkdir(2) — so the recorded bit is served and the directory is NOT created a second time. Replay does not re-run the write side effect, the same rule as the subprocess/audio boundary. Oracle: test_replay.sh records with the fs in one state, mutates it, and replays; the recorded answer must win (a live probe would now disagree). Verified JIT on and EIGS_JIT_OFF. mkdir additionally asserts the directory is not re-created on replay. exe_path uses the same getcwd pattern but is invariant between record and replay (same interpreter), so it is documented rather than mutation-tested. docs/TRACE.md lists the five under Recorded Builtins with mkdir's write-side-effect note; docs/BUILTINS.md marks each trace-recorded, closing the side-by-side inconsistency the issue flagged. Consumer gates green: liferaft/test/replay.sh (unaffected, N=1 argv only), tidelog/test/replay.sh (calls file_exists; byte-for-byte identical). Suite 3090/3090 release, 3094/3094 ASan+UBSan (detect_leaks=1, tally 0); test_replay.sh 23/23 incl. strict-mode.
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.
Closes #585
Five filesystem builtins predated the trace tape and were neither recorded nor replay-blocked, so under
EIGS_REPLAYthey ran live. A program branching onfile_exists of preplayed against the current filesystem, not the recorded one — a tape recorded on one machine silently diverged on another. The same closed-world hole #471 closed forargsand #148 covered forproc_*, and it sat visibly next to the already-tapedis_dir(#576) in the sameBUILTINS.mdtable.Reproduction (before)
Fix
Each is wrapped with the
TAKE/RECORDpair — their live paths do real work (fopen,readdirlist construction, a syscall) before the return value exists, soTRACE_NONDET_RETalone would still run that work under replay. The earlyTAKEshort-circuits before any fs access and serves the recorded value.mkdir— the design decisionmkdiris a write whose return (a success bit) is filesystem-dependent. It's Recorded, not #148-non-replayable, because that bit is pinnable by the tape (unlike a subprocess fd). UnderEIGS_REPLAYtheTAKEshort-circuits beforemkdir(2), so the recorded bit is served and the directory is not created a second time — replay doesn't re-run the write side effect, the same rule as the subprocess/audio boundary.Oracle (transcript, both tiers)
test_replay.shrecords with the fs in one state, mutates it, and replays — the recorded answer must win:file_existsEIGS_JIT_OFF)lsmkdirgetcwd/exe_pathuses the samegetcwdpattern but is invariant between record and replay (same interpreter binary), so it's documented rather than mutation-tested.Consumer gates
The reason the trace-tape skill mandates checking them — one consumer calls these:
test/replay.sh: unaffected (calls none of the five), stillN=1(argv only).test/replay.sh: callsfile_exists; byte-for-byte identical record vs replay. The change improves its determinism —file_existsno longer runs live under replay.Docs
docs/TRACE.mdlists the five under Recorded Builtins withmkdir's write-side-effect note;docs/BUILTINS.mdmarks each trace-recorded, closing the side-by-side inconsistency the issue flagged.Gates
detect_leaks=13094/3094, leak tally 0test_replay.sh23/23 including strict-mode (exit 3 on name mismatch)🤖 Generated with Claude Code