Syntax
Overview
NCL programs are composed of instructions, operands, comments, constants, and labels.
Each instruction begins with a verb followed by zero or more whitespace-separated operands:
MOVE r0 10
ADD r1 r0 5
BEQ $done r1 15
Multiple instructions may appear on the same program line when separated by semicolons:
MOVE r0 10; ADD r0 r0 5
Before execution begins, the program undergoes a pre-pass that resolves source-level symbols such as constants, labels, and relative line references.
Comments
Comments begin with -- and continue to the end of the program line.
-- This entire line is a comment.
MOVE r0 42 -- Store the answer.
Comments are ignored during program processing.
Program lines
A program consists of numbered lines containing one or more instructions.
Normally, a line contains a single instruction:
MOVE r0 10
ADD r0 r0 5
D.TXT "Hello!"
Multiple instructions may be placed on the same line by separating them with semicolons:
MOVE r0 10; ADD r0 r0 5; MUL r0 r0 2
Instructions on the same line execute sequentially from left to right.
Coalesced instructions
Instructions separated by semicolons are called coalesced instructions.
Coalescing allows several instructions to share a single program line:
MOVE r0 10; INC r0; INC r0
-- > r0 = 12
Each instruction completes before the next instruction begins.
This includes EXT calls to system services, expansion modules, and peripherals. If an EXT instruction must wait for a response, execution of the line pauses until that operation completes.
MATH.SQRT r0 2000; MOVE r1 r0
MOVE does not execute until MATH.SQRT has completed.
Control flow within a line
Writing to the program counter (pc) immediately ends execution of the current program line.
Any remaining coalesced instructions are skipped, and execution continues at the line specified by pc.
MOVE r0 10; BEQ $done r0 10; MOVE r1 20
Because the branch is taken, it writes $done to pc. MOVE r1 20 is therefore skipped and execution continues at $done.
If a conditional branch is not taken, it does not write to pc, and execution continues with the next instruction on the same line:
MOVE r0 10; BEQ $done r0 20; MOVE r1 20
-- > r1 = 20
Unconditional branches, subroutine calls, and returns always write to pc and therefore end the current line:
MOVE r0 10; JUMP $next; MOVE r0 20
-- MOVE r0 20 is not executed
Writing to pc ends the line even if the value written is the same as the normal next line:
JUMP @1; MOVE r0 42
-- MOVE r0 42 is not executed
The same rule applies when an expansion module or peripheral writes to pc.
Lines and instruction positions
Coalesced instructions share a program line. They do not receive individual line numbers.
MOVE r0 10; INC r0; INC r0
All three instructions belong to the same line.
Labels, relative line references, and the program counter operate on program lines rather than individual coalesced instructions.
For example, @0 refers to the current program line regardless of which instruction on that line is executing.
If the line completes without an instruction writing to pc, execution continues normally with the following program line.
Operand types
Instruction documentation describes operands using the following types.
| Type | Accepts |
|---|---|
| Integer register | r0–r31 |
| String register | s0–s15 |
| Integer value | Integer literal, integer register, or value convertible to an integer. |
| String value | String literal, string register, or value convertible to a string. |
| Branch target | Absolute line number, label, or relative line reference. |
A register operand specifically requires a register.
A value operand describes a value consumed by an instruction. Registers and literals may therefore be used interchangeably where appropriate.
For example:
ADD r0 r1 10
r0 must be an integer register because it is the destination. r1 and 10 are both integer values.
Literals
Integer literals
Integer literals represent 32-bit signed integer values and are written in decimal notation.
0
42
-42
NCL does not provide hexadecimal integer literal syntax. Hexadecimal values may instead be represented using string-to-integer conversion:
MOVE r0 "0xFF"
-- > r0 = 255
Here, "0xFF" is a string literal which is converted to an integer when used as an integer value.
String literals
String literals are enclosed in double quotation marks:
"Hello, world!"
"42"
""
Strings are stored as UTF-16.
A quoted numeric value remains a string literal:
"42"
When supplied to an instruction expecting an integer value, it is implicitly converted.
String escape sequences
String literals support escape sequences for characters that are difficult or inconvenient to write directly.
An escape sequence begins with a backslash:
"Hello\nWorld"
"She said \"Hello!\""
Escape sequences are resolved during the program pre-pass before the string is passed to an instruction.
Named escapes
NCL provides short escapes for several common characters:
| Escape | Character | Codepoint | Description |
|---|---|---|---|
\" |
" |
U+0022 |
Double quotation mark. |
\\ |
\ |
U+005C |
Backslash. |
\a |
BEL | U+0007 |
Alert. |
\b |
BS | U+0008 |
Backspace. |
\t |
HT | U+0009 |
Horizontal tab. |
\n |
LF | U+000A |
Line feed. |
\v |
VT | U+000B |
Vertical tab. |
\f |
FF | U+000C |
Form feed. |
\r |
CR | U+000D |
Carriage return. |
\d |
DEL | U+007F |
Delete. |
\cd |
PLD | U+008B |
Partial Line Forward. |
\cu |
PLU | U+008C |
Partial Line Backward. |
\i |
RI | U+008D |
Reverse index. |
The meaning of a control character depends on the instruction or peripheral receiving the resulting string. For example, the Display interprets several of these characters as cursor and display controls.
D.TXT "Hello!\a\nNext line"
Hexadecimal escapes
\xXX
\x inserts the character represented by a two-digit hexadecimal value.
"\x41"
-- > "A"
"\x0A"
-- > Line feed
Exactly two hexadecimal digits follow \x.
This form is particularly useful for inserting C0 and C1 control characters.
Unicode escapes
Unicode characters may be specified by codepoint using \u or \U.
\uXXXX
\UXXXXXXXX
\u accepts four hexadecimal digits:
"\u03BB"
-- > "λ"
\U accepts eight hexadecimal digits and may represent codepoints throughout the Unicode range:
"\U0001F680"
-- > "🚀"
Exactly four hexadecimal digits follow \u, and exactly eight follow \U.
Display style escapes
\sXX
\s inserts a character in the private-use range U+F100 through U+F1FF.
The two hexadecimal digits become the low byte of the resulting codepoint:
"\s12"
-- > U+F112
These characters are used by compatible Display peripherals to encode inline colour and text attributes.
For example:
D.TXT "\s12Inverted Red\s0FNormal White"
The interpretation of the encoded attributes is documented with the Display peripheral.
Unknown escapes
Unknown escape sequences are permissive. The backslash is removed and the escaped character is retained.
"\q"
-- > "q"
This allows a backslash to precede an otherwise unrecognized character without producing a program error.
Implicit conversions
Instructions automatically convert values to the type they expect.
This allows integer and string values to be passed across operand types without requiring explicit conversion instructions.
String → Integer
When an instruction expects an integer value, a string value is converted using NCL's integer conversion rules.
- Leading whitespace is ignored.
- An optional leading
-is permitted. - Parsing stops at the first character that is not part of the integer.
- Strings beginning with
0xmay represent hexadecimal values. - If no valid integer can be read, the result is
0.
| String | Integer |
|---|---|
"42" |
42 |
"-42" |
-42 |
" 42" |
42 |
"42 apples" |
42 |
"0xFF" |
255 |
"abc" |
0 |
"" |
0 |
For example:
ADD r0 "40" "2"
-- > r0 = 42
Integer → String
When an instruction expects a string value, an integer value is converted to its decimal representation.
| Integer | String |
|---|---|
42 |
"42" |
-42 |
"-42" |
0 |
"0" |
For example:
SJOIN s0 "Value: " 42
-- > s0 = "Value: 42"
Constants
Constants provide symbolic names for values and other operands.
Constant names begin with # and may contain letters, numbers, and underscores.
#MAX_SCORE
#PLAYER2
#0
Constants are resolved during the program pre-pass before execution begins.
Defining constants
Custom constants are defined using a constant name followed by its definition:
#MAX_SCORE 100
#GREETING "Hello, world!"
Once defined, a constant may be used anywhere its resulting operand is accepted:
#LIMIT 100
MOVE r0 #LIMIT
-- > r0 = 100
#MESSAGE "READY"
SMOVE s0 #MESSAGE
-- > s0 = "READY"
Constant definitions are collected before constants are substituted. This allows constants to be referenced before their definitions appear in the source:
MOVE r0 #LIMIT
#LIMIT 100
-- > r0 = 100
Redefining constants
A constant may be defined more than once.
If multiple definitions use the same name, the last definition takes precedence for every reference to that constant, including references appearing earlier in the source.
#VALUE 10
MOVE r0 #VALUE
#VALUE 20
MOVE r1 #VALUE
-- > r0 = 20
-- > r1 = 20
Constant definitions do not take effect sequentially during execution. The final definition is determined during the pre-pass and then used throughout the program.
This behavior allows constants to support forward references and lookahead.
Named registers
Constants may refer to registers, allowing registers to be given descriptive names.
#SCORE r0
#LIVES r1
#PLAYER_NAME s0
The names may then be used anywhere the corresponding register would normally be accepted:
MOVE #SCORE 100
DEC #LIVES
SMOVE #PLAYER_NAME "Alice"
Constants used this way act as register aliases rather than separate storage locations.
#COUNTER r4
MOVE #COUNTER 10
DEC #COUNTER
-- > r4 = 9
Named registers can therefore be used to approximate variables while retaining the fixed register architecture of the CPU.
Constant substitution
A constant is replaced by its definition during the pre-pass. The resulting operand is then interpreted normally by the instruction.
This means implicit conversions still apply:
#VALUE "42"
MOVE r0 #VALUE
-- > #VALUE resolves to "42"
-- > "42" is converted to the integer 42
Likewise, an integer constant may be used where a string value is expected:
#VALUE 42
SJOIN s0 "Value: " #VALUE
-- > s0 = "Value: 42"
Constants do not allocate storage or create runtime variables.
A constant definition should normally be an integer literal, string literal, or register reference. If the substituted value cannot be interpreted as the type expected by an instruction, the default value for that type is used (0 for integer values or "" for string values).
If a constant is referenced but has no definition, it resolves to the empty string ("").
Built-in constants
NCL provides a small set of built-in constants for values that are part of the language itself.
Additional constants may be provided for standard peripherals and other system interfaces. These are documented alongside the corresponding interface.
Built-in constants may also be redefined. They follow the same last-definition-wins rule as other constants.
Boolean constants
NCL does not have a separate boolean data type. Boolean values are represented using integers.
| Constant | Value | Description |
|---|---|---|
#FALSE |
0 |
False value. |
#TRUE |
1 |
True value. |
For example:
MOVE r0 #TRUE
-- > r0 = 1
BEQ $enabled r0 #TRUE
#FALSE and #TRUE are conventional names for 0 and 1; they do not create a distinct boolean type.
Labels
Labels provide symbolic names for program lines.
A label begins with $:
$loop
A label identifies its associated program line and is not itself an instruction.
Labels may be used anywhere a branch target is accepted:
$loop
INC r0
BLT $loop r0 10
Labels are collected and resolved to absolute line numbers during the program pre-pass.
Because labels are resolved before execution begins, they may be referenced before their definitions:
JUMP $start
MOVE r0 100
$start
MOVE r0 42
The source therefore does not need to define a label before referring to it.
Labels may also be used as named entrypoints when launching programs:
SYS.RUN "TOOLS" "$diagnostics"
The target program resolves $diagnostics to its corresponding program line before execution begins.
Labels may also be redefined. They follow the same last-definition-wins rule as constants.
If a label is referenced but has no definition, it resolves to line 0. Branching to line 0 terminates the current program.
Relative line references
Relative line references begin with @ and specify a program line relative to the line on which the reference appears.
@0
@1
@-1
@0 refers to the current line. Positive values refer to later lines and negative values refer to earlier lines.
For example:
DEC r0
BNEQ @-1 r0 0
Relative references operate on complete program lines, not individual coalesced instructions:
INC r0; BLT @0 r0 10
-- > Repeats the complete line while r0 < 10
Relative references are resolved to absolute line numbers during the program pre-pass.
They are therefore a source-level convenience; instructions receive absolute line numbers during execution.
Program pre-pass
Before a program begins execution, NCL performs a pre-pass over the complete program.
The pre-pass resolves source-level symbols and references before instructions are executed.
This includes:
- Resolving string escape sequences.
- Collecting constant and label definitions.
- Resolving constants and labels using their final definitions.
- Resolving relative line references to absolute program lines.
Because the complete program is examined before these references are resolved, constants and labels may be referenced before their definitions:
JUMP $start
#VALUE 42
$start
MOVE r0 #VALUE
By the time execution begins, $start and #VALUE have already been resolved.
The same label-resolution mechanism allows programs to expose named entrypoints such as $ENTRY, $diagnostics, or $benchmark.