diff --git a/docs/COMPILE_TIME_FUNCTIONS.md b/docs/COMPILE_TIME_FUNCTIONS.md index a8ad3d24..222df916 100644 --- a/docs/COMPILE_TIME_FUNCTIONS.md +++ b/docs/COMPILE_TIME_FUNCTIONS.md @@ -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()` 在不同表达式路径上的行为。 diff --git a/phpunit/code/composite-arrow-return-mismatch.php b/phpunit/code/composite-arrow-return-mismatch.php new file mode 100644 index 00000000..6d719e10 --- /dev/null +++ b/phpunit/code/composite-arrow-return-mismatch.php @@ -0,0 +1,6 @@ + []; +} diff --git a/phpunit/code/composite-closure-return-mismatch.php b/phpunit/code/composite-closure-return-mismatch.php new file mode 100644 index 00000000..1de2758b --- /dev/null +++ b/phpunit/code/composite-closure-return-mismatch.php @@ -0,0 +1,8 @@ +value = false; + } +} diff --git a/phpunit/src/CompositeStaticTypeTest.php b/phpunit/src/CompositeStaticTypeTest.php new file mode 100644 index 00000000..77492947 --- /dev/null +++ b/phpunit/src/CompositeStaticTypeTest.php @@ -0,0 +1,41 @@ +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'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index dd133582..d8dd53d6 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.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 diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 436e2856..b58fcd34 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -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); diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 76e99962..0cbf2d67 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -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--; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 2e518347..ce11561d 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -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); } diff --git a/tests/aot/basic/any-expression-positions.phpt b/tests/aot/basic/any-expression-positions.phpt new file mode 100644 index 00000000..d4351838 --- /dev/null +++ b/tests/aot/basic/any-expression-positions.phpt @@ -0,0 +1,32 @@ +--TEST-- +any() is available in arbitrary expression positions +--FILE-- + +--EXPECT-- +int(1) +int(2) +array(2) { + [0]=> + int(3) + [1]=> + string(4) "four" +} +int(5) +int(5) +int(7) diff --git a/tests/aot/generator/union-signatures.phpt b/tests/aot/generator/union-signatures.phpt index a5313333..d58f7ef8 100644 --- a/tests/aot/generator/union-signatures.phpt +++ b/tests/aot/generator/union-signatures.phpt @@ -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"; } diff --git a/tests/aot/object_property/private-prop-001.phpt b/tests/aot/object_property/private-prop-001.phpt index a7682cc4..68b73ae8 100644 --- a/tests/aot/object_property/private-prop-001.phpt +++ b/tests/aot/object_property/private-prop-001.phpt @@ -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" \ No newline at end of file +string(4) "test" diff --git a/tests/aot/place-holder/003.phpt b/tests/aot/place-holder/003.phpt index 67ed626d..ccffa1b3 100644 --- a/tests/aot/place-holder/003.phpt +++ b/tests/aot/place-holder/003.phpt @@ -2,7 +2,9 @@ place-holder --FILE-- 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" diff --git a/tests/aot/type_decl/intersection-param-check.phpt b/tests/aot/type_decl/intersection-param-check.phpt index 7281fa3d..343330f8 100644 --- a/tests/aot/type_decl/intersection-param-check.phpt +++ b/tests/aot/type_decl/intersection-param-check.phpt @@ -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(); } diff --git a/tests/aot/type_decl/union-param-check.phpt b/tests/aot/type_decl/union-param-check.phpt index 1808a8dd..348242b4 100644 --- a/tests/aot/type_decl/union-param-check.phpt +++ b/tests/aot/type_decl/union-param-check.phpt @@ -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(); } diff --git a/tests/aot/type_decl/variadic-union-param-check.phpt b/tests/aot/type_decl/variadic-union-param-check.phpt index 0ad7a0d7..44b1ebb4 100644 --- a/tests/aot/type_decl/variadic-union-param-check.phpt +++ b/tests/aot/type_decl/variadic-union-param-check.phpt @@ -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(); } diff --git a/tests/aot/type_hits/009.phpt b/tests/aot/type_hits/009.phpt index ffec0626..24b38f30 100644 --- a/tests/aot/type_hits/009.phpt +++ b/tests/aot/type_hits/009.phpt @@ -2,7 +2,6 @@ type hits: instance property type check message includes class name --FILE-- union = dynamic_value(null); + $this->union = any(null); } catch (TypeError $e) { var_dump($e->getMessage()); } diff --git a/tests/aot/type_hits/010.phpt b/tests/aot/type_hits/010.phpt index d6133ccd..ca2cd360 100644 --- a/tests/aot/type_hits/010.phpt +++ b/tests/aot/type_hits/010.phpt @@ -4,7 +4,6 @@ type hits: property coalesce assignment uses runtime type check USE_ZEND_ALLOC=0 --FILE-- 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); } }