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 r0r31
String register s0s15
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 0x may 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:

  1. Resolving string escape sequences.
  2. Collecting constant and label definitions.
  3. Resolving constants and labels using their final definitions.
  4. 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.