Historical Note:
This report has been getting a lot more attention recently as a result
of it being discovered by Yahoo!.
In light of that, I figured I would provide just a little context for it.
As a COOP student at the University of
Waterloo, I was required to write no fewer than four work term reports
over the course of six internships. This was the fourth. It's important
to realize that the reports are graded almost exclusively for form rather
than content. An English major is assigned to read the report for grammar,
spelling and punctuation. As far as they're concerned, I could be writing
about crabgrass. Knowing my intended audience, I made some concessions
towards a reader without extensive programming experience by including, for
example, a glossary.
What you're about to read was written circa 1994. I re-read it recently.
The writing is pretty rough (the bar was low for work-term reports, a fact
that I certainly didn't call into question) but the ideas are sound. Hope
you enjoy it.
I'm not exactly sure why Yahoo! put this page under its "jokes and humour"
category. I guess it's not funny ha ha, just funny peculiar...
The C programs no longer conform to the current standard of the C language (as of 2025), for two reasons:
#include <stdio.h> should be included at the beginning, for
the printf or putchar
function to be defined.
main program should be declared to be of type int.
-std=c89 is provided.
char*f="...". Trying to modify parts of the string to
which f points leads to a memory access violation error
on modern architectures.
Consequently, this declaration has been replaced by
char f[]="..." and the program has been adapted.
This report examines the problem of writing a self-documenting program: a program that, when run, produces itself as output. The problem is examined from the point of view of self-reference, the property a self-documenting program must exhibit. The report proceeds from early programs that fail to work correctly, through successively sophisticated programs which approach a solution, to working self-documenting programs. Then it steps back a bit and shows how some programs can seem to cheat and still fit the definition of a self-documenting program, suggesting improvements to that definition. At each step, the report addresses how the given programs demonstrate the subtle relationship between computer programming and self-reference.
This report presents a novel intellectual challenge: the problem of writing a self-documenting program. It then attempts to solve this problem, bringing to light along the way a series of pitfalls and curiosities that underlie our understanding of context and meaning within formal systems such as computer languages. It demonstrates that the problem of self-documenting code probes at the core of questions about self-reference and the use/mention distinction.
As this report deals with issues relating to programming languages in general and C in particular, the reader is assumed to have at least a basic understanding of computer programming. Some experience with various languages, especially C, would certainly be an asset. Since the report examines the problem from the point of view of computer languages, knowledge of specific hardware platforms and operating systems is not required. However, it should be possible for the novice programmer or non-programmer to follow the main arguments presented here.
The body of this report contains six main sections. The first and second sections introduce the concepts of self-reference and self-documenting programs. They set the stage for the challenge by defining the key concepts and showing why the problem of writing a self-documenting program is non-trivial. The next three sections build up from initial attempts at self-documenting programs that fail to more sophisticated attempts that still fail, to actual working programs. At each step, a discussion of the attempt shows what needs to be revised or improved to proceed onward. The final section shows how we can stretch our initial definition of a self-documenting program to produce solutions that seem unsatisfying, and how we can appropriately modify the definition.
Throughout the report, the reader will find terms or phrases which are italicized and underlined, like this. This indicates that the word could be unfamiliar to the reader, and that a short explanation is given in the glossary, which appears at the end of the report. Clicking on that word will take the reader directly to that explanation.
Pieces of C source code appear throughout the report. In some cases, lines of source
were too long to fit on a single line. In these cases, a bold ampersand
'&' has been used to indicate that in the source code, the line
containing the ampersand and the line immediately following it should be concatenated
and treated as a single line.
Self-reference occurs whenever a system loops back on itself and describes some aspect of its own form or structure. This mysterious phenomenon happens all the time. Whenever someone says 'I' or 'me', whenever a movie talks about movies or a television program talks about television programs, self-reference is being demonstrated.
Self-reference has some important applications, too. The proof of Gödel's Incompleteness Theorem, one of the most far-reaching concepts in mathematics and logic, relies on a formal system being able to describe its own structure. Turing used the idea of a self-referential computer program to provide a proof to the Halting Problem, an important question of computer science.
An important aspect of self-reference is the differentiation between 'use' and 'mention'. To use something is to actually employ its meaning, to get at its deeper content. To mention something, on the other hand, is merely to refer to its form, its outer appearance. For instance, consider the following two sentences:
Intuitively, both sentences assert true propositions. Yet despite the nearly identical choice of words, they make these assertions in entirely different ways. The distinction is in the words "this sentence". In the first sentence, "this sentence" is being used. It is actually being employed, and the reader forms a mental association between the words and their meaning, namely the sentence as a whole. On the other hand, in the second sentence "this sentence" is being mentioned. The content of the two words is unimportant, because there is no attempt to derive meaning from them. All that matters is that the phrase "this sentence" does indeed have two words.
Note that people are able to see this distinction automatically, without thought. Although this sentence does not occur at all in the previous paragraph, "this sentence" occurs many times. And each time, the reader has no problem understanding that "this sentence" is being mentioned rather than used. As another example, when reading this sentence, the reader has no trouble distinguishing between the two contexts of "this sentence".
struct link {
int data;
struct link *next;
};
This fragment of code declares a new type - a template from which to construct
objects later. In this case, the type is declared as an aggregate which
contains two subobjects: some piece of numerical information, called
data, and a pointer to another link structure, called
next.
This simple structure embodies the use/mention distinction. If I declare an
object of type struct link, the computer will set aside memory to
hold my new object. This block of memory will behave according to its
definition, and allow access to two subobjects of the appropriate types.
This new object is a use of a link. But notice how the second subobject
is defined as a pointer to another link. A pointer is the encoding of a location
in memory. It does not make sense for next to be of type
struct link; that would make the type infinitely large! So
next is not a link itself, but indicates where another
such object can be found. It is a mention of a link.
The idea is simple: write a computer program that, when run, produces itself as output. For now, we will use this as the definition of a self-documenting program, although later we will see how we can stretch this definition to produce examples that seem unsatisfactory.
Because we are not dealing with complex data or abstractions, we would traditionally employ a top-down design to write this program. The idea behind top-down design is to begin with a very high level description of the solution, and iteratively refine this description until you arrive at the level of actual program statements. So, let us attempt this with the given problem. We begin with a simple (if somewhat ambitious) program definition, with the section to be refined written in italics:
The next step would be to refine the "print myself out" section of the program. But with what? There is no straightforward way to insert code into this section of the program, because whatever code gets used in the end, it has to perform a double duty. It has to not only function properly, that is to say actually print itself out, it also has to specify what needs to be printed out, namely itself! In a sense, the code that gets inserted here needs to anticipate its own structure before it even gets written. It has to be the program and the description of the program. It has to be the use and the mention. It has to form a closed loop on itself. In this sense, you could never just start writing such a program. You would have to compose the entire program at once in a conceptual leap.main() {print myself out.}
When first faced with the problem of writing a self-documenting program, the initial attempts at solutions tend to lead to dead ends. Still, they provide insight into the structure of a working solution. Two such ideas are examined here.
main() {
So obviously, the second line will have to read:
printf( "main() {\n" );
Namely, a line that prints out the first line. Now, we have to add a line that
prints out the second line. That will look like this:
printf( "printf( \"main() {\\n\" );\n" );
And, of course, we need to include a line to print that line out:
printf( "printf( \"printf( \\\"main() {\\\\n\\\" );\\n\" );\n" );
There is an obvious difficulty here. Any program that attempts to solve the
problem this way will have to be infinitely long. As soon as the program
contains any instructions at all, it will have to contain instructions to
print those instructions, and a loop will begin. Brute force fails because we
continuously need to refer to previously done work; we can never catch up.
To accomplish this, we first define a string
f which contains a mention of the whole program. Then we can print the program from
within its own code by printing f - a mention, rather than a use, of the code. Based
on this idea, we get the following program:
When run, this program produces the following output:
char*f="main(){printf(f);}";
main(){printf(f);}
Unfortunately, we do not yet have a solution, because this is not the complete
program! This idea failed to take into account the extra line that was added
at the start to define the mention of the program. Our solution would have to
output a copy of this line as well. But where do we obtain a mention of this
line? We almost have one already! Except for the initial ' main(){printf(f);}char*f="'
and the closing '";', the line is a mention of itself. So we may
be very close to a solution indeed. The critical step is to print out the same
string twice: once as the mention, and once as the use.
We now proceed to a proto-attempt at a self-documenting program which is very close to a solution. This example is correct except for the fine details of the C language.
Making use of the ideas from the Name That Program example, we obtain the following program:
Failure I
The output from this program is:
char*f="main(){printf(f);printf(f);}";main(){printf(f);printf(f);}
which is very nearly correct. All that is missing is the code that defines the string
f. The intuitive way to put the finishing touches on this program is to rewrite it as
follows:
main(){printf(f);printf(f);}main(){printf(f);printf(f);}
Failure II
Namely, to add the necessary code to print out the text which delimits the
mention of the program and reformat the output so it runs over two lines.
In theory, there is nothing wrong with this solution. In practice, it fails to
even compile! As it turns out, the
problem reduces to the way the C language handles the use/mention distinction internally.
char*f="main(){printf("char*f="");printf(f);printf("";\n");printf(f);}";
main(){printf("char*f="");printf(f);printf("";\n");printf(f);}
In C, certain characters are treated differently depending on whether they are being used (as part of a programming instruction) or merely mentioned (as part of a string literal). Firstly, the double quote character " cannot be directly inserted into a string, because C uses it to delimit strings. There is no easy way of knowing whether a given double quote was meant as a use or a mention. C resolves this conflict by forcing the programmer to state which sense of " is desired. To mention a double quote from within a string, you must escape it by preceding it with a backslash. C understands backslashes as a special means of mentioning characters under any context. (Incidentally, to avoid taking the backslash character out of context when it is being used explicitly, one places a second backslash in front of it). Also, one cannot insert a newline character directly into a string. That, too, must be escaped, and appears in strings as \n.
Based on this discussion, one might think that the next level of refinement would simply be to escape all the double quotes and newlines inside strings. If we were to perform this transformation on Failure II above, we would obtain a new program, Failure III (the code for Failure III can be found in Appendix A). Unfortunately, when Failure III is compiled and run, its output is not itself, but Failure II!
The reason for this is that escaping a character in C introduces no new information in the final program; it is a lexical convenience, a way for the compiler to know how to treat special characters in its input. The backslashes are thrown away internally, so Failure III outputs a copy of itself without escaped characters - namely, Failure II.
The C language itself is beginning to complicate the creation of a self-documenting program with its use of escaped characters. Of course, this should not be taken as a flaw in the language; in any other application, the approach taken by C is suitable and even desirable. There simply is no way to discern a use of a double quote from a mention without additional information. That information comes in the form of the optional backslash. In the application of self-documenting code, however, it looks like another conceptual leap is in order.
That conceptual leap comes in the form of finding an alternate way to express these escaped characters. Obviously, the program is going to have to find some way to output a double quote - they will, by necessity, appear in the source code, after all! But there can be no double quotes in the actual instructions, because if a double quote appears in the program proper, it will also have to appear in the description of the program, in which case it will have to be escaped. As we have seen, this is forbidden.
Fortunately, there are other ways to output this character. The easiest is
the statement putchar(34);. 34 is the standard
ASCII code for the double quote. The
putchar function translates the code into the correct
character and outputs the double quote to the screen. Similarly,
putchar(10); will output a newline character.
This leaves one more step. We still need to output 'char*f=' and
';' on either side of the string. We could use a series of statements
of the form:
and so on (the resulting code, Self 0, can be found in
Appendix A), but a slightly more elegant solution is to incorporate these extra
characters into the string used to describe the program. We can, at runtime,
selectively print sections of that string corresponding to different parts of
the program, by printing from an offset into the string and temporarily placing
an end-of-string marker at the end of the section we wish to print.
putchar('c');putchar('h');putchar('a');putchar('r');...
Combining these techniques generates the first working self-documenting program:
Self I
char f[]="char f[]=;main(){f[9]=0;printf(f);putchar(34);f[9]=';';printf(f);&
putchar(34);f[10]=0;printf(&f[9]);f[10]='m';putchar(10);printf(&f[10]);putchar(10);}";
main(){f[9]=0;printf(f);putchar(34);f[9]=';';printf(f);putchar(34);&
f[10]=0;printf(&f[9]);f[10]='m';putchar(10);printf(&f[10]);putchar(10);}
When Self I is compiled and run, it outputs an exact duplicate of itself, and is therefore a successful self-documenting program. Unfortunately, Self I is cryptic and unreadable, even by C standards! The lines are long, and spacing is minimal. Furthermore, there is no practical way to reformat this program while preserving its self-reference. We cannot break the definition of string f over several lines, because as we have seen, newlines must be escaped within a string. If we inserted a newline directly into f, the program would fail to compile.
That is not to say there is no such thing as an elegant self-documenting program.
It is in fact possible to write a much shorter program. The trick lies in
careful use of the printf function and the fact that it allows the user to
include format specifiers to
reformat text before it gets printed out. Furthermore, the
format string passed to
printf is just a normal C string like any other. In fact, it is possible
to achieve self-reference by letting the string which describes the program also serve
as the format specifier for its own output! This idea gives us the following program:
Self II
Here, we have overcome the problem of outputting the characters which delimit
the string by making the string represent not just the use of the
program, but rather the program in its entirety. The string f represents
everything that needs to be printed out, with a few critical sections left out -
namely, the characters that need to be escaped, and the contents of the string.
That is the crucial idea behind Self II. The string f can avoid the
infinite recursion of including a copy of own its contents within itself by
specifying that some unknown string will later be substituted for the ' char*f="char*f=%c%s%c;%cmain(){printf(f,34,f,34,10,10);}%c";
main(){printf(f,34,f,34,10,10);}
%s',
and then substituting f itself! This is why the printf statement contains
f twice. The first time is as the format specifier, and the second is as the replacement
text for the '%s' in f. And Self II has the added advantage of being
relatively easy to read and understand.
There are, of course, many more possibilities. There are numerous ways to rephrase these working programs, and even programs that attack the problem from a different angle (for an example, see Self IV in Appendix A). Unfortunately, there is no room here to discuss the strategies used by those programs. At the very least, the examples given above provide a good starting point for moving forward towards exploring those other ideas.
All is not lost, though. Self II can be recoded to eliminate the
dependencies on specific character codes (that solution, called Self III, can
be found in Appendix A).
Once that is out of the way, is there anything left that might be unsatisfying?
One important element remains: the printf function.
Printf is included by the committee responsible for defining the C language
as a part of the standard C libraries.
Note that printf is not part of C itself - C on its own provides no
mechanism for output whatsoever! We are only justified in using printf
(and putchar, for that matter) based on its recognition as a standard C
function.
But what if C also had a standard function called
print_a_prog, which was defined as follows:
The implementation of this function would be hidden away in a library. The
programmer need only declare the function's existence and then use it. In this
case it becomes trivial to write a self-documenting program:
void print_a_prog() {
printf( "extern void print_a_prog();\n" );
printf( "main(){printf_a_prog();}\n" );
}
Cheat I
This program is not very satisfying, because it seems as if we are making use
of a function tailor-made to our needs, one which just happened to be in the
library. Perhaps one should feel compelled by one's conscience to
require that Cheat I also output the definition of
extern void print_a_prog();
main(){print_a_prog();}
print_a_prog. That means that the print_a_prog
function has to be self-documenting, which naturally begs the question of writing
a self-documenting program.
Furthermore, we certainly cannot require that Self II output the
definition of printf - we do not know what it is! And even if the
definition of printf was known to us, it would be a much
more difficult task to include it in the output of our program. So
it looks as if we must be satisfied with allowing standard library functions.
Even in this case, at what level should the program duplicate itself? Over its lifetime, a computer program exists in many different forms. Depending on what form we identify as the program's 'itself', we obtain different solutions to the original problem. For instance, we can identify a computer program with the file that contains its source code. In this case, we obtain the following:
Cheat II
This program literaly opens its source file, reads each character in it, and
echoes that character as output. The self-reference occurs in the
#include <stdio.h>
main(){
int c;
FILE *f;
f = fopen( __FILE__, "r" );
c = fgetc( f );
while( c != EOF ) {
putchar( c );
c = fgetc( f );
}
fclose( f );
}
__FILE__ macro, which gets replaced at compile time by the name
of the file which contains the given program.
This example is unsatisfying because it relies on the existence of its source code in a fixed location. It seems as if a self-documenting program should exist independently of the files that created it. So, intuitively, we are making the identification between a program and itself at the wrong level. Our definition of a self-documenting program should be revised to include the clause that the program be able to run in isolation of its source.
We could take the opposite viewpoint, and identify a program with the pattern of bits that make up its representation in the computer's memory as it runs. A high level description of the resulting program would look like this:
Cheat III
In some sense, this program is indeed self-documenting. But this result, too,
is unsatisfying, because the output is more or less meaningless. The
representation of the program in memory changes from compiler to compiler, from
computer to computer, even between executions! The output from this program breaks
the chain of self-documentation, because it cannot be interpreted in some way as
a self-documenting program, which suggests it is not identical to the program that
generated it. This suggests another addition to the definition of a self-documenting
program. Not only should the program produce itself as output, but the output should
be a self-documenting program!
main(){
Find myself in the computer's memory.
Output all the bits that make me up.
}
The implementation language presents even more difficulties. So far, we have simply been using C for all our attempts at self-documentation, without addressing the possibility of such programs in other languages. The problem of defining self-documenting programs becomes overwhelming when applied to more than one language. For instance, suppose there was a language called CAT which had very simple semantics: given a file containing source code, CAT generates an executable which outputs the source code. In this case, any program will suffice! They all print out their own source. (The name CAT comes from the UNIX command 'cat' which echoes its input). This example suggests that we revise our definition yet again, to specify that the implementation language be sufficiently complex. Of course, 'sufficiently complex' is another definition altogether.
So, after reviewing the different ways a program can cheat the initial definition, a new, stronger, definition emerges:
Self-reference occurs whenever a system loops back on itself and describes some aspect of its own form or structure. It occurs frequently in the world, and has practical applications in domains such as mathematics and theoretical computer science.
The use/mention distinction plays an important role in self-reference. To use something is to exploit its content and meaning. To mention something is to refer to its form and appearance.
Self-reference occurs frequently in the domain of computer programming. Structures such as linked lists embody the use/mention distinction.
Self-documenting programs are a simple idea - a program that, when run, produces itself as output. The task of writing a self-documenting program is not nearly as easy as it sounds. In a certain sense, it is impossible to ever begin.
The brute force approach at a self-documenting program fails because we get caught in an infinite loop of adding an instruction to output a previous instruction.
Merely taking the description of the program out of the code fails bacause the resulting program outputs only a use, and not a mention.
The problem is further complicated by the way C handles the use/mention distinction internally: one cannot mention double quotes or newlines in a string without escaping them.
By finding an alternate expression for these special characters, such as an encoding like ASCII, one can write true self-documenting programs. Such programs tend to be cryptic and difficult to read.
These self-documenting programs can be made more elegant by using
the printf function's capacity for format specifiers.
ASCII is not the only encoding used in the world; this foils some self-documenting programs, causing them to give incorrect output on some computers.
Printf is not part of the C language, but defined as a part of its
standard libraries. Although this introduces a dilemma as to what
functions one should be allowed to use as helpers in ones program,
functions in standard libraries should be permitted in self-documenting
programs anyway.
Whether or not to consider a program self-documenting also depends on what we identify as 'itself' when referring to a program. The file that contains the program's source seems to exist at too high a level. The representation of the program in the computer's memory at runtime seems too low.
Our definition of a self-documenting program should be revised to specify that the program be implemented in a sufficiently complex language, that it be able to run in isolation from its source, and that its output also be a self-documenting program.
printf function to allow it to reformat the
supplied format string. For example, when %s appears in the format string,
it is replaced by a string supplied as another argument to printf.
printf, the format string contains plain
text, which is copied directly to the output stream, and format specifiers, which
instruct printf to insert the value of an argument and insert it at the
location of the specifier in the output stream.
This appendix contains all the individual programs which appear in the report, along with the ones which are referred to but which to not appear in the body of the report.
Failure I
char*f="main(){printf(f);printf(f);}";main(){printf(f);printf(f);}
Failure II
char*f="main(){printf("char*f="");printf(f);printf("";\n");printf(f);}";
main(){printf("char*f="");printf(f);printf("";\n");printf(f);}
Failure III
char*f="main(){printf(\"char*f=\"\");printf(f);printf(\"\";\\n\");printf(f);}";
main(){printf("char*f=\"");printf(f);printf("\";\n");printf(f);}
Self 0
char*f="main(){putchar('c');putchar('h');putchar('a');putchar('r');putchar('*');&
putchar('f');putchar('=');putchar(34);printf(f);putchar(34);putchar(';');&
putchar(10);printf(f);putchar(10);}";
main(){putchar('c');putchar('h');putchar('a');putchar('r');putchar('*');putchar('f');&
putchar('=');putchar(34);printf(f);putchar(34);putchar(';');putchar(10);&
printf(f);putchar(10);}
Self I
char f[]="char f[]=;main(){f[9]=0;printf(f);putchar(34);f[9]=';';printf(f);&
putchar(34);f[10]=0;printf(&f[9]);f[10]='m';putchar(10);printf(&f[10]);putchar(10);}";
main(){f[9]=0;printf(f);putchar(34);f[9]=';';printf(f);putchar(34);&
f[10]=0;printf(&f[9]);f[10]='m';putchar(10);printf(&f[10]);putchar(10);}
Self II
char*f="char*f=%c%s%c;%cmain(){printf(f,34,f,34,10,10);}%c";
main(){printf(f,34,f,34,10,10);}
Self III
char a='"';char b='\n';char c='\\';
char*f="char a='%c';char b='%cn';char c='%c%c';%cchar*f=%c%s%c;%cmain(){&
printf(f,a,c,c,c,b,a,f,a,b,b);}%c";
main(){printf(f,a,c,c,c,b,a,f,a,b,b);}
Self IV
char*lines[]={
"char*lines[]={",
"%c%s%c%c%c",
"0};",
"main(){",
"int idx;",
"puts(lines[0]);",
"for(idx=0;lines[idx]!=0;idx++){",
"printf(lines[1],34,lines[idx],34,',',10);",
"}",
"puts(lines[2]);",
"for(idx=3;lines[idx]!=0;idx++){",
"puts(lines[idx]);",
"}",
"}",
0};
main(){
int idx;
puts(lines[0]);
for(idx=0;lines[idx]!=0;idx++){
printf(lines[1],34,lines[idx],34,',',10);
}
puts(lines[2]);
for(idx=3;lines[idx]!=0;idx++){
puts(lines[idx]);
}
}
(Self IV assumes in advance that it will be iterating across an array of
strings rather than a single string. It iterates across the array the first time
to output the mention, namely the definition of the array. The second iteration
provides outputs the use).
Cheat I
extern void print_a_prog();
main(){print_a_prog();}
Cheat II
#include <stdio.h>
main(){
int c;
FILE *f;
f = fopen( __FILE__, "r" );
c = fgetc( f );
while( c != EOF ) {
putchar( c );
c = fgetc( f );
}
fclose( f );
}
Cheat III
main(){
Find myself in the computer's memory.
Output all the bits that make me up.
}