Chapter 11. Control Flow

栏目: IT技术 · 发布时间: 4年前

内容简介:We started Chapter 8 with an attempt to implement theThe language construction we are going to implement first is the prefixThe

Chapter 11. Control Flow This is a chapter from

Creating a compiler with Raku

We started Chapter 8 with an attempt to implement the if keyword. It was tricky to do so in the interpreter that executes the code line by line. Introducing the AST allowed to parse the program without executing it, so we can first build the node and only run the code behind it if the condition is true.

Implementing if

The language construction we are going to implement first is the prefix if clause, demonstrated on the following example:

my flag = 42;
if flag say "Depends";
say "Always";

The if keyword is followed by a value (which is a condition) and a statement. A value is a  value in the sense of our grammar, thus it is either a variable as in the example, or a string, or an expression. A statement can be any construct recognised by the  statement rule.

Embedding the if clause to the grammar is simple, as we have all statement definitions in one rule:

<strong>rule condition {
</strong>    <strong>'if' <value>
}
</strong>
rule statement {
    <strong><condition> ?</strong> [
        | <statement=variable-declaration>
        | <statement=assignment>
        | <statement=function-call>
    ]
}

We have seen earlier that we should not execute the statement before we know if the condition is true. In the AST world, we can pick the statement node out of the tree and place it to an attribute of the condition node. Later, the evaluator decides whether to execute the statement node. So, we need a new kind of an AST node:

class <strong>AST::Condition</strong> is ASTNode {
    has ASTNode $.value;
    has ASTNode $.statement;
}

In the actions class, the method trigged in response to a parsed statement should now be split into two branches. When there is no if clause, everything remains as is. When there is such a clause, the statement node is enters the tree via a proxy AST::Condition node:

multi method statement($/ where !$<condition>) {
$/.make($<statement>.made);
}

multi method statement($/ where $<condition>) {
$/.make(AST::Condition.new(
value => $/<condition><value>.made,
statement => $/<statement>.made,
));
}

The whole AST for the test program is drawn below.

Chapter 11. Control Flow

Take a look at the two calls of say . The first call is a child of the  AST::Condition node. The second one (in the right side of the diagram) is located at the top level inside the TOP’s  @.statements array.

The tree is ready and we can evaluate it. The LinguaEvaluator class needs a way to react to the  AST::Condition type of AST nodes. If should check the condition value and pass execution further if needed.

multi method eval-node(AST::Condition $node) {
if $node.value.value {
self.eval-node($node.statement);
}
}

That’s all! Test the program with different values of the flag variable and see how the program reacts.

To complete the implementation, add a test in the t directory:

my flag = 1;
if flag say "Printed"; <strong>## Printed</strong>

flag = 0;
if flag say "Ignored";

say "Done"; <strong>## Done</strong>

Implementing else

A logical next step is implementing the else clause. Let us expand the test program:

my flag = 0;
if flag say "Depends" else say "Otherwise";
say "Always";

The else keyword can only appear if there was an  if before. In our current grammar, adding the  else block after the statement is tricky. It is better to re-organise the grammar so that the  statement rule contains different variants of statements only.

Let us allow the ifelse construct on the top level:

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

rule conditional-statement {
'if' <value> <statement>
[
'else' <statement>
]?
}

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

Notice that in the TOP rule, the new alternative is given an alias,  statement , which let us keep the same loop in the corresponding action:

method TOP($/) {
    . . .
    for $<statement> -> $statement {
        $top.statements.push($statement.made);
    }
    . . .
}

OK, we can have one or two statements in the conditional construct now. The AST::Conditional node gets the second attribute:

class AST::Condition is ASTNode {
    has ASTNode $.value;
    has ASTNode $.statement;
    has ASTNode <strong>$.antistatement;
</strong>}

The new attribute is filled with either AST::Null (if there is no else clause) or with a  AST::Statement node:

method conditional-statement($/) {
    $/.make(AST::Condition.new(
        value => $/<value>.made,
        statement => $/<statement>[0].made,
        antistatement => $/<statement>[1] ?? 
                         $/<statement>[1].made !! AST::Null,
    ));
}

The statement action can now be reverted to a single method with a trivial body:

method statement($/) {
    $/.make($<statement>.made);
}

Finally, the evaluator must deside which statement to execute:

multi method eval-node(<strong>AST::Condition</strong> $node) {
    <strong>if</strong> $node.value.value {
        self.eval-node($node.<strong>statement</strong>);
    }
    <strong>else </strong>{
        self.eval-node($node.<strong>antistatement</strong>);
    }
}

We have to implement the no-operation behaviour of the AST::Null node too:

multi method eval-node(AST::Null) {
}

The else keyword is ready and functioning, and a test  t/else.t must be added to the repository:

if 1 say "A" else say "B"; <strong>## A</strong>
if 0 say "A" else say "B"; <strong>## B</strong>

my x;
if 1 x = 10 else x = 20;
say x; <strong>## 10</strong>

if 0 x = 30 else x = 40;
say x; <strong>## 40</strong>

Implementing a loop

Let us implement a loop in our Lingua language. At the moment, let us make it simple and allow only the  loop keyword with the counter variable decremented by the compiler.

my n = 5;
loop n say n;

This program prints the values from 5 to 1 and stops.

The syntax of the loop clause is similar to the  if condition we saw in this chapter, and it can be embedded on the top level of the grammar too:

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

<strong>rule loopped-statement</strong> {
    'loop' <variable-name> <statement>
}

Next, we need a new type of AST node:

class <strong>AST::Loop</strong> is ASTNode {
    has ASTNode $.variable;
    has ASTNode $.statement;
}

The syntax is chosen in such a way so that you have to provide the loop with an already existing variable. In this case, we do not need to create a temporary loop variable. In the action method, a reference to the variable is kept in an AST::Variable node:

method loopped-statement($/) {
    $/.make(AST::Loop.new(
        <strong>variable</strong> => AST::Variable.new(
            variable-name => ~$/<variable-name>
        ),
        <strong>statement</strong> => $/<statement>.made,
    ));
}

Decreasing the counter is done in the evaluator:

multi method eval-node(AST::Loop $node) {
    while %!var{$node.variable.variable-name} {
        self.eval-node($node.statement);
        <strong>%!var{$node.variable.variable-name}--</strong>;
    }
}

It may be safer to re-write the stop condition a bit:

