Adding logical expressions

Currently, our language only supports numerical expressions. Another useful addition would be to support Boolean expressions that do not evaluate to numeric values but to true or false. Possible examples would include expressions such as 3 = 4 (which would always evaluate to false), 2 < 4 (which would always evaluate to true), or a <= 5 (which depends on the value of variable a).

Comparisons

As before, let's start by extending the object model of our syntax tree. We'll start with an Equals node that represents an equality check between two expressions. Using this node, the 1 + 2 = 4 - 1 expression would produce the following syntax tree (and should of course eventually evaluate to true):

Comparisons

The syntax tree that should result from parsing the 1 + 2 = 4 - 1 expression

For this, we will implement the PacktChp8DSLASTEquals class. This class can inherit the BinaryOperation class that we implemented earlier:

namespace PacktChp8DSLAST; 
 
class Equals extends BinaryOperation 
{ 
    public function evaluate(array $variables = []) 
    { 
        return $this->left->evaluate($variables) 
            == $this->right->evaluate($variables); 
    } 
} 

While we're at it, we can also implement the NotEquals node at the same time:

namespace PacktChp8DSLAST; 
 
class NotEquals extends BinaryOperation 
{ 
    public function evaluate(array $variables = []) 
    { 
        return $this->left->evaluate($variables)

            != $this->right->evaluate($variables); 
    } 
} 

In the next step, we'll need to adjust our parser's grammar. First, we need to change the grammar to differentiate between numerical and Boolean expressions. For this, we'll rename the Expr symbol to NumExpr in the entire grammar. This affects the Value symbol:

