with-continuation-mark based tracer #169
Merged
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.
This PR implements a new strategy to instrument code via the
with-continuation-mark
feature. Note that usingw-c-m
naively, like how theerrortrace
library does, gives a suboptimal stack trace, so we use a different algorithm that gives a better result (see details below).The new strategy is more efficient than the current one. Here's a benchmark result, obtained from running
test/trace/perf-runner.rkt
.The new strategy preserves proper tail recursion (as does
errortrace
): space complexity does not change after instrumentation. This seems to have a side effect of speeding up programs with tail recursion significantly, as shown in the above benchmark (tail.rkt
). However, this property is a double-edged sword because it means some logical stack frames must be elided. In practice, the elision doesn't seem to affect debuggability much, as most other frames can still provide useful information, and it is very rare for a symbolic program to have tail recursion due to path merging, so everything seems to work out just fine.Instrumentation
See the proposal to improve Racket's
errortrace
at https://www.mail-archive.com/racket-users@googlegroups.com/msg44786.html for the background and see Matthew's suggestion for an efficient implementation strategy that I ended up basing my algorithm on.It turns out that the proposal doesn't quite work as I expected. Consider the program:
By using the mentioned algorithm, the
add1
frame will be kept, even though we have not really calledadd1
yet when the error occurs.To filter out the
add1
frame, we refine the algorithm by assigning a flag saying that a mark is "uncertified" every time we attach a new one. Therefore, bothadd1
andfact
frames will be uncertified initially. Then, in every function body, we certify its frame. This causesfact
to be certified while leavingadd1
uncertified. By throwing away uncertified frames when an error occurs, we obtain the stack trace as desired.The actual implementation is slightly more complicated because we need to work with the expanded code. Not all function calls are user-written, so we should not add new information for these calls (e.g.,
branch-and-merge
should not appear in the stack trace).Limitation
The above algorithm relies on a function to certify its frame. This means that if the function is not instrumented (especially for higher-order functions that are defined in stdlib), the uncertified frame will be missing. This could be seen as either a bug or a feature. Arguably, these missing frames are not useful on their own, but their absence might be surprising.
Tests
Interestingly, from all 31 existing tests, only one outputs differently after switching to the new strategy. This suggests that the new strategy is highly compatible with the current one. For the (only one) test that failed, it is due to the limitation described above (
list.rkt
misses a frame due to the higher-order, built-in functionmap
).