fix: record lost a mark born during the block, and the backtrace of what threw - #25
Merged
sotashimozono merged 1 commit intoSep 7, 2026
Conversation
…hat threw Two defects in `record`, both found by reviewing #24 and both older than it. The first breaks the one thing the default layer promises. **A mark that came into existence WHILE the block ran was lost — from the always-on layer too.** `record` snapshotted the probe set before calling `f` and never looked again, so a probe born during the call was entered by code that ran, counted by nobody, and left with its flag `false` for the rest of the process. `entered()` and the exit summary never learned about it either, because while a recording is open the write side counts into the probe instead of setting the flag, and only `record`'s epilogue sets it — over the stale snapshot. record saw: [:tracked] entered(D) = [:tracked] …after: [:newborn, :tracked] entered(D) = [:newborn, :tracked] A package extension loaded inside the block is the ordinary way this happens, and this package ships three of them. The probe set is now re-derived after the call, `saved` is keyed by probe rather than by position, and the reconciliation runs over the union. Re-deriving needs `invokelatest`: the new probes' bindings are younger than the frame reading them, and 1.12 warns that will become an error. **The exception's backtrace pointed at `record`, not at the caller.** `throw(err)` after the `catch` block manufactures a fresh backtrace, so a user debugging a failed run saw `record.jl` and macro expansion where `outer → mid → deep → energy` should be, with nothing to say frames had been dropped — in exactly the case `record(f; rethrow = false)`'s own docstring names as the reason to use it. Closing now happens inside the `catch` and the exception is re-raised with `rethrow()`, which keeps the backtrace it arrived with. The old comment claiming this was impossible was wrong: it is impossible *after* the catch, which is where the call had drifted to. Both are pinned by tests that fail against the unfixed file, each with a control — a mark defined and never called is still absent, and the direct call is shown to carry the frames the recorded one must also carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
📚 Docs preview: https://codes.sota-shimozono.com/ExperimentalAPI.jl/previews/PR25/ (updates on each push to this PR) |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
sotashimozono
deleted the
fix/record-loses-newborn-probes-and-backtraces
branch
September 7, 2026 13:18
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.
Two defects in
record, both surfaced by reviewing #24 and both older than it. The first breaksthe one thing the default layer promises.
A mark born while the block ran was lost — from the always-on layer too
recordsnapshotted the probe set before callingfand never looked again. A probe that cameinto existence during the call was entered by code that ran, counted by nobody, and left with its
flag
falsefor the rest of the process — because while a recording is open the write sidecounts into the probe instead of setting the flag, and only
record's epilogue sets it, over thestale snapshot.
newbornhad run and returned a value in both cases.A package extension loaded inside the block is the ordinary way this happens, and this package
ships three of them. So
entered()and the exit summary — the layer whose whole claim is that itcannot miss an entry — would have said nothing.
The probe set is now re-derived after the call,
savedis keyed by probe rather than by position,and reconciliation runs over the union. Re-deriving needs
invokelatest: the new probes' bindingsare younger than the frame reading them, and 1.12 warns that will become an error.
The exception's backtrace pointed at
record, not at the callerthrow(err)after thecatchblock manufactures a fresh backtrace:A user debugging a failed run saw
record.jlwhere their own call chain should be, with nothingto say frames had been dropped — in exactly the case the docstring names as the reason to reach
for
rethrow = false. Closing now happens inside thecatchand the exception is re-raised withrethrow(), which keeps the backtrace it arrived with.The comment that claimed this was impossible was right about
Base.rethrow(err)outside a catchand wrong about the conclusion: the call had simply drifted to after the catch.
Tests
Both fail against the unfixed file, and each carries a control — a mark defined and never called
is still absent (so the fix did not start reporting everything it can see), and the direct call is
shown to carry the frames the recorded one must also carry.
1062 assertions, green.
🤖 Generated with Claude Code