From eed229fa0accd7bb47f20a34277afb250cf6db5f Mon Sep 17 00:00:00 2001 From: Enes Uygur Date: Thu, 22 Oct 2026 21:33:48 +0300 Subject: [PATCH 1/2] fix(vars): compile % with Python's sign-of-divisor semantics --- .../news/+var-modulo-python-semantics.bugfix.md | 1 + packages/reflex-base/src/reflex_base/vars/number.py | 5 ++++- tests/units/reflex_base/vars/test_number.py | 9 +++++++++ tests/units/test_var.py | 11 +++++++---- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 packages/reflex-base/news/+var-modulo-python-semantics.bugfix.md diff --git a/packages/reflex-base/news/+var-modulo-python-semantics.bugfix.md b/packages/reflex-base/news/+var-modulo-python-semantics.bugfix.md new file mode 100644 index 00000000000..03948413cae --- /dev/null +++ b/packages/reflex-base/news/+var-modulo-python-semantics.bugfix.md @@ -0,0 +1 @@ +Compile the `%` operator on number Vars with Python semantics, so the result takes the sign of the divisor (`-7 % 3` is now `2` on the frontend, not `-1`). diff --git a/packages/reflex-base/src/reflex_base/vars/number.py b/packages/reflex-base/src/reflex_base/vars/number.py index cacb83577d1..d6610d0dec9 100644 --- a/packages/reflex-base/src/reflex_base/vars/number.py +++ b/packages/reflex-base/src/reflex_base/vars/number.py @@ -645,7 +645,10 @@ def number_modulo_operation(lhs: NumberVar, rhs: NumberVar): Returns: The number modulo operation. """ - return f"({lhs!s} % {rhs!s})" + # JavaScript's `%` takes the sign of the dividend (`-7 % 3` is -1) where + # Python's takes the sign of the divisor (`-7 % 3` is 2), so a nonzero + # remainder whose sign differs from the divisor's is shifted by the divisor. + return f"((a, b) => {{ const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; }})({lhs!s}, {rhs!s})" _STARTS_WITH_UNARY_OPERATOR = re.compile(r"[-+!~]|(?:typeof|void|delete|await)\b") diff --git a/tests/units/reflex_base/vars/test_number.py b/tests/units/reflex_base/vars/test_number.py index b8041f3d93c..54f8048cbc0 100644 --- a/tests/units/reflex_base/vars/test_number.py +++ b/tests/units/reflex_base/vars/test_number.py @@ -13,3 +13,12 @@ def test_exponent_parenthesizes_a_unary_base(): assert str(-(x**2)) == "-((x ** 2))" assert str(x**-1) == "(x ** -1)" assert str(2**-x) == "(2 ** -(x))" + + +def test_modulo_takes_the_sign_of_the_divisor(): + """Python's `%` takes the sign of the divisor (`-7 % 3` is 2), JavaScript's the dividend's.""" + x = Var(_js_expr="x").to(int) + py_mod = "((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })" + assert str(LiteralNumberVar.create(-7) % 3) == f"{py_mod}(-7, 3)" + assert str(x % -2.5) == f"{py_mod}(x, -2.5)" + assert str(5 % x) == f"{py_mod}(5, x)" diff --git a/tests/units/test_var.py b/tests/units/test_var.py index b7f81370f27..1b10a9f0875 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -326,7 +326,10 @@ def test_basic_operations(TestObj): assert str(LiteralNumberVar.create(1) * 2) == "(1 * 2)" assert str(LiteralNumberVar.create(1) / 2) == "(1 / 2)" assert str(LiteralNumberVar.create(1) // 2) == "Math.floor(1 / 2)" - assert str(LiteralNumberVar.create(1) % 2) == "(1 % 2)" + assert ( + str(LiteralNumberVar.create(1) % 2) + == "((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(1, 2)" + ) assert str(LiteralNumberVar.create(1) ** 2) == "(1 ** 2)" assert str(LiteralNumberVar.create(1) & v(2)) == "pyAnd(1, () => (2))" assert str(LiteralNumberVar.create(1) | v(2)) == "pyOr(1, () => (2))" @@ -1172,7 +1175,7 @@ def test_all_number_operations(): assert ( str(complicated_number) - == "((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)" + == "(((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2)" ) even_more_complicated_number = ~( @@ -1181,7 +1184,7 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(isTrue(pyOr(Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))), () => (pyAnd(2, () => (Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))))" + == "!(isTrue(pyOr(Math.abs(Math.floor((((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))), () => (pyAnd(2, () => (Math.round((((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))))))))" ) assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" @@ -1410,7 +1413,7 @@ def test_array_filter(): # non-boolean results are evaluated with python truthiness truthy_predicate = array_var.filter(lambda x: x % 2) assert re.fullmatch( - r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => isTrue\(\(\1 % 2\)\)\)\)", + r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => isTrue\(\(\(a, b\) => \{ const r = a % b; return r !== 0 && \(r < 0\) !== \(b < 0\) \? r \+ b : r; \}\)\(\1, 2\)\)\)\)", str(truthy_predicate), ) _assert_var_imports(truthy_predicate, "isTrue") From 0ae0d85708122ed7112f8855be070f7f74035e90 Mon Sep 17 00:00:00 2001 From: Enes Uygur Date: Fri, 23 Oct 2026 14:49:46 +0300 Subject: [PATCH 2/2] fix(vars): give a zero remainder the divisor's sign in compiled % Python returns a zero remainder with the sign of the divisor (`6.0 % -3.0` is -0.0, `-6.0 % 3.0` is 0.0), while the compiled expression kept the dividend's sign for a zero result. --- packages/reflex-base/src/reflex_base/vars/number.py | 5 +++-- tests/units/reflex_base/vars/test_number.py | 2 +- tests/units/test_var.py | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/number.py b/packages/reflex-base/src/reflex_base/vars/number.py index d6610d0dec9..37c621ed99c 100644 --- a/packages/reflex-base/src/reflex_base/vars/number.py +++ b/packages/reflex-base/src/reflex_base/vars/number.py @@ -647,8 +647,9 @@ def number_modulo_operation(lhs: NumberVar, rhs: NumberVar): """ # JavaScript's `%` takes the sign of the dividend (`-7 % 3` is -1) where # Python's takes the sign of the divisor (`-7 % 3` is 2), so a nonzero - # remainder whose sign differs from the divisor's is shifted by the divisor. - return f"((a, b) => {{ const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; }})({lhs!s}, {rhs!s})" + # remainder whose sign differs from the divisor's is shifted by the divisor + # and a zero remainder takes the divisor's sign (`6.0 % -3.0` is -0.0). + return f"((a, b) => {{ const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; }})({lhs!s}, {rhs!s})" _STARTS_WITH_UNARY_OPERATOR = re.compile(r"[-+!~]|(?:typeof|void|delete|await)\b") diff --git a/tests/units/reflex_base/vars/test_number.py b/tests/units/reflex_base/vars/test_number.py index 54f8048cbc0..a5491ac9776 100644 --- a/tests/units/reflex_base/vars/test_number.py +++ b/tests/units/reflex_base/vars/test_number.py @@ -18,7 +18,7 @@ def test_exponent_parenthesizes_a_unary_base(): def test_modulo_takes_the_sign_of_the_divisor(): """Python's `%` takes the sign of the divisor (`-7 % 3` is 2), JavaScript's the dividend's.""" x = Var(_js_expr="x").to(int) - py_mod = "((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })" + py_mod = "((a, b) => { const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; })" assert str(LiteralNumberVar.create(-7) % 3) == f"{py_mod}(-7, 3)" assert str(x % -2.5) == f"{py_mod}(x, -2.5)" assert str(5 % x) == f"{py_mod}(5, x)" diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 1b10a9f0875..7d48a8c6d44 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -328,7 +328,7 @@ def test_basic_operations(TestObj): assert str(LiteralNumberVar.create(1) // 2) == "Math.floor(1 / 2)" assert ( str(LiteralNumberVar.create(1) % 2) - == "((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(1, 2)" + == "((a, b) => { const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; })(1, 2)" ) assert str(LiteralNumberVar.create(1) ** 2) == "(1 ** 2)" assert str(LiteralNumberVar.create(1) & v(2)) == "pyAnd(1, () => (2))" @@ -1175,7 +1175,7 @@ def test_all_number_operations(): assert ( str(complicated_number) - == "(((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2)" + == "(((a, b) => { const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2)" ) even_more_complicated_number = ~( @@ -1184,7 +1184,7 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(isTrue(pyOr(Math.abs(Math.floor((((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))), () => (pyAnd(2, () => (Math.round((((a, b) => { const r = a % b; return r !== 0 && (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))))))))" + == "!(isTrue(pyOr(Math.abs(Math.floor((((a, b) => { const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))), () => (pyAnd(2, () => (Math.round((((a, b) => { const r = a % b; return r === 0 ? (b < 0 ? -0 : 0) : (r < 0) !== (b < 0) ? r + b : r; })(Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2), 3) ** 2))))))))" ) assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" @@ -1413,7 +1413,7 @@ def test_array_filter(): # non-boolean results are evaluated with python truthiness truthy_predicate = array_var.filter(lambda x: x % 2) assert re.fullmatch( - r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => isTrue\(\(\(a, b\) => \{ const r = a % b; return r !== 0 && \(r < 0\) !== \(b < 0\) \? r \+ b : r; \}\)\(\1, 2\)\)\)\)", + r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => isTrue\(\(\(a, b\) => \{ const r = a % b; return r === 0 \? \(b < 0 \? -0 : 0\) : \(r < 0\) !== \(b < 0\) \? r \+ b : r; \}\)\(\1, 2\)\)\)\)", str(truthy_predicate), ) _assert_var_imports(truthy_predicate, "isTrue")