[DISCUSSION] Adding the "is_integer_safe()" function

php.internals

Alexandre Daubois

1 year ago
Hello Internals, I would like to gather feedback on the proposal to add an "is_integer_safe()" function to PHP. The idea came to me while replying to a thread [1], but I think creating a discussion is more appropriate. Here are a few reasons I think this would be useful: - PHP automatically converts integers that exceed PHP_INT_MAX to float, which can cause undetected precision loss. This function would allow checking that an integer remains within safe bounds before critical operations ; - Integer limits vary between 32-bit and 64-bit systems. This function would provide a standardized way to check the portability of integer values across different architectures ; - This would improve code readability. We had the case in Symfony [2] to implement a JSONPath crawler, and the solution feels hacky to say the least ; - PHP already has functions like `is_finite()` and `is_nan()` for floats. Adding `is_integer_safe()` would logically complete this set of type validation functions. - Of course more optimized than dealing with strings and using arithmetic operators on strings ; - As PHP continues to evolve towards stricter type safety (strict_types, typed properties, etc.), having more granular type validation functions supports this direction. If this proposition gets some interest, I'll take care of the implementation in the engine. I guess it would also require an RFC. Best, Alex [1] https://externals.io/message/128032 [2] https://github.com/symfony/symfony/blob/d3a0df0243e1c68598fa066eaa6cd0cf39f256dc/src/Symfony/Component/JsonPath/JsonPathUtils.php#L243

Rowan Tommins [IMSoP]

1 year ago
On 25 July 2025 14:40:47 BST, Alexandre Daubois <alex.daubois+php@gmail.com> wrote:
> >I would like to gather feedback on the proposal to add an >"is_integer_safe()" function to PHP.
From the name, I don't understand what this function would do. Do I pass in an integer, and get a boolean telling me if it's safe? Safe for what? Do I pass in some other value and get a boolean if it's safe to convert to an integer? What value exactly, and safe in what way? That tells me two things: 1) You need to flesh out your proposal to be explicit about *what* it would do, as well as why. 2) The function needs a better name, to avoid confusion over what "safe" means. Regards, Rowan Tommins [IMSoP]

Alexandre Daubois

1 year ago
Hello Rowan, Le ven. 25 juil. 2025 à 23:10, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> a écrit :
> 1) You need to flesh out your proposal to be explicit about *what* it would do, as well as why.
Indeed, I was too focused on the why and I forgot the most important part. The idea is to have a function that receives an integer or a float and returns bool if the provided argument is inside the safe Javascript integer interval, namely [-(2^53)+1 ; (2^53)-1]. It's signature would be `is_integer_safe(int|float $num): bool`. This interval is considered safe because the floating-point mantissa is stored on 52 bits. This is nicely described in MDN [1].
> 2) The function needs a better name, to avoid confusion over what "safe" means.
I agree with you. I haven't come up with a better name yet as this one could be ambiguous. Best, Alex [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description

Rowan Tommins [IMSoP]

1 year ago
On 26/07/2025 07:13, Alexandre Daubois wrote:
> The idea is to have a function that receives an integer or a float and > returns bool if the provided argument is inside the safe Javascript > integer interval, namely [-(2^53)+1 ; (2^53)-1].
That suggests maybe the name should communicate "safe for JavaScript"; or more generally "safe for 64-bit IEEE floating point". is_safe_for_js() is_safe_for_float()
> It's signature would > be `is_integer_safe(int|float $num): bool`.
I'm not sure if accepting floats in the same function makes sense. If the question is "can this value safely be stored in a float?" and you give it a float, then the answer is surely "yes". You have no way of knowing if it _already_ lost precision during a previous operation. Possibly there could be a separate function which asks "can this float value be safely converted to an integer?", but that implies a different definition - it wouldn't make much sense to answer "yes" for 3.5, or NaN. That then makes me wonder if this is really part of a family of functions relating to casts - something I've been thinking about off an on for some time. Namely, "can this value be losslessly converted to this type?" My thinking is that this needs new syntax, to avoid dozens of specific functions. For instance, a generic (or generic-like) form: function can_lossless_cast<T>(mixed $value): bool can_lossless_cast<float>(2 ** 50) === true can_lossless_cast<float>(2 ** 54) === false can_lossless_cast<int>(2 .0** 54) === true can_lossless_cast<int>(2.0 ** 65) === false can_lossless_cast<int>(3.5) === false can_lossless_cast<int>(NaN) === false can_lossless_cast<int>('9007199254740991000') == true // less than 2**64 can_lossless_cast<float>('9007199254740991000') == false // more than 2**53 can_lossless_cast<int|float>('9007199254740991000') == true can_lossless_cast<int|float>('3.5') == true Regards,
-- Rowan Tommins [IMSoP]

