---
title: 'Compilation Stages'
source: 'https://academia.sh/en/courses/c-fundamentals/compilation-stages'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes which input each of the four stages looks at when it makes its decision'
    - 'Compares the four classes in a single run using a small measurement excerpt'
    - 'Shows with a source example that the stages run in sequence and cannot be reversed'
---

# Compilation Stages

Each of the four stages looks at a different input: the preprocessor only text, the compiler a single unit, the linker multiple units, and runtime only data. A small measurement excerpt runs all four side by side; a separate pair of programs also shows how one line of the same source enters two different compilations, through a decision made in the source beforehand.

The previous lesson named the four stages: preprocessor, compiler, linker, runtime; it ran the
three-file core in a single pass and distributed fourteen observations across these four
classes. This lesson deepens the same four names with the question of which input each looks at
when it makes its decision. The four stages look at the same source, but none of them sees the
same thing — one sees only text, one a single file, one the intersection of multiple files, and
one only the data it holds at that moment. This difference is not an abstract classification;
each stage genuinely has nothing in hand but that input, and this lesson will show it by
measuring.

## The Four Inputs

What each stage has in hand when it makes its decision differs:

| Class | Input it looks at |
|---|---|
| preprocessor | Only the source's text; none of the concepts of type, constant, or unit exist yet |
| compiler | The entirety of a single translation unit; it does not look outside that unit |
| linker | The definitions multiple units produce; it does not interpret their content, only matches names |
| runtime | The program's own data; the source's text is no longer in hand |

This ordering is also an order of narrowing. The preprocessor holds the widest information — it
can access the entire source text, but understands none of the structure inside it. The compiler
is responsible for only one unit of this text, but it now sees structures such as types and
expressions. The linker does not even see a single unit; it sees only the names units expose
outward. Runtime sees no part of the source at all, only the data the program holds at that
moment.

This narrowing is one-directional: a stage cannot recover information the one before it lost. A
macro name turns into text at the preprocessor, and the compiler never sees that name — what it
sees is only the substituted text, and the information "this was a macro" leaves no trace in the
source. In the same way, a variable's type is reduced at the compiler to a binary arrangement (a
symbol in the object file); the linker matches this symbol only by its name, does not know and
has no need to know which type of variable that name belongs to — if there is a type mismatch,
the compiler has already caught it in its own stage. Every stage works assuming the one before
it has finished its job.

This chain of trust is not limited to the source itself. The linker also assumes that the
object files it takes as input came from a correct compiler; an object file produced by any
tool, as long as it carries the same format, is accepted the same way. This shows why the order
of the four stages depends not on an implementation but on a format — each stage checks not
which tool carried out the step before it, but whether the format it produced conforms to the
contract.

## Four Classes, Four Lines

The excerpt below takes four lines from the previous lesson's core — one from each class — and
runs only these four side by side. Showing the linker class still requires two units.

```c
// stage.h — declaration of the one name defined in another unit
#ifndef STAGE_H
#define STAGE_H

int identity(void);

#endif
```

`stage.h` is neither compiled on its own nor counted as a unit — what makes a file a unit is
that it is given to the `cc` command as a separate source, not that it is copied into another
file with `#include`. Throughout this lesson `stage.h` will appear to the verifier only as
SOURCE: its content reaches the compiler only after it has mixed into the text of the unit that
`#include`s it. This sharing role of the header file is the subject of the next lesson.

```c
// choice.c — the second unit: the definition lives here, the call resolves at link time
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// stage.c — small excerpt measuring the four classes side by side: one line per class
#include <stdio.h>
#include "stage.h"

#define FACTOR 3

static void observe(const char *name, const char *stage, const char *evidence) {
    printf("%-30s%-14s%s\n", name, stage, evidence);
}

int main(void) {
    char buffer[64];

    printf("%-30s%-14s%s\n", "observation", "stage", "evidence");

    snprintf(buffer, sizeof buffer, "FACTOR placed as the text %d", FACTOR);
    observe("macro value", "preprocessor", buffer);

    int whole = 7 / 2;
    double fractional = 7 / 2.0;
    snprintf(buffer, sizeof buffer, "whole %d fractional %.1f", whole, fractional);
    observe("operator choice by type", "compiler", buffer);

    snprintf(buffer, sizeof buffer, "the call returned %d", identity());
    observe("function in another unit", "linker", buffer);

    int data[] = {4, 8, 15, 16};
    int sum = 0;
    for (size_t i = 0; i < sizeof data / sizeof data[0]; i++) {
        sum += data[i];
    }
    const char *verdict = (sum > 40) ? "large" : "small";
    observe("data-dependent branch", "runtime", verdict);

    return 0;
}
```

