Repository: reflex-dev/reflex

Issue:
rx.cond not working properly with boolean vars
As acknowledged in this discussion https://discord.com/channels/1029853095527727165/1061874061250150441/threads/1327278371234906162 using boolean vars checks in rx.cond is not working as expected.

To Reproduce

rx.cond(fixture.has_started == rx.Var('true', _var_type=bool),'A ','B ') -> works
rx.cond(fixture.has_started == rx.Var.create(True),'A ','B ') -> doesn't work
rx.cond(fixture.has_started == True,'A ','B ') -> doesn't work (used to work on reflex 0.5.x)
rx.cond(fixture.has_started,'A ','B ') -> works

here's the compilation output for each of them:

fixture["has_started"] === true ? "A " : "B "
fixture["has_started"] === Number(true) -> wrong
fixture["has_started"] === 1 ? "A " : "B " -> wrong
fixture["has_started"] ? "A " : "B "

Reflex Version: v0.6.6.post3

rx.cond not working properly with multiple checks
As acknowledged in this discussion https://discord.com/channels/1029853095527727165/1061874061250150441/threads/1327278371234906162 using multiple checks in rx.cond is not working as expected.

To Reproduce

this condition (minute is a int | Non var)
fixture.minute & (fixture.minute >= 45) & (fixture.state_code != 'INPLAY_1ST_HALF')

gets compiled to 
fixture["minute"] && fixture["minute"] >= 45 && !(fixture["state_code"] === "INPLAY_1ST_HALF")

as you can see, parenthesis are wrongly removed from fixture["minute"] >= 45, therefore the conditions fails

instead, if i remove the first item from rx.cond like this

(fixture.minute >= 45) & (fixture.state_code != 'INPLAY_1ST_HALF')

it gets compiled to the following code

fixture["minute"] >= 45 && !(fixture["state_code"] === "INPLAY_1ST_HALF")

which works, but it would stop working as soon as I add one more item to the condition.

I wonder why there are no parenthesis around the first condition and why we use !(x === y) instead of (x !== y)
I think it would be much safer to always wrap items with parenthesis, so I would expect the compilation to work as follows

input
fixture.minute & (fixture.minute >= 45) & (fixture.state_code != 'INPLAY_1ST_HALF')

expected output
(fixture["minute"] !== null && fixture["minute"] !== 0) && (fixture["minute"] >= 45) && (fixture["state_code"] !== "INPLAY_1ST_HALF")

Reflex Version: v0.6.6.post3



Discussion:
<p><a href="https://linear.app/reflex-dev/issue/ENG-4348/rxcond-not-working-properly-with-boolean-vars">ENG-4348 rx.cond not working properly with boolean vars</a></p>

<p><a href="https://linear.app/reflex-dev/issue/ENG-4349/rxcond-not-working-properly-with-multiple-checks">ENG-4349 rx.cond not working properly with multiple checks</a></p>
what is the type of `fixture`? sometimes it seems like an object, and other times it seems like a list
> what is the type of `fixture`? sometimes it seems like an object, and other times it seems like a list

sorry, i just fixed the typos.

In the original code fixture is a tuple and the first item (fixture[0]) is a var based on rx.Mode.
However, I've also experienced the same issue with other kinds of vars which are not tuples but simple rx.Model based vars.
The `&` is behaving like `and` in Python here. No additional `.bool()` should be added in this case. (This behavior changed in 0.6.0, previously `&` called `.bool()` on all of its arguments)
> The `&` is behaving like `and` in Python here. No additional `.bool()` should be added in this case. (This behavior changed in 0.6.0, previously `&` called `.bool()` on all of its arguments)

Yes I knew that. In this case we aren’t calling bool() anywhere, what are you referring to?
> The `&` is behaving like `and` in Python here. No additional `.bool()` should be added in this case. (This behavior changed in 0.6.0, previously `&` called `.bool()` on all of its arguments)

By the way, as you can see the following condition in the compiled javascript code is failing

fixture["minute"] && fixture["minute"] >= 45 && !(fixture["state_code"] === "INPLAY_1ST_HALF")

This happens because there are missing parenthesis here
fixture[“minute”] && fixture["minute"] >= 45
I think it should be
fixture[“minute”] && (fixture["minute"] >= 45)

Right?

As an additional evidence, I found out that this

rx.cond(fixture.minute, …) // minute is int | None

produces

fixture.minute !== null && fixture.minute !== 0

which is what I would expect, but this

rx.cond(fixture.minute & (fixture.minute >= 45) & (fixture.state_code != 'INPLAY_1ST_HALF'), …)

produces

fixture["minute"] && fixture["minute"] >= 45 && !(fixture["state_code"] === "INPLAY_1ST_HALF")

which doesn’t work.

Looks like something breaks when there are multiple conditions.
> fixture[“minute”] && (fixture["minute"] >= 45)

