Skip to content

apd: fix Modf, Floor and Ceil when the result aliases the operand - #155

Merged
rafiss merged 2 commits into
cockroachdb:masterfrom
chiliec:fix-floor-ceil-aliasing
Oct 22, 2026
Merged

rafiss merged 2 commits into
cockroachdb:masterfrom
chiliec:fix-floor-ceil-aliasing

Conversation

@chiliec

@chiliec chiliec commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #154.

Decimal.Modf set integ's exponent and sign (or zeroed it outright in the |d| < 1-with-leading-zeros branch) before it read d. When integ == d, which is how Floor and Ceil call it in the aliased case, that overwrote the operand. For values like -0.01 the fractional part then came back as zero, so Floor(x, x) returned -0 instead of -1, and Ceil(x, x) for 0.01 returned 0 instead of 1. The frac == d case had the same problem in the Exponent > 0 branch.

The fix only reorders the writes. Every read of d now happens before integ/frac are modified. The computation itself is unchanged.

Tests:

  • TestFloor/TestCeil: added -0.01/0.01 (they already run aliased, Floor(d, d)).
  • TestModf: every existing case now also runs with integ aliased to x and with frac aliased to x.

Validation (go1.23.4, linux/amd64):

  • On master with the new tests: 29 subtests fail, e.g. TestCeil/0.01: got 0, expected 1 and TestFloor/-0.01: got 0, expected -1.
  • With this change: go test ./... ok, go test -race ./... ok, go vet -unsafeptr=false ./... clean.

Modf wrote the integral part's sign/exponent (or the zero value) into
integ before reading d, so calling it with integ == d (as Floor and
Ceil do) clobbered the operand. For |x| < 0.1 this made the fractional
part zero, so Floor(-0.01) and Ceil(0.01) returned 0 instead of -1 and 1
when the result aliased the operand.

Fixes cockroachdb#154.
Modf's write order matters: integ or frac may be d itself, so every
read of d happens before either result is written. Say so in the
godoc and at the trailing write, note that integ and frac must be
distinct, and fix the comment on the |d| < 1 branch, which said d < 0.

Extend TestModf to the x.Modf(x, nil) and x.Modf(nil, x) forms and
make TestCeil and TestFloor check that the aliased result matches a
separate destination.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@rafiss rafiss left a comment

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.

The reordering is correct and minimal. I traced all three branches for integ == d and frac == d, and the new tests fail on master for exactly the #154 cases.

I pushed 400d13c on top: it states in the Modf godoc that integ or frac may alias d and that the two must be distinct, fixes the "d < 0" comment on the |d| < 1 branch, notes at the trailing write why it comes after the reads, adds the x.Modf(x, nil) and x.Modf(nil, x) forms to TestModf, and makes TestCeil and TestFloor check the aliased result against a separate destination.

Floor and Ceil of NaN or Infinity still return a finite 0 into a non-aliased destination. That predates this PR and is being handled in #158.

@rafiss
rafiss merged commit 2e1091e into cockroachdb:master Oct 22, 2026
18 checks passed
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.

Context.Floor and Context.Ceil with the result aliased to the operand return 0 for values smaller than 0.1

2 participants