feat(compiler): implement unified any() function handling and enhanced composite type checking

- Move any() function handling from assignment-specific paths to general function call entry point
- Add comprehensive composite type relation analysis with INT/COMPOSITE_TYPE_MATCH/COMPOSITE_TYPE_UNKNOWN states
- Implement static type checking for closure and arrow function return values
- Add support for int to float coercion in composite type contexts
- Enhance type checking for property assignments with composite types
- Update documentation to reflect unified any() function behavior across all expression positions
- Add extensive test coverage for composite type edge cases and error scenarios
- Remove redundant dynamic_value helper functions from test files
- Implement proper null
pull/17/head
韩天峰 2 months ago
parent 9f6a13d5f7
commit b6cc597725
  1. 12
      docs/COMPILE_TIME_FUNCTIONS.md
  2. 6
      phpunit/code/composite-arrow-return-mismatch.php
  3. 8
      phpunit/code/composite-closure-return-mismatch.php
  4. 6
      phpunit/code/composite-empty-return-mismatch.php
  5. 16
      phpunit/code/composite-external-actual-unknown.php
  6. 11
      phpunit/code/composite-true-false-mismatch.php
  7. 41
      phpunit/src/CompositeStaticTypeTest.php
  8. 202
      src/CompilerBase.php
  9. 9
      src/Generator/ClosureGenerator.php
  10. 62
      src/Generator/TypeCheckGenerator.php
  11. 16
      src/Parser/AssignOpTrait.php
  12. 32
      tests/aot/basic/any-expression-positions.phpt
  13. 2
      tests/aot/generator/union-signatures.phpt
  14. 4
      tests/aot/object_property/private-prop-001.phpt
  15. 5
      tests/aot/place-holder/003.phpt
  16. 60
      tests/aot/type_decl/composite-static-edge-cases.phpt
  17. 6
      tests/aot/type_decl/intersection-param-check.phpt
  18. 12
      tests/aot/type_decl/union-param-check.phpt
  19. 4
      tests/aot/type_decl/variadic-union-param-check.phpt
  20. 3
      tests/aot/type_hits/009.phpt
  21. 5
      tests/aot/type_hits/010.phpt

@ -8,7 +8,7 @@
| 名称 | 参数 | 作用 | 当前主要处理位置 |
| --- | --- | --- | --- |
| `any($value)` | 1 个 | 将表达式降级为 `mixed/any`,阻止继续按静态 native/object 类型处理。 | 赋值右值路径中特判。 |
| `any($value)` | 1 个 | 将表达式降级为 `mixed/any`,阻止继续按静态 native/object 类型处理。 | 通用函数调用表达式入口。 |
| `refval($target)` | 1 个 | 显式把变量、数组元素或对象属性作为引用传给动态调用或无法静态识别引用参数的调用。 | 参数解析、动态调用、SSA/优化器引用逃逸分析。 |
| `objval($value, ClassName::class 或 'ClassName')` | 2 个 | 告诉编译器 `$value` 是指定类对象,并生成 `php::toObject(..., target_ce)` 运行时兜底检查。 | 函数调用解析、对象类型推导。 |
@ -16,7 +16,7 @@
- `refval()` 只接受变量、数组元素或对象属性。
- `objval()` 第二个参数必须是编译期可解析的类名字符串或 `ClassName::class`
- `any()` 语义上应是任意表达式位置可用的编译期标记;当前实现仍有路径差异,后续应统一到表达式解析入口,而不是只在部分赋值路径中处理
- `any()` 可在任意表达式位置使用,编译时直接展开其唯一参数,不生成运行时函数调用
## 关键词方法
@ -77,11 +77,11 @@
- `native_types::type_*`、`complex_types::type_*` 是编译期类型描述常量,不是函数。
- keyword extension method 是用户自定义扩展方法机制,不属于固定内置编译期函数清单。
## 当前实现风险
## 实现约束
编译期函数应当在任意合法表达式位置可用,并且在所有路径上保持一致语义。当前代码中仍存在处理入口分散的问题
编译期函数应当在任意合法表达式位置可用,并且在所有路径上保持一致语义:
- `any()` 主要在赋值右值路径中被特殊识别,表达式参数、二元运算、返回值等位置可能走普通函数调用或依赖 polyfill
- `any()` 已统一在普通函数调用表达式入口处理;赋值、参数、返回值、数组元素和运算子表达式共用相同语义
- `refval()` / `toRef()` 在参数解析和动态调用路径中特判较多,后续应统一为一个“引用包装表达式”解析入口。
- `objval()` 当前通过函数调用解析和类型推导路径识别,整体较集中。
@ -89,4 +89,4 @@
- 建立统一的 `CompileTimeFunctionResolver` 或等价模块。
- 在 `parseExpr()` / `detectTypeOfExpr()` / `detectClassOfExpr()` / 参数解析路径中复用同一份编译期函数元信息。
- 保证 `any()`、`refval()`、`objval()` 在任意表达式位置行为一致
- 继续统一 `refval()`、`objval()` 在不同表达式路径上的行为

