A breakpoint, which is only error under a restart-case

This commit is contained in:
Joseph Ferano 2026-09-12 12:25:08 +07:00
parent 81d46c342e
commit 4857db1fc4
3 changed files with 44 additions and 0 deletions

View File

@ -389,3 +389,22 @@ is stopped inside it, and the new body happens to have the same number of slots
of the same types, the frame will show the *new* names against the *old*
values. The check that should catch this does not fire. There is a failing test
pinned to it, so this is recorded rather than lurking.
## Stopping on purpose
`(pause)` stops the program where it stands and hands it to the break loop.
`C-c C-b` then shows the stack, `TAB` opens a frame's locals, and taking
`continue` resumes at the call as though nothing happened.
It is spelled `pause` rather than `break` because `break` is reserved for
leaving a loop — the same word meaning "exit this loop" and "stop for
inspection" in the same position would be the worst available collision.
Nothing in the compiler implements it. It is an `error` under a `restart-case`,
written in the prelude, which is what a breakpoint *is* in a language that
already has conditions. One consequence worth knowing: a `handler-bind` above
it can intercept a `Pause` and decline to stop, so a release build can neuter
every breakpoint in the program without editing any of them.
**Untested.** It compiles and the shape is right, but nobody has run it into a
real break loop yet.

View File

@ -44,6 +44,28 @@ let source = {flan|
;; handler or the break loop, where a working allocator is known.
(defstruct StorageExhausted [bytes i64 align i64 allocator i64])
;; A breakpoint. (pause) stops the program where it stands and hands it to the
;; break loop, with the whole stack under it readable C-c C-b lists the
;; frames, TAB opens one, and taking `continue` resumes at the call.
;;
;; It is spelled `pause` and not `break` because `break` is reserved for
;; leaving a loop (parse.ml refuses it by name, with the milestone), and a
;; breakpoint and a loop exit in the same word would be the worst kind of
;; collision: both are legal in the same place and mean opposite things.
;;
;; Nothing in the compiler knows about this. It is `error` under a
;; `restart-case`, which is exactly what a breakpoint is in a language that
;; already has conditions: the break loop is entered because nothing handled
;; the condition, and `continue` is an ordinary restart whose body is empty, so
;; taking it returns here and the caller carries on. A handler-bind above it
;; can therefore also intercept a Pause and decline to stop, which is the
;; behaviour a release build wants and gets for free.
(defstruct Pause [])
(defn pause []
(restart-case (error (Pause {}))
(continue [] (do))))
;; A seeded PRNG in Flan rather than libc's, because a grid hash is only a
;; regression test if the sequence is byte-identical on native and wasm32
;; (plan.org, RNG is ours). PCG-XSH-RR 32: one u64 LCG step per draw, folded

View File

@ -418,6 +418,8 @@
col (/ (i32 (.x m)) cell-size)]
(paint-at row col)))
(defstruct Pause [])
;; Every cross-function call in a dev build routes through an indirection cell,
;; so redefining this from the REPL reaches the running loop on the next frame.
;; No `varfn` (Janet), no `let update = ref` (OCaml), no var-routing (jank).
@ -439,6 +441,7 @@
(when (rl/key-released? :space) (next-color) (plink 1.4))
(when (rl/mouse-button-pressed? :right) (reset-view) (plink 0.6))
(when (rl/mouse-button-pressed? :left) (plink 1.0))
(when (rl/key-pressed? :f) (error (Pause {})))
(when (rl/key-pressed? :m) (toggle-music))
;; The pad, when there is one. Pressed and released are separate events here
;; too, for the same reason.