From 9a63e9e3d67285e22be7f4dfda852362eff423ae Mon Sep 17 00:00:00 2001 From: Alessio Giacobbe Date: Mon, 31 Aug 2026 12:32:20 +0200 Subject: [PATCH 1/3] fix(parser): always parenthesize the unary minus operand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- phpunit/code/unary-minus-codegen.php | 11 +++++ phpunit/src/UnaryMinusCodegenTest.php | 43 +++++++++++++++++++ src/Parser/UnaryExpressionTrait.php | 15 +++---- .../compiler/operator/unary-minus-parens.phpt | 35 +++++++++++++++ 4 files changed, 95 insertions(+), 9 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 @@ +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..2d379af4 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -125,15 +125,12 @@ 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 . ')'; - } - - return '-' . $code; + // Always parenthesize the 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) From 69c7b1c6a652da942779593a5649121be11c0023 Mon Sep 17 00:00:00 2001 From: Alessio Giacobbe Date: Mon, 31 Aug 2026 12:48:42 +0200 Subject: [PATCH 2/3] 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. --- phpunit/src/OperatorTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpunit/src/OperatorTest.php b/phpunit/src/OperatorTest.php index 92e4823a..0e569f66 100644 --- a/phpunit/src/OperatorTest.php +++ b/phpunit/src/OperatorTest.php @@ -107,7 +107,8 @@ public function testFloatLiteralSpecialValuesAndWholeNumbers(): void $this->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); From cbd018d70f4db0f09642fac9ea85f4c68f801858 Mon Sep 17 00:00:00 2001 From: Alessio Giacobbe Date: Tue, 1 Sep 2026 11:55:53 +0200 Subject: [PATCH 3/3] 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. --- src/Parser/UnaryExpressionTrait.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 2d379af4..9fd45e5a 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -125,7 +125,14 @@ protected function parseUnaryMinus(Expr\UnaryMinus $expr): string } $code = $this->parseExprAsValue($expr->expr); - // Always parenthesize the operand. An unparenthesized operand can + // 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; + } + + // 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++