From f9ae500949c0cd3b40066d0a6241e0a8ea9530fc Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:37:55 +0700 Subject: [PATCH] The lattice pinned at its edges, and the two calls that could go the other way --- test/test_flan.ml | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/test/test_flan.ml b/test/test_flan.ml index 0790e2d..5ab3e21 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -930,6 +930,88 @@ let () = rejects_check "float literal into an int" "(defn f [] i32 (+ 1 0.5))" ~needle:"expected i32"; + (* ── Implicit widening, FIX.org 2026-09-20 ───────────────────────── + The lattice, pinned at its edges rather than row by row: what is in, what + is out, and the two boundaries that were a judgement call and could be + argued the other way — int-into-float admitting only the exact ones, and + equal-width cross-signedness admitting nothing. + + [programs/widening.flan] is the other half and asserts the bits; these + assert which programs exist. *) + accepts "same signedness widens" + "(defvar a i32) (defn g [x i64] ()) (defn f [] () (g a))"; + accepts "unsigned widens into a wider signed" + "(defvar a u32) (defn g [x i64] ()) (defn f [] () (g a))"; + accepts "u8 widens into i16" + "(defvar a u8) (defn g [x i16] ()) (defn f [] () (g a))"; + accepts "f32 widens into f64" + "(defvar a f32) (defn g [x f64] ()) (defn f [] () (g a))"; + (* Narrowing is the thing that did not change, and the message has to say + narrowing rather than "these are different types" — it also names the + direction that needs nothing, because that is the half a reader coming + from the old rule will not expect. *) + rejects_check "narrowing is still refused, and says so" + "(defvar a i64) (defn g [x i32] ()) (defn f [] () (g a))" + ~needle:"i64 into i32 can lose"; + rejects_check "and says the other direction is free" + "(defvar a i64) (defn g [x i32] ()) (defn f [] () (g a))" + ~needle:"i32 widens into i64 by itself"; + rejects_check "float narrowing is refused too" + "(defvar a f64) (defn g [x f32] ()) (defn f [] () (g a))" + ~needle:"f64 into f32 can lose"; + (* Equal width across signedness: each holds values the other cannot, so + there is no direction at all and the message says that instead. *) + rejects_check "signed does not reach the same-width unsigned" + "(defvar a i32) (defn g [x u32] ()) (defn f [] () (g a))" + ~needle:"neither widens into the other"; + rejects_check "and a signed value never reaches an unsigned, wider or not" + "(defvar a i32) (defn g [x u64] ()) (defn f [] () (g a))" + ~needle:"neither widens into the other"; + (* Int into float, exact only. This is where the rule is tighter than + Odin's, which admits any integer into any float; i64 has values no f64 + holds, so it is out, and the cast is written. *) + accepts "i32 reaches f64 exactly" + "(defvar a i32) (defn g [x f64] ()) (defn f [] () (g a))"; + accepts "u32 reaches f64 exactly" + "(defvar a u32) (defn g [x f64] ()) (defn f [] () (g a))"; + accepts "i16 reaches f32 exactly" + "(defvar a i16) (defn g [x f32] ()) (defn f [] () (g a))"; + rejects_check "i64 does not reach f64 — above 2^53 it would round" + "(defvar a i64) (defn g [x f64] ()) (defn f [] () (g a))" + ~needle:"(f64 x)"; + rejects_check "i32 does not reach f32 — above 2^24 it would round" + "(defvar a i32) (defn g [x f32] ()) (defn f [] () (g a))" + ~needle:"(f32 x)"; + (* Containers are invariant: widening rewrites a value with a cast, and + there is no value to rewrite in a slice that does not own its bytes. *) + rejects_check "a slice of i32 is not a slice of i64" + "(defn g [s [i64]] ()) (defn f [t [i32]] () (g t))" + ~needle:"expected [i64]"; + + (* The binary join. The wider operand decides, in either written order, and + an equal-width cross-signed pair still has nothing to decide on. *) + accepts "the wider operand decides, wider written first" + "(defvar a i64) (defvar b i32) (defn f [] i64 (+ a b))"; + accepts "and decides when it is written second" + "(defvar a i64) (defvar b i32) (defn f [] i64 (+ b a))"; + accepts "min and max join the same way" + "(defvar a i8) (defvar b i16) (defn f [] i16 (max a b))"; + rejects_check "i32 and u32 have no join" + "(defvar a i32) (defvar b u32) (defn f [] i32 (+ a b))" + ~needle:"neither widens into the other"; + (* The literal rule is untouched, which is what keeps a u64 constant's + arithmetic at u64 rather than defaulting the 1 to an i32. *) + accepts "a literal still takes the other operand's type" + "(defconst fnv u64 14695981039346656037) (defn f [] u64 (+ fnv 1))"; + (* Shifts are the carve-out: the value's type decides and the count widens + to it, never the reverse, because the result's width and the poison check + both belong to the value. *) + accepts "a narrower count widens to the value" + "(defvar v i64) (defvar n u8) (defn f [] i64 (<< v n))"; + rejects_check "a wider count does not drag the value up with it" + "(defvar v u8) (defvar n i32) (defn f [] u8 (<< v n))" + ~needle:"expected u8"; + (* ── Bidirectional flow ────────────────────────────────────────── *) accepts "return type types the literal" "(defn f [] u8 0)"; accepts "return type types None" "(defn f [] (Option f64) None)";