NCL 405: Preserving State

In the previous lesson, we started passing arguments and return values through the value stack.

That freed our subroutines from fixed interface registers:

PUSH 12
PUSH 30
CALL $subtract
POP r8

The caller doesn't need to know which scratch registers $subtract uses internally.

But that raises another question.

What if the caller was already using one of those scratch registers?

Values That Need to Survive

In this course, we'll usually treat r0 through r7 as scratch registers.

A subroutine is free to change them.

Suppose the caller has a value in r0 that it still needs:

MOVE r0 100

PUSH 12
PUSH 30
CALL $subtract
POP r8

D.TXT r0
D.BLT

If $subtract uses r0 internally, the original 100 is gone.

That isn't a mistake in $subtract. We agreed that r0 is scratch.

The caller knew that the value mattered, so the caller is responsible for preserving it.

We already know how:

MOVE r0 100

PUSH r0

PUSH 12
PUSH 30
CALL $subtract
POP r8

POP r0

D.TXT r0
D.BLT

The original value waits on the stack while the call takes place.

Event Value stack
Caller saves r0 100
Push first argument 100, 12
Push second argument 100, 12, 30
Subroutine consumes arguments 100
Subroutine pushes return value 100, -18
Caller pops return value 100
Caller restores r0 empty

The saved value can remain underneath the entire subroutine exchange.

If a caller needs a scratch-register value after a call, the caller must preserve it before the call and restore it afterward.

Preserving Several Registers

We can preserve as many scratch values as we need:

PUSH r0
PUSH r1
PUSH r2

CALL $work

POP r2
POP r1
POP r0

The registers are restored in reverse order because the value stack is last in, first out.

If we instead wrote:

PUSH r0
PUSH r1

CALL $work

POP r0
POP r1

the old value of r1 would be written into r0, and the old value of r0 would be written into r1.

Nothing special happens to saved register values. They're ordinary values on the value stack, so all the stack behavior we've already learned still applies.

A Subroutine Can Be a Caller

Caller responsibility doesn't mean only the main program needs to think about preservation.

A subroutine can call another subroutine.

Suppose $outer has a useful intermediate value in r0:

$outer
POP r0

-- Work with r0.

CALL $inner

-- Work with r0 again.

PUSH r0
RET

If $inner uses r0 as scratch, $outer loses its intermediate value.

While $outer is calling $inner, $outer is the caller.

It can preserve its value in exactly the same way:

$outer
POP r0

-- Work with r0.

PUSH r0
CALL $inner
POP r0

-- Continue working with r0.

PUSH r0
RET

This applies at every level of nested calls.

A routine may be a subroutine from one perspective and a caller from another.

Borrowing Other Registers

Scratch registers are expected to change.

Registers outside the scratch range are different.

Suppose r8 contains longer-lived program state. A subroutine normally shouldn't destroy it just because it wanted another temporary register.

Sometimes, however, borrowing r8 is useful.

If the subroutine chooses to borrow it, the subroutine can preserve its previous value:

$work
PUSH r8

-- Use r8 temporarily.

POP r8
RET

From the caller's perspective, r8 never changed.

This gives us two complementary responsibilities:

  • If a caller needs a scratch-register value to survive a call, the caller preserves it.
  • If a subroutine borrows a register outside the agreed scratch range, the subroutine preserves it.

The underlying principle is simple:

Preservation follows responsibility.

Our convention

NCL does not assign special preservation rules to r0 through r31.

Treating r0 through r7 as scratch registers is a convention we're using in this course. Other programs are free to organize their registers differently.

Similar conventions are common in other low-level programming systems. Scratch registers that a caller must preserve when needed are often called caller-saved registers. Registers that a subroutine must restore when it borrows them are often called callee-saved registers.

When There's Somewhere to Put the Arguments

There's an easy way for a subroutine to borrow another register when scratch registers are available.

Suppose $work receives two arguments and needs to borrow r8.

It can pop the arguments into scratch first:

$work
POP r1
POP r0

PUSH r8

-- Work with r0, r1, and r8.

POP r8
PUSH r0
RET

By the time we save r8, the arguments have already been removed from the stack.

Everything has somewhere convenient to go.

But that won't always be true.

When Every Register Has a Job

Suppose r0 through r7 already contain live values.

The routine receives an argument on the value stack, and it wants to use r8 for that argument.

There are two things we need to accomplish:

  1. Get the argument from the stack into r8.
  2. Preserve the old value of r8.

Normally, we might do this:

POP r8

But that destroys the old value of r8.

We could save r8 first:

PUSH r8

