RFA-304 · Case file with fixtures · Case 276 of 694 · Runtime evidence
Signed Integer isqrt Panics on Negative Input
Integer square root is defined for non-negative signed inputs. The direct isqrt method panics outside that domain; checked_isqrt turns the same precondition failure into None.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The direct signed integer square-root method requires a non-negative operand and uses a panic to report input outside that mathematical domain.
- First discriminating check
- Exercise the negative, zero, square-boundary, and maximum inputs and use checked_isqrt when negativity can arrive as data.
I replaced a floating-point square-root calculation with integer isqrt because the domain needed an exact floor. Most tests were non-negative. One intermediate subtraction produced -1, and the service panicked instead of returning a special numeric value.
The failing program catches (-1_i32).isqrt() and expects success. The result is an unwind.
Integer square root has a narrower domain
For a non-negative integer n, integer square root returns the greatest integer r for which r * r <= n. For example, both 15 and 16 are accepted, but their results are 3 and 4.
There is no signed integer result whose square behaves as a real square root of -1. The i32::isqrt documentation therefore gives the direct method a precondition: it panics when self is negative.
This is different from floating-point square root, which can return NaN for a negative finite operand. Integer types have no NaN bit pattern to carry that outcome.
Checked arithmetic expresses an untrusted domain
i32::checked_isqrt returns None for a negative value and Some(root) otherwise. I use it when negativity can arrive through input, state transitions, rounding adjustments, or intermediate arithmetic.
The code can then attach domain meaning:
let root = value.checked_isqrt().ok_or(InputError::NegativeMagnitude)?;
This is better than catching a panic. Panics are not a routine branch mechanism, hooks run before catches, and production profiles may abort. A checked method keeps the failure in the ordinary type system.
If a negative value represents an internal impossibility, I may validate with an assertion before calling isqrt. The assertion should state the violated invariant and retain enough context to diagnose its producer.
Casting to unsigned can hide the bug
A tempting repair is value as u32. For -1_i32, that cast produces a very large unsigned value through two's-complement wrapping semantics. Its square root is a valid large integer, not a rejection.
I use u32::try_from(value) when conversion is meant to validate non-negativity. It returns an error for the negative case. Then u32::isqrt is total over all values of the resulting unsigned type.
Choosing an unsigned domain earlier can eliminate repeated checks when negative states truly make no sense. It does not replace validation at the boundary where signed or textual input enters.
Floor behavior needs its own tests
Avoiding the panic is only half the contract. Integer square root floors the mathematical result. If a caller requires a perfect square, it must verify that condition.
I avoid naïvely checking root * root == value in a generic large type without considering overflow. The returned root is constrained so its square fits the original non-negative type for normal primitive integer isqrt, but related calculations such as (root + 1) * (root + 1) can overflow near the maximum.
Checked multiplication or division-based comparisons keep boundary tests honest.
Normalization must match the domain
Sometimes developers apply abs() before the root. That changes the mathematical question from square root of a signed value to square root of its magnitude. It may be correct for distance, and completely wrong for a discriminant or balance.
Signed minimum also deserves care: ordinary absolute value cannot be represented in the same signed type for i32::MIN. Methods such as checked_abs or conversion through an unsigned magnitude make the decision explicit.
I do not normalize only to satisfy an API. I write why the negative state is rejected, converted to magnitude, clamped, or treated as an invariant failure.
What I test
The repaired program asserts None for negative one and checks roots around the square boundary at 15 and 16. It also includes zero.
My broader table includes the signed minimum, -1, zero, one, perfect squares, one below and above a square, and the type maximum. If input starts as text or a larger integer, I test conversion failure separately from square-root domain failure.
For geometry or capacity code I also state whether flooring is acceptable. A radius calculation that must cover an area might need a ceiling square root instead; silently using floor can under-allocate even when every input is non-negative.
The core principle is that numeric methods encode mathematical domains, not only machine operations. Signed isqrt rejects negative inputs by panicking, while checked_isqrt preserves that boundary as Option. I choose between them based on whether the precondition is proven internally or must be handled as data.