Cookbook & Tricks

Overview

This page collects common NCL programming patterns, useful idioms, and techniques that combine several instructions.

For the complete behavior of individual instructions, see the corresponding NCL or peripheral reference page.


Style recommendations

NCL places relatively few restrictions on how programs are organized. Consistent conventions can make larger programs easier to read and maintain.

Use labels for meaningful destinations

Prefer labels when a branch represents a meaningful point in the program:

BEQ $done r0 0

-- Work...

$done
HALT

Absolute and relative line references are useful when the relationship between lines is itself important, but labels generally survive program changes more easily.

Give important registers names

Constants may be used as aliases for registers:

#score r0
#lives r1
#name s0

MOVE #score 100
MOVE #lives 3
SMOVE #name "Player"

This can make the purpose of long-lived registers clearer without changing how they are used.

Temporary registers generally do not need names.

Keep destructive operations obvious

Many NCL instructions permit their destination to also be one of their sources:

ADD r0 r0 1
SJOIN s0 s0 "!"

This is useful and concise, but when the original value will be needed later, store the result elsewhere or preserve the value first.


Branching patterns

Conditional branches compare their operands directly and transfer execution when the condition is true.

More complex control structures can be built by combining conditional branches with JUMP.

If / else

An if / else structure can be constructed using a conditional branch and an unconditional jump:

-- if r0 >= 100
BLT $else r0 100

D.TXT "HIGH"
JUMP $done

$else
D.TXT "LOW"

$done

If r0 is at least 100, the first block executes. Otherwise, execution branches to $else.


Chained conditions (AND)

To require several conditions to be true, branch away as soon as one fails:

-- if r0 >= 10 AND r0 <= 20
BLT $outside r0 10
BGT $outside r0 20

D.TXT "INSIDE"
JUMP $done

$outside
D.TXT "OUTSIDE"

$done

Execution reaches the INSIDE case only if both comparisons succeed.


Alternate conditions (OR)

To accept any of several conditions, branch to the same destination whenever one succeeds:

-- if r0 == 1 OR r0 == 3 OR r0 == 5
BEQ $match r0 1
BEQ $match r0 3
BEQ $match r0 5

D.TXT "NO MATCH"
JUMP $done

$match
D.TXT "MATCH"

$done

Multi-way branch

Several comparisons may be chained to select between multiple cases:

BEQ $red r0 1
BEQ $green r0 2
BEQ $blue r0 3
JUMP $other

$red
D.TXT "RED"
JUMP $done

$green
D.TXT "GREEN"
JUMP $done

$blue
D.TXT "BLUE"
JUMP $done

$other
D.TXT "OTHER"

$done

This provides a simple equivalent to a switch or case statement.


Constructing boolean values from branches

Conditional branching verbs perform comparisons directly and do not store the result.

If a comparison result must be preserved as a boolean value, it can be constructed explicitly using branching and MOVE.

-- Set r0 to whether the score is at least 100.
#score r1

BLT $below #score 100
MOVE r0 1
JUMP $done

$below
MOVE r0 0

$done
-- > r0 = 1 if #score >= 100, otherwise 0

Once normalized to 0 or 1, a boolean may also be toggled using XOR:

XOR r0 r0 1

Loops

Loops are constructed by branching to an earlier point in the program.

Counted loops

A counter may be decremented until it reaches zero:

MOVE r0 10

$loop
D.TXT "Hello!\n"

DEC r0
BNEQ $loop r0 0

The loop executes ten times.

If the count may initially be zero or negative, test it before entering the loop:

BLE $done r0 0

$loop
-- Work...

DEC r0
BNEQ $loop r0 0

$done

Sentinel loops

A sentinel loop continues until a particular value is encountered.

For example, keyboard input may be processed until the user enters Q:

$input
SYS.AKEY s0
BSEQ $done s0 "Q"

-- Process s0 here.

JUMP $input

$done

The sentinel may be any value appropriate to the operation.


