NCL 404: Passing Values to Subroutines
So far, our subroutines have mostly communicated through agreed registers.
For example:
MOVE r0 12
MOVE r1 30
CALL $subtract
D.TXT r2
D.BLT
JUMP 0
$subtract
SUB r2 r0 r1
RET
This works, but it creates an agreement:
r0contains the first input.r1contains the second input.r2receives the result.
Those registers are now part of the subroutine's interface.
As programs grow, keeping track of many fixed register agreements becomes awkward.
We already have another place where values can be stored temporarily: the value stack.
Arguments and Return Values
A value supplied to a subroutine is commonly called an argument.
A value produced by a subroutine and given back to its caller is called a return value.
Instead of assigning particular registers to those jobs, we can pass values through the value stack.
For example:
PUSH 12
PUSH 30
CALL $subtract
POP r0
D.TXT r0
D.BLT
JUMP 0
The caller pushes the arguments before the call.
The subroutine removes them, performs its work, and pushes the return value:
$subtract
POP r1
POP r0
SUB r0 r0 r1
PUSH r0
RET
The caller then pops the result.
The complete exchange looks like this:
| Event | Value stack |
|---|---|
| Start | empty |
PUSH 12 |
12 |
PUSH 30 |
12, 30 |
CALL $subtract |
12, 30 |
$subtract pops into r1 |
12 |
$subtract pops into r0 |
empty |
$subtract pushes result |
42 |
RET |
42 |
| Caller pops result | empty |
Notice that CALL and RET do not change the value stack.
They use the separate call stack from the previous lesson.
The two stacks have different jobs:
The call stack carries control between subroutines. The value stack can carry data between them.
Argument Order
The value stack is last in, first out.
That means the order of arguments matters.
Suppose the caller writes:
PUSH 12
PUSH 30
CALL $subtract
The last value pushed is 30, so it is the first value the subroutine receives:
POP r1 -- 30
POP r0 -- 12
Then:
SUB r0 r0 r1
calculates:
12 - 30
The caller and the subroutine simply need to agree on the order.
NCL does not impose a universal argument order.
In this course, when passing several arguments, we'll usually push them in the order we naturally write them and pop them in reverse order inside the subroutine.
Returning a Value
Passing a return value works the same way.
The caller provides arguments:
PUSH 7
PUSH 5
CALL $multiply
The subroutine consumes them:
$multiply
POP r1
POP r0
MUL r0 r0 r1
Before returning, it pushes the result:
PUSH r0
RET
The caller receives it:
POP r8
Now r8 contains 35.
This creates a simple pattern:
- Caller pushes arguments.
CALL- Subroutine pops arguments.
- Subroutine does its work.
- Subroutine pushes return value.
RET- Caller pops return value.
The important part is the agreement between caller and subroutine.
The caller does not need to know which scratch registers the subroutine uses internally.
Scratch Registers Stay Internal
Compare these two versions.
A fixed-register interface:
MOVE r0 12
MOVE r1 30
CALL $subtract
-- result is in r2
with:
$subtract
SUB r2 r0 r1
RET
The caller needs to know about r0, r1, and r2.
Now the stack version:
PUSH 12
PUSH 30
CALL $subtract
POP r8
with:
$subtract
POP r1
POP r0
SUB r0 r0 r1
PUSH r0
RET
The caller only needs to know this:
$subtractconsumes two values.$subtractproduces one value.
Which scratch registers it uses internally are no longer part of the interface.
In this course, we'll usually treat r0 through r7 as general scratch registers.
A subroutine is free to use them unless some other agreement says otherwise.
That convention belongs to our programs, not to NCL itself.
Returning More Than One Value
The value stack does not require a subroutine to return exactly one value.
A routine may return none, one, or several.
For example:
PUSH 17
CALL $double_and_original
POP r1
POP r0
The subroutine could be:
$double_and_original
POP r0
MUL r1 r0 2
PUSH r0
PUSH r1
RET
After the call:
r1receives34.r0receives17.
Again, the order follows normal stack behavior.
The subroutine and its caller only need to agree on what is being passed and in what order.
Nested Calls
Stack-based interfaces also work with nested subroutines.
For example:
PUSH 6
CALL $outer
POP r8
$outer might itself call another routine:
$outer
POP r0
PUSH r0
PUSH 2
CALL $multiply
POP r0
PUSH r0
RET
The call stack remembers where $multiply should return.
The value stack carries the values being passed.
Those are separate mechanisms working at the same time.
This is one reason stack-based subroutine interfaces are so useful: the same pattern continues to work as routines call other routines.
A Subroutine Interface
Once we use the stack this way, we can describe a subroutine by what it consumes and produces.
For example:
$subtract
Arguments: 2 integers
Returns: 1 integer
or:
$double_and_original
Arguments: 1 integer
Returns: 2 integers
The caller does not need to care how the routine performs its work internally.
It only needs to follow the agreed interface.
This makes a subroutine easier to reuse and easier to change.
If we later rewrite $subtract to use different scratch registers, its callers do not need to change at all.
Try it
Write a subroutine named $difference.
It should:
- receive two integer arguments from the value stack;
- subtract the second from the first;
- return one integer result on the value stack.
The following program:
PUSH 50
PUSH 18
CALL $difference
POP r8
D.TXT r8
D.BLT
should display:
32
Then try changing the arguments.
Make sure the result still follows:
first argument - second argument
Pay attention to the order in which the values are pushed and popped.
Passing arguments and return values through the value stack means our subroutines no longer need fixed interface registers.
But scratch registers are still shared machine state.
If a caller has something important in a scratch register and needs it after a call, it must preserve that value before making the call.
In the next lesson, NCL 405: Preserving State, we'll learn how callers preserve scratch state, and how subroutines can safely borrow registers outside the agreed scratch range.