GitHub Toggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto mode Back to homepage
Edit this page

Qualified Name Scoping

Qualified name scoping refers to a style of referencing elements using a fully qualified name. Such a fully qualified name is usually composed of the original name of the target element and the names of its container elements. You will usually see this method of scoping in C-like languages using namespaces or in Java using packages. The following code snippet shows an example of how qualified name scoping works from an end-user perspective, by using a function in a C++ namespace:

namespace Langium {
    void getDocumentation();
}

void main() {
    // Should call the `getDocumentation` function defined in the `Langium` namespace
    Langium::getDocumentation();
}

As can be seen, using qualified name scoping is quite helpful in this case. It allows us to reference the getDocumentation function through the scope computed & made available by the Langium namespace, even though it’s not directly accessible within the scope of main by itself.

Note that such behavior can also be accomplished using class member scoping. However, there is one core advantage to using globally available elements: Compared to member scoping, this type of scoping requires few resources. The lookup required for qualified name scoping can be done in near constant time with just a bit of additional computation on a per-document basis, whereas member scoping needs to do a lot of computation on a per-reference basis. With large workspaces, complex scoping might become a performance bottleneck.

This behavior can be achieved in Langium by exporting the getDocumentation function under the name Langium::getDocumentation. To do this, we will first set up a new ScopeComputation class that extends the DefaultScopeComputation. This class will be responsible for our custom scope computation. Then, we’ll want to bind our custom scope computation class in our module:

// Scope computation for our C++-like language
export class CppScopeComputation extends DefaultScopeComputation {

    constructor(services: LangiumServices) {
        super(services);
    }
}

// Services module for overriding the scope computation
// Your language module is usually placed in your `<dsl-name>-module.ts` file
export const CppModule: Module<CppServices, PartialLangiumServices & CppAddedServices> = {
    references: {
        ScopeComputation: (services) => new CppScopeComputation(services)
    }
}

Next, we can start implementing our custom scoping by overriding the computeExports function. This function is particularly important, as it allows us to change export nodes of our model using qualified names: We’ll also want to annotate this function with override, since there’s already a default definition provided.

export class CppScopeComputation extends DefaultScopeComputation {

    // Emitting previous implementation for brevity

    /**
     * Export all functions using their fully qualified name
     */
    override async computeExports(document: LangiumDocument): Promise<AstNodeDescription[]> {
        const exportedDescriptions: AstNodeDescription[] = [];
        for (const childNode of streamAllContents(document.parseResult.value)) {
            if (isFunctionDeclaration(childNode)) {
                const fullyQualifiedName = this.getQualifiedName(childNode, childNode.name);
                // `descriptions` is our `AstNodeDescriptionProvider` defined in `DefaultScopeComputation`
                // It allows us to easily create descriptions that point to elements using a name.
                exportedDescriptions.push(this.descriptions.createDescription(modelNode, fullyQualifiedName, document));
            }
        }
        return exportedDescriptions;
    }

    /**
     * Build a qualified name for a model node
     */
    private getQualifiedName(node: AstNode, name: string): string {
        let parent: AstNode | undefined = node.$container;
        while (isNamespace(parent)) {
            // Iteratively prepend the name of the parent namespace
            // This allows us to work with nested namespaces
            name = `${parent.name}::${name}`;
            parent = parent.$container;
        }
        return name;
    }

Once we start exporting functions using their fully qualified name, references such as QualifiedName::target will start working correctly. We can even nest multiple namespaces to create Fully::Qualified::Name::target. However, this leads us to another problem. We can now only reference functions using their fully qualified names, even if they’re locally available:

namespace QualifiedName {
    void target();
    void test() {
        // Will not link correctly
        target(); 
        // Requires the new fully qualified name
        QualifiedName::target();
    }
}

To rectify this problem, we have to override the computeLocalScopes method, which provides access to elements that aren’t exported globally. We can also use this method to provide secondary access to globally available objects using a local name.

export class CppScopeComputation extends DefaultScopeComputation {

    // Emitting previous implementation for brevity

    override async computeLocalScopes(document: LangiumDocument): Promise<PrecomputedScopes> {
        const model = document.parseResult.value as CppProgram;
        // This map stores a list of descriptions for each node in our document
        const scopes = new MultiMap<AstNode, AstNodeDescription>();
        this.processContainer(model, scopes, document);
        return scopes;
    }

