7.4 Declarations, Bindings, Shadowing, and Imports
A declaration introduces a name; a binding connects that declaration to a semantic symbol; a use resolves to one binding. These steps sound similar but must stay separate. let count = 0; declares count. The resolver allocates Symbol#42 and binds the declaration to it. Every later count use receives Symbol#42, even if another file also spells a variable count.
The table below keeps the three concepts apart:
| concept | what it is | example |
|---|---|---|
| declaration | the syntax that introduces a name | let count = 0; |
| binding | the link from a name to a semantic symbol | count -> Symbol#42 |
| use | an occurrence resolved to some binding | count in print(count) |
Keeping these separate is what explains “same spelling, different thing”: two identically spelled names can be two independent symbols, and one symbol can be referenced by many uses.
Shadowing Is a Policy Decision
let mode = "fast";
{
let mode = "safe";
print(mode);
}
print(mode);The first print resolves to the inner symbol; the second resolves to the outer symbol. This is lexical shadowing. It can be useful for short-lived transformations, but it can also hide mistakes, especially when a parameter or imported name is shadowed. Decide separately whether to allow shadowing, warn on it, or ban selected cases. Do not confuse it with duplicate declaration in the same scope, which should usually be an error.
Forward references need an explicit rule. Function declarations are often visible throughout a module to support mutual recursion. Variables may become visible only after their initializer. Types, macros, and imports can each have different visibility phases; one universal “put every name in a map” pass is rarely accurate enough.
Imports Create Module-Level Scope Questions
An import is not just text inclusion. import math as m creates a binding m to a module; from math import sqrt creates a binding sqrt in the importing module; a wildcard import may create many bindings and conflicts. Resolve imported modules before resolving dependent bodies, detect import cycles deliberately, and give every imported symbol an origin module and declaration range.
Names can collide across local declarations, imports, builtins, and re-exports. Define a deterministic policy, then make diagnostics explain the competing origins. An import cycle might be allowed for declarations but not for initialization; that is a runtime/module-design rule, not a simple map lookup.
Hint
Represent an unresolved name, resolved symbol, ambiguous import, and error symbol as different states. Collapsing all failures into null forces every later pass to guess what happened.
Module Resolution Needs Its Own Graph
Treat modules as graph nodes and imports as directed edges. Load a module once, place it in a loading state while resolving imports, and mark it loaded only after its exported interface is ready. Seeing an edge to a loading module identifies a cycle. The language then decides whether cycles are illegal, allowed for declarations only, or supported with lazy initialization.
For example, geometry may export Point, while draw imports it and exports render. If geometry also imports render merely to run a top-level initializer, the cycle can create an uninitialized export. This is not a parser error. A useful diagnostic names the import path, symbol requested, and phase in which it was unavailable.
Import Policy Checklist
- Are imports ordered, hoisted, or resolved before local declarations?
- Can a local declaration shadow an imported name, and should it warn?
- Are re-exports explicit enough that a diagnostic can show the origin module?
- Does the resolver distinguish missing module, missing export, and ambiguous symbol?