diff --git a/src/Call.php b/src/Call.php index 83fcbbf..fe09472 100644 --- a/src/Call.php +++ b/src/Call.php @@ -30,10 +30,21 @@ public function __construct( public readonly Type $type, public readonly array $arguments, Span $location, + public readonly CallType $callType = CallType::Method, ) { $this->location = $location; } + public static function infix(string $name, Expression $left, Expression $right, Type $type): self + { + return new self($left, $name, $type, [$right], $left->location()->to($right->location()), CallType::Infix); + } + + public static function prefix(string $name, Expression $argument, Type $type, Span $location): self + { + return new self($argument, $name, $type, [], $location, CallType::Prefix); + } + /** * @param list $a * @param list $b @@ -54,7 +65,11 @@ private static function compareArguments(array $a, array $b): bool public function __toString(): string { - return sprintf('%s.%s:%s(%s)', $this->target, $this->name, $this->type, implode(', ', $this->arguments)); + return match ($this->callType) { + CallType::Infix => sprintf('%s %s %s', $this->target, $this->name, $this->arguments[0]), + CallType::Prefix => sprintf('%s%s', $this->name, $this->target), + CallType::Method => sprintf('%s.%s:%s(%s)', $this->target, $this->name, $this->type, implode(', ', $this->arguments)), + }; } public function evaluate(Scope $scope): mixed diff --git a/src/CallType.php b/src/CallType.php new file mode 100644 index 0000000..69e75e6 --- /dev/null +++ b/src/CallType.php @@ -0,0 +1,12 @@ +left, $this->right); - } - - public function evaluate(Scope $scope): bool - { - return $this->left->evaluate($scope) === $this->right->evaluate($scope); - } - - public function equals(Expression $other): bool - { - return $other instanceof self - && $this->left->equals($other->left) - && $this->right->equals($other->right); - } - - public function getType(): Type - { - return Type::bool(); - } - - public function location(): Span - { - return $this->left->location()->to($this->right->location()); - } -} diff --git a/src/Expr.php b/src/Expr.php index 45cd712..2e66c54 100644 --- a/src/Expr.php +++ b/src/Expr.php @@ -22,9 +22,9 @@ private function __construct() { } - public static function eq(Expression $left, Expression $right): Eq + public static function eq(Expression $left, Expression $right): Expression { - return new Eq($left, $right); + return Call::infix('===', $left, $right, Type::bool()); } /** @@ -60,9 +60,9 @@ public static function call(Expression $target, string $name, Type $type, array return new Call($target, $name, $type, $arguments, $location ?? self::dummySpan()); } - public static function or_(Expression $left, Expression $right): Or_ + public static function or_(Expression $left, Expression $right): Expression { - return new Or_($left, $right); + return Call::infix('||', $left, $right, Type::bool()); } public static function and_(Expression $left, Expression $right): And_ @@ -80,19 +80,19 @@ public static function lambda(Expression $body, array $parameters = [], Span|nul return new Lambda($body, $parameters, $location); } - public static function subtract(Expression $minuend, Expression $subtrahend): Subtract + public static function subtract(Expression $minuend, Expression $subtrahend): Expression { - return new Subtract($minuend, $subtrahend); + return Call::infix('-', $minuend, $subtrahend, $minuend->getType()); } - public static function gt(Expression $left, Expression $right): Gt + public static function gt(Expression $left, Expression $right): Expression { - return new Gt($left, $right); + return Call::infix('>', $left, $right, Type::bool()); } - public static function negative(Expression $expression, Span|null $location = null): Negative + public static function negative(Expression $expression, Span|null $location = null): Expression { - return new Negative($expression, $location ?? self::dummySpan()); + return Call::prefix('-', $expression, $expression->getType(), $location ?? self::dummySpan()); } private static function dummySpan(): Span diff --git a/src/Expression.php b/src/Expression.php index 735c3a8..95a0db9 100644 --- a/src/Expression.php +++ b/src/Expression.php @@ -12,26 +12,26 @@ */ abstract class Expression implements Stringable { - public function eq(self $other): Eq + public function eq(self $other): self { return Expr::eq($this, $other); } - public function subtract(self $subtrahend): Subtract + public function subtract(self $subtrahend): self { /** @var self $self */ $self = $this; return Expr::subtract($self, $subtrahend); } - public function gt(self $right): Gt + public function gt(self $right): self { /** @var self $self */ $self = $this; return Expr::gt($self, $right); } - public function or_(self $other): Or_ + public function or_(self $other): self { /** @var self $self */ $self = $this; diff --git a/src/Gt.php b/src/Gt.php deleted file mode 100644 index 93c1f52..0000000 --- a/src/Gt.php +++ /dev/null @@ -1,47 +0,0 @@ - %s', $this->left, $this->right); - } - - public function evaluate(Scope $scope): bool - { - return $this->left->evaluate($scope) > $this->right->evaluate($scope); - } - - public function equals(Expression $other): bool - { - return $other instanceof self - && $this->left->equals($other->left) - && $this->right->equals($other->right); - } - - public function getType(): Type - { - return Type::bool(); - } - - public function location(): Span - { - return $this->left->location()->to($this->right->location()); - } -} diff --git a/src/Negative.php b/src/Negative.php deleted file mode 100644 index 7e944d3..0000000 --- a/src/Negative.php +++ /dev/null @@ -1,46 +0,0 @@ -location = $location; - } - - public function __toString(): string - { - return sprintf('-%s', $this->expression); - } - - public function evaluate(Scope $scope): float|int - { - $value = $this->expression->evaluate($scope); - if (!is_int($value) && !is_float($value)) { - throw new EvaluationError('Expected operand to be of type int or float'); - } - return -$value; - } - - public function equals(Expression $other): bool - { - return $other instanceof self - && $this->expression->equals($other->expression); - } - - public function getType(): Type - { - return $this->expression->getType(); - } -} diff --git a/src/Or_.php b/src/Or_.php deleted file mode 100644 index edfa87e..0000000 --- a/src/Or_.php +++ /dev/null @@ -1,56 +0,0 @@ -left, $this->right); - } - - public function evaluate(Scope $scope): bool - { - $left = $this->left->evaluate($scope); - $right = $this->right->evaluate($scope); - if (!is_bool($left) || !is_bool($right)) { - throw new EvaluationError( - sprintf('Expected boolean operands, got %s and %s', get_debug_type($left), get_debug_type($right)), - ); - } - return $left || $right; - } - - public function equals(Expression $other): bool - { - return $other instanceof self - && $this->left->equals($other->left) - && $this->right->equals($other->right); - } - - public function getType(): Type - { - return Type::bool(); - } - - public function location(): Span - { - return $this->left->location()->to($this->right->location()); - } -} diff --git a/src/Scope.php b/src/Scope.php index 5a0b69e..bb29df3 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -41,6 +41,10 @@ final class Scope public function __construct(private readonly array $vars = [], array $funcs = [], private readonly Scope|null $parent = null) { $predefinedFuncs = $this->parent === null ? [ + '||' => self::or(...), + '===' => self::eq(...), + '-' => self::subtract(...), + '>' => self::gt(...), 'contains' => self::contains(...), 'count' => self::count(...), 'filter' => self::filter(...), @@ -215,6 +219,33 @@ private static function identity(mixed $value): mixed return $value; } + private static function or(bool $left, bool $right): bool + { + return $left || $right; + } + + private static function eq(mixed $left, mixed $right): bool + { + return $left === $right; + } + + /** + * @return ($minuend is float ? float : ($subtrahend is float ? float : int)) + */ + private static function subtract(int|float $minuend, int|float|null $subtrahend = null): int|float + { + if ($subtrahend === null) { + return -$minuend; + } + /** @psalm-suppress InvalidOperand */ + return $minuend - $subtrahend; + } + + private static function gt(int|float $left, int|float $right): bool + { + return $left > $right; + } + /** * @internal */ diff --git a/src/Subtract.php b/src/Subtract.php deleted file mode 100644 index 46e82f0..0000000 --- a/src/Subtract.php +++ /dev/null @@ -1,76 +0,0 @@ -minuend, $this->subtrahend); - } - - public function evaluate(Scope $scope): int|float - { - /** @var mixed $minuend */ - $minuend = $this->minuend->evaluate($scope); - /** @var mixed $subtrahend */ - $subtrahend = $this->subtrahend->evaluate($scope); - if (is_int($minuend) && is_int($subtrahend)) { - return $minuend - $subtrahend; - } - if (is_float($minuend) && is_float($subtrahend)) { - return $minuend - $subtrahend; - } - if (gettype($minuend) === gettype($subtrahend)) { - throw new EvaluationError( - sprintf( - 'Expected operands to be of type int or float, got %s and %s', - get_debug_type($minuend), - get_debug_type($subtrahend), - ), - ); - } - throw new EvaluationError( - sprintf( - 'Expected operands to be of the same type, got %s and %s', - get_debug_type($minuend), - get_debug_type($subtrahend), - ), - ); - } - - public function equals(Expression $other): bool - { - return $other instanceof self - && $this->minuend->equals($other->minuend) - && $this->subtrahend->equals($other->subtrahend); - } - - public function getType(): Type - { - return $this->minuend->getType(); - } - - public function location(): Span - { - return $this->minuend->location()->to($this->subtrahend->location()); - } -} diff --git a/tests/unit/ExpressionComparisonTest.php b/tests/unit/ExpressionComparisonTest.php index 90251d4..2504559 100644 --- a/tests/unit/ExpressionComparisonTest.php +++ b/tests/unit/ExpressionComparisonTest.php @@ -6,18 +6,13 @@ use Eventjet\Ausdruck\And_; use Eventjet\Ausdruck\Call; -use Eventjet\Ausdruck\Eq; use Eventjet\Ausdruck\Expr; use Eventjet\Ausdruck\Expression; use Eventjet\Ausdruck\Get; -use Eventjet\Ausdruck\Gt; use Eventjet\Ausdruck\Lambda; use Eventjet\Ausdruck\ListLiteral; use Eventjet\Ausdruck\Literal; -use Eventjet\Ausdruck\Negative; -use Eventjet\Ausdruck\Or_; use Eventjet\Ausdruck\Parser\Span; -use Eventjet\Ausdruck\Subtract; use Eventjet\Ausdruck\Type; use PHPUnit\Framework\TestCase; @@ -71,15 +66,15 @@ public static function equalsCases(): iterable */ public static function notEqualsCases(): iterable { - yield Eq::class . ': left is different' => [ + yield '===: left is different' => [ Expr::eq(Expr::literal('a'), Expr::literal('a')), Expr::eq(Expr::literal('b'), Expr::literal('a')), ]; - yield Eq::class . ': right is different' => [ + yield '===: right is different' => [ Expr::eq(Expr::literal('a'), Expr::literal('a')), Expr::eq(Expr::literal('a'), Expr::literal('b')), ]; - yield Eq::class . ': different type' => [ + yield '===: different type' => [ Expr::eq(Expr::literal('a'), Expr::literal('a')), Expr::or_(Expr::literal(true), Expr::literal(false)), ]; @@ -119,19 +114,19 @@ public static function notEqualsCases(): iterable Expr::literal('foo'), Expr::get('foo', Type::string()), ]; - yield Or_::class . ': left is different' => [ + yield '||: left is different' => [ Expr::or_(Expr::literal(true), Expr::literal(false)), Expr::or_(Expr::literal(false), Expr::literal(false)), ]; - yield Or_::class . ': right is different' => [ + yield '||: right is different' => [ Expr::or_(Expr::literal(true), Expr::literal(false)), Expr::or_(Expr::literal(true), Expr::literal(true)), ]; - yield Or_::class . ': both are different' => [ + yield '||: both are different' => [ Expr::or_(Expr::literal(true), Expr::literal(false)), Expr::or_(Expr::literal(false), Expr::literal(true)), ]; - yield Or_::class . ': different type' => [ + yield '||: different type' => [ Expr::or_(Expr::literal(true), Expr::literal(false)), Expr::eq(Expr::literal(true), Expr::literal(false)), ]; @@ -139,39 +134,39 @@ public static function notEqualsCases(): iterable Expr::and_(Expr::literal(true), Expr::literal(false)), Expr::and_(Expr::literal(false), Expr::literal(false)), ]; - yield And_::class . ': right is different' => [ + yield '&&: right is different' => [ Expr::and_(Expr::literal(true), Expr::literal(false)), Expr::and_(Expr::literal(true), Expr::literal(true)), ]; - yield And_::class . ': both are different' => [ + yield '&&: both are different' => [ Expr::and_(Expr::literal(true), Expr::literal(false)), Expr::and_(Expr::literal(false), Expr::literal(true)), ]; - yield And_::class . ': different type' => [ + yield '&&: different type' => [ Expr::and_(Expr::literal(true), Expr::literal(false)), Expr::eq(Expr::literal(true), Expr::literal(false)), ]; - yield And_::class . ' and ' . Or_::class => [ + yield '&& and ||' => [ Expr::and_(Expr::literal(false), Expr::literal(false)), Expr::or_(Expr::literal(false), Expr::literal(false)), ]; - yield And_::class . ' and ' . Or_::class . ' with different operands' => [ + yield '&& and || with different operands' => [ Expr::and_(Expr::literal(false), Expr::literal(false)), Expr::or_(Expr::literal(true), Expr::literal(true)), ]; - yield Subtract::class . ': minuend is different' => [ + yield '-: minuend is different' => [ Expr::subtract(Expr::literal(1), Expr::literal(2)), Expr::subtract(Expr::literal(2), Expr::literal(2)), ]; - yield Subtract::class . ': subtrahend is different' => [ + yield '-: subtrahend is different' => [ Expr::subtract(Expr::literal(1), Expr::literal(2)), Expr::subtract(Expr::literal(1), Expr::literal(1)), ]; - yield Subtract::class . ': subtrahend and minuend are different' => [ + yield '-: subtrahend and minuend are different' => [ Expr::subtract(Expr::literal(1), Expr::literal(2)), Expr::subtract(Expr::literal(2), Expr::literal(1)), ]; - yield Subtract::class . ': different type' => [ + yield '-: different type' => [ Expr::subtract(Expr::literal(1), Expr::literal(2)), Expr::literal(1), ]; @@ -199,27 +194,27 @@ public static function notEqualsCases(): iterable Expr::call(Expr::literal(1), 'foo', Type::int(), []), Expr::literal(1), ]; - yield Gt::class . ': left is different' => [ + yield '>: left is different' => [ Expr::gt(Expr::literal(1), Expr::literal(2)), Expr::gt(Expr::literal(2), Expr::literal(2)), ]; - yield Gt::class . ': right is different' => [ + yield '>: right is different' => [ Expr::gt(Expr::literal(1), Expr::literal(2)), Expr::gt(Expr::literal(1), Expr::literal(1)), ]; - yield Gt::class . ': both are different' => [ + yield '>: both are different' => [ Expr::gt(Expr::literal(1), Expr::literal(2)), Expr::gt(Expr::literal(2), Expr::literal(1)), ]; - yield Gt::class . ': different type' => [ + yield '>: different type' => [ Expr::gt(Expr::literal(1), Expr::literal(2)), Expr::eq(Expr::literal(1), Expr::literal(2)), ]; - yield Negative::class . ': different type' => [ + yield 'Negative: different type' => [ Expr::negative(Expr::literal(1)), Expr::literal(1), ]; - yield Negative::class . ': different expression' => [ + yield 'Negative: different expression' => [ Expr::negative(Expr::literal(1)), Expr::negative(Expr::literal(2)), ]; diff --git a/tests/unit/ExpressionTest.php b/tests/unit/ExpressionTest.php index d33897b..a1ab877 100644 --- a/tests/unit/ExpressionTest.php +++ b/tests/unit/ExpressionTest.php @@ -228,6 +228,7 @@ public static function toStringCases(): iterable ]; yield 'Any type' => ['myval:any', 'myval:any']; yield 'List literal' => ['["foo", "bar"]', '["foo", "bar"]']; + yield 'Negative variable' => ['-myval:int', '-myval:int']; } /** diff --git a/tests/unit/Parser/TypeParserTest.php b/tests/unit/Parser/TypeParserTest.php index b29b5bb..751f3ee 100644 --- a/tests/unit/Parser/TypeParserTest.php +++ b/tests/unit/Parser/TypeParserTest.php @@ -10,7 +10,6 @@ use LogicException; use PHPUnit\Framework\TestCase; -use function assert; use function explode; use function implode; use function preg_match; @@ -76,7 +75,7 @@ public function testSyntaxErrors(string $type, string $expectedMessage): void } $startCol = strlen($matches['indent']) + 1; $endCol = strlen($matches['indent']) + strlen($matches['marker']); - assert($endCol > 0, 'End column can\'t be lower than 1 because the marker is at least one character long'); + /** @psalm-suppress InvalidArgument False positive */ $expectedSpan = new Span($lineNumber, $startCol, $lineNumber, $endCol); unset($lines[$lineIndex]); }