@@ -3667,13 +3667,31 @@ static int unary_validate_numeric_string(const char *str, const char *which) {
36673667 return 0 ;
36683668}
36693669
3670+ /**
3671+ * @brief Skip leading ASCII whitespace, returning the first non-space char's
3672+ * pointer. Mirrors `strtoll`'s leading-whitespace handling.
3673+ */
3674+ static inline const char * unary_skip_ws (const char * s ) {
3675+ while (* s == ' ' || * s == '\t' || * s == '\n' || * s == '\r' ) s ++ ;
3676+ return s ;
3677+ }
3678+
36703679/**
36713680 * @brief Parse @p str into the typed scalar buffer @p out_buf for @p dt.
36723681 *
36733682 * Validates the string syntactically first so callers get a clean error
3674- * instead of a silent 0 coerced from a malformed input. Routes through
3675- * `ndarray_set_from_string` so `float128` / `int64` / `uint64` strings
3676- * preserve precision. Other dtypes route through `double`.
3683+ * instead of a silent 0 coerced from a malformed input. For integer
3684+ * dtypes the value is *saturated* to the dtype's representable range
3685+ * (PyTorch `clamp` semantics): a negative bound for an unsigned dtype
3686+ * collapses to 0; a magnitude exceeding the signed dtype's `INT*_MAX`
3687+ * saturates to that max (or `INT*_MIN` if negative); without this
3688+ * saturation, `clip(uint8 tensor, -50, 100)` would silently wrap `-50`
3689+ * via the modulo-2^N cast inside `ndarray_set_from_string`, then see
3690+ * `lo (206) > hi (100)` and collapse every element to `100`. For
3691+ * float dtypes (and `int64`/`uint64`/`float128` where wide-precision
3692+ * strings carry the only loss-free intake), the call falls through to
3693+ * `ndarray_set_from_string` so `strtoll`/`strtoull`/`strtoflt128` keep
3694+ * the full source precision.
36773695 *
36783696 * @param[in] dt Canonical dtype string.
36793697 * @param[in] str Decimal literal.
@@ -3684,6 +3702,66 @@ static int unary_validate_numeric_string(const char *str, const char *which) {
36843702static int unary_parse_typed_scalar (const char * dt , const char * str ,
36853703 const char * which , void * out_buf ) {
36863704 if (unary_validate_numeric_string (str , which ) < 0 ) return -1 ;
3705+
3706+ /* Narrow integer dtypes — saturate the bound to the dtype range so
3707+ out-of-range literals don't wrap via the implicit `(T)strtoll(...)`
3708+ cast inside `ndarray_set_from_string`. int64/uint64 keep the
3709+ wide-precision intake path (their saturating boundary is exactly
3710+ at the strtoll/strtoull edge already). */
3711+ const char * p = unary_skip_ws (str );
3712+ int is_neg = (* p == '-' );
3713+ if (!strcmp (dt , "uint8" )) {
3714+ if (is_neg ) { * (uint8_t * )out_buf = 0 ; return 0 ; }
3715+ unsigned long long v = strtoull (p , NULL , 10 );
3716+ * (uint8_t * )out_buf = (uint8_t )(v > 0xFFu ? 0xFFu : v );
3717+ return 0 ;
3718+ }
3719+ if (!strcmp (dt , "uint16" )) {
3720+ if (is_neg ) { * (uint16_t * )out_buf = 0 ; return 0 ; }
3721+ unsigned long long v = strtoull (p , NULL , 10 );
3722+ * (uint16_t * )out_buf = (uint16_t )(v > 0xFFFFu ? 0xFFFFu : v );
3723+ return 0 ;
3724+ }
3725+ if (!strcmp (dt , "uint32" )) {
3726+ if (is_neg ) { * (uint32_t * )out_buf = 0 ; return 0 ; }
3727+ unsigned long long v = strtoull (p , NULL , 10 );
3728+ * (uint32_t * )out_buf = (uint32_t )(v > 0xFFFFFFFFu ? 0xFFFFFFFFu : v );
3729+ return 0 ;
3730+ }
3731+ if (!strcmp (dt , "int8" )) {
3732+ long long v = strtoll (str , NULL , 10 );
3733+ if (v > 0x7F ) v = 0x7F ;
3734+ else if (v < -0x80 ) v = -0x80 ;
3735+ * (int8_t * )out_buf = (int8_t )v ;
3736+ return 0 ;
3737+ }
3738+ if (!strcmp (dt , "int16" )) {
3739+ long long v = strtoll (str , NULL , 10 );
3740+ if (v > 0x7FFF ) v = 0x7FFF ;
3741+ else if (v < -0x8000 ) v = -0x8000 ;
3742+ * (int16_t * )out_buf = (int16_t )v ;
3743+ return 0 ;
3744+ }
3745+ if (!strcmp (dt , "int32" )) {
3746+ long long v = strtoll (str , NULL , 10 );
3747+ if (v > 0x7FFFFFFFLL ) v = 0x7FFFFFFFLL ;
3748+ else if (v < -0x80000000LL ) v = -0x80000000LL ;
3749+ * (int32_t * )out_buf = (int32_t )v ;
3750+ return 0 ;
3751+ }
3752+ /* uint64: strtoull silently wraps a negative literal modulo 2^64
3753+ (`strtoull("-50")` returns `UINT64_MAX - 49`) — saturate to 0
3754+ explicitly. ERANGE on a positive overflow is what strtoull would
3755+ cap at UINT64_MAX anyway. */
3756+ if (!strcmp (dt , "uint64" )) {
3757+ if (is_neg ) { * (uint64_t * )out_buf = 0 ; return 0 ; }
3758+ /* Fall through to ndarray_set_from_string for the positive path
3759+ so wide-precision literals route through the same parser used
3760+ elsewhere. */
3761+ }
3762+ /* int64 / uint64 (positive) / float* : strtoll / strtoull / strtod /
3763+ strtoflt128 already saturate at the dtype's edge under ERANGE,
3764+ matching the behaviour we want without an explicit upper-bound check. */
36873765 ndarray_set_from_string (dt , (char * )out_buf , 0 , str );
36883766 return 0 ;
36893767}
@@ -3912,20 +3990,14 @@ static int unary_run_cpu_inplace(void *data, long n, const char *dt,
39123990 }
39133991 default : break ;
39143992 }
3915- /* Normalize NaN sign bit to canonical +NaN. libquadmath /
3916- libm leak a sign-bit-set "-nan" out of `logq(-x)`,
3917- `sqrtq(-x)`, `log1pq(-x)` etc. The fp64 path returns
3918- NaN with the sign bit set too, but PHP's float
3919- stringifier hides the sign (`var_dump(NAN)` prints
3920- "NAN"); `quadmath_snprintf` honours it, so the user
3921- sees the inconsistency only on fp128. Force-clear the
3922- sign bit so display matches the rest of the unary
3923- family. Skip on NDARRAY_UNOP_SIGN — that op uses NaN
3924- propagation as a meaningful value (PyTorch parity) and
3925- the input's sign bit is part of its signal. */
3926- if (op != NDARRAY_UNOP_SIGN && NDARRAY_FP128_ISNAN (y )) {
3927- y = NDARRAY_FP128_NAN ();
3928- }
3993+ /* NaN-sign canonicalization happens at stringification time
3994+ (`ndarray_fp128_to_string`) rather than here. This keeps
3995+ the in-memory bit pattern mathematically faithful:
3996+ `NumPower::negative(NaN)` flips the sign bit (matches
3997+ NumPy / PyTorch `neg` on NaN), `NumPower::positive(NaN)`
3998+ preserves the input, while `__toString` / `toArray`
3999+ render every NaN as the unsigned `"nan"` literal across
4000+ every fp dtype. */
39294001 p [i ] = y ;
39304002 }
39314003 return 0 ;
0 commit comments