Semanticsspec

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.

The question

Can a function call another one that is defined further down the file, and what does the compiler have to do to allow it?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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, var as undefined, and let/const as 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.

Collect, then resolve
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 is
8 // 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

specEvery row is specified language behavior. Two are routinely misremembered. Python has no hoisting: 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.

May a use precede its declaration?spec
LanguageFunction at file/module scopeLocal variableMechanism
CNo — needs a prior declarationNoSingle-pass by design; headers supply the forward declarations
C++No at namespace scope; yes for members inside a class bodyNoThe class body is a complete scope; namespace scope is not
Java / C# / KotlinYes, anywhere in the classNo — locals are position-dependentA member-collection pass before body resolution
RustYes, anywhere in the moduleNo — let is position-dependentItems are collected module-wide before resolution
GoYes at package level, across filesNoPackage-level declarations are collected before any body is checked
JavaScriptYes for function; let/const throw before their declarationvar is undefined; let/const throwHoisting: bindings are created on scope entry, initialisation is separate
PythonOnly if the call executes after the def runsName is local for the whole function; reading before assignment raisesA 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 let before its declaration returns undefined instead 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-prototypes exists.
  • 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: def is 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-declaration and 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(){}" prints function; the same with let f = () => {} throws. Two lines that demonstrate both hoisting rules.
  • Python: python -c "def g(): print(x); x = 1" then call g()UnboundLocalError, because the assignment made x local for the whole function.
  • Rust: reorder two mutually recursive functions in a module and observe nothing changes; then move a let after its use inside a body and observe that it does.
  • C++: g++ -fsyntax-only on 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. def executes; before it executes, the name does not exist. What Python decides statically is *locality*, not availability.

Misconceptions

The claim, and what is actually true.

Headers exist because C is old.
Headers exist because C was designed to compile in one pass with minimal memory, and a single-pass compiler cannot resolve a forward reference. The design choice has a reason, and modules are the modern replacement for its consequences.
Hoisting is a JavaScript quirk with no analogue elsewhere.
Every order-independent language does the same thing — it collects declarations before resolving uses. JavaScript is unusual only in that it hoists the *binding* separately from the initialisation and made that observable.
Mutual recursion needs special support in the compiler.
It needs the collection pass, which is already there for forward references. A body needs only the callee's signature, and the collection pass recorded it.

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.

specHoisting behavior in JavaScript is fully specified and differs by declaration form: 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.
typicalThe two-pass collect-then-resolve structure describes mainstream compilers for order-independent languages, but the granularity differs: Java collects per class, Rust per module, Go per package across all its files. That granularity is what decides whether two declarations in *different files* may refer to each other without an import, which is a visible language difference and not an implementation detail.
simplifiedOur collection pass registers a name and a signature. Real implementations must decide how much of a declaration to process eagerly: enough of a type to know its size (for layout), enough of a constant to detect cycles, enough of a generic signature to check callers — and each thing processed eagerly is a place where a dependency cycle can appear that the simple version never meets.

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

Domains that do not exist yet
  • DevOps / Production Engineering — Dependency graphs and topological build ordering across files and packages
    The 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.