Skip to content

Qualified Names

Qualified name scoping lets users reference declarations through names such as billing.Invoice or company.sales.Customer.

Pegium's domainmodel example uses this pattern for packages and types. The important idea is that globally exported names can be qualified while local lookup can still keep short names available inside the right container.

What is the problem?

Qualified names are useful as soon as one short name is no longer enough. Once users can write names such as blog.User or sales.Invoice, the language can disambiguate declarations without forcing everything into one flat namespace.

At the same time, users usually still expect local short names to work inside the right container. That means qualified names are rarely just a lookup problem. They are usually a question of how symbols are exported and how local symbols are precomputed.

What needs to change?

For qualified names, you usually do not replace the linker. Instead, you customize the exported names and the local symbols that the default scope provider later consumes.

The domainmodel example does this with:

  • a QualifiedNameProvider
  • a custom DomainModelScopeComputation
  • the existing default scope provider

This approach is also usually cheaper than a heavily custom scope lookup, because most of the work is done once per document during scope computation instead of once per reference during completion or linking.

Step 1: define how names are joined

examples/domainmodel/src/references/QualifiedNameProvider.cpp keeps the rule small and explicit:

std::string QualifiedNameProvider::getQualifiedName(std::string_view qualifier,
                                                    std::string_view name) const {
  if (qualifier.empty()) {
    return std::string(name);
  }
  return std::string(qualifier) + "." + std::string(name);
}

That helper is also used recursively for nested packages.

Step 2: export global symbols under qualified names

DomainModelScopeComputation::collectExportedSymbols(...) walks the model, tracks the current qualifier, and publishes types under their fully qualified name:

if (const auto *package =
        dynamic_cast<const PackageDeclaration *>(element.get())) {
  const auto nestedQualifier =
      _qualifiedNameProvider != nullptr
          ? _qualifiedNameProvider->getQualifiedName(qualifier, package->name)
          : package->name;
  collectExportedSymbols(package->elements, nestedQualifier, document, symbols,
                         cancelToken);
  continue;
}

Later, when a type description is created, the exported name becomes something like foo.bar.Customer.

Step 3: keep local names usable inside containers

Only exporting qualified names is not enough. Inside a package, users still expect short names to work.

The same example therefore overrides collectLocalSymbols(...) and builds local descriptions per container. Nested descriptions are re-added with a qualified form where needed, then stored in document.localSymbols.

That is the data structure consumed later by the default scope provider.

Why the default scope provider still works

pegium::references::DefaultScopeProvider already knows how to combine:

  • local symbols from the current container chain
  • exported symbols from the workspace index

So once your scope computation emits the right names, the default lookup logic often remains sufficient.

This is why qualified-name support is usually a scope-computation problem rather than a linker problem.

When to go further

If your language also needs imports, visibility modifiers, or access rules that depend on the reference site, continue with Custom Scope Provider.