Visual walkthrough — Phony targets, pattern rules, implicit rules
Step 1 — A file has a birthday: the timestamp
WHAT. Every file on disk carries a number: the moment it was last written, called its modification time (we write it ). Think of it as a clock stamp glued to the file. When you edit main.c and save, the operating system re-stamps it with "now".
WHY this and not file contents? Make could read both files and compare them byte-by-byte, but that means reading the whole file — slow for big projects. Asking the OS "when was this touched?" is a single instant lookup (one stat() call). Make chooses speed: compare two clock numbers, not two whole files.
PICTURE. Two files sit on a timeline. main.c was saved at time 10. main.o (the compiled result) was built at time 12. Later than its source — good.

Step 2 — The rebuild question, drawn as an arrow
WHAT. A rule ties a target to the things it is built from:
The colon : reads as "depends on". We draw it as an arrow from prerequisite to target.
WHY an arrow? Because the freshness flows one way. If the source changes, the target becomes stale — never the reverse. The arrow points in the direction "must be rebuilt after".
PICTURE. main.c → main.o. Under each file, its . Make's whole job is: compare the two numbers along that arrow.

Step 3 — Case walk: the three things that can happen on one arrow
WHAT. With one target and one prerequisite there are exactly three situations. We must cover all of them — a reader should never meet a case we skipped.
WHY enumerate? The formula in Step 2 is one line, but a beginner can't feel it until they see each branch resolve. So we split vs into: target absent, target older, target newer.
PICTURE. Three little timelines stacked. Green = recipe runs, grey = skipped.

| Case | Situation | check | Verdict |
|---|---|---|---|
| (a) | main.o missing |
target | run (build it) |
| (b) | main.c newer than main.o |
run (stale) | |
| (c) | main.o newer than main.c |
skip (up to date) |
Recall Quick check
If you touch main.c (re-stamp it to "now") but change nothing inside it, does main.o rebuild?
Yes ::: Make only reads timestamps, not contents. main.c is now newer, so case (b) fires — even though the text is identical.
Step 4 — The degenerate case that births phony targets
WHAT. Now a target with no prerequisites at all — like the clean job:
clean:
rm -f *.o programWHY it breaks. Feed this into the Step 2 formula. Is clean missing? Suppose a file literally named clean sits in the folder — then no, it exists. Is any prerequisite newer? There are no prerequisites, so the part is vacuously false. Both branches false → skip. Make prints 'clean' is up to date and your rm never runs. That's the bug.
PICTURE. The formula's two boxes both light up "false", so the whole OR collapses to "skip" — and we watch the recipe die.

Step 5 — .PHONY = "pretend this target is eternally missing"
WHAT. Declaring the target phony:
.PHONY: cleantells Make: this name is never a real file — do not stat it, treat it as forever out of date.
WHY it fixes Step 4. .PHONY short-circuits the whole formula to run = true, always. Equivalently: a phony target behaves as if from the "is it missing?" side, so the first OR-branch is permanently true.
PICTURE. The same formula, but the phony flag forces the left box to true — the OR lights up green no matter what's on disk.

Step 6 — One arrow becomes many: the pattern rule %
WHAT. You have main.c, math.c, parser.c, … all compiling the same way. Instead of writing one rule per file, write one template:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@The % is the stem — a wildcard that matches any non-empty string, and the same match is reused on both sides.
WHY the % must match on both sides. That identical stem is exactly how Make links a target back to its source. When it wants math.o, it peels off the literal .o, so the stem is math, then re-glues that stem onto %.c → math.c. Same stem = the two files belong together.
PICTURE. Watch the stem math get extracted from the target and injected into the prerequisite; the automatic variables $@, $<, $* are labelled right where they land.

Each symbol: $@ = the target Make is currently building, $< = its first prerequisite, $* = the bare stem. Make fills these in at match time because the template never knew the concrete names.
Step 7 — When you write no rule at all: implicit rules & precedence
WHAT. GNU Make already ships a %.o: %.c template internally. So even an empty-of-patterns Makefile compiles your .o files. When several rules could apply to one target, Make picks by a fixed priority.
WHY a priority order? Because more than one template can match parser.o (your explicit rule, your pattern rule, and the built-in one). Make resolves the tie by specificity: the most specific, most deliberate rule wins.
PICTURE. Three candidate rules queue up for parser.o; the explicit one wins, the built-in is the last resort.

Step 8 — The cascade: how one edit ripples through the graph
WHAT. Real projects chain the arrows: sources → objects → program. Editing one source must ripple up the chain. This is the dependency graph in action.
WHY it cascades. Rebuilding math.o re-stamps it to "now". Now program, which lists math.o as a prerequisite, sees a newer prereq → case (b) of Step 3 → program relinks. The freshness propagates one arrow at a time. (This is exactly what makes Incremental compilation fast: only the touched sub-tree rebuilds.)
PICTURE. Touch math.c. Follow the orange "stale" wave: math.c ⇒ math.o ⇒ program. main.o and main.c stay grey — untouched.

The one-picture summary
Everything above is a single flow: stamp → compare → decide → (maybe) run → re-stamp → cascade. Phony bypasses the compare; pattern/implicit rules supply the recipe when no explicit one exists.

Recall Feynman retelling — say it in plain words
Make is a lazy but careful assistant. Every file wears a little clock showing when it was last touched. You hand the assistant a list of "this is made from that" arrows. When you ask for something, it walks each arrow and asks one question: "Is the thing I want missing, or is any of its ingredients fresher than it?" If yes, it cooks; if no, it saves its energy and skips. Some jobs — like clean — aren't files at all, so its clock-comparison makes no sense; you tape a .PHONY sign on them meaning "always cook this, don't check the clock." Writing a rule for every file would be exhausting, so you give it a template with a % blank: it peels the name apart, keeps the blank part (the stem), and reuses it to find the source. And Make already carries a stack of these templates in its pocket, so often you write nothing. Finally, when it does cook something, that file's clock resets to now — which can make its dependents look stale, and the freshness ripples up the chain until everything that needed rebuilding is rebuilt, and nothing that didn't, wasn't.