Skip to content

Improve solve_for_outer_interval for max/min comparisons#9162

Open
alexreinking wants to merge 1 commit into
mainfrom
alexreinking/solve-for-max-interval
Open

Improve solve_for_outer_interval for max/min comparisons#9162
alexreinking wants to merge 1 commit into
mainfrom
alexreinking/solve-for-max-interval

Conversation

@alexreinking
Copy link
Copy Markdown
Member

Two related improvements to SolveForInterval:

  1. SolveExpression::visit(Sub): add f(x)*a - f(x) → f(x)*(a-1) and the symmetric f(x) - f(x)*a → f(x)*(1-a) reduction rules. Without these, expressions like 2*x - x couldn't be simplified, blocking the solver from rearranging comparisons such as 2*x <= x into x <= 0.

  2. SolveForInterval::visit(LE/GE): when solve_expression fails to fully isolate the variable, try a direct max/min decomposition on the LHS before falling back to fail(). The rules applied are: max(a, b) <= c ⟺ a <= c && b <= c min(a, b) <= c ⟺ a <= c || b <= c max(a, b) * pos_c <= rhs ⟺ a*pos_c <= rhs && b*pos_c <= rhs min(a, b) * pos_c <= rhs ⟺ a*pos_c <= rhs || b*pos_c <= rhs (and symmetric >= variants)

Together these enable two previously unhandled cases:

  • max(x, 1) * 2 <= xInterval::nothing() (always false)
  • max(abs(select(x < 0, f(x-1), 5)), 2) <= x[2, +∞) (the constant branch forces x >= 2 even when f is opaque)

These limitations were found by @stevenraphael

Checklist

  • Tests added or updated (not required for docs, CI config, or typo fixes)
  • Commits include AI attribution where applicable (see Code of Conduct)

Two related improvements to SolveForInterval:

1. SolveExpression::visit(Sub): add f(x)*a - f(x) → f(x)*(a-1) and the
   symmetric f(x) - f(x)*a → f(x)*(1-a) reduction rules. Without these,
   expressions like 2*x - x couldn't be simplified, blocking the solver
   from rearranging comparisons such as 2*x <= x into x <= 0.

2. SolveForInterval::visit(LE/GE): when solve_expression fails to fully
   isolate the variable, try a direct max/min decomposition on the LHS
   before falling back to fail(). The rules applied are:
     max(a, b) <= c  ⟺  a <= c && b <= c
     min(a, b) <= c  ⟺  a <= c || b <= c
     max(a, b) * pos_c <= rhs  ⟺  a*pos_c <= rhs && b*pos_c <= rhs
     min(a, b) * pos_c <= rhs  ⟺  a*pos_c <= rhs || b*pos_c <= rhs
   (and symmetric >= variants)

Together these enable two previously unhandled cases:
  - max(x, 1) * 2 <= x  →  Interval::nothing()  (always false)
  - max(abs(select(x < 0, f(x-1), 5)), 2) <= x  →  [2, +∞)
    (the constant branch forces x >= 2 even when f is opaque)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alexreinking alexreinking requested a review from abadams May 29, 2026 16:27
@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 11 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@7e2ecf2). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Solve.cpp 75.00% 4 Missing and 7 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9162   +/-   ##
=======================================
  Coverage        ?   69.29%           
=======================================
  Files           ?      254           
  Lines           ?    78258           
  Branches        ?    18726           
=======================================
  Hits            ?    54230           
  Misses          ?    18498           
  Partials        ?     5530           

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Member

@abadams abadams left a comment

Choose a reason for hiding this comment

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

lgtm provided the fuzzer accepts it

@alexreinking
Copy link
Copy Markdown
Member Author

The fuzz failure in CI is not related to this change. It's in fuzz_lossless_cast:

Seed: 15862464924509321052
lossless_cast failure
x = 0
buf_u8 = 37
buf_i8 = -80
out1 = -81
out2 = 175
Original: (int64)widening_sub(int32(int16(-16_i8 + (32_i8/int8((uint8)buf_u8(x))))), int32(uint16(223_u8*uint8(((-16_i8 + (32_i8/int8((uint8)buf_u8(x))))/-33_i8)*-33_i8))))
Lossless cast: (int16(-16_i8 + (32_i8/int8((uint8)buf_u8(x)))) - int16(223_u8*uint8(((-16_i8 + (32_i8/int8((uint8)buf_u8(x))))/-33_i8)*-33_i8)))

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