@ -0,0 +1,6 @@
<?php
function composite_arrow_return(): void
{
$callback = fn (): int|string => [];
}

@ -0,0 +1,8 @@
<?php
function composite_closure_return(): void
{
$callback = function (): int|string {
return [];
};
}

@ -0,0 +1,6 @@
<?php
function composite_empty_return(): int|string
{
return;
}

@ -0,0 +1,16 @@
<?php
use PhpParser\Node\Expr;
interface CompositeKnownInterface
{
}
function accepts_composite_known(CompositeKnownInterface|int $value): void
{
}
function main(): void
{
accepts_composite_known(new Expr\Variable('value'));
}

@ -0,0 +1,11 @@
<?php
class CompositeTrueBox
{
public true|null $value = null;
public function fail(): void
{
$this->value = false;
}
}

@ -0,0 +1,41 @@
<?php
class CompositeStaticTypeTest extends \BaseTest
{
public function testLiteralFalseDoesNotMatchTrueUnion(): void
{
$this->exec(
'Cannot assign bool to property assignment of type `true|null`',
'composite-true-false-mismatch.php'
);
}
public function testExplicitEmptyReturnIsRejectedStatically(): void
{
$this->exec(
'Cannot assign null to return value of type `int|string`',
'composite-empty-return-mismatch.php'
);
}
public function testClosureReturnIsCheckedStatically(): void
{
$this->exec(
'Cannot assign array to closure return value of type `int|string`',
'composite-closure-return-mismatch.php'
);
}
public function testArrowFunctionReturnIsCheckedStatically(): void
{
$this->exec(
'Cannot assign array to closure return value of type `int|string`',
'composite-arrow-return-mismatch.php'
);
}
public function testExternalActualClassAgainstKnownInterfaceRemainsRuntimeUnknown(): void
{
$this->compile('composite-external-actual-unknown.php');
}
}

