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.