Skip to content

Commit

Permalink
Implement comparison operators (address #9)
Browse files Browse the repository at this point in the history
  • Loading branch information
Muqsit committed Jul 9, 2024
1 parent 3413e18 commit 427e2f4
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 41 deletions.
10 changes: 10 additions & 0 deletions src/muqsit/arithmexp/ParseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ public static function noBinaryOperandRight(string $expression, Position $positi
), self::ERR_NO_OPERAND_BINARY_RIGHT));
}

public static function undefinedOperatorAssociativity(string $expression, Position $position) : self{
return self::generateWithHighlightedSubstring(new self($expression, $position, sprintf(
"Associativity cannot be implicitly determined for operator at \"%s\" (%d:%d) in \"%s\"",
$position->in($expression),
$position->start,
$position->end,
$expression
), self::ERR_NO_OPERAND_BINARY_LEFT));
}

public static function noClosingParenthesis(string $expression, Position $position) : self{
return self::generateWithHighlightedSubstring(new self($expression, $position, sprintf(
"No closing parenthesis specified for opening parenthesis at \"%s\" (%d:%d) in \"%s\"",
Expand Down
2 changes: 1 addition & 1 deletion src/muqsit/arithmexp/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ private function groupOperatorTokens(string $expression, array &$tokens) : void{
$prioritize = [];
do{
foreach($this->operator_manager->getByPrecedence() as $list){
foreach($list->assignment->traverse($list, $entry) as $state){
foreach($list->assignment->traverse($list, $expression, $entry) as $state){
$index = $state->index;
$token = $state->value;

Expand Down
116 changes: 94 additions & 22 deletions src/muqsit/arithmexp/expression/RawExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,32 @@ public function __construct(string $expression, array $postfix_expression_tokens

$i = 0;
foreach($postfix_expression_tokens as $token){
$this->by_kind[($i++ << 4) | match(true){
$this->by_kind[($i++ << 5) | match(true){
$token instanceof OpcodeExpressionToken => match($token->code){
OpcodeToken::OP_BINARY_ADD => 0,
OpcodeToken::OP_BINARY_DIV => 1,
OpcodeToken::OP_BINARY_EXP => 2,
OpcodeToken::OP_BINARY_MOD => 3,
OpcodeToken::OP_BINARY_MUL => 4,
OpcodeToken::OP_BINARY_SUB => 5,
OpcodeToken::OP_UNARY_NOT => 6,
OpcodeToken::OP_UNARY_NVE => 7,
OpcodeToken::OP_UNARY_PVE => 8
OpcodeToken::OP_BINARY_EQUAL => 2,
OpcodeToken::OP_BINARY_EQUAL_NOT => 3,
OpcodeToken::OP_BINARY_EXP => 4,
OpcodeToken::OP_BINARY_GREATER_THAN => 5,
OpcodeToken::OP_BINARY_GREATER_THAN_EQUAL_TO => 6,
OpcodeToken::OP_BINARY_IDENTICAL => 7,
OpcodeToken::OP_BINARY_IDENTICAL_NOT => 8,
OpcodeToken::OP_BINARY_LESSER_THAN => 9,
OpcodeToken::OP_BINARY_LESSER_THAN_EQUAL_TO => 10,
OpcodeToken::OP_BINARY_MOD => 11,
OpcodeToken::OP_BINARY_MUL => 12,
OpcodeToken::OP_BINARY_SPACESHIP => 13,
OpcodeToken::OP_BINARY_SUB => 14,
OpcodeToken::OP_UNARY_NOT => 15,
OpcodeToken::OP_UNARY_NVE => 16,
OpcodeToken::OP_UNARY_PVE => 17
},
$token instanceof NumericLiteralExpressionToken,
$token instanceof BooleanLiteralExpressionToken => 9,
$token instanceof VariableExpressionToken => 10,
$token instanceof FunctionCallExpressionToken => 11,
default => 15
$token instanceof BooleanLiteralExpressionToken => 18,
$token instanceof VariableExpressionToken => 19,
$token instanceof FunctionCallExpressionToken => 20,
default => 31
}] = $token;
}
}
Expand All @@ -63,7 +72,7 @@ public function evaluate(array $variable_values = []) : int|float|bool{
$stack = [];
$ptr = -1;
foreach($this->by_kind as $index => $token){
switch($index & 15){
switch($index & 31){
case 0:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_ADD);
Expand All @@ -77,53 +86,116 @@ public function evaluate(array $variable_values = []) : int|float|bool{
$stack[--$ptr] /= $rvalue;
break;
case 2:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_EQUAL);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue == $rvalue;
break;
case 3:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_EQUAL_NOT);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue != $rvalue;
break;
case 4:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_EXP);
$rvalue = $stack[$ptr];
$stack[--$ptr] **= $rvalue;
break;
case 3:
case 5:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_GREATER_THAN);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue > $rvalue;
break;
case 6:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_GREATER_THAN_EQUAL_TO);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue >= $rvalue;
break;
case 7:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_IDENTICAL);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue === $rvalue;
break;
case 8:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_IDENTICAL_NOT);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue !== $rvalue;
break;
case 9:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_LESSER_THAN);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue < $rvalue;
break;
case 10:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_LESSER_THAN_EQUAL_TO);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue <= $rvalue;
break;
case 11:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_MOD);
$rvalue = $stack[$ptr];
$stack[--$ptr] %= $rvalue;
break;
case 4:
case 12:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_MUL);
$rvalue = $stack[$ptr];
$stack[--$ptr] *= $rvalue;
break;
case 5:
case 13:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_SPACESHIP);
$lvalue = $stack[$ptr - 1];
$rvalue = $stack[$ptr];
$stack[--$ptr] = $lvalue <=> $rvalue;
break;
case 14:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_BINARY_SUB);
$rvalue = $stack[$ptr];
$stack[--$ptr] -= $rvalue;
break;
case 6:
case 15:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_UNARY_NOT);
$stack[$ptr] = !$stack[$ptr];
break;
case 7:
case 16:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_UNARY_NVE);
$stack[$ptr] = -$stack[$ptr];
break;
case 8:
case 17:
assert($token instanceof OpcodeExpressionToken);
assert($token->code === OpcodeToken::OP_UNARY_PVE);
$stack[$ptr] = +$stack[$ptr];
break;
case 9:
case 18:
assert($token instanceof BooleanLiteralExpressionToken || $token instanceof NumericLiteralExpressionToken);
$stack[++$ptr] = $token->value;
break;
case 10:
case 19:
assert($token instanceof VariableExpressionToken);
$stack[++$ptr] = $variable_values[$token->label] ?? throw new InvalidArgumentException("No value supplied for variable \"{$token->label}\" in \"{$this->expression}\"");;
break;
case 11:
case 20:
assert($token instanceof FunctionCallExpressionToken);
$ptr -= $token->argument_count - 1;
$stack[$ptr] = ($token->function)(...array_slice($stack, $ptr, $token->argument_count));
Expand Down
5 changes: 4 additions & 1 deletion src/muqsit/arithmexp/operator/OperatorManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

