Skip to content

float tests fails on wasm32-unknown-emscripten #42630

Closed
@malbarbo

Description

@malbarbo
Contributor

This tests (taken from rust tests):

#![feature(dec2flt)]

#![allow(unused_macros, unused_imports, dead_code)]

extern crate core;

use std::num::FpCategory::*;
use std::num::FpCategory as Fp;
use core::num::dec2flt::rawfp::{RawFloat, Unpacked};

const SOME_FLOATS: [f64; 9] =
    [0.1f64, 33.568, 42.1e-5, 777.0e9, 1.1111, 0.347997,
     9843579834.35892, 12456.0e-150, 54389573.0e-150];

// src/libcore/tests/num/dec2flt/mod.rs

// Take a float literal, turn it into a string in various ways (that are all trusted
// to be correct) and see if those strings are parsed back to the value of the literal.
// Requires a *polymorphic literal*, i.e. one that can serve as f64 as well as f32.
macro_rules! test_literal {
    ($x: expr) => ({
        let x32: f32 = $x;
        let x64: f64 = $x;
        let inputs = &[stringify!($x).into(), format!("{:?}", x64), format!("{:e}", x64)];
        for input in inputs {
            assert_eq!(input.parse(), Ok(x64));
            assert_eq!(input.parse(), Ok(x32));
            let neg_input = &format!("-{}", input);
            assert_eq!(neg_input.parse(), Ok(-x64));
            assert_eq!(neg_input.parse(), Ok(-x32));
        }
    })
}

#[test]
fn ordinary() {
    test_literal!(1.0);
    test_literal!(3e-5);
    test_literal!(0.1);
    test_literal!(12345.);
    test_literal!(0.9999999);
    test_literal!(2.2250738585072014e-308);
}

#[test]
fn special_code_paths() {
    test_literal!(36893488147419103229.0); // 2^65 - 3, triggers half-to-even with even significand
    test_literal!(101e-33); // Triggers the tricky underflow case in AlgorithmM (for f32)
    test_literal!(1e23); // Triggers AlgorithmR
    test_literal!(2075e23); // Triggers another path through AlgorithmR
    test_literal!(8713e-23); // ... and yet another.
}


// src/libcore/tests/num/dec2flt/rawfp.rs

#[test]
fn prev_float_monotonic() {
    let mut x = 1.0;
    for _ in 0..100 {
        let x1 = prev_float(x);
        assert!(x1 < x);
        assert!(x - x1 < 1e-15);
        x = x1;
    }
}

#[test]
fn next_prev_identity() {
    for &x in &SOME_FLOATS {
        assert_eq!(prev_float(next_float(x)), x);
        assert_eq!(prev_float(prev_float(next_float(next_float(x)))), x);
        assert_eq!(next_float(prev_float(x)), x);
        assert_eq!(next_float(next_float(prev_float(prev_float(x)))), x);
    }
}


/// Inverse of `RawFloat::unpack()` for normalized numbers.
/// Panics if the significand or exponent are not valid for normalized numbers.
pub fn encode_normal<T: RawFloat>(x: Unpacked) -> T {
    debug_assert!(T::MIN_SIG <= x.sig && x.sig <= T::MAX_SIG,
        "encode_normal: significand not normalized");
    // Remove the hidden bit
    let sig_enc = x.sig & !(1 << T::EXPLICIT_SIG_BITS);
    // Adjust the exponent for exponent bias and mantissa shift
    let k_enc = x.k + T::MAX_EXP + T::EXPLICIT_SIG_BITS as i16;
    debug_assert!(k_enc != 0 && k_enc < T::MAX_ENCODED_EXP,
        "encode_normal: exponent out of range");
    // Leave sign bit at 0 ("+"), our numbers are all positive
    let bits = (k_enc as u64) << T::EXPLICIT_SIG_BITS | sig_enc;
    T::from_bits(bits)
}

/// Find the largest floating point number strictly smaller than the argument.
/// Does not handle subnormals, zero, or exponent underflow.
pub fn prev_float<T: RawFloat>(x: T) -> T {
    match x.classify() {
        Infinite => panic!("prev_float: argument is infinite"),
        Nan => panic!("prev_float: argument is NaN"),
        Subnormal => panic!("prev_float: argument is subnormal"),
        Zero => panic!("prev_float: argument is zero"),
        Normal => {
            let Unpacked { sig, k } = x.unpack();
            if sig == T::MIN_SIG {
                encode_normal(Unpacked::new(T::MAX_SIG, k - 1))
            } else {
                encode_normal(Unpacked::new(sig - 1, k))
            }
        }
    }
}

// Find the smallest floating point number strictly larger than the argument.
// This operation is saturating, i.e. next_float(inf) == inf.
// Unlike most code in this module, this function does handle zero, subnormals, and infinities.
// However, like all other code here, it does not deal with NaN and negative numbers.
pub fn next_float<T: RawFloat>(x: T) -> T {
    match x.classify() {
        Nan => panic!("next_float: argument is NaN"),
        Infinite => T::INFINITY,
        // This seems too good to be true, but it works.
        // 0.0 is encoded as the all-zero word. Subnormals are 0x000m...m where m is the mantissa.
        // In particular, the smallest subnormal is 0x0...01 and the largest is 0x000F...F.
        // The smallest normal number is 0x0010...0, so this corner case works as well.
        // If the increment overflows the mantissa, the carry bit increments the exponent as we
        // want, and the mantissa bits become zero. Because of the hidden bit convention, this
        // too is exactly what we want!
        // Finally, f64::MAX + 1 = 7eff...f + 1 = 7ff0...0 = f64::INFINITY.
        Zero | Subnormal | Normal => {
            let bits: u64 = x.transmute();
            T::from_bits(bits + 1)
        }
    }
}


// src/libcore/tests/num/mod.rs
#[test]
fn test_f32f64() {
    use core::f32;

    let max: f64 = f32::MAX.into();
    assert_eq!(max as f32, f32::MAX);
    assert!(max.is_normal());

    let min: f64 = f32::MIN.into();
    assert_eq!(min as f32, f32::MIN);
    assert!(min.is_normal());

    let min_positive: f64 = f32::MIN_POSITIVE.into();
    assert_eq!(min_positive as f32, f32::MIN_POSITIVE);
    assert!(min_positive.is_normal());

    let epsilon: f64 = f32::EPSILON.into();
    assert_eq!(epsilon as f32, f32::EPSILON);
    assert!(epsilon.is_normal());

    let zero: f64 = (0.0f32).into();
    assert_eq!(zero as f32, 0.0f32);
    assert!(zero.is_sign_positive());

    let neg_zero: f64 = (-0.0f32).into();
    assert_eq!(neg_zero as f32, -0.0f32);
    assert!(neg_zero.is_sign_negative());

    let infinity: f64 = f32::INFINITY.into();
    assert_eq!(infinity as f32, f32::INFINITY);
    assert!(infinity.is_infinite());
    assert!(infinity.is_sign_positive());

    let neg_infinity: f64 = f32::NEG_INFINITY.into();
    assert_eq!(neg_infinity as f32, f32::NEG_INFINITY);
    assert!(neg_infinity.is_infinite());
    assert!(neg_infinity.is_sign_negative());

    let nan: f64 = f32::NAN.into();
    assert!(nan.is_nan());
}


// src/libstd/f64.rs
#[test]
fn test_one() {
    let one: f64 = 1.0f64;
    assert_eq!(1.0, one);
    assert!(!one.is_infinite());
    assert!(one.is_finite());
    assert!(one.is_sign_positive());
    assert!(!one.is_sign_negative());
    assert!(!one.is_nan());
    assert!(one.is_normal());
    assert_eq!(Fp::Normal, one.classify());
}

#[test]
fn test_is_normal() {
    use std::f64::*;
    let nan: f64 = NAN;
    let inf: f64 = INFINITY;
    let neg_inf: f64 = NEG_INFINITY;
    let zero: f64 = 0.0f64;
    let neg_zero: f64 = -0.0;
    assert!(!nan.is_normal());
    assert!(!inf.is_normal());
    assert!(!neg_inf.is_normal());
    assert!(!zero.is_normal());
    assert!(!neg_zero.is_normal());
    assert!(1f64.is_normal());
    assert!(1e-307f64.is_normal());
    assert!(!1e-308f64.is_normal());
}

#[test]
fn test_classify() {
    use std::f64::*;
    let nan: f64 = NAN;
    let inf: f64 = INFINITY;
    let neg_inf: f64 = NEG_INFINITY;
    let zero: f64 = 0.0f64;
    let neg_zero: f64 = -0.0;
    assert_eq!(nan.classify(), Fp::Nan);
    assert_eq!(inf.classify(), Fp::Infinite);
    assert_eq!(neg_inf.classify(), Fp::Infinite);
    assert_eq!(zero.classify(), Fp::Zero);
    assert_eq!(neg_zero.classify(), Fp::Zero);
    assert_eq!(1e-307f64.classify(), Fp::Normal);
    assert_eq!(1e-308f64.classify(), Fp::Subnormal);
}

fails on wasm32-unknown-emscripten. I think that this as bug in asm2wasm because the same tests works on asmjs-unknown-emscripten. I used cross to run the tests (it is necessary to create a project and put the code in lib.rs):

cross test --target asmjs-unknown-emscripten # works
cross test --target wasm32-unknown-emscripten # fails

Tests results:

running 8 tests
test next_prev_identity ... FAILED
test ordinary ... FAILED
test prev_float_monotonic ... FAILED
test special_code_paths ... FAILED
test test_classify ... FAILED
test test_f32f64 ... FAILED
test test_is_normal ... FAILED
test test_one ... FAILED

failures:

---- next_prev_identity stdout ----
	thread 'main' panicked at 'prev_float: argument is subnormal', src/lib.rs:101
note: Run with `RUST_BACKTRACE=1` for a backtrace.

---- ordinary stdout ----
	thread 'main' panicked at 'assertion failed: `(left == right)` (left: `Ok(0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002225073858507201)`, right: `Ok(0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002225073858507201)`)', src/lib.rs:42

---- prev_float_monotonic stdout ----
	thread 'main' panicked at 'prev_float: argument is subnormal', src/lib.rs:101

---- special_code_paths stdout ----
	thread 'main' panicked at 'assertion failed: `(left == right)` (left: `Ok(36893488147419100000)`, right: `Ok(36893488147419100000)`)', src/lib.rs:47

---- test_classify stdout ----
	thread 'main' panicked at 'assertion failed: `(left == right)` (left: `Subnormal`, right: `Normal`)', src/lib.rs:227

---- test_f32f64 stdout ----
	thread 'main' panicked at 'assertion failed: max.is_normal()', src/lib.rs:145

---- test_is_normal stdout ----
	thread 'main' panicked at 'assertion failed: 1f64.is_normal()', src/lib.rs:209

---- test_one stdout ----
	thread 'main' panicked at 'assertion failed: one.is_normal()', src/lib.rs:192


failures:
    next_prev_identity
    ordinary
    prev_float_monotonic
    special_code_paths
    test_classify
    test_f32f64
    test_is_normal
    test_one

test result: FAILED. 0 passed; 8 failed; 0 ignored; 0 measured; 0 filtered out

Activity

added
A-testsuiteArea: The testsuite used to check the correctness of rustc
O-wasmTarget: WASM (WebAssembly), http://webassembly.org/
on Jun 23, 2017
pepyakin

pepyakin commented on Jul 23, 2017

@pepyakin
Contributor

Looks like one culprit is classify.

I managed to minimize example:

use std::num::FpCategory as Fp;

fn repr2() -> Fp {
    const EXP_MASK: u64 = 0x7ff0000000000000;

    let man: u64 = 0;
    let exp: u64 = 0x3FF0000000000000;

    match (man, exp) {
        (0, EXP_MASK) => Fp::Infinite,
        (0, 0) => Fp::Zero,
        _ => Fp::Normal,
    }
}

fn main() {
    let class = repr2();
    assert_eq!(class, Fp::Normal);
}

This passes on asmjs-unknown-emscripten but fails on wasm32-unknown-emscripten.

Outputs: https://gist.github.com/pepyakin/89e31cb565662b216a794875e6258da1

pepyakin

pepyakin commented on Jul 23, 2017

@pepyakin
Contributor

I finally figured it out.

Let's consider further minified example:

fn main() {
    let x: u64 = 0;
    match x {
        0x7ff0000000000000 => panic!("1"),
        0x7ff0000000000001 => panic!("2"),
        _ => {}
    };
}

asmjs version of switch looks fine:

switch (i64($0)) {
 case i64_const(0,2146435072):  {
  __ZN3std9panicking15begin_panic_new17hd7ebc2b159ba3f26E(5257,1,3232); //@line 47 "main.rs"
  // unreachable; //@line 47 "main.rs"
  break;
 }
 case i64_const(1,2146435072):  {
  __ZN3std9panicking15begin_panic_new17hd7ebc2b159ba3f26E(5256,1,3248); //@line 48 "main.rs"
  // unreachable; //@line 48 "main.rs"
  break;
 }

Then let's look on wasm output produced for this asmjs (only first arm 0x7ff0000000000000):

   (block $switch-case0
     (block $switch-case
      (br_table $switch-case $switch-case0 $switch-default
       (i32.wrap/i64
        (i64.sub
         (get_local $$0)
         (i64.const 9218868437227405312)
        )
       )
      )
     )
     (block
      (call $__ZN3std9panicking15begin_panic_new17hd7ebc2b159ba3f26E
       (i32.const 5257)
       (i32.const 1)
       (i32.const 3232)
      )
      (br $switch)
     )
    )

If I get it right, i32.wrap/i64 takes i64 and truncates it to i32, leaving only lower 32 bits.
So i32.wrap/i64 (i32.const -9218868437227405312) produces 0.

br_table $switch-case $switch-case0 $switch-default (i32.const 0) is same as
br $switch-case, which means first match arm taken and panic!("1") is executed.

pepyakin

pepyakin commented on Jul 23, 2017

@pepyakin
Contributor

Test ordinary can be minimized to the following example:

fn main() {
    let x64: f64 = 2.2250738585072014e-308;
    let a: String = format!("{:?}", x64);
    let b: String = "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022250738585072014".into();
    assert_eq!(a, b);
}
pepyakin

pepyakin commented on Jul 25, 2017

@pepyakin
Contributor

It seems that test ordinary is fails because of the same issue with match.
Good news: there is fix already here WebAssembly/binaryen#1111 and all tests in this issue passes with it!

malbarbo

malbarbo commented on Jul 25, 2017

@malbarbo
ContributorAuthor

This is great @pepyakin! Thanks for tracking this!

pepyakin

pepyakin commented on Jul 26, 2017

@pepyakin
Contributor

6 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-testsuiteArea: The testsuite used to check the correctness of rustcC-bugCategory: This is a bug.O-wasmTarget: WASM (WebAssembly), http://webassembly.org/

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @alexcrichton@malbarbo@pepyakin@Mark-Simulacrum

        Issue actions

          float tests fails on wasm32-unknown-emscripten · Issue #42630 · rust-lang/rust