Thunder and memory

Copyright © 2024 J. M. Spivey
Jump to navigation Jump to search

Here's an example of a Thunder function that uses floating point and access to arrays and variables in memory. We will use Thunder to replicate a function sum that sums a global array of floats:

static float a[] = { 3.0, 1.0, 4.0, 1.0, 5.0, 9.0 };

void sum(int n, float *y) {
     int i = 0;
     float s = 0.0;

     while (i < n) {
          s = s + a[i];
          i = i+1;
     }

     *y = s;
}

That's not quite idiomatic C, because the whole loop body could be written s += a[i++];, but let's leave it as it is.

To begin creating the same function with Thunder, we should for sanity's sake define a type for a pointer to a function like sum.

typedef void (*sumptr)(int, float *);

We declare a compiling function that returns this type, create a couple of labels, and name a few registers.

sumptr compile3(void) {
     code_addr entry;
     vmlabel lab1 = vm_newlab(), lab2 = vm_newlab();
     vmreg n = ireg[0], i = ireg[1], t = ireg[2], y = ireg[3];
     vmreg s = freg[0], x = freg[1];

Any implementation of Thunder ought to have enough registers for this. Now here comes the beginning of the function. It has two 'integer' arguments, that is to say, one genuine integer and a pointer that can live in an integer register. We fetch them into the registers n and y.

     entry = vm_begin("sum", 2);
     vm_gen(GETARG, n, 0);
     vm_gen(GETARG, y, 1);

We continue with the function body, coding the loop with branches. As you can see, there's a special instruction for zeroing a floating point register.

     vm_gen(MOV, i, 0);             // i = 0
     vm_gen(ZEROF, s);              // s = 0.0
     vm_label(lab1);
     vm_gen(BGEQ, i, n, lab2);      // if (i >= n) goto lab2

At this point we need the contents of a[i]. For this, we first compute the offset t = 4*i, then load from the address a + t, using an LDW instruction that specifies a destination (in this case a floating-point register), a source register, and an offset. The source register and offset are added together to form a memory address, and the word at that address is loaded into the destination register.

     vm_gen(LSH, t, i, 2);          // t = i << 2
     vm_gen(LDW, x, t, vm_addr(a)); // x = a[i]

Even if the host machine has a scaled addressing mode, we can't use it here, and we are forced to accept the 'least common denominator'. To complete the loop, we add x to the total s, increment i, and branch back the the top.

     vm_gen(ADDF, s, s, x);         // s = s + x
     vm_gen(ADD, i, i, 1);          // i = i + 1
     vm_gen(JUMP, lab1);            // goto lab1

When the loop is finished, we must store the total into the location pointed to by y. For this, we use a two-register form of the STW instruction, with STW s, y equivalent to STW s, y, 0. Then we can return, with no result, so no useful value in the ret register.

     vm_label(lab2);
     vm_gen(STW, s, y);             // *y = s
     vm_gen(RET);

You can also write vm_gen(LDW, x, a) for vm_gen(LDW, x, zero, a), and so on, where zero names a fictitious register that always contains zero. Though no such register may exist on the host machine, Thunder will use addressing modes of the host to get the same effect. The zero register should not be used for arithmetic.

The compiling function ends:

     vm_end();
     return (sumptr) entry;
}