feat(compiler): add static type checking for composite types

- Implement static type validation for union, intersection, and nullable types
- Add composite type assignment checking for parameters, return values, and property assignments
- Introduce dynamic value wrapper tests to verify type checking behavior
- Enhance error messaging for composite type mismatches
- Update documentation to reflect improved static type analysis capabilities
- Add proper restoration of compiler state after generator conversion errors
- Improve parallel compilation process management and error handling
- Add metadata caching for object files with PHP ABI compatibility checks
pull/16/head
韩天峰 2 months ago
parent 35795be037
commit 9f6a13d5f7
  1. 2
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 7
      phpunit/code/generator-conversion-error.php
  3. 16
      phpunit/src/CompilerBaseApiTest.php
  4. 33
      phpunit/src/Generator/FiberGeneratorTest.php
  5. 107
      phpunit/src/ParallelCompileTest.php
  6. 160
      src/CompilerBase.php
  7. 15
      src/Generator/FiberGenerator.php
  8. 252
      src/Translator.php
  9. 6
      tests/aot/type_decl/intersection-param-check.phpt
  10. 12
      tests/aot/type_decl/union-param-check.phpt
  11. 4
      tests/aot/type_hits/009.phpt
  12. 6
      tests/aot/type_hits/010.phpt

@ -19,7 +19,7 @@
- `__construct()` 不允许返回值。
- 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。
- 不支持引用可变参数 `&...$args`
- 联合类型、交叉类型、`nullable` 类型在静态编译阶段按 `mixed/any` 处理,只保留运行时 type check。
- 联合类型、交叉类型、`nullable` 类型仍以 `mixed/any` 作为 C++ 表示,但静态阶段会利用已知表达式类型提前拒绝确定不兼容的参数、返回值和属性赋值;动态值仍保留运行时 type check。
- 局部变量类型一旦被静态推断为具体 native 类型,不支持在同一作用域内重新赋值为不兼容类型。
- attribute 参数不支持数组值和 `new` 表达式。

@ -0,0 +1,7 @@
<?php
function broken_generator(): iterable
{
$name = 'value';
yield $$name;
}

@ -115,6 +115,22 @@ class CompilerBaseApiTest extends TestCase
$this->assertSame(CompilerBase::BUILD_MODE_BIN, $this->compiler->getBuildMode());
}
public function testMiscObjectCacheIsInvalidatedWhenCompileOptionsChange(): void
{
$source = $this->testDir . '/typephp_runtime.cc';
$object = $this->testDir . '/typephp_runtime.o';
file_put_contents($source, "int typephp_runtime_test = 1;\n");
file_put_contents($object, 'object');
touch($source, time() - 10);
touch($object, time() + 10);
$this->invokeMethod('writeMiscObjectCacheMetadata', $source, $object);
$this->assertTrue($this->compiler->hasMiscObjectFileCache($source));
$this->setPropertyValue('cxxFlags', '-fno-rtti');
$this->assertFalse($this->compiler->hasMiscObjectFileCache($source));
}
// ========================================================================
// getTypeFromZendType
// ========================================================================

@ -0,0 +1,33 @@
<?php
namespace TypePhp\Tests\Generator;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
class FiberGeneratorTest extends TestCase
{
public function testCompilerStateIsRestoredAfterGeneratorConversionError(): void
{
$compiler = CompilerTest::create(ROOT_PATH);
$file = __DIR__ . '/../../code/generator-conversion-error.php';
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
try {
$compiler->convertFile($file);
$this->fail('The variable-variable expression should fail conversion');
} catch (TestError $e) {
$this->assertStringContainsString('The `$$` syntax is not supported', $e->getMessage());
}
$reflection = new \ReflectionClass($compiler);
$this->assertFalse($reflection->getProperty('inGeneratorBody')->getValue($compiler));
$this->assertSame(0, $reflection->getProperty('indentLevel')->getValue($compiler));
$this->assertSame('', $reflection->getProperty('function')->getValue($compiler));
$this->assertNull($reflection->getProperty('functionDef')->getValue($compiler));
$context = $reflection->getProperty('context')->getValue($compiler);
$this->assertFalse($context->inClosure);
}
}

