Skip to content

Arithmetic and Comparisons

WebAssembly provides a rich set of arithmetic and comparison instructions. Each instruction is typed — you always know whether you are working with integers or floats, and for integers, whether the operation is signed or unsigned.

The four basic operations for i32 are i32.add, i32.sub, i32.mul, and division. All pop two operands and push one result:

(module
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add)
(func (export "sub") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.sub)
(func (export "mul") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.mul))

The same instructions exist for i64: i64.add, i64.sub, i64.mul.

Division is where integer types reveal their complexity. An i32 is just 32 bits — the bits themselves carry no sign information. The instruction decides whether to interpret those bits as a two’s-complement signed integer or as an unsigned integer:

(module
;; signed: -7 / 2 = -3 (rounds toward zero)
(func (export "divSigned") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.div_s)
;; unsigned: the same bit pattern as -7 (0xFFFFFFF9) / 2 = 2147483644
(func (export "divUnsigned") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.div_u))

Similarly i32.rem_s and i32.rem_u give signed and unsigned remainder.

Float instructions mirror integer ones, but the type prefix changes:

(module
(func (export "fadd") (param $a f64) (param $b f64) (result f64)
local.get $a
local.get $b
f64.add))

f64.sub, f64.mul, f64.div, f64.sqrt, f64.floor, f64.ceil, f64.min, and f64.max are all available. The same set exists for f32.

Comparisons pop two values and push an i32 result: 1 for true, 0 for false. There is no boolean type — WebAssembly uses i32 as a boolean whenever it needs one.

(module
;; returns 1 if $a equals $b, else 0
(func (export "eq") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.eq)
;; signed less-than: returns 1 if $a < $b (signed), else 0
(func (export "ltSigned") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.lt_s))

Comparison variants follow the same signed/unsigned pattern: i32.lt_s vs i32.lt_u, i32.gt_s vs i32.gt_u, i32.le_s, i32.ge_u, and so on.

The example below compiles a module with subtraction, signed division, and a signed less-than comparison.

WebAssembly
OptionBenefitCost
Writing WAT by handYou see operand order and opcodes directly, so it’s clear exactly how i32.div_s differs from i32.div_uYou have to track the stack yourself on every line — easy to get wrong as the code grows
Letting a compiler generate Wasm (e.g. from Rust/C)Write normal high-level code and get optimized output, well suited to real workYou can’t see where the compiler chose the _s or _u variant, harder to debug if the result is wrong
  • Pushing operands in the wrong order — e.g. intending a - b but pushing $b before $a, so i32.sub computes b - a instead, because WAT is stack-based and push order directly affects the result
  • Mixing up div_s/div_u or lt_s/lt_u — since i32 carries no sign of its own, picking the wrong instruction silently produces a wrong result with no error
  • Forgetting that whatever is left on the stack at the end of a function must match the declared result exactly — too much or too little left over fails validation

💡 Real-world example

The official WebAssembly spec testsuite — mirrored for convenience by tools like WABT — contains many hand-written .wat files that verify opcodes like div_s/div_u behave exactly per spec, including edge cases like division by zero.

What does `i32.lt_s` return when the comparison is true?
What is the difference between `i32.div_s` and `i32.div_u`?
Which instruction multiplies two i32 values?
Wasm comparisons like `i32.eq` push what type onto the stack?