diff --git a/lib/check.ml b/lib/check.ml index 49711623..6957088c 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3744,7 +3744,8 @@ and in_range loc k n = else if bits = 64 then (* A u64 literal is its 64-bit pattern, so anything at or above 2^63 arrives here as a negative [int64] and is still in range — - 0xcbf29ce484222325 is a real u64 and not an error. The cost is that a + 0xcbf29ce484222325 is a real u64 and not an error, and so is its + decimal, which the reader reads as the same pattern. The cost is that a negative *decimal* literal is accepted as a u64 too, because the reader records only the value and not how it was written. Narrower unsigned types keep the strict check, which is where a typo like 300 @@ -8796,7 +8797,18 @@ and named_call ?(qualified = false) ctx ~want loc name args = prim (Tast.Cast target) target [ a ] | _ when is_cast name && List.length args = 1 -> let target = resolve_name ctx.env ~seen:[] loc name in - let a = check ctx (List.hd args) in + (* An integer literal too wide for the i32 it would default to is checked + at the target instead, so (u64 2935910691) and (i64 5000000000) are the + constants they say. One that fits i32 keeps the default and the cast, + which is what (u32 -1) has always meant. *) + let want = + match (List.hd args).Ast.e, target with + | Ast.Int n, (Types.Int _ | Types.Float _) + when Int64.compare n (-2147483648L) < 0 + || Int64.compare n 2147483647L > 0 -> Some target + | _ -> None + in + let a = check ctx ?want (List.hd args) in (match a.Tast.ty with | Types.Enum _ -> () (* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn diff --git a/lib/reader.ml b/lib/reader.ml index 0e098a79..eee007a5 100644 --- a/lib/reader.ml +++ b/lib/reader.ml @@ -153,10 +153,22 @@ let read_number st = | None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st)) "malformed float literal %s" text else + (* A decimal above the largest i64 and below 2^64 is read as its 64-bit + pattern, as a hex literal is, so a u64 constant can be written in + decimal. It therefore arrives negative, and [Check.in_range] makes the + same allowance for it that it makes for hex. *) + let unsigned () = + if String.for_all (fun c -> c >= '0' && c <= '9') text then + Int64.of_string_opt ("0u" ^ text) + else None + in match Int64.of_string_opt text with | Some i -> spanned st loc (Form.Int i) - | None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st)) - "malformed integer literal %s" text + | None -> + match unsigned () with + | Some i -> spanned st loc (Form.Int i) + | None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st)) + "malformed integer literal %s" text let read_symbol_or_keyword st = let loc = here st in diff --git a/test/programs/u64-decimal.flan b/test/programs/u64-decimal.flan new file mode 100644 index 00000000..00b0c92a --- /dev/null +++ b/test/programs/u64-decimal.flan @@ -0,0 +1,16 @@ +;;;; A u64 constant above 2^63 written in decimal, and an integer literal too +;;;; wide for i32 as a cast's argument. + +(defconst top u64 18446744073709551615) +(defonce fnv u64 14695981039346656037) + +(defn main [] i32 + (println top) + (println fnv) + (println (u64 2935910691)) + (println (u64 18446744073709551615)) + (println (i64 -5000000000)) + (println (f64 4000000000)) + ;; A literal that fits i32 keeps its default and is cast, as before. + (println (u32 -1)) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index d1163867..745846d8 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -508,6 +508,14 @@ let () = "2 3 4 7 9 3\n"; outputs ~x86:true "vec-new takes a type expression, x86" "programs/vec-new-type.flan" "2 3 4 7 9 3\n"; + (* A u64 above 2^63 in decimal, and a cast of a literal too wide for i32. *) + let u64_out = + "18446744073709551615\n14695981039346656037\n2935910691\n\ + 18446744073709551615\n-5000000000\n4e+09\n4294967295\n" + in + outputs "a u64 constant in decimal" "programs/u64-decimal.flan" u64_out; + outputs ~x86:true "a u64 constant in decimal, x86" + "programs/u64-decimal.flan" u64_out; (* into. The count of pulls is the assertion a unit test cannot make: one pass, one call per element per stage it reaches, and no intermediate collection anywhere. The two show lines either side of it are the same diff --git a/test/test_flan.ml b/test/test_flan.ml index ff0cf47d..fa77e13c 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -92,6 +92,13 @@ let () = reads "bare plus" "+" "+"; reads "float" "0.05" "0.05"; reads "hex" "0xE6B800FF" "3870818559"; + (* A decimal between 2^63 and 2^64 is its bit pattern, as hex is; one past + 2^64, or a negative one past the smallest i64, is still malformed. *) + reads "u64 decimal" "18446744073709551615" "-1"; + rejects "decimal past 2^64" "18446744073709551616" + ~needle:"malformed integer literal"; + rejects "negative decimal past i64" "-9223372036854775809" + ~needle:"malformed integer literal"; reads "string" "\"SAND\"" "\"SAND\""; reads "symbol" "empty-at?" "empty-at?"; reads "qualified" "rl/draw-fps" "rl/draw-fps";