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:

  • r0 contains the first input.
  • r1 contains the second input.
  • r2 receives 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:

  1. Caller pushes arguments.
  2. CALL
  3. Subroutine pops arguments.
  4. Subroutine does its work.
  5. Subroutine pushes return value.
  6. RET
  7. 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:

  • $subtract consumes two values.
  • $subtract produces 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:

  • r1 receives 34.
  • r0 receives 17.

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.