Skip to content

Conversation

@ecpeterson
Copy link
Contributor

@ecpeterson ecpeterson commented Oct 12, 2025

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 flet can go to a tagbody point 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 originating signal.
    • handler-case — Like handler-bind, but control will continue after the case statement after the first matching handler. (You can think of it as a macro built out of handler-bind, which installs its own enclosing block which gets return-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-case is to handler-bin as restart-case is to restart-bind.

(I hope I got this roughly right!!)

Innards of this PR

I want to let users dynamically install predicates which guard whether log-entry has any effect. This has the following key features:

  • This is a dynamically scoped problem: log-entry needs to evaluate a family of predicates which are unknown to it at the time of its own definition.
  • I want users to be able to write whatever custom predicate they like (e.g., (<= some-start-time (now))), which matches the behavior of restarts rather than of condition handlers.
  • When one of these conditions fails, I want only to drop out of log-entry, not of whatever scope at which the user's predicate was defined. The return point for log-entry is lexically defined.

We fit together the above constructs to realize this in the following way:

  • We use 'logging-entry to name a restart family whose main job is to evaluate custom predicates.
  • log-entry sets up its own handler-case to look for untrapped conditions of type logging-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.)
  • If any of the restart predicates match, the actual restart which is triggered then signals such a condition.
  • We bundle the first two bits together into the macro only-log-when to encourage programmers to use these in the intended way. Each only-log-when generates a restart-bind to 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

  • We could have a dynamically bound set of predicates to check and return-from log-entry on 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 also unwind-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.

@ecpeterson ecpeterson requested a review from karalekas October 12, 2025 17:18
@karalekas karalekas merged commit 221c813 into main Oct 20, 2025
1 check passed
@karalekas karalekas deleted the extensible-logger-gates branch October 20, 2025 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants