Raku Books / Creating a Compiler in Raku / Functions / User-defined functions

Defining a function

A definition can appear wherever a statement can, so the TOP rule receives one more alternative:

rule TOP {
    [
        | <one-line-comment>
        | <statement=function-definition>
        | <statement=conditional-statement>
        | <statement=loopped-statement>
        | <statement=while-statement>
        | <statement> ';'
    ]*
}

The rule itself re-uses two things we already have: the variable-name token for both the function name and the parameter names, and the block rule for the body:

rule function-definition {
    'func' <function-name=variable-name>
        '(' <parameter=variable-name>* %% ',' ')'
        <block(';')>
}

Notice the aliases. The same variable-name token parses both kinds of names, but in the match object we want to distinguish the name of the function from the names of its parameters. With the aliases, the former lands in $<function-name>, and the latter in the $<parameter> array.

The body of a function may contain the return instruction, which is a new type of statement:

rule return-statement {
    'return' <value>
}

rule statement {
    | <statement=variable-declaration>
    | <statement=return-statement>
    | <statement=assignment>
    | <statement=function-call>
}

Two new AST node types support the additions. A function definition keeps its name, the list of the parameter names, and the statements of the body. A return node keeps the value it has to return:

class AST::FunctionDefinition is ASTNode {
    has Str $.function-name;
    has Str @.parameters;
    has ASTNode @.statements;
}

class AST::Return is ASTNode {
    has ASTNode $.value;
}

The action methods build the nodes from the parsed data:

method function-definition($/) {
    $/.make(AST::FunctionDefinition.new(
        function-name => ~$<function-name>,
        parameters => ($<parameter> ?? ($<parameter>.map: ~*) !! ()),
        statements => ($<block><statement>.map: *.made),
    ));
}

method return-statement($/) {
    $/.make(AST::Return.new(
        value => $<value>.made,
    ));
}

The check of $<parameter> covers the functions that take no arguments at all: in that case, there is nothing to map over.

What should the evaluator do when it meets a function definition? Nothing should be executed, but the definition must be remembered so that future calls can find it. A new hash in the evaluator, %!func, plays the same role for functions that %!var plays for variables:

class LinguaEvaluator {
    has %.var;
    has %!func;
    . . .
    multi method eval-node(AST::FunctionDefinition $node) {
        %!func{$node.function-name} = $node;
    }
}

Run a program that defines a function, and find its complete body stored inside a single node of the tree:

func greet(name) {
    say "Hello, $name!";
}
AST::FunctionDefinition.new(
    function-name => "greet",
    parameters => Array[Str].new("name"),
    statements => Array[ASTNode].new(
        AST::FunctionCall.new(
            function-name => "say",
            . . .

The function is parsed and remembered, but there is no way to call it yet.

Course navigation

User-defined functions   |   Calling a function