diff --git a/phpunit/code/unary-minus-codegen.php b/phpunit/code/unary-minus-codegen.php new file mode 100644 index 00000000..c8d60f4b --- /dev/null +++ b/phpunit/code/unary-minus-codegen.php @@ -0,0 +1,11 @@ +assertStringContainsString('1.0', $cpp); $this->assertStringContainsString('0.0', $cpp); $this->assertStringContainsString('std::numeric_limits::infinity()', $cpp); - $this->assertStringContainsString('-std::numeric_limits::infinity()', $cpp); + // Unary minus always parenthesizes its operand (see parseUnaryMinus). + $this->assertStringContainsString('-(std::numeric_limits::infinity())', $cpp); $this->assertStringContainsString('std::numeric_limits::quiet_NaN()', $cpp); $this->assertStringContainsString('2.7182818284590451', $cpp); $this->assertStringNotContainsString('2.718281828459)', $cpp); diff --git a/phpunit/src/UnaryMinusCodegenTest.php b/phpunit/src/UnaryMinusCodegenTest.php new file mode 100644 index 00000000..e6c5ed7d --- /dev/null +++ b/phpunit/src/UnaryMinusCodegenTest.php @@ -0,0 +1,43 @@ +compileFixture(); + + self::assertStringContainsString('-((php::toBool(a)) ? (b) : (c))', $code); + self::assertStringNotContainsString('-(php::toBool(a)) ?', $code); + } + + public function testNestedUnaryMinusDoesNotPasteIntoPreDecrement(): void + { + $code = $this->compileFixture(); + + self::assertStringNotContainsString('--a', $code); + } + + private function compileFixture(): string + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/unary-minus-codegen.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + return $code; + } +} diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index c4a92e6a..9fd45e5a 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -125,15 +125,19 @@ protected function parseUnaryMinus(Expr\UnaryMinus $expr): string } $code = $this->parseExprAsValue($expr->expr); - // An operand that already starts with `-` (a nested unary minus, a - // negative literal) would paste into the C++ pre-decrement token: - // `- -$a` -> `--a`. Parenthesize exactly then, so plain literals - // keep their compact `-7L` form. - if (str_starts_with($code, '-')) { - return '-(' . $code . ')'; + // A bare numeric literal is a single C++ token; negating it directly + // cannot change the parse, and keeps the emitted code (and the test + // snapshots built on it) readable. + if (preg_match('/^(?:\d[\d\'.]*(?:[eE][+-]?\d+)?|0[xX][0-9a-fA-F\']+|0[bB][01\']+)(?:[uU]?[lL]{0,2})?$/', $code)) { + return '-' . $code; } - return '-' . $code; + // Parenthesize every other operand. An unparenthesized operand can + // change the C++ parse: `-($a ? $b : $c)` would emit + // `-cond ? b : c`, binding the minus to the condition and possibly + // selecting the wrong branch, and `- -$a` would paste into the C++ + // pre-decrement token `--a`. + return '-(' . $code . ')'; } protected function parseUnaryPlus(Expr\UnaryPlus $expr): string diff --git a/tests/compiler/operator/unary-minus-parens.phpt b/tests/compiler/operator/unary-minus-parens.phpt new file mode 100644 index 00000000..8464b182 --- /dev/null +++ b/tests/compiler/operator/unary-minus-parens.phpt @@ -0,0 +1,35 @@ +--TEST-- +Unary minus applies to the whole operand expression +--FILE-- + +--EXPECT-- +int(-2) +int(-3) +int(5) +int(-5) +int(-4) +int(-7)