Z80Golf, 53 36 34 bytes
-16 bytes thanks to @Lynn
-2 byte thanks to @Neil
Since this is just Z80 machine code, there is a lot of unprintables in this one, so have a xxd -r
-reversible hexdump:
00000000: ddb6 2120 10dd b615 280c 003e 62ff 3e65 ..! ....(..>b.>e
00000010: ffff 3e70 ff76 003e 62ff 3e65 ffff 3e70 ..>p.v.>b.>e..>p
00000020: ff76 .v
Try it online! (exhaustive tester in Python)
Explanation
z80golf is Anarchy Golf's hypothetical Z80 machine, where call $8000
is a putchar, call $8003
is a getchar, halt
makes the interpreter exit, your program is placed at $0000
, and all other memory is filled with zeroes. Making programs radiation-proof in assembly is pretty hard, but a generically useful technique is using one-byte idempotent instructions. For example,
or c ; b1 ; a = a | c
is just one byte, and a | c | c == a | c
, so it can be made radiation-proof by just repeating the instruction. On the Z80, an 8-bit immediate load is two bytes (where the immediate is in the second byte), so you can load some values into registers reliably too. This is what I originally did at the beginning of the program, so you can analyze the longer variants I archived at the bottom of the answer, but then I realized that there is a simpler way.
The program consists of two independent payloads, where one of them could have been damaged by radiation. I check whether a byte was removed, and whether the removed byte was before the second copy of the payload, by checking the values of some absolute memory addresses.
First, we need to exit if no radiation was observed:
or a, (ix+endbyte) ; dd b6 21 ; a |= memory[ix+0x0021]
jr nz, midbyte ; 20 10 ; jump to a halt instruction if not zero
If any byte was removed, then all the bytes will shift and $0020
will contain the last 76
, so $0021
will be a zero. We can afford radiating the beginning of the program, even though there is virtually no redundancy:
- If the jump offset
$10
is removed, then radiation will correctly be detected, the jump will not be taken, and the offset will not matter. The first byte of the next instruction will be consumed, but since it's designed to be resistant to byte removals, this doesn't matter.
- If the jump opcode
$20
is removed, then the jump offset $10
will decode as djnz $ffe4
(consuming the next instruction byte as the offset -- see above), which is a loop instruction -- decrement B, and jump if the result is not zero. Because ffe4-ffff
is filled with zeroes (nop
s), and the program counter wraps around, this will run the beginning of the program 256 times, and then finally continue. I am amazed this works.
- Removing the
$dd
will make the rest of the snippet decode as or (hl) / ld ($1020), hl
, and then slide into the next part of the program. The or
will not change any important registers, and because HL is zero at this point, the write will also cancel out.
- Removing the
$b6
will make the rest decode as ld ($1020), ix
and proceed as above.
- Removing the
$21
will make the decoder eat the $20
, triggering the djnz
behavior.
Note that using or a, (ix+*)
saves two bytes over ld a, (**) / and a / and a
thanks to the integrated check for zero.
We now need to decide which of the two copies of the payload to execute:
or (ix+midbyte) ; dd b6 15
jr z, otherimpl ; 28 0c
nop ; 00
; first payload
ld a, 'b' ; 3e 62
rst $0038 ; ff
ld a, 'e' ; 3e 65
rst $0038 ; ff
rst $0038 ; ff
ld a, 'p' ; 3e 70
rst $0038 ; ff
midbyte:
halt ; 76
otherimpl:
nop ; 00
ld a, 'b' ; 3e 62
; ... ; ...
rst $0038 ; ff
endbyte:
halt ; 76
The two copies are separated by a nop, since a relative jump is used to choose between them, and radiation could have shifted the program in a way that would make the jump skip the first byte after the destination. Additionally, the nop is encoded as a zero, which makes it easy to detect shifted bytes. Note that it doesn't matter which payload is chosen if the switch itself is corrupted, because then both copies are safe. Let's make sure that it will not jump into uninitialized memory, though:
- Deleting
$dd
will make the next two bytes decode as or (hl) / dec d
. Clobbers D. No big deal.
- Deleting
$b6
will create an undocumented longer encoding for dec d
. Same as above.
- Deleting
$15
will read the $28
instead as the offset, and execution will proceed at the $0c
, as below.
- When
$28
disappears, the $0c
is decoded as inc c
. The payload does not care about c
.
- Deleting
$0c
- that's what the nop is for. Otherwise, the first byte of the payload would have been read as the jump offset, and the program would jump into uninitialized memory.
The payload itself is pretty simple. I think the small size of the string makes this approach smaller than a loop, and it's easier to make position-independent this way. The e
in beep
repeats, so I can shave off one ld a
. Also, because all memory between $0038
and $8000
is zeroed, I can fall through it and use a shorter rst
variant of the call
instruction, which only works for $0
, $8
, $10
and so on, up to $38
.
Older approaches
64 bytes
00000000: 2e3f 3f2e 3f3f 7e7e a7a7 201f 1e2b 2b1e .??.??~~.. ..++.
00000010: 2b2b 6b00 7ea7 2814 003e 62cd 0080 3e65 ++k.~.(..>b...>e
00000020: cd00 80cd 0080 3e70 cd00 8076 003e 62cd ......>p...v.>b.
00000030: 0080 3e65 cd00 80cd 0080 3e70 cd00 8076 ..>e......>p...v
58 bytes
00000000: 2e39 392e 3939 7e7e a7a7 2019 3a25 00a7 .99.99~~.. .:%..
00000010: 2814 003e 62cd 0080 3e65 cd00 80cd 0080 (..>b...>e......
00000020: 3e70 cd00 8076 003e 62cd 0080 3e65 cd00 >p...v.>b...>e..
00000030: 80cd 0080 3e70 cd00 8076 ....>p...v
53 bytes
This one has an explanation in the edit history, but it's not too different.
00000000: 3a34 00a7 a720 193a 2000 a728 1400 3e62 :4... .: ..(..>b
00000010: cd00 803e 65cd 0080 cd00 803e 70cd 0080 ...>e......>p...
00000020: 7600 3e62 cd00 803e 65cd 0080 cd00 803e v.>b...>e......>
00000030: 70cd 0080 76 p...v
What if: any non-empty output was fine instead of beep
1 byte
v
halt
s the program normally, but if radiation removes it, then the memory will be full of zeroes, making $8000
execute an infinite number of times, printing a lot of null bytes.
Related, Related. – Post Rock Garf Hunter – 2018-08-04T03:18:29.257
*Geiger-Muller Tube connected to a counter ;) – Beta Decay – 2018-08-04T11:30:48.010
7Can we use the BEL control character to output an actual beep? – Jo King – 2018-08-05T06:47:04.547
2@JoKing I toyed with the idea, it is amusing, but I have to say no. It is too substantially different. – Post Rock Garf Hunter – 2018-08-05T06:48:11.543
And much easier as well. (1 byte vs 4 byte) – user202729 – 2018-08-06T13:11:58.417
2I want to see a solution in Retina. – mbomb007 – 2018-08-07T20:28:23.130
3
I'm trying to figure out how to do this in SMBF... but the only way to compare two cells involves changing them. And in SMBF, the cells you need to check are the cells the program is currently running on. So it's like the Heisenberg Uncertainty Principle. So you have to determine if anything changed using only control flow.
– mbomb007 – 2018-08-07T21:39:56.203