Skip to content

MOD-14427 Fix numeric operations on homogeneous array. Operation will now promote to the next type if results overflows#1520

Merged
AvivDavid23 merged 7 commits intomasterfrom
MOD-14427-json-oss-homogeneous-array-num-operations-wrap-results-for-smaller-typed-arrays-8-16-32-instead-of-promoting
Mar 17, 2026
Merged

MOD-14427 Fix numeric operations on homogeneous array. Operation will now promote to the next type if results overflows#1520
AvivDavid23 merged 7 commits intomasterfrom
MOD-14427-json-oss-homogeneous-array-num-operations-wrap-results-for-smaller-typed-arrays-8-16-32-instead-of-promoting

Conversation

@AvivDavid23
Copy link
Copy Markdown
Contributor

@AvivDavid23 AvivDavid23 commented Mar 11, 2026

Note

Medium Risk
Touches core numeric update logic for JSON values/typed arrays, changing overflow handling and type promotion behavior; regressions could affect arithmetic correctness across commands and numeric types.

Overview
Fixes JSON.NUMINCRBY/JSON.NUMMULTBY/JSON.NUMPOWBY behavior for homogeneous (typed) numeric arrays so results no longer silently overflow or produce inf in-place.

Integer operations now compute via i128 and, when a result no longer fits the underlying typed-array element type, the element is replaced via IArray insertion to trigger type promotion to the smallest fitting integer type (including correct handling of large u64 values and overflow errors at true limits). Float/half-float ops now validate finiteness and either promote to wider storage on narrowing overflow or raise numeric overflow when the computed result is non-finite.

Adds extensive pytest coverage for typed-array promotion/overflow across i8/u8/i16/u16/i32/u32/i64/u64 and f16/bf16/f32/f64.

Written by Cursor Bugbot for commit 09fe718. This will update automatically on new commits. Configure here.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.05%. Comparing base (10f558b) to head (09fe718).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1520      +/-   ##
==========================================
+ Coverage   77.94%   78.05%   +0.11%     
==========================================
  Files          15       15              
  Lines        3935     3942       +7     
==========================================
+ Hits         3067     3077      +10     
+ Misses        868      865       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

_ => unreachable!(),
}
}
ret
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved ret value reused after match consumption

High Severity

NumOpResult does not derive Copy, so match ret in the else branch moves ret. The code then attempts to return ret on line 243 after the if/else, which is a use-after-move. In the else path (promotion case), ret is consumed by the match to extract the value for num1_slice.insert(...), and is no longer available as the arm's return value. This affects every integer type variant expanded by the macro.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor Author

@AvivDavid23 AvivDavid23 Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are wrong, the inner value of ret impl Copy so its ok

Comment on lines +237 to +243
match ret {
NumOpResult::I64(v) => num1_slice.insert(index, v)?,
NumOpResult::U64(v) => num1_slice.insert(index, v)?,
_ => unreachable!(),
}
}
ret
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Use-after-move on ret

NumOpResult does not derive Copy (it can't, because INumber is not Copy). The match ret { ... } in the else branch moves ret, but then ret is used as the return value after the if/else block on line 243. Rust's borrow checker considers all branches — since ret is potentially moved in the else path, it's unavailable after the if/else. This should be a compile error.

The fact that i64/u64 inner values are Copy doesn't help — the outer enum is still consumed by the match.

Suggested fix: Match on a reference instead:

match &ret {
    NumOpResult::I64(v) => num1_slice.insert(index, *v)?,
    NumOpResult::U64(v) => num1_slice.insert(index, *v)?,
    _ => unreachable!(),
}

This borrows ret rather than consuming it, so ret remains available for the return on line 243.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, read the comments above

Comment thread redis_json/src/ivalue_manager.rs Outdated
num1_slice.remove(index);
num1_slice.insert(index, new_val)?;
}
Ok(NumOpResult::F64(new_val))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral change: Return value no longer reflects stored precision

Previously, the half-float and float paths returned the value as stored (after narrowing to $hf_type/$f_type), reflecting the actual precision loss:

// Old half-float:
*num1 = <$hf_type>::from_f64(new_val);
Ok(NumOpResult::F64(f64::from(*num1)))  // returned the narrowed value

// Old float:
*num1 = new_val as $f_type;
Ok(NumOpResult::F64(*num1 as f64))      // returned the narrowed value

Now both paths return NumOpResult::F64(new_val) — the full f64 result, even when the stored value was narrowed to fit.

This means for non-promotion cases (where the value fits in f16/f32), the command returns a different value than what's actually stored. For example, an operation on an f16 array might return 1.000976... but actually store 1.0 in f16 precision.

Is this change intentional? If the goal is to always return the "mathematically correct" result regardless of storage precision, that's a valid design choice — but it should be documented. If not, consider returning f64::from(*num1) in the if narrowed.is_finite() branch (when the value fits without promotion).

AvivDavid23 and others added 2 commits March 16, 2026 12:10
…perations-wrap-results-for-smaller-typed-arrays-8-16-32-instead-of-promoting
@AvivDavid23 AvivDavid23 requested a review from gabsow March 16, 2026 10:11
@AvivDavid23 AvivDavid23 merged commit 70ca31d into master Mar 17, 2026
39 checks passed
@AvivDavid23 AvivDavid23 deleted the MOD-14427-json-oss-homogeneous-array-num-operations-wrap-results-for-smaller-typed-arrays-8-16-32-instead-of-promoting branch March 17, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants