wasm_num_float (wasm v0.1.0)

View Source

Floating-point arithmetic over the hybrid representation wasm_num describes.

Erlang cannot hold NaN or Infinity in a float, so every operation is split into a fast path (both operands finite, result finite) and a slow path that handles the values Erlang refuses. The fast path is guarded with is_float, which measured 1.9 ns, faster than the unguarded version because the guard lets the compiler unbox the operands.

Width is the first argument rather than being split across two modules: the specials, comparisons and NaN propagation rules are identical for f32 and f64, and only rounding differs. Duplicating three hundred lines to save one argument would double the surface for the subtle bugs.

Two rules are worth stating because they are where implementations diverge:

  • NaN generation versus propagation. An operation that creates a NaN from non-NaN operands (inf - inf, 0/0, sqrt of a negative) must produce the canonical NaN. An operation that propagates an operand's NaN may keep its payload. The test suite distinguishes these as nan:canonical and nan:arithmetic.
  • f32 rounding via f64 is exact. Computing in double precision and rounding once to single is correctly rounded for add, sub, mul, div and sqrt, because double has more than 2*24+2 bits of significand. That is what makes it safe to reuse Erlang's arithmetic rather than simulating single precision.

Summary

Functions

Magnitude. Clears the sign bit, so it is defined on NaN.

Add. Traps never; every result is a value, including NaN and infinity.

The NaN to produce when an operation has to make one.

Round toward positive infinity.

A signed 32-bit integer as a float. Total: every i32 is representable.

An unsigned 32-bit integer as a float. Total.

A signed 64-bit integer as a float, rounding when it does not fit exactly.

An unsigned 64-bit integer as a float, rounding when it does not fit exactly.

The magnitude of the first with the sign of the second. Defined on NaN and on both zeros.

f64 to f32, rounding to nearest and possibly to infinity.

Divide. Division by zero is infinity or NaN, not a trap: only the integer forms trap.

Equality, as 1 or 0. NaN compares equal to nothing, itself included.

Round toward negative infinity.

Greater or equal, as 1 or 0. False when either side is NaN.

Greater than, as 1 or 0. False when either side is NaN.

Whether the sign bit is set, which is true of -0.0 and of a negative NaN.

Less or equal, as 1 or 0. False when either side is NaN.

Less than, as 1 or 0. False when either side is NaN.

The larger, with the same NaN and signed-zero rules as min/3.

The smaller. NaN wins over any value, and -0.0 wins over +0.0, which is not what erlang:min/2 does.

Multiply.

Inequality, as 1 or 0. True when either side is NaN.

Round to nearest, ties to even. Not erlang:round/1, which rounds halves away from zero.

Flip the sign bit. Not 0 - X, which would lose the sign of zero.

f32 to f64. Exact, and the NaN payload is carried across.

Round a double to the target width, mapping overflow to infinity.

Square root. Negative input is NaN rather than an error.

Subtract.

Round toward zero. Still a float: the integer forms are in wasm_num_trunc.

Types

f()

-type f() :: float() | fspecial().

fspecial()

-type fspecial() :: infinity | neg_infinity | {nan, Sign :: 0 | 1, Payload :: non_neg_integer()}.

width()

-type width() :: 32 | 64.

Functions

abs/2

-spec abs(width(), f()) -> f().

Magnitude. Clears the sign bit, so it is defined on NaN.

add/3

-spec add(width(), f(), f()) -> f().

Add. Traps never; every result is a value, including NaN and infinity.

canonical_nan/1

-spec canonical_nan(width()) -> f().

The NaN to produce when an operation has to make one.

ceil/2

-spec ceil(width(), f()) -> f().

Round toward positive infinity.

convert_i32_s(W, V)

-spec convert_i32_s(width(), integer()) -> f().

A signed 32-bit integer as a float. Total: every i32 is representable.

convert_i32_u(W, V)

-spec convert_i32_u(width(), integer()) -> f().

An unsigned 32-bit integer as a float. Total.

convert_i64_s/2

-spec convert_i64_s(width(), integer()) -> f().

A signed 64-bit integer as a float, rounding when it does not fit exactly.

convert_i64_u/2

-spec convert_i64_u(width(), integer()) -> f().

An unsigned 64-bit integer as a float, rounding when it does not fit exactly.

copysign(W, A, B)

-spec copysign(width(), f(), f()) -> f().

The magnitude of the first with the sign of the second. Defined on NaN and on both zeros.

demote/1

-spec demote(f()) -> f().

f64 to f32, rounding to nearest and possibly to infinity.

divide/3

-spec divide(width(), f(), f()) -> f().

Divide. Division by zero is infinity or NaN, not a trap: only the integer forms trap.

eq(W, A, B)

-spec eq(width(), f(), f()) -> 0 | 1.

Equality, as 1 or 0. NaN compares equal to nothing, itself included.

floor/2

-spec floor(width(), f()) -> f().

Round toward negative infinity.

ge(W, A, B)

-spec ge(width(), f(), f()) -> 0 | 1.

Greater or equal, as 1 or 0. False when either side is NaN.

gt(W, A, B)

-spec gt(width(), f(), f()) -> 0 | 1.

Greater than, as 1 or 0. False when either side is NaN.

is_negative/1

-spec is_negative(f()) -> boolean().

Whether the sign bit is set, which is true of -0.0 and of a negative NaN.

le(W, A, B)

-spec le(width(), f(), f()) -> 0 | 1.

Less or equal, as 1 or 0. False when either side is NaN.

lt(W, A, B)

-spec lt(width(), f(), f()) -> 0 | 1.

Less than, as 1 or 0. False when either side is NaN.

max(W, A, B)

-spec max(width(), f(), f()) -> f().

The larger, with the same NaN and signed-zero rules as min/3.

min(W, A, B)

-spec min(width(), f(), f()) -> f().

The smaller. NaN wins over any value, and -0.0 wins over +0.0, which is not what erlang:min/2 does.

mul/3

-spec mul(width(), f(), f()) -> f().

Multiply.

ne(W, A, B)

-spec ne(width(), f(), f()) -> 0 | 1.

Inequality, as 1 or 0. True when either side is NaN.

nearest/2

-spec nearest(width(), f()) -> f().

Round to nearest, ties to even. Not erlang:round/1, which rounds halves away from zero.

neg/2

-spec neg(width(), f()) -> f().

Flip the sign bit. Not 0 - X, which would lose the sign of zero.

promote/1

-spec promote(f()) -> f().

f32 to f64. Exact, and the NaN payload is carried across.

round_to/2

-spec round_to(width(), f()) -> f().

Round a double to the target width, mapping overflow to infinity.

Erlang's float-to-binary conversion saturates to the infinity bit pattern on overflow, and wasm_num:f32_from_bits/1 turns that pattern back into the infinity atom, so overflow needs no explicit range test.

sqrt/2

-spec sqrt(width(), f()) -> f().

Square root. Negative input is NaN rather than an error.

sub/3

-spec sub(width(), f(), f()) -> f().

Subtract.

trunc/2

-spec trunc(width(), f()) -> f().

Round toward zero. Still a float: the integer forms are in wasm_num_trunc.