    private processContainer(
        container: CppProgram | Namespace, 
        scopes: PrecomputedScopes, 
        document: LangiumDocument
    ): AstNodeDescription[] {
        const localDescriptions: AstNodeDescription[] = [];
        for (const element of container.elements) {
            if (isFunctionDeclaration(element)) {
                // Create a simple local name for the function
                const description = this.descriptions.createDescription(element, element.name, document);
                localDescriptions.push(description);
            } else if (isNamespace(element)) {
                const nestedDescriptions = this.processContainer(element, scopes, document, cancelToken);
                for (const description of nestedDescriptions) {
                    // Add qualified names to the container
                    // This could also be a partial qualified name
                    const qualified = this.createQualifiedDescription(element, description, document);
                    localDescriptions.push(qualified);
                }
            }
        }
        scopes.addAll(container, localDescriptions);
        return localDescriptions;
    }

    private createQualifiedDescription(
        container: Namespace, 
        description: AstNodeDescription, 
        document: LangiumDocument
    ): AstNodeDescription {
        // `getQualifiedName` has been implemented in the previous section
        const name = this.getQualifiedName(container.name, description.name);
        return this.descriptions.createDescription(description.node!, name, document);
    }
}

This new change now allows us to use local names of functions in the local scope, while they are still exported using their fully qualified name to the global scope. Another example for this style of scoping can be seen in the domain-model example language. Also, click the following note to see the full implementation of the scope computation service.

Full Implementation
export class CppScopeComputation extends DefaultScopeComputation {

    /**
     * Export all functions using their fully qualified name
     */
    override async computeExports(document: LangiumDocument): Promise<AstNodeDescription[]> {
        const exportedDescriptions: AstNodeDescription[] = [];
        for (const childNode of streamAllContents(document.parseResult.value)) {
            if (isFunctionDeclaration(childNode)) {
                const fullyQualifiedName = this.getQualifiedName(childNode, childNode.name);
                // `descriptions` is our `AstNodeDescriptionProvider` defined in `DefaultScopeComputation`
                // It allows us to easily create descriptions that point to elements using a name.
                exportedDescriptions.push(this.descriptions.createDescription(modelNode, fullyQualifiedName, document));
            }
        }
        return exportedDescriptions;
    }

    override async computeLocalScopes(document: LangiumDocument): Promise<PrecomputedScopes> {
        const model = document.parseResult.value as CppProgram;
        // This multi-map stores a list of descriptions for each node in our document
        const scopes = new MultiMap<AstNode, AstNodeDescription>();
        this.processContainer(model, scopes, document);
        return scopes;
    }

    private processContainer(
        container: CppProgram | Namespace, 
        scopes: PrecomputedScopes, 
        document: LangiumDocument
    ): AstNodeDescription[] {
        const localDescriptions: AstNodeDescription[] = [];
        for (const element of container.elements) {
            if (isFunctionDeclaration(element)) {
                // Create a simple local name for the function
                const description = this.descriptions.createDescription(element, element.name, document);
                localDescriptions.push(description);
            } else if (isNamespace(element)) {
                const nestedDescriptions = this.processContainer(element, scopes, document, cancelToken);
                for (const description of nestedDescriptions) {
                    // Add qualified names to the container
                    // This could also be a partially qualified name
                    const qualified = this.createQualifiedDescription(element, description, document);
                    localDescriptions.push(qualified);
                }
            }
        }
        scopes.addAll(container, localDescriptions);
        return localDescriptions;
    }

    private createQualifiedDescription(
        container: Namespace, 
        description: AstNodeDescription, 
        document: LangiumDocument
    ): AstNodeDescription {
        const name = this.getQualifiedName(container.name, description.name);
        return this.descriptions.createDescription(description.node!, name, document);
    }

    /**
     * Build a qualified name for a model node
     */
    private getQualifiedName(node: AstNode, name: string): string {
        let parent: AstNode | undefined = node.$container;
        while (isNamespace(parent)) {
            // Iteratively prepend the name of the parent namespace
            // This allows us to work with nested namespaces
            name = `${parent.name}::${name}`;
            parent = parent.$container;
        }
        return name;
    }
}