Why Zolo Warns at 4 Levels of Nesting (And the 60-Year-Old Idea Behind It)
A lint warning about nesting depth, traced back through Linus Torvalds, McCabe's cyclomatic complexity (1976), Dijkstra's "Go To Considered Harmful" (1968), and Miller's working-memory limits (1956).
Why Zolo Warns at 4 Levels of Nesting (And the 60-Year-Old Idea Behind It)
🇧🇷 Versão em português: Por que o Zolo avisa em 4 níveis de aninhamento · 🇪🇸 Versión en español: Por qué Zolo avisa con 4 niveles de anidamiento
You write some Zolo code. The compiler comes back with:
warning[max-nesting-depth]: nesting depth 5 exceeds max recommended (4)
warning[max-nesting-depth]: nesting depth 6 exceeds max recommended (4)
warning[max-nesting-depth]: nesting depth 7 exceeds max recommended (4)
You think: "Who decided 4? Why not 5? Why not 8?"
The honest answer is that the exact number is arbitrary, but the idea behind it is one of the oldest and most validated principles in software engineering — going back to a 1956 cognitive psychology paper, a 1968 letter that essentially birthed structured programming, and a 1976 metric that's still used today by every serious static analyzer.
This post walks through the lint, the history, and what it means for how you write Zolo.
What the lint actually does #
The max-nesting-depth rule lives in crates/zolo-compiler/src/lint.rs. The implementation is small enough to fit on one screen:
fn enter_nesting(&mut self, span: Span) {
self.nesting_depth += 1;
let max = self.config.settings.max_nesting_depth;
if self.nesting_depth > max {
self.emit(
"max-nesting-depth",
format!(
"nesting depth {} exceeds max recommended ({})",
self.nesting_depth, max
),
span,
LintLevel::Warn,
);
}
}
A visitor walks the AST. Each of these constructs bumps the counter by one:
if/elsematch(and each arm)forwhileloop- control-flow expressions used as values (
let x = if ...,let x = match ...)
The function body itself does not count. So fn foo() { if a { if b { if c { if d { if e { ... } } } } } } produces depth 5 and trips the warning.
The default threshold is 4, configurable in your project's .lint.toml. Why 4? Let's rewind 70 years.
1956 — Miller and the limits of human working memory #
In The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information (Miller, 1956), psychologist George A. Miller observed that humans can hold roughly 7 ± 2 discrete items in short-term memory simultaneously.
The original paper isn't about code — Miller was studying perception of tones, tastes, and recall of digit strings. But the finding got picked up by every later field that cares about how humans process information. Subsequent cognitive-load research has actually pulled the number lower — Nelson Cowan's work in the early 2000s suggests the real working-memory capacity is closer to 4 ± 1 chunks (Cowan, 2001).
Why does this matter for nested code? Because reading a deeply nested block isn't a single thought. To understand line N inside if A { for B { match C { if D { ... } } } }, you have to hold A, B, C, and D simultaneously in your head as active context — and remember what each one means in the surrounding logic. Each level is a chunk. At 4–5 levels, you're already at the edge of what a human can track without losing the thread.
🔖 Miller, G. A. (1956). The magical number seven, plus or minus two: some limits on our capacity for processing information. Psychological Review, 63(2), 81–97. DOI: 10.1037/h0043158
1968 — Dijkstra and structured programming #
In March 1968, Edsger Dijkstra published a one-and-a-half-page letter in Communications of the ACM titled Go To Statement Considered Harmful (Dijkstra, 1968). It's perhaps the most quoted paper in computing.
"The unbridled use of the go to statement has as an immediate consequence that it becomes terribly hard to find a meaningful set of coordinates in which to describe the process progress."
The core argument: as code grows in dynamic complexity, humans lose their ability to mentally map source code to program state. He argues for restricting control flow to sequence, selection (if), and iteration (while) — what we now call structured programming.
Dijkstra's letter killed goto in mainstream practice, but the deeper insight applies equally to deep nesting: every additional level of control structure is one more coordinate you have to track to know where you are in the program. Five levels of if is, in this sense, only slightly better than goto — both make it hard to answer "what's true here?"
🔖 Dijkstra, E. W. (1968). Go to statement considered harmful. Communications of the ACM, 11(3), 147–148. DOI: 10.1145/362929.362947 — also archived as EWD215.
1976 — McCabe gives complexity a number #
In A Complexity Measure (McCabe, 1976), Thomas J. McCabe proposed a graph-theoretic metric: count the number of linearly independent paths through a function's control-flow graph. He called it cyclomatic complexity.
The formula is simple: V(G) = E − N + 2P where E is edges, N is nodes, P is connected components. For typical code, each if, while, for, case adds 1.
McCabe didn't measure nesting directly — but his metric and nesting are deeply correlated, because each nested branch multiplies the path count. He proposed 10 as a soft upper bound for cyclomatic complexity per function. At 5 levels of nested if, you've already burned through that budget on structure alone, with no room for the actual logic.
This is the metric that lives, four decades later, inside SonarQube, Code Climate, ESLint's complexity rule, and Clippy's cognitive_complexity.
🔖 McCabe, T. J. (1976). A complexity measure. IEEE Transactions on Software Engineering, SE-2(4), 308–320. DOI: 10.1109/TSE.1976.233837
2001 — Torvalds, blunt as ever #
Fast-forward to the Linux Kernel Coding Style (Torvalds, current). Section 1, on indentation:
"The answer to that is that if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program."
Linus doesn't cite McCabe or Miller. He's offering the field-tested practitioner version of the same finding: deeply nested code is a symptom — almost always a symptom that a function is doing too many things and should be split.
It's worth noting that he says 3, not 4. The kernel uses 8-character tabs partly to make this physically painful: at 3 levels you're already 24 columns in.
What everyone else picked #
| Linter / Tool | Rule | Default | Notes |
|---|---|---|---|
| ESLint | max-depth |
4 | "Maximum depth that blocks can be nested" |
| Clippy (Rust) | excessive_nesting |
opt-in (threshold default 0) |
Restriction lint; users typically set 5 |
| SonarQube | S134 | 3–4 (varies by language) | Java/C++ default 4, several others 3 |
| Linux Kernel | coding-style §1 | 3 | "If you need more, you're screwed" |
| Zolo | max-nesting-depth |
4 | Configurable per-project |
There's no consensus on the exact number, but the band is narrow: 3 to 5. Zolo chose 4 because it matches ESLint (the most widely-deployed linter on Earth) and because in practice it draws the line right where the code starts becoming harder to read out loud.
What this looks like in Zolo #
A 5-level function (warning):
fn process(items) {
if items != nil { // depth 1
for item in items { // depth 2
if item.valid { // depth 3
match item.kind { // depth 4
"x" => if item.urgent { // depth 5 → warning
do_urgent_x(item)
},
_ => skip(item),
}
}
}
}
}
The same logic, refactored with guard clauses and an extracted function:
fn process(items) {
if items == nil { return }
for item in items {
handle(item)
}
}
fn handle(item) {
if !item.valid { return }
match item.kind {
"x" if item.urgent => do_urgent_x(item),
"x" => {},
_ => skip(item),
}
}
Two things happened:
- Guard clauses flipped negative cases into early returns, removing one level of
if. This pattern is sometimes called "return early, return often" and traces back to Bertrand Meyer's Object-Oriented Software Construction (1988). - Extraction moved the inner work into
handle, which itself never goes past depth 2. The function name now documents what was previously buried structure.
The result reads top-to-bottom. You can stop at any line and know what's true above.
When the rule is wrong #
Lints encode defaults, not absolutes. Cases where deep nesting is genuinely the clearest expression:
- Parsers and tokenizers — context-sensitive grammars naturally produce deep
matchtrees. - State machines — explicit nested state can be clearer than a flat dispatch table when transitions are sparse.
- Numerical kernels — tight loops over multi-dimensional arrays (image processing, ML kernels) often need 4+ levels just for
forloops.
For these, adjust per project in .lint.toml:
[settings]
max-nesting-depth = 6
[rules]
# or disable entirely
max-nesting-depth = "off"
The point of the warning isn't that 5 is illegal. It's that the default should be conservative, and crossing it should be a deliberate choice you can explain.
Why Zolo bothers #
A lint isn't a style preference. It's a tiny piece of automated peer review that runs every time you compile. The cumulative effect, across thousands of edits, is that the codebase stays inside the band where humans can still reason about it without dedicating a coffee to each function.
Miller showed us the cognitive ceiling. Dijkstra showed us why structure matters. McCabe gave us a way to measure it. Linus translated it into something a maintainer would actually say.
Zolo's default of 4 is one number in that long lineage. If you ever feel the urge to silence the warning, ask yourself the Linus question first: am I screwed, and should I fix my program?
Most of the time, the answer is yes.
Further reading #
- Miller, G. A. (1956). The magical number seven, plus or minus two. PDF · DOI
- Dijkstra, E. W. (1968). Go to statement considered harmful. PDF · EWD215
- McCabe, T. J. (1976). A complexity measure. PDF
- Cowan, N. (2001). The magical number 4 in short-term memory. DOI
- Torvalds, L. Linux kernel coding style. kernel.org
- ESLint.
max-depthrule - Rust Clippy.
excessive_nestinglint - SonarSource. Rule S134