Value: Number | Variable | '(' > NumExpr > ')' 
    function Number(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 
    function Variable(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 
    function NumExpr(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 

Of course, you'll also need to change the Expr rule itself:

NumExpr: Sum 
    function Sum(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 

Next, we can define a rule for equality (and also non-equality):

ComparisonOperator: '=' | '|=' 
Comparison: left:NumExpr (operand:(> op:ComparisonOperator > right:NumExpr)) 
    function left(&$result, $sub) { 
        $result['leftNode'] = $sub['node']; 
    } 
    function right(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 
    function op(array &$result, array $sub) { 
        $result['op'] = $sub['text']; 
    } 
    function operand(&$result, $sub) { 
        if ($sub['op'] == '=') { 
            $result['node'] = new Equals($result['leftNode'], $sub['node']); 
        } else { 
            $result['node'] = new NotEquals($result['leftNode'], $sub['node']); 
        } 
    } 

Note that this rule got a bit more complicated in this case, as it supports multiple operators. However, these rules are now relatively easy to be extended by more operators (when we're checking non-equality things such as "greater than" or "smaller than" might be the next logical steps). The ComparisonOperator symbol, which is defined first, matches all kinds of comparison operators and the Comparison rule that uses this symbol to match the actual expressions.

Lastly, we can add a new BoolExpr symbol, and also define the Expr symbol again:

BoolExpr: Comparison 
    function Comparison(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 
 
Expr: BoolExpr | NumExpr 
    function BoolExpr(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 
    function NumExpr(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 

When calling the match_Expr() function, our parser will now match both numeric and Boolean expressions. Rebuild your parser using PHP-PEG's cli.php script, and add a few new calls to your test.php script:

$expr = $builder->parseExpression('1 = 2'); 
var_dump($expr->evaluate()); 
 
$expr = $builder->parseExpression('a * 2 = 6'); 
var_dump($expr->evaluate(['a' => 3]); 
var_dump($expr->evaluate(['a' => 4]); 

These expressions should evaluate to false, true, and false respectively. The numeric expressions that you've added before should continue to work as before.

Similar to this, you could now add additional comparison operators, such as >, >=, <, or <= to your grammar. Since the implementation of these operators would be largely identical to the = and |= operations, we'll leave it as an exercise for you.

The "and" and "or" operators

Another important feature in order to fully support logical expressions is the ability to combine logical expressions via the "and" and "or" operators. As we are developing our language with an end user in mind, we'll build our language to actually support and and or as logical operators (in contrast to the ubiquitous && and || that you find in many general-purpose programming language that are derived from the C syntax).

Again, let's start by implementing the respective node types for the syntax tree. We will need node types modeling both the and and or operation so that a statement such as a = 1 or b = 2 will be parsed into the following syntax tree:

The "and" and "or" operators

The syntax tree resulting from parsing a=1 or b=2

Begin by implementing the PacktChp8DSLASTLogicalAnd class (we cannot use And as a class name, because that's a reserved word in PHP):

namespace PacktChp8DSLAST; 
 
class LogicalAnd extends BinaryOperation 
{ 
    public function evaluate(array $variables=[]) 
    { 
        return $this->left->evaluate($variables) 
            && $this->right->evaluate($variables); 
    } 
} 

For the or operator, you can also implement the PacktChp8DSLASTLogicalOr class the same way.

When working with the and and or operators, you will need to think about operator precedence. While operator precedence is well defined for arithmetic operations, this is not the case for logical operators. For example, the statement a and b or c and d could be interpreted as (((a and b) or c) and d) (same precedence, left to right), or just as well as (a and b) or (c and d) (precedence on and) or (a and (b or c)) and d (precedence on or). However, most programming languages treat the and operator with the highest precedence, so barring any other convention it makes sense to stick with this tradition.

The following figure shows the syntax trees that result from applying this precedence on the a=1 and b=2 or b=3 and a=1 and (b=2 or b=3) statements:

The "and" and "or" operators

The syntax trees resulting form parsing a=1 and b=2 or b=3 and a=1 and (b=2 or b=3)

We will need a few new rules in our grammar for this. First of all, we need a new symbol representing a Boolean value. For now, such a Boolean value may either be a comparison or any Boolean expression wrapped in brackets.

BoolValue: Comparison | '(' > BoolExpr > ')' 
    function Comparison(array &$res, array $sub) { 
        $res['node'] = $sub['node']; 
    } 
    function BoolExpr(array &$res, array $sub) { 
        $res['node'] = $sub['node']; 
    } 

Do you remember how we implemented operator precedence previously using the Product and Sum rules? We can implement the And and Or rules the same way:

And: left:BoolValue (> "and" > right:BoolValue)* 
    function left(array &$res, array $sub) { 
        $res['node'] = $sub['node']; 
    } 
    function right(array &$res, array $sub) { 
        $res['node'] = new LogicalAnd($res['node'], $sub['node']); 
    } 
 
Or: left:And (> "or" > right:And)* 
    function left(array &$res, array $sub) { 
        $res['node'] = $sub['node']; 
    } 
    function right(array &$res, array $sub) { 
        $res['node'] = new LogicalOr($res['node'], $sub['node']); 
    } 

After this, we can extend the BoolExpr rule to also match Or expressions (and since a single And symbol also matches the Or rule, a single And symbol will also be a BoolExpr):

BoolExpr: Or | Comparison 
    function Or(array &$result, array $sub) {

        $result['node'] = $sub['node'];

    } 
    function Comparison(array &$result, array $sub) { 
        $result['node'] = $sub['node']; 
    } 

You can now add a few new test cases to your test.php script. Play around with variables and pay special attention to how operator precedence is resolved:

$expr = $builder->parseExpression('a=1 or b=2 and c=3'); 
var_dump($expr->evaluate([ 
    'a' => 0, 
    'b' => 2, 
    'c' => 3 
]); 

Conditions

Now that our language supports (arbitrarily complex) logical expressions, we can use these to implement another important feature: conditional statements. Our language currently supports only expressions that evaluate to a single numeric or the Boolean value; we'll now implement a variant of the ternary operator, which is also known in PHP:

($b > 0) ? 1 : 2; 

As our language is targeted at end users, we'll use a more readable syntax, which will allow statements such as when <condition> then <value> else <value>. In our syntax tree, constructs such as these will be represented by the PacktChp8DSLASTCondition class:

<?php 
namespace PacktChp8DSLAST; 
 
class Condition implements Expression 
{ 
    private $when; 
    private $then; 
    private $else; 
 
    public function __construct(Expression $when, Expression $then, Expression $else) 
    { 
        $this->when = $when; 
        $this->then = $then; 
        $this->else = $else; 
    } 
 
    public function evaluate(array $variables = []) 
    { 
        if ($this->when->evaluate($variables)) { 
            return $this->then->evaluate($variables); 
        } 
        return $this->else->evaluate($variables); 
    } 
} 

This means that, for example, the when a > 2 then a * 1.5 else a * 2 expression should be parsed into the following syntax tree:

Conditions

In theory, our language should also support complex expressions in the condition or the then/else part, allowing statements such as when (a > 2 or b = 2) then (2 * a + 3 * b) else (3 * a - b) or even nested statements such as when a=2 then (when b=2 then 1 else 2) else 3:

Conditions

Continue by adding a new symbol and rule to your parser's grammar:

Condition: "when" > when:BoolExpr > "then" > then:Expr > "else" > else:Expr 
    function when(array &$res, array $sub) { 
        $res['when'] = $sub['node']; 
    } 
    function then(array &$res, $sub) { 
        $res['then'] = $sub['node']; 
    } 
    function else(array &$res, array $sub) { 
        $res['node'] = new Condition($res['when'], $res['then'], $sub['node']); 
    } 

Also, adjust the BoolExpr rule to also match conditions. In this case, the order is important: if you're putting the Or or Comparison symbol first in the BoolExpr rule, the rule might interpret when as a variable name, instead of a conditional expression.

BoolExpr: Condition | Or | Comparison 
    function Condition(array &$result, array $sub) {

        $result['node'] = $sub['node'];

    } 
    function Or(&$result, $sub) { 
        $result['node'] = $sub['node']; 
    } 
    function Comparison(&$result, $sub) { 
        $result['node'] = $sub['node']; 
    } 

Again, rebuild your parser using PHP-PEG's cli.php script, and add a few test statements to your test script to test the new grammar rules:

$expr = $builder->parseExpression('when a=1 then 3.14 else a*2'); 
var_dump($expr->evaluate(['a' => 1]); 
var_dump($expr->evaluate(['a' => 2]); 
var_dump($expr->evaluate(['a' => 3]); 

These test cases should evaluate to 3.14, 4, and 6 respectively.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.144.228