@ -0,0 +1,107 @@
<?php
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
class ParallelCompileTest extends TestCase
{
public function testWaitRetriesWhenInterrupted(): void
{
$compiler = new ScriptedWaitCompiler([
[-1, 0, PCNTL_EINTR],
[123, 0, 0],
]);
$this->assertSame([123, 0], $compiler->waitForTest());
$this->assertSame(2, $compiler->getWaitCallCount());
}
public function testWaitFailureOtherThanInterruptionThrows(): void
{
$compiler = new ScriptedWaitCompiler([
[-1, 0, PCNTL_ECHILD],
]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Failed to wait for compiler process');
$compiler->waitForTest();
}
public function testSignaledChildIsNotSuccessful(): void
{
$compiler = new ScriptedWaitCompiler([]);
$this->assertTrue($compiler->statusSucceeded(0));
$this->assertFalse($compiler->statusSucceeded(1 << 8));
$this->assertFalse($compiler->statusSucceeded(SIGTERM));
}
public function testForkFailureStillReapsRunningCompilerProcesses(): void
{
$compiler = new ScriptedWaitCompiler(
[[101, 0, 0]],
[101, -1]
);
try {
$compiler->compileInParallelForTest(['first.cc', 'second.cc', 'third.cc'], 2);
$this->fail('The fork failure should fail the parallel compilation');
} catch (\Exception $e) {
$this->assertStringContainsString('second.cc', $e->getMessage());
$this->assertStringContainsString('third.cc', $e->getMessage());
}
$this->assertSame(1, $compiler->getWaitCallCount());
}
}
class ScriptedWaitCompiler extends CompilerTest
{
private int $waitCallCount = 0;
private int $lastWaitError = 0;
public function __construct(private array $waitResults, private array $forkResults = [])
{
parent::__construct(ROOT_PATH);
$this->noProgress = true;
}
protected function pcntlFork(): int
{
return array_shift($this->forkResults);
}
protected function pcntlWait(?int &$status): int
{
$this->waitCallCount++;
[$pid, $status, $this->lastWaitError] = array_shift($this->waitResults);
return $pid;
}
protected function pcntlLastError(): int
{
return $this->lastWaitError;
}
public function waitForTest(): array
{
return $this->waitForCompileChild();
}
public function statusSucceeded(int $status): bool
{
return $this->compileChildSucceeded($status);
}
public function getWaitCallCount(): int
{
return $this->waitCallCount;
}
public function compileInParallelForTest(array $sourceFiles, int $jobs): array
{
return $this->compileWithPcntl($sourceFiles, $jobs);
}
}

@ -1977,6 +1977,15 @@ class CompilerBase implements PropertyAccessContext
if ($this->isCurrentConstructor() && !$this->context->inClosure) {
$this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value');
}
if (!$this->context->inClosure && !empty($this->functionDef->returnTypeCheck)) {
$this->checkCompositeTypeAssignment(
$v,
$this->functionDef->returnTypeCheck,
$this->functionDef->returnTypeStr,
$v->expr,
'return value'
);
}
$expr = $this->parseExprAsValue($v->expr);
$returnType = $this->getReturnType();
@ -5125,6 +5134,16 @@ class CompilerBase implements PropertyAccessContext
$type = $this->detectTypeOfExpr($arg->value);
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
if (!empty($argInfo->typeCheck)) {
$this->checkCompositeTypeAssignment(
$arg,
$argInfo->typeCheck,
$argInfo->typeStr,
$arg->value,
'argument `$' . ($argInfo->phpName ?: $this->unescapeVarName($argInfo->name)) . '`'
);
}
if ($argInfo->byRef) {
if ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
@ -5324,6 +5343,18 @@ class CompilerBase implements PropertyAccessContext
}
$rightType = $this->detectTypeOfExpr($right);
if (!empty($def->typeCheck) && $this->checkCompositeTypeAssignment(
$left,
$def->typeCheck,
$def->typeStr,
$right,
'property assignment'
) && $rightType !== self::TYPE_VAR) {
// A statically known member of the composite type needs no
// Variant runtime guard on this property write.
return $rightExpr;
}
if ($rightType !== self::TYPE_VAR && $this->canAssignStaticTypeToObjectProperty($def, $rightType)) {
return $rightExpr;
}
@ -7584,6 +7615,135 @@ class CompilerBase implements PropertyAccessContext
$this->fatalError($left, "Cannot re-assign $varName from `{$fromType}` to `{$toType}`");
}
/**
* Check a value against a composite PHP type when the value's static type
* is precise enough to prove a mismatch. Composite declarations still use
* Variant in generated C++, so unknown values must be left to the runtime
* type check emitted from the same descriptor.
*
* The outer descriptor list is a union (OR); an allOf entry represents an
* intersection (AND). Nullable is represented by an isNull union member.
*/
protected function checkCompositeTypeAssignment(
NodeAbstract $errorNode,
array $typeCheck,
string $typeStr,
NodeAbstract $value,
string $context
): bool {
if ($this->compositeTypeMayMatch($value, $typeCheck)) {
return true;
}
$valueType = $this->staticTypeNameOfExpr($value);
$this->fatalError($errorNode, "Cannot assign {$valueType} to {$context} of type `{$typeStr}`");
}
protected function compositeTypeMayMatch(NodeAbstract $value, array $clauses): bool
{
// TYPE_VAR means that the expression is dynamic or its result cannot
// be represented by the current scalar type system. Do not reject it.
if ($this->detectTypeOfExpr($value) === self::TYPE_VAR && !$this->isNullExpr($value)) {
return true;
}
foreach ($clauses as $clause) {
if ($this->compositeTypeClauseMayMatch($value, $clause)) {
return true;
}
}
return false;
}
protected function compositeTypeClauseMayMatch(NodeAbstract $value, array $clause): bool
{
if (($clause['kind'] ?? '') === 'allOf') {
foreach ($clause['types'] ?? [] as $entry) {
if (!$this->compositeTypeEntryMayMatch($value, $entry)) {
return false;
}
}
return true;
}
return $this->compositeTypeEntryMayMatch($value, $clause);
}
protected function compositeTypeEntryMayMatch(NodeAbstract $value, array $entry): bool
{
$kind = $entry['kind'] ?? '';
if ($kind === 'isNull') {
return $this->isNullExpr($value);
}
$type = $this->detectTypeOfExpr($value);
return match ($kind) {
'isInt' => $type === self::TYPE_INT,
'isFloat' => $type === self::TYPE_FLOAT,
'isBool' => $type === self::TYPE_BOOL,
'isString' => $type === self::TYPE_STR,
'isArray' => $type === self::TYPE_ARRAY,
'isObject' => $type === self::TYPE_OBJECT,
'isTrue', 'isFalse' => $type === self::TYPE_BOOL,
'isResource' => $type === self::TYPE_RESOURCE,
// These checks depend on runtime callable/traversable state unless
// a future value lattice adds those properties.
'callable', 'iterable' => true,
'instanceof' => $this->compositeObjectEntryMayMatch($value, $entry),
default => true,
};
}
protected function compositeObjectEntryMayMatch(NodeAbstract $value, array $entry): bool
{
if ($this->detectTypeOfExpr($value) !== self::TYPE_OBJECT) {
return false;
}
$class = $this->detectDeclaredClassOfExpr($value);
if ($class === '') {
return true;
}
$expected = $entry['class'] ?? '';
// If the expected class/interface is outside the AOT class graph,
// static analysis cannot prove incompatibility. Keep the runtime
// instanceof check (this is common for extension-provided interfaces).
if ($expected === ''
|| (!$this->hasClass($expected)
&& !$this->hasInterface($expected)
&& !$this->isInternalClass($expected)
&& !$this->isInternalInterface($expected))) {
return true;
}
return $expected === 'static'
? true
: $this->isObjectClassStaticallyAssignableTo($class, $expected);
}
protected function isNullExpr(NodeAbstract $expr): bool
{
return $expr instanceof Expr\ConstFetch
&& strcasecmp($this->parseIdentifier($expr->name), 'null') === 0;
}
protected function staticTypeNameOfExpr(NodeAbstract $expr): string
{
if ($this->isNullExpr($expr)) {
return 'null';
}
$type = $this->detectTypeOfExpr($expr);
return match ($type) {
self::TYPE_INT => 'int',
self::TYPE_FLOAT => 'float',
self::TYPE_BOOL => 'bool',
self::TYPE_STR => 'string',
self::TYPE_ARRAY => 'array',
self::TYPE_OBJECT => 'object',
default => 'mixed',
};
}
protected function mustNoCall(NodeAbstract $node): void
{
$nodeFinder = new NodeFinder();

@ -164,6 +164,21 @@ trait FiberGenerator
}
protected function genFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string
{
$entryContext = $this->context;
$entryIndent = $this->indentLevel;
$entryInGeneratorBody = $this->inGeneratorBody;
try {
return $this->doGenFiberGeneratorFunction($v, $functionDef, $nativeName);
} finally {
$this->context = $entryContext;
$this->indentLevel = $entryIndent;
$this->inGeneratorBody = $entryInGeneratorBody;
}
}
private function doGenFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string
{
$functionDeclCode = self::TYPE_VAR . ' ' . self::PREFIX . $nativeName . '(';
if ($this->class) {

@ -1204,7 +1204,7 @@ CODE;
/**
* 检查 phpx/src/misc/ 下的源文件是否已有有效缓存,始终生效(除非指定 --force)。
* 仅检查 .o 文件是否比源文件和 phpx 头文件更新
* 缓存必须匹配编译命令和 PHP ABI,且 .o 文件必须不早于源文件和 phpx 头文件
*/
public function hasMiscObjectFileCache(string $cppFile): bool
{
@ -1217,6 +1217,16 @@ CODE;
return false;
}
$metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile);
if (!is_file($metadataFile)) {
return false;
}
$cachedKey = file_get_contents($metadataFile);
if ($cachedKey === false || trim($cachedKey) !== $this->getMiscObjectCacheKey($cppFile, $objectFile)) {
return false;
}
$objectMtime = filemtime($objectFile);
if ($objectMtime <= filemtime($cppFile)) {
return false;
@ -1242,6 +1252,41 @@ CODE;
return true;
}
protected function getMiscObjectCacheMetadataFile(string $objectFile): string
{
return $objectFile . '.typephp-cache';
}
protected function getMiscObjectCacheKey(string $sourceFile, string $objectFile): string
{
$abi = [
'php_version_id' => PHP_VERSION_ID,
'php_api_version' => defined('PHP_API_VERSION') ? PHP_API_VERSION : null,
'zend_module_api' => defined('ZEND_MODULE_API_NO') ? ZEND_MODULE_API_NO : null,
'php_zts' => defined('PHP_ZTS') ? PHP_ZTS : null,
'php_debug' => defined('PHP_DEBUG') ? PHP_DEBUG : null,
'integer_size' => PHP_INT_SIZE,
];
return hash('sha256', $this->buildCompileFileCommand($sourceFile, $objectFile) . "\0" . serialize($abi));
}
protected function writeMiscObjectCacheMetadata(string $sourceFile, string $objectFile): void
{
$metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile);
if (file_put_contents($metadataFile, $this->getMiscObjectCacheKey($sourceFile, $objectFile) . PHP_EOL) === false) {
throw new \RuntimeException('Cannot write misc object cache metadata: ' . $metadataFile);
}
}
protected function invalidateMiscObjectCache(string $objectFile): void
{
$metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile);
if (is_file($metadataFile)) {
unlink($metadataFile);
}
}
public function isPhpxMiscFile(string $cppFile): bool
{
$miscDir = $this->getPhpxDir() . '/src/misc/';
@ -1293,35 +1338,12 @@ CODE;
return;
}
// 检测文件语言类型
$language = $this->getLanguageFromExtension($cppFile);
// 使用 Backend 层构建编译命令
if ($language === null) {
// C++ 文件:使用标准的编译命令构建
$cmd = $this->getCompilerBackend()->buildCompileCommand(
$cppFile,
$objectFile,
$this->getCompileCommandOptions()
);
} elseif ($language === 'c') {
// C 文件:使用 buildCCompileCommand,后端自动添加 -x c 或 /TC
$cmd = $this->getCompilerBackend()->buildCCompileCommand(
$cppFile,
$objectFile,
$this->getCCompileCommandOptions()
);
} else {
// 其他原生源文件(assembler, objective-c, objective-c++)
// 使用 buildNativeCompileCommand,传入语言类型以添加 -x 标志
$cmd = $this->getCompilerBackend()->buildNativeCompileCommand(
$cppFile,
$objectFile,
$this->getNativeCompileCommandOptions($language),
$language
);
$isMiscFile = $this->isPhpxMiscFile($cppFile);
if ($isMiscFile) {
$this->invalidateMiscObjectCache($objectFile);
}
$cmd = $this->buildCompileFileCommand($cppFile, $objectFile);
if (!$parallel) {
$this->climate->comment($cmd);
}
@ -1339,6 +1361,36 @@ CODE;
}
$this->error('compile failed: ' . $cppFile);
}
if ($isMiscFile) {
$this->writeMiscObjectCacheMetadata($cppFile, $objectFile);
}
}
protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string
{
$language = $this->getLanguageFromExtension($sourceFile);
if ($language === null) {
return $this->getCompilerBackend()->buildCompileCommand(
$sourceFile,
$objectFile,
$this->getCompileCommandOptions()
);
}
if ($language === 'c') {
return $this->getCompilerBackend()->buildCCompileCommand(
$sourceFile,
$objectFile,
$this->getCCompileCommandOptions()
);
}
return $this->getCompilerBackend()->buildNativeCompileCommand(
$sourceFile,
$objectFile,
$this->getNativeCompileCommandOptions($language),
$language
);
}
public function compile(array $sourceFiles): array
@ -1416,6 +1468,53 @@ CODE;
/**
* Unix/Linux/macOS 平台并行编译(使用 pcntl)
*/
protected function pcntlWait(?int &$status): int
{
return pcntl_wait($status);
}
protected function pcntlFork(): int
{
return pcntl_fork();
}
protected function pcntlLastError(): int
{
return pcntl_get_last_error();
}
protected function waitForCompileChild(): array
{
do {
$status = null;
$pid = $this->pcntlWait($status);
$error = $pid === -1 ? $this->pcntlLastError() : 0;
} while ($pid === -1 && defined('PCNTL_EINTR') && $error === PCNTL_EINTR);
if ($pid === -1) {
$message = function_exists('pcntl_strerror') ? pcntl_strerror($error) : 'error ' . $error;
throw new \RuntimeException('Failed to wait for compiler process: ' . $message);
}
return [$pid, (int) $status];
}
protected function compileChildSucceeded(int $status): bool
{
return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0;
}
protected function getCompileChildFailureReason(int $status): string
{
if (pcntl_wifsignaled($status)) {
return 'terminated by signal ' . pcntl_wtermsig($status);
}
if (pcntl_wifexited($status)) {
return 'exited with status ' . pcntl_wexitstatus($status);
}
return 'terminated abnormally';
}
protected function compileWithPcntl(array $sourceFiles, int $job): array
{
// 检查 pcntl 扩展是否可用
@ -1448,9 +1547,16 @@ CODE;
$cppFile = array_shift($fileQueue);
$objectFile = $this->getObjectFile($cppFile);
$pid = pcntl_fork();
$pid = $this->pcntlFork();
if ($pid == -1) {
throw new \Exception('Failed to fork process');
$failedFiles[] = $cppFile;
foreach ($fileQueue as $queuedFile) {
$failedFiles[] = $queuedFile;
}
$compiledCount += count($fileQueue) + 1;
$fileQueue = [];
$this->climate->red('Failed to fork compiler process; no additional files will be scheduled');
break;
}
if ($pid === 0) {
// 子进程:执行编译
@ -1473,54 +1579,54 @@ CODE;
// 等待任意一个子进程完成
if ($runningProcesses > 0) {
$status = null;
$pid = pcntl_wait($status);
if ($pid > 0) {
$processInfo = $processPipes[$pid] ?? null;
unset($processPipes[$pid]);
$runningProcesses--;
$exitCode = pcntl_wexitstatus($status);
if ($exitCode !== 0) {
$failedFile = $processInfo['file'] ?? 'unknown';
$failedFiles[] = $failedFile;
echo PHP_EOL;
$this->climate->red("Compilation failed: {$failedFile}");
echo PHP_EOL;
} else {
if ($processInfo) {
$objectFiles[] = $processInfo['object'];
}
}
$compiledCount++;
if ($this->noProgress) {
$percent = intval($compiledCount / $totalFiles * 100);
$file = $processInfo['file'] ?? 'unknown';
$fileShorted = $this->removeCommonPrefix($this->buildDir, $file);
$this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}");
} else {
$progress->renderInPlace($compiledCount, $totalFiles, 'Compiling');
}
}
}
}
// 确保所有子进程都已结束
while ($runningProcesses > 0) {
$status = null;
$pid = pcntl_wait($status);
if ($pid > 0) {
[$pid, $status] = $this->waitForCompileChild();
$processInfo = $processPipes[$pid] ?? null;
unset($processPipes[$pid]);
$runningProcesses--;
if (!$this->compileChildSucceeded($status)) {
$failedFile = $processInfo['file'] ?? 'unknown';
$failedFiles[] = $failedFile;
echo PHP_EOL;
$reason = $this->getCompileChildFailureReason($status);
$this->climate->red("Compilation failed: {$failedFile} ({$reason})");
echo PHP_EOL;
} elseif ($processInfo) {
$objectFiles[] = $processInfo['object'];
}
$compiledCount++;
if ($this->noProgress && $processInfo) {
if ($this->noProgress) {
$percent = intval($compiledCount / $totalFiles * 100);
$this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}");
$file = $processInfo['file'] ?? 'unknown';
$fileShorted = $this->removeCommonPrefix($this->buildDir, $file);
$this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}");
} else {
$progress->renderInPlace($compiledCount, $totalFiles, 'Compiling');
}
}
}
// 确保所有子进程都已结束
while ($runningProcesses > 0) {
[$pid, $status] = $this->waitForCompileChild();
$processInfo = $processPipes[$pid] ?? null;
unset($processPipes[$pid]);
$runningProcesses--;
$compiledCount++;
if (!$this->compileChildSucceeded($status)) {
$failedFile = $processInfo['file'] ?? 'unknown';
$failedFiles[] = $failedFile;
$reason = $this->getCompileChildFailureReason($status);
$this->climate->red("Compilation failed: {$failedFile} ({$reason})");
} elseif ($processInfo) {
$objectFiles[] = $processInfo['object'];
}
if ($this->noProgress && $processInfo) {
$percent = intval($compiledCount / $totalFiles * 100);
$this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}");
}
}
echo PHP_EOL;
if (!empty($failedFiles)) {
@ -3422,9 +3528,11 @@ CODE;
}
if ($this->functionDef->generator) {
$code = $this->genFiberGeneratorFunction($v, $this->functionDef, $name);
$this->resetFunction();
return $code;
try {
return $this->genFiberGeneratorFunction($v, $this->functionDef, $name);
} finally {
$this->resetFunction();
}
}
// Build SSA/e-SSA analysis for this function

