From 7414cb5e09d3467cdc68289ae89ba3a953005481 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 18 May 2026 18:20:07 +0000 Subject: [PATCH] Implement spaceship operator (<=>) for VM and JIT (#208) Add TYPE_SPACESHIP opcode end-to-end with LLVM icmp/select lowering for native int/float pairs, VM spaceshipOp with string strcmp semantics, and compliance PHPTs. Patch php-types TypeReconstructor for Expr_BinaryOp_Spaceship. Also fix #211 compliance tests (correct EXPECT, bool param, loose equals). Co-authored-by: Cursor --- lib/Compiler.php | 2 + lib/JIT.php | 1 + lib/JIT.pre | 1 + lib/JIT/Helper.php | 21 ++++ lib/JIT/Helper.pre | 22 +++++ lib/OpCode.php | 1 + lib/VM.php | 6 ++ lib/VM/Variable.php | 95 +++++++++++++++++++ patches/php-types-binaryop-spaceship.patch | 10 ++ script/apply-patches.sh | 1 + .../cases/language/not_equal_identical.phpt | 4 +- .../language/not_equal_identical_jit.phpt | 2 +- .../cases/language/spaceship_operator.phpt | 9 ++ .../language/spaceship_operator_jit.phpt | 7 ++ 14 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 patches/php-types-binaryop-spaceship.patch create mode 100644 test/compliance/cases/language/spaceship_operator.phpt create mode 100644 test/compliance/cases/language/spaceship_operator_jit.phpt diff --git a/lib/Compiler.php b/lib/Compiler.php index 10e5025a..e2da6e10 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -280,6 +280,8 @@ protected function getOpCodeTypeFromBinaryOp(Op\Expr\BinaryOp $expr): int { return OpCode::TYPE_IDENTICAL; } elseif ($expr instanceof Op\Expr\BinaryOp\NotIdentical) { return OpCode::TYPE_NOT_IDENTICAL; + } elseif ($expr instanceof Op\Expr\BinaryOp\Spaceship) { + return OpCode::TYPE_SPACESHIP; } elseif ($expr instanceof Op\Expr\BinaryOp\Minus) { return OpCode::TYPE_MINUS; } elseif ($expr instanceof Op\Expr\BinaryOp\Mul) { diff --git a/lib/JIT.php b/lib/JIT.php index b178e922..878ef839 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -414,6 +414,7 @@ private function compileBlockInternal( case OpCode::TYPE_NOT_IDENTICAL: case OpCode::TYPE_EQUAL: case OpCode::TYPE_NOT_EQUAL: + case OpCode::TYPE_SPACESHIP: $this->assignOperand( $block->getOperand($op->arg1), $this->context->helper->binaryOp( diff --git a/lib/JIT.pre b/lib/JIT.pre index 24730166..aff76d38 100755 --- a/lib/JIT.pre +++ b/lib/JIT.pre @@ -343,6 +343,7 @@ class JIT { case OpCode::TYPE_NOT_IDENTICAL: case OpCode::TYPE_EQUAL: case OpCode::TYPE_NOT_EQUAL: + case OpCode::TYPE_SPACESHIP: $this->assignOperand( $block->getOperand($op->arg1), $this->context->helper->binaryOp( diff --git a/lib/JIT/Helper.php b/lib/JIT/Helper.php index 8601866b..b9b96c19 100644 --- a/lib/JIT/Helper.php +++ b/lib/JIT/Helper.php @@ -108,6 +108,15 @@ public function binaryOp(OpCode $opcode, Variable $left, Variable $right): Varia case OpCode::TYPE_NOT_EQUAL: $result = $this->context->builder->fcmp(Builder::REAL_ONE, $leftValue, $rightValue); goto return_bool; + case OpCode::TYPE_SPACESHIP: + $lt = $this->context->builder->fcmp(Builder::REAL_OLT, $leftValue, $rightValue); + $gt = $this->context->builder->fcmp(Builder::REAL_OGT, $leftValue, $rightValue); + $ty = $leftValue->typeOf(); + $negOne = $ty->constInt(-1, true); + $one = $ty->constInt(1, true); + $zero = $ty->constInt(0, false); + $result = $this->context->builder->select($gt, $one, $this->context->builder->select($lt, $negOne, $zero)); + goto return_long; } break; case TYPE_PAIR_NATIVE_LONG_NATIVE_LONG: @@ -433,12 +442,24 @@ public function binaryOp(OpCode $opcode, Variable $left, Variable $right): Varia + + $result = $this->context->builder->icmp(\PHPLLVM\Builder::INT_NE, $leftValue, $__right); goto return_bool; + case OpCode::TYPE_SPACESHIP: + $__right = $this->context->builder->intCast($rightValue, $leftValue->typeOf()); + $lt = $this->context->builder->icmp(\PHPLLVM\Builder::INT_SLT, $leftValue, $__right); + $gt = $this->context->builder->icmp(\PHPLLVM\Builder::INT_SGT, $leftValue, $__right); + $ty = $leftValue->typeOf(); + $negOne = $ty->constInt(-1, true); + $one = $ty->constInt(1, true); + $zero = $ty->constInt(0, false); + $result = $this->context->builder->select($gt, $one, $this->context->builder->select($lt, $negOne, $zero)); + goto return_long; } break; case TYPE_PAIR_NATIVE_LONG_NATIVE_BOOL: diff --git a/lib/JIT/Helper.pre b/lib/JIT/Helper.pre index 51f1f6cf..69e0a69e 100644 --- a/lib/JIT/Helper.pre +++ b/lib/JIT/Helper.pre @@ -105,6 +105,17 @@ restart: case OpCode::TYPE_NOT_EQUAL: $result = $this->context->builder->fcmp(Builder::REAL_ONE, $leftValue, $rightValue); goto return_bool; + case OpCode::TYPE_SPACESHIP: + compile { + if ($leftValue < $rightValue) { + $result = -1; + } elseif ($leftValue > $rightValue) { + $result = 1; + } else { + $result = 0; + } + } + goto return_long; } break; case TYPE_PAIR_NATIVE_LONG_NATIVE_LONG: @@ -181,6 +192,17 @@ restart: $result = $leftValue != $rightValue; } goto return_bool; + case OpCode::TYPE_SPACESHIP: + compile { + if ($leftValue < $rightValue) { + $result = -1; + } elseif ($leftValue > $rightValue) { + $result = 1; + } else { + $result = 0; + } + } + goto return_long; } break; case TYPE_PAIR_NATIVE_LONG_NATIVE_BOOL: diff --git a/lib/OpCode.php b/lib/OpCode.php index a2180e2b..51223835 100755 --- a/lib/OpCode.php +++ b/lib/OpCode.php @@ -70,6 +70,7 @@ class OpCode { const TYPE_POW = 58; const TYPE_NOT_EQUAL = 59; const TYPE_NOT_IDENTICAL = 60; + const TYPE_SPACESHIP = 61; public int $type; public ?int $arg1; diff --git a/lib/VM.php b/lib/VM.php index a342b9af..d358a3a3 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -109,6 +109,12 @@ public function run(Block $block): int { $arg3 = $frame->scope[$op->arg3]; $arg1->compareOp($op->type, $arg2, $arg3); break; + case OpCode::TYPE_SPACESHIP: + $arg1 = $frame->scope[$op->arg1]; + $arg2 = $frame->scope[$op->arg2]; + $arg3 = $frame->scope[$op->arg3]; + $arg1->spaceshipOp($arg2, $arg3); + break; case OpCode::TYPE_PLUS: case OpCode::TYPE_MINUS: case OpCode::TYPE_MUL: diff --git a/lib/VM/Variable.php b/lib/VM/Variable.php index 12b93bfc..b0b9c3fc 100755 --- a/lib/VM/Variable.php +++ b/lib/VM/Variable.php @@ -373,10 +373,55 @@ public function equals(Variable $other): bool { $other = $other->indirect; goto restart; } + + return $this->looseEquals($self, $other); } throw new \LogicException("Equals comparison between {$self->type} and {$other->type} not implemented"); } + private function looseEquals(self $left, self $right): bool { + if ($left->type === self::TYPE_NULL || $right->type === self::TYPE_NULL) { + $null = $left->type === self::TYPE_NULL ? $left : $right; + $other = $left->type === self::TYPE_NULL ? $right : $left; + if ($other->type === self::TYPE_NULL) { + return true; + } + if ($other->type === self::TYPE_BOOLEAN) { + return $null->type === self::TYPE_NULL && !$other->bool; + } + if ($other->type === self::TYPE_INTEGER) { + return 0 === $other->integer; + } + if ($other->type === self::TYPE_STRING) { + return '' === $other->string; + } + + return false; + } + if ($left->type === self::TYPE_BOOLEAN || $right->type === self::TYPE_BOOLEAN) { + $bool = $left->type === self::TYPE_BOOLEAN ? $left : $right; + $other = $left->type === self::TYPE_BOOLEAN ? $right : $left; + if ($other->type === self::TYPE_INTEGER) { + return ($bool->bool ? 1 : 0) === $other->integer; + } + if ($other->type === self::TYPE_STRING) { + return ($bool->bool ? '1' : '0') === $other->string + || ($bool->bool && is_numeric($other->string) && (int) $other->string !== 0); + } + + return false; + } + if ($left->type === self::TYPE_INTEGER && $right->type === self::TYPE_STRING) { + return (string) $left->integer === $right->string + || ($right->string !== '' && is_numeric($right->string) && $left->integer == $right->string); + } + if ($left->type === self::TYPE_STRING && $right->type === self::TYPE_INTEGER) { + return $this->looseEquals($right, $left); + } + + return false; + } + public function compareOp(int $opCode, Variable $left, Variable $right): void { $this->reset(); restart: @@ -432,6 +477,56 @@ private function _compareOp(int $opCode, $left, $right): bool { } } + public function spaceshipOp(Variable $left, Variable $right): void { + $this->reset(); +restart: + switch (type_pair($left->type, $right->type)) { + case TYPE_PAIR_INTEGER_INTEGER: + $this->int($this->_spaceship($left->integer, $right->integer)); + break; + case TYPE_PAIR_INTEGER_FLOAT: + $this->int($this->_spaceship($left->integer, $right->float)); + break; + case TYPE_PAIR_FLOAT_INTEGER: + $this->int($this->_spaceship($left->float, $right->integer)); + break; + case TYPE_PAIR_FLOAT_FLOAT: + $this->int($this->_spaceship($left->float, $right->float)); + break; + case TYPE_PAIR_STRING_STRING: + $cmp = strcmp($left->string, $right->string); + $this->int($cmp < 0 ? -1 : ($cmp > 0 ? 1 : 0)); + break; + case TYPE_PAIR_BOOLEAN_BOOLEAN: + $this->int($this->_spaceship((int) $left->bool, (int) $right->bool)); + break; + case TYPE_PAIR_NULL_NULL: + $this->int(0); + break; + default: + if ($left->type === self::TYPE_INDIRECT) { + $left = $left->indirect; + goto restart; + } elseif ($right->type === self::TYPE_INDIRECT) { + $right = $right->indirect; + goto restart; + } else { + $this->int($this->_spaceship($left->toNumeric(), $right->toNumeric())); + } + } + } + + private function _spaceship($left, $right): int { + if ($left < $right) { + return -1; + } + if ($left > $right) { + return 1; + } + + return 0; + } + public function bitwiseOp(int $opCode, Variable $left, Variable $right): void { $this->reset(); restart: diff --git a/patches/php-types-binaryop-spaceship.patch b/patches/php-types-binaryop-spaceship.patch new file mode 100644 index 00000000..5abdc9fc --- /dev/null +++ b/patches/php-types-binaryop-spaceship.patch @@ -0,0 +1,10 @@ +--- vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php ++++ vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php +@@ -205,6 +205,7 @@ + case 'Expr_BinaryOp_Mod': + case 'Expr_BinaryOp_ShiftLeft': + case 'Expr_BinaryOp_ShiftRight': ++ case 'Expr_BinaryOp_Spaceship': + case 'Expr_Cast_Int': + case 'Expr_Print': + return [Type::int()]; diff --git a/script/apply-patches.sh b/script/apply-patches.sh index a32acdc3..41c5add8 100755 --- a/script/apply-patches.sh +++ b/script/apply-patches.sh @@ -33,6 +33,7 @@ apply_patch "$PATCH_DIR/php-llvm-x86-posix-fallback.patch" if [[ -d "$ROOT/vendor/ircmaxell/php-types" ]]; then apply_patch "$PATCH_DIR/php-types-binaryop-pow.patch" + apply_patch "$PATCH_DIR/php-types-binaryop-spaceship.patch" apply_patch "$PATCH_DIR/php-types-str-bool-fns.patch" apply_patch "$PATCH_DIR/php-types-dollars-brace.patch" fi diff --git a/test/compliance/cases/language/not_equal_identical.phpt b/test/compliance/cases/language/not_equal_identical.phpt index 97d0f079..2f4c3e55 100644 --- a/test/compliance/cases/language/not_equal_identical.phpt +++ b/test/compliance/cases/language/not_equal_identical.phpt @@ -2,7 +2,7 @@ Not-equal and not-identical operators (VM/JIT parity) --FILE-- ) for numbers and strings +--FILE-- + 2, 2 <=> 2, 3 <=> 2, "\n"; +echo 'b' <=> 'a', 'a' <=> 'a', 'a' <=> 'b', "\n"; +--EXPECT-- +-101 +10-1 diff --git a/test/compliance/cases/language/spaceship_operator_jit.phpt b/test/compliance/cases/language/spaceship_operator_jit.phpt new file mode 100644 index 00000000..90ee8e7f --- /dev/null +++ b/test/compliance/cases/language/spaceship_operator_jit.phpt @@ -0,0 +1,7 @@ +--TEST-- +Spaceship operator (<=>) under JIT for integers +--FILE-- + 2, 2 <=> 2, 3 <=> 2; +--EXPECT-- +-101