Symbols and Scopes

The scoping module for AST1 is a major service in SOLP that provides scope trees and tables to the AST2 Builder.

We’ll work through using this API by considering a service that takes LSP requests to find the definition of whatever you click on in the IDE (e.g., Visual Studio Code). This won’t be the full plug-in, just the SOLP code required to make it work. The code is adapted from an existing plug-in written with the pygls and lsprotocol libraries.

Line to Node

As we saw in the Working with Source Code tutorial, SOLP lets us map nodes to source code locations easily. Usually, IDEs make requests based on the line and column number and expect the language tool to figure out what is at that location.

Let’s make a function that does that: it should take a list of possible AST1 nodes and a source location and determine the exact node that is defined at that source location. The SourceLocationSpan.does_contain(), available for every node with get_source_span(), will work for this.

All we need to do is recurse until we find the deeepest node whose span includes the location:

def get_containing_ast_node(src_loc: SourceLocation, nodes: List[Node]) -> Optional[Node]:
    for n in nodes:
        if not n:
            continue
        n_span = n.get_source_span()
        if n_span.does_contain(src_loc):
            children = list(n.get_children())
            return get_containing_ast_node(src_loc, children) if children else n
    return None

When a node has no more children, it must be the deepest node in the tree (a leaf).

Note

Since each node has a get_children() function, we can do this in a generic way without having to handle each node separately using a visitor.

Idents Only

If this node is an identifier, then we can do the reference search. If it’s anything else (a Solidity keyword, a punctuator, etc.), then we can’t get a definition.

if isinstance(ast1_node, Ident):
    return get_definitions_for_node(ast1_node)

Resolving the Reference

The reference could be qualified (e.g., x.y) or unqualified (y). The way in which y is accessed changes the scopes we need to search. The differences between the cases are the following:

  • Unqualified: Search for y in the node scope of ast1_node.

  • Qualified: Figure out the type of x, search for that type in ast1_node.scope to find a type scope, and search for y in that type scope.

Qualified lookups are modelled by the GetMember node in AST1. So far we know that y is an Ident; we need to determine what type of lookup it is.

if isinstance(ast1_node.parent, solnodes.GetMember):
    # qualified
else:
    # unqualified

Check the parent! Qualified lookups have a base x, and the member is y.

Unqualified

In the unqualified lookup case, search the node’s scope directly:

symbols = ast1_node.scope.find(ast1_node.text)
for s in symbols:
    for rs in s.res_syms():
        links.append(get_symbol_link(rs))

Note

The get_symbol_link function will be shown later.

What does res_syms do? Why not just return the symbols found in the scope?

In short, res_syms resolves symbolic links in the symbol table to their underlying symbols. This is because SOLP has different types of symbols; some are actual symbols based on elements in the real source code and some are created because of links created from inherits and imports or using statements. Since we want to locate source code elements, we need to get the underlying symbol(s).

Qualified

To get the base type of x, we’re going to cheat a bit and use the TypeHelper that’s built into the AST2 builder.

type_helper = ast2builder.type_helper

base_obj: solnodes1.AST1Node = ast1_node.parent.obj_base
base_type: solnodes2.Types = type_helper.get_expr_type(base_obj)

This bit of code is tricky, so it’s best to use Python type hints here. The Type returned from the TypeHelper is an AST2 type.

This AST2 type is passed back to the type helper to find the scopes to search:

base_scopes = type_helper.scopes_for_type(base_obj, base_type)

Search these scopes in the same way as the previous case:

for scope in base_scopes:
    symbols = scope.find(n.text)
    for s in symbols:
        for rs in s.res_syms():
            links.append(get_symbol_link(rs))

Closing Notes

While this tutorial can’t cover the entire plumbing required to make a language server for Solidity, the concepts introduced here will help you get there. In fact, most of the code in this guide is taken from our open-source demo implementation available on GitHub.