Skip to content

fix(vars): match Python round semantics#6769

Open
anxkhn wants to merge 1 commit into
reflex-dev:mainfrom
anxkhn:fix/python-round-semantics
Open

fix(vars): match Python round semantics#6769
anxkhn wants to merge 1 commit into
reflex-dev:mainfrom
anxkhn:fix/python-round-semantics

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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)

Description

Compiled NumberVar rounding previously used JavaScript Math.round and
Number.prototype.toFixed. These APIs differ from Python on half ties, and
toFixed rejects negative ndigits values.

This change routes compiled rounding through a frontend helper that decomposes
IEEE 754 values into exact rational numbers and applies round-half-even. It adds
support for negative precision while preserving the existing static result types.

Testing

  • uv run pytest tests/units/test_var.py -q (223 passed)
  • uv run pytest tests/units --cov --no-cov-on-fail --cov-report= (6664 passed, 17 skipped, 78.09% coverage)
  • uv run ruff check .
  • uv run ruff format --check .
  • uv run pyright reflex tests (0 errors)
  • uv run pre-commit run --files packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/round.js packages/reflex-base/src/reflex_base/vars/number.py packages/reflex-base/news/6757.bugfix.md tests/units/test_var.py
  • node --check packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/round.js
  • uv run towncrier check --config pyproject.toml --dir packages/reflex-base --compare-with origin/main

The regression test executes the generated JavaScript in Node and covers positive
and negative half ties, negative precision, binary-float edge cases, extreme
precision, signed zero, non-finite values, helper imports, and result types.

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?

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn anxkhn requested a review from a team as a code owner July 15, 2026 05:04
@codspeed-hq

codspeed-hq Bot commented Jul 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing anxkhn:fix/python-round-semantics (d4de756) with main (65a2889)2

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (127e4d8) during the generation of this report, so 65a2889 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4de756510

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

const sign = value < 0 || Object.is(value, -0) ? "-" : "";
const result = Number(`${sign}${rounded}e${-ndigits}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard overflow when materializing rounded values

When rounding very large finite values with negative precisions near -308, this conversion silently overflows the exact BigInt result to Infinity. For example, pyRound(Number.MAX_VALUE, -308) returns Infinity, while Python's round(1.7976931348623157e308, -308) raises OverflowError instead of producing a non-finite value. This means compiled frontend expressions can introduce Infinity for finite inputs in cases where the helper is intended to mirror Python semantics; please guard the conversion and handle overflow consistently.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces Math.round / Number.prototype.toFixed in compiled NumberVar rounding with a new pyRound JS helper that decomposes IEEE 754 doubles into exact rationals and applies round-half-even, matching Python's round() semantics. It also adds support for negative ndigits values, which toFixed could not handle.

  • round.js implements banker's rounding by reading raw IEEE 754 bits via DataView/BigInt, computing exact numerator/denominator, and checking the tie condition (doubledRemainder === denominator && rounded % 2n === 1n).
  • number.py now routes all number_round_operation calls through pyRound(value, ndigits), adding the module import via VarData; the int/float return-type distinction for ndigits == 0 is preserved.
  • Tests extend test_var.py with 15 parametrized cases that execute the helper in Node.js and verify outputs against Python reference values.

Confidence Score: 5/5

Safe to merge — the change is well-scoped, the algorithm is arithmetically correct, and the 15 end-to-end test cases cover all boundary conditions.

The BigInt-based round-half-even algorithm produces correct results for the full set of test cases. The Python-side type plumbing (int for ndigits==0, float otherwise) is preserved. No pre-existing code paths are broken beyond the intentional change in rounding output.

No files require special attention.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/round.js New JS helper implementing Python round-half-even (banker's rounding) via IEEE 754 BigInt decomposition; handles negative ndigits, signed zero, and non-finite values correctly.
packages/reflex-base/src/reflex_base/vars/number.py Routes all NumberVar rounding through pyRound helper with correct import declaration; int/float return-type discrimination based on ndigits==0 is preserved.
tests/units/test_var.py Adds parametrized test that verifies Python round-half-even semantics end-to-end by running generated JS in Node.js; updates the existing snapshot for the changed expression string.
packages/reflex-base/news/6757.bugfix.md Changelog entry for the bugfix; no logic changes.

Reviews (2): Last reviewed commit: "fix(vars): match Python round semantics" | Re-trigger Greptile

Comment on lines +9 to +11
export default function pyRound(value, ndigits) {
if (!Number.isFinite(value) || ndigits > 323) return value;
if (ndigits < -308) return value * 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No guard for fractional ndigits values

BigInt() throws a RangeError for non-integer floats (e.g. BigInt(2.5)RangeError). Because shift = binaryExponent + ndigits (line 24) uses ndigits directly, any fractional ndigits — such as a Reflex state variable typed as float with a non-integer runtime value like 2.5 — would cause BigInt(shift) to throw and crash the frontend render. Adding if (!Number.isInteger(ndigits)) return value; after the existing early-return guards would make the function robust against this class of misuse.

Comment thread tests/units/test_var.py
Comment on lines +1075 to +1095
(-1.2345, -309, "-0"),
(float("inf"), 2, "Infinity"),
],
)
def test_number_round_python_semantics(value, ndigits, expected):
rounded = round(LiteralNumberVar.create(value), ndigits)
var_data = rounded._get_all_var_data()
assert var_data is not None
assert any(
imported.tag == "pyRound" and imported.is_default
for imported in dict(var_data.imports)["$/utils/helpers/round.js"]
)

result = subprocess.run(
[
"node",
"--input-type=module",
"--eval",
f"{ROUND_HELPER.read_text(encoding='utf-8')}\nconsole.log({rounded!s})",
],
check=True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 test_number_round_python_semantics requires Node.js in PATH

The parametrized test spawns node --input-type=module --eval ... via subprocess.run(check=True, ...). Any environment where node is not on PATH (minimal CI containers, pip install smoke tests, etc.) will cause all 15 parametrized cases to fail with a FileNotFoundError or CalledProcessError, not a proper pytest.skip. Consider guarding with pytest.importorskip or a skipif decorator based on shutil.which("node") so the suite degrades gracefully instead of erroring out.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@Alek99 Alek99 closed this Jul 15, 2026
@Alek99 Alek99 reopened this Jul 15, 2026
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