From e259b3fbd162d62158f2f26f0c5bc6d8680a583a Mon Sep 17 00:00:00 2001 From: Alessio Giacobbe Date: Tue, 1 Sep 2026 12:52:21 +0200 Subject: [PATCH] fix(parser): parenthesize unary minus operands that can change the C++ parse (#46) --skip-tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(parser): always parenthesize the unary minus operand Unary minus concatenated '-' directly onto the operand's generated C++. For a compound operand the minus then bound to the wrong subexpression: PHP's -($a ? $b : $c) emitted `-cond ? b : c`, which C++ parses as `(-cond) ? b : c` — the negation lands on the condition and the branch choice itself can flip (pick(1,2,3) returned 2 instead of -2). The previous str_starts_with('-') guard only covered operands already beginning with '-' (the `--` token-pasting case). Emit '-(' operand ')' unconditionally; this subsumes the pre-decrement guard. Unary '+' emits no operator text and boolean/bitwise not already close their operands, so they are unaffected. * test(operator): accept parenthesized negative infinity literal parseUnaryMinus now always parenthesizes its operand, so the -INF float literal is emitted as -(std::numeric_limits::infinity()). The C++ value is unchanged; only the spelling assertion needed updating. * fix(parser): keep bare numeric literals unparenthesized under unary minus A single-token numeric literal cannot change the C++ parse; emitting -7L directly keeps the generated code and the existing test snapshots readable. Every other operand stays parenthesized. --- phpunit/code/unary-minus-codegen.php | 11 +++++ phpunit/src/OperatorTest.php | 3 +- phpunit/src/UnaryMinusCodegenTest.php | 43 +++++++++++++++++++ src/Parser/UnaryExpressionTrait.php | 18 +++++--- .../compiler/operator/unary-minus-parens.phpt | 35 +++++++++++++++ 5 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 phpunit/code/unary-minus-codegen.php create mode 100644 phpunit/src/UnaryMinusCodegenTest.php create mode 100644 tests/compiler/operator/unary-minus-parens.phpt 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 @@ trait UnaryExpressionTrait } $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)