Introduction
Recently we released an article [] that discusses some suspiciously life-like phenomena that manifest themselves in a few simple computation substrates. Among the experiments conducted, the majority utilized minimalist artificial languages as substrates. However, one experiment stood out by employing a renowned instruction set from the Z80 CPU.
In this article we will take a deep dive into the heart of the 2D grid of 16-byte programs where a curious tapestry of self-replicating patterns emerges, resembling the building blocks of a digital ecosystem. These replicators compete, collaborate, and evolve, echoing the complexities of biological life. We witness the origins and evolution of life-like phenomena in this fascinating experiment that pays homage to the Z80, a legendary CPU from the golden age of computing.
Let’s set the stage for the experiment first. Imagine a 2D grid of cells, containing 16-byte memory buffers, initially filled with random noise.
- At every step we select a number of random non-overlapping pairs of adjacent cells and concatenate each pair into a single 32-byte tape.
- Each tape is attached to a freshly reset z80 CPU and executed for 128 instruction steps with all read/write memory requests wrapped into 32 byte memory.
- Resulting tapes are split into halves and put back into corresponding cells of the grid.
- Random mutations are applied to some bytes of the grid.
- The process repeats.
That’s the whole setup. It may look like a sort of an evolutionary algorithm but with no explicit replication or fitness function involved. In spite of its simplicity we will observe some fascinating phenomena to arise and will try to analyze the mechanics of some processes. In order to do this we have to say a few words about the Z80 CPU and its instruction set.
Primordial soup and the first replicator
Above, we witness the what we might regard as pre-life. At first, the tape is relatively uninteresting, with no particular instructions that would appear to be capable of replicating anything. However, at step 1, we execute instruction 0xD7 from position 0 in memory. This is an innocuous instruction, writing the current value of PC to the stack, and then changing the value of PC to 16. Recall that PC in our setup "wraps around" and is always utilized modolu 32, but stored in full. The PC at this point is just 1, so the value pushed to the stack is 0x001, occupying two bytes of the stack. It just so happens that the instruction 0x01 corresponds to "NN -> BC"; loading NN (the next two byte after the current PC byte) into the registers B and C. This turns out to be a no-op, as these registers are already initialized to zero, and the only change is that we now have "NN -> BC" at the end of memory, and we wrap back around to the first index.
We are now back at the instruction 0xD7, but recall that PC was also reset to be 0x0A (16) when we last ran this instruction. Thus, our PC has now incremented by 17 instructions since then, and this time instead it writes a 0x0021 instruction to stack, which corresponds to "NN -> HL". This instruction then copies NN, consisting of (0x2100) to HL, but has no other effect, and thus the program continues its march to fill the memory with 0x21 instructions.
In fact, if we look at this tape, we notice that the first tape doesn't execute its instructions at all, and if it were to be matched with any other tape that didn't influence memory or control flow, it would also fill it with 0x21 followed by 0x00 (except one 0x01 followed by 0x00 at the end of the memory).
However it does not yet self replicate - it doesn't write the magic instruction 0xD7 to the second tape, or anywhere else in the first tape.
Here we start with a tape where the second half is semi-filled with our 0x21 instructions, and a single 0xD7 at the 16th byte - the product of one of the previous examples. The first tape consists of "NN -> SP" instructions, but these are essentially NOPs. As soon as the PC reaches the 0xD7, we again see it start to fill the stack with the two bytes, 0x1100, just as in the last example, but filling it with a different instruction since the PC at 0xD7 is on byte 17.
We now start to see the the light green entries we've seen before appear. Note that at this point, we still aren't seeing a 'replicator' per se.
Here, we see the remnants of an Example 2 replicator in the second tape, with the addition of a 0xCDC911 instruction at byte 19! This instruction does something similar to 0xD7 - it copies the current PC value (+3) to the stack. This happens to be 0x11D5, which ends up in the stack as 0xD511, or the two instructions "push DE to stack" and "push NN to DE". It then jumps PC to 0xC911, which happens to be byte 10. Nothing interesting happens between byte 10 and byte 19, where it again writes 0xD511 to stack. It continues to do this until it ovewrites the 0xCD instruction itself at step N. At this point, we might imagine it stops writing 0xD511 to the stack as the previous pre-life replicators did, but instead something magical happens! The PC is now at byte 22, which is one of the 0xD5 instructions. This writes the value of registers DE to stack, which happen to be empty - so two NOPs are written. However, the next instruction that follows is 0x11, which does copies the next two bytes after PC to DE. These of course are 0xD511. It now gets to a 0x11 instruction again, and this time 0xD511 are in the DE registers, which it then writes to stack! We have a replicator! It contains the full code to copy itself (0xD511) to registers, then back to memory on the stack, thereby taking over the whole tape. This is the first species of replicator we observe!
Here, we have a first tape filled with the "green" replicator we previously discovered. The second tape is largely filled junk, and the first tape diligently copies itself to the second tape until step 11, when the PC is at the instruction 0x34. This proceeds to increment the memory pointed to by HL, by 1. Since HL is 0x0000, this simply increments the first byte of memory by one, turning the "nn->DE" into a "A -> (DE)". The replication continues, and the PC loops around, now executing "A -> (DE)", which stores the contents of A (0xFF) into memory at (DE). This puts a 0xFF byte at byte 22 in the memory. Again, the replication continues until the PC reaches this 0XFF instruction. This instruction now writes PC+1 to stack, and continues from PC=56. This results in writing 0xB618 to stack! The first signs of corrupting a replicator! However, 0xB618 is innocuous, but again we wrap around and reach 0xFF. This time PC is 0x55, and thus 0x5600 is written to stack. Now, things start to unravel! Our typical replication instruction "nn->DE" now instead copies 0x0056 to registers DE, and DE push writes this back to stack! Nonetheless, the replicator tries going back to replicating, putting 0x11D5 back into registers DE. Now we continue, and hit instruction 0x56. This puts memory at (HL) into register D. HL happens to be pointing at 0x0000, so we overwrite 0x11 with 0x18D5. This then gets written to stack again, and we continue to wrap back around to byte 0. This is a jump forward by 18 instruction, so we again get to the middle of a replicator (DE push), and again write the 0x18D5 instructions to stack. This then continues until most of the replicator is overwritten with the new 0x18D5 instructions.