```
observation                   stage         evidence
macro value                   preprocessor  FACTOR placed as the text 3
operator choice by type       compiler      whole 3 fractional 3.5
function in another unit      linker        the call returned 7
data-dependent branch         runtime       large
```

**LC4.** This excerpt takes only four of the core's fourteen lines and carries none of the
`preprocessor`, `compiler`, `linker`, `runtime` counters — the goal here is not to recount the
distribution but to place the four classes side by side in a single glance. The counting is
done exactly and only in the previous lesson, and the 4/4/3/3 distribution given there does not
change here.

Each of the four lines confirms the "input it looks at" definition from the previous section.
The `macro value` row was produced without ever seeing `FACTOR`'s numeric value, only by
substituting the text `3` — the preprocessor does not even know that `FACTOR` is an integer
constant. The `operator choice by type` row was produced by looking at the type of two
variables inside `stage.c` itself — whether `choice.c` exists makes no difference. The
`function in another unit` row shows the exact opposite: `identity`'s body does not exist at
all in `stage.c`, only a declaration does; the evidence appears only once it is linked with
`choice.c`. The `data-dependent branch` row also carries no fixed number in the source; `large`
or `small` is computed at runtime by looking only at the content of the `data` array.

The order in which the four lines are produced also reflects the order of the stages. By the
time the program starts running, `stage.c` has already been compiled and linked with
`choice.c`; runtime only proceeds inside this finished program. Even though the order of the
`observe` calls in the source matches the order of the output lines, this is an observation,
not a requirement: which stage a line's decision was made at is independent of what order it
stands in in the output.

Two lines can stand close to each other and still fall into different classes. In the previous
lesson's full core, `macro value` and `enumeration constant` seem at first glance to do the
same job — both produce a "constant value." But because `FACTOR` is a `#define`, its value is
placed into the source as text, and the preprocessor does this without the compiler ever
getting involved; because `BLUE` is an `enum` member, its value is computed as a compile-time
constant expression, and this computation stays within `stage.c`'s own unit bounds. Both look
like constants in the source, neither changes at runtime, but one is finished at the text
level, while the other passes through the compiler's type and expression rules. This
difference shows why the assumption "everything that looks constant falls into the same class"
is wrong: what determines the class is not appearance but how it is written in the source.

## The Stages' Closure to One Another

One stage being closed to another means there is no channel of communication between them. The
compiler does not ask why the preprocessor produced a text the way it did; it only accepts what
reaches it. The linker does not know how the compiler checked a type; it only looks at the name
of the symbol produced. Runtime no longer carries which line of the source something came from;
all it has in hand is machine instructions and the data itself.

This closure has a concrete consequence: a stage can leave only its own output for the next
one, it cannot carry any extra information about its own process. The preprocessor does not
tell the compiler that `FACTOR` was a macro, because it has no channel to tell it with — what
the compiler sees is only the number `3`, as if it had always been written that way in the
source. Nor does the compiler "tell" the linker that `identity` is defined in another unit; it
only leaves an unresolved name in the object file it produces, and the linker looks for that
name by its own method. At every handoff, the previous stage's process is lost, only its result
remains.

This also explains why the four classes never mix. To understand which class an observation
falls into, it is enough to ask "which stage could have made this decision": if it is a text
substitution, the preprocessor; if it is a type or constant expression, the compiler; if it is
a name across units, the linker; if it is a condition that looks at data, runtime. This
criterion will work the same way not only in this lesson but through the rest of the course.

The linker's "only looking at names" is also a consequence of the same closure. The name
`identity` stands on `stage.c`'s side only as a declaration, and on `choice.c`'s side as a
definition; the linker's job is to match these two sides through the same name. In doing so, it
never looks at what `identity` does or what its body contains — it only checks whether the name
exists on both sides and, if so, how many times it is defined. How a declaration and a
definition are kept apart, and how the linker brings the two together, is the subject of the
next lesson.

The runtime class's closure is the most thorough of all. By the time the program starts
running, no trace of the source remains in its hands; there is no mechanism that knows which
source line the `data-dependent branch` row came from, the running program only looks at its
own data and the results that data produces. This is why "tracing back" a runtime decision to
find its counterpart in the source is not as direct as it is for the other three stages — the
link between the source and runtime is established once, irreversibly, during compiling and
linking.

