flan/test/programs/signedness.flan
Joseph Ferano a5980734dc Tests for the cleanup paths nothing was watching
From a mutation-testing pass: about sixty small, plausible changes to the
compiler and runtime, each applied, run and restored. Nineteen of them left the
whole suite green. The compiler was right in every case - what was missing was
anything that looked.

The two programs here close the severe cluster. cleanup.flan covers six claims:
an early return runs the defers registered above it, and runs them innermost
first; a defer that calls something, which is what puts a guard inside a defer
on the transfer path; a transfer out of a handler-bind pops its frames; a
two-clause handler-bind pops both; and a signal stops once a handler has
answered it by transferring. The numbers differ per failure, so a wrong answer
names its own cause rather than just being wrong.

signedness.flan covers the ashr/lshr and slt/ult choices. Either could have been
hardcoded to one arm and nothing would have noticed, because no program in the
corpus shifted a negative integer right or compared an unsigned value above
2^31 - where a signed compare answers the other way on every operator.

Each was verified able to fail, with the numbers the report predicted: hardcode
lshr and -4 becomes 9223372036854775804; drop the defers from the return path
and 21 becomes 0; reverse them and it becomes 12; let the signal walk continue
past a handler that transferred and the outer handler runs too.

The ones left open are recorded for the next pass: Reach's walk of index
expressions, addr places and restart clause bodies; the dev registry's
size-change guard; a local shadowing an imported name; and the 4K result cap,
which has no coverage at all rather than a missing assertion.
2026-09-11 21:01:53 +07:00

22 lines
1.2 KiB
Plaintext

;;;; Signedness, which nothing in the corpus was exercising.
;;;;
;;;; Found by mutation testing: emit.ml chooses ashr vs lshr and slt vs ult
;;;; from the operand's type, and both choices could be hardcoded to one arm
;;;; with the whole suite still green — because no program shifted a negative
;;;; integer right, and none compared an unsigned value above 2^31.
(defn main [] i32
;; An arithmetic shift keeps the sign. A logical one on -8 gives a number
;; near 2^63, which is the wrong answer that looks like a huge right one.
(print-i64 (>> (i64 -8) 1)) (newline) ; -4
(print-i64 (>> (i64 -1) 40)) (newline) ; -1, still, however far it goes
;; And unsigned stays unsigned: 3000000000 has its top bit set, so a signed
;; compare reads it as negative and answers the other way on every operator.
(let [big (bit-or (<< (u32 1) 31) (u32 1000))] ; 2^31 + 1000
(print-line (if (< big (u32 5)) "wrong: signed compare" "big is not small"))
(print-line (if (> big (u32 5)) "big is large" "wrong: signed compare"))
;; The same value through >>, which is logical on an unsigned type: a
;; signed shift here would keep the top bit and answer near 2^31 again.
(print-i64 (i64 (>> big 31))) (newline)) ; 1
0)