Numeric operations
Describes integer wrap, checked operations, shift, type conversion errors, and floating point results.
Wave Foundation
Integer representation
An N-bit integer has N value bits. Unsigned integers range from 0 to 2^N − 1; signed integers range from −2^(N−1) to 2^(N−1) − 1. The signedness of an operation determines how the bit pattern is interpreted.
The tables below describe operation results. The IR code example shows the current printer representation.
Addition, subtraction, and multiplication
Basic integer add, sub, and mul keep the low N bits of the result. Overflow does not trap. Checked arithmetic returns the same wrapped result together with a Bool indicating whether the mathematical result exceeded the signed or unsigned range of the operation.
| Operation | Wrapped result | Checked overflow |
|---|---|---|
| u8: 255 + 1 | 0 | true |
| i8: 127 + 1 | −128 | true |
| u8: 0 − 1 | 255 | true |
| i8: 12 × 3 | 36 | false |
A frontend that requires overflow to terminate execution must use checked arithmetic and an explicit trap_if on the overflow result. Basic arithmetic does not inherit the source language's overflow policy implicitly.
Wrapping and explicitly checked IR
This module was constructed with the Rust builder and accepted by the verifier. It is current printer output; a text parser and execution backend are not yet available.
module {
format_version 2
semantics_version 1
target "x86_64-whale-linux"
datalayout { ptr=64, endian=little }
declare @f0 "add_u8": whale () -> u8, linkage internal
declare @f1 "require_no_overflow": whale () -> u8, linkage internal
fn @add_u8() -> u8, id @f0 {
entry:
%v0: u8 = const u8 255
%v1: u8 = const u8 1
%v2: u8 = add u8 %v0, %v1
ret u8 %v2
}
fn @require_no_overflow() -> u8, id @f1 {
entry:
%v3: u8 = const u8 255
%v4: u8 = const u8 1
%v5: tuple<u8, bool> = uadd_chk u8 %v3, %v4
%v6: u8 = extract %v5, 0
%v7: bool = extract %v5, 1
trap_if bool %v7, reason="integer overflow"
ret u8 %v6
}
}
Under the arithmetic contract, add_u8 returns 0 because 256 wraps to eight bits. require_no_overflow separates the wrapped result (extract ..., 0) from the Bool overflow flag (extract ..., 1). Its explicit trap_if stops execution before the return when that flag is true. These are specified execution results, not results from an implemented interpreter.
Division and remainder
Integer division and remainder by zero trap. Signed division of the minimum representable value by −1 produces that minimum value by wrapping. The corresponding remainder is zero.
| Operation | Result |
|---|---|
| i8: −128 / −1 | −128 |
| i8: −128 % −1 | 0 |
| Integer division by 0 | trap |
| Integer remainder by 0 | trap |
Shifts
For an N-bit value, interpret the shift-count bit pattern as unsigned and reduce it modulo N. A count outside the range 0 through N−1 does not itself trap.
For an 8-bit value, counts 0, 8, and 16 all select a shift of zero. An 8-bit count with bit pattern 11111111 selects a shift of 7, including when that pattern represents signed −1. This unsigned interpretation occurs before the modulo operation.
A source language that rejects excessive or negative counts must express that policy with explicit checks before the shift.
Conversions
| Conversion | Meaning |
|---|---|
| Zero extension | Increase width by adding zero high bits |
| Sign extension | Increase width by replicating the sign bit |
| Bit truncation | Retain the low bits at the destination width |
| Bit reinterpretation | Interpret the same bits using another type |
| Lossless numerical conversion | Preserve the numerical value; trap when it cannot be represented |
For example, extending the bit pattern 11111111 from 8 to 16 bits by zero extension produces 0000000011111111. Sign extension produces 1111111111111111. These are different operations even when the source bits are identical.
Float-to-integer conversion truncates toward zero, then checks the integer range. NaN and infinity trap. For conversion to i8, 127.9 produces 127, while 128.0 traps. Bool converts to integer 0 or 1, except that signed i1 is not a permitted destination for this conversion because it cannot represent 1.
Converting an address to an integer does not preserve a right to recover pointer access permissions from that integer. See pointer validity.
Floating-point arithmetic
Floating-point values have exact f16, f32, or f64 bit patterns. Arithmetic rounds at the declared width using round-to-nearest, ties-to-even: a halfway result selects the representable value with an even least-significant significand bit.
Default arithmetic does not permit fast math, implicit fused multiply-add, or flushing small values to zero. A multiply followed by an add retains its separate rounding steps; the backend must not combine them implicitly.
Numerical operations may produce NaN or infinity. NaNs produced by numerical operations or width conversions are normalized to one fixed positive quiet NaN per width. Storage and copying instead preserve the original NaN bits. This distinction matters when moving a NaN payload through memory without performing arithmetic on it.
Floating-point status flags are not exposed. Float-to-integer conversion has the trapping rules above even though default floating-point arithmetic permits NaN and infinity results.
Exact constant storage
Use FloatBits variants or parse an exact-width hexadecimal bit string. Storage equality compares bits, including signed zero and NaN payloads. The verifier rejects a payload width that differs from its IR type.
use ir::{FloatBits, ModuleBuilder, Target, Type};
fn main() {
let bits = FloatBits::parse(32, "0xffc01234").unwrap();
assert_eq!(bits, FloatBits::F32(0xffc01234));
let target = Target::X86_64WhaleLinux;
let mut module = ModuleBuilder::new(target.name(), target.data_layout());
let mut function = module.begin_function("payload", vec![], Type::F32);
let value = function.const_float_bits(Type::F32, bits);
function.ret(Some(value));
function.finish();
let module = module.finish();
ir::verify_module(&module).unwrap();
assert!(ir::print_module(&module).contains("const f32 0xffc01234"));
println!("{}", bits);
}
0xffc01234
f16/f32/f64 strings have exactly 4/8/16 hex digits after 0x. 0x80000000 is f32 negative zero; 0x7f800000 is positive infinity. const_float is a numeric convenience conversion from host f64; use const_float_bits to preserve original storage. Bit-exact storage does not complete the floating arithmetic backend: compile-time arithmetic still uses host f64 intermediates, so the complete declared-width rounding contract remains unfinished.