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
parent
014d5d55cb
commit
e259b3fbd1
5 changed files with 102 additions and 8 deletions
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue