diff --git a/src/Definition/UnaryOperator.php b/src/Definition/UnaryOperator.php new file mode 100644 index 00000000..2e7c6f32 --- /dev/null +++ b/src/Definition/UnaryOperator.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Definition; + +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +enum UnaryOperator: string +{ + case NOT = 'NOT'; + + public static function fromTokenType(TokenType $tokenType): self + { + return match ($tokenType) { + TokenType::OPERATOR_BOOLEAN_NOT => self::NOT, + default => throw new \Exception('@TODO: Unknown Unary Operator') + }; + } +} diff --git a/src/Parser/Ast/ExpressionNode.php b/src/Parser/Ast/ExpressionNode.php index 5c09d7a1..fc47685d 100644 --- a/src/Parser/Ast/ExpressionNode.php +++ b/src/Parser/Ast/ExpressionNode.php @@ -33,7 +33,7 @@ final class ExpressionNode implements \JsonSerializable { private function __construct( - public readonly IdentifierNode | NumberLiteralNode | BinaryOperationNode | AccessNode | TernaryOperationNode | TagNode | StringLiteralNode | MatchNode | TemplateLiteralNode | BooleanLiteralNode | NullLiteralNode $root + public readonly IdentifierNode | NumberLiteralNode | BinaryOperationNode | UnaryOperationNode | AccessNode | TernaryOperationNode | TagNode | StringLiteralNode | MatchNode | TemplateLiteralNode | BooleanLiteralNode | NullLiteralNode $root ) { } @@ -109,6 +109,9 @@ public static function fromTokens(\Iterator $tokens, Precedence $precedence = Pr case TokenType::TEMPLATE_LITERAL_START: $root = TemplateLiteralNode::fromTokens($tokens); break; + case TokenType::OPERATOR_BOOLEAN_NOT: + $root = UnaryOperationNode::fromTokens($tokens); + break; default: $root = IdentifierNode::fromTokens($tokens); break; diff --git a/src/Parser/Ast/UnaryOperationNode.php b/src/Parser/Ast/UnaryOperationNode.php new file mode 100644 index 00000000..df8112ad --- /dev/null +++ b/src/Parser/Ast/UnaryOperationNode.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Ast; + +use PackageFactory\ComponentEngine\Definition\Precedence; +use PackageFactory\ComponentEngine\Definition\UnaryOperator; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; + +final class UnaryOperationNode implements \JsonSerializable +{ + private function __construct( + public readonly UnaryOperator $operator, + public readonly ExpressionNode $argument + ) { + } + + /** + * @param \Iterator $tokens + */ + public static function fromTokens(\Iterator $tokens): self + { + Scanner::skipSpace($tokens); + + $operator = UnaryOperator::fromTokenType(Scanner::type($tokens)); + + Scanner::skipOne($tokens); + + $argument = ExpressionNode::fromTokens($tokens, Precedence::UNARY); + + return new self( + operator: $operator, + argument: $argument + ); + } + + public function jsonSerialize(): mixed + { + return [ + 'type' => 'UnaryOperationNode', + 'payload' => [ + 'operator' => $this->operator, + 'argument' => $this->argument + ] + ]; + } +} diff --git a/src/Parser/Tokenizer/Tokenizer.php b/src/Parser/Tokenizer/Tokenizer.php index 0760410c..25423fdb 100644 --- a/src/Parser/Tokenizer/Tokenizer.php +++ b/src/Parser/Tokenizer/Tokenizer.php @@ -256,14 +256,22 @@ public static function period(\Iterator $fragments): \Iterator yield from $buffer->flush(TokenType::PERIOD); } + /** + * @param \Iterator $fragments + */ public static function symbol(\Iterator $fragments, ?Buffer $buffer = null): \Iterator { $buffer = $buffer ?? Buffer::empty(); $capture = true; while ($capture && $fragments->valid()) { - /** @var Fragment $fragment */ $fragment = $fragments->current(); + + if ($buffer->value() === '!' && $fragment->value === '!') { + // chained `!` must be kept as individual fragments/tokens + break; + } + $capture = match (CharacterType::get($fragment->value)) { CharacterType::ANGLE_CLOSE, CharacterType::FORWARD_SLASH, diff --git a/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php b/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php index 4bf08539..c54a503e 100644 --- a/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php +++ b/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php @@ -34,6 +34,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\TagNode; use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Access\AccessTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BinaryOperation\BinaryOperationTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BooleanLiteral\BooleanLiteralTranspiler; @@ -45,6 +46,7 @@ use PackageFactory\ComponentEngine\Target\Php\Transpiler\Tag\TagTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TemplateLiteral\TemplateLiteralTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TernaryOperation\TernaryOperationTranspiler; +use PackageFactory\ComponentEngine\Target\Php\Transpiler\UnaryOperation\UnaryOperationTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; final class ExpressionTranspiler @@ -70,6 +72,9 @@ public function transpile(ExpressionNode $expressionNode): string BinaryOperationNode::class => new BinaryOperationTranspiler( scope: $this->scope ), + UnaryOperationNode::class => new UnaryOperationTranspiler( + scope: $this->scope + ), BooleanLiteralNode::class => new BooleanLiteralTranspiler(), MatchNode::class => new MatchTranspiler( scope: $this->scope diff --git a/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php b/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php new file mode 100644 index 00000000..dd6e7595 --- /dev/null +++ b/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php @@ -0,0 +1,55 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\UnaryOperation; + +use PackageFactory\ComponentEngine\Definition\UnaryOperator; +use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; +use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; +use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; + +final class UnaryOperationTranspiler +{ + public function __construct(private readonly ScopeInterface $scope) + { + } + + private function transpileUnaryOperator(UnaryOperator $operator): string + { + return match ($operator) { + UnaryOperator::NOT => '!' + }; + } + + public function transpile(UnaryOperationNode $unaryOperationNode): string + { + $expressionTranspiler = new ExpressionTranspiler( + scope: $this->scope, + shouldAddQuotesIfNecessary: true + ); + + $operator = $this->transpileUnaryOperator($unaryOperationNode->operator); + $argument = $expressionTranspiler->transpile($unaryOperationNode->argument); + + return sprintf('(%s%s)', $operator, $argument); + } +} diff --git a/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php index d4b5112a..2e6d0fb9 100644 --- a/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php @@ -70,6 +70,8 @@ public function binaryOperationExamples(): array '2 % b' => ['2 % b', '(2 % $this->b)'], 'a % b' => ['a % b', '($this->a % $this->b)'], + '!1 + 1' => ['!1 + 1', '((!1) + 1)'], + '42 * a / 23 + b - 17 * c' => [ '42 * a / 23 + b - 17 * c', '((((42 * $this->a) / 23) + $this->b) - (17 * $this->c))' @@ -126,4 +128,4 @@ public function transpilesBinaryOperationNodes(string $binaryOperationAsString, $actualTranspilationResult ); } -} \ No newline at end of file +} diff --git a/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php new file mode 100644 index 00000000..5fecea66 --- /dev/null +++ b/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\UnaryOperation; + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; +use PackageFactory\ComponentEngine\Target\Php\Transpiler\UnaryOperation\UnaryOperationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PHPUnit\Framework\TestCase; + +final class UnaryOperationTranspilerTest extends TestCase +{ + /** + * @return array + */ + public function unaryOperationExamples(): array + { + return [ + '!false' => ['!false', '(!false)'], + '!!false' => ['!!false', '(!(!false))'], + '!!!false' => ['!!!false', '(!(!(!false)))'], + '!foo' => ['!foo', '(!$this->foo)'], + ]; + } + + /** + * @dataProvider unaryOperationExamples + * @test + */ + public function transpilesUnaryOperationNodes(string $unaryOperationAsString, string $expectedTranspilationResult): void + { + $transpiler = new UnaryOperationTranspiler( + scope: new DummyScope(['foo' => StringType::get()]) + ); + $node = ExpressionNode::fromString($unaryOperationAsString)->root; + assert($node instanceof UnaryOperationNode); + + $actualTranspilationResult = $transpiler->transpile( + $node + ); + + $this->assertEquals( + $expectedTranspilationResult, + $actualTranspilationResult + ); + } +}