Alexandre Daubois

1 year ago
> That suggests maybe the name should communicate "safe for JavaScript"; or more generally "safe for 64-bit IEEE floating point". > > is_safe_for_js() > is_safe_for_float()
I'm not sure we should mention Javascript, because it's actually related to floating point storage.
> I'm not sure if accepting floats in the same function makes sense. If the question is "can this value safely be stored in a float?" and you give it a float, then the answer is surely "yes". You have no way of knowing if it _already_ lost precision during a previous operation. > > Possibly there could be a separate function which asks "can this float value be safely converted to an integer?", but that implies a different definition - it wouldn't make much sense to answer "yes" for 3.5, or NaN.
I think we can see this function more like "can this number be compared safely?", e.g. `var_dump(2**53 === (2**53)+1);` returns true as these numbers are not in the safe interval. A name like `is_safely_comparable()` would fit better maybe.
> My thinking is that this needs new syntax, to avoid dozens of specific functions. For instance, a generic (or generic-like) form: > > function can_lossless_cast<T>(mixed $value): bool > > can_lossless_cast<float>(2 ** 50) === true > can_lossless_cast<float>(2 ** 54) === false > > can_lossless_cast<int>(2 .0** 54) === true > can_lossless_cast<int>(2.0 ** 65) === false > can_lossless_cast<int>(3.5) === false > can_lossless_cast<int>(NaN) === false > > can_lossless_cast<int>('9007199254740991000') == true // less than 2**64 > can_lossless_cast<float>('9007199254740991000') == false // more than 2**53 > > can_lossless_cast<int|float>('9007199254740991000') == true > can_lossless_cast<int|float>('3.5') == true
I really like the idea of generic functions, I however imagine it would bring a lot more complexity and really profound changes to the engine, well beyond the scope of the proposal. Maybe the better naming of the function would solve this? Best, Alex

Claude Pache

1 year ago
> Le 26 juil. 2025 à 18:13, Alexandre Daubois <alex.daubois+php@gmail.com> a écrit : > >> >> I'm not sure if accepting floats in the same function makes sense. If the question is "can this value safely be stored in a float?" and you give it a float, then the answer is surely "yes". You have no way of knowing if it _already_ lost precision during a previous operation. >> >> Possibly there could be a separate function which asks "can this float value be safely converted to an integer?", but that implies a different definition - it wouldn't make much sense to answer "yes" for 3.5, or NaN. > > I think we can see this function more like "can this number be > compared safely?", e.g. `var_dump(2**53 === (2**53)+1);` returns true > as these numbers are not in the safe interval. A name like > `is_safely_comparable()` would fit better maybe.
This is not correct: 2**53 + 1 is perfectly “safe” (for 64-bit builds of PHP), see: https://3v4l.org/P939d The specific notion of “safe integer” as introduced in JavaScript makes sense only for numbers encoded using IEEE 754, which is what PHP uses for `float`. In PHP, there is a specialised type for integers, so that the need of such a function is not clear, because every integer encoded as `int` is “safe”. Or maybe you want something like `is_safe_integer_when_interpreted_as_float()`? Also, note that the particular Symfony example given at the beginning of this thread uses a function that expects a string, not an int or a float. In that example, something like `is_numeric_value_producing_exact_integer_when_interpreted_as_float()` could have been useful, but this use case is very specialised. —Claude

Alexandre Daubois

1 year ago
Le lun. 28 juil. 2025 à 12:18, Claude Pache <claude.pache@gmail.com> a écrit :
> This is not correct: 2**53 + 1 is perfectly “safe” (for 64-bit builds of PHP), see: https://3v4l.org/P939d > > The specific notion of “safe integer” as introduced in JavaScript makes sense only for numbers encoded using IEEE 754, which is what PHP uses for `float`. In PHP, there is a specialised type for integers, so that the need of such a function is not clear, because every integer encoded as `int` is “safe”. Or maybe you want something like `is_safe_integer_when_interpreted_as_float()`? > > Also, note that the particular Symfony example given at the beginning of this thread uses a function that expects a string, not an int or a float. In that example, something like `is_numeric_value_producing_exact_integer_when_interpreted_as_float()` could have been useful, but this use case is very specialised. > > —Claude
Right! I think we found Gina proposed a nice alternative. I wrote a few specs in my last thread message in response to Gina. Please let me know if there's something that is unclear or bothering you. Best, Alexandre Daubois

