Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

math: Nextafter, Nextafter32 clarify behavior with -0, +0 #42613

Open
nsajko opened this issue Nov 14, 2020 · 4 comments
Open

math: Nextafter, Nextafter32 clarify behavior with -0, +0 #42613

nsajko opened this issue Nov 14, 2020 · 4 comments

Comments

@nsajko
Copy link
Contributor

@nsajko nsajko commented Nov 14, 2020

What version of Go are you using (go version)?

$ go version
go version go1.15.4 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/tmp/freedesktopCache/go-build"
GOENV="/home/nsajko/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/nsajko/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/nsajko"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build930141060=/tmp/go-build -gno-record-gcc-switches"

What did you do?

Some explanations are in the comments: https://play.golang.org/p/vaQBl7C6FNm

What did you expect to see?

Nextafter(x, y) should return the next representable
float64 value after x towards y, because none of the
three special cases apply here, because negative and
positive zeros and 1 and -1 are neither the same value
nor NaN.

-4.941e-324 -0 0 0
-0 -0 0 4.941e-324

What did you see instead?

...

-4.941e-324 -0 -0 4.941e-324
-4.941e-324 0 0 4.941e-324

Other notes

Relevant observation: negative and positive floating point zeros are distinct values in Go, but compare equal with the == operator. This is OK, but could be considered as the cause of the bug in the implementation.

Inconsistency with other finite float64 arguments

The inconsistency with the handling of zero arguments causes unexpected behavior when looping using the Nextafter functions.

This is not an actual issue for me, but it seems like it might be for someone who uses Nextafter: assuming a nonzero negative number x is chosen, start iterating on it like this: x = math.Nextafter32(x, 0), with the aim of breaking the loop at zero (a similar situation is observed when starting from a positive value, instead of a negative one). The loop, unexpectedly, will never exit in some cases: https://play.golang.org/p/eWs0pg8SXri

Inconsistency with C

Even though at first I thought that this is a relatively straightforward issue (because of the "next representable float64 value" explicit wording), this actually seems to be a tricky issue if compared with C, and it's even possible you may want to consider this a documentation bug instead of a behavioral bug, along with a couple other options.

The C nextafter functions were supposedly the inspiration for the Go version so it might be relevant to compare to them, also, it might be desirable to be compatible with them if you're appropriating their names already.

Both C17 and the current C draft have this to say about nextafter:

Description
The nextafter functions determine the next representable value, in the type of the function, after x
in the direction of y, where x and y are first converted to the type of the function.244) The nextafter
functions return y if x equals y. A range error may occur if the magnitude of x is the largest finite
value representable in the type and the result is infinite or not representable in the type.

Returns
The nextafter functions return the next representable value in the specified format after x in the
direction of y.

Notice the return y if x equals y explicit requirement about zeros - this is where C differs from Go currently.

But, except for that, the C implementations are actually like Go: they don't follow the spec when x is negative zero and y is a positive nonzero value, or when x is positive zero and y is a negative nonzero value. (In that case the other zero is "skipped over".)

C program for comparison (give it "0" on its stdin):

#include <math.h>
#include <stdio.h>

int
main(void) {
        int n;
        scanf("%d", &n);
        double x = n;
        double negX = -x;
        printf("%.4g %.4g %.4g %.4g\n%.4g %.4g %.4g %.4g\n",
            nextafter(negX, (double)-1), negX, nextafter(negX, x), nextafter(negX, (double)1),
            nextafter(x, (double)-1), nextafter(x, negX), x, nextafter(x, (double)1));

        return 0;
}

Outputs:

-4.941e-324 -0 0 4.941e-324
-4.941e-324 -0 0 4.941e-324
@randall77
Copy link
Contributor

@randall77 randall77 commented Nov 14, 2020

This all depends on the phrase

the next representable float64 value

I would interpret the next representable value after -0 to be 4.941e-324, not 0. 0 is not "after" -0 in my mind, as 0 == -0.
I can certainly see how that might be confusing, though.

We could add additional special cases to the docs, if you think that would help. Or was there something else you had in mind?

@randall77 randall77 changed the title math: Nextafter, Nextafter32 don't behave as documented with null arguments math: Nextafter, Nextafter32 clarify behavior with -0, +0 Nov 14, 2020
@nsajko
Copy link
Contributor Author

@nsajko nsajko commented Nov 14, 2020

I see these options:

  1. document the special cases around zero and keep the behavior the same
  2. have a special case for x == y (return y), like in C standard and implementations, otherwise keep behavior the same. I don't know where does this matter, but it might make sense to align with the C precedent.
  3. switch to the way I, at least, interpreted the "next representable float64 value" phrase. I.e., 0 and -0 are not the same value.
@randall77
Copy link
Contributor

@randall77 randall77 commented Nov 14, 2020

I vote for 1. We don't want to violate the Go 1 compatibility guarantee. I guess you could argue that this fits under the "bugs" exception, but it is not clear to me that it is even a bug. I think we'd need a stronger argument to make a backwards-incompatible change.

@cagedmantis cagedmantis added this to the Backlog milestone Nov 30, 2020
@cagedmantis
Copy link
Contributor

@cagedmantis cagedmantis commented Nov 30, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants
You can’t perform that action at this time.