A name finds one frame; it does not search for one that fits

§4 meets §3, and the answer a reader will assume is the other one. An
inner (use-value [s string] ...) shadows an outer (use-value [v i32] ...),
so an i32 is refused there and the outer clause that would have taken it
is never consulted. Searching outward for a frame whose signature fits
would make which restart runs depend on the arguments, which is overload
resolution on a dynamic stack.

Also: neither of the new guards is a bounds check, so --no-bounds-checks
does not remove them. A wrong index is a wrong answer; a transfer into a
clause whose parameters were written to a different layout is not.
This commit is contained in:
Joseph Ferano 2026-09-12 10:51:00 +07:00
parent 5184d732c9
commit 7c1fcbff19
4 changed files with 36 additions and 0 deletions

View File

@ -1163,6 +1163,17 @@ unfilled, `invoke-restart` marks it filled, and a clause with parameters checks
top of `NEXT.md`: the answer is a Flan expression, and there is already something that compiles one against the live
program.
**Lookup stayed by name, and the signature is checked against what it found.** `flan_find_restart` matches the name
hash and nothing else, so an inner `(use-value [s string] ...)` shadows an outer `(use-value [v i32] ...)` and an i32
is refused there — the outer frame that would have taken it is never consulted. That is §4 read straight ("the first
frame offering the name") and it is the thing a reader will assume works the other way, so `restarts.flan` has a case
for it. Searching outward for a frame whose signature fits would make which restart runs depend on the arguments,
which is overload resolution on a dynamic stack.
Both of these guards go through `fail_block` unconditionally, unlike the bounds checks: `--no-bounds-checks` does not
remove them. A wrong index is a wrong answer, and a transfer into a clause whose parameters were never written, or
written to a different layout, is memory corruption.
`runtime/flan_rt.c` gained two message functions and nothing else. The restart frame's first four fields are the ones C
declares and their offsets do not move; everything §3 needed is appended after them, and C never allocates one.

View File

@ -87,6 +87,10 @@ the agent's socket instead.
- *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.
- *Lookup is by name; the signature is checked after it.* Nothing searches for
a frame the arguments would fit. An inner ~(use-value [s string] ...)~
shadows an outer ~(use-value [v i32] ...)~, so ~(invoke-restart 'use-value
21)~ stops the program even though the outer clause would have taken it.
- *No supertype*, so nothing can say "any condition".
- *~signal~ cannot hand a value back.* Deliberate (§1).
- A condition must be a *struct*. ~return~ is refused inside either form.

View File

@ -87,6 +87,20 @@
(handler-bind [(AssetMissing [c] (invoke-restart 'retry 1))]
(supplied n)))
;;; §4 meets §3: lookup is by *name*, and the signature is checked against
;;; whatever that finds. An inner frame offering use-value shadows the outer
;;; one, so an i32 is refused here even though the outer clause would have
;;; taken it — a name finds one frame, it does not search for a fitting one.
(defn shadowed [n i32] i32
(restart-case
(restart-case (middle n)
(use-value [s string] (do (print s) 0)))
(use-value [v i32] v)))
(defn mislaid [n i32] i32
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
(shadowed n)))
(defn main [args [string]] i32
;; One argument selects a trap; none runs the table's case.
(if (> (len args) 1)
@ -95,6 +109,7 @@
(= k 1) (print (mismatched 90))
(= k 2) (print (mistyped 91))
(= k 3) (print (overfull 92))
(= k 4) (print (mislaid 93))
:else (println "?"))
(return 0)))

View File

@ -298,6 +298,12 @@ let () =
"restart use-value takes (i32), given (string)";
refuses "arguments given to a restart that takes none" "3"
"restart retry takes (), given (i32)";
(* §4 meets §3. The name finds the innermost frame offering it and the
signature is checked against *that*; nothing searches outward for a
frame the arguments would have fitted, and an outer clause that would
have taken them is not consulted. *)
refuses "a shadowing clause of the same name and a different signature" "4"
"restart use-value takes (string), given (i32)";
(try Sys.remove exe with Sys_error _ -> ())
in
restart_mismatch ();