Infinite loops

An unconditional backward branch creates an infinite loop:

$loop

-- Work...

JUMP $loop

Interactive programs commonly combine this with SYS.AKEY or other operations that wait for external input:

$input
SYS.AKEY s0
D.CHR s0
JUMP $input

The program may still be terminated externally using ABORT.


Stack usage

The value stack provides temporary integer storage independent of the general-purpose registers.

Saving registers

A register may be preserved while it is temporarily needed for another purpose:

PUSH r0

-- r0 may be used temporarily.
MOVE r0 42

-- Restore its previous value.
POP r0

Multiple values may be preserved. Because the stack is LIFO, they must be restored in reverse order:

PUSH r0
PUSH r1
PUSH r2

-- Work...

POP r2
POP r1
POP r0

Passing temporary values

The stack can also hold intermediate values when convenient:

MUL r0 r1 r2
PUSH r0

MUL r0 r3 r4
POP r1

ADD r0 r0 r1

For small expressions, another general-purpose register is usually simpler.

The stack becomes useful when temporary values must survive longer sections of code or when registers are already occupied.


Program flow

Leaving and returning to a program

SYS.NEXT and SYS.RUN can be combined to temporarily transfer execution to another program and return later.

The special constant #THIS contains the exact path used to launch the current program invocation. Using it with SYS.NEXT allows a program to queue another invocation of itself without hard-coding its path.

-- Do some work before leaving.
D.TXT "Leaving...\n"

-- Queue a future invocation of this program.
SYS.NEXT #THIS "$return" "Welcome back!"

-- Terminate this invocation and run another program.
SYS.RUN "A:/OTHER.NCL" 0

$return
-- Arguments are available as #0 through #15.
D.TXT #0

SYS.NEXT places a new invocation of the current program at the front of the execution queue.

SYS.RUN then terminates the current invocation and immediately starts OTHER.NCL.

When OTHER.NCL finishes, the queued invocation is loaded at $return. The argument "Welcome back!" is available as #0.

The returning program is a new invocation. Registers and stacks from the previous invocation are not preserved. Any state that must survive should be passed as arguments or stored elsewhere.

Returning to the next instruction

A relative entrypoint can be used when the program should simply continue after the transfer:

SYS.NEXT #THIS @2 "Welcome back!"
SYS.RUN "A:/OTHER.NCL" 0

-- A new invocation resumes here.
D.TXT #0

The relative reference is resolved to an absolute program line during the pre-pass. That line is then used as the entrypoint of the new invocation.

This creates continuation-like program flow without keeping the original invocation resident.


Random numbers

Rolling dice

RNG generates values starting at 0, while dice are conventionally numbered starting at 1.

A standard die can therefore be rolled by adding 1 to the generated value:

-- Roll 1d6.
RNG r0 6
INC r0
-- > r0 is between 1 and 6

The same pattern works for any die size:

-- Roll 1d20.
RNG r0 20
INC r0
-- > r0 is between 1 and 20

Rolling multiple dice

Multiple dice can be rolled by accumulating successive results:

-- Roll 2d6.
RNG r0 6
RNG r1 6
ADD r0 r0 r1
ADD r0 r0 2
-- > r0 is between 2 and 12

Because each RNG 6 produces a value from 0 through 5, adding 2 after summing the rolls converts the result to the conventional 2d6 range of 2 through 12.


Roll 4d6, drop the lowest

A common method for generating D&D ability scores is to roll four six-sided dice and discard the lowest result.

-- Roll four six-sided dice.
RNG r0 6
RNG r1 6
RNG r2 6
RNG r3 6

-- Find the lowest roll.
MIN r4 r0 r1
MIN r4 r4 r2
MIN r4 r4 r3

-- Add all four rolls.
ADD r5 r0 r1
ADD r5 r5 r2
ADD r5 r5 r3

-- Drop the lowest and convert the remaining
-- three dice from 0–5 to 1–6.
SUB r5 r5 r4
ADD r5 r5 3
-- > r5 is between 3 and 18

