fix(parser): resolve variable type conflicts in destructuring and closure captures

- Replace manual array key parsing with direct Node\Scalar\Int_ creation
- Route all destructuring assignments through parseAssignFinally for proper type handling
- Remove redundant left expression type detection in assignment operations
- Fix argument index mapping in CallArgumentGenerator to use source indices correctly
- Restore outer context before closure reference validation to prevent type conflicts
- Remove unused local variable promotion logic in foreach reference binding
- Add explicit error messages for native variable reference capture attempts
- Track foreach target variables in SSA optimizer for proper type analysis
- Use numeric indices instead of case labels in switch statement generation
- Add comprehensive tests for varint type handling in foreach loops
master
韩天峰 1 month ago
parent 86c56ad1bf
commit b92f782606
  1. 10
      phpunit/code/any-reference-capture.php
  2. 9
      phpunit/code/foreach-key-any-reuse.php
  3. 9
      phpunit/code/foreach-key-native-reuse.php
  4. 11
      phpunit/code/foreach-key-varint-reuse.php
  5. 15
      phpunit/code/native-destructuring-reuse.php
  6. 10
      phpunit/code/native-scalar-reference-capture.php
  7. 60
      phpunit/src/VarIntTypesTest.php
  8. 14
      src/Generator/CallArgumentGenerator.php
  9. 6
      src/Generator/ClosureGenerator.php
  10. 3
      src/Optimizer/SsaTypeOptimizer.php
  11. 27
      src/Parser/AssignOpTrait.php
  12. 8
      src/Parser/ForeachTrait.php
  13. 8
      src/Parser/SwitchTrait.php
  14. 6
      src/Parser/TypeConversionTrait.php
  15. 16
      src/gen_stub.php

@ -0,0 +1,10 @@
<?php
function anyReferenceCapture(): void
{
$changed = std::any(false);
$set = static function () use (&$changed): void {
$changed = true;
};
$set();
}

@ -0,0 +1,9 @@
<?php
function foreachKeyAnyReuse(array $values): void
{
$index = std::any(100);
foreach ($values as $index => $value) {
var_dump($value);
}
}

@ -0,0 +1,9 @@
<?php
function foreachKeyNativeReuse(array $values): void
{
$index = 100;
foreach ($values as $index => $value) {
var_dump($value);
}
}

@ -0,0 +1,11 @@
<?php
use varint_types;
function foreachKeyVarIntReuse(array $values): void
{
$index = 100;
foreach ($values as $index => $value) {
var_dump($value);
}
}

@ -0,0 +1,15 @@
<?php
class NativeDestructuringBox
{
public int $value = 0;
}
function nativeDestructuringReuse(array $values): void
{
$index = 100;
[$index] = $values;
$box = new NativeDestructuringBox();
[$box->value] = $values;
}

@ -0,0 +1,10 @@
<?php
function nativeScalarReferenceCapture(): void
{
$changed = false;
$set = static function () use (&$changed): void {
$changed = true;
};
$set();
}

@ -46,6 +46,66 @@ final class VarIntTypesTest extends BaseTest
self::assertStringNotContainsString('php::Var integer = 42L;', $defaultCode);
}
public function testNativeIntegerCannotBeReusedAsForeachKey(): void
{
$this->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;

@ -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;
}

@ -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

@ -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;

@ -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 '';

@ -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;
}

@ -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--;

@ -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()';
}

@ -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));

Loading…
Cancel
Save