diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 18732f9..5e1cae0 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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. diff --git a/lib/prelude.ml b/lib/prelude.ml index 5b1a212..5ce9bd5 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -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 diff --git a/sand.flan b/sand.flan index fe731fd..116f226 100644 --- a/sand.flan +++ b/sand.flan @@ -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.