Functions

In the introduction to SICO, we showed how to perform several higher level operations, such as comparing values. Even though we can perform these useful operations, we may need to write out dozens of SICO instructions to do so. Rewriting these instructions every time we need them is going to be both messy and prone to errors.

In this section, we will solve this problem by creating a calling convention for functions. These functions will allow us to reuse other instructions with very little effort on our part, and they can be implemented within SICO. That is, we won't need to introduce any new assembly notation.

Calling Convention

We will continue to use the shorthand [x] to represent mem[x], or the value at memory address x. Now, assume that the label func is the function we want to call, and address 0 always holds the value 0 when we begin. Then the function calling convention we will use is:

0 ? func arg0 arg1 ...

When the SICO instruction 0 ? func is executed, it will perform the operation [0]=[0]-[?]. Since ? refers to the current address in memory, we have the equality [?]=?. Also, by our assumptions, [0]=0. Thus, we have [0]=0-?. That is, we store the negation of the current address in memory address 0. And since the value 0 will always be less than or equal to any other value, we will always jump to func.

Now that our instruction pointer has jumped to func, we will want to parse the arguments that we are passing: arg0 arg1 ... etc. Since we know that address 0 holds the negation of the calling address, and that the arguments are listed immediately after the calling address, we can retrieve arg0 at -[0]+2, arg1 at -[0]+3, and so on.

0 ? func arg0 arg1 ... -[0] -[0]+1 -[0]+2 -[0]+3 ...

When the function is done, it will need jump to the end of the original function call to resume program flow. So if the function has two arguments like in the example above, it will need to jump to -[0]+4 when it's done. We'll also want to make sure that [0]=0 upon returning so that any subsequent function calls can assume [0]=0.

First Function: Adding Numbers

SICO's single instruction nature means that it lacks even the common mathematical operations that most languages have by default. Large SICO programs will need functions like multiplication and division in order to be easy to read and write. So, for our first function, it will pay off immensely to show how to create a function that can add numbers.

Assume our function takes addresses r, a, and b as inputs and we want to compute [r]=[a]+[b]. To do this in plain SICO instructions, we would need to write 5 instructions every time we wanted to perform this operation:

tmp tmp ?+1 tmp a ?+1 tmp b ?+1 # [tmp]=-[a]-[b] r r ?+1 r tmp ?+1 # [r]=[a]+[b]

After writing our function, called addnum, we will be able to shrink this down to a single line:

0 ? addnum r a b

Before we even write addnum, we can write a program to show how we intend to use it.

0 0 main addnum: # ... main: # Set [c]=[a]+[b]. 0 ? addnum .c .a .b # Add 48 to convert the numbers to ASCII digits. 0 ? addnum .a .a .asc0 0 ? addnum .b .b .asc0 0 ? addnum .c .c .asc0 # Print "[a]+[b]=[c]". 0-2 .a ?+1 0-2 .plus ?+1 0-2 .b ?+1 0-2 .equal ?+1 0-2 .c ?+1 0-2 .eol ?+1 # End the program. 0-1 0 0 # Constants. .a:3 .b:5 .c:0 .asc0:48 .plus:43 .equal:61 .eol:10

The program makes use of the sublabels .a, .b, etc under main. Whereas normal labels need to be unique across the entire SICO program, sublabels only need to be unique for the label they're defined under. This helps out with larger programs where we expect to reuse common label names.

The program so far is very simple. The very first line, 0 0 main, mostly serves to set [0] to 0 and jump to the main entry point of our program. In main, we use addnum to set [c]=[a]+[b] and then add the value 48 to our digits. Since the ASCII range for the characters "0" to "9" is 48 to 57, we need to add 48 to our numbers to convert them to printable digits. After that shift, we can print our digits and end the program.

Here is the outline of what we want addnum to do. All that we need to do is fill it in.

addnum: # Get r. # Get a. # Get b. # Set the return address. # Set [r]=[a]+[b]. # Return and set [0]=0. # Constants.

In our outline, we deliberately specify getting the addresses r, a, and b instead of their values. This is because we won't need their values until the #Set [r]=[a]+[b]. step.

Before we cover the rest of the function, we'll first define the constants the function will use. There's only a few of them, but they're used throughout the function. Covering them first will help with understanding the other sections.

# Constants. .z:0 1 2 .tmp:0 .val:0

z is used to intuitively access constant values around 0. For example, we can access the value 0 with z, or the value 2 with z+2. tmp and val are temporary values we'll need to calculate [r]=[a]+[b] and store intermediate values.

To get the return address, r, we'll need to perform 3 dereferences.

# Get r. 0 .z+2 ?+1 .r0 .r0 ?+1 .r0 0 ?+1 .tmp .tmp ?+1 .tmp .r0:0 ?+1 # [tmp]=-r .r1 .r1 ?+1 .r1 .tmp ?+1 # [r1]=r .r2 .r2 ?+1 .r2 .tmp ?+1 # [r2]=r