@ -109,6 +109,9 @@ class CompilerBase implements PropertyAccessContext
protected const string NATIVE_PROPERTY_VALUE_VAR = 'var';
protected const string NATIVE_PROPERTY_VALUE_DYNAMIC = 'dynamic';
protected const int COMPOSITE_TYPE_MISMATCH = -1;
protected const int COMPOSITE_TYPE_UNKNOWN = 0;
protected const int COMPOSITE_TYPE_MATCH = 1;
protected const string ATTR_ARRAY_DIM_FETCH_UPDATE = 'aotArrayDimFetchUpdate';
protected const string ATTR_PROPERTY_FETCH_UPDATE = 'aotPropertyFetchUpdate';
@ -1962,6 +1965,24 @@ class CompilerBase implements PropertyAccessContext
return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';';
}
if ($v->expr === null) {
$nullExpr = new Expr\ConstFetch(new Node\Name('null'));
if ($this->shouldCheckClosureReturnType()) {
$this->checkCompositeTypeAssignment(
$v,
$this->context->closureReturnTypeCheck,
$this->context->closureReturnTypeStr,
$nullExpr,
'closure return value'
);
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
$this->checkCompositeTypeAssignment(
$v,
$this->functionDef->returnTypeCheck,
$this->functionDef->returnTypeStr,
$nullExpr,
'return value'
);
}
if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) {
return 'return;';
} elseif ($this->shouldCheckClosureReturnType()) {
@ -1977,7 +1998,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)) {
if ($this->shouldCheckClosureReturnType()) {
$this->checkCompositeTypeAssignment(
$v,
$this->context->closureReturnTypeCheck,
$this->context->closureReturnTypeStr,
$v->expr,
'closure return value'
);
} elseif (!$this->context->inClosure && !empty($this->functionDef->returnTypeCheck)) {
$this->checkCompositeTypeAssignment(
$v,
$this->functionDef->returnTypeCheck,
@ -3521,6 +3550,12 @@ class CompilerBase implements PropertyAccessContext
if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) {
$this->fatalError($expr, 'Unsupported function: `' . $name . '`');
}
if ($name === 'any') {
if (count($expr->args) !== 1 || $expr->args[0]->unpack) {
$this->fatalError($expr, 'The any function expects exactly one non-unpacked argument');
}
return $this->parseExprAsValue($expr->args[0]->value);
}
if ($name === 'objval') {
return $this->genObjvalCall($expr);
}
@ -5343,13 +5378,17 @@ class CompilerBase implements PropertyAccessContext
}
$rightType = $this->detectTypeOfExpr($right);
if (!empty($def->typeCheck) && $this->checkCompositeTypeAssignment(
$compositeRelation = null;
if (!empty($def->typeCheck)) {
$compositeRelation = $this->checkCompositeTypeAssignment(
$left,
$def->typeCheck,
$def->typeStr,
$right,
'property assignment'
) && $rightType !== self::TYPE_VAR) {
);
}
if ($compositeRelation === self::COMPOSITE_TYPE_MATCH && $rightType !== self::TYPE_VAR) {
// A statically known member of the composite type needs no
// Variant runtime guard on this property write.
return $rightExpr;
@ -5363,7 +5402,7 @@ class CompilerBase implements PropertyAccessContext
}
$rightClass = $this->detectClassOfExpr($right);
if ($rightClass !== '') {
if ($rightClass !== '' && $compositeRelation === null) {
return $rightExpr;
}
@ -5390,8 +5429,13 @@ class CompilerBase implements PropertyAccessContext
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $tmpVar . '.typeStr()), php::Str(" given"))';
}
$coercion = $this->compositeTypeNeedsIntToFloatCoercion($typeCheck)
? 'if (' . $tmpVar . '.isInt()) { ' . $tmpVar . ' = php::toFloat(' . $tmpVar . '); } '
: '';
return '([&]() -> ' . self::TYPE_VAR . ' { '
. $tmpVar . ' = ' . $rightExpr . '; '
. $coercion
. 'if (UNEXPECTED(!(' . implode(' || ', $conditions) . '))) { '
. 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString()); '
. '} '
@ -7630,95 +7674,155 @@ class CompilerBase implements PropertyAccessContext
string $typeStr,
NodeAbstract $value,
string $context
): bool {
if ($this->compositeTypeMayMatch($value, $typeCheck)) {
return true;
): int {
$relation = $this->compositeTypeRelation($value, $typeCheck);
if ($relation !== self::COMPOSITE_TYPE_MISMATCH) {
return $relation;
}
$valueType = $this->staticTypeNameOfExpr($value);
$this->fatalError($errorNode, "Cannot assign {$valueType} to {$context} of type `{$typeStr}`");
}
protected function compositeTypeMayMatch(NodeAbstract $value, array $clauses): bool
protected function compositeTypeRelation(NodeAbstract $value, array $clauses): int
{
// TYPE_VAR means that the expression is dynamic or its result cannot
// be represented by the current scalar type system. Do not reject it.
// be represented by the current scalar type system. It must retain the
// runtime type check.
if ($this->detectTypeOfExpr($value) === self::TYPE_VAR && !$this->isNullExpr($value)) {
return true;
return self::COMPOSITE_TYPE_UNKNOWN;
}
$hasUnknown = false;
foreach ($clauses as $clause) {
if ($this->compositeTypeClauseMayMatch($value, $clause)) {
return true;
$relation = $this->compositeTypeClauseRelation($value, $clause);
if ($relation === self::COMPOSITE_TYPE_MATCH) {
return self::COMPOSITE_TYPE_MATCH;
}
if ($relation === self::COMPOSITE_TYPE_UNKNOWN) {
$hasUnknown = true;
}
}
return false;
return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MISMATCH;
}
protected function compositeTypeClauseMayMatch(NodeAbstract $value, array $clause): bool
protected function compositeTypeClauseRelation(NodeAbstract $value, array $clause): int
{
if (($clause['kind'] ?? '') === 'allOf') {
$hasUnknown = false;
foreach ($clause['types'] ?? [] as $entry) {
if (!$this->compositeTypeEntryMayMatch($value, $entry)) {
return false;
$relation = $this->compositeTypeEntryRelation($value, $entry);
if ($relation === self::COMPOSITE_TYPE_MISMATCH) {
return self::COMPOSITE_TYPE_MISMATCH;
}
if ($relation === self::COMPOSITE_TYPE_UNKNOWN) {
$hasUnknown = true;
}
}
return true;
return $hasUnknown ? self::COMPOSITE_TYPE_UNKNOWN : self::COMPOSITE_TYPE_MATCH;
}
return $this->compositeTypeEntryMayMatch($value, $clause);
return $this->compositeTypeEntryRelation($value, $clause);
}
protected function compositeTypeEntryMayMatch(NodeAbstract $value, array $entry): bool
protected function compositeTypeEntryRelation(NodeAbstract $value, array $entry): int
{
$kind = $entry['kind'] ?? '';
if ($kind === 'isNull') {
return $this->isNullExpr($value);
return $this->isNullExpr($value) ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH;
}
$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,
'isInt' => $this->exactCompositeTypeRelation($type, self::TYPE_INT),
// PHP permits int -> float widening. It is compatible but still
// needs conversion, so retain the runtime normalization path.
'isFloat' => $type === self::TYPE_INT
? self::COMPOSITE_TYPE_UNKNOWN
: $this->exactCompositeTypeRelation($type, self::TYPE_FLOAT),
'isBool' => $this->exactCompositeTypeRelation($type, self::TYPE_BOOL),
'isString' => $this->exactCompositeTypeRelation($type, self::TYPE_STR),
'isArray' => $this->exactCompositeTypeRelation($type, self::TYPE_ARRAY),
'isObject' => $this->exactCompositeTypeRelation($type, self::TYPE_OBJECT),
'isTrue' => $this->compositeLiteralBoolRelation($value, true),
'isFalse' => $this->compositeLiteralBoolRelation($value, false),
'isResource' => $this->exactCompositeTypeRelation($type, self::TYPE_RESOURCE),
'callable' => $this->compositeCallableRelation($value, $type),
'iterable' => $this->compositeIterableRelation($value, $type),
'instanceof' => $this->compositeObjectEntryRelation($value, $entry),
default => self::COMPOSITE_TYPE_UNKNOWN,
};
}
protected function compositeObjectEntryMayMatch(NodeAbstract $value, array $entry): bool
protected function exactCompositeTypeRelation(string $actual, string $expected): int
{
return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH;
}
protected function compositeLiteralBoolRelation(NodeAbstract $value, bool $expected): int
{
if ($this->isScalarBool($value)) {
$actual = strcasecmp($value->name->toString(), 'true') === 0;
return $actual === $expected ? self::COMPOSITE_TYPE_MATCH : self::COMPOSITE_TYPE_MISMATCH;
}
return $this->detectTypeOfExpr($value) === self::TYPE_BOOL
? self::COMPOSITE_TYPE_UNKNOWN
: self::COMPOSITE_TYPE_MISMATCH;
}
protected function compositeCallableRelation(NodeAbstract $value, string $type): int
{
if ($type === self::TYPE_STR || $type === self::TYPE_ARRAY || $type === self::TYPE_OBJECT) {
return self::COMPOSITE_TYPE_UNKNOWN;
}
return self::COMPOSITE_TYPE_MISMATCH;
}
protected function compositeIterableRelation(NodeAbstract $value, string $type): int
{
if ($type === self::TYPE_ARRAY) {
return self::COMPOSITE_TYPE_MATCH;
}
if ($type !== self::TYPE_OBJECT) {
return self::COMPOSITE_TYPE_MISMATCH;
}
return $this->compositeObjectTypeRelation($value, 'Traversable');
}
protected function compositeObjectEntryRelation(NodeAbstract $value, array $entry): int
{
if ($this->detectTypeOfExpr($value) !== self::TYPE_OBJECT) {
return false;
return self::COMPOSITE_TYPE_MISMATCH;
}
return $this->compositeObjectTypeRelation($value, $entry['class'] ?? '');
}
protected function compositeObjectTypeRelation(NodeAbstract $value, string $expected): int
{
$class = $this->detectDeclaredClassOfExpr($value);
if ($class === '') {
return true;
return self::COMPOSITE_TYPE_UNKNOWN;
}
$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;
if ($expected === '' || $expected === 'static') {
return self::COMPOSITE_TYPE_UNKNOWN;
}
$actualKnown = $this->hasClass($class)
|| $this->hasInterface($class)
|| $this->isInternalClass($class)
|| $this->isInternalInterface($class);
$expectedKnown = $this->hasClass($expected)
|| $this->hasInterface($expected)
|| $this->isInternalClass($expected)
|| $this->isInternalInterface($expected);
if (!$actualKnown || !$expectedKnown) {
return self::COMPOSITE_TYPE_UNKNOWN;
}
return $expected === 'static'
? true
: $this->isObjectClassStaticallyAssignableTo($class, $expected);
return $this->isObjectClassStaticallyAssignableTo($class, $expected)
? self::COMPOSITE_TYPE_MATCH
: self::COMPOSITE_TYPE_MISMATCH;
}
protected function isNullExpr(NodeAbstract $expr): bool

@ -158,6 +158,15 @@ trait ClosureGenerator
protected function genArrowFunctionBody(Node\Expr\ArrowFunction $expr): string
{
if (!empty($this->context->closureReturnTypeCheck)) {
$this->checkCompositeTypeAssignment(
$expr,
$this->context->closureReturnTypeCheck,
$this->context->closureReturnTypeStr,
$expr->expr,
'closure return value'
);
}
$code = $this->parseExpr($expr->expr);
if ($this->context->beforeStmtLines) {
$beforeCode = implode(PHP_EOL, $this->context->beforeStmtLines);

@ -184,6 +184,40 @@ trait TypeCheckGenerator
return '(' . implode(' && ', $conditions) . ')';
}
protected function compositeTypeNeedsIntToFloatCoercion(array $typeCheck): bool
{
return $this->compositeTypeContainsKind($typeCheck, 'isFloat')
&& !$this->compositeTypeContainsKind($typeCheck, 'isInt');
}
private function compositeTypeContainsKind(array $typeCheck, string $kind): bool
{
foreach ($typeCheck as $entry) {
if (($entry['kind'] ?? '') === $kind) {
return true;
}
if (($entry['kind'] ?? '') === 'allOf'
&& $this->compositeTypeContainsKind($entry['types'] ?? [], $kind)) {
return true;
}
}
return false;
}
protected function genCompositeIntToFloatCoercion(string $varName, array $typeCheck): string
{
if (!$this->compositeTypeNeedsIntToFloatCoercion($typeCheck)) {
return '';
}
$code = $this->getIndent() . 'if (' . $varName . '.isInt()) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $varName . ' = php::toFloat(' . $varName . ');' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
}
protected function getTypeCheckCallableName(): string
{
if ($this->classDef) {
@ -218,7 +252,8 @@ trait TypeCheckGenerator
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genUnionParamTypeErrorExpr($argInfo, $varName, (string) ($argIndex + 1));
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$code = $this->genCompositeIntToFloatCoercion($varName, $argInfo->typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
@ -250,6 +285,14 @@ trait TypeCheckGenerator
$code = $this->getIndent() . 'for (auto ' . $iterVar . ' = ' . $argInfo->name . '.begin(); ' . $iterVar . ' != ' . $argInfo->name . '.end(); ++' . $iterVar . ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $valueVar . ' = ' . $iterVar . '.value();' . PHP_EOL;
if ($this->compositeTypeNeedsIntToFloatCoercion($argInfo->typeCheck)) {
$code .= $this->getIndent() . 'if (' . $valueVar . '.isInt()) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $valueVar . ' = php::toFloat(' . $valueVar . ');' . PHP_EOL;
$code .= $this->getIndent() . $iterVar . '.valueRef() = ' . $valueVar . ';' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
$code .= $this->getIndent() . self::TYPE_INT . ' ' . $argNoVar . ' = ' . ($argIndex + 1) . ' + ' . $iterVar . '.index();' . PHP_EOL;
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
@ -302,7 +345,8 @@ trait TypeCheckGenerator
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr($fnName, true) . ' "(): Return value must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $varName . '.typeStr()), php::Str(" given"))';
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
@ -335,7 +379,8 @@ trait TypeCheckGenerator
$orExpr = implode(' || ', $conditions);
$msgExpr = $this->genClosureParamTypeErrorExpr($argInfo, $argInfo->name, (string) ($argIndex + 1));
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$code = $this->genCompositeIntToFloatCoercion($argInfo->name, $argInfo->typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;
@ -367,6 +412,14 @@ trait TypeCheckGenerator
$code = $this->getIndent() . 'for (auto ' . $iterVar . ' = ' . $argInfo->name . '.begin(); ' . $iterVar . ' != ' . $argInfo->name . '.end(); ++' . $iterVar . ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . self::TYPE_VAR . ' ' . $valueVar . ' = ' . $iterVar . '.value();' . PHP_EOL;
if ($this->compositeTypeNeedsIntToFloatCoercion($argInfo->typeCheck)) {
$code .= $this->getIndent() . 'if (' . $valueVar . '.isInt()) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $valueVar . ' = php::toFloat(' . $valueVar . ');' . PHP_EOL;
$code .= $this->getIndent() . $iterVar . '.valueRef() = ' . $valueVar . ';' . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
$code .= $this->getIndent() . self::TYPE_INT . ' ' . $argNoVar . ' = ' . ($argIndex + 1) . ' + ' . $iterVar . '.index();' . PHP_EOL;
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
@ -416,7 +469,8 @@ trait TypeCheckGenerator
$msgExpr = 'php::concat(php::concat(php::Str(' . $this->genCharPtr('{closure}', true) . ' "(): Return value must be of type " '
. $this->genCharPtr($typeStr, true) . ' ", "), ' . $varName . '.typeStr()), php::Str(" given"))';
$code = $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck);
$code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . 'return php::throwException(zend_ce_type_error, (' . $msgExpr . ').toCString());' . PHP_EOL;
$this->indentLevel--;

@ -148,7 +148,6 @@ trait AssignOpTrait
$type = $this->detectTypeOfExpr($right);
$finalVarType = $this->getNormalAssignType($type);
$runtimeObjectAssignClass = '';
$rightExprOverride = null;
if ($type === self::TYPE_VOID) {
$type = self::TYPE_VAR;
}
@ -204,18 +203,7 @@ trait AssignOpTrait
}
}
if ($this->isFuncCallExpr($right) and $this->isNameExpr($right->name)) {
$fn = $this->parseIdentifier($right->name);
if (count($right->args) === 1 and $fn === 'any') {
$type = self::TYPE_VAR;
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
$finalVarType = $type;
return $var . ' = ' . $this->parseIdentifier($right->args[0]->value);
}
$rightExprOverride = $this->parseIdentifier($right->args[0]->value);
} else {
$type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type;
}
$type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type;
} elseif ($this->isStaticCall($right) and $this->isNameExpr($right->class) and $this->isIdExpr($right->name)) {
$class = $this->parseIdentifier($right->class);
if ($class === 'std') {
@ -299,7 +287,7 @@ trait AssignOpTrait
}
$var = $this->parseWritableIdentifier($left);
$rightExpr = $rightExprOverride ?? $this->parseAssignRightExpr($right);
$rightExpr = $this->parseAssignRightExpr($right);
if ($propertyWriteTarget !== null) {
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
}

@ -0,0 +1,32 @@
--TEST--
any() is available in arbitrary expression positions
--FILE--
<?php
function return_any(int $value): mixed
{
return any($value);
}
function main(): void
{
var_dump(any(1));
var_dump(return_any(2));
var_dump([any(3), any("four")]);
var_dump(any(4) + 1);
var_dump(true ? any(5) : any(6));
var_dump(any(any(7)));
}
?>
--EXPECT--
int(1)
int(2)
array(2) {
[0]=>
int(3)
[1]=>
string(4) "four"
}
int(5)
int(5)
int(7)

@ -16,7 +16,7 @@ function main(): void
var_dump($generator->getReturn());
try {
union_generator([]);
union_generator(any([]));
} catch (Throwable $e) {
echo get_class($e), "\n";
}

@ -13,7 +13,7 @@ class Select {
class Worker
{
public static ?stdClass $globalEvent = null;
public static ?Select $globalEvent = null;
public static function init() {
self::$globalEvent = new Select;
@ -26,4 +26,4 @@ function main() {
}
?>
--EXPECT--
string(4) "test"
string(4) "test"

@ -2,7 +2,9 @@
place-holder
--FILE--
<?php
class Select {
interface EventInterface {}
class Select implements EventInterface {
public function onReadable($stream, callable $func): void
{
$func($stream);
@ -38,4 +40,3 @@ function main()
--EXPECT--
string(27) "Worker::acceptUdpConnection"
string(10) "tcp-stream"

@ -0,0 +1,60 @@
--TEST--
Composite static checks preserve unknown runtime guards and float widening
--FILE--
<?php
function float_or_string(float|string $value): float|string
{
return $value;
}
function variadic_float_or_string(float|string ...$values): array
{
return $values;
}
class CompositeEdgeBox
{
public float|string $number;
public true|null $flag = null;
public function setDynamicFlag(mixed $value): void
{
$this->flag = $value;
}
}
function main(): void
{
var_dump(float_or_string(1));
var_dump(float_or_string(any(2)));
var_dump(variadic_float_or_string(3, "ok"));
$closure = fn (float|string $value): float|string => $value;
var_dump($closure(5));
$box = new CompositeEdgeBox();
$box->number = 4;
var_dump($box->number);
$box->flag = true;
var_dump($box->flag);
try {
$box->setDynamicFlag(false);
} catch (TypeError $e) {
var_dump(get_class($e));
}
}
?>
--EXPECT--
float(1)
float(2)
array(2) {
[0]=>
float(3)
[1]=>
string(2) "ok"
}
float(5)
float(4)
bool(true)
string(9) "TypeError"

@ -13,17 +13,13 @@ 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(dynamic_value(new OnlyA()));
expect_both(any(new OnlyA()));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}

@ -27,10 +27,6 @@ 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);
@ -51,25 +47,25 @@ function main() {
$errors = [];
try {
expect_int_or_string(dynamic_value(3.14));
expect_int_or_string(any(3.14));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_int_or_string(dynamic_value([]));
expect_int_or_string(any([]));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_nullable_int(dynamic_value("hello"));
expect_nullable_int(any("hello"));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
expect_bool_or_array(dynamic_value(42));
expect_bool_or_array(any(42));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}

@ -19,12 +19,12 @@ function main(): void
$errors = [];
try {
collect_scalars(1, "two", []);
collect_scalars(1, "two", any([]));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}
try {
collect_nullable(ok: 1, bad: "x");
collect_nullable(ok: 1, bad: any("x"));
} catch (\TypeError $e) {
$errors[] = $e->getMessage();
}

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

@ -4,7 +4,6 @@ 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
{
@ -13,13 +12,13 @@ class TypeHitCoalesceProperty
public function run(): void
{
try {
$this->union ??= dynamic_value(null);
$this->union ??= any(null);
} catch (TypeError $e) {
var_dump($e->getMessage());
}
$this->union = "ok";
$this->union ??= dynamic_value(null);
$this->union ??= any(null);
var_dump($this->union);
}
}

Loading…
Cancel
Save