W14: pin crit-2 signal-return kont non-vacuously (test-only)

crit-2's failure mode discards every frame outside the signal site —
including the covering test's own assert — which is why the shipped test
"signal returns handler value to call site" passed vacuously pre-fix. A
plain assert pin would inherit that vacuity on regression.

Add suite gate-crit2-signal-return-kont with a side-effect sentinel: test 1
runs the core.md repros ((list "outer" (handler-bind ... (+ 1
(signal-condition 5))) "end") -> ("outer" 43 "end"); raise-continuable ->
143) then set!s a top-level flag; test 2 independently asserts the flag, so
a dropped continuation fails loudly even though test 1 would "pass". Third
test pins the shipped-test expression (51). 267 passed / 0 failed under
OCaml run_tests.

Test-only: no semantics edits, no push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:06:46 +00:00
parent 88e03daf4b
commit cbdde5fe63
2 changed files with 68 additions and 1 deletions

View File

@@ -161,3 +161,59 @@
(quote (embed))
(quote (param))
(quote (track))))))
;; --------------------------------------------------------------------------
;; crit-2 [W1, critical] signal-return frame stored the saved kont under :f
;; but the reader looked up "saved-kont" — the resume kont was always nil,
;; so the handler value became the WHOLE program's result and every frame
;; outside the signal site (including the covering test's own assert!) was
;; silently discarded. The shipped test "signal returns handler value to
;; call site" passed VACUOUSLY — the bug defeated its own test.
;;
;; A plain assert around the repro would inherit the same vacuity on
;; regression (the dropped continuation includes the assert frame). So this
;; pin uses a side-effect sentinel: test 1 runs the repro and then sets a
;; flag; test 2 independently asserts the flag was reached. If crit-2
;; regresses, test 1 still "passes" (vacuously) but test 2 FAILS.
;; Repro (core.md).
;; --------------------------------------------------------------------------
(define *gate-crit2-after-signal* false)
(define *gate-crit2-result* nil)
(define *gate-crit2-rc-result* nil)
(defsuite
"gate-crit2-signal-return-kont"
(deftest
"continuable signal resumes at the raise site"
(do
(set!
*gate-crit2-result*
(list
"outer"
(handler-bind
(((fn (c) true) (fn (c) 42)))
(+ 1 (signal-condition 5)))
"end"))
(set!
*gate-crit2-rc-result*
(handler-bind
(((fn (c) true) (fn (c) (+ c 100))))
(+ 1 (raise-continuable 42))))
(set! *gate-crit2-after-signal* true)
(assert= *gate-crit2-result* (list "outer" 43 "end"))
(assert= *gate-crit2-rc-result* 143)))
(deftest
"non-vacuity sentinel: the continuation after the signal actually ran"
(do
(assert
*gate-crit2-after-signal*
"continuation dropped — crit-2 regressed (previous test passed vacuously)")
(assert= *gate-crit2-result* (list "outer" 43 "end"))
(assert= *gate-crit2-rc-result* 143)))
(deftest
"handler value feeds the arithmetic frame, not the program result"
(assert=
(handler-bind
(((fn (c) true) (fn (c) (* c 10))))
(+ 1 (signal-condition 5)))
51)))