Declaration Order
Can a function call one defined later in the file? C says no without a forward declaration; Java, Rust and Go say yes anywhere; JavaScript says yes for functions and throws for `let`. The compiler achieves order-independence with one extra pass, and the language decides whether to make you do it by hand.
Can a function call another one that is defined further down the file, and what does the compiler have to do to allow it?
The AST is unchanged; what changes is *when* the symbol table is complete. Under order-independence, a first pass populates every scope with the declarations it contains before any use is resolved, so the table a resolver consults describes the whole scope rather than the prefix of it seen so far. That is the entire mechanism, and every language rule in this area is a decision about whether to perform that pass and for which kinds of declaration.
A resolver may resolve a use to a later declaration only if the language says that declaration is in scope at the use site — visibility is a specified property, not a convenience. Where the language allows it, the resolver must not resolve any use until the collection pass has completed for the enclosing scope, or a forward reference will fail non-deterministically depending on traversal order. Where the language forbids it (C without a declaration, JavaScript let before initialisation), resolving anyway is a miscompilation: it turns a program the language defines as an error into one that silently reads an uninitialised entity.
Key points
- Order-independence costs one extra pass: collect declarations, then resolve uses.
- The collection pass must not descend into bodies, or locals leak into the enclosing scope.
- C requires prior declaration because it was designed for single-pass compilation; headers are the consequence, not the cause.
- JavaScript hoists functions fully,
varasundefined, andlet/constas an inaccessible binding that throws — three different behaviors in one language. - Almost no language makes local variables order-independent, even when its top-level declarations are.
- Mutual recursion works because a body needs only the signature of what it calls, which the collection pass has already recorded.
- The general form is a dependency graph: topologically sort where it is acyclic, and use signatures or indirection where it is not.
Two passes, and the problem they solve
Resolve as you go, and a call to a function defined below fails: the name is not in the table yet. This is not an exotic case — mutual recursion makes it unavoidable, since whichever of two mutually recursive functions comes first refers to one that does not exist yet.
The fix is one extra traversal. Walk the scope collecting *declarations only*, without descending into bodies; then walk again resolving uses against the now-complete table. Both passes are cheap and the second one is the one that was already there. That is how Java, C#, Rust, Go, Kotlin and most modern languages achieve order-independence at file and class level.
The critical detail is the one people get wrong when implementing it: the collection pass must not descend into function bodies. A local variable inside a function is not a member of the enclosing scope, and registering it as one makes it visible to unrelated code — one of the failure modes below. This is the [[visitor-pattern]] case where deliberately *not* recursing is correct.
1function analyzeScope(scope: Scope, items: Node[]) {2 // Pass 1: declarations only. Does NOT descend into bodies.3 for (const item of items) {4 if (isDeclaration(item)) scope.declare(item.name, makeDecl(item))5 }6 7 // Pass 2: resolve every use, including inside bodies. The table is8 // already complete, so a call to something declared later succeeds.9 for (const item of items) resolveUses(item, scope)10}Two loops where a naive implementation has one. The cost is a second traversal of the declaration list — not of the whole tree — and the benefit is that mutual recursion and forward references work without the programmer arranging the file in dependency order.
What each language decided
def is an ordinary statement, so calling a function before the def has executed raises NameError — but the *locality* of a name is decided statically, so assigning to a name anywhere in a function makes it local everywhere in that function, and reading it earlier raises UnboundLocalError. And JavaScript's temporal dead zone is a specified error, not an implementation choice: let bindings exist from the top of the block and are inaccessible until their declaration is evaluated.C requires a declaration before use, which is why header files exist. That is not stubbornness: C was designed to be compilable in one pass on a machine with very little memory, and a single-pass compiler cannot know about a function it has not read yet. The cost is paid forever after in headers, in the separation of declaration from definition, and in [[the-preprocessor]] existing to manage it.
JavaScript hoists, and hoists two things differently. A function declaration is fully hoisted — callable before its textual position. A var is hoisted as a binding initialised to undefined, so reading it early gives undefined rather than an error. A let or const is hoisted as a binding but left uninitialised, and reading it before its declaration throws a ReferenceError — the temporal dead zone, which is deliberately an error rather than undefined because the alternative was judged worse.
C++ adds a wrinkle that catches everyone: member functions of a class may refer to members declared later in the class, because the class body is processed as a complete scope, but code at namespace scope may not. Two different rules in one language, and the reason is that the class body is exactly where the collect-then-resolve pass is applied.
| Language | Function at file/module scope | Local variable | Mechanism |
|---|---|---|---|
| C | No — needs a prior declaration | No | Single-pass by design; headers supply the forward declarations |
| C++ | No at namespace scope; yes for members inside a class body | No | The class body is a complete scope; namespace scope is not |
| Java / C# / Kotlin | Yes, anywhere in the class | No — locals are position-dependent | A member-collection pass before body resolution |
| Rust | Yes, anywhere in the module | No — let is position-dependent | Items are collected module-wide before resolution |
| Go | Yes at package level, across files | No | Package-level declarations are collected before any body is checked |
| JavaScript | Yes for function; let/const throw before their declaration | var is undefined; let/const throw | Hoisting: bindings are created on scope entry, initialisation is separate |
| Python | Only if the call executes after the def runs | Name is local for the whole function; reading before assignment raises | A def is a statement executed at runtime, not a declaration |
The general shape: dependencies, not order
Order-independence within a scope is one instance of a bigger pattern. What a compiler really needs is not an ordering of the *text* but an ordering of the *dependencies*, and where those dependencies form a DAG it can compute one — a topological sort over a DAG, the same algorithm a build system uses on files.
Where the dependencies are cyclic, no ordering exists and the compiler needs something else. For mutually recursive functions the answer is easy: collect signatures first, then check bodies, because a body only needs the *signature* of what it calls. For mutually recursive types the answer is a fixed point or a size-independent representation (a pointer, a reference), which is why C requires an indirection to make two structs refer to each other and why a struct cannot contain itself by value.
Type inference across mutually recursive definitions is the hard version: Hindley-Milner groups mutually recursive bindings into strongly connected components, generalises each group together, and processes the groups in topological order. That is the same dependency reasoning applied where the answer is a type rather than a name — see [[hindley-milner]].
- Collect signatures, then check bodies: enough for mutual recursion among functions.
- A dependency DAG plus topological order: enough for constants, types and initialisers that reference each other acyclically.
- Cyclic value dependencies — a constant defined in terms of itself — must be an error, and detecting the cycle is what produces a good message instead of a stack overflow.
- Cyclic *type* dependencies need an indirection whose size is known regardless, which is why a self-referential struct needs a pointer.
- Module-level cycles push the same problem up a level, where
[[separate-compilation]]and interface files handle it.
How it works
The steps, in the order the compiler takes them.
- Walk the scope's immediate items and register every declaration — name, kind, and enough of a signature for other declarations to refer to it — without entering bodies.
- Where declarations refer to each other (types, constants), build a dependency graph and process it in topological order, reporting a cycle as an error rather than recursing.
- Walk again, now resolving every use inside bodies against the complete table.
- Type-check bodies last, since a body may call anything the collection pass recorded and needs its signature, not its implementation.
- For a language with hoisting, create bindings on scope entry but keep an initialised flag, so a read before initialisation can be diagnosed rather than returning garbage.
- For a single-pass language, require an explicit forward declaration and check at the end of the translation unit that every declared entity was defined.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- The collection pass descends into function bodies, and a local is registered at module scope. An unrelated function resolves a name to it, the program compiles, and two functions silently share a variable.
- Resolution runs before collection completes for the scope, so forward references work or fail depending on traversal order. The same file compiles on one machine and not another after an unrelated change moves a declaration.
- A cyclic constant dependency is not detected, and the resolver recurses until the stack overflows. The compiler crashes with no diagnostic and the user has no idea which two constants are involved.
- A hoisted binding is created without an initialised flag, so reading a
letbefore its declaration returnsundefinedinstead of throwing. The specified error becomes a silent wrong value, and the bug surfaces far from the read. - A C header declares a function with the wrong signature and the definition lives in another translation unit. The call site passes arguments per the wrong prototype, the linker is satisfied, and the callee reads garbage — the classic reason
-Wmissing-prototypesexists. - A Python module calls a function defined later at import time rather than inside a function body, and the module fails to import — but only when imported directly, not when imported after something else has run.
When it helps
- Letting programmers organise a file by importance rather than by dependency, which is almost always more readable.
- Mutual recursion, which is otherwise impossible to express without forward declarations.
- Refactoring: moving a function within a file cannot break anything, which makes automated reordering and code generation safe.
When it hurts
- Single-pass compilers and very constrained environments, where the extra pass and the complete symbol table cost memory that was the reason for the design.
- Languages where top-level code has side effects, since order-independence for *declarations* does not extend to *execution* — a Python module's statements run in order regardless of what is declared where.
- Initialisation order across compilation units, where no collection pass helps: C++'s static initialisation order fiasco is exactly this problem at a scope the compiler cannot see all of.
What it costs
Every one of these is paid by something.
- Order-independence buys readable file organisation and mutual recursion, and costs an extra traversal plus a symbol table that must be complete before any resolution — which forecloses single-pass compilation entirely.
- Requiring forward declarations buys single-pass compilation and small memory use, and costs headers, duplicated signatures that can drift out of sync, and a whole preprocessor to manage them.
- Hoisting with a temporal dead zone buys a clear error instead of a silent
undefined, and costs a runtime initialisation check on reads the engine cannot prove are after the declaration. - Topological ordering of interdependent declarations buys automatic handling of the acyclic case and costs a graph build plus cycle detection, with a diagnostic good enough to name the cycle rather than merely reporting one.
What else you could do
What a different compiler or language does instead, and when that is better.
- Require dependency order, as C does and as some proof assistants do deliberately. Simple, single-pass, and it forces a file layout that is sometimes clearer and often not.
- Explicit forward declarations only where needed, as C++ allows for types. Keeps most code order-free and gives an escape hatch for cycles.
- Whole-program collection before any resolution, as Go does at package level across all files. Order stops mattering even between files, at the cost of the package being the unit of compilation.
- Runtime definition, as Python has:
defis a statement, so "declaration order" is really execution order, and the question becomes when the code runs rather than where it sits.
See it for yourself
The flag, dump or tool that shows you this directly.
- C: compile a call to an undeclared function with
-Wall -Werror=implicit-function-declarationand see what C89 used to allow silently. Then add the prototype and diff the generated call. - JavaScript:
node -e "console.log(typeof f); function f(){}"printsfunction; the same withlet f = () => {}throws. Two lines that demonstrate both hoisting rules. - Python:
python -c "def g(): print(x); x = 1"then callg()—UnboundLocalError, because the assignment madexlocal for the whole function. - Rust: reorder two mutually recursive functions in a module and observe nothing changes; then move a
letafter its use inside a body and observe that it does. - C++:
g++ -fsyntax-onlyon a class whose method references a member declared later — legal — and the same code at namespace scope — not legal.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Hoisting moves declarations to the top of the file." Nothing moves. Bindings are created when the scope is entered; the *initialisation* stays where it is written, which is the entire reason the temporal dead zone exists.
- "Order-independence needs a complicated algorithm." It needs one extra loop over the declarations. The complications only start when declarations depend on each other cyclically.
- "If top-level declarations are order-free, locals are too." Almost no language does this. Locals are position-dependent in Java, Rust, Go, C# and JavaScript alike, and for good reason: a local's value depends on execution order in a way a function's definition does not.
- "Python hoists functions." It does not.
defexecutes; before it executes, the name does not exist. What Python decides statically is *locality*, not availability.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
Can you call a function defined further down the file? In Java, Rust, Go and most modern languages, yes — the compiler reads all the declarations first and only then resolves the uses. In C, no, unless you declare it first, which is what headers are for. In JavaScript it depends on how you declared it: function works, let throws.
practical
When a name is "not defined" and you can see it defined, check three things: whether the language requires it earlier, whether the definition is at a scope the use can reach, and — in Python — whether the module has actually executed the def by that point. Import-time code calling a function defined later in the same module is the specific Python shape of this, and it fails only when the module is entered directly.
advanced
Order-independence stops at the scope the compiler can see whole, and the interesting failures are just past that boundary. C++ static initialisation order across translation units is the canonical one: within a unit, initialisation is ordered by definition; across units it is unspecified, and no collection pass can help because the units are compiled separately. Every workaround — function-local statics, explicit init functions, constinit — is really a way of turning an unresolvable cross-unit ordering question into an intra-unit one the compiler can answer. The same shape recurs at every level: module initialisation cycles, and build-graph cycles in [[build-dependency-graph]].
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
function declarations are initialised at scope entry; var bindings are created and set to undefined; let, const and class bindings are created but remain uninitialised until their declaration is evaluated, and any access before that throws a ReferenceError. C's requirement for a prior declaration is likewise specified — implicit function declarations were removed in C99 and are an error in C23.If you were asked this in an interview
- How does a compiler let a function call one that is defined later in the file?
- Why must the declaration-collection pass avoid descending into function bodies?
- What are the three different hoisting behaviors in JavaScript, and why is the temporal dead zone an error rather than
undefined? - Two constants are defined in terms of each other. What should the compiler do, and what does a bad implementation do instead?
Connections
- DevOps / Production Engineering — Dependency graphs and topological build ordering across files and packagesThe collect-then-resolve pattern is the same dependency reasoning a build system applies to targets. The build-level version is owned there; here it is what makes a forward reference resolvable inside one scope.