Tim Düsterhus

1 year ago
Hi Am 2025-07-26 15:45, schrieb Rowan Tommins [IMSoP]:
> can_lossless_cast<float>(2 ** 54) === false
That one is incorrect, 2**54 *can* be exactly represented as a `float`. The magic 2**53 boundary just means that the difference between consecutive representable values becomes larger than `1`. See: https://float.exposed/0x4350000000000000 and try incrementing or decrementing the "Raw Decimal Integer Value". Best regards Tim Düsterhus

Gina P. Banyard

1 year ago
On Friday, 25 July 2025 at 14:43, Alexandre Daubois <alex.daubois+php@gmail.com> wrote:
> Hello Internals, > > I would like to gather feedback on the proposal to add an > "is_integer_safe()" function to PHP. The idea came to me while > replying to a thread [1], but I think creating a discussion is more > appropriate. > > Here are a few reasons I think this would be useful: > > - PHP automatically converts integers that exceed PHP_INT_MAX to > float, which can cause undetected precision loss. This function would > allow checking that an integer remains within safe bounds before > critical operations ; > - Integer limits vary between 32-bit and 64-bit systems. This function > would provide a standardized way to check the portability of integer > values across different architectures ; > - This would improve code readability. We had the case in Symfony [2] > to implement a JSONPath crawler, and the solution feels hacky to say > the least ; > - PHP already has functions like `is_finite()` and `is_nan()` for > floats. Adding `is_integer_safe()` would logically complete this set > of type validation functions. > - Of course more optimized than dealing with strings and using > arithmetic operators on strings ; > - As PHP continues to evolve towards stricter type safety > (strict_types, typed properties, etc.), having more granular type > validation functions supports this direction. > > If this proposition gets some interest, I'll take care of the > implementation in the engine. I guess it would also require an RFC. > > Best, > Alex > > [1] https://externals.io/message/128032 > [2] https://github.com/symfony/symfony/blob/d3a0df0243e1c68598fa066eaa6cd0cf39f256dc/src/Symfony/Component/JsonPath/JsonPathUtils.php#L243
Naming aside, I would be very much in favour of anything that makes it easier to make bidirectional transformations between floats and ints reliable. For naming, maybe the following pair of functions make sense? - is_representable_as_int() - is_representable_as_float() Best regards, Gina P. Banyard

Alexandre Daubois

1 year ago
Le lun. 28 juil. 2025 à 12:14, Gina P. Banyard <internals@gpb.moe> a écrit :
> For naming, maybe the following pair of functions make sense? > - is_representable_as_int()
Definitely, thank you for proposing. Having a pair of functions for bidirectional transformations makes much more sense than a single ambiguous function. Here are some quick specs for what these functions could do. `is_representable_as_int(mixed $value): bool` checks if a given value can be losslessly converted to an integer. - Floats: returns true if the float is within PHP_INT_MIN to PHP_INT_MAX range AND has no fractional part - Strings: returns true if the string represents a valid integer within the platform's integer range - Always returns true for integers - Special float values: returns false for NaN, INF, -INF This gives the following example: ```php is_representable_as_int(42.0); // true is_representable_as_int(42.5); // false is_representable_as_int(2**31); // false on 32-bit, true on 64-bit is_representable_as_int(2**63); // false on both platforms is_representable_as_int("123"); // true is_representable_as_int("123.0"); // true is_representable_as_int("123.5"); // false is_representable_as_int(NAN); // false ```
> - is_representable_as_float()
Now, `is_representable_as_float(mixed $value): bool`. The function would check if a value can be represented as a float without precision loss. - Integers: returns true if within the IEEE 754 safe integer range (+/-(2^53-1)) regardless of the system's PHP_INT_MAX - Strings: returns true if parseable as a float within safe precision bounds - Floats always returns true - The IEEE 754 safe integer limit applies universally This give the following examples: ```php is_representable_as_float(2**53 - 1); // true is_representable_as_float(2**53); // false, precision loss when casted to float is_representable_as_float(PHP_INT_MAX); // false on 64-bit, true on 32-bit is_representable_as_float(42); // true is_representable_as_float("123.456"); // true is_representable_as_float("1e308"); // true is_representable_as_float("1e400"); // false ``` What do you think of this new approach? Best, Alexandre Daubois

