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
33 changes: 28 additions & 5 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -10917,9 +10917,11 @@ private function initJitMethodCall(Block $block, Operand $receiverOp, string $me
}
}

$className = $receiverOp->type?->userType
?? ($this->context->scope->className !== '' ? $this->context->scope->className : 'object');
$declaringClassLc = strtolower($className);
$userType = $receiverOp->type?->userType;
$className = (is_string($userType) && '' !== ltrim($userType, '\\'))
? $userType
: ($this->context->scope->className !== '' ? $this->context->scope->className : 'object');
$declaringClassLc = strtolower(ltrim($className, '\\'));
$methodLc = strtolower($methodName);

if ('object' === $declaringClassLc) {
Expand All @@ -10944,6 +10946,25 @@ private function initJitMethodCall(Block $block, Operand $receiverOp, string $me
);
}
if (!$this->context->functionIsRegistered($proxyName)) {
if ('getmessage' === $methodLc && $this->context->functionIsRegistered('exception::getmessage')) {
$this->context->scope->toCall = $this->context->resolveFunctionProxy('exception::getmessage');
$this->context->scope->args = [$receiverVar];

return;
}
if ('object' === $declaringClassLc || '' === $declaringClassLc) {
$runtimeCandidates = $this->buildRuntimeInstanceMethodCandidatesByClassId($methodLc);
if ([] !== $runtimeCandidates) {
$this->context->scope->toCall = new JIT\Call\RuntimeIndirectInstanceMethodCall(
$receiverVar,
$methodLc,
$runtimeCandidates
);
$this->context->scope->args = [$receiverVar];

return;
}
}
if (JIT\MagicMethodDispatch::tryInitMagicCall(
$this->context,
$declaringClassLc,
Expand All @@ -10955,6 +10976,7 @@ private function initJitMethodCall(Block $block, Operand $receiverOp, string $me
throw new \LogicException("Call to undefined method {$className}::{$methodLc}()");
}
$receiverUserType = $receiverOp->type?->userType;
$normalizedReceiverUserType = is_string($receiverUserType) ? ltrim($receiverUserType, '\\') : null;
$staticProxy = $this->context->resolveFunctionProxy($proxyName);
// :object receivers use RuntimeIndirectInstanceMethodCall; MCJIT segfaults on
// ReflectionAttribute::newInstance() through that path (#4598).
Expand All @@ -10964,8 +10986,9 @@ private function initJitMethodCall(Block $block, Operand $receiverOp, string $me

return;
}
$needsRuntimeDispatch = null === $receiverUserType
|| 'object' === strtolower(ltrim((string) $receiverUserType, '\\'))
$needsRuntimeDispatch = null === $normalizedReceiverUserType
|| '' === $normalizedReceiverUserType
|| 'object' === strtolower($normalizedReceiverUserType)
|| $staticProxy instanceof JIT\Call\ExternalMethod;
if ($needsRuntimeDispatch) {
$runtimeCandidates = $this->buildRuntimeInstanceMethodCandidatesByClassId($methodLc);
Expand Down
11 changes: 3 additions & 8 deletions lib/JIT/Builtin/StringDirJit.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,11 @@ private static function ensureLibc(Context $context): void
$i8p = $context->getTypeFromString('int8*');
$i8pp = $context->getTypeFromString('int8**');
$i8ppp = $i8pp->pointerType(0);
$voidPtr = $context->getTypeFromString('void*');
$sizeT = $context->getTypeFromString('size_t');
$voidTy = $context->getTypeFromString('void');

// scandir/alphasort only — free/calloc/strlen come from LibcExtern (i8*).
foreach ([
['scandir', $i32, [$i8p, $i8ppp, $voidPtr, $voidPtr]],
['scandir', $i32, [$i8p, $i8ppp, $i8p, $i8p]],
['alphasort', $i32, [$i8pp, $i8pp]],
['free', $voidTy, [$voidPtr]],
['calloc', $voidPtr, [$sizeT, $sizeT]],
['strlen', $sizeT, [$i8p]],
] as [$name, $ret, $params]) {
self::ensureExternal($context, $name, $context->context->functionType($ret, false, ...$params));
}
Expand Down Expand Up @@ -412,7 +407,7 @@ private static function emitOpendir(Context $context, LlvmFunction $fn): void
$context->lookupFunction('scandir'),
$pathData,
$namelistSlot,
$voidPtr->constNull(),
$i8p->constNull(),
$context->bytePtr($alphasort)
);
$scanFailBlock = $fn->appendBasicBlock('scan_fail');
Expand Down
25 changes: 9 additions & 16 deletions lib/JIT/Builtin/StringFsGlobVecJit.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,22 +111,14 @@ private static function ensureLibc(Context $context): void
$i8p = $context->getTypeFromString('int8*');
$i8pp = $context->getTypeFromString('int8**');
$i8ppp = $i8pp->pointerType(0);
$voidPtr = $context->getTypeFromString('void*');
$sizeT = $context->getTypeFromString('size_t');
$voidTy = $context->getTypeFromString('void');

// glob/scandir only — malloc/free/memset/stat/strcmp come from LibcExtern (i8*).
foreach ([
['glob', $i32, [$i8p, $i32, $voidPtr, $voidPtr]],
['globfree', $voidTy, [$voidPtr]],
['scandir', $i32, [$i8p, $i8ppp, $voidPtr, $voidPtr]],
['glob', $i32, [$i8p, $i32, $i8p, $i8p]],
['globfree', $voidTy, [$i8p]],
['scandir', $i32, [$i8p, $i8ppp, $i8p, $i8p]],
['alphasort', $i32, [$i8pp, $i8pp]],
['stat', $i32, [$i8p, $voidPtr]],
['strcmp', $i32, [$i8p, $i8p]],
['malloc', $voidPtr, [$sizeT]],
['realloc', $voidPtr, [$voidPtr, $sizeT]],
['strdup', $i8p, [$i8p]],
['free', $voidTy, [$voidPtr]],
['memset', $voidPtr, [$voidPtr, $i32, $sizeT]],
] as [$name, $ret, $params]) {
self::ensureExternal($context, $name, $context->context->functionType($ret, false, ...$params));
}
Expand Down Expand Up @@ -324,7 +316,7 @@ private static function emitGlobVec(Context $context, LlvmFunction $fn): void
$context->lookupFunction('glob'),
$pat,
$flags,
$voidPtr->constNull(),
$i8p->constNull(),
$context->bytePtr($globBase)
);
$nomatch = $context->builder->icmp(Builder::INT_EQ, $rc, $i32->constInt(self::GLOB_NOMATCH, false));
Expand Down Expand Up @@ -499,20 +491,21 @@ private static function emitScandirVec(Context $context, LlvmFunction $fn): void

$isDesc = $context->builder->icmp(Builder::INT_EQ, $sortOrder, $i32->constInt(self::SCANDIR_SORT_DESCENDING, false));
$isNone = $context->builder->icmp(Builder::INT_EQ, $sortOrder, $i32->constInt(self::SCANDIR_SORT_NONE, false));
$cmpSlot = BasicBlockHelper::entryAlloca($context, $voidPtr);
$cmpSlot = BasicBlockHelper::entryAlloca($context, $i8p);
$pickCmp = $fn->appendBasicBlock('scan_pick_cmp');
$afterCmp = $fn->appendBasicBlock('scan_after_cmp');
$context->builder->branch($pickCmp);

$context->builder->positionAtEnd($pickCmp);
$descCmp = $context->lookupFunction('__phpc_scandir_desc_cmp');
$alphaCmp = $context->lookupFunction('alphasort');
$nullCmp = $i8p->constNull();
$cmp = $context->builder->select(
$isDesc,
$context->bytePtr($descCmp),
$context->builder->select(
$isNone,
$voidPtr->constNull(),
$nullCmp,
$context->bytePtr($alphaCmp)
)
);
Expand All @@ -524,7 +517,7 @@ private static function emitScandirVec(Context $context, LlvmFunction $fn): void
$context->lookupFunction('scandir'),
$dir,
$namelistSlot,
$voidPtr->constNull(),
$nullCmp,
$context->builder->load($cmpSlot)
);
$scanFail = $context->builder->icmp(Builder::INT_SLT, $n, $zero32);
Expand Down
30 changes: 30 additions & 0 deletions lib/JIT/Call/ExceptionGetMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT\Call;

use PHPCompiler\JIT\Builtin\ReflectionSetup;
use PHPCompiler\JIT\Call;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitValueBox;
use PHPCompiler\JIT\Variable;
use PHPLLVM\Value;

/** Throwable/Exception::getMessage() — read message property (#4531, #7461). */
final class ExceptionGetMessage implements Call
{
public function call(Context $context, Variable ...$args): Value
{
if ([] === $args) {
throw new \LogicException('getMessage() requires an object receiver');
}
$obj = ReflectionSetup::loadObjectFromArg($context, $args[0]);
$messageVar = $context->type->object->propertyFetch($obj, 'Exception', 'message');

return $context->builder->call(
$context->lookupFunction('__value__readString'),
JitValueBox::valuePtrFromVariable($context, $messageVar)
);
}
}
1 change: 1 addition & 0 deletions lib/JIT/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ private function defineBuiltins(int $loadType): void {
$this->functionProxies['reflectionmethod::getattributes'] = new Call\ReflectionMethodGetAttributes();
$this->functionProxies['reflectionattribute::getname'] = new Call\ReflectionAttributeGetName();
$this->functionProxies['reflectionattribute::newinstance'] = new Call\ReflectionAttributeNewInstance();
$this->functionProxies['exception::getmessage'] = new Call\ExceptionGetMessage();

FiberHelper::registerJitMethods($this);
GeneratorHelper::registerJitMethods($this);
Expand Down
8 changes: 0 additions & 8 deletions test/compliance/JITTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -515,14 +515,6 @@ public static function providePHPTests(): \Generator
if (str_contains($name, 'byref_method_static_local')) {
continue;
}
// list() from null/false/int: VM + LLVM verify (#4325); MCJIT execute segfault until list unpack branch stable.
if (str_contains($name, 'list_destructure_null')) {
continue;
}
// list() from string: VM + LLVM verify (#4308); MCJIT execute segfault until list unpack branch stable.
if (str_contains($name, 'list_destructure_string')) {
continue;
}
// Pipe operator (|>): LLVM verify green in PipeOperatorJitCompileTest (#4783); MCJIT execute in PipeOperatorJitExecuteTest (#98).
if (str_contains($name, 'pipe_first_class')) {
continue;
Expand Down
13 changes: 13 additions & 0 deletions test/repro/list_destruct_string_typeerror.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
try {
[$a] = 'ab';
echo "no-exception\n";
} catch (TypeError $e) {
echo 'TypeError: ', $e->getMessage(), "\n";
}
try {
[$b, $c] = 'xy';
echo "no-exception\n";
} catch (TypeError $e) {
echo 'TypeError: ', $e->getMessage(), "\n";
}
76 changes: 59 additions & 17 deletions test/unit/ListDestructNullJitCompileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../LlvmToolchain.php';
require_once __DIR__.'/../LlvmToolchain.php';

/**
* JIT lowering for list destructuring from non-array RHS (#4325).
* JIT compile-only verify for guarded list destructuring from non-array RHS (#4325, #4531).
*
* Uses bin/jit.php -l in a child process (issue #98 — no in-process LLVM preload).
*
* @group llvm
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
final class ListDestructNullJitCompileTest extends TestCase
{
Expand All @@ -22,31 +22,73 @@ final class ListDestructNullJitCompileTest extends TestCase
protected function setUp(): void
{
$this->repoRoot = dirname(__DIR__, 2);
if (!LlvmToolchain::isReady($this->repoRoot)) {
if (!LlvmToolchain::hasLibrary($this->repoRoot)) {
$reason = LlvmToolchain::readyFailureReason() ?? 'LLVM 9 toolchain not available';
$this->markTestSkipped($reason . ' — list destruct null JIT compile test needs LLVM (#4325)');
$this->markTestSkipped($reason.' — list destruct null JIT compile test needs LLVM (#4325)');
}
}

public function testListDestructNullModuleVerify(): void
{
$code = <<<'PHP'
$this->assertJitCompileOnly(<<<'PHP'
<?php
[$a, $b] = null;
echo "a=", var_export($a, true), " b=", var_export($b, true), "\n";
list($x) = false;
echo "x=", var_export($x, true), "\n";
[$y] = 0;
echo "y=", var_export($y, true), "\n";
PHP;
$runtime = new Runtime();
$block = $runtime->parseAndCompile($code, 'list_destructure_null.php');
$runtime->jitCompileBlock($block);

$context = $runtime->loadJitContext();
$verify = new \ReflectionMethod($context, 'compileCommon');
$verify->setAccessible(true);
$verify->invoke($context);
self::addToAssertionCount(1);
PHP
);
}

private function assertJitCompileOnly(string $code): void
{
$dir = $this->repoRoot.'/var';
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
$this->fail('Could not create var/ for JIT compile probe');
}
$source = $dir.'/list-destruct-null-compile-'.getmypid().'-'.md5($code).'.php';
file_put_contents($source, $code);

$llvmDir = LlvmToolchain::resolveDir($this->repoRoot);
$this->assertNotNull($llvmDir);

$bash = <<<'BASH'
set -euo pipefail
ROOT=%s
SOURCE=%s
source "$ROOT/script/php-env.sh"
export PHP_COMPILER_LLVM_PATH=%s
export LD_LIBRARY_PATH="%s${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
unset PHP_COMPILER_SKIP_LLVM_PRELOAD
"$PHP_BIN" "${PHP_OPTS[@]}" "$ROOT/bin/jit.php" -l "$SOURCE"
BASH;

$command = sprintf(
$bash,
escapeshellarg($this->repoRoot),
escapeshellarg($source),
escapeshellarg($llvmDir),
$llvmDir
);

$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open(['bash', '-lc', $command], $descriptorSpec, $pipes, $this->repoRoot);
$this->assertIsResource($proc);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($proc);

@unlink($source);

$this->assertSame(0, $exit, trim((string) $stderr."\n".(string) $stdout));
}
}
Loading