fix(optimizer): intval() with a base must not drop the base

The four scalar conversions are lowered to a single-argument Native cast
by dispatchConversion, which reads args[0] and ignores the rest. intval()
takes a $base as its second argument, so it was silently discarded:

    intval("ff", 16);    // php::toInt("ff")   -> 0,   PHP gives 255
    intval("0x1A", 16);  // php::toInt("0x1A") -> 0,   PHP gives 26
    intval("101", 2);    // php::toInt("101")  -> 101, PHP gives 5

Nothing reports the loss: the program compiles clean and the number is
simply wrong, which is easy to miss in the code that most often uses a
base - parsing hex colors, permission masks and binary flags.

A conversion call with any arity other than one now falls through to the
dynamic path, where both arguments are passed to the runtime function.
Single-argument intval(), strval(), floatval() and boolval() keep their
Native cast, so the common case is unchanged.

type_conv.phpt gains the base cases, with a literal and a variable base;
none of them were covered anywhere in the test suite. ConversionArityTest
pins the lowering decision in the generated C++.
master
Giandonn 1 day ago
parent b493ac79c5
commit 93909274e2
  1. 15
      phpunit/code/intval-single-argument.php
  2. 15
      phpunit/code/intval-with-base.php
  3. 52
      phpunit/src/ConversionArityTest.php
  4. 8
      src/Optimizer/FuncCallOptimizer.php
  5. 13
      tests/compiler/stdlib/type_conv.phpt

@ -0,0 +1,15 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
function main(): void
{
var_dump(intval('42'));
var_dump(strval(42));
var_dump(floatval('3.14'));
var_dump(boolval(1));
}

@ -0,0 +1,15 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
function main(): void
{
$base = 16;
var_dump(intval('ff', 16));
var_dump(intval('ff', $base));
}

@ -0,0 +1,52 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
/**
* @internal
* @coversNothing
*/
class ConversionArityTest extends TestCase
{
public function testIntvalWithABaseReachesTheRuntimeFunction(): void
{
$cpp = $this->compileToCpp('intval-with-base.php');
// The Native cast carries no base, so both calls must keep the second
// argument by going through the dynamic path.
self::assertStringNotContainsString('php::toInt(', $cpp);
self::assertSame(2, substr_count($cpp, '16L'));
}
public function testSingleArgumentConversionsStillLowerToNativeCasts(): void
{
$cpp = $this->compileToCpp('intval-single-argument.php');
self::assertStringContainsString('php::toInt(', $cpp);
self::assertStringContainsString('php::toString(', $cpp);
self::assertStringContainsString('php::toFloat(', $cpp);
self::assertStringContainsString('php::toBool(', $cpp);
}
private function compileToCpp(string $file): string
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
return file_get_contents($compiler->convertFile($source));
}
}

@ -527,8 +527,14 @@ trait FuncCallOptimizer
return $target . '(' . implode(', ', $args) . ')';
}
protected function dispatchConversion(Node\Expr\FuncCall $expr, string $convType): string
protected function dispatchConversion(Node\Expr\FuncCall $expr, string $convType): string|false
{
// These four are lowered as single-argument Native casts, which cannot
// carry intval()'s $base. Any other arity must reach the runtime
// function instead of silently dropping the extra argument.
if (count($expr->args) !== 1) {
return false;
}
$arg = $expr->args[0]->value;
$type = $this->detectTypeOfExpr($arg);
$nativeClass = $this->detectClassOfExpr($arg);

@ -18,6 +18,14 @@ function main() {
var_dump(intval(true));
var_dump(intval(false));
// intval with an explicit base
var_dump(intval("ff", 16));
var_dump(intval("0x1A", 16));
var_dump(intval("101", 2));
var_dump(intval("777", 8));
$base = 16;
var_dump(intval("ff", $base));
// floatval
var_dump(floatval(42));
var_dump(floatval("3.14"));
@ -44,6 +52,11 @@ int(42)
int(3)
int(1)
int(0)
int(255)
int(26)
int(5)
int(511)
int(255)
float(42)
float(3.14)
float(42)

Loading…
Cancel
Save