[As someone else noted elsethread, the C standard specifies eight,
not four, phases -- but typically various ones are combined in a
single "user-visible" step.]
Of course, Standard C forbids "handling" sizeof(int) in #if
expressions, because by this phase keywords do not yet exist --
the only tokens are "preprocessing tokens" (pp-tokens), which are
actually more general than C tokens, and no pp-token is a keyword.
(The pp-token "defined" is recognized in #if expressions, but is
not a "keyword" in the Standard's sense.) Since both sizeof and
int are keywords, the preprocessor is not supposed to know about
them. Moreover, in #if expressions, sizeof and int match the format
allowed for identifiers, so if they are not already #define'd to
some expansion, they must behave as does any other undefined
identifier, and be considered identical to an integral constant
zero:
#if sizeof ( int ) == 2
and
#if 0 ( 0 ) == 2
are quite literally identical in this context. The latter is a
syntax violation and must draw a diagnostic. (Note that once the
one required diagnostic comes out, THEN the compiler can go back
and re-interpret the line and turn "sizeof" and "int" into keywords,
though!)
This "undefined identifier acts like 0" is sometimes quite annoying,
as it might be nice to get diagnostics for typos:
#define LITTLE_ENDIAN 1234
#define BIG_ENDIAN 4321
#define PDP_ENDIAN 3412
#define BYTE_ORDER LITTLE_ENDIAN
/* many lines later */
#if BYTE_ODRER == LITTLE_ENDIAN
... some code here ...
#endif
This translates in the same manner as "#if 0 == LITTLE_ENDIAN",
because of the transposition error in turning BYTE_ORDER into
BYTE_ODRER. (Of course, a "high quality" compiler might have an
optional warning anyway.)
Thanks, this was more or less what I was thinking. I don't know anything
about Borland specifics but I thought that doing steps 2 and 3 in one go
was fully possible.
Indeed, doing all eight translation phases in one go is *also*
possible. Separate compilation, in such a system, is handled by
recording the files to compile (and perhaps doing a pre-scan to
check for errors that might be easy to discover early). The trick
is not so much "what is possible", but rather "what is the most
practical and useful" -- and it turns out that a number of different
approaches have a number of different advantages and disadvantages.
On systems with multiple CPUs, it can be fastest, as measured by
"stopwatch time", to run many separate translation phases, one on
each CPU. A traditional Unix-like "cpp | cc1 | as" sequence can
run on three separate CPUs, with all three doing useful work at
the same time and producing results twice as fast (as measured by
stopwatch) as an all-in-one-process compiler.
As another example, the Plan 9 C compiler defers actual code
generation to link-time, so as to be able to do global register
allocation -- i.e., global variables can wind up in registers, or
a register can be shared between two or three functions even if
they are in separate .c source files -- and other such optimizations.
This turns out to be a strong disadvantage sometimes, because
compiling 100 or so modules takes, e.g., two seconds in the "compile"
stage (running on eight CPUs) and then 30 seconds in the "link"
stage (on one CPU). Had more work been done in the "compile" stage,
the eight CPUs could have taken, say, 6 seconds, leaving only two
or three seconds of work to do in the "link" stage. The code might
run somewhat slower, but one could then have a compile-time switch
for "how much to defer".