Repeat Syntax Using the Basic Storm AST
This page assumes that you have followed the setup for the syntax extension guide already,
so that you have somewhere to work. In this first stage, we will implement the repeat
syntax as a
form of macro using the facilities already present in Basic Storm.
Syntax
The first step is to create the grammar for the new syntax. To do this, create a file syntax.bnf
inside the directory repeat
. Inside the file we start by using the lang.bs
namespace to easily
access the productions in Basic Storm, and then we define the delimiters as in the
previous tutorial:
use lang.bs; optional delimiter = SDelimiter; required delimiter = SRequiredDelimiter;
After this, we can add a production that extends the SStmt
rule in Basic Storm. We pick the
SStmt
rule since the repeat
block will act as a statement (i.e. it is not necessary to put a
semicolon after it). We start simple, by adding just the syntax and saving transforms for later:
SStmt : "repeat" #keyword, "(", SExpr, ")", SStmt;
This rule matches the word repeat
(colored as a keyword), followed by a start parenthesis, an
expression that indicates the number of iterations, and end parenthesis, and a statement that
constitutes the body of the repeat
block.
We can now write a simple test in the test.bs
file as follows:
void main() { repeat(5) { print("Hello"); } }
If you try to run the test now (with storm .
), you will receive a parse error (complaining about
{
). This is because we have not yet included our package, so the new syntax is not yet visible. To
include the grammar, we simply add the following to the top of the file:
use expressions:repeat;
If we run the test yet again, we will receive the error: "No return value specified for a production that does not return 'void'." This means that we have not yet added a transform function to our production. However, due to Storm's lazy compilation, this means that the rule was successfully matched at least!
So, the next step is to add logic for the transform. We start by adding a call to a function that creates a syntax node, and passing it the repeat count and the body of the statement:
SStmt => repeatStmt(times, body) : "repeat" #keyword, "(", SExpr times, ")", SStmt body;
If we run the code now, Storm will complain that it is unable to transform a lang.bs.Expr
with
parameters ()
. This is because we have not passed the necessary parameters to the SExpr
and
SStmt
rules. If we look in the Basic Storm grammar for the definition of the rules, we see that
they accept a parameter named block
that corresponds to the block in which they are located. Since
we are a statement as well, we can simply forward our parameter of the same name. We also forward the parameter to the repeatStmt
function, since we will need it:
SStmt => repeatStmt(block, times, body) : "repeat" #keyword, "(", SExpr(block) times, ")", SStmt(block) body;
We try to run the code again. This time, we will receive an error stating that Storm is unable to
find repeatStmt
with parameters (lang.bs.Block&, lang.bs.Expr&, lang.bs.Expr&)
. This is because
we have not yet implemented the repeatStmt
function. So let's do that. Create a file sematics.bs
next to the syntax.bnf
where we created the syntax. Then we add the following:
use lang:bs; Expr repeatStmt(Block parent, Expr times, Expr body) on Compiler { Expr(core:lang:SrcPos()); }
That is, we define a function that does nothing but return an empty expression (which we need to
give a SrcPos
). At this point, our example will actually compile and run without errors. However,
since we have not yet implemented any semantics, our program will simply ignore the body of the
repeat
statement altogether (the reason it works at this stage is since we don't expect our
repeat
statement to return anything, and the no-op behavior of Expr
works for that case).
Semantics
The next step is to actually implement the behavior of our block. We do this by observing that:
repeat(<times>) { <body>; }
is equivalent to:
for (Nat i = <times>; i > 0; i--) { <body>; }
This means that we can use the
patterns functionality from the
lang.bs.macro
package to create the AST nodes for us. We have already matched the placeholders
<times>
and <body>
in our grammar. So using a pattern
, we can insert them into the new
structure using the ${...}
syntax:
use lang:bs; use lang:bs:macro; Expr repeatStmt(Block parent, Expr times, Expr body) on Compiler { pattern(parent) { for (Nat i = ${times}; i > 0; i--) { ${body}; } }; }
At this point, our language extension works as indented. If we run the test program now (storm .
),
we see that the program prints Hello
five times as intended.
It is worth noting that even though it looks like we introduce a variable i
that is visible inside
the body of the loop, this is not the case. Name lookup in Basic Storm is performed using the
block
that is passed to statements and expressions. Since we passed the block that corresponds to
the context outside of the generated for
-loop to the body of the loop, it will not be able to find
any variables we generate inside the pattern
, even though it will be placed inside the for
loop
eventually.
It is of course possible (and sometimes even necessary) to create AST nodes manually. Inspecting the grammar for Basic Storm can give some insights into what nodes exist, and how they are used.