Raku Books / Creating a Compiler in Raku / Evaluating AST

Working with variables

The variables can be stored inside the LinguaEvaluator objects, and we can add our data storage to it:

class LinguaEvaluator {
    has %!var;

    . . .
}

Use this hash to create the first variable:

multi method eval-node(AST::ScalarDeclaration $node) {
    %!var{$node.variable-name} = 
        $node.value ~~ AST::Null ?? 0 !! $node.value.value;
}

All the information for the evaluator is taken from an AST node. In this case it tells us its type (AST::ScalarDeclaration is scalar declaration), the name of the variable ($node.variable-name) and the content of the variable ($node.value). If there’s no value (the $node.value attribute contains AST::Null), let us store 0 in a variable. Otherwise, just copy the value attribute of the value node.

For debugging, add a couple of dd calls to display the AST and the variable storage content:

method eval(ASTNode $top) {
    dd $top;
    self.eval-node($_) for $top.statements;
    dd %!var;
}

Now the program can execute our first example and successfully create and initialise the variable:

Hash %!var = {:a("7")}

This is all great, so now let the program itself print the value of the variable:

my a = 7;
say a;

The AST of this program contains two statements:

AST::TOP.new(
    statements => Array[ASTNode].new(
        AST::ScalarDeclaration.new(
            variable-name => "a",
            value => AST::NumberValue.new(
                value => 7
            )
        ),
        AST::FunctionCall.new(
            function-name => "say",
            value => AST::Variable.new(
                variable-name => "a"
            )
        )
    )
)

The second statement is an AST::FunctionCall node, keeping the name of the function and in another AST node, in this case the AST::Variable node referring to the variable.

The evaluator should now find a multi-method for the function call node. Here it is:

multi method eval-node(AST::FunctionCall $node) {
    self.call-function($node.function-name, $node.value);
}

As we expect that there will be more than one function, and their arguments can be of different type, let us implement another set of multi-methods specifically for each function and its arguments. For the given program, we need a say function printing a variable:

multi method call-function('say', AST::Variable $value) {
    say %!var{$value.variable-name};
}

This method does the final job: it accesses the variable storage and prints the variable to the output.

It is now obvious how printing numbers and strings can be implemented using multi-methods:

multi method call-function('say', AST::NumberValue $value) {
    say $value.value;
}

multi method call-function('say', AST::StringValue $value) {
    say $value.value;
}

With these additions, the compiler can handle the following program:

my a = 7;
say a;

say 8;
say "nine";

A few other steps can be done to allow other simple operations. We start with a scalar assignment:

a = 10;

There’s a variable on the left and a NumberValue on the right.

AST::ScalarAssignment.new(
    variable-name => "a",
    rhs => AST::NumberValue.new(
        value => 10
    )
)

The value is directly accessible via the $.value attribute:

multi method eval-node(AST::ScalarAssignment $node) {
    %!var{$node.variable-name} = $node.rhs.value;
}

The good part about AST is that its nodes represent quite simple actions. Such as variable declaration or assignment. It means that the implementation is straightforward enough.

Let us teach the evaluator to create array and hashes:

my data[];
my relation{};

To implement it, we have to define two more multi-methods for AST::ArrayDeclaration and AST::HashDeclaration:

multi method eval-node(AST::ArrayDeclaration $node) {
    %!var{$node.variable-name} = Array.new;
}

multi method eval-node(AST::HashDeclaration $node) {
    %!var{$node.variable-name} = Hash.new;
}

After it run, the variable storage contains the desired objects:

Hash %!var = {:data($[]), :relation(${})}

We also have to think about array and hash initialisation an assignment, but let us first work with single elements:

data[3] = 4;
color{"red"} = "#FF0000";

Assignments to the elements of arrays and hashes are as simple as that:

multi method eval-node(AST::ArrayItemAssignment $node) {
    %!var{$node.variable-name}[$node.index] = $node.rhs.value;
}

multi method eval-node(AST::HashItemAssignment $node) {
    %!var{$node.variable-name}{$node.key} = $node.rhs.value;
}

The next logical step is to implement array and hash assignments:

data = 7, 8, 9;
color = "white": "#FFFFFF", "black": "#000000";

Codewise, this is about two more multi-methods:

multi method eval-node(AST::ArrayAssignment $node) {
    %!var{$node.variable-name} = 
        ($node.elements.map: *.value).Array;
}

multi method eval-node(AST::HashAssignment $node) {
    %!var{$node.variable-name} = Hash.new;
    while $node.keys {
        %!var{$node.variable-name}.{$node.keys.shift} =
            $node.values.shift.value;
    }
}

We did not talk about initialization yet, but you will be surprised that the following code already works:

my array[] = 1, 3, 4;
my h{} = "a": 1, "b": 2;

This is possible because in the AST tree, array and hash declaration with initialization is presented by the same AST nodes: AST::ArrayAssignment and AST::HashAssignment.

Course navigation

Evaluating from the TOP   |   String interpolation