use InvalidArgumentException;
use muqsit\arithmexp\operator\assignment\LeftOperatorAssignment;
use muqsit\arithmexp\operator\assignment\NonAssociativeOperatorAssignment;
use muqsit\arithmexp\operator\assignment\NullOperatorAssignment;
use muqsit\arithmexp\operator\assignment\OperatorAssignment;
use muqsit\arithmexp\operator\assignment\RightOperatorAssignment;
use muqsit\arithmexp\operator\binary\BinaryOperator;
use muqsit\arithmexp\operator\binary\BinaryOperatorRegistry;
use muqsit\arithmexp\operator\unary\UnaryOperator;
use muqsit\arithmexp\operator\unary\UnaryOperatorRegistry;
use function array_key_first;

final class OperatorManager{

Expand Down Expand Up @@ -69,7 +71,7 @@ private function sortedByPrecedence() : array{
foreach($sorted as $list){
$assignments = array_unique(array_map(static fn(BinaryOperator|UnaryOperator $operator) : int => $operator instanceof BinaryOperator ? $operator->getAssignment()->getType() : OperatorAssignment::TYPE_NA, $list));
if(count($assignments) > 1){
throw new InvalidArgumentException("Cannot process operators with same precedence ({$operator->getPrecedence()}) but different assignment types (" . implode(", ", $assignments) . ")");
throw new InvalidArgumentException("Cannot process operators with same precedence ({$list[array_key_first($list)]->getPrecedence()}) but different assignment types (" . implode(", ", $assignments) . ")");
}

$binary = [];
Expand All @@ -84,6 +86,7 @@ private function sortedByPrecedence() : array{
$result[] = new OperatorList(match($assignments[array_key_first($assignments)]){
OperatorAssignment::TYPE_LEFT => LeftOperatorAssignment::instance(),
OperatorAssignment::TYPE_RIGHT => RightOperatorAssignment::instance(),
OperatorAssignment::TYPE_NON_ASSOCIATIVE => NonAssociativeOperatorAssignment::instance(),
OperatorAssignment::TYPE_NA => NullOperatorAssignment::instance()
}, $binary, $unary);
}
Expand Down
2 changes: 2 additions & 0 deletions src/muqsit/arithmexp/operator/OperatorPrecedence.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ interface OperatorPrecedence{
public const UNARY_NOT = 2;
public const MULTIPLICATION_DIVISION_MODULO = 3;
public const ADDITION_SUBTRACTION = 4;
public const COMPARISON_GREATER_LESSER = 5;
public const COMPARISON_EQUALITY = 6;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function getType() : int{
return self::TYPE_LEFT;
}

public function traverse(OperatorList $list, array &$tokens) : Generator{
public function traverse(OperatorList $list, string $expression, array &$tokens) : Generator{
$state = new OperatorAssignmentTraverserState($tokens);
$operators = $list->binary;
$index = -1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace muqsit\arithmexp\operator\assignment;

use Generator;
use muqsit\arithmexp\operator\OperatorList;
use muqsit\arithmexp\ParseException;
use muqsit\arithmexp\token\BinaryOperatorToken;
use function count;
use function is_array;

final class NonAssociativeOperatorAssignment implements OperatorAssignment{

public static function instance() : self{
static $instance = null;
return $instance ??= new self();
}

private function __construct(){
}

public function getType() : int{
return self::TYPE_NON_ASSOCIATIVE;
}

private function getPairPrecedence(OperatorList $list, mixed $entry) : ?int{
if(!is_array($entry) || count($entry) !== 3 || !($entry[1] instanceof BinaryOperatorToken)){
return null;
}
if(!isset($list->binary[$entry[1]->operator])){
return null;
}
return $list->binary[$entry[1]->operator]->getPrecedence();
}

public function traverse(OperatorList $list, string $expression, array &$tokens) : Generator{
$state = new OperatorAssignmentTraverserState($tokens);
$index = -1;
$count = count($tokens);
while(++$index < $count){
$value = $tokens[$index];
if($value instanceof BinaryOperatorToken && isset($list->binary[$value->operator])){
$precedence = $list->binary[$value->operator]->getPrecedence();
if($index > 0){
$lvalue = $tokens[$index - 1];
$lvalue_precedence = $this->getPairPrecedence($list, $lvalue);
if($lvalue_precedence === $precedence){
throw ParseException::undefinedOperatorAssociativity($expression, $value->getPos());
}
}
if($index + 1 < $count){
$rvalue = $tokens[$index + 1];
$rvalue_precedence = $this->getPairPrecedence($list, $rvalue);
if($rvalue_precedence === $precedence){
throw ParseException::undefinedOperatorAssociativity($expression, $rvalue[1]->getPos());
}
}
$state->index = $index;
$state->value = $value;
yield $state;
if($state->changed){
$index = -1;
$count = count($tokens);
$state->changed = false;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ public function getType() : int{
return self::TYPE_NA;
}

public function traverse(OperatorList $list, array &$tokens) : Generator{
public function traverse(OperatorList $list, string $expression, array &$tokens) : Generator{
$state = new OperatorAssignmentTraverserState($tokens);
$operators = $list->unary;
for($i = count($tokens) - 1; $i >= 0; --$i){
$token = $tokens[$i];
if($token instanceof UnaryOperatorToken && isset($operators[$token->operator])){
if($token instanceof UnaryOperatorToken && isset($list->unary[$token->operator])){
$state->index = $i;
$state->value = $token;
yield $state;
Expand Down
12 changes: 8 additions & 4 deletions src/muqsit/arithmexp/operator/assignment/OperatorAssignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

use Generator;
use muqsit\arithmexp\operator\OperatorList;
use muqsit\arithmexp\ParseException;
use muqsit\arithmexp\token\Token;

interface OperatorAssignment{

public const TYPE_LEFT = 0;
public const TYPE_RIGHT = 1;
public const TYPE_NA = 2;
public const TYPE_LEFT = 0; // x + y + z = (x + y) + z
public const TYPE_RIGHT = 1; // x ** y ** z = x ** (y ** z)
public const TYPE_NA = 2; // undefined assignment behaviour
public const TYPE_NON_ASSOCIATIVE = 3; // x == y == z = illegal. implicit associativity is disallowed.

/**
* @return self::TYPE_*
Expand All @@ -21,8 +23,10 @@ public function getType() : int;

/**
* @param OperatorList $list
* @param string $expression
* @param list<Token|list<Token>> $tokens
* @return Generator<OperatorAssignmentTraverserState>
* @throws ParseException
*/
public function traverse(OperatorList $list, array &$tokens) : Generator;
public function traverse(OperatorList $list, string $expression, array &$tokens) : Generator;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function getType() : int{
return self::TYPE_RIGHT;
}

public function traverse(OperatorList $list, array &$tokens) : Generator{
public function traverse(OperatorList $list, string $expression, array &$tokens) : Generator{
$state = new OperatorAssignmentTraverserState($tokens);
$operators = $list->binary;
$index = count($tokens);
Expand Down
Loading

0 comments on commit 427e2f4

Please sign in to comment.