Khaled Hammouda
Home
Fusion OS
NimJet
Home
Fusion OS
NimJet
  • NimJet
    • Introduction
    • Project Setup
    • File Type
    • Lexer
    • Parser
    • Parser Implementation
    • Grammar-Kit
    • Go To Declaration
    • Scope Resolution
    • Rename Refactoring
    • Indentation
    • Declaration Sections
    • Comments
    • Numeric Literals
    • Expressions

Go To Declaration

Now that we've got the basics in place, let's add our first actual feature: Go To Declaration. Consider this simple Nim program:

let msg = "hello"   # <- declaration
echo msg            # <- reference

We want to be able to navigate from the identifier msg in echo msg to the declaration of msg on the first line. In IntelliJ terminology, the msg identifier on the second line is called a reference, which can be resolved to its declaration element.

The IDE knows that a PSI element is a reference if its getReference() method returns a PsiReference object. This object is not the target element, but rather an object that can resolve the reference to its target by invoking its resolve() method. The implementation of the resolve() method will have to locate the target element in the PSI tree, which can get complex depending on the scoping rules of the language.

Let's take a look at the AST generated by the Nim compiler for this code snippet. We can use the dumpTree macro from the macros module to print the AST:

# dumptree.nim
import std/macros

dumpTree:
  let msg = "hello"
  echo msg

Macros are evaluated at compile time, so to see the output we just need to compile the program using nim c dumptree.nim:

StmtList
  LetSection
    IdentDefs
      Ident "msg"
      Empty
      StrLit "hello"
  Command
    Ident "echo"
    Ident "msg"

The Ident node on line 4 is the declaration of msg, and the one on line 9 is the reference to that declaration. In Nim, one of the places where declarations can be found is the LetSection node, as seen in the output above. So, we need to implement getReference on the element representing the reference to return a PsiReference object. That object will implement resolve to find and return the declaration under the LetSection node.

However, instead of creating a dedicated class for the reference, we can use the PSI class representing the identifier itself; we just need to make it implement the PsiReference interface. One more thing, even though the Nim AST defines both the declaration and the reference as Ident nodes, it will make our lives easier if we create a separate PSI class for each, e.g. IdentDecl and IdentRef. This way, we can avoid any confusion between the two, and will make identifying IdentDecl in the tree easier when resolving a reference.

Updating the Grammar

Let's start by updating the grammar to support the two statements in the example above. We'll use a simplified version of the Nim AST to guide us:

// src/main/kotlin/khaledh/nimjet/parser/Nim.bnf
{
  ...
}

Module     ::= !<<eof>> StmtList
StmtList   ::= Stmt*
Stmt       ::= LetSection
             | Command

LetSection ::= LET IdentDecl EQ STRING_LIT

Command    ::= IdentRef IdentRef

IdentDecl  ::= IDENT
IdentRef   ::= IDENT

You'll notice that I simplified the LetSection rule to allow only a single declaration for now (we'll come back to handling multiple declarations in a single let section later), using IdentDecl instead of an undifferentiated IDENT. The Command rule is also simplified to only allow two IdentRef elements.

If we generate the parser and PSI classes now, we'll have the following files:

src/main/gen
└── khaledh/nimjet
    ├── parser
    │   ├── NimElement.java
    │   └── NimParser.java
    └── psi
        ├── impl
        │   ├── CommandImpl.java
        │   ├── IdentDeclImpl.java
        │   ├── IdentRefImpl.java
        │   ├── LetSectionImpl.java
        │   ├── Stmt.java
        │   └── StmtListImpl.java
        ├── Command.java
        ├── IdentDecl.java
        ├── IdentRef.java
        ├── LetSection.java
        ├── Stmt.java
        └── StmtList.java

Reference Mixin

Our goal is to be able to navigate from a reference to its declaration. In our case, the reference element is IdentRef, and the declaration element is IdentDecl. The reference element needs to override the getReference method to return a PsiReference object. But how do we tell Grammar-Kit to generate the getReference method for IdentRef? The answer is using mixins. The way mixins work in Grammar-Kit is that we create a mixin class for a particular element, and then we tell Grammar-Kit to mix that class into the generated PSI class for that element. So, let's go ahead and create a mixin class for IdentRef that implements the getReference method:

// src/main/kotlin/khaledh/nimjet/psi/impl/IdentRefMixin.kt
...

abstract class IdentRefMixin(node: ASTNode) : ASTWrapperPsiElement(node), IdentRef {

    override fun getReference(): PsiReference {
        return IdentReference(this, TextRange(0, this.textLength))
    }

}

We haven't implemented IdentReference yet; we'll do that in a moment. But first, notice that the mixin class extends ASTWrapperPsiElement, and implements the IdentRef interface generated by Grammar-Kit. Normally, this signature would be implemented by the generated IdentRefImpl class, but when we tell Grammar-Kit to use a mixin class for an element, it will use the mixin class as the base class for the element instead. So, the generated IdentRefImpl class will extend IdentRefMixin, and that's why the mixin class needs to take over the responsibility of extending ASTWrapperPsiElement.

