Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ext/standard/strval.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPCompiler\VM;
use PHPCompiler\VM\ErrorReporter;
use PHPCompiler\VM\Variable;
use PHPCompiler\VM\VmResourceIdString;
use PHPLLVM\Builder;
use PHPLLVM\Value;

Expand Down Expand Up @@ -68,7 +69,8 @@ public function call(Context $context, JITVariable ...$args): Value
case JITVariable::TYPE_NATIVE_LONG:
return JitResourceIdString::formatNativeLong(
$context,
$context->helper->loadValue($args[0])
$context->helper->loadValue($args[0]),
null
);
case JITVariable::TYPE_NATIVE_DOUBLE:
return \PHPCompiler\JIT\Builtin\ZendDoubleStringRuntime::format(
Expand Down Expand Up @@ -123,7 +125,7 @@ public function valueToString(Context $context, Value $valuePtr): Value
);

$context->builder->positionAtEnd($longBlock);
$longStr = JitResourceIdString::formatNativeLong(
$longStr = VmResourceIdString::formatBoxedNativeLong(
$context,
$context->builder->call($context->lookupFunction('__value__readLong'), $valuePtr)
);
Expand Down
32 changes: 24 additions & 8 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -8388,7 +8388,12 @@ private function compileBlockInternal(
JIT\StringOffsetHelper::emitAssignOpError($this->context);
break;
}
$newVal = $this->compileConcatIntoNewString($left, $right);
$newVal = $this->compileConcatIntoNewString(
$left,
$right,
$block->getOperand($op->arg2),
$block->getOperand($op->arg3)
);
$this->assignOperand($destOp, $newVal, true);
$this->maybeRefreshIncludeBindingsBeforeUse();
break;
Expand Down Expand Up @@ -8455,7 +8460,12 @@ private function compileBlockInternal(
if (null !== $result->objectPropertySlot) {
$this->compileObjectPropertyConcatOp($result, $left, $right);
} elseif (Variable::TYPE_VALUE === $result->type || JIT\JitValueBox::isValueOperand($result)) {
$newVal = $this->compileConcatIntoNewString($left, $right);
$newVal = $this->compileConcatIntoNewString(
$left,
$right,
$block->getOperand($op->arg2),
$block->getOperand($op->arg3)
);
JIT\JitValueBox::assignToPointer(
$this->context,
$this->valueBoxPointer($result),
Expand All @@ -8472,10 +8482,10 @@ private function compileBlockInternal(
// Fresh and in-place native concat: JitStringConcat + store. Avoid
// string->concat __string__realloc on entry allocas (AOT strlen→0, #15642).
$leftVar = $this->context->helper->loadValue(
JIT\JitNativeString::coerce($this->context, $left)
JIT\JitNativeString::coerce($this->context, $left, $block->getOperand($op->arg2))
);
$rightVar = $this->context->helper->loadValue(
JIT\JitNativeString::coerce($this->context, $right)
JIT\JitNativeString::coerce($this->context, $right, $block->getOperand($op->arg3))
);
$newStr = \PHPCompiler\ext\standard\JitStringConcat::concat(
$this->context,
Expand Down Expand Up @@ -9056,7 +9066,8 @@ private function compileBlockInternal(
case Variable::TYPE_NATIVE_LONG:
JIT\ValueEchoHelper::echoNativeLong(
$this->context,
$this->context->helper->loadValue($arg)
$this->context->helper->loadValue($arg),
$echoOp
);
break;
case Variable::TYPE_NATIVE_DOUBLE:
Expand Down Expand Up @@ -15661,11 +15672,16 @@ function () use ($dest, $newVal): void {
}

/** Allocate a fresh native string holding left . right (php-src string concat semantics). */
private function compileConcatIntoNewString(Variable $left, Variable $right): Variable
private function compileConcatIntoNewString(
Variable $left,
Variable $right,
?\PHPCfg\Operand $leftOp = null,
?\PHPCfg\Operand $rightOp = null
): Variable
{
$this->context->intrinsic->builder = $this->context->builder;
$left = JIT\JitNativeString::coerce($this->context, $left);
$right = JIT\JitNativeString::coerce($this->context, $right);
$left = JIT\JitNativeString::coerce($this->context, $left, $leftOp);
$right = JIT\JitNativeString::coerce($this->context, $right, $rightOp);
$leftVar = $this->context->helper->loadValue($left);
$rightVar = $this->context->helper->loadValue($right);
$map = $this->context->structFieldMap['__string__'];
Expand Down
15 changes: 15 additions & 0 deletions lib/JIT/IncDecResourceProvenance.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
* The analysis is deliberately conservative: anything it does not recognise — a call, a parameter,
* a property or array read — answers "unknown" and keeps the guard. Only the listed
* value-producing ops, which cannot yield a resource under any input, allow it to be dropped.
*
* Also used to skip resource-handle checks in int→string lowering (#23811).
*/
final class IncDecResourceProvenance
{
Expand Down Expand Up @@ -93,6 +95,19 @@ public static function cannotBeResource(?Operand $op): bool
return self::operandIsSafe($op, $seen, $budget);
}

/**
* Like {@see cannotBeResource()} but peels php-cfg {@see Operand\Temporary} wrappers first.
* Used for int→string lowering where concat operands are often temporaries (#23811).
*/
public static function cannotBeResourceForString(?Operand $op): bool
{
while ($op instanceof Operand\Temporary && $op->original instanceof Operand) {
$op = $op->original;
}

return self::cannotBeResource($op);
}

/**
* @param array<int, true> $seen
*/
Expand Down
5 changes: 3 additions & 2 deletions lib/JIT/JitNativeString.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace PHPCompiler\JIT;

use PHPCfg\Operand;
use PHPLLVM\Builder;
use PHPLLVM\Value;

Expand All @@ -26,7 +27,7 @@ public static function ensureInsertBlock(Context $context): void
$context->builder->positionAtEnd($resume);
}

public static function coerce(Context $context, Variable $var): Variable
public static function coerce(Context $context, Variable $var, ?Operand $sourceOperand = null): Variable
{
if (Variable::TYPE_STRING === $var->type) {
return $var;
Expand Down Expand Up @@ -80,7 +81,7 @@ public static function coerce(Context $context, Variable $var): Variable
$context,
Variable::TYPE_STRING,
Variable::KIND_VALUE,
JitResourceIdString::formatNativeLong($context, $value)
JitResourceIdString::formatNativeLong($context, $value, $sourceOperand)
);
case Variable::TYPE_NATIVE_DOUBLE:
// PG(precision) via VmZendDoubleString (#21963, Zend/zend_operators.c).
Expand Down
9 changes: 7 additions & 2 deletions lib/JIT/JitResourceIdString.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PHPCompiler\JIT;

use PHPCompiler\VM\VmResourceIdString;
use PHPCfg\Operand;
use PHPLLVM\Value;

/**
Expand All @@ -14,8 +15,12 @@
*/
final class JitResourceIdString
{
public static function formatNativeLong(Context $context, Value $longVal): Value
public static function formatNativeLong(
Context $context,
Value $longVal,
?Operand $sourceOperand = null
): Value
{
return VmResourceIdString::formatNativeLong($context, $longVal);
return VmResourceIdString::formatNativeLong($context, $longVal, $sourceOperand);
}
}
16 changes: 15 additions & 1 deletion lib/JIT/ValueEchoHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace PHPCompiler\JIT;

use PHPCompiler\JIT\Builtin\ValueEchoRuntime;
use PHPCompiler\JIT\IncDecResourceProvenance;
use PHPCfg\Operand;
use PHPCompiler\VM\ValueEchoSupport;
use PHPLLVM\Value;

Expand All @@ -34,12 +36,24 @@ public static function echoLiteral(Context $context, string $literal): void
/**
* Echo a native long, formatting stream/dir resources like Zend (ext/standard, #4740).
*/
public static function echoNativeLong(Context $context, Value $longVal): void
public static function echoNativeLong(
Context $context,
Value $longVal,
?Operand $sourceOperand = null
): void
{
Builtin\StringDir::ensureLinked($context);
$tag = 'enl'.(string) ++self::$seq;
$i64 = $context->getTypeFromString('int64');
$handle = $context->builder->zExt($longVal, $i64);
if (IncDecResourceProvenance::cannotBeResourceForString($sourceOperand)) {
$context->builder->call(
$context->lookupFunction('__phpc_ob_echo_ll'),
$handle
);

return;
}
$isRes = JitValueCompare::nativeLongIsResource($context, $handle);

$plainBlock = BasicBlockHelper::append($context, 'echo_native_long_plain_'.$tag);
Expand Down
22 changes: 21 additions & 1 deletion lib/VM/VmResourceIdString.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use PHPCompiler\JIT\BasicBlockHelper;
use PHPCompiler\JIT\Builtin\StringDir;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\IncDecResourceProvenance;
use PHPCfg\Operand;
use PHPCompiler\VM\ValueEchoSupport;
use PHPLLVM\Value;

Expand All @@ -18,13 +20,20 @@
*/
final class VmResourceIdString
{
public static function formatNativeLong(Context $context, Value $longVal): Value
public static function formatNativeLong(
Context $context,
Value $longVal,
?Operand $sourceOperand = null
): Value
{
StringDir::ensureLinked($context);
$i64 = $context->getTypeFromString('int64');
$handle = $longVal->typeOf() === $i64
? $longVal
: $context->builder->zExt($longVal, $i64);
if (IncDecResourceProvenance::cannotBeResourceForString($sourceOperand)) {
return self::snprintf($context, $handle, '%lld');
}
$isRes = VmValueCompare::nativeLongIsResource($context, $handle);

$tag = 'resid_'.(string) spl_object_id($context);
Expand Down Expand Up @@ -53,6 +62,17 @@ public static function formatNativeLong(Context $context, Value $longVal): Value
return $phi;
}

/** Boxed {@see __value__} TYPE_NATIVE_LONG — never a resource handle (#23811). */
public static function formatBoxedNativeLong(Context $context, Value $longVal): Value
{
$i64 = $context->getTypeFromString('int64');
$handle = $longVal->typeOf() === $i64
? $longVal
: $context->builder->zExt($longVal, $i64);

return self::snprintf($context, $handle, '%lld');
}

private static function snprintf(Context $context, Value $handle, string $format): Value
{
$sizeT = $context->getTypeFromString('size_t');
Expand Down
7 changes: 2 additions & 5 deletions test/differential/cases/g07_incdec_resource_provenance.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
<?php
// @differential-skip-aot: AOT renders int 2 as "Resource id #2" and mis-decrements (#23811)
// @differential-skip-aot: decrement-below-zero / post-echo truncation still broken (#23811 partial)
//
// This case is a VM guard for now. It FAILS under --aot, and that failure is real, not a
// mis-specified expectation: see #23811. It is skipped rather than deleted because the moment
// #23811 is fixed, removing the marker turns this back into an AOT guard for free. Do NOT read the
// skip as "this shape works under AOT".
// String interpolation with live handles is guarded by g07a_int_string_resource_collision.php.
//
// #23483: ++/-- guards every native long with __compiler_is_resource, because resource handles
// ARE native longs here and php-types has no resource type. IncDecResourceProvenance elides that
Expand Down
13 changes: 13 additions & 0 deletions test/differential/cases/g07a_int_string_resource_collision.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
// AOT guard for #23811 string collision — int 2 must not render as Resource id #2 while handles are open.
$fh = fopen('php://memory', 'r+');
$fh2 = fopen('php://memory', 'r+');
$a = 1;
++$a;
$b = 2;
++$b;
$c = 3;
++$c;
$d = 4;
--$d;
echo "$a $b $c $d\n";
13 changes: 13 additions & 0 deletions test/repro/aot_int_string_resource_collision.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
// Issue #23811: int-to-string must not consult resource registry for proven integers.
$fh = fopen('php://memory', 'r+');
$fh2 = fopen('php://memory', 'r+');
$a = 1;
++$a;
$b = 2;
++$b;
$c = 3;
++$c;
$d = 4;
--$d;
echo "$a $b $c $d\n";
27 changes: 27 additions & 0 deletions test/unit/IncDecResourceProvenanceStringTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPCompiler\JIT\IncDecResourceProvenance;
use PHPCompiler\VM\VmResourceIdString;
use PHPCfg\Operand;
use PHPUnit\Framework\TestCase;

/** Provenance + boxed long formatting for int→string (#23811). */
final class IncDecResourceProvenanceStringTest extends TestCase
{
public function testCannotBeResourceForStringAcceptsLiterals(): void
{
$lit = new Operand\Literal(2);
self::assertTrue(IncDecResourceProvenance::cannotBeResourceForString($lit));
}

public function testVmResourceIdStringExposesBoxedNativeLongFormatter(): void
{
$source = (string) file_get_contents(__DIR__.'/../../lib/VM/VmResourceIdString.php');
self::assertStringContainsString('formatBoxedNativeLong', $source);
self::assertStringContainsString('cannotBeResourceForString', $source);
}
}