Tim Düsterhus

1 year ago
Hi Am 2025-07-28 15:00, schrieb Alexandre Daubois:
> is_representable_as_float(2**53); // false, precision loss when > casted to float
Similarly to my other email: This is false. 2**53 is exactly representable as float. Every power of two (<= 1024) is.
> is_representable_as_float("1e308"); // true
This is false. 1e308 is not exactly representable. The nearest representable value is 1.00000000000000001098e+308: https://float.exposed/0x7fe1ccf385ebc8a0 Best regards Tim Düsterhus

Tim Düsterhus

1 year ago
Hi Am 2025-07-28 15:35, schrieb Tim Düsterhus:
> Similarly to my other email: This is false. 2**53 is exactly > representable as float. Every power of two (<= 1024) is.
Small correction: That should've read `<= 1023` (or `< 1024`). Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Mon, Jul 28, 2025, at 8:00 AM, Alexandre Daubois wrote:
> Le lun. 28 juil. 2025 à 12:14, Gina P. Banyard <internals@gpb.moe> a écrit : >> For naming, maybe the following pair of functions make sense? >> - is_representable_as_int() > > Definitely, thank you for proposing. Having a pair of functions for > bidirectional transformations makes much more sense than a single > ambiguous function. Here are some quick specs for what these functions > could do. > > `is_representable_as_int(mixed $value): bool` checks if a given value > can be losslessly converted to an integer. > > - Floats: returns true if the float is within PHP_INT_MIN to > PHP_INT_MAX range AND has no fractional part > - Strings: returns true if the string represents a valid integer > within the platform's integer range > - Always returns true for integers > - Special float values: returns false for NaN, INF, -INF > > This gives the following example: > > ```php > is_representable_as_int(42.0); // true > is_representable_as_int(42.5); // false > is_representable_as_int(2**31); // false on 32-bit, true on 64-bit > is_representable_as_int(2**63); // false on both platforms > is_representable_as_int("123"); // true > is_representable_as_int("123.0"); // true > is_representable_as_int("123.5"); // false > is_representable_as_int(NAN); // false > ```
Even without the precision loss question, such a function would be very useful when parsing input data of unknown type (and thus is a string). "Can I cast this string to an int to use with an int parameter without any loss" is currently not as simple as it ought to me. Providing a single standardized function for that would be most welcome. The name is quite long though. :-)
>> - is_representable_as_float()
The same argument applies here, although in most cases is_numeric() is close enough. A more precise option would not be unwelcome, though. --Larry Garfield

Rowan Tommins [IMSoP]

1 year ago
On 28/07/2025 16:32, Larry Garfield wrote:
> Even without the precision loss question, such a function would be very useful when parsing input data of unknown type (and thus is a string). "Can I cast this string to an int to use with an int parameter without any loss" is currently not as simple as it ought to me. Providing a single standardized function for that would be most welcome. > > The name is quite long though. 🙂
This is basically where I was going with "can_lossless_cast". The reason I went with a generic-like syntax is because it makes it exntendible to any representable type, without an ever-growing list of long names like "is_representable_as_int_or_float". I also see it as part of a wider set of missing cast functions; for any value V and type T: 1. Is V of type T? 2. Can V be safely converted to T (for some definition of "safe")? 3. If V can safely be converted to T, give that result; otherwise, throw an Exception|Error 4. If V can safely be converted to T, give that result, else return null 5. If V can safely be converted to T, give that result, else return a default value of type T Given (5), you can implement (2), (3), and (4), but not elegantly, particularly if null is a valid output, e.g.: ( cast<?int>($v, default: false) !== false ) ? cast<?int>($v, default: false) : throw new TypeError('Not null, and not castable to int'); vs cast_or_throw<?int>($v) I've got a half-drafted RFC around this, but stalled on a) naming; and b) what a "safe" cast means for different types. It seems like both problems have already come to light in this thread.
-- Rowan Tommins [IMSoP]