Implementing forward references does indeed require two passes: one to discover the set of available definitions, and another to resolve names to those definitions.
(Type names can typically be resolved in the same way as other names, and type checking is a separate problem entirely, so I will just talk about name resolution here. I will also stick to early binding and lexical scoping- sometimes forward references are actually implemented by runtime name lookup/late binding, but that seems unlikely to apply to this question about types.)
One direct approach is to build a tree of scopes, each represented as a map of names to definitions. The structure of this tree will typically match the structure of the syntax tree pretty closely, with (in Java, for example) a package-level root node, child nodes for classes, grandchild nodes for methods, etc.
These passes don't necessarily need to run on the entire program. Rather, the first pass only needs to process definitions that are in scope and thus may be needed by the second pass. So, another approach replaces the tree of scopes with a stack of scopes: collect all the definitions (or at least those can be used by forward reference) in a scope and push that onto the stack before descending into nested scopes; pop it off the stack when leaving the scope.
Resolving a reference involves walking up the tree or stack, starting from the referencing scope and going up to parents until a matching definition is found.
A few potential complications:
- Qualified names involve multiple individual name lookups, often using different rules about how to walk the scope tree. (For example, you may not want
Foo.Bar to find a Bar in some parent of Foo.)
- Imports, and especially globs, can influence the walk as well. Sometimes this involves coming up with some rules around how to resolve, report, or avoid ambiguities and cycles.
- Some language features can tangle up these two passes even further, when figuring out what a definition even means depends on some amount of name lookup. For example, macros that expand to definitions sometimes run into this.