[m-rev.] for review: fix a bug in the string to int and uint overflow checks
Zoltan Somogyi
zoltan.somogyi at runbox.com
Fri Jul 17 22:41:22 AEST 2026
On Fri, 17 Jul 2026 17:07:47 +1000, Julien Fischer <jfischer at opturion.com> wrote:
> We can almost certainly do better for the common bases performance-wise (and
> likely the generic case as well). I will look into doing that separately.
The obvious way to do it would be to check the base being in 2..36 not using
comparisons, but by using a switch, and having that switch also return
the precomputed value of maxint/base (for both 32 and 64 bits) as well.
> For example, with 32-bit ints consider the input string "5368709120" (in the
> base 10 case). At the last step of the conversion loop, we have N0 = 536870912
> and the final digit '0'. The true value of (10 * 536870912) + 0 is 5368709120,
> which does not fit in 32 bits. The multiplication wraps around, giving N =
> 1073741824. Since 536870912 =< 1073741824, the overflow check succeeds, and
> the conversion incorrectly returns 1073741824 instead of failing.
Such examples would be MUCH more understandable if you added a comma
every third decimal place, to allow distinguishing three billion from thirty billion
without tedious digit counting.
> --- a/library/string.m
> +++ b/library/string.m
> @@ -6035,10 +6035,10 @@ base_positive_int_accumulator(Base) = Pred :-
>
> accumulate_int(Base, Char, N0, N) :-
I would rename this to either accumulate_positive_int
or accumulate_non_negative_int.
> char.unsafe_base_digit_to_int(Base, Char, M),
> - N = (Base * N0) + M,
> - % Fail on overflow.
> - % XXX depends on undefined behaviour
> - N0 =< N.
> + % Fail if Base * N0 + M would exceed max_int.
> + % The division is safe since our caller sets Base to be in 2..36.
> + N0 =< (max_int - M) `unchecked_quotient` Base,
> + N = (Base * N0) + M.
>
> :- func base_negative_int_accumulator(int) = pred(char, int, int).
> :- mode base_negative_int_accumulator(in) = out(pred(in, in, out) is semidet)
> @@ -6067,10 +6067,13 @@ base_negative_int_accumulator(Base) = Pred :-
>
> accumulate_negative_int(Base, Char, N0, N) :-
> char.unsafe_base_digit_to_int(Base, Char, M),
> - N = (Base * N0) - M,
> - % Fail on overflow.
> - % XXX depends on undefined behaviour
> - N =< N0.
> + % Fail if Base * N0 - M would be less than min_int.
> + % We must use truncating division in the following check.
> + % Flooring division (i.e. div) causes the test to succeed
> + % for values of N0 for which the multiplication overflows.
> + % The division is safe since our caller sets Base to be in 2..36.
> + N0 >= (min_int + M) `unchecked_quotient` Base,
> + N = (Base * N0) - M.
What guarantees do our target languages offer about truncating
vs flooring? If you know, it would help to mention them here.
I did not check the test cases, partly because the absence of commas
in large numbers makes it too hard :-(
However, the rest of the diff is fine.
Zoltan.
More information about the reviews
mailing list