fix(vars): compile % with Python's sign-of-divisor semantics

EN
1d ago
+22-5

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Description

number_modulo_operation compiles a % b to JavaScript's %, which takes the sign of the dividend. Python's % takes the sign of the divisor. So a modulo that is correct in Python gives a different answer on the frontend once an operand is negative:

  • -7 % 3 is 2 in Python but renders as (-7 % 3), which is -1 in the browser.
  • 5.5 % -2 is -0.5 in Python but 1.5 in JS.

Floor division already follows Python (Math.floor(a / b)), so x // y * y + x % y doesn't give back x for negative values. The usual wrap-around pattern (State.index - 1) % State.count also yields -1 instead of the last index.

The operation now compiles to a small arrow function that applies CPython's float-modulo rule: when the remainder is nonzero and its sign differs from the divisor's, the divisor is added. Each operand is still evaluated once, and no new import is needed.

I ran the compiled expressions through Node and compared them with Python's %. All of these match: (-7, 3), (7, -3), (-7, -3), (5.5, -2), (-5.5, 2), (0, 5), (-6, 3), (0.3, 0.1), and ±Infinity divisors.

test_modulo_takes_the_sign_of_the_divisor fails on main with '(-7 % 3)' == '((a, b) => {... r; })(-7, 3)'. The three existing test_var.py assertions that pinned the old (a % b) output are updated. The PR also includes a bugfix news fragment for reflex-base. ruff and pyright are clean on the changed files.