Let's update the BNF file to use the mixin:

{
  ...
  mixin("IdentRef")="khaledh.nimjet.psi.impl.IdentRefMixin"
}
...

Now, if we generate the parser and PSI classes, we'll see that the IdentRefImpl class extends IdentRefMixin instead of ASTWrapperPsiElement:

// src/main/gen/khaledh/nimjet/psi/impl/IdentRefImpl.java
...

public class IdentRefImpl extends IdentRefMixin implements IdentRef {
    ...
}

Now, let's implement the IdentReference class, which represents the reference to an identifier. This class will implement the PsiReference interface, and its resolve method will find and return the declaration element in the PSI tree.

// src/main/kotlin/khaledh/nimjet/psi/IdentReference.kt
...

class IdentReference(element: IdentRef, textRange: TextRange)
    : PsiReferenceBase<IdentRef>(element, textRange) {

    override fun resolve(): PsiElement? {
        // This is a naive implementation; we'll improve it later
        val identDecl = PsiTreeUtil.findChildOfType<IdentDecl>(
            myElement.containingFile, IdentDecl::class.java
        )
        return if (identDecl?.name == myElement.text) identDecl else null
    }

}

The resolve method uses the PsiTreeUtil utility class to find the first child of the file that is an IdentDecl element. It then checks if the name of the declaration matches the text of the reference, and returns the declaration element if it does.

Obviously, this is a naive implementation that works only if the first declaration in the file is the one we're looking for. Another thing I'm ignoring here is Nim's insensitivity to naming style (e.g. myVar and my_var are considered the same). This is just simple way to demonstrate the concept. We'll improve it later.

Declaration Mixin

Now that we have the reference side of things set up, let's move on to the declaration side. The platform docs state that:

Every PSI element that can be renamed or referenced needs to implement [the] PsiNamedElement interface.

It also mentions, in a different section, that:

In order to enable more advanced functionality, prefer implementing PsiNameIdentifierOwner over PsiNamedElement where possible.

It's not clear to me in what context and what features PsiNameIdentifierOwner provides over PsiNamedElement. From what I can tell, PsiNameIdentifierOwner is used when the name of the element is not directly accessible from the element itself, but is instead stored in a separate child element. In our case, the actual name of the identifier is stored in the IDENT token, which is a child of the IdentDecl element. So, we'll go with PsiNamedElement and implement its getNameIdentifier method to return the IDENT element.

Let's create a mixin class for IdentDecl that implements PsiNameIdentifierOwner:

abstract class IdentDeclMixin(node: ASTNode) : ASTWrapperPsiElement(node), IdentDecl {

    override fun getNameIdentifier(): PsiElement? = ident

    override fun getName(): String = ident.text

    override fun setName(name: String): PsiElement {
        TODO("Not yet implemented")
    }
}

The getNameIdentifier method returns the IDENT token element. Let's look at how it's implemented in the generated IdentDeclImpl class:

public class IdentDeclImpl extends IdentDeclMixin implements IdentDecl {

  public IdentDeclImpl(@NotNull ASTNode node) {
    super(node);
  }

  @Override
  @NotNull
  public PsiElement getIdent() {
    return findNotNullChildByType(IDENT);
  }

}

The implementation generated by Grammar-Kit has a getIdent method that finds the IDENT child token. Since we're using Kotlin, the getIdent method is automatically converted to a property ident in the Kotlin class. This is why we can access ident directly in the mixin class.

Let's update the BNF file to use the mixin, and also tell Grammar-Kit that IdentDecl implements PsiNameIdentifierOwner:

{
  ...
  implements("IdentDecl")="com.intellij.psi.PsiNameIdentifierOwner"
  mixin("IdentDecl")="khaledh.nimjet.psi.impl.IdentDeclMixin"
}
...

Now, let's generate the parser and PSI classes and test the Go To Declaration feature in the sandbox IDE.

Ident Reference and Declaration

Notice that the reference and declaration are highlighted when the cursor is on either of them. This is because the IDE recognizes that the reference can be resolved to the declaration, and it highlights the declaration when the reference is selected (and vice versa). To test the navigation feature, place the cursor on the reference and press Ctrl+B (or Cmd+B on macOS) to navigate to the declaration. We can also place the cursor on the declaration and press Ctrl+B to navigate to the reference. Neat!

Let's also take a look at the PSI tree:

Ident Ref and Decl PSI

If we click on the IdentRef node, we can see that it has a reference to the IdentDecl node. Great!

In the next section, we'll improve reference resolution to take into account scoping rules, so that references are resolved to their correct declaration.

Last Updated:
Prev
Grammar-Kit
Next
Scope Resolution