Skip to content

Commit

Permalink
Merge pull request #15 from SerhiiCho/v2.8.0
Browse files Browse the repository at this point in the history
V2.8.0
  • Loading branch information
SerhiiCho authored Nov 22, 2023
2 parents ef53a84 + 9aa2b79 commit 1fb01ff
Show file tree
Hide file tree
Showing 17 changed files with 261 additions and 49 deletions.
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ This package is not for creating a full-featured template engine. It's just a si
- [x] Assigning variables
- [x] Using variables
- [x] Printing variables
- [x] Comparison operators
- [x] Equal (==)
- [x] Not equal (!=)
- [x] Strong equal (===)
- [x] Strong not equal (!==)
- [x] Greater than (>)
- [x] Less than (<)
- [x] Greater than or equal (>=)
- [x] Less than or equal (<=)
- [x] If/Else-If/Else statements
- [x] Ternary expressions
- [x] Loops
Expand Down Expand Up @@ -168,15 +177,23 @@ Prefix operators are used to change the value of the variable. For example if yo

Infix operators are used to perform math operations or string concatenation. For example if you have a variable `$age` and you want to add 1 to it, you can use `+` infix operator to add 1 to the variable. Or if you have a variable `$first_name` and you want to concatenate it with `$last_name`, you can use `.` infix operator to concatenate these 2 variables.

| Operator name | Operator literal | Example | Supported types for prefix |
|---------------|------------------|--------------------|----------------------------|
| Plus | + | 3 + 4 | int, float |
| Minus | - | 5 - 4 | int, float |
| Multiply | * | 3 * 4 | int, float |
| Divide | / | 6 / 3 | int, float |
| Modulo | % | 5 % 2 | int, float |
| Concatenate | . | 'Hello' . ' world' | string |
| Assigning | = | {{ $a = 5 }} | all the types |
| Operator name | Operator literal | Example | Supported types for prefix |
|------------------|------------------|--------------------|----------------------------|
| Plus | + | 3 + 4 | int, float |
| Minus | - | 5 - 4 | int, float |
| Multiply | * | 3 * 4 | int, float |
| Divide | / | 6 / 3 | int, float |
| Modulo | % | 5 % 2 | int, float |
| Concatenate | . | 'Hello' . ' world' | string |
| Assigning | = | {{ $a = 5 }} | all the types |
| Equal | == | 5 == 5 | all the types |
| Not equal | != | 5 != 4 | all the types |
| Strong equal | === | 5 === 5 | all the types |
| Strong not equal | !== | 5 !== 4 | all the types |
| Greater than | > | 5 > 4 | int, float |
| Less than | < | 5 < 4 | int, float |
| Greater or equal | >= | 5 >= 4 | int, float |
| Less or equal | <= | 5 <= 4 | int, float |

## All the available syntax in html/text file

Expand Down Expand Up @@ -270,7 +287,7 @@ Infix operators are used to perform math operations or string concatenation. For
```html
<!-- With strings -->
<section class="container">
{{ $show_main_title ? '<h1>Main title</h1>' : '<h2>Secondary</h2>' }}
{{ 23 === 23 ? '<h1>Main title</h1>' : '<h2>Secondary</h2>' }}
</section>
```

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"scripts": {
"test": "./vendor/bin/phpunit --order-by=random",
"pint": "./vendor/bin/pint",
"pint": "./vendor/bin/pint --test",
"stan": "./vendor/bin/phpstan analyse",
"cs": "./vendor/bin/phpcs src --colors -p",
"check": ["@test", "@pint", "@stan", "@cs"]
Expand Down
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

----

## v2.8.0 (2023-11-22)

- Added support for comparison operators like `==`, `===`, `!==`, `!=`, `<`, `>`, `<=`, `>=`. Now you can use them like this: `{{ if 1 == 1 }}`, `{{ if 1 === 1 }}`, `{{ if 1 !== 1 }}`, `{{ if 1 != 1 }}`, `{{ if 1 < 1 }}`, `{{ if 1 > 1 }}`, `{{ if 1 <= 1 }}`, `{{ if 1 >= 1 }}`
- Improved error handling for operators

----

## v2.7.0 (2023-11-22)

- Added more tests to make sure that everything works as expected
Expand Down
2 changes: 1 addition & 1 deletion docs/goodbye-html.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

<ternary-expression> ::= <expression> "?" <expression> ":" <expression>

<infix-operator> ::= "+" | "-" | "*" | "/" | "%"
<infix-operator> ::= "+" | "-" | "*" | "/" | "%" | "==" | "!=" | ">" | "<" | ">=" | "<=" | "===" | "!=="

<infix-expression> ::= <expression> <infix-operator> <expression>

Expand Down
2 changes: 1 addition & 1 deletion pint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"rules": {
"is_null": true,
"declare_strict_types": true,
"strict_comparison": true,
"strict_comparison": false,
"no_unused_imports": true,
"explicit_string_variable": true,
"native_function_casing": true,
Expand Down
20 changes: 16 additions & 4 deletions src/CoreParser/CoreParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@
class CoreParser
{
private const PRECEDENCES = [
// TokenType::EQUAL->value => Precedence::EQUALS,
// TokenType::NOT_EQUAL->value => Precedence::EQUALS,
// TokenType::LESS_THAN->value => Precedence::LESS_GREATER,
// TokenType::GREATER_THAN->value => Precedence::LESS_GREATER,
TokenType::QUESTION->value => Precedence::TERNARY,
TokenType::EQ->value => Precedence::EQUALS,
TokenType::NOT_EQ->value => Precedence::EQUALS,
TokenType::STRONG_EQ->value => Precedence::EQUALS,
TokenType::STRONG_NOT_EQ->value => Precedence::EQUALS,
TokenType::LTHAN->value => Precedence::LESS_GREATER,
TokenType::GTHAN->value => Precedence::LESS_GREATER,
TokenType::LTHAN_EQ->value => Precedence::LESS_GREATER,
TokenType::GTHAN_EQ->value => Precedence::LESS_GREATER,
TokenType::PERIOD->value => Precedence::SUM,
TokenType::PLUS->value => Precedence::SUM,
TokenType::MINUS->value => Precedence::SUM,
Expand Down Expand Up @@ -86,6 +90,14 @@ public function __construct(private readonly Lexer $lexer)
$this->registerInfix(TokenType::SLASH, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::ASTERISK, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::MODULO, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::EQ, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::STRONG_EQ, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::NOT_EQ, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::STRONG_NOT_EQ, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::LTHAN, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::GTHAN, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::LTHAN_EQ, fn ($l) => $this->parseInfixExpression($l));
$this->registerInfix(TokenType::GTHAN_EQ, fn ($l) => $this->parseInfixExpression($l));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/CoreParser/Precedence.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
enum Precedence: int
{
case LOWEST = 0;
case EQUALS = 1; // == and ===
case TERNARY = 2; // a ? b : c
case TERNARY = 1; // a ? b : c
case EQUALS = 2; // == and ===
case LESS_GREATER = 3; // > or <
case SUM = 4; // +
case PRODUCT = 5; // *
Expand Down
15 changes: 10 additions & 5 deletions src/Evaluator/EvalError.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@

readonly class EvalError
{
private const PREFIX = '[EVAL_ERROR]';

public static function wrongArgumentType(string $type, ObjType $expect, Obj $actual): ErrorObj
{
return new ErrorObj(sprintf(
'[EVAL_ERROR] "%s" is not allowed argument type for "%s", expected "%s"',
'%s "%s" is not allowed argument type for "%s", expected "%s"',
self::PREFIX,
$actual->type()->value,
$type,
$expect->value,
Expand All @@ -24,14 +27,15 @@ public static function wrongArgumentType(string $type, ObjType $expect, Obj $act

public static function unknownType(Node $node): ErrorObj
{
return new ErrorObj('[EVAL_ERROR] unknown type: "' . get_class($node) . '"');
return new ErrorObj(sprintf('%s unknown type: "%s"', self::PREFIX, get_class($node)));
}

public static function operatorNotAllowed(string $operator, Obj $right): ErrorObj
{
return new ErrorObj(
sprintf(
'[EVAL_ERROR] operator "%s" is not allowed for type "%s"',
'%s operator "%s" is not allowed for type "%s"',
self::PREFIX,
$operator,
$right->type()->value,
)
Expand All @@ -40,14 +44,15 @@ public static function operatorNotAllowed(string $operator, Obj $right): ErrorOb

public static function variableIsUndefined(VariableExpression $node): ErrorObj
{
return new ErrorObj(sprintf('[EVAL_ERROR] variable "$%s" is undefined', $node->value));
return new ErrorObj(sprintf('%s variable "$%s" is undefined', self::PREFIX, $node->value));
}

public static function infixExpressionMustBeBetweenNumbers(string $side, string $operator, Obj $obj): ErrorObj
{
return new ErrorObj(
sprintf(
'[EVAL_ERROR] %s side must be INT or FLOAT for operator %s. Got %s',
'%s %s side must be INT or FLOAT for operator %s. Got %s',
self::PREFIX,
$side,
$operator,
$obj->type()->value,
Expand Down
69 changes: 54 additions & 15 deletions src/Evaluator/Evaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,36 +105,75 @@ private function evalInfixExpression(InfixExpression $node, Env $env): Obj
return $left;
}

return $this->calculateBinaryExpression($left, $right, $node->operator);
return $this->handleInfixExpressionResult($left, $right, $node->operator);
}

private function calculateBinaryExpression(Obj $left, Obj $right, string $operator): Obj
private function handleInfixExpressionResult(Obj $left, Obj $right, string $operator): Obj
{
$leftValue = $left->value();
$rightValue = $right->value();

if ($operator === '.' && ($left instanceof StringObj || $right instanceof StringObj)) {
return new StringObj($left->value() . $right->value());
if (is_string($leftValue) || is_string($rightValue)) {
return $this->evalStringInfixExpressions($left, $right, $operator);
}

if (!is_numeric($leftValue)) {
return EvalError::infixExpressionMustBeBetweenNumbers('left', $operator, $left);
}
$leftIsNum = $left instanceof IntegerObj || $left instanceof FloatObj;
$rightIsNum = $right instanceof IntegerObj || $right instanceof FloatObj;

if (!is_numeric($rightValue)) {
return EvalError::infixExpressionMustBeBetweenNumbers('right', $operator, $right);
if ($leftIsNum && $rightIsNum) {
return $this->evalIntegerInfixExpressions($left, $right, $operator);
}

return match ($operator) {
'+' => $this->numberObject($leftValue + $rightValue),
'-' => $this->numberObject($leftValue - $rightValue),
'*' => $this->numberObject($leftValue * $rightValue),
'/' => $this->numberObject($leftValue / $rightValue),
'%' => $this->numberObject($leftValue % $rightValue),
'==' => new BooleanObj($leftValue == $rightValue),
'!=' => new BooleanObj($leftValue != $rightValue),
'===' => new BooleanObj($leftValue === $rightValue),
'!==' => new BooleanObj($leftValue !== $rightValue),
default => EvalError::operatorNotAllowed($operator, $right),
};
}

private function evalStringInfixExpressions(Obj $left, Obj $right, string $operator): Obj
{
$leftVal = $left->value();
$rightVal = $right->value();

return match ($operator) {
'==' => new BooleanObj($leftVal == $rightVal),
'!=' => new BooleanObj($leftVal != $rightVal),
'===' => new BooleanObj($leftVal === $rightVal),
'!==' => new BooleanObj($leftVal !== $rightVal),
'.' => new StringObj($leftVal . $rightVal),
default => EvalError::operatorNotAllowed($operator, is_string($leftVal) ? $left : $right),
};
}

private function evalIntegerInfixExpressions(
IntegerObj|FloatObj $left,
IntegerObj|FloatObj $right,
string $operator,
): Obj {
$leftVal = $left->value();
$rightVal = $right->value();

return match ($operator) {
'+' => $this->numberObject($leftVal + $rightVal),
'-' => $this->numberObject($leftVal - $rightVal),
'*' => $this->numberObject($leftVal * $rightVal),
'/' => $this->numberObject($leftVal / $rightVal),
'%' => $this->numberObject($leftVal % $rightVal),
'>' => new BooleanObj($leftVal > $rightVal),
'<' => new BooleanObj($leftVal < $rightVal),
'>=' => new BooleanObj($leftVal >= $rightVal),
'<=' => new BooleanObj($leftVal <= $rightVal),
'==' => new BooleanObj($leftVal === $rightVal),
'!=' => new BooleanObj($leftVal !== $rightVal),
'===' => new BooleanObj($leftVal === $rightVal),
'!==' => new BooleanObj($leftVal !== $rightVal),
default => EvalError::operatorNotAllowed($operator, $left),
};
}

private function numberObject(int|float $num): Obj
{
return is_int($num) ? new IntegerObj($num) : new FloatObj($num);
Expand Down Expand Up @@ -177,7 +216,7 @@ private function evalIfStatement(IfStatement $node, Env $env): Obj
}
}

if ($node->elseBlock !== null) {
if (null !== $node->elseBlock) {
return $this->eval($node->elseBlock, $env);
}

Expand Down
68 changes: 62 additions & 6 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ class Lexer
*/
private int $nextPosition = 0;

private readonly string $input;

/**
* The current character under examination
*/
Expand All @@ -37,9 +35,8 @@ class Lexer
*/
private bool $isHtml = true;

public function __construct(string $input)
public function __construct(private readonly string $input)
{
$this->input = $input;
$this->advanceChar();
}

Expand Down Expand Up @@ -83,9 +80,11 @@ private function readEmbeddedCodeToken(): Token
',' => $this->createTokenAndAdvanceChar(TokenType::COMMA),
'?' => $this->createTokenAndAdvanceChar(TokenType::QUESTION),
':' => $this->createTokenAndAdvanceChar(TokenType::COLON),
'!' => $this->createTokenAndAdvanceChar(TokenType::BANG),
'.' => $this->createTokenAndAdvanceChar(TokenType::PERIOD),
'=' => $this->createTokenAndAdvanceChar(TokenType::ASSIGN),
'<' => $this->createLessThanComparisonToken(),
'>' => $this->createGreaterThanComparisonToken(),
'!' => $this->createNegationToken(),
'=' => $this->createEqualToken(),
default => false,
};

Expand Down Expand Up @@ -125,6 +124,63 @@ private function isVariableStart(): bool
return $this->char === '$' && $this->isLetter($this->peekChar());
}

private function createLessThanComparisonToken(): Token
{
if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::LTHAN);
}

$this->advanceChar(); // skip "<"

return $this->createTokenAndAdvanceChar(TokenType::LTHAN_EQ, '<=');
}

private function createGreaterThanComparisonToken(): Token
{
if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::GTHAN);
}

$this->advanceChar(); // skip ">"

return $this->createTokenAndAdvanceChar(TokenType::GTHAN_EQ, '>=');
}

private function createNegationToken(): Token
{
if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::BANG);
}

$this->advanceChar(); // skip "!"

if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::NOT_EQ, '!=');
}

$this->advanceChar(); // skip first "="

return $this->createTokenAndAdvanceChar(TokenType::STRONG_NOT_EQ, '!==');
}

private function createEqualToken(): Token
{
// Handle assign
if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::ASSIGN);
}

$this->advanceChar(); // skip first "="

if ($this->peekChar() !== '=') {
return $this->createTokenAndAdvanceChar(TokenType::EQ, '==');
}

$this->advanceChar(); // skip second "="

return $this->createTokenAndAdvanceChar(TokenType::STRONG_EQ, '===');
}

private function createTokenAndAdvanceChar(TokenType $type, ?string $char = null): Token
{
$char ??= $this->char;
Expand Down
Loading

0 comments on commit 1fb01ff

Please sign in to comment.