while %!var{$node.variable.variable-name} <strong>> 0</strong> {
    . . .

This allows to avoid infinite loops when the initial value is not an integer number, for example:

my n = 5.5;
loop n say n;

Let’s save our achievements in the test suite. As the loop prints a few lines, you should add them all to the test file:

my n = 3;
loop n say n;
<strong>## 3
## 2
## 1</strong>

Statement blocks

Until now, both if and  else constructs could contain only a single statement. Of course, if you need more than one, you can always add an  if clause to each of them, but it would be much nicer to implement support of  code blocks , which is present in many other languages. The goal of this section is to execute the following program with the blocks directed by both  ifelse and  loop :

my c = 0;
if c <strong>{</strong>
    say "c = $c";
    say "IF block";
<strong>}</strong>
else <strong>{</strong>
    say "c = $c";
    say "ELSE block";
<strong>}</strong>

my n = 5;
loop n <strong>{</strong>
    say n;
    say 2 * n;
<strong>}</strong>

Let us make a first attempt to replace a statement with a newly defined  block . If the block contains a single instruction, curly braces are optional:

rule <strong>block</strong> {
    | '{' ~ '}' <statement>* %% ';'
    | <statement> ';'
}

The '{' ~ '}' <X> construct in Raku is equivalent to  '{' <X> '}' , but it often looks cleaner, especially when X is a represented by a big piece of code.

On a conceptual level, we have to allow code blocks where a statement is allowed in the grammar. From a practical perspective, we have to think about semicolons at the end of the statements.

For example, a semicolon is not allowed before the else keyword:

if 1 say "A"<strong>;</strong> else say "B";

The problem is that it still stands after a statement in the grammar. So, we should distinguish between a statement in the if clause with and without the  else clause.

Also, we should not insist on a semicolon after the closing curly brace:

loop n {
say n;
say 2 * n;
};

And finally, a semicolon before the closing brace can be declared optional:

loop n {
    say n;
    say 2 * n
}

To satisfy all the conditions listed above, let us tell the grammar where we expect a semicolon and where it is not needed. We can introduce two multi-alternatives of the block rule (remember how we were adding arguments to the methods in our grammar for the calculator).

multi rule block() {
| '{' ~ '}' <statement>* %% ';'
| <statement>
}

multi rule block(';') {
| '{' ~ '}' <statement>* %% ';'
| <statement> ';'
}

By doing so, we do not add a new rule name. Now, we can rewrite the rules for conditions and preliminary introduce the while loop (we’ll return to it at the end of the chapter):

rule conditional-statement {
| 'if' <value> <block()> 'else' <block(';')>
| 'if' <value> <block(';')>
}

rule loopped-statement {
'while' <variable-name> <block(';')>
}

Notice that in the conditional-statement rule, it was also possible to use square brackets to extract the common part:

rule conditional-statement {
'if' <value> [
| <block()> 'else' <block(';')>
| <block(';')>
]
}

You may prefer either; my own personal preference is using the form where you see both branches one after another:

    | 'if' <value> <block()> 'else' <block(';')>
    | 'if' <value> <block(';')>

Finally, adjust the TOP rule to remove extra semicolons from there:

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

Now to the actions part. First, lets us change the actions for the conditional constructs and use blocks there:

method conditional-statement($/) {
$/.make(AST::Condition.new(
value => $/<value>.made,
statement => $/<block>[0]<statement>[0].made,
antistatement => $/<block>[1] ??
$/<block>[1]<statement>[0].made !! AST::Null,
));
}

method loopped-statement($/) {
$/.make(AST::Loop.new(
variable => AST::Variable.new(
variable-name => ~$/<variable-name>
),
statement => $/<block><statement>[0].made,
));
}

Although the result contains long chains of keys and indices of the match object, such as $/<block>[1]<statement>[0] , the transformation is simple and easy to understand. You can test the program, and it will execute the first statement from each curly block.

The next step is to modify the AST so that it keeps more than one statement where a block is allowed. We have to keep all of them, so let us use arrays instead of scalars:

class AST::Condition is ASTNode {
has ASTNode $.value;
has ASTNode @.statements;
has ASTNode @.antistatements;
}

class AST::Loop is ASTNode {
has ASTNode $.variable;
has ASTNode @.statements;
}

The corresponding actions should create the new AST nodes and pass all the statements from the code blocks. In the next fragment, we are using map s to loop over the statements:

method conditional-statement($/) {
    $/.make(AST::Condition.new(
        value => $/<value>.made,
        statements => <strong>($/<block>[0]<statement>.map: *.made</strong>),
        antistatements => $/<block>[1] ?? 
            (<strong>$/<block>[1]<statement>.map: *.made</strong>) !! (AST::Null),
    ));
}

method loopped-statement($/) {
    $/.make(AST::Loop.new(
        variable => AST::Variable.new(
            variable-name => ~$/<variable-name>
        ),
        statements => (<strong>$/<block><statement>.map: *.made</strong>),
    ));
}

Blocks in the AST

OK, we can return to the AST and let the nodes keep arrays of statements when the block in curly braces was seen.

Let us take a closer look at the loop from the example above:

loop n {
    say n;
    say 2 * n;
}

In the form of AST, this fragment takes the following shape:

AST::Loop.new(
    variable => AST::Variable.new(
        . . .
    ),
    statements => Array[ASTNode].new(
        AST::FunctionCall.new(
            . . .
        ),
        AST::FunctionCall.new(
            . . .
        )
    )
)

Again, the abstract nature of the abstract syntax tree is demonstrated here. Although we added the  block rule to the grammar, there is no trace of it in the tree. The two  say calls appeared immediately as the elements of the  statements array attribute within the  AST::Loop node.

What we need is to loop over the statements in the evaluator:

multi method eval-node(AST::Condition $node) {
if $node.value.value {
self.eval-node($_) for $node.statements;
}
else {
self.eval-node($_) for $node.antistatements;
}
}

multi method eval-node(AST::Loop $node) {
while %!var{$node.variable.variable-name} > 0 {
self.eval-node($_) for $node.statements;
%!var{$node.variable.variable-name}--;
}
}

That’s it! Introducing the { } blocks was a relatively simple task.

A real-life test

If you run the loop, you’ll see an error very soon:

5
10
4
Cannot shift from an empty Array[ASTNode]
  in method value at /Users/ash/lingua/LinguaAST.rakumod    
  (LinguaAST) line 112

The first three lines display the expected beginning of the result, but then, everything suddenly stopped. The error message helps to understand that this happened because we were using shift to compute the expressions. Within a loop, expressions can be reused, so we should not change them. Let us change all shifts to loops. There are two such places in our code. The first one in the  AST::MathOperations class:

method value() {
    my $result = @.operands[0].value;

    for 1 .. @.operands.elems - 1 -> $i {
        $result = operation(@.operators[$i - 1], 
                  $result, @.operands[$i].value);
    }

    return $result;
}

The second in one of the multi-methods of the evaluator:

multi method eval-node(AST::HashAssignment $node) {
    %!var{$node.variable-name} = Hash.new;
    for 0 .. $node.keys.elems - 1 -> $i {
        %!var{$node.variable-name}.{$node.keys[$i]} =
            $node.values[$i].value;
    }
}

The test code is working now. Let us fix the behaviour in the test with similar instructions:

my n = 5;
loop n {
    my n2 = n * n;
    say "n = $n, n2 = $n2";
}
<strong>## n = 5, n2 = 25
## n = 4, n2 = 16
## n = 3, n2 = 9
## n = 2, n2 = 4
## n = 1, n2 = 1</strong>

Don’t forget to test the if and  else blocks, too. The program should be able to execute all the statements from each of them.

Conditional operators

So far, the if clause can only check if the value is zero or not. Let us make it more powerful and allow conditional and Boolean operators:

|  &  <  <=  >  >=  ==  !=

We are not going to introduce the Boolean data type, but instead, let’s add the operators to existing expressions. It makes both 3 + n and  x < y valid expressions, which can be used as a condition in the  if test.

The above listed operators are binary operators and have precedence lower than arithmetic operators + and  - . Among themselves,  & has a higher precedence. So, we have to update the  op tokens in the grammar and add the new operators:

multi token op<strong>(1)</strong> {
    | '|'
    | '<'  | '>'
    | '<=' | '>='
    | '==' | '!='
}

multi token op<strong>(2)</strong> {
    '&'
}

Update the rest to explicitly give the precedence:

multi token op(3) {
'+' | '-'
}

multi token op(4) {
'*' | '/'
}

multi token op(5) {
'**'
}

multi rule expr(6) {
| <number>
| <variable-name> <index>?
| '(' <expression> ')'
}

Add the functions to the AST::MathOperations class:

multi sub operation('|', $a, $b) {
    $a || $b
}

multi sub operation('&', $a, $b) {
    $a && $b
}

multi sub operation('<', $a, $b) {
    $a < $b
}

multi sub operation('<=', $a, $b) {
    $a <= $b
}

multi sub operation('>', $a, $b) {
    $a > $b
}

multi sub operation('>=', $a, $b) {
    $a >= $b
}

multi sub operation('!=', $a, $b) {
    $a != $b
}

multi sub operation('==', $a, $b) {
    $a == $b
}

We rely on Raku’s operators here. Another alternative would be to only generate 1 or 0, for example:

multi sub operation('<', $a, $b) {
    $a < $b ?? 1 !! 0
}

In the test, try different operators and their combinations:

my x = 10;
my y = 20;

if x > y say ">" else say "<"; <strong>## <</strong>
if x < y say ">" else say "<"; <strong>## ></strong>

if x != y say "!=" else say "=="; <strong>## !=</strong>
if x != x say "!=" else say "=="; <strong>## ==</strong>

if x == y say "==" else say "!="; <strong>## !=</strong>
if x == x say "==" else say "!="; <strong>## ==</strong>

if 5 <= 5 say "5 <= 5"; <strong>## 5 <= 5</strong>
if 5 <= 6 say "5 <= 6"; <strong>## 5 <= 6</strong>

if 5 >= 5 say "5 >= 5"; <strong>## 5 >= 5</strong>
if 6 >= 5 say "6 >= 5"; <strong>## 6 >= 5</strong>

if (10 > 1) & (5 < 50) say "OK 1"; <strong>## OK 1</strong>
if (10 < 1) | (5 < 50) say "OK 2"; <strong>## OK 2</strong>

Implementing the while loop

Before the end of the chapter, let us implement the while keyword, which is illustrated by the following code:

my n = 1;
while n <= 5 {
    say n;
    n = n + 1
}

The loop body is run until the condition stops being true.

Syntactically and semantically, the while loop is very close to the  loop cycle we worked on before. Implementing it is mostly a copy-and-paste work.

In the grammar:

rule TOP {
    [
        . . .
        | <statement=loopped-statement>
        <strong>| <statement=while-statement></strong>
        . . .
    ]*
}

<strong>rule while-statement</strong> {
    <strong>'while'</strong> <value> <block(';')>
}

Define a new AST node:

class AST::While is ASTNode {
has ASTNode $.value;
has ASTNode @.statements;
}

In the actions:

method while-statement($/) {
$/.make(AST::While.new(
value => $/<value>.made,
statements => ($/<block><statement>.map: *.made),
));
}

In the evaluator:

multi method eval-node(AST::While $node) {
while $node.value.value {
self.eval-node($_) for $node.statements;
}
}

The while construct is implemented. Add a test and move on to the next chapter.

my n = 1;
while n <= 5 {
    say n;
    n = n + 1
}
<strong>## 1
## 2
## 3
## 4
## 5</strong>

my k = 1;
while k < 10 k = k + 1;
say k; <strong>## 10</strong>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP程序设计

PHP程序设计

Kevin Tatroe、Rasmus Lerdorf / 邓云佳 / 中国电力出版社 / 2003-7-1 / 68.00

本书涵盖了创建一个高效PHP Web应用程序所需要的所有技术,其内容包括:PHP语言基础的详细信息,包括数据类型、变量、操作符和流控制语句。用专门章节讨论关于函数、字符串、数组和对象的基本内容。涵盖通用的PHP Web应用程序设计技术,如表单处理和验证、会话跟踪以及cookie。用和数据库无关的PEAR DB库与关系数据库(如MySQL和Oracle)进行交互的内容。介绍用PHP生成动态图像、创建一起来看看 《PHP程序设计》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具