Web Assembly, WAT and the sin & cos functions

We all remember those days in school where the maths teacher would try to show us how great trigonometry was, drawing lots of triangles, working out angles and writing all those confusing equations; sin, cos, tan and so on, all very puzzling. But over the years, as we begin to understand it all, more or less, we are quite happy to press those sin and cos buttons on our calculator, without giving much thought about what is going on behind the scenes.

If you are a software developer then you would have come across some maths library with a whole host of functions, including those sin and cos ones. Somewhere at some point those functions are performed; some type of algorithm must be used; some process must be followed to calculate the actual values. But who cares, there is a library that does everything for you, right? Yes, however...

In Web Assembly there are some maths functions, add, subtract, multiply and so on, even a square root function, but for some reason, most of the normal maths related functions you find in a standard maths library are not included. That means no sin or cos functions either. So I decided to create my own; how hard can it be; must be simple surely, right? Well, yes and no.

First the Maths

The first place I went looking was Wikipedia: Sine and cosine page. There is more to these functions than I would ever want to know. Still, I did find two important equations that helped to explain how it is all worked out.

The “x” angle values are in radians, not degrees, if that helps. All simple stuff right? No, me either to start with. I was hoping for software algorithms not mathematical formulae. However, if we remember our days learning maths, we can slowly work our way through this and create the functions ourselves. To start with, we will look at the sin function.

Looking at the diagram above, we need to do the following steps, which I have written in a software-like way, to help you understand it better.

  sin(x) =
    x
    - (pow(x, 3) / factorial(3))
    + (pow(x, 5) / factorial(5))
    - (pow(x, 7) / factorial(7))
    + (and so on, in odd numbers, switching from minusing to adding as you go)  

But how long should we continue calculating? It looks like the more levels you use the greater the precision. But we have limits. A 64 bit floating point number can only be so precise and continuing too long would be a waste of time. So what is the magic number? I couldn't even begin to know, but from other examples I have found, written in C, they limit themselves to using only 9 terms, so that is what we will use too.

First Attempt

I will be showing you some code examples, all written in WAT, which can look a bit scary, but I have created the same functions in JavaScript to help you better understand what is going on.

