math and debug: Mathematical utilities and diagnostics

Use mathematical functions with range checking and diagnostic output.

Wave Foundation

Integer function to check range

std::math::int
min_i32(a: i32, b: i32) -> i32
max_i32(a: i32, b: i32) -> i32
abs_i32_checked(x: i32) -> MathResult<i32>
clamp_i32_checked(x: i32, lo: i32, hi: i32) -> MathResult<i32>
div_floor_i32_checked(a: i32, b: i32) -> MathResult<i32>
div_ceil_i32_checked(a: i32, b: i32) -> MathResult<i32>

MathResult<T> contains value and error. Use value only after checking error == MATH_ERROR_NONE; import the error constants from std::math::result. The absolute value of the minimum signed integer cannot be represented in the same type, so the checked absolute-value function reports failure. clamp rejects lo greater than hi. Division checks for a zero divisor and range overflow. floor and ceil round differently from the language’s integer division, which truncates toward zero.

Classifying floating-point values

is_nan_f64, is_infinite_f64, and is_finite_f64 in std::math::float distinguish special values. The f32 function is also provided. float_to_bits_f64(value: f64) -> u64 is a function to obtain storage bits and is different from the numeric conversion of value as u64. NaN is not even equal to itself, so it is not checked against value == nan.

diagnosis

debug_assert(condition: bool, message: str) in std::debug::core terminates after diagnosing a false condition. Situations that may normally fail, such as user input, are handled with the result value, and assert is used when checking internal program conditions that must be met.

Save the program below as main.wave and run it.

import("std::math::int")::{
    min_i32, max_i32
};
import("std::debug::core")::{
    debug_assert
};

fun main() {
    var low: i32 = min_i32(9, 4);
    var high: i32 = max_i32(9, 4);
    debug_assert(low <= high, "invalid range");
    println("{} {}", low, high);
}

Execution result:

4 9

Rounding direction for negative division

Compare how to divide -7 by 3. / is truncated toward 0 to become -2. floor selects the smaller integer -3, and ceil selects the larger integer -2.

import("std::math::int")::{
    div_floor_i32_checked,
    div_ceil_i32_checked,
    abs_i32_checked
};
import("std::math::result")::{
    MathResult,
    MATH_ERROR_NONE,
    MATH_ERROR_OVERFLOW
};

fun main() -> i32 {
    var floor: MathResult<i32> = div_floor_i32_checked(-7, 3);
    var ceil: MathResult<i32> = div_ceil_i32_checked(-7, 3);

    if (floor.error != MATH_ERROR_NONE || ceil.error != MATH_ERROR_NONE) {
        return 1;
    }

    println("truncate={} floor={} ceil={}", -7 / 3, floor.value, ceil.value);

    var absolute: MathResult<i32> = abs_i32_checked(-2147483648);

    if (absolute.error == MATH_ERROR_OVERFLOW) {
        println("absolute value is outside i32");
    }

    return 0;
}

Execution result:

truncate=-2 floor=-3 ceil=-2
absolute value is outside i32

If you only check the result value, you cannot distinguish between the substitute value included in case of failure and the actual calculation result. Follow the order of checking error first. floor is useful when putting negative coordinates into an interval of a certain size, and ceil is useful when rounding up the required number of bundles.

What errors should I handle?

situation error Processing example
Divide by zero MATH_ERROR_DIVIDE_BY_ZERO Takes denominator input again
Result cannot be stored in type MATH_ERROR_OVERFLOW Calculate with wider type or reject input
The minimum is greater than the maximum in clamp MATH_ERROR_INVALID_ARGUMENT Modify setting range

assert is not an error repair tool. Errors in user input are handled using conditional statements and return values, and after completing the calculation, the internal conditions that must be met are checked with debug_assert.