Perl, 1116 1124 bytes, n=3, score=1124^(2/3) or approximately 108.1
Update: I've now verified that this works with n=3 via brute force (which took a couple of days); with a program this complex, it's hard to check for radiation-resistance by hand (and I made one mistake in a previous version, which is why the byte count increased). End update
I recommend redirecting stderr somewhere that you won't see it; this program produces a ton of warnings about dubious syntax even when you aren't deleting characters from it.
It's possible that the program can be shortened. Working on this is fairly painful, making it easy to miss possible micro-optimizations. I was mostly aiming to get the number of deletable characters as high as possible (because that's the really challenging part of the program), and treated the code-golf tiebreak as something that was nice to aim for but as something I wouldn't put ridiculous effort into optimizing (on the basis that it's very easy to break radiation resistance by accident).
The program
Note: there's a literal Control-_
character (ASCII 31) immediately before each of the four occurrences of -+
. I don't think it copy-and-pasted onto StackOverflow correctly, so you'll have to re-add it before running the program.
eval+<eval+<eval+<eval+(q(FoPqOlengthFoBBPP181XXVVVVJJJKKKNdoWchopJFtPDevalMODx4KNFrPIPA-MN-TUV-ZPINFsPIFoPqOI.Fo.IQNevalFoINevalIFsPZyI.Fr.IT-UPINsayDFtJqJFsKPZyPT-UFWrYrKD.DEEEEQDx6NsayDNDforB1..4YforB1..4NexitQNevalFo)=~y=A-Z=-+;-AZz-~=r)####>####>####>####>####>####>
;
;
;
;
eval+<eval+<eval+<eval+(q(FoPqOlengthFoBBPP181XXVVVVJJJKKKNdoWchopJFtPDevalMODx4KNFrPIPA-MN-TUV-ZPINFsPIFoPqOI.Fo.IQNevalFoINevalIFsPZyI.Fr.IT-UPINsayDFtJqJFsKPZyPT-UFWrYrKD.DEEEEQDx6NsayDNDforB1..4YforB1..4NexitQNevalFo)=~y=A-Z=-+;-AZz-~=r)####>####>####>####>####>####>
;
;
;
;
eval+<eval+<eval+<eval+(q(FoPqOlengthFoBBPP181XXVVVVJJJKKKNdoWchopJFtPDevalMODx4KNFrPIPA-MN-TUV-ZPINFsPIFoPqOI.Fo.IQNevalFoINevalIFsPZyI.Fr.IT-UPINsayDFtJqJFsKPZyPT-UFWrYrKD.DEEEEQDx6NsayDNDforB1..4YforB1..4NexitQNevalFo)=~y=A-Z=-+;-AZz-~=r)####>####>####>####>####>####>
;
;
;
;
eval+<eval+<eval+<eval+(q(FoPqOlengthFoBBPP181XXVVVVJJJKKKNdoWchopJFtPDevalMODx4KNFrPIPA-MN-TUV-ZPINFsPIFoPqOI.Fo.IQNevalFoINevalIFsPZyI.Fr.IT-UPINsayDFtJqJFsKPZyPT-UFWrYrKD.DEEEEQDx6NsayDNDforB1..4YforB1..4NexitQNevalFo)=~y=A-Z=-+;-AZz-~=r)####>####>####>####>####>####>
;
;
;
;
The explanation
This program is, quite clearly, made out of four identical smaller programs concatenated together. The basic idea is that each copy of the program will verify whether it's been damaged too badly to run or not; if it has been, it will do nothing (other than possibly spew warnings) and let the next copy run; if it hasn't been (i.e. no deletions, or the character that got deleted was one that makes no difference to the program's operation), it will do its quiny thing (printing out the full program's source code; this is a proper quine, with each part containing an encoding of the entire source code) and then exit (preventing any other undamaged copies from printing the source code out again and thus ruining the quine by printing too much text).
Each part is in turn made of two parts which are effectively functionally independent; an outside wrapper and some internal code. As such, we can consider them separately.
Outside wrapper
The outside wrapper is, basically, eval<+eval<+eval< ... >####>####...>###
(plus a bunch of semicolons and newlines whose purpose should be pretty obvious; it's to ensure that the parts of the program will remain separated regardless of if some of the semicolons, or the newlines before them, get deleted). This might look fairly simple, but it's subtle in a number of ways, and the reason I picked Perl for this challenge.
First, let's look at how the wrapper functions in an undamaged copy of the program. eval
parses as a built-in function, which takes one argument. Because an argument is expected, +
here is a unary +
(which will be very familiar to Perl golfers by now; they come in useful surprisingly often). We're still expecting an argument (we just saw a unary operator), so the <
that comes next is interpreted as the start of the <>
operator (which takes no prefix or postfix arguments, and thus can be used in operand position).
<>
is a fairly weird operator. Its usual purpose is to read filehandles, and you place the filehandle name inside the angle brackets. Alternatively, if the expression isn't valid as a filehandle name, it does globbing (basically, the same process that UNIX shells use to translate text entered by the user to a sequence of command-line arguments; much older versions of Perl actually used the shell for this, but nowadays Perl handles the globbing internally). The intended use, therefore, is along the lines of <*.c>
, which would typically return a list like ("foo.c", "bar.c")
. In a scalar context (such as the argument to eval
), it just returns the first entry it finds the first time it's run (the equivalent of the first argument), and would return other entries on hypothetical future runs that never happen.
Now, shells often handle command-line arguments; if you give something like -r
with no arguments, it'll just be passed to the program verbatim, regardless of whether there's a file with that name or not. Perl acts the same way, so as long as we ensure that there are no characters that are special to the shell or to Perl between the <
and the matching >
, we can effectively use this like a really awkward form of string literal. Even better, Perl's parser for quote-like operators has a compulsive tendency to match brackets even in contexts like this one where it makes no sense, so we can nest <>
safely (which is the discovery needed for this program to be possible). The major downside of all these nested <>
is that escaping the contents of the <>
is almost impossible; there seem to be two layers of unescaping with each <>
, so to escape something on the inside of all three, it needs to be preceded with 63 backslashes. I decided that even though code size is only a secondary consideration in this problem, it almost certainly wasn't worth paying this sort of penalty to my score, so I just decided to write the rest of the program without using the offending characters.
So what happens if parts of the wrapper get deleted?
- Deletions in the word
eval
cause it to turn into a bareword, a string with no meaning. Perl doesn't like these, but it treats them as though they were surrounded with quotes; thus eal<+eval<+...
is interpreted as "eal" < +eval<+...
. This has no effect on the program's operation, because it's basically just taking the result from the heavily nested evals (which we don't use anyway), casting it to an integer, and doing some pointless comparisons on it. (This sort of thing causes a lot of warning spam as it's clearly not a useful thing to do under normal circumstances; we're just using it to absorb deletions.) This changes the number of closing angle brackets we need (because the opening bracket is now being interpreted as a comparison operator instead), but the chain of comments at the end ensures the string will end safely no matter how many times it's nested. (There are more #
signs than strictly needed here; I wrote it like I did in order to make the program more compressible, letting me use less data to store the quine.)
- If a
<
gets deleted, the code now parses as eval(eval<...>)
. The secondary, outside eval
has no effect, because the programs we're evaluating don't return anything that has any real effects as a program (if they return normally at all, it's normally a null string or a bareword; more commonly they return via exception, which causes eval
to return a null string, or use exit
to avoid returning at all).
- If a
+
gets deleted, this has no immediate effect if the adjacent code is intact; unary +
has no effect on the program. (The reason the original +
s are there is to help repair damage; they increase the number of situations in which <
is interpreted as a unary <>
rather than as a relational operator, meaning you need more deletions to produce an invalid program.)
The wrapper can be damaged with enough deletions, but you need to do a series of deletions in order to produce something that doesn't parse. With four deletions, you can do this:
eal<evl<eval+<...
and in Perl, the relational operator <
is nonassociative, and thus you get a syntax error (the same one you'd get with 1<2<3
). As such, the cap for the program as written is n=3. Adding more unary +
s seems like a promising way to increase it, but as that'd make it increasingly likely that the inside of the wrapper could break too, verifying that the new version of the program works could be very difficult.
The reason the wrapper is so valuable is that eval
in Perl catches exceptions, such as (for example) the exception that you get when you try to compile a syntax error. Because this eval
is of a string literal, the compile of the string happens at runtime, and if the literal fails to compile, the resulting exception gets caught. This causes eval
to return a null string and set the error indicator $@
, but we never check either (except by occasionally executing the returned null string in a few mutated versions of the program). Crucially, this means that if something should happen to the code inside the wrapper, causing a syntax error, then the wrapper will just cause the code to do nothing instead (and the program will keep executing in an attempt to find an undamaged copy of itself). Therefore, the inside code doesn't have to be nearly as radiation-proof as the wrapper; all that we care about is that if damaged, it'll either act identically to the nondamaged version of the program, or else it'll crash (allowing eval
to catch the exception and continue) or exit normally without printing anything.
Inside the wrapper
The code inside the wrapper, fundamentally, looks like this (again, there's a Control-_
that Stack Exchange won't show immediately before the -+
):
eval+(q(...)=~y=A-Z=-+;-AZz-~=r)
This code is written entirely with glob-safe characters, and its purpose is to add a new alphabet of punctuation marks that make it possible to write a real program, via transliterating and evaluating a string literal (we can't use '
or "
as our quote marks, but q(
…)
is also a valid way to form a string in Perl). (The reason for the unprintable character is that we need to transliterate something into the space character without a literal space character in the program; thus we form a range starting at ASCII 31, and catch the space as the second element of the range.) Obviously, if we're producing some characters via transliteration, we have to sacrifice characters to transliterate them from, but uppercase letters aren't very useful and it's much easier to write without access to those than without access to punctuation marks.
Here's the alphabet of punctuation marks that become available as a result of the glob (the upper line shows the encoding, the lower line the character it encodes):
BCDEFGHIJKLMNOPQRSTUVWXYZ
!"#$%&'()*+;<=>?@AZz{|}~
Most notably, we have a bunch of punctuation marks that aren't glob-safe but are useful in writing Perl programs, together with the space character. I also saved two uppercase letters, the literal A
and Z
(which encode not to themselves, but to T
and U
, because A
was needed as an upper as well as a lower range endpoint); this allows us to write the transliteration instruction itself using the new encoded character set (although uppercase letters aren't that useful, they're useful in specifying changes to the uppercase letters). The most notable characters that we don't have available are [
, \
, and ]
, but none are needed (when I needed a newline in the output, I produced it using the implicit newline from say
rather than needing to write \n
; chr 10
would also have worked but is more verbose).
As usual, we need to worry about what happens if the inside of the wrapper gets damaged outside the string literal. A corrupted eval
will prevent anything running; we're fine with that. If the quote marks get damaged, the inside of the string isn't valid Perl, and thus the wrapper will catch it (and the numerous subtractions on strings mean that even if you could make it valid Perl, it'd do nothing, which is an acceptable outcome). Damage to the transliteration, if it isn't a syntax error, will mangle the string being evaluated, typically causing it to become a syntax error; I'm not 100% sure there are no cases in which this breaks, but I'm brute-forcing it at the moment to make sure, and it should be easy enough to fix if there are.
The encoded program
Looking inside the string literal, reversing the encoding I used, and adding whitespace to make it more readable, we get this (again, imagine a control-underscore before the -+
, which is encoded as A
):
$o=q<
length$o ==181 || zzzz((()));
do {
chop ($t = "eval+<"x4);
$r = '=-+;-AZz-~=';
$s = '$o=q<' . $o . '>;eval$o';
eval '$s=~y' . $r . 'A-Z=';
say "$t(q($s)=~y=A-Z${r}r)" . "####>"x6;
say ";" for 1..4
} for 1..4;
exit>;
eval $o
People who are used to quines will recognise this general structure. The most crucial part is at the start, where we verify that $o is undamaged; if characters have been deleted, its length won't match 181
, so we run zzzz((()))
which, if it isn't a syntax error due to unmatched brackets, will be a runtime error even if you delete any three characters, because none of zzzz
, zzz
, zz
, and z
is a function, and there's no way to prevent it parsing as a function other than deleting (((
and causing an obvous syntax error. The check itself is also immune to damage; the ||
can be damaged to |
but that will cause the zzzz((()))
call to run unconditionally; damaging variables or constants will cause a mismatch because you're comparing one of 0
, 180
, 179
, 178
for equality to some subset of the digits of 181
; and removing one =
will cause a parse failure, and two =
to inevitably cause the LHS to evaluate to either integer 0 or a null string, both of which are falsey.
Update: This check was slightly wrong in a preceding version of the program, so I had to edit it to fix the issue. The previous version looked like this after the decode:
length$o==179||zzzz((()))
and it was possible to delete the first three punctuation marks to get this:
lengtho179||zzz((()))
lengtho179
, being a bareword, is truthy and thus breaks the check. I fixed this by adding an extra two B
characters (which encode space characters), meaning the latest version of the quine does this:
length$o ==181||zzzz((()))
Now it's impossible to hide both the =
signs and the $
sign without producing a syntax error. (I had to add two spaces rather than one because a length of 180
would put a literal 0
character into the source, which could be abused in this context to integer-compare zero with a bareword, which succeeds.) End update
Once the length check passes, we know the copy is undamaged, at least in terms of character deletions from it, so it's all just straightforward quining from there (substitutions of punctuation marks due to a corrupted decoding table wouldn't be caught with this check, but I've already verified via brute-forcing that no three deletions from only the decoding table break the quine; presumably most of them cause syntax errors). We have $o
in a variable already, so all we need to do is hardcode the outside wrappers (with some small degree of compression; I didn't skip out the code-golf part of the question entirely). One trick is that we store the bulk of the encoding table in $r
; we can either print it literally in order to generate the encoding table section of the inside wrapper, or concatenate some code around it and eval
it in order to run the decoding process in reverse (allowing us to figure out what the encoded version of $o is, having only the decoded version available at this point).
Finally, if we were an intact copy and thus could output the entire original program, we call exit
in order to prevent the other copies also trying to print the program out.
Verification script
Not very pretty, but posting it because someone asked. I ran this several times with a variety of settings (typically changing $min
and $max
to check for various areas of interest); it wasn't a fully automated process. It has a tendency to stop running due to heavy CPU load elsewhere; when this happened, I just changed $min
to the first value of $x
that wasn't fully checked and continued running the script (thus ensuring that all the programs in the range got checked eventually). I only checked deletions from the first copy of the program, because it's fairly obvious that deletions from the other copies can't do more.
use 5.010;
use IPC::Run qw/run/;
undef $/;
my $program = <>;
my $min = 1;
my $max = (length $program) / 4 - 3;
for my $x ($min .. $max) {
for my $y ($x .. $max) {
for my $z ($y .. $max) {
print "$x, $y, $z\n";
my $p = $program;
substr $p, $x, 1, "";
substr $p, $y, 1, "";
substr $p, $z, 1, "";
alarm 4;
run [$^X, '-M5.010'], '<', \$p, '>', \my $out, '2>', \my $err;
if ($out ne $program) {
print "Failed deleting at $x, $y, $z\n";
print "Output: {{{\n$out}}}\n";
exit;
}
}
}
}
say "All OK!";
Subleq
. I think it would be ideal for this kind of challenge! \$\endgroup\$