@ -13,13 +13,17 @@ function expect_both(IA&IB $value): void {
var_dump(get_class($value));
}
function dynamic_value(mixed $value): mixed {
return $value;
}
function main() {
expect_both(new Both());
$errors = [];
try {
expect_both(new OnlyA());
expect_both(dynamic_value(new OnlyA()));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}

@ -27,6 +27,10 @@ function expect_bool_or_array(bool|array $x): void {
var_dump($x);
}
function dynamic_value(mixed $value): mixed {
return $value;
}
function main() {
// Valid calls - should pass
expect_int_or_string(42);
@ -47,25 +51,25 @@ function main() {
$errors = [];
try {
expect_int_or_string(3.14);
expect_int_or_string(dynamic_value(3.14));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_int_or_string([]);
expect_int_or_string(dynamic_value([]));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_nullable_int("hello");
expect_nullable_int(dynamic_value("hello"));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_bool_or_array(42);
expect_bool_or_array(dynamic_value(42));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}

@ -2,6 +2,8 @@
type hits: instance property type check message includes class name
--FILE--
<?php
function dynamic_value(mixed $value): mixed { return $value; }
class TypeHitPropertyMessage
{
public int|string $union;
@ -9,7 +11,7 @@ class TypeHitPropertyMessage
public function setInvalid(): void
{
try {
$this->union = null;
$this->union = dynamic_value(null);
} catch (TypeError $e) {
var_dump($e->getMessage());
}

@ -4,6 +4,8 @@ type hits: property coalesce assignment uses runtime type check
USE_ZEND_ALLOC=0
--FILE--
<?php
function dynamic_value(mixed $value): mixed { return $value; }
class TypeHitCoalesceProperty
{
public int|string $union;
@ -11,13 +13,13 @@ class TypeHitCoalesceProperty
public function run(): void
{
try {
$this->union ??= null;
$this->union ??= dynamic_value(null);
} catch (TypeError $e) {
var_dump($e->getMessage());
}
$this->union = "ok";
$this->union ??= null;
$this->union ??= dynamic_value(null);
var_dump($this->union);
}
}

Loading…
Cancel
Save