fix: parenthesize a unary base in the compiled ** expression

DA
12h ago
+26-1

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_exponent_operation renders ({lhs} ** {rhs}) as is. JavaScript doesn't allow a unary expression as the base of ** (-2 ** 2 is a SyntaxError, unlike Python where it means -(2 ** 2)), so when the base is a negative literal or a negated var the compiled page doesn't build. For example (-State.velocity) ** 2 renders as (-(…velocity…) ** 2) and rx.Var.create(-2) ** State.n as (-2 ** …n…).

The base is now parenthesized when it starts with a unary operator, so those become ((-(…velocity…)) ** 2) and ((-2) ** …n…). Everything else renders exactly as before ((1 ** 2), (x ** -1), and -(x ** 2) from -x ** 2), so no existing snapshot changes.

Before / After with a one-page app, rx.text("squared: ", (-State.velocity) ** 2), running reflex export --frontend-only:

# before (main)
error: Build failed with 1 error:
[builtin:vite-transform] A unary expression with the '-' operator cannot
be used as the left operand of an exponentiation expression
    ╭─[ app_components/powapp/powapp.jsx:21:10 ]
 21 │ (-(reflex___state____state__powapp___powapp____state.velocity_rx_state_) ** 2)
    │ Help: Wrap the unary expression in parentheses to make the precedence explicit
exit=1

# after
Creating Production Build:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 4/4
exit=0

I also ran the rendered expressions through Node and they match Python: ((-(x)) ** 2) is 9, ((-2) ** x) is -8, ((-2.5) ** 2) is 6.25 for x = 3.

test_exponent_parenthesizes_a_unary_base covers the negative-literal and negated-var bases and the cases that must stay unwrapped, and fails on main with '(-2 ** 3)' == '((-2) ** 3)'. tests/units passes (11965 passed), along with ruff and pyright on the changed files.