5. Resolve Cross-References¶
Once your language contains names that point to declarations, the syntax tree is no longer enough on its own. The parser can record the written text of a reference, but it cannot decide yet what that text means.
That is the purpose of the reference pipeline.
The problem¶
Consider this AST shape:
struct Feature : pegium::AstNode {
string name;
reference<Type> type;
};
The parser can fill type with the written name, for example User or
blog.User. But at parse time, that is still only reference text. The real
target node has to be found later.
The main services¶
Pegium separates this into several steps:
NameProviderdecides how nodes get namesScopeComputationdecides what symbols are exported or cached locallyScopeProviderdecides what is visible from a given reference siteLinkerresolves the written name to one concrete target
This separation matters because the same scope information is reused by several features, especially linking and completion.
For naming, Pegium keeps two related questions separate:
getName(...): what symbolic name should this node export?getNameNode(...): which CST node marks the declaration in source?
That distinction matters when a language normalizes names for indexing but still wants navigation and rename to target the original declaration text. A good default naming pattern is described in References and Scoping.
When you implement editor features, the helper utilities
named_node_info(...) and required_declaration_site_node(...) let you reuse
that naming contract without rechecking the same CST fallbacks in every
provider.
Cross-reference resolution from a high-level perspective¶
- The parser builds the AST and records reference text.
- Name and scope computation export symbols into the workspace index and prepare document-local scope data.
- The scope provider exposes the visible candidates for each reference site.
- The linker resolves the written name to one concrete target.
A real example¶
The domainmodel example overrides scope computation to export qualified names
for nested types:
services->references.scopeComputation =
std::make_unique<references::DomainModelScopeComputation>(
*services, qualifiedNameProvider);
That customization makes names such as blog.User visible in the model while
still letting the default linking flow do most of the heavy lifting. The same
example also uses the recommended AST-backed naming pattern from the reference
guide.
How to think about the problem¶
Two ideas are easy to mix up:
- exported symbols: what a document contributes to the workspace index
- visible symbols: what a concrete reference may see at one location
If linking behaves strangely, it is usually best to debug in this order:
- is the target declaration exported with the right name?
- does the scope at the reference site contain the right candidates?
- only then ask whether the linker itself needs to change
Why this step comes before heavy validation¶
Many useful features become much easier once references are linked correctly:
- unresolved-name diagnostics
- go to definition
- rename
- workspace-level navigation
- semantic checks that depend on target declarations
What to expect at the end of this step¶
At the end of this step, names in your model should resolve to the right target nodes within one file or across documents, and completion should already be able to benefit from the same scope information.