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
).
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):
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.
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:
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:
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 ]);
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:
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
:
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.
3.148.144.228