fix(parser): parenthesize unary minus operands that can change the C++ parse (#46) --skip-tests

* 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<double>::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.
master
Alessio Giacobbe 1 day ago committed by GitHub
parent 014d5d55cb
commit e259b3fbd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      phpunit/code/unary-minus-codegen.php
  2. 3
      phpunit/src/OperatorTest.php
  3. 43
      phpunit/src/UnaryMinusCodegenTest.php
  4. 18
      src/Parser/UnaryExpressionTrait.php
  5. 35
      tests/compiler/operator/unary-minus-parens.phpt

@ -0,0 +1,11 @@
<?php
function pickNegated(int $a, int $b, int $c): int
{
return -($a ? $b : $c);
}
function doubleNegate(int $a): int
{
return - -$a;
}

@ -107,7 +107,8 @@ class OperatorTest extends \BaseTest
$this->assertStringContainsString('1.0', $cpp);
$this->assertStringContainsString('0.0', $cpp);
$this->assertStringContainsString('std::numeric_limits<double>::infinity()', $cpp);
$this->assertStringContainsString('-std::numeric_limits<double>::infinity()', $cpp);
// Unary minus always parenthesizes its operand (see parseUnaryMinus).
$this->assertStringContainsString('-(std::numeric_limits<double>::infinity())', $cpp);
$this->assertStringContainsString('std::numeric_limits<double>::quiet_NaN()', $cpp);
$this->assertStringContainsString('2.7182818284590451', $cpp);
$this->assertStringNotContainsString('2.718281828459)', $cpp);

@ -0,0 +1,43 @@
<?php
use TypePhp\CompilerTest;
/**
* Unary minus must parenthesize its operand. Without the parentheses,
* `-($a ? $b : $c)` emits `-cond ? b : c`, which C++ parses as
* `(-cond) ? b : c`: the minus is applied to the condition instead of the
* selected branch, and the branch choice itself can flip.
*/
final class UnaryMinusCodegenTest extends \BaseTest
{
public function testUnaryMinusParenthesizesTernaryOperand(): void
{
$code = $this->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;
}
}

@ -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

@ -0,0 +1,35 @@
--TEST--
Unary minus applies to the whole operand expression
--FILE--
<?php
declare(strict_types=1);
function pick(int $a, int $b, int $c): int
{
return -($a ? $b : $c);
}
function doubleNegate(int $a): int
{
return - -$a;
}
function main(): void
{
var_dump(pick(1, 2, 3));
var_dump(pick(0, 2, 3));
var_dump(doubleNegate(5));
var_dump(doubleNegate(-5));
$x = 4;
var_dump(-($x ?: 7));
$y = 0;
var_dump(-($y ?: 7));
}
?>
--EXPECT--
int(-2)
int(-3)
int(5)
int(-5)
int(-4)
int(-7)
Loading…
Cancel
Save