A u64 constant above 2^63 can be written in decimal, and a cast's integer literal too wide for i32 is read at the cast's type

This commit is contained in:
Joseph Ferano 2026-09-25 07:16:42 +07:00
parent aa629da2e5
commit f142cfaadf
5 changed files with 59 additions and 4 deletions

View File

@ -3744,7 +3744,8 @@ and in_range loc k n =
else if bits = 64 then else if bits = 64 then
(* A u64 literal is its 64-bit pattern, so anything at or above 2^63 (* 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 — 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 negative *decimal* literal is accepted as a u64 too, because the
reader records only the value and not how it was written. Narrower 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 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 ] prim (Tast.Cast target) target [ a ]
| _ when is_cast name && List.length args = 1 -> | _ when is_cast name && List.length args = 1 ->
let target = resolve_name ctx.env ~seen:[] loc name in 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 (match a.Tast.ty with
| Types.Enum _ -> () | Types.Enum _ -> ()
(* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn (* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn

View File

@ -153,10 +153,22 @@ let read_number st =
| None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st)) | None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st))
"malformed float literal %s" text "malformed float literal %s" text
else 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 match Int64.of_string_opt text with
| Some i -> spanned st loc (Form.Int i) | Some i -> spanned st loc (Form.Int i)
| None -> Loc.failk "reader/malformed-number" (Loc.upto loc (here st)) | None ->
"malformed integer literal %s" text 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 read_symbol_or_keyword st =
let loc = here st in let loc = here st in

View File

@ -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)

View File

@ -508,6 +508,14 @@ let () =
"2 3 4 7 9 3\n"; "2 3 4 7 9 3\n";
outputs ~x86:true "vec-new takes a type expression, x86" outputs ~x86:true "vec-new takes a type expression, x86"
"programs/vec-new-type.flan" "2 3 4 7 9 3\n"; "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 (* 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 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 collection anywhere. The two show lines either side of it are the same

View File

@ -92,6 +92,13 @@ let () =
reads "bare plus" "+" "+"; reads "bare plus" "+" "+";
reads "float" "0.05" "0.05"; reads "float" "0.05" "0.05";
reads "hex" "0xE6B800FF" "3870818559"; 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 "string" "\"SAND\"" "\"SAND\"";
reads "symbol" "empty-at?" "empty-at?"; reads "symbol" "empty-at?" "empty-at?";
reads "qualified" "rl/draw-fps" "rl/draw-fps"; reads "qualified" "rl/draw-fps" "rl/draw-fps";