Each roll initially uses the zero-based range 0 through 5.

After the lowest roll is removed, adding 3 shifts each of the three remaining dice by one, producing the same result as rolling four conventional six-sided dice and dropping the lowest.


Reproducible random sequences

Use PRNG with a fixed non-zero seed when a random sequence must be reproducible:

SEED 12345

PRNG r0 100
PRNG r1 100
PRNG r2 100

Running the same sequence after SEED 12345 produces the same values again.

This can be useful for procedural generation, repeatable simulations, tests, or any program that needs to reconstruct a random sequence later.


String tricks

Exact-width multi-character padding

SPADL and SPADR append whole copies of their padding string. When a multi-character pad is used, the result may therefore exceed the requested length.

Use SSUB after padding when an exact width is required.

-- Left-pad to exactly 10 characters.
SPADL s0 "42" "abc" 10
-- > s0 = "abcabcabc42"

SSUB s0 s0 -10 -1
-- > s0 = "bcabcabc42"

For right-padding, keep the first len characters instead:

-- Right-pad to exactly 10 characters.
SPADR s0 "42" "abc" 10
-- > s0 = "42abcabcabc"

SSUB s0 s0 0 9
-- > s0 = "42abcabcab"

This preserves the original string while removing any excess padding.


Processing a string character by character

SLEN, SSUB, and a counter can be combined to process each character of a string:

SMOVE s0 "Hello!"
SLEN r0 s0
MOVE r1 0

$loop
BGE $done r1 r0

SSUB s1 s0 r1 r1

-- Process s1 here.

INC r1
JUMP $loop

$done

String lengths and positions operate on complete characters rather than UTF-16 code units, so this pattern also works with characters outside the Basic Multilingual Plane.

For example, "🚀" has a length of 1 and occupies one string position.


Bit tricks

Testing a bit flag

Use AND to isolate a flag:

-- Test bit 3.
AND r1 r0 8
BNEQ $set r1 0

Execution branches to $set if bit 3 is set.

The original value in r0 is unchanged.


Setting a bit flag

Use OR to set one or more bits:

-- Set bit 3.
OR r0 r0 8

Other bits in the value are left unchanged.


Clearing a bit flag

Use NOT to construct an inverted mask, then apply it using AND:

-- Clear bit 3.
NOT r1 8
AND r0 r0 r1

Toggling a bit flag

Use XOR to toggle a bit:

-- Toggle bit 3.
XOR r0 r0 8

The bit becomes set if it was clear and clear if it was set.


Display tricks

Preparing a complete update before displaying it

Most Display operations modify the internal display state without immediately refreshing the physical display.

Several changes can therefore be prepared before issuing a single D.BLT:

D.CUR 0 0
D.TXT "SYSTEM STATUS"

D.CUR 0 2
D.TXT "CPU: READY"

D.CUR 0 3
D.TXT "DRIVE: READY"

D.BLT

The complete prepared display is refreshed together.

Use D.BLTN instead when the program does not need to wait for the refresh to finish.


Polling for keyboard input

SYS.KEY can be used when a program should continue doing other work while checking for keyboard input:

$loop
SYS.KEY s0
BSEQ $noInput s0 ""

-- Handle the key here.
D.CHR s0

$noInput

-- Continue other work here.

JUMP $loop

If no key is available, SYS.KEY returns immediately with an empty string.

Use SYS.AKEY instead when the program cannot continue until a key is received.


Performance tips

Coalesced instructions

Several instructions may be placed on one program line using ;:

MOVE r0 10; MOVE r1 20; ADD r2 r0 r1

Coalescing small sequences can reduce the number of program lines that must be fetched and dispatched.

It is particularly useful for short sequences that logically form one operation:

RNG r0 6; INC r0

Do not coalesce instructions merely to reduce line count. Separate lines are often easier to read, label, branch to, and debug.

