fix(optimizer): intval() with a base must not drop the base - #28
fix(optimizer): intval() with a base must not drop the base#28Giandonn wants to merge 2 commits into
Conversation
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++.
matyhtf
left a comment
There was a problem hiding this comment.
Thank you for the clear report and for covering both literal and variable bases. The ordinary two-argument intval() cases are fixed correctly.
There is still one conversion path that silently drops the call semantics: argument unpacking. An unpacked call has one Node\Arg in the AST regardless of its runtime arity, so the new count($expr->args) === 1 check still accepts it as a single-argument Native conversion.
For example:
$withBase = ['ff', 16];
var_dump(intval(...$withBase)); // PHP: 255
$single = ['42'];
var_dump(intval(...$single)); // PHP: 42With this PR, both calls are currently lowered as conversions of the array itself:
php::toInt(withBase);
php::toInt(single);This produces the wrong result and bypasses unpacking entirely. The same structural issue applies to unpacked strval(), floatval(), and boolval() calls.
Please also reject an unpacked argument in dispatchConversion(), for example:
if (
count($expr->args) !== 1
|| !($expr->args[0] instanceof Node\Arg)
|| $expr->args[0]->unpack
) {
return false;
}That form must stay on the dynamic call path so the runtime can determine the expanded arity. Please add PHPT coverage for both intval(...['ff', 16]) and a single expanded argument, plus a code-generation assertion that the unpacked array is not passed to php::toInt().
The PR's current targeted PHPUnit and PHPT tests pass locally, but no GitHub checks are reported yet. Once the unpack case is covered, the core fix looks good.
An unpacked call carries a single Node\Arg regardless of its runtime
arity, so the arity check alone still accepted intval(...$args) as a
single-argument Native conversion and lowered the array itself:
intval(...['ff', 16]) // php::toInt(withBase) -> int(1)
strval(...['42']) // php::toString(single) -> "Array" + warning
A named argument has the same shape and need not be the value being
converted: intval(bogus: 1) must raise "Unknown named parameter", not
fold to a cast of 1.
Reject both in dispatchConversion() so the runtime determines the
expanded arity and the parameter names, matching what
dispatchFuncCall() already does for every other builtin.
|
Thank you — you are right, and the failure is worse than a wrong result. I have pushed the guard. Reproducing it. Adding your two examples to So The guard. intval(bogus: 1); // PHP: Error: Unknown named parameter $bogus
// before: php::toInt(1) -> int(1)That makes the four conversions agree with Coverage. Verified locally on PHP 8.5.4 ZTS with embed: the PHPT compiles and runs green, |
Fixes #27
The four scalar conversions are lowered to a single-argument Native cast by
dispatchConversion, which readsargs[0]and ignores the rest.intval()takes a
$baseas its second argument, so it was silently discarded.Measured on a compiled binary (PHP 8.5.4 ZTS + embed, GCC 15, Linux x64):
intval("ff", 16)intval("0x1A", 16)intval("101", 2)The change
A conversion call with any arity other than one now returns
falsefromdispatchConversion, so it falls through to the dynamic path and both argumentsreach the runtime function. Single-argument
intval(),strval(),floatval()and
boolval()still lower tophp::toInt()and friends, so the common case isuntouched.
Tests
tests/compiler/stdlib/type_conv.phptgains the base cases, with a literaland a variable base. It already covered six single-argument
intval()calls;no test in the suite passed a base.
phpunit/src/ConversionArityTest.phppins both lowering decisions in thegenerated C++ and needs no native toolchain.
The PHPUnit test fails on master and passes with the change.
type_conv.phptpasses against a real build. The full PHPUnit suite reports the same results as
master, and PHPStan reports no new findings for the touched file.