The Apilar language is a stack-based assembly language. Each instruction can read from the stack or push to the stack. Instructions can also affect read/write heads which can be used to read and write memory, and to control jumps. Some operations have effects on the computer they run in, or on the world itself.
Instructions exist in memory of bytes. Not all bytes are used for instructions; those unused are also interpreted as NOOP.
Each processor at present has a hard-coded maximum stack size of length 64; each value on the stack is an unsigned 64 bit number.
Apilar implements stack machine. Let's see how that works. We start with an empty stack:
[]
If you place a number on the stack with the N1
instruction, the stack
contains the number 1
:
[1]
Let's look at the program:
N1
N2
ADD
Let's run it, one by one:
First N1
:
[1]
Then N2
:
[1 2]
Then ADD
. This pops the two top items from the stack and adds them together,
putting the result back on the stack:
[3]
You can write longer programs entirely composed of words; nothing else is
needed. Here we do (1 + 2) * 3
:
1
2
ADD
3
MUL
Apilar has operations like DUP
, ROT
which are used to manipulate the stack
itself. It adopts these from the Forth programming
language, so
information about Forth is a good place to look for more details. For instance,
see the section "Stack Maneuvers" in this Forth
manual.
Besides the stack, each processor also has a number of read/write heads. These
are like pointers into memory. The HEAD
instruction can be used to change the
current head. READ
reads at the position of the current head. WRITE
writes
from the stack into memory at the current head position. There are also
BACKWARD
and FORWARD
instructions to move the head up and down memory. The
JMP
and JMPIF
instructions also jump to the current head.
In an evolutionary simulation it's important all operations succeed; it
should never result in a crash. So, division by 0
is allowed, and results
in 0
.
When a stack overflow occurs, the stack is compacted - the bottom half of the stack is thrown away and the top half of the stack is the new stack.
What happens with an instruction if it tries to take a value from the stack and nothing is there depends on the instruction and is described there.
Do absolutely nothing. The one official NOOP, besides those of unrecognized bytes.
The instructions N0, N2, ... N8 place the respective number on the stack. So, N1 puts 1 on the stack, N2 puts 2 on the stack, etc.
Places a random number (under 256) on the stack.
Duplicate the top of the stack. So, if you have a stack:
[1]
then executing DUP
results in this stack:
[1 1]
Stack underflow means no op.
Like DUP
but for the two top values on the stack:
[1 2]
becomes:
[1 2 1 2]
Stack underflow, meaning less than 2 values on the stack, means no op.
Drops the top of the stack. It disappears forever:
[1 2]
becomes:
[1]
Stack underflow means no op.
Swaps the two top values on the stack:
[1 2]
where 2
is on top, it becomes:
[2 1]
where 1
is now on top.
Stack underflow, meaning less than 2 values on the stack, means no op.
Duplicates the second value just below the top.
Given:
[1 2]
it results in:
[1 2 1]
Stack underflow, meaning less than 2 values on the stack, means no op.
Takes the third value and puts it on top instead:
[1 2 3]
becomes:
[2 3 1]
So this pulled the 1
and put it on top.
Stack underflow, meaning less than 3 values on the stack, means no op.
All the operations are wrapping so if there's an number overflow or underflow,
it wraps around. So u64 MAX + 1
becomes 0
, and 0 - 1
becomes u64 MAX
.
Stack underflows in these operations means the operation proceeds but u64 MAX is popped from the stack instead.
Pops the two top numbers from the stack and places the sum back on top.
So:
[1 2]
Becomes:
[3]
Substracts top of the stack from the second value on the stack, so:
[4 3]
becomes:
[1]
Multiplies the two top values on the stack.
Divides the second value of the stack by the top value on the stack, so:
[6 3]
becomes:
[2]
Division is down to the floor of the nearest integer.
It's important operations can't fail, so division by 0
results in 0
:
[3 ]
Divides the second value of the stack by the top value and puts the remainder on the top of the stack, so that:
[5 2]
becomes:
[1]
In case of stack underflow, underflowing values popped are MAX u64 and then compared.
Pops the two top values on the stack. If they are equal, put 1
back
on the stack, otherwise '0'.
Pops the two top values on the stack. If the second value is greater than the
top, put 1
back on the stack, otherwise '0'.
Pops the two top values on the stack. If the second value is lesser than the
top, put 1
back on the stack, otherwise '0'.
In case of stack underflow, underflowing values popped are MAX u64.
Take the top of the stack. If it's greater than 0
, put 0
on the stack.
Otherwise if it's 0
, put 1
on the stack.
Take the two values on the stack. If they are both greater than 0
, put 1
on
the stack, otherwise 0
.
Take the two values on the stack. If either or both are greater than 0
, put
1
on the stack, otherwise 0
.
Take a number from the top of the stack, clamped to the maximum number of heads, and set the current head to this.
Take the address of this instruction and put it in the current head, overwriting the last value in the head.
Take a number from the top of the stack, clamped to the maximum number of heads. Take its address (if any) and set the current head to this. If it has no address, the current head remains unchanged.
Take a number from the top of the stack. Move the current head forward in memory by that amount. If the number is bigger than the maximum amount the head may move in one go, do not move the head.
Take a number from the top of the stack. Move the current head backward in memory by that amount. If the number is bigger than the maximum amount the head may move in one go, do not move the head.
Read from the position in memory pointed to by the current head, putting the result on top of the stack. If the current head is empty, no read occurs.
Take a value of the top of the stack and write it to the address indicated by the current head. If the current empty is empty, no write occurs.
Stack values are 64 bit unsigned numbers, so can be too large for memory, which
is unsigned bytes. If the value is greater than 255
, put 255
on instead.
These instructions modify the instruction pointer of the processor - which instruction in memory the processor is executing.
They use the current head as the jump destination.
Jump to the address in the current head. If no address exists in the current head, do not jump.
If the top of the stack is 0
, this is a no op. Otherwise, jump to the address
that's in the current head, unless it's empty, in which case do not jump.
These operations only operate once per cycle of instructions in which they were
initiated. The length of the cycle of instructions is configured by
instructions_per_update
.
Spawns a new processor in the address by the current head. If the current head is empty, no new processor is started.
Starting a lot of processors in a single cycle of instructions results in only
a a single processor to spawn. The last START
instruction determines the
address.
Destroys this processor after the cycle of instructions is finished.
These operations only operate once per cycle of instructions in which they were
initiated. The length of the cycle of instructions is configured by
instructions_per_update
.
Take a direction off the top of the stack. A direction is either 0 (north), 1 (east), 2 (south) or 3 (west) from this computer's location. Any numbers greater than 3 are taken as the remainder of 4, so are interpreted as a direction as well.
Now use the address indicated by the current head as the split point - just before this address the computer's memory is split into two. The first half remains in place. The second half goes into the neighboring location in the direction given, unless that is full, in which case nothing happens. If the current head is empty, no split happens either.
Processors remain in their parts. Any addresses in processors (the instruction pointer, heads) are automatically adjusted so they continue to point at the same instructions. If any head ends up pointing into the wrong half, it's set to empty.
Computer resources are divided equally between the two halves.
Take a direction off the top of the stack. A direction is either 0 (north), 1 (east), 2 (south) or 3 (west) from this computer's location. Any numbers greater than 3 are taken as the remainder of 4, so are interpreted as a direction as well.
This computer is then merged with the neighboring computer indicated by that direction, if there is one. The memory of the merged computer is added to the bottom of the computer that initiated the merging. The neighboring location is now empty. The resources are added together and the processors all run on the same computer.
Any addresses in processors (the instruction pointer, heads) are automatically adjusted, so they remain pointing at the same instructions.
If the maximum amount of processors is reached after a merge, the excess processors are eliminated.
These operations only operate once per cycle of instructions in which they were
initiated. The length of the cycle of instructions is configured by
instructions_per_update
.
Take the top of the stack as the amount of resources to eat, clamped by the maximum the computer can eat in one go. Free resources in the location are turned into bound resources in the computer.
If not enough free resources are available, all of them are eaten.
This is executed only once per update cycle. If multiple processors issue eat instructions in a single update cycle, take the maximum.
Take the top of the stack as the amount to grow, clamped by the maximum the computer is allowed to grow.
Memory is grown by the amount indicated, at the end.
Each memory value grown costs 1 bound resource. If not enough bound resources are available, use up all resources and grow the maximum possible.
This is executed only once per update cycle. If multiple processors issue grow instructions in a single update cycle, take the maximum.
Take the top of the stack as the amount to shrink, clamped by the maximum the computer is allowed to shrink.
Memory is shrunk by the amount indicated, at the end. Any processors pointing beyond the end die and are removed. Heads pointing beyond the end are reset to empty.
Each memory value shrunk creates 1 bound resource.
This is executed only once per update cycle. If multiple processors issue shrink instructions in a single update cycle, take the maximum.