diff --git a/phpunit/code/any-reference-capture.php b/phpunit/code/any-reference-capture.php new file mode 100644 index 00000000..9d09cd46 --- /dev/null +++ b/phpunit/code/any-reference-capture.php @@ -0,0 +1,10 @@ + $value) { + var_dump($value); + } +} diff --git a/phpunit/code/foreach-key-native-reuse.php b/phpunit/code/foreach-key-native-reuse.php new file mode 100644 index 00000000..57057355 --- /dev/null +++ b/phpunit/code/foreach-key-native-reuse.php @@ -0,0 +1,9 @@ + $value) { + var_dump($value); + } +} diff --git a/phpunit/code/foreach-key-varint-reuse.php b/phpunit/code/foreach-key-varint-reuse.php new file mode 100644 index 00000000..ba0c8fdf --- /dev/null +++ b/phpunit/code/foreach-key-varint-reuse.php @@ -0,0 +1,11 @@ + $value) { + var_dump($value); + } +} diff --git a/phpunit/code/native-destructuring-reuse.php b/phpunit/code/native-destructuring-reuse.php new file mode 100644 index 00000000..0310ed63 --- /dev/null +++ b/phpunit/code/native-destructuring-reuse.php @@ -0,0 +1,15 @@ +value] = $values; +} diff --git a/phpunit/code/native-scalar-reference-capture.php b/phpunit/code/native-scalar-reference-capture.php new file mode 100644 index 00000000..516a5f62 --- /dev/null +++ b/phpunit/code/native-scalar-reference-capture.php @@ -0,0 +1,10 @@ +expectException(TestError::class); + $this->expectExceptionMessage( + 'Cannot assign value to variable $index of type php::Int with type php::Var', + ); + + $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/foreach-key-native-reuse.php', + ); + } + + public function testVarIntAndExplicitAnyCanBeReusedAsForeachKeys(): void + { + foreach (['foreach-key-varint-reuse.php', 'foreach-key-any-reuse.php'] as $file) { + $code = $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file, + ); + self::assertStringContainsString('php::Var index', $code); + self::assertStringNotContainsString('php::Int index', $code); + } + } + + public function testNativeScalarCannotBePromotedForReferenceCapture(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage( + 'Cannot create a reference to native variable of type php::Bool; initialize it with std::any()', + ); + + $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/native-scalar-reference-capture.php', + ); + } + + public function testExplicitAnySupportsReferenceCapture(): void + { + $code = $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/any-reference-capture.php', + ); + self::assertStringContainsString('php::Var changed', $code); + self::assertStringContainsString('changed.toReference()', $code); + } + + public function testDestructuringKeepsNativeTargetsAndConvertsDynamicItems(): void + { + $code = $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/native-destructuring-reuse.php', + ); + + self::assertStringContainsString('php::Int index', $code); + self::assertMatchesRegularExpression('/index = php::toInt\([^;]+\.item\(0L?/', $code); + self::assertStringContainsString('php::toInt(', $code); + } + private function createCompiler(): CompilerTest { global $translator; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index f960e6f6..16476ae4 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -150,28 +150,28 @@ trait CallArgumentGenerator // Evaluate every supplied argument in PHP source order. The resulting // expressions/temporaries may then be rearranged safely for the native // C++ ABI without changing observable call order. - foreach ($sourceArgs as $sourceIndex => [$argIndex, $variadicName, $arg]) { + foreach ($sourceArgs as $sourceIndex => [$sourceArgIndex, $variadicName, $arg]) { if ($sourceIndex < $lastHoistingSourceIndex && $arg instanceof Node\Arg && !$arg->unpack && $this->isSnapshotableVariableRead($arg->value) ) { - $paramInfo = $argIndex === $variadicArgIndex + $paramInfo = $sourceArgIndex === $variadicArgIndex ? $functionDef->argInfoList[$variadicArgIndex] - : $this->getArgInfo($arg, $nativeFunc, $argIndex); + : $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); if ($paramInfo !== null && !$paramInfo->byRef) { $snapshot = $this->parseOrderedOperand($arg->value, false, true); $arg = clone $arg; $arg->value = new Expr\Variable($snapshot, $arg->value->getAttributes()); } } - if ($argIndex !== $variadicArgIndex) { - $argInfo = $this->getArgInfo($arg, $nativeFunc, $argIndex); - $resolvedArgs[$argIndex] = $this->getTypeConvertedArg( + if ($sourceArgIndex !== $variadicArgIndex) { + $argInfo = $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); + $resolvedArgs[$sourceArgIndex] = $this->getTypeConvertedArg( $arg, $argInfo, $callableName, - $argIndex + $sourceArgIndex ); continue; } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 8492b555..3940d994 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -291,6 +291,11 @@ trait ClosureGenerator $this->context->inClosure = false; $code .= '};' . PHP_EOL; + // Capture expressions belong to the enclosing function. Restore its + // type table before validating references; the closure body registers + // every captured value as php::Var and must not hide a native outer + // variable that cannot legally acquire reference semantics. + $this->context = $oriContext; $useVars = []; if ($uses) { foreach ($uses as $useItem) { @@ -313,7 +318,6 @@ trait ClosureGenerator } } - $this->context = $oriContext; $this->context->beforeStmtLines[] = $code; // Even a static closure inherits the outer called scope for late diff --git a/src/Optimizer/SsaTypeOptimizer.php b/src/Optimizer/SsaTypeOptimizer.php index 758af384..75895b7b 100644 --- a/src/Optimizer/SsaTypeOptimizer.php +++ b/src/Optimizer/SsaTypeOptimizer.php @@ -123,6 +123,9 @@ trait SsaTypeOptimizer if (!$target instanceof Node) { continue; } + if ($target instanceof Node\Expr\Variable && is_string($target->name)) { + $foreachTargets[$target->name] = true; + } foreach ($nodeFinder->findInstanceOf($target, Node\Expr\Variable::class) as $variable) { if (is_string($variable->name)) { $foreachTargets[$variable->name] = true; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 08358029..a8a0673d 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -287,20 +287,24 @@ trait AssignOpTrait continue; } if ($item instanceof ArrayItem) { - $key = $item->key ? $this->parseArrayKey($item->key, true) : (string) $k; + $key = $item->key ?? new Node\Scalar\Int_($k, $item->getAttributes()); + $itemExpr = new Expr\ArrayDimFetch( + new Variable($tmpVar, $item->getAttributes()), + $key, + $item->getAttributes(), + ); if ($item->value instanceof Expr\List_) { - $nestedTmp = $this->genTmpVarName(); - $this->addLocalVar($nestedTmp, Type::ARRAY); - $code .= $this->getIndent() . "{$nestedTmp} = {$tmpVar}.item({$key});" . PHP_EOL; $code .= $this->getIndent() - . $this->parseAssignToList($item->value, new Variable($nestedTmp)) + . $this->parseAssignToList($item->value, $itemExpr) . PHP_EOL; } else { - $var = $this->parseWritableIdentifier($item->value); - if ($this->isVarExpr($item->value) and !$this->hasVar($var)) { - $this->addLocalVar($var, Type::VAR); - } - $code .= $this->getIndent() . "{$var} = {$tmpVar}.item({$key});" . PHP_EOL; + // Route every destructuring target through the normal + // assignment pipeline. Existing native locals remain + // native and receive the usual scalar conversion, while + // typed properties retain their runtime type checks. + $code .= $this->getIndent() + . $this->parseAssignFinally($item->value, $itemExpr) + . ';' . PHP_EOL; } } else { $this->unsupportedSyntax($item); @@ -715,7 +719,6 @@ trait AssignOpTrait // assignment may not execute on every path. $this->markNativeObjectNonNull($var); } - $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); if ($propertyWriteTarget !== null && ($propertyDef = $this->getNativePropertyDef($left)) !== null) { $effectiveRightType = $rightExprType === Type::VAR && $this->getFixedPropertyTypeCheckHelper($propertyDef) !== null @@ -725,7 +728,7 @@ trait AssignOpTrait } $assignedExpr = $finalVarType === Type::VAR ? $rightExpr - : $this->convertExprType($rightExpr, $leftExprType, $rightExprType); + : $this->convertExprType($rightExpr, $finalVarType, $rightExprType); if ($foldIntoDeclaration) { $this->context->localVarInitializers[$var] = $assignedExpr; return ''; diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 583515b9..a52d2ecc 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -101,13 +101,7 @@ trait ForeachTrait if (!$this->hasVar($valueVar)) { $this->addLocalVar($valueVar, Type::REF); } elseif ($this->getVarType($valueVar) !== Type::REF && $this->getVarType($valueVar) !== Type::VAR) { - if ($this->hasLocalVar($valueVar) && !$this->hasArgument($valueVar)) { - // Local declarations are emitted after the body is parsed, so a - // previously optimized scalar can still be promoted to Variant. - $this->context->localVars[$valueVar] = Type::VAR; - } else { - $this->fatalError($node, 'Cannot bind foreach reference to native variable of type ' . $this->getVarType($valueVar)); - } + $this->fatalError($node, 'Cannot bind foreach reference to native variable of type ' . $this->getVarType($valueVar)); } return $this->getIndent() . $valueRefExpr . '(' . $valueVar . ');' . PHP_EOL; } diff --git a/src/Parser/SwitchTrait.php b/src/Parser/SwitchTrait.php index b851b54a..5e9829cf 100644 --- a/src/Parser/SwitchTrait.php +++ b/src/Parser/SwitchTrait.php @@ -113,7 +113,7 @@ trait SwitchTrait $hasDefault = false; } - foreach ($caseGroups as $target => [$conds]) { + foreach ($caseGroups as $groupIndex => [$conds]) { if (!empty($conds)) { $groupMatched = $this->genTmpVarName(); $code .= $this->getIndent() . 'bool ' . $groupMatched . ' = false;' . PHP_EOL; @@ -140,7 +140,7 @@ trait SwitchTrait } $code .= $this->getIndent() . 'if (' . $groupMatched . ') {' . PHP_EOL; $code .= $this->getIndent() . $switchMatched . ' = true;' . PHP_EOL; - $code .= $this->getIndent() . $switchTarget . ' = ' . $target . ';' . PHP_EOL; + $code .= $this->getIndent() . $switchTarget . ' = ' . $groupIndex . ';' . PHP_EOL; $code .= $this->getIndent() . '}' . PHP_EOL; } } @@ -150,8 +150,8 @@ trait SwitchTrait $code .= $this->getIndent() . '}' . PHP_EOL; } - foreach ($caseGroups as $target => [, , $stmts]) { - $code .= $this->getIndent() . 'if (' . $switchTarget . ' == ' . $target . ') {' . PHP_EOL; + foreach ($caseGroups as $groupIndex => [, , $stmts]) { + $code .= $this->getIndent() . 'if (' . $switchTarget . ' == ' . $groupIndex . ') {' . PHP_EOL; $this->indentLevel++; $code .= $this->parseStmts($stmts); $this->indentLevel--; diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 5db48a9b..f61d2d0d 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -291,7 +291,11 @@ trait TypeConversionTrait } $var = $this->parseIdentifier($expr); if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) { - $this->context->localVars[$var] = Type::VAR; + $this->fatalError( + $expr, + 'Cannot create a reference to native variable of type ' . $this->getVarType($var) + . '; initialize it with std::any() when reference semantics are required', + ); } return $var . '.toReference()'; } diff --git a/src/gen_stub.php b/src/gen_stub.php index a6acd681..4b16bb10 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -14,6 +14,10 @@ declare(strict_types=1); namespace TypePhp\StubGenerator; +// php-src's stub generator intentionally follows Zend PHP's dynamic integer +// semantics and frequently reuses loop/index variables for array keys. +use varint_types; + use Closure; use DOMCdataSection; use DOMComment; @@ -2632,13 +2636,18 @@ class EvaluatedValue $nodeTraverser->addVisitor($visitor); $expr = $nodeTraverser->traverse([$expr])[0]; - $isUnknownConstValue = false; + // Keep mutable callback state in an object: TypePHP deliberately uses + // native storage for inferred bool locals, which cannot be captured by + // PHP reference. Object capture preserves identity in both Zend PHP and + // the self-hosted compiler without changing the evaluator's behavior. + $evaluationState = new \stdClass(); + $evaluationState->isUnknownConstValue = false; $evaluator = null; $evaluator = new ConstExprEvaluator( static function (Expr $expr) use ( $allConstInfos, - &$isUnknownConstValue, + $evaluationState, &$evaluator, ) { // php-parser's ConstExprEvaluator predates PHP 8.5 constant @@ -2687,7 +2696,7 @@ class EvaluatedValue } else { $constName = $expr->name->__toString(); if (strtolower($constName) === "unknown") { - $isUnknownConstValue = true; + $evaluationState->isUnknownConstValue = true; return null; } } @@ -2745,6 +2754,7 @@ class EvaluatedValue // emitted through its @cvalue macro. For a concrete null expression, // however, the zval must be initialized as null even when the declared // type is nullable (for example, `const ?int VALUE = null`). + $isUnknownConstValue = $evaluationState->isUnknownConstValue; $valueType = $result === null && !$isUnknownConstValue ? SimpleType::null() : ($constType ?? SimpleType::fromValue($result));