Silicon to Scripting calling conventions

Bla bla bla abstraction or something …

Functions

We can think of functions as blocks of code that can be executed.

Recall, the program counter (PC) is the thing that tells the CPU what instruction to look at next. If we move the current location of the PC to the beginning of the function’s code, the next thing that will be run is the function. And when the function is done running, we set the PC back to where it previously was.

This works for directing execution to the function, but functions also have the ability to “communicate” with the outer scope via parameters and return values.

Parameters, return values, and local variables

In higher level languages like C, when calling a function, you can “pass” values to that function.

int main() {
    int sum = add3(3, 4, 5);

    return 0;
}

int add3(int x, int y, int z) {
    int z = x + y + z;
    return z;
}

In our CPU, there is no concept of functions, let alone a mechanism to pass values between functions. We need to create a way for functions to pass values to each other, and it needs to work both ways.

We have one very powerful, and very dangerous (in the correctness meaning), mechanism we can utilize: shared memory.

Analogy about shared state bla bla bla …

If we create a contract that both the caller and the function being called adheres to, a protocol, the two can communicate. If we set up the memory in a specific way before moving the PC, then the called function can look at the memory and use it. When it’s done, it can set up the memory in a specific way and the caller can look at the memory they need to.

The hard part is coming up with a protocol that both the caller and the called function can follow.

And to not worry too much about raw memory, we can use the stack for most of it. The other information will be stored in our constants memory.

  1. Push the parameters onto the stack (done by the caller)

  2. Push the location of where the current arguments begin

  3. Push the location of where the current local vars begin

  4. Push the address to “return to” after the called function ends

  5. Calculate the new address of where the ARGS begin

  6. GOTO where the function is

  7. Set the new address of the local vars (top of the stack)

  8. run the function

  9. restore the state of ARGS and LOCALS


The stack would look like

// start in "main" func
... prev stack

// main calls func1
PARAM 0 for func1
PARAM 1 for func1
PARAM 2 for func1
PARAM ...
ARGS addr for main
LOCALS addr for main
RETURN addr in main

... local vars for func1

// func1 calls func2
PARAM 0 for func2
PARAM 1 for func2
ARGS addr for func1
LOCALS addr for func1
RETURN addr in func1
... local vars for func1
// start in "main" func
... prev stack

// main calls func1
PARAM 0 for func1
PARAM 1 for func1
PARAM 2 for func1
PARAM ...
ARGS addr for main
LOCALS addr for main
RETURN addr in main

... local vars for func1

// func1 calls func2
PARAM 0 for func2
PARAM 1 for func2
ARGS addr for func1
LOCALS addr for func1

INIT_STACK
GOTO start

FUNCTION add3 0
PUSH_ARG 0
PUSH_ARG 1
ADD
PUSH_ARG 2
ADD
RETURN

start:
PUSH_VALUE 3
PUSH_VALUE 4
PUSH_VALUE 5
CALL add3 3