Parsing Predecesors

In some programming languages, there are constructs that attach additional information to existing AST (Abstract Syntax Tree) nodes instead of creating new ones. For example, in Java, annotations can be used to add extra information to classes and methods. Similarly, modifiers indicate to the compiler how a method or class can be accessed.

In this guide you will learn how to parse a java like annotation. An example implementation of this language feature can be found in the FuncSample.

  1. The Syntax

We wanna implement annotations like in java so we want to parse something like this:

@Override let value = getValue()

@generate(get, set)
@log
let doSomething = ...

We have to cover two cases. An annotation with and without arguments. You see annotations look like normal function calls or names but with @ as predecessor. Many language constructs can be implemented by composing old language constructs or reusing them.

  1. The implementation

To implement this functionality we can write our own parselet by implementing the IPrefixParselet interface.

public class AnnotationParselet : IPrefixParselet
{
    public AstNode Parse(Parser parser, Token token)
    {
        var call = parser.ParseExpression();

        if (call is NameNode name) //to allow annotations without arguments we convert a name into a call
        {
            call = new CallNode(call, []);
        }

        if (call is not CallNode node) //make sure we do not have something like literals
        {
            call.AddMessage(MessageSeverity.Error, "Annotation can only be a name or call");
            return new InvalidNode(token).WithRange(token, parser.LookAhead(0));
        }

        var expr = parser.ParseExpression(); //parses the expression that we wanna annotate
        if (expr is AnnotatedNode a)
        {
            a.Annotations = a.Annotations.Add(node);
        }
        else
        {
            expr.AddMessage(MessageSeverity.Error, "Annotation cannot be used for " + expr.GetType());
        }

        return expr;
    }
}

AnnotatedNode is a subclass of AstNode with an extra Property Annotations. All nodes that can be annotated have to inherit from it instead of AstNode.

Don't forget to register our new parselet to the parser:

def.Register("@", new AnnotationParselet());
  1. Explanation

  • Parse something like a name or function call

  • Parse a expression we wanna annotate

  • Add the call as annotation to the ast node

  • Return the annotated expression

We do not explicitly handle multiple annotations, but why does it still work? This is because an annotation itself is treated as an expression, and instead of returning a new type of AST node, we simply reuse the existing one. So, when we parse the example, we look for an annotation. If the following expression is also an annotation, it gets added to the same node.

Last updated