I'm not sure they are different ? but i don't think it particularly hurts to have those parentheses. We can add them.
> Looks like something breaks when there are multiple conditions.

Cond basically takes whatever result and casts it as bool, in the first case it makes sense, in the second case, it should cast the whole thing as bool which i believe it does, Cond doesn't dig into the expression to convert parameters into bools

Again, && is working like `and` in Python, returning the first falsy expression. If you want to have `&&` in the C++ sense, you should call `.bool()` on the non-bool parameters of your `&&`.
> > Looks like something breaks when there are multiple conditions.
> 
> Cond basically takes whatever result and casts it as bool, in the first case it makes sense, in the second case, it should cast the whole thing as bool which i believe it does, Cond doesn't dig into the expression to convert parameters into bools
> 
> Again, && is working like `and` in Python, returning the first falsy expression. If you want to have `&&` in the C++ sense, you should call `.bool()` on the non-bool parameters of your `&&`.

I don't think the way the condition is compiled is correct, look here below

![Screenshot 2025-01-10 at 20 24 17](https://github.com/user-attachments/assets/3b92b15b-a8ef-4dfc-9be3-5a8c3ffea576)

this returns false in Javascript, while the original condition is expected to return true if minute is equal to 1.

Again, if the input condition is just as follows

`rx.cond(fixture.minute, …)`

the compilation output is

`fixture["minute"] !== null && fixture["minute"] !== 0`

which is correct, however if I chain it with other conditions, it generates a wrong Javascript code

`rx.cond(fixture.minute & (fixture.minute >= 45) & (fixture.state_code != 'INPLAY_1ST_HALF'), …)`

gets compiled to: `fixture["minute"] && fixture["minute"] >= 45 && !(fixture["state_code"] === "INPLAY_1ST_HALF")`

see the issue here? Why `fixture["minute"] !== null && fixture["minute"] !== 0` is now missing? Why there are no parenthesis around `fixture["minute"] >= 45` ?
> this returns false in Javascript, while the original condition is expected to return true if minute is equal to 1.

why would and between `1` and `1 >= 45` should return true?

also, your compilation output is wrong,

```py
class Fixture(rx.Base):
    minute: int | None
    state_code: str | None


fixture = rx.Var("fixture").to(Fixture)

print(
    fixture.minute & (fixture.minute >= 45) & (fixture.state_code != "INPLAY_1ST_HALF")
)
```
gives:
```js
((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF")))
```
which cannot be more detailed

if we have it inside of a cond we get:
```js
isTrue(((((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF"))) !== null) && (((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF"))) !== 0)))
```
which does seem to be correct

> > this returns false in Javascript, while the original condition is expected to return true if minute is equal to 1.
> 
> why would and between `1` and `1 >= 45` should return true?
> 
> also, your compilation output is wrong,
> 
> ```python
> class Fixture(rx.Base):
>     minute: int | None
>     state_code: str | None
> 
> 
> fixture = rx.Var("fixture").to(Fixture)
> 
> print(
>     fixture.minute & (fixture.minute >= 45) & (fixture.state_code != "INPLAY_1ST_HALF")
> )
> ```
> 
> gives:
> 
> ```js
> ((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF")))
> ```
> 
> which cannot be more detailed
> 
> if we have it inside of a cond we get:
> 
> ```js
> isTrue(((((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF"))) !== null) && (((fixture["minute"] && (fixture["minute"] >= 45)) && !((fixture["state_code"] === "INPLAY_1ST_HALF"))) !== 0)))
> ```
> 
> which does seem to be correct

Sorry, I posted thew wrong screenshot, my bad :)

However, when using this condition in Reflex

`rx.cond(fixture[0].minute & (fixture[0].minute >= 45) & (fixture[0].state_code != 'INPLAY_1ST_HALF'),'('+fixture[0].home_1st_score.to_string()+') ',''),`

I get the following compiled code (I've just tested it once again)

`(f.at(0)[\"minute\"] && f.at(0)[\"minute\"] >= 45 && !(f.at(0)[\"state_code\"] === \"INPLAY_1ST_HALF\")) !== null && (f.at(0)[\"minute\"] && f.at(0)[\"minute\"] >= 45 && !(f.at(0)[\"state_code\"] === \"INPLAY_1ST_HALF\")) !== 0)`

which isn't at all what I'm checking in rx.cond() and in fact it's not working. In my test minute was equal to 37 but the condition is still returning true. How is that possible?

I mean, I'm not a a Javascript expert but this compilation looks wrong to me (just like the compiled code you posted).

`(f.at(0)[\"minute\"] && f.at(0)[\"minute\"] >= 45 && !(f.at(0)[\"state_code\"] === \"INPLAY_1ST_HALF\")) !== null`

This is not what I'm trying to check in rx.cond(). I would like to check that:
- minute is not null
- minute is greater than or equal to 45
- state_code != "INPLAY_1ST_HALF"

so I would rather expect the output to be something like:

`(f.at(0).minute !== null && f.at(0).minute !== 0) && (f.at(0).minute >= 45) && (f.at(0).state_code !== "INPLAY_1ST_HALF")`

Does that make sense to you?

Thanks
There *is* a bug here. I removed the special case of `bool` in number vars.

However, you still have a misunderstanding. The `&` operator with Vars behave similarly to `and`, which doesn't differ if it was inside of `if` or not. In this case your equivalent python code:

```py
fixture.minute and (fixture[0].minute >= 45) and (fixture[0].state_code != 'INPLAY_1ST_HALF')
```
can sometimes return int or None! When you put it in `if` those get casted to `False`. That is expected, and correct.

The bug was that the casting was a bit weird, because it didn't capture the different between numbers and booleans (which are different in JS, but not in Python)
> There _is_ a bug here. I removed the special case of `bool` in number vars.
> 
> However, you still have a misunderstanding. The `&` operator with Vars behave similarly to `and`, which doesn't differ if it was inside of `if` or not. In this case your equivalent python code:
> 
> ```python
> fixture.minute and (fixture[0].minute >= 45) and (fixture[0].state_code != 'INPLAY_1ST_HALF')
> ```
> 
> can sometimes return int or None! When you put it in `if` those get casted to `False`. That is expected, and correct.
> 
> The bug was that the casting was a bit weird, because it didn't capture the different between numbers and booleans (which are different in JS, but not in Python)

Ok, thank you very much for the explanation and for the fix!




Patch:
diff --git a/reflex/vars/number.py b/reflex/vars/number.py
index d04aded353e..a2a0293d562 100644
--- a/reflex/vars/number.py
+++ b/reflex/vars/number.py
@@ -20,7 +20,6 @@
 from reflex.constants.base import Dirs
 from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError
 from reflex.utils.imports import ImportDict, ImportVar
-from reflex.utils.types import is_optional
 
 from .base import (
     CustomVarOperationReturn,
@@ -431,7 +430,7 @@ def __lt__(self, other: Any):
         """
         if not isinstance(other, NUMBER_TYPES):
             raise_unsupported_operand_types("<", (type(self), type(other)))
-        return less_than_operation(self, +other)
+        return less_than_operation(+self, +other)
 
     @overload
     def __le__(self, other: number_types) -> BooleanVar: ...
@@ -450,7 +449,7 @@ def __le__(self, other: Any):
         """
         if not isinstance(other, NUMBER_TYPES):
             raise_unsupported_operand_types("<=", (type(self), type(other)))
-        return less_than_or_equal_operation(self, +other)
+        return less_than_or_equal_operation(+self, +other)
 
     def __eq__(self, other: Any):
         """Equal comparison.
@@ -462,7 +461,7 @@ def __eq__(self, other: Any):
             The result of the comparison.
         """
         if isinstance(other, NUMBER_TYPES):
-            return equal_operation(self, +other)
+            return equal_operation(+self, +other)
         return equal_operation(self, other)
 
     def __ne__(self, other: Any):
@@ -475,7 +474,7 @@ def __ne__(self, other: Any):
             The result of the comparison.
         """
         if isinstance(other, NUMBER_TYPES):
-            return not_equal_operation(self, +other)
+            return not_equal_operation(+self, +other)
         return not_equal_operation(self, other)
 
     @overload
@@ -495,7 +494,7 @@ def __gt__(self, other: Any):
         """
         if not isinstance(other, NUMBER_TYPES):
             raise_unsupported_operand_types(">", (type(self), type(other)))
-        return greater_than_operation(self, +other)
+        return greater_than_operation(+self, +other)
 
     @overload
     def __ge__(self, other: number_types) -> BooleanVar: ...
@@ -514,17 +513,7 @@ def __ge__(self, other: Any):
         """
         if not isinstance(other, NUMBER_TYPES):
             raise_unsupported_operand_types(">=", (type(self), type(other)))
-        return greater_than_or_equal_operation(self, +other)
-
-    def bool(self):
-        """Boolean conversion.
-
-        Returns:
-            The boolean value of the number.
-        """
-        if is_optional(self._var_type):
-            return boolify((self != None) & (self != 0))  # noqa: E711
-        return self != 0
+        return greater_than_or_equal_operation(+self, +other)
 
     def _is_strict_float(self) -> bool:
         """Check if the number is a float.


Test patch:
diff --git a/tests/units/test_var.py b/tests/units/test_var.py
index bfa8aa35aa1..e5f5bbb2a2d 100644
--- a/tests/units/test_var.py
+++ b/tests/units/test_var.py
@@ -1004,7 +1004,7 @@ def test_all_number_operations():
 
     assert (
         str(even_more_complicated_number)
-        == "!(((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)))) !== 0))"
+        == "!(isTrue((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))"
     )
 
     assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)"