Skip to content

fix: std.round preserves identity for |x| >= 2^52#1004

Open
He-Pin wants to merge 1 commit into
databricks:masterfrom
He-Pin:worktree-fix-round-large-integer
Open

fix: std.round preserves identity for |x| >= 2^52#1004
He-Pin wants to merge 1 commit into
databricks:masterfrom
He-Pin:worktree-fix-round-large-integer

Conversation

@He-Pin

@He-Pin He-Pin commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Motivation

std.round returned the wrong integer for large odd inputs in
[2^52, 2^53). For example, std.round(9007199254740991) (2^53 - 1)
produced 9007199254740992. The prior floor(x + 0.5) /
ceil(x - 0.5) implementation invokes IEEE 754 round-to-even when the
ULP is >= 1.0, so adding 0.5 rounds to the nearest even integer
rather than the correct half-away-from-zero value. go-jsonnet
(math.Round) and jrsonnet both return the identity for these inputs;
sjsonnet was the outlier.

Modification

  • Short-circuit in MathModule.scala std.round when |x| >= 2^52
    or x is NaN — at that magnitude every double is already an exact
    integer (ULP >= 1.0), so returning x unchanged is both correct and
    avoids the precision trap.
  • Half-away-from-zero semantics are preserved for all smaller
    magnitudes; NaN / Infinity propagate naturally.
  • Add five regression assertions (2^53-1, 2^53-2, 2^52+1, -(2^53-1),
    1e20) to the existing std.round/isEven/isOdd/isInteger/isDecimal
    fixture.

Result

std.round now matches go-jsonnet and jrsonnet on every integer in
[2^52, 2^53). The existing std.round(0.5)==1 /
std.round(-0.5)==-1 half-away-from-zero semantics are preserved.
All 23 file tests plus 86 evaluator tests pass.

References

Motivation:
std.round returned the wrong integer for large odd inputs in [2^52, 2^53)
(e.g. std.round(9007199254740991) produced 9007199254740992). The prior
`floor(x + 0.5)` / `ceil(x - 0.5)` implementation invokes IEEE 754
round-to-even when the ULP is >= 1.0, so adding 0.5 rounds to the
nearest *even* integer rather than the correct half-away-from-zero.
go-jsonnet (math.Round) and jrsonnet both return the identity for
these inputs; sjsonnet was the outlier.

Modification:
Short-circuit in MathModule.scala std.round when |x| >= 2^52 or x is
NaN — at that magnitude every double is already an exact integer
(ULP >= 1.0), so returning x unchanged is both correct and avoids the
precision trap. The half-away-from-zero rule is preserved for all
smaller magnitudes, and NaN/Infinity propagate naturally.

Added five regression assertions (2^53 - 1, 2^53 - 2, 2^52 + 1,
-(2^53 - 1), 1e20) to the existing std.round/isEven/isOdd/isInteger/
isDecimal test fixture to lock in the corrected behavior.

Result:
std.round now matches go-jsonnet and jrsonnet on every integer in
[2^52, 2^53). The existing std.round(0.5)==1 / std.round(-0.5)==-1
half-away-from-zero semantics are preserved, and all 23 file tests
plus 86 evaluator tests pass.

Cross-implementation comparison (std.round input → output):
| Input               | sjsonnet (before) | sjsonnet (after) | go-jsonnet 0.22.0 | jrsonnet 0.5.0-pre99 | C++ jsonnet 0.22.0 |
|---------------------|-------------------|------------------|-------------------|----------------------|--------------------|
| 9007199254740991    | 9007199254740992  | 9007199254740991 | 9007199254740991  | 9007199254740991     | 9007199254740992 * |
| 4503599627370497    | 4503599627370498  | 4503599627370497 | 4503599627370497  | 4503599627370497     | 4503599627370498 * |
| -9007199254740991   | -9007199254740992 | -9007199254740991| -9007199254740991 | -9007199254740991    | -9007199254740990* |
| 0.5                 | 1                 | 1                | 1                 | 1                    | 1                  |
| -0.5                | -1                | -1               | -1                | -1                   | 0 *                |
| 2.5                 | 3                 | 3                | 3                 | 3                    | 2 *                |

* C++ jsonnet v0.22.0 uses round-half-to-even (round(-0.5)==0,
  round(2.5)==2) and exhibits the same large-integer precision loss
  as pre-fix sjsonnet; go-jsonnet and jrsonnet use half-away-from-zero.
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.

1 participant