Skip to content

Commit

Permalink
cgen: use standard checks for float comparisons
Browse files Browse the repository at this point in the history
  • Loading branch information
UweKrueger authored Jun 4, 2020
1 parent 42e314d commit cf9498e
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 203 deletions.
21 changes: 0 additions & 21 deletions vlib/builtin/cfns.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -174,27 +174,6 @@ fn C.WIFSIGNALED() bool
fn C.WTERMSIG() int


fn C.DEFAULT_LE() bool


fn C.DEFAULT_EQ() bool


fn C.DEFAULT_GT() bool


fn C.DEFAULT_EQUAL() bool


fn C.DEFAULT_NOT_EQUAL() bool


fn C.DEFAULT_LT() bool


fn C.DEFAULT_GE() bool


fn C.isatty() int


Expand Down
136 changes: 45 additions & 91 deletions vlib/builtin/float.v
Original file line number Diff line number Diff line change
Expand Up @@ -86,107 +86,61 @@ fn f64_abs(a f64) f64 {
}
}

// compare floats using C epsilon
// ==
[inline]
pub fn (a f64) eq(b f64) bool {
return f64_abs(a - b) <= C.DBL_EPSILON
}

[inline]
pub fn (a f32) eq(b f32) bool {
return f32_abs(a - b) <= f32(C.FLT_EPSILON)
}

pub fn (a f64) eqbit(b f64) bool {
return C.DEFAULT_EQUAL(a, b)
}

pub fn (a f32) eqbit(b f32) bool {
return C.DEFAULT_EQUAL(a, b)
}

// !=
fn (a f64) ne(b f64) bool {
return !a.eq(b)
}

fn (a f32) ne(b f32) bool {
return !a.eq(b)
}

pub fn (a f64) nebit(b f64) bool {
return C.DEFAULT_NOT_EQUAL(a, b)
}

pub fn (a f32) nebit(b f32) bool {
return C.DEFAULT_NOT_EQUAL(a, b)
}

// a < b
fn (a f64) lt(b f64) bool {
return a.ne(b) && a.ltbit(b)
}

fn (a f32) lt(b f32) bool {
return a.ne(b) && a.ltbit(b)
}

fn (a f64) ltbit(b f64) bool {
return C.DEFAULT_LT(a, b)
}

fn (a f32) ltbit(b f32) bool {
return C.DEFAULT_LT(a, b)
}

// a <= b
fn (a f64) le(b f64) bool {
return !a.gt(b)
}

fn (a f32) le(b f32) bool {
return !a.gt(b)
}

fn (a f64) lebit(b f64) bool {
return C.DEFAULT_LE(a, b)
}

fn (a f32) lebit(b f32) bool {
return C.DEFAULT_LE(a, b)
}

// a > b
fn (a f64) gt(b f64) bool {
return a.ne(b) && a.gtbit(b)
}

fn (a f32) gt(b f32) bool {
return a.ne(b) && a.gtbit(b)
}

fn (a f64) gtbit(b f64) bool {
return C.DEFAULT_GT(a, b)
fn f32_max(a, b f32) f32 {
return if a > b {
a
} else {
b
}
}

fn (a f32) gtbit(b f32) bool {
return C.DEFAULT_GT(a, b)
[inline]
fn f32_min(a, b f32) f32 {
return if a < b {
a
} else {
b
}
}

// a >= b
fn (a f64) ge(b f64) bool {
return !a.lt(b)
[inline]
fn f64_max(a, b f64) f64 {
return if a > b {
a
} else {
b
}
}

fn (a f32) ge(b f32) bool {
return !a.lt(b)
[inline]
fn f64_min(a, b f64) f64 {
return if a < b {
a
} else {
b
}
}

fn (a f64) gebit(b f64) bool {
return C.DEFAULT_GE(a, b)
[inline]
fn (a f32) eq_epsilon(b f32) bool {
hi := f32_max(f32_abs(a), f32_abs(b))
delta := f32_abs(a - b)
if hi > f32(1.0) {
return delta <= hi * (4 * f32(C.FLT_EPSILON))
} else {
return (1 / (4 * f32(C.FLT_EPSILON))) * delta <= hi
}
}

fn (a f32) gebit(b f32) bool {
return C.DEFAULT_GE(a, b)
[inline]
fn (a f64) eq_epsilon(b f64) bool {
hi := f64_max(f64_abs(a), f64_abs(b))
delta := f64_abs(a - b)
if hi > 1.0 {
return delta <= hi * (4 * f64(C.DBL_EPSILON))
} else {
return (1 / (4 * f64(C.DBL_EPSILON))) * delta <= hi
}
}
111 changes: 111 additions & 0 deletions vlib/builtin/float_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,114 @@ fn test_float_decl() {
assert typeof(x15) == 'f64'
assert typeof(x16) == 'f64'
}

fn test_f32_equal_operator() {
b := f32(1.0)
mut a := f32(1.0)
a += 0.0000019073486328125
assert a != b
a -= 0.0000019073486328125
assert a == b
assert -1 == 1 * -1
assert -1.0 == 1.0 * -1.0
a = 1
a += 0.0000019073486328125
a -= 0.0000019073486328125
assert a == f32(1.0)
a += 0.000001
assert !(a < f32(1))
assert !(a <= f32(1))
assert a > f32(1)
assert a >= 1
assert a != 1
f := 1.2
ab := int(f)
assert ab == 1
e := f32(-1.602176634e-19)
m := f32(9.1093837015e-31)
assert e < m
assert e <= m
assert e != m
assert !(e == m)
assert m >= e
assert m > e
}

fn test_f64_equal_operator() {
b := 1.0
mut a := 1.0
a += 0.0000019073486328125
assert a != b
a -= 0.0000019073486328125
assert a == b
e := -1.602176634e-19
m := 9.1093837015e-31
assert e < m
assert e <= m
assert e != m
assert !(e == m)
assert m >= e
assert m > e
}

fn test_f64_eq_epsilon() {
a := 1.662248544459347e308
b := 1.662248544459348e308
x := 1.662248544459352e308
assert a != b
assert a.eq_epsilon(b)
assert b.eq_epsilon(a)
assert (-a).eq_epsilon(-b)
assert (-b).eq_epsilon(-a)
assert !a.eq_epsilon(x)
assert !x.eq_epsilon(a)
assert !a.eq_epsilon(-b)
assert !(-a).eq_epsilon(b)
c := 1.5367748374385438503
d := -1.5367748374385447257
z := 1.5367748378943546
assert c != -d
assert c.eq_epsilon(-d)
assert d.eq_epsilon(-c)
assert !c.eq_epsilon(z)
assert !z.eq_epsilon(c)
e := 2.531434251587394233e-308
f := 2.531434251587395675e-308
y := 2.531434251587398934e-308
assert e != f
assert e.eq_epsilon(f)
assert (-f).eq_epsilon(-e)
assert !e.eq_epsilon(y)
assert !(-y).eq_epsilon(-e)
}

fn test_f32_eq_epsilon() {
a := f32(3.244331e38)
b := f32(3.244332e38)
x := f32(3.244338e38)
assert a != b
assert a.eq_epsilon(b)
assert b.eq_epsilon(a)
assert (-a).eq_epsilon(-b)
assert (-b).eq_epsilon(-a)
assert !a.eq_epsilon(x)
assert !(-x).eq_epsilon(-a)
assert !a.eq_epsilon(-b)
assert !(-a).eq_epsilon(b)
c := f32(0.9546742)
d := f32(-0.9546745)
z := f32(0.9546754)
assert c != -d
assert c.eq_epsilon(-d)
assert d.eq_epsilon(-c)
assert !c.eq_epsilon(z)
assert !z.eq_epsilon(c)
e := f32(-1.5004390e-38)
f := f32(-1.5004395e-38)
y := f32(-1.5004409e-38)
assert e != f
assert e.eq_epsilon(f)
assert (-f).eq_epsilon(-e)
assert !e.eq_epsilon(y)
assert !(-y).eq_epsilon(-e)
}
40 changes: 0 additions & 40 deletions vlib/builtin/int_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,6 @@ fn test_const() {
assert u == 1 // make sure this works without the cast
}

fn test_float_equal_operator() {
b := f32(1.0)
mut a := f32(1.0)
a += 0.000001
a -= 0.000001
assert a == b
assert !a.eqbit(1.0)
assert !(a != f32(1.0))
assert a.nebit(f32(1.0))
a += 0.000001
assert !(a < 1.0)
assert !a.ltbit(1.0)
assert !(a <= 1)
assert !a.lebit(1)
assert a > 1
assert a.gtbit(1)
assert a >= 1
assert a.gebit(1)
assert -1 == 1 * -1
assert -1.0 == 1.0 * -1.0
a = 1
a += 0.000001
a -= 0.000001
assert a == f32(1.0)
assert !a.eqbit(f32(1.0))
assert !(a != f32(1.0))
a += 0.000001
assert !(a < f32(1))
assert !a.ltbit(f32(1))
assert !(a <= f32(1))
assert !a.lebit(f32(1))
assert a > f32(1)
assert a.gtbit(f32(1))
assert a >= 1
assert a.gebit(1)
f := 1.2
ab := int(f)
assert ab == 1
}

fn test_str_methods() {
assert i8(1).str() == '1'
assert i8(-1).str() == '-1'
Expand Down
16 changes: 8 additions & 8 deletions vlib/math/complex/complex_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ fn test_complex_abs() {
mut c1 := cmplx.complex(3,4)
assert c1.abs() == 5
c1 = cmplx.complex(1,2)
assert c1.abs().eq(math.sqrt(5))
assert c1.abs().eq(c1.conjugate().abs())
assert c1.abs() == math.sqrt(5)
assert c1.abs() == c1.conjugate().abs()
c1 = cmplx.complex(7,0)
assert c1.abs() == 7
}
Expand All @@ -132,17 +132,17 @@ fn test_complex_angle(){
// Test is based on and verified from practice examples of Khan Academy
// https://www.khanacademy.org/math/precalculus/imaginary-and-complex-numbers
mut c := cmplx.complex(1, 0)
assert (c.angle() * 180 / math.pi).eq(0)
assert c.angle() * 180 / math.pi == 0
c = cmplx.complex(1, 1)
assert (c.angle() * 180 / math.pi).eq(45)
assert c.angle() * 180 / math.pi == 45
c = cmplx.complex(0, 1)
assert (c.angle() * 180 / math.pi).eq(90)
assert c.angle() * 180 / math.pi == 90
c = cmplx.complex(-1, 1)
assert (c.angle() * 180 / math.pi).eq(135)
assert c.angle() * 180 / math.pi == 135
c = cmplx.complex(-1, -1)
assert (c.angle() * 180 / math.pi).eq(-135)
assert c.angle() * 180 / math.pi == -135
cc := c.conjugate()
assert (cc.angle() + c.angle()).eq(0)
assert cc.angle() + c.angle() == 0
}


Expand Down
Loading

4 comments on commit cf9498e

@dumblob
Copy link
Contributor

@dumblob dumblob commented on cf9498e Jun 4, 2020

Choose a reason for hiding this comment

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

@UweKrueger what is the motivation behing making == operator for floats behave like bitwise (i.e. unrelated to floats/doubles) equality check?

There was an agreement (see #2133 and #2158 ), that comparing floats with epsilon (at best dynamically - see e.g. g-truc/glm@72327ce ) is nowadays a standard practise (Lua, JS, ...) and only older languages like (C being one of them) have the flaw. It's a notorious source of bugs. In V it's also problematic to make == compare bitwise because of JS backend. Last it's super easy and maximally performant to use intrinsic biteq() method.

Could you maybe shed some light on this significant and backwards-incompatible change?

@spytheman @medvednikov thoughts?

@UweKrueger
Copy link
Member Author

Choose a reason for hiding this comment

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

@dumblob please have a look at issue/discussion #5180 and the comments to PR #5203.

@UweKrueger
Copy link
Member Author

Choose a reason for hiding this comment

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

@dumblob BTW: Are you sure about lua? I've just checked the source code of lua-5.3.5 and lua-5.4.0rc4: in both versions float comparisons are done with luai_numeq() which is a macro that expands just to the standard C comparison: #define luai_numeq(a,b) ((a)==(b)). I've also checked the interpreter and have not found any hints for an implied tolerance in float comparisons.

@UweKrueger
Copy link
Member Author

Choose a reason for hiding this comment

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

@dumblob Javascript seems to do a standard IEEE754 comparison, too, IMHO...
Python also uses standard comparisons. In numpy there is an interesting function (which is of cause not used for default comparisons): numpy.isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False) which allows sophisticated checks with relative and absolute tolerance.

Please sign in to comment.