At this point, value -[0] is pointing to the calling instruction (0 ? addnum r a b), and the first argument is at -([0]-2). So, our first dereference requires decrementing [0] by 2 and overwriting another instruction by setting [r0]=-[0]. This operation dynamically changes where we read our stack from, and allows us read r. Specifically, we set [tmp]=-r. [tmp] is then used to set r1 and r2, which will later be used to read and overwrite [r] respectively. These are the second and third dereferences.

To get a we decrement [0] by 1 and perform another 2 dereferences. This time we only have 1 destination address to write to.

# Get a. 0 .z+1 ?+1 .a0 .a0 ?+1 .a0 0 ?+1 .tmp .tmp ?+1 .tmp .a0:0 ?+1 # [tmp]=-a .a1 .a1 ?+1 .a1 .tmp ?+1 # [a1]=a

To get b we decrement [0] and perform another 2 dereferences.

# Get b. 0 .z+1 ?+1 .b0 .b0 ?+1 .b0 0 ?+1 .tmp .tmp ?+1 .tmp .b0:0 ?+1 # [tmp]=-b .b1 .b1 ?+1 .b1 .tmp ?+1 # [b1]=b

Getting the return address is much simpler than getting the arguments.

# Set the return address. 0 .z+1 ?+1 .ret .ret ?+1 .ret 0 ?+1

After we decrement [0], it will be pointing to the address after the calling instruction. We then use it overwrite [ret], the jump operand of the last instruction of our function.

At his point we are all set to perform what the function originally set out to do: add numbers.

#Set [r]=[a]+[b]. .tmp .tmp ?+1 .tmp .r1:0 ?+1 # [tmp]=-[r] .val .val ?+1 .val .tmp ?+1 # [val]=[r] .val .a1:0 ?+1 # [val]=[r]-[a] .val .b1:0 ?+1 # [val]=[r]-[a]-[b] .r2:0 .val ?+1 # [r]=[a]+[b]

The block above looks a bit like the 5 instruction example we gave at the start of this section, and that's because it is effectively the same. The only difference is instead of zeroing out r with r r ?+1, we zero it out by a double negation starting at r1. This technique uses the same number of instructions as a regular zero-out, is more cache friendly, and allows us to write both negative and positive values to the return address.

After setting [r], we have the last instruction:

# Return and set [0]=0. 0 0 .ret:0

As the comment says, our last instruction simply sets [0] to 0 for future functions to use and returns to the end of the calling instruction.

The full program is given below.

0 0 main addnum: # Call : 0 ? addnum r a b # Effect: Set [r]=[a]+[b]. # Get r. 0 .z+2 ?+1 .r0 .r0 ?+1 .r0 0 ?+1 .tmp .tmp ?+1 .tmp .r0:0 ?+1 # [tmp]=-r .r1 .r1 ?+1 .r1 .tmp ?+1 # [r1]=r .r2 .r2 ?+1 .r2 .tmp ?+1 # [r2]=r # Get a. 0 .z+1 ?+1 .a0 .a0 ?+1 .a0 0 ?+1 .tmp .tmp ?+1 .tmp .a0:0 ?+1 # [tmp]=-a .a1 .a1 ?+1 .a1 .tmp ?+1 # [a1]=a # Get b. 0 .z+1 ?+1 .b0 .b0 ?+1 .b0 0 ?+1 .tmp .tmp ?+1 .tmp .b0:0 ?+1 # [tmp]=-b .b1 .b1 ?+1 .b1 .tmp ?+1 # [b1]=b # Set the return address. 0 .z+1 ?+1 .ret .ret ?+1 .ret 0 ?+1 # Set [r]=[a]+[b]. .tmp .tmp ?+1 .tmp .r1:0 ?+1 # [tmp]=-[r] .val .val ?+1 .val .tmp ?+1 # [val]=[r] .val .a1:0 ?+1 # [val]=[r]-[a] .val .b1:0 ?+1 # [val]=[r]-[a]-[b] .r2:0 .val ?+1 # [r]=[a]+[b] # Return and set [0]=0. 0 0 .ret:0 # Constants. .z:0 1 2 .tmp:0 .val:0 main: # Set [c]=[a]+[b]. 0 ? addnum .c .a .b # Add 48 to convert the numbers to ASCII digits. 0 ? addnum .a .a .asc0 0 ? addnum .b .b .asc0 0 ? addnum .c .c .asc0 # Print "[a]+[b]=[c]". 0-2 .a ?+1 0-2 .plus ?+1 0-2 .b ?+1 0-2 .equal ?+1 0-2 .c ?+1 0-2 .eol ?+1 # End the program. 0-1 0 0 # Constants. .a:3 .b:5 .c:0 .asc0:48 .plus:43 .equal:61 .eol:10

When executed, it will print

3+5=8

You can see in the comments that we specify what the full function call looks like, 0 ? addnum r a b, even though we could probably omit the 0 ? portion. This is needed because some special case functions may need a different calling convention. Especially if they avoid the use of address 0.

Notes

Because mathematical functions are used so often, it pays to optimize them as much as possible. The definition of addnum given above takes 35 instructions to execute, but it can be trimmed down to 30 instructions.