NCL 408: Indirect Registers

So far, every register operand we've written has identified one particular register:

MOVE r4 100
D.TXT r4
D.BLT

r4 means integer register 4. Which register the instruction uses is decided when we write the program.

But a register number is also just a number.

What if the program could choose that number while it was running?

Choosing a Register

Consider:

MOVE r0 4
MOVE rr0 100

The first instruction puts 4 into r0.

The second instruction uses rr0.

rr0 means:

the integer register whose number is stored in r0

Since r0 contains 4, this instruction:

MOVE rr0 100

is currently writing to r4.

We can verify that normally:

MOVE r0 4
MOVE rr0 100

D.TXT r4
D.BLT

The Display shows:

100

This is indirect register access.

Instead of naming the register directly, we use another integer register to select it.

Reading Indirectly

Indirect registers work as sources too.

MOVE r0 4
MOVE r4 100

MOVE r1 rr0

Since r0 contains 4, rr0 refers to r4.

The last instruction therefore reads the value from r4 and writes it into r1.

Afterward:

r0 = 4
r1 = 100
r4 = 100

Change the value in r0, and the same rr0 operand refers to a different integer register.

For example:

MOVE r4 100
MOVE r5 200

MOVE r0 4
D.TXT rr0
D.TXT " "

MOVE r0 5
D.TXT rr0

D.BLT

The Display shows:

100 200

The instructions using rr0 didn't change.

The register selected by rr0 did.

Integer and String Registers

Indirect access can select either kind of general-purpose register.

Operand Meaning
rr0 Integer register whose number is stored in r0
sr0 String register whose number is stored in r0

For example:

MOVE r0 3
SMOVE sr0 "Hello"

D.TXT s3
D.BLT

Since r0 contains 3, sr0 refers to s3.

The Display shows:

Hello

Indirect register operands have two parts:

  • rr0 uses the value in r0 to select an integer register.
  • sr0 uses the value in r0 to select a string register.

In both cases, r0 is an ordinary integer register containing the register number. The leading r or s determines which kind of register that number selects.

There is only one level of indirection. rr0 reads r0 once and uses its value as an integer register number.

A Small Indexed Collection

In NCL 402, we learned to use part of the value stack as indexed storage.

If our data is already stored in consecutive registers, indirect access gives us another option.

Suppose:

MOVE r8 10
MOVE r9 20
MOVE r10 30
MOVE r11 40

We can think of these four registers as a small collection beginning at register 8.

If an index is stored in r0:

MOVE r0 2

we can calculate the corresponding register number:

ADD r1 r0 8

r1 now contains 10.

That means:

D.TXT rr1
D.BLT

displays:

30

The same code can access any of the four values. Only the index needs to change.

MOVE r0 0
ADD r1 r0 8
D.TXT rr1

selects r8.

MOVE r0 1
ADD r1 r0 8
D.TXT rr1

selects r9.

The familiar calculation:

base + index

has appeared again.

This time, the result isn't a position in the value stack.

It's a register number.

Registers or Indexed Data?

Indirect registers don't replace the indexed value-stack storage we learned in NCL 402.

They solve different problems.

Need Good fit
Temporary values and subroutine interfaces PUSH and POP
Small collection usually accessed by fixed names Consecutive registers
Small collection needing occasional runtime indexing Consecutive registers with indirect access
Larger indexed collection Indexed value-stack storage

Our Noughts and Crosses board is a good example of a small collection:

#cell0 r20
#cell1 r21
#cell2 r22
#cell3 r23
#cell4 r24
#cell5 r25
#cell6 r26
#cell7 r27
#cell8 r28

There are only nine cells, and direct access is often useful.

If we specifically mean the center cell:

MOVE #cell4 #player

is wonderfully clear.

Likewise, a diagonal can still be written using its meaningful aliases:

ADD #line #cell0 #cell4
ADD #line #line #cell8

Indirect access doesn't make those expressions better.

It becomes useful when the register itself is chosen at runtime.

Our game already has exactly such a value:

#selection r0

#selection ranges from 0 through 8.

The board occupies integer registers 20 through 28.

So:

ADD r1 #selection 20
MOVE rr1 #player

turns the selection into the corresponding board register and places the player's value there.

If #selection is 0, r1 becomes 20, so rr1 refers to r20.

If #selection is 4, r1 becomes 24, so rr1 refers to r24.

If #selection is 8, r1 becomes 28, so rr1 refers to r28.

The board didn't need to move anywhere.

It was already stored somewhere we could index.

Does this data need to move into indexed storage, or is it already stored somewhere that can be indexed?

For something much larger, the answer may be different. A 64-cell chess board cannot reasonably consume 64 integer registers when the entire CPU only has 32.

The indexed value-stack techniques from NCL 402 remain useful for collections like that.

Representation depends on how the data is used.

Existing Instructions Become Dynamic

Indirect registers aren't special versions of MOVE.

They're register operands.

That means we can use them with other instructions too.

Suppose two integer registers contain the numbers of registers we want to exchange:

MOVE r0 8
MOVE r1 9

We can write:

SWAP rr0 rr1

If r0 contains 8 and r1 contains 9, this swaps r8 and r9.

Change those values and the same SWAP instruction exchanges a different pair of registers.

This is especially useful because the locations are selected at runtime.

Consider trying to build the same operation manually:

MOVE r2 rr0
MOVE rr0 rr1
MOVE rr1 r2

That works only if r2 is safe to use as temporary storage.

But the values in r0 and r1 are chosen at runtime. One of them could select r2 itself.

Our temporary register could collide with one of the values we're trying to exchange.

SWAP rr0 rr1

needs no scratch register.

Back in NCL 401, SWAP saved us from manually exchanging two known registers.

Now the same abstraction can exchange two registers that weren't known until the program was already running.

Invalid Register Numbers

Indirect access still has to select a register that exists.

For rr0, the value in r0 must identify a valid integer register.

For sr0, it must identify a valid string register.

For example:

MOVE r0 100
MOVE rr0 5

attempts to access integer register 100.

There is no such register.

An invalid register access causes a fatal:

REGISTER_ERROR

The same applies when reading indirectly.

If a value will be used as a register number, your program is responsible for making sure that value is valid.

Try It

Start with three values:

MOVE r8 10
MOVE r9 20
MOVE r10 30

Use r0 as an index:

MOVE r0 1

Without directly naming r9, change the selected value to 99.

First, turn the index into a register number:

ADD r1 r0 8

Then use that register number indirectly:

MOVE rr1 99

Display all three values:

D.TXT r8
D.TXT " "
D.TXT r9
D.TXT " "
D.TXT r10
D.BLT

You should see:

10 99 30

Now change only:

MOVE r0 1

Try 0 and 2.

The same ADD and indirect MOVE can modify any of the three registers.


Our Noughts and Crosses board already lives in nine consecutive registers. Until now, a runtime selection couldn't directly choose one of them, so the game used control flow to bridge that gap.

It doesn't need to anymore.

In the next lesson, NCL 409: Refactoring the Game, we're going back to the game and rebuilding it with what we've learned.