(func $get_sin (param $x f64) (result f64)
  ;; Set locals
  (local $result f64)
  (local $term i32)
  (local $termResult f64)
  (local $term2 i32)
  (local $term2p1 i32)
  (local $fractorial f64)

This is the start of the get_sin function. It has an $x f64 parameter (in radians) and returns a f64 result. Below the function declaration is a list of local variables.

;; Set $result to the starting $x value
local.get $x
local.set $result

The next step is to set the $result to the starting $x value. This is the starting amount set out in the sin formula we looked at before. The next process is to loop between $term 1 to 9.

;; Set $term to 1
i32.const 1
local.set $term

;; Loop for each $term (1 to 9)
loop $for_each_term
  
    ... do $term related calculations

    ;; Increase the $term
    local.get $term
    i32.const 1
    i32.add
    local.set $term

    ;; We need to check the $term is less than or equal to 9
    local.get $term
    i32.const 9
    i32.le_u

    ;; If so then continue on with the next $term
    br_if $for_each_term
end

The looping parts increase the $term value from 1 to 9 and perform the required calculations for each one. For each $term we need to do some maths.

;; Workout $term * 2 (we can shift left to do this)
local.get $term
i32.const 1
i32.shl
local.set $term2

;; Workout ($term * 2) + 1
local.get $term2
i32.const 1
i32.add
local.set $term2p1

;; Workout pow($x, $term2p1)
local.get $x
local.get $term2p1
call $math_pow
local.set $termResult

;; Get the $fractorial for $term2p1
local.get $term2p1
call $get_fractorial
local.set $fractorial

;; Workout $termResult /= $fractorial
local.get $termResult
local.get $fractorial
f64.div
local.set $termResult  

The inner loop calculation is used to find a term's result value. You can look at this in the following way.

$term2p1 = ($term * 2) + 1;
$termResult = pos($x, $term2p1) / factorial($term2p1);

The $term value is 1, 2, 3, and so on, but we need to process it in 3!, 5!, 7!, order. So for $term 1, we workout $term2p1 to be (1 * 2) + 1 = 3, and then use that to work out the term's result.

;; Workout if $term is odd
local.get $term
i32.const 1
i32.and

;; If odd
if
    ;; Adjust the $result by removing the $termResult
    local.get $result
    local.get $termResult
    f64.sub
    local.set $result
else
    ;; Adjust the $result by adding the $termResult
    local.get $result
    local.get $termResult
    f64.add
    local.set $result
end

We now need to work out if the term is odd or even. This is so we know if we need to minus the term result from the current sin result or add to it. If you look at the equation again you will see that it switches between adding and minusing to the final result.

That is all there is to it. With the cos function everything is similar, except you start off with the value 1.0, not the $x parameter, and you move along term by term in even numbers.

So that is it, all done. Well yes, but sadly no. After creating some testing software, comparing my sin and cos functions to JavaScripts Math versions, I found out that the numbers do match, to start with, but when the degrees get closer to 360 and beyond, the results start to drift. It seemed my journey was not yet over.

Adjustments Needed

Looking over the Wikipedia page again it looks like I needed to do two more things. The first thing was to find the modulo of the given angle, so that it is between 0 and 360 degrees (nothing above or below that), and the second thing was something to do with quadrants.

Going back to those school days again, you'll hopefully remember that there are only 360 degrees in a circle. If you have more than that, then you have just gone around the circle once and you can just use another, smaller, angle to represent the same thing. For example, the angle 370 degrees is the same as 10 degrees. Also, if you go backwards around the circle, you can still represent that angle as a positive amount. For example, -30 degrees is the same as 330 degrees.

The next thing we can do is split the circle into 4 parts. In each quadrant we perform different sin or cos functions to get the result we want. In a way we are only using the sin and cos functions between 0 and 90 degrees, which helps to keep our calculations accurate. Below is a list of what we do within each quadrant.

sin

Quadrant (in degrees) Calculation (in degrees)
0 to 90 sin($x)
90 to 180 cos($x - 90)
180 to 270 -sin($x - 180)
270 to 360 -cos($x - 270)

cos

Quadrant (in degrees) Calculation (in degrees)
0 to 90 cos($x)
90 to 180 -sin($x - 90)
180 to 270 -cos($x - 180)
270 to 360 sin($x - 270)

Final Version

Taking everything we have learnt so far, we can now create our final version of the sin and cos functions. Let us take a look at the whole function first and then go through some of it in detail.

(func $math_sin (param $x f64) (result f64)
  ;; Set local variables
  (local $modRadian f64)

  ;; Mod the radian angle (0 to 360 degrees)
  local.get $x
  call $get_radian_mod
  local.set $modRadian

  ;; Check angle is less than or equal to 90 degrees
  local.get $modRadian
  f64.const 1.57079632679489661923
  f64.le

  ;; If it is
  if
      ;; Call the sin function as it is
      local.get $modRadian
      call $get_sin

      ;; The result is on the stack which is this functions return result too
      return
  end

  ;; Check angle is less than or equal to 180 degrees
  local.get $modRadian
  f64.const 3.14159265358979323846
  f64.le

  ;; If it is
  if
      ;; Set angle
      local.get $modRadian
      f64.const 1.57079632679489661923
      f64.sub

      ;; Call the cos function
      call $get_cos

      ;; The result is on the stack which is this functions return result too
      return
  end

  ;; Check angle is less than or equal to 270 degrees
  local.get $modRadian
  f64.const 4.71238898038468985769
  f64.le

  ;; If it is
  if
      ;; Set angle
      local.get $modRadian
      f64.const 3.14159265358979323846
      f64.sub

      ;; Call the sin function
      call $get_sin

      ;; Negate the result
      f64.neg

      ;; The result is on the stack which is this functions return result too
      return
  end

  ;; Otherwise the angle is less than 360 degrees

  ;; Set angle
  local.get $modRadian
  f64.const 4.71238898038468985769
  f64.sub

  ;; Call the cos function
  call $get_cos

  ;; Negate the result
  f64.neg

  ;; The result is on the stack which is this functions return result too
)

We are calling the function math_sin because it will be the function you will want to call. We still have the get_sin function, we are not replacing it, because we still need to call it as it is.

The first step is to get the modulo of the angle, so that the given value is between 0 and 360 degrees. Now we check to see which quadrant the angle is in, adjust the angle to use, and then call the sin or cos function as required.

Both the sin and cos functions look similar. You can take a look for yourself; it is all on GitHub

So there we have it. All the tests seem to work correctly. We have our own WAT sin and cos maths functions. A fun journey indeed.