convert log-entry conditions into dynamic handlers #61
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.
Follow on from #60 as we accumulate more of these predicates.
Background
There are four† nonlocal control flow constructs (/ families) in Lisp, two lexical and two dynamic. First, the lexical ones:
block+return-from— lexically scoped, single return point.tagbody+go— lexically scoped, multiple jump points, no return value.One can close over the lexical symbols used to define the control points, so, e.g., an
fletcangoto atagbodypoint which encloses the flet, i.e., is defined outside of the body of the local function.Then, the dynamic ones:
signal(/error/cerror/ ...) — Invocation point for dynamically scoped condition handlers. Re-raising from within a handler only cascades further up the stack from the installation point of that handler.handler-bind— Installs handlers which match on condition subtypes. Each handler will process until one performs a nonlocal transfer of control that interrupts the chain (e.g.,return-froming out of the handler entirely). If no handler so transfers control, execution finally resumes after the originatingsignal.handler-case— Likehandler-bind, but control will continue after the case statement after the first matching handler. (You can think of it as a macro built out ofhandler-bind, which installs its own enclosingblockwhich getsreturn-fromed at the conclusion of each handler.)invoke-restart— Invocation point for dynamically scoped restarts. Re-raising from within a restart begins again from the innermost scope.‡restart-bind— Matches exactly on a symbol, so no opportunity for subtyping (hence neither for user extensibility of the type tree). Instead, restarts can supply an optional predicate function which guards applicability.‡ Again, each handler is serviced in turn until one breaks out with a nonlocal transfer of control. Whenever they are applicable, these are advertised in the debugger.restart-case—handler-caseis tohandler-binasrestart-caseis torestart-bind.(I hope I got this roughly right!!)
Innards of this PR
I want to let users dynamically install predicates which guard whether
log-entryhas any effect. This has the following key features:log-entryneeds to evaluate a family of predicates which are unknown to it at the time of its own definition.(<= some-start-time (now))), which matches the behavior of restarts rather than of condition handlers.log-entry, not of whatever scope at which the user's predicate was defined. The return point forlog-entryis lexically defined.We fit together the above constructs to realize this in the following way:
'logging-entryto name a restart family whose main job is to evaluate custom predicates.log-entrysets up its ownhandler-caseto look for untrapped conditions of typelogging-entry, which transfers to its own lexically defined point of exit. (A previous version of this PR used restart-case here, but this was visible in the debugger, which I found distasteful.)only-log-whento encourage programmers to use these in the intended way. Eachonly-log-whengenerates arestart-bindto install a new predicate, which hooks into the above flow.What users see
See the included test for an illustration of what this looks like to a user.
Alternatives
return-from log-entryon their disjunction. The upside is that this would let a user uninstall predicates without leaving their scope. The downside is that it’s obviously more manual. It might also be more idiomatic; I don’t know whether this PR qualifies as an abuse of the restart system.Footnotes
† — There is also
throw+catch, but they're very inflexible and rarely used outside of "target to compile down to". There is also alsounwind-protect, but it’s of a rather different flavor to anything relevant here.‡ — Confusingly, these predicates are required to take a condition object, despite conditions having to do with handlers and not with restarts.