## The Bounding Measurement

The stages run in sequence and do not reverse: if the text the preprocessor produces is wrong,
the compiler does not fix it, it only sees the result. The most direct way to show this is to
compile the same conditional body twice — with the only difference being one line added to the
source.

The source choosing between different bodies with `#ifdef` is a common pattern, and the choice
is always settled at the preprocessor, before compilation even begins. The `FACTOR` macro and
the `PASTE` token pasting from the previous lesson belonged to the same class: both reached a
result at the text level before the compiler ever saw the source. The difference is this —
`FACTOR` always expanded to the same text, but whether `VERBOSE` is defined determines which
branch of the source enters compilation. This is a stronger example than a single macro value:
what is chosen here is not a number but a part of the source itself.

```c
// view.c — body choice is settled in the source beforehand by a #define
#include <stdio.h>

/* VERBOSE is not defined here: the preprocessor sends the short body into compilation. */

int main(void) {
    printf("conditional body: ");
#ifdef VERBOSE
    printf("the verbose body entered compilation\n");
#else
    printf("the short body entered compilation\n");
#endif
    return 0;
}
```

```
conditional body: the short body entered compilation
```

```c
// view.c — the same conditional block, with one line added that changes the body choice
#include <stdio.h>

#define VERBOSE

int main(void) {
    printf("conditional body: ");
#ifdef VERBOSE
    printf("the verbose body entered compilation\n");
#else
    printf("the short body entered compilation\n");
#endif
    return 0;
}
```

```
conditional body: the verbose body entered compilation
```

**LC5.** Both files carry the same name (`view.c`), and the second block replaces the first one
on disk; this is to show that these are two separate compilations, not two different runs of the
same program. The only difference is a single `#define` line added at the top of the second
file — the rest of the body is identical, letter for letter.

`#ifdef` here is just one member of the conditional compilation family; the same family
includes `#ifndef`, which tests that a name is not defined (this is also the directive
`stage.h` uses to prevent double inclusion), `#if`, which tests the value of an expression,
`#elif`, which adds another branch, and `#else`, which covers the case where none of them is
true. All of them fall into the same class, because all of them decide without the compiler
getting involved, looking only at the names the preprocessor has defined.

The source of both compilations, looked at line by line, carries both branches of the `#ifdef`
block. But the compiler never sees both branches: the preprocessor checks whether `VERBOSE` is
defined and removes the branch not chosen from the source entirely — in the text that goes to
the compiler, that branch no longer exists, not unparsed or skipped, but as if it had never
been written. The compiler therefore does not say "it eliminated one of two options"; the
source it sees already has only a single body. Once the next stage (the compiler) is reached,
the decision the preprocessor made can no longer be questioned; the only way is to change the
source and start over from the beginning — and that is exactly the difference between the two
blocks.

This shows why the stages are not a chain but a one-directional sequence. In a chain, each link
can inspect the one before it; here, no stage can undo a decision the one before it made, it can
only take over its result. This is also where the name of the Compilation Stages lesson comes
from: a stage is not a step that can be reversed, it is a sequential, one-directional order.

This one-directionality has a practical consequence: when looking for the wrong result a
program gives, asking first which stage settled that result narrows the search. If the
`conditional body` row is seen to be wrong, the place to look is not the compiler or the linker
but the `#define` line in the source — because the text that reaches the compiler has already
been reduced to a single body, and the compiler has done nothing but interpret that text
correctly. The same logic holds for the other three classes: behind the result a class produces
lies only the input that class looked at, not what came before it.

## Summary

- The four stages look at the same source but see different inputs: the preprocessor text, the
  compiler a single unit, the linker multiple units' definitions, and runtime only data.
- A small excerpt ran one line from each of the four classes side by side; each line confirmed
  the input its own class looks at.
- The stages run in sequence and do not reverse: a branch the preprocessor eliminates never
  reaches the compiler, and the compiler sees it not as "eliminated" but as if it never existed.
- The only way to change a stage's decision is to change the source and run the chain from the
  beginning; no stage receives feedback from the one after it.
- Knowing which stage an error belongs to narrows the search: behind the result lies only the
  input that stage looked at, not what happened before it.

## Next Step

The linker class was shown in this lesson with only a single call: `identity`'s declaration was
in one file, its definition in another, and `stage.h` was the tool that carried this declaration
between the two. The next lesson looks at how this sharing is set up — how a header file carries
a declaration into multiple units, why a declaration and a definition must be kept separate, and
how far mixing the two up stays harmless.