Control-flow instructions should be used carefully within coalesced lines, because changing pc affects where execution continues after the current line.


Common pitfalls

Unknown constants and labels have default values

An unknown constant is substituted with an empty string:

MOVE r0 #DOES_NOT_EXIST
-- > equivalent to MOVE r0 ""
-- > r0 = 0

The unknown constant becomes "", which is then interpreted as an integer value by MOVE.

An unknown label resolves to line 0:

JUMP $does_not_exist
-- > equivalent to JUMP 0

Branching to line 0 terminates execution.


Constants do not recursively expand

Constant substitution is performed once.

A constant may contain text that resembles another constant, but the substituted value is not processed again as another constant reference:

#foo 20
#bar #foo

MOVE r0 #bar
-- > equivalent to MOVE r0 #foo
-- > r0 = 0

#bar is replaced with the literal text #foo. The resulting #foo is not subsequently replaced with 20.


Definitions are last-definition-wins

Constants and labels may be redefined. Their final definitions are used throughout the program:

#value 10
#value 20

MOVE r0 #value
-- > r0 = 20

This also applies to built-in constants:

#TRUE 42

MOVE r0 #TRUE
-- > r0 = 42

Use deliberate redefinition carefully.


Program arguments are constants

Program arguments are exposed as constants #0 through #15.

Like other constants, they may be redefined by the program:

-- #0 was supplied by the caller.
#0 "replacement"

D.TXT #0
-- > prints "replacement"

The same applies to other built-in constants such as #THIS.


A new program invocation has new CPU state

SYS.RUN, queued programs, and returning through SYS.NEXT load new program invocations.

Do not expect general-purpose registers, the value stack, or the call stack to preserve state across invocations.

For example:

MOVE r0 42

SYS.NEXT #THIS "$return" 42
SYS.RUN "A:/OTHER.NCL" 0

$return
-- r0 from the previous invocation is not preserved.
-- The value passed as an argument is available through #0.
MOVE r0 #0

Pass required values as arguments or store them externally.


Repeated SYS.NEXT calls insert at the front

SYS.NEXT adds each invocation to the front of the execution queue.

For example:

SYS.NEXT "A" 0
SYS.NEXT "B" 0

places B ahead of A.

Use SYS.QUEUE when programs should instead be added to the back of the queue in insertion order.


SHR and LSHR are different

SHR performs an arithmetic right shift and extends the sign bit.

LSHR performs a logical right shift and fills the high bits with zeroes:

MOVE r0 -1

SHR r1 r0 1
-- > r1 = -1

LSHR r2 r0 1
-- > r2 = 2147483647

For positive values, the two operations often produce the same result. The distinction becomes important when the high bit is set.


Multi-character padding may exceed its requested length

SPADL and SPADR add complete copies of their padding string:

SPADL s0 "42" "abc" 10
-- > s0 = "abcabcabc42"
-- > length 11

Use the exact-width padding technique above when the final length must be exact.


Display units depend on the operation

The standard Display contains two characters per tile.

Horizontal D.SCR distances are measured in tiles, while east and west D.CRP distances are measured in characters:

D.SCR 1
-- > scrolls horizontally by 2 characters

D.CRP 0 1
-- > crops 1 character from the right

North and south distances for both operations are measured in rows.


D.BLTN does not wait

D.BLTN begins a display refresh and allows program execution to continue without waiting for the refresh to finish:

D.TXT "Loading..."
D.BLTN

-- Execution may continue while the Display refreshes.

Use D.BLT when subsequent behavior depends on the refresh having completed.


PRINT.NEW is destructive

PRINT.NEW always discards the Printer's existing job, regardless of its current state.

This includes a job still being assembled, a Printer error, or a completed document awaiting pickup:

PRINT.NEW
PRINT.DATA "Old document"

PRINT.NEW
-- > "Old document" has been discarded

Use PRINT.NEW when deliberately starting from a clean print job.

Use PRINT.CANCEL when the existing job should be discarded without starting another one.