Instruction set
AI notice: This text is created with the support of AI systems; it is reviewed editorially and taken responsibility for before publication.
Redcode has 16 instructions. This page describes each one: what it does, which modifiers matter for it, and where it appears in real warriors.
If you only want a quick reminder of what an instruction does, the overview is enough. The sections below go into detail.
Overview
| Instruction | In one sentence |
|---|---|
DAT |
Kills the process that runs onto it. |
MOV |
Copies A to B. |
ADD |
Adds A onto B. |
SUB |
Subtracts A from B. |
MUL |
Multiplies B by A. |
DIV |
Divides B by A. |
MOD |
Remainder of B divided by A. |
JMP |
Jumps to A. |
JMZ |
Jumps to A if B is zero. |
JMN |
Jumps to A if B is not zero. |
DJN |
Subtracts one from B, then jumps if B is not zero. |
SPL |
Creates a second process at A. |
SEQ |
Skips the next line if A equals B. |
SNE |
Skips the next line if A differs from B. |
SLT |
Skips the next line if A is less than B. |
NOP |
Does nothing. |
LDP and STP exist in addition and access p-space. Under the standard '94nop
rule set they are disabled; see Standards.
The instructions in detail
DAT
DAT.F #0, #0
Does nothing — and thereby terminates the process that runs onto it. DAT is
the only instruction that kills.
Two things worth knowing:
Empty core consists entirely of DAT.F $0, $0. Memory is not empty before a
round starts but lethal throughout. A warrior therefore does not die from being
"hit" but from running onto a DAT.
As a bomb, DAT is invisible. A scanner looks for deviations from empty
core. A cell containing DAT 0, 0 looks to it like unused memory — the bomb is
lethal and inconspicuous at once. That is why many warriors bomb with exactly
this value.
DAT operands are never executed but are ordinary memory. Warriors therefore
like to use DAT cells as pointers and counters.
MOV
MOV.I $0, $1
Copies. The most important instruction in the language — and the one where the modifier makes the biggest difference.
| Modifier | What is copied |
|---|---|
.A |
A-field to A-field |
.B |
B-field to B-field |
.AB |
A-field of source into B-field of target |
.BA |
B-field of source into A-field of target |
.F |
both fields, A→A and B→B |
.X |
both fields crossed, A→B and B→A |
.I |
the whole instruction, including opcode and addressing modes |
Only .I transfers the opcode. Every other modifier moves numbers alone and
leaves the target cell's opcode untouched.
This is not a detail. An imp using MOV.I survives three quarters of its
fights; the same imp with any other modifier loses every single round,
because its copies are only numbers and the opcode there stays DAT. The
measurement is in Understanding Redcode.
ADD and SUB
ADD.AB #4, $bomb
Arithmetic on fields. ADD adds, SUB subtracts — field by field, as the
modifier prescribes.
The main purpose is advancing pointers. A bomber adds its step size to the
target field on each pass and thereby travels through the core.
ADD.AB #4, $bomb means: take the number 4 and add it to the B-field of cell
bomb.
Note the .AB modifier — it is almost always the right one here, because the
target value usually sits in the B-field while the addend is an immediate value
in the A-field. ADD.F would change both fields at once, which is generally not
what you want.
All arithmetic is modulo the core size. There is no overflow and there are no negative addresses; 8000 − 1 gives 7999, and 7999 + 1 gives 0 again.
MUL, DIV and MOD
MUL.B $2, $3
Multiplication, division and remainder. Rare in practice — with one important exception.
DIV and MOD by zero kill the process. That is the only case besides
DAT in which an instruction terminates a process, and a popular trap: if you
build in a division, you must ensure the divisor never becomes zero.
MUL has one elegant special use in modern quickscanners. There, constants are
chosen so that a single multiplication reconstructs the opponent's location
exactly — instead of computing it in several steps. The gain is latency: fewer
cycles between finding and the first attack.
JMP
JMP.A $start
Jumps to A, unconditionally. The B-field is not evaluated and therefore often serves as storage for constants or pointers.
The usual modifier is .A or .B — with JMP it makes practically no
difference, since only the address matters.
JMZ and JMN
JMZ.B $loop, $counter
Conditional jumps. JMZ jumps if the tested field is zero, JMN if it is not.
Which field is tested is decided by the modifier: .A tests the A-field, .B
the B-field, .F and .X test both (and jump only if both satisfy the
condition).
Beware of side effects: if the B-operand carries a < or >, the pointer is
altered even when no jump is taken. The side effect happens when the address is
resolved, not when the jump executes.
DJN
DJN.F $loop, $counter
Decrement and jump if not zero. Subtracts one from the tested field, then jumps if the result is not zero.
The classic loop counter — two operations in one instruction, and therefore one cycle saved per pass. In a core clear that overwrites tens of thousands of cells, that adds up considerably.
DJN also has a role as a weapon: a so-called DJN stream continuously
decrements values in foreign cells, rendering them useless without overwriting
them.
SPL
SPL.B $1, $0
Creates a second process that continues at A while the first carries on after
the SPL line. The basis of every replicator.
And here sits the most expensive beginner's misconception in the whole language:
Processes do not make you faster.
A warrior executes exactly one instruction per cycle, no matter how many processes it has. The processes share the time, they do not multiply it. A warrior with 100 processes advances a hundred times more slowly in each of them than with one.
Processes are robustness, not speed: a warrior with many processes is hard to kill, because a single hit only catches one of them. That is exactly why papers work — not because they are fast, but because they do not all die at once.
How pronounced this is shows in a real battle: one warrior grew to over 6000 processes and still lost against a scanner with a few hundred.
The number of concurrent processes is capped — at 8000 under the standard rule
set. Once the limit is reached, a further SPL has no effect.
SEQ and SNE
SEQ.I $ptr, $ptr+100
Compares two cells and skips the next instruction if the condition holds:
SEQ on equality, SNE on inequality. SEQ is called CMP in older sources;
both are the same.
This is the heart of every scanner. The usual construction:
SEQ.I $ptr, $ptr+dist ; are both locations identical?
JMP $found ; no -> somebody is here
ADD #step, $ptr ; yes -> keep searching
Because empty core looks identical everywhere, a difference between two
locations means somebody is there. The modifier decides the precision: .I
compares the complete instruction, .F only the two number fields — quicker to
write, but blind to differences in the opcode.
SLT
SLT.AB #10, $ptr
Skip if less than. Skips the next instruction if A is less than B.
The comparison is unsigned: all values lie between 0 and core size − 1, and −1 is the same as 7999. Expecting a sign here builds you a bug that is hard to find.
SLT is used for range checks — for instance to determine whether a pointer has
reached your own code.
NOP
NOP
Does nothing and costs one cycle.
Sounds useless, but has two genuine applications. First, as a placeholder to
align code to a particular address. Second as a decoy — filler that keeps a
scanner busy. Because NOP differs from empty core, a scanner takes the cell
for a find and wastes its attack on an instruction whose destruction achieves
nothing.
What the assembler fills in
If you write no modifier, the assembler inserts one — according to fixed rules that depend on the opcode and on the addressing mode of the A-operand:
| Instructions | A is # |
otherwise |
|---|---|---|
DAT, NOP |
.F |
.F |
MOV, SEQ, SNE |
.AB |
.I |
ADD, SUB, MUL, DIV, MOD |
.AB |
.F |
SLT |
.AB |
.B |
JMP, JMZ, JMN, DJN, SPL |
.B |
.B |
Examples:
| Written | Becomes |
|---|---|
MOV $1, $2 |
MOV.I |
MOV #1, $2 |
MOV.AB |
ADD $1, $2 |
ADD.F |
ADD #1, $2 |
ADD.AB |
SLT $1, $2 |
SLT.B |
JMP $1 |
JMP.B |
DJN #1, $2 |
DJN.B |
DAT #1, #2 |
DAT.F |
Two places where it is easy to slip: SLT without # falls back to .B,
not to .F like the other arithmetic instructions. And the jump instructions
ignore the A-mode entirely — DJN #1, $2 becomes .B even though the
A-operand holds an immediate value.
Recommendation: always write the modifier yourself. Not because the defaults are wrong, but because they are invisible. Relying on them means eventually hunting a long time for a bug that lives in a character which is not there.
Related pages
- Addressing modes — the eight modes and their side effects
- Modifiers — complete semantics of all seven modifiers
- Standards — differences between '88, '94 and '94nop