A restart is not a transaction, and nothing rolls back
If a frame mutates a global and then signals, taking a retry re-runs the mutation. Control resumes at the restart-case and runs forward; nothing is undone. Common Lisp has the same property and offers no help either, so this is written down rather than fixed. The discipline is that the author chooses where the retry boundary is: a restart-case above the mutations re-runs them, one below re-runs only what follows. Put the restart before anything mutates, make the retried section idempotent, or snapshot what will be re-applied. It matters more here than in most Lisps because the intended use is a game loop, and a bad index signalling BoundsError rather than ending the process made abandoning and retrying a frame an ordinary thing to do. conditions.org and web/index.html already carried the mechanical half as a one-line gotcha; those are rewritten in place rather than gaining a second bullet beside them. spec-conditions.md takes it in section 5, which already enumerates what a transfer does and does not do. No numbered case changed meaning.
This commit is contained in:
parent
61c41ed27c
commit
042b2ce4d0
24
BUILT.md
24
BUILT.md
@ -4445,3 +4445,27 @@ everything else there is arithmetic over a container header.
|
||||
failure — the region a container lived in was released, and there is no frame to go back to that would not read freed
|
||||
memory — and the map path was left alone rather than converted half-way. Written down here so it is a known edge
|
||||
rather than a discovery.
|
||||
|
||||
## A restart is not a transaction
|
||||
|
||||
Written down in three places rather than fixed, because it is a property and not a defect. If a frame mutates a global
|
||||
and then signals, taking a `retry` **re-runs the mutation**. Control resumes at the `restart-case` and runs forward
|
||||
from there; nothing is undone. Common Lisp has exactly this property and offers no help either — rollback would mean
|
||||
journalling every store, which is a different language.
|
||||
|
||||
The discipline is that **the author chooses where the retry boundary is**. A `restart-case` at the top of a frame
|
||||
re-runs everything including mutations already applied; one placed after the mutations re-runs only what follows them.
|
||||
So: put the restart before anything mutates, make the retried section idempotent, or snapshot what will be re-applied.
|
||||
§3 of `spec-conditions.md` places a restart *syntactically* — every clause body and the body share a type — and
|
||||
nothing places it semantically.
|
||||
|
||||
It matters more here than in most Lisps because the intended use is a **game loop**, where the plan is to skip a frame
|
||||
and carry on rather than die. That stopped being hypothetical when a failed bounds check began signalling
|
||||
`BoundsError` instead of ending the process (see "An index out of range is a condition"): a frame can now be abandoned
|
||||
and retried, which is exactly the case a non-idempotent mutation spoils.
|
||||
|
||||
Both `conditions.org` and `web/index.html` already carried the mechanical half — "a restart re-runs whatever sits
|
||||
between it and the target" — as a one-line gotcha. Those were rewritten in place into the full statement rather than
|
||||
having a second bullet added beside them, and the same reasoning went into `spec-conditions.md` §5, which is the
|
||||
section that already enumerates what a transfer does and does not do: it runs `defer`s, it skips `errdefer`s, and it
|
||||
does not undo. No numbered case changed meaning, so the freeze holds.
|
||||
|
||||
15
NEXT.md
15
NEXT.md
@ -5,21 +5,6 @@ that is what makes it checkable against the header — and the layer is where a
|
||||
already exist by hand in `vendor/raylib/raylib.flan` and are the shape to copy: `collision-point-poly?` takes a slice
|
||||
and `collision-lines` answers with an `Option`, each wrapping a `-raw` binding of the same name.
|
||||
|
||||
## Queued: a restart is not a transaction, and the docs must say so
|
||||
|
||||
Raised by the author, and it is a real sharp edge rather than a gap. **If a frame mutates a global and then signals,
|
||||
taking a `retry` re-runs the mutation.** Nothing rolls back. Common Lisp has exactly this property and offers no help
|
||||
either — restarts are explicitly not transactional.
|
||||
|
||||
The discipline is that **the author chooses where the retry boundary is**: a `restart-case` at the top of a frame
|
||||
re-runs everything including mutations already applied; one placed after the mutations re-runs only what follows. So
|
||||
either put the restart before anything mutates, make the retried section idempotent, or snapshot what will be
|
||||
re-applied.
|
||||
|
||||
This matters more here than in most Lisps because the intended use is a **game loop**, where the author's plan is to
|
||||
skip a frame and carry on rather than die — exactly the case where a non-idempotent mutation bites. Write it into
|
||||
`conditions.org` and `spec-conditions.md`'s prose, and into `web/index.html` beside the restart documentation.
|
||||
|
||||
## Queued: a dev-build allocation registry — address to type
|
||||
|
||||
Raised in conversation and wanted. **What it is:** in a dev build only, every allocation records what type it was
|
||||
|
||||
@ -81,9 +81,22 @@ the agent's socket instead.
|
||||
|
||||
- *A handler closes over nothing.* It is lifted into its own function.
|
||||
Accumulate into a global, or put the value on the condition.
|
||||
- *A restart re-runs whatever sits between it and the target.* Control resumes
|
||||
at the ~restart-case~, so a ~retry~ repeats side effects after it. Put the
|
||||
~restart-case~ where re-entry is safe.
|
||||
- *A restart is not a transaction.* Control resumes at the ~restart-case~ and
|
||||
runs forward from there, so a ~retry~ repeats every side effect between it
|
||||
and the target. Nothing rolls back: a global the frame already set stays set,
|
||||
and gets set again. Common Lisp has exactly this property and offers no help
|
||||
either — restarts are not transactional there and are not here.
|
||||
|
||||
So *you* choose where the retry boundary is. A ~restart-case~ at the top of a
|
||||
frame re-runs everything, mutations included; one placed after the mutations
|
||||
re-runs only what follows. Put the restart before anything mutates, make the
|
||||
retried section idempotent, or snapshot what will be re-applied.
|
||||
|
||||
This bites harder here than in most Lisps because the point is a *game loop*
|
||||
— skip the frame, carry on, don't die. Since a bad index signals
|
||||
~BoundsError~ rather than ending the process, abandoning a frame and retrying
|
||||
it is a real thing to do, and a non-idempotent mutation is what makes it go
|
||||
wrong.
|
||||
- *An unknown restart name is a hard stop.* No ~find-restart~ to test with.
|
||||
- *So are the wrong arguments*, and for the same reason: nothing static can
|
||||
know what a name will find. The message names both signatures.
|
||||
|
||||
@ -117,6 +117,28 @@ Invoking a restart transfers control outward past zero or more frames.
|
||||
the signalling frame dies. Anything a handler keeps must be copied out
|
||||
(conditions are value structs, so `(push errors c)` copies).
|
||||
|
||||
**A restart is not a transaction.** The list above is the whole of what a
|
||||
transfer does: it runs `defer`s and it moves control. It does not undo. Control
|
||||
resumes at the `restart-case` and runs forward from there, so a `retry` re-runs
|
||||
every effect between the restart and the target — a global the frame already
|
||||
assigned stays assigned, and is assigned again. This is not a gap to be closed
|
||||
later. Common Lisp has exactly this property and offers no help either; rollback
|
||||
would mean journalling every store, which is a different language.
|
||||
|
||||
What follows is a discipline rather than a mechanism: **the author chooses where
|
||||
the retry boundary is.** A `restart-case` at the top of a frame re-runs
|
||||
everything, mutations included; one placed after the mutations re-runs only what
|
||||
follows them. So either put the restart before anything mutates, make the
|
||||
retried section idempotent, or snapshot what will be re-applied.
|
||||
|
||||
It matters more here than in most Lisps because of the intended use. A game loop
|
||||
means to skip a frame and carry on rather than die, and a failed bounds check
|
||||
signalling `BoundsError` rather than ending the process makes abandoning and
|
||||
retrying a frame an ordinary thing to do — which is precisely the case where a
|
||||
non-idempotent mutation bites. §3's rule that every clause body and the body
|
||||
share a type places the restart syntactically; nothing places it *semantically*,
|
||||
and that choice is the author's.
|
||||
|
||||
## 6. Crossing compiler-generated frames
|
||||
|
||||
Transfer is lowered **explicitly** — result propagation plus branch targets — not
|
||||
|
||||
@ -1024,9 +1024,20 @@ a guard after each call.</p>
|
||||
of its own, because it runs from wherever the signal was. Accumulate into a global, or
|
||||
put the value on the condition. A reference to an enclosing local is refused for that
|
||||
reason rather than reported as an unknown name.</li>
|
||||
<li><strong>A restart re-runs whatever sits between it and the target.</strong> Control
|
||||
resumes at the <code>restart-case</code>, so a <code>retry</code> repeats the side
|
||||
effects after it. Put the <code>restart-case</code> where re-entry is safe.</li>
|
||||
<li><strong>A restart is not a transaction.</strong> Control resumes at the
|
||||
<code>restart-case</code> and runs forward from there, so a <code>retry</code> repeats
|
||||
every side effect between it and the target. Nothing rolls back — a global the frame
|
||||
already set stays set, and is set again. Common Lisp has exactly this property and
|
||||
offers no help either.
|
||||
<p>So the author chooses where the retry boundary is. A <code>restart-case</code> at the
|
||||
top of a frame re-runs everything including mutations already applied; one placed after
|
||||
the mutations re-runs only what follows. Put the restart before anything mutates, make
|
||||
the retried section idempotent, or snapshot what will be re-applied.</p>
|
||||
<p>This matters more here than in most Lisps because the intended use is a
|
||||
<em>game loop</em>, where the plan is to skip a frame and carry on rather than die. Now
|
||||
that a bad index signals <code>BoundsError</code> instead of ending the process,
|
||||
abandoning a frame and retrying it is a real thing to do — and that is exactly the case
|
||||
a non-idempotent mutation spoils.</p></li>
|
||||
<li><strong>An unknown restart name is a hard stop</strong> — a located runtime error.
|
||||
There is no <code>find-restart</code> to test with yet.</li>
|
||||
<li><strong>No supertype</strong>, so nothing can say "any condition".</li>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user