but now the saved value is on top of the argument. A normal POP r8 would simply give us the old value again.

We appear to need somewhere else to hold one of the two values.

But look at what we actually want:

Location Before After
r8 old r8 argument
Top of stack argument old r8

We don't need another storage location.

We need to exchange the two values.

Swapping With the Stack

sv represents the value on top of the stack.

That means we can write:

SWAP r8 sv

Before the instruction:

Location Value
r8 old r8
sv argument

Afterward:

Location Value
r8 argument
sv old r8

The argument has moved into the register we wanted to use.

At the same time, the old register value has taken its place on the stack.

The routine can now work with the argument in r8:

$work
SWAP r8 sv

-- Work with the argument in r8.

POP r8
RET

At the end, POP r8 restores the old value of r8 and removes it from the stack.

The argument's original stack position became the storage position for the register value we needed to preserve.

No additional stack position was required.

Back in NCL 401, SWAP looked mostly like a convenient way to exchange two registers.

Now we're using the same instruction to solve a state-management problem without needing another temporary location.

Exchanging Several Arguments

We can extend the same idea to several arguments.

Suppose three arguments were pushed:

PUSH #first
PUSH #second
PUSH #third
CALL $work

Inside $work, we want:

  • #first in r8;
  • #second in r9;
  • #third in r10;

while preserving the old values of all three registers.

The stack currently ends with:

Position Value
Lowest #first
#second
Highest #third

We can exchange each argument with its destination register.

First, save the current stack pointer:

MOVE r31 sp

Then start at the top:

SWAP r10 sv

r10 now contains #third, while its old value occupies that argument's stack position.

Move down one position:

DEC sp
SWAP r9 sv

Now r9 contains #second.

Move down once more:

DEC sp
SWAP r8 sv

Now r8 contains #first.

Finally, restore the real stack pointer:

MOVE sp r31

Together:

MOVE r31 sp

SWAP r10 sv
DEC sp
SWAP r9 sv
DEC sp
SWAP r8 sv

MOVE sp r31

The exchange has transformed our state:

Location Before After
r8 old r8 #first
r9 old r9 #second
r10 old r10 #third
First argument position #first old r8
Second argument position #second old r9
Third argument position #third old r10

The routine can now use r8, r9, and r10 as its arguments.

When it's finished:

POP r10
POP r9
POP r8

restores all three previous register values.

The arguments have been consumed, the borrowed registers have been restored, and the stack is back where it was before those arguments were pushed.

Why Save sp?

We could manually move sp back up after the exchanges:

INC sp
INC sp

But we've already established a convention for temporarily navigating the value stack:

MOVE r31 sp
...
MOVE sp r31

Using r31 makes our intent clear.

We are borrowing sp to reach other stack positions, then restoring it to its previous value.

This is the same principle we used for indexed data in NCL 402.

Keep the layout straight

Moving sp does not move the values stored in the stack.

While sp is temporarily lower, the values above it are still where you left them. Restoring sp makes those positions active again.

When manipulating several live stack values directly, keep track of which value occupies each position and restore sp before returning to ordinary PUSH and POP operations.

Returning a Result

If our routine also needs to return a value, the order matters.

The old register values are still waiting on the stack while the routine performs its work.

Restore them first:

POP r10
POP r9
POP r8

Then push the return value:

PUSH #result
RET

The caller now sees the return value on top of the stack, just as it expects.

This is another reason to think about the layout of the value stack rather than treating it as an anonymous pile of temporary values.

Different values may be there for different reasons:

  • arguments waiting to be consumed;
  • values preserved across a call;
  • borrowed register values waiting to be restored;
  • return values waiting for the caller.

They all use the same 128 stack positions.

Try It

Suppose a subroutine receives three arguments:

PUSH 10
PUSH 20
PUSH 30
CALL $total
POP r0

$total should:

  • receive the arguments into r8, r9, and r10;
  • preserve the previous values of all three registers;
  • add the three arguments together;
  • restore r8, r9, and r10;
  • return the total on the value stack.

Use r31 to preserve sp while exchanging the arguments with the previous register values.

The caller should receive:

60

Try setting r8, r9, and r10 to recognizable values before the call. After $total returns, verify that all three still contain their original values.


We've now used the value stack for more than temporary storage. It can carry arguments and results, preserve live state, and hold deliberately arranged values while a subroutine works.

So far, we've also generally given each instruction its own source line.

NCL doesn't require that. In the next lesson, NCL 406: Coalescing Instructions, we'll look at how several instructions can share a source line, and how to group them without making a program harder to follow.