dim !== null) { $this->assertNotNativeObjectArrayKey($left->dim); } if ($left instanceof Expr\ArrayDimFetch) { $this->assertNotNativeObjectArrayDimensionReceiver($left->var, $left); } if ($this->isPropertyFetch($left)) { return $this->parseAssignPropertyArrayDim($left, $right); } if ($this->isVarExpr($left->var) && $left->var->name === 'GLOBALS') { $target = $this->parseGlobalsArrayDimFetch($left); $tmp = $this->genTmpVarName(); if ($this->isNativeObjectVar($target)) { $targetClass = $this->getNativeObjectVarClass($target); $rightClass = $this->detectClassOfExpr($right); if ($this->isNull($right)) { $value = 'nullptr'; } elseif (!$this->isNativeObjectClass($rightClass)) { $this->fatalError($right, "Native object `\${$target}` cannot be converted to var/object"); } elseif (!$this->isObjectClassStaticallyAssignableTo($rightClass, $targetClass)) { $this->fatalError($right, "Cannot assign native object `{$rightClass}` to `{$targetClass}`"); } else { $value = $this->parseExprAsValue($right); } $this->addLocalVar($tmp, $this->getNativeObjectPointerType($targetClass)); $this->addNativeObject($tmp, $targetClass); } else { $value = $this->parseExprAsValue($right); $this->addLocalVar($tmp, Type::VAR); } return '((' . $tmp . ' = ' . $value . ', ' . $target . ' = ' . $tmp . '), ' . $tmp . ')'; } $array = $this->parseWritableIdentifier($left->var); $code = ''; if (!$this->hasVar($array) and $this->isVarExpr($left->var)) { $this->addLocalVar($array, Type::ARRAY); } $value = $this->parseExprAsValue($right); $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); // item(dim, true) updates an existing reference's value, while offsetSet() // replaces the array bucket and breaks the reference. Keep offsetSet() for // ArrayAccess objects; dynamically typed/reference containers need a // runtime array check because either representation is possible. $arrayType = $this->getVarType($array); if ($left->dim === null) { return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')'; } $dim = $this->parseIdentifier($left->dim); if ($arrayType === Type::ARRAY) { return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')'; } if ($arrayType === Type::VAR || $arrayType === Type::REF) { $writeArray = "static_cast({$array}.item({$dim}, true) = {$tmp})"; $writeOther = "{$array}.offsetSet({$dim}, {$tmp})"; return $code . '((' . $tmp . ' = ' . $value . ', ' . "({$array}.isArray() ? {$writeArray} : {$writeOther})" . '), ' . $tmp . ')'; } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')'; } protected function parseAssignPropertyFetch(NodeAbstract $left, NodeAbstract $right, ?PropertyWriteTarget $target = null): string { if ($target !== null) { $this->assertCanAssignPropertyWrite($target, $right); } $rightExpr = $this->parseExprAsValue($right); if ($target !== null) { $rightExpr = $this->wrapPropertyWriteTypeCheck($target, $right, $rightExpr); } else { $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr); } $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); // Comma expression: store RHS → execute side effect → evaluate to stored value return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyFetchWrite($left, $tmp, $target) . '), ' . $tmp . ')'; } protected function parseRightAssociativeAssign(NodeAbstract $left, Expr\Assign $right): string { $chain[] = $left; $next = $right; while ($this->isAssignExpr($next)) { $var = $next->var; $chain[] = $var; $next = $next->expr; } $tmpVar = $this->genTmpVarName(); $nativeClass = $this->detectClassOfExpr($next); if ($this->isNativeObjectClass($nativeClass)) { $this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass)); $this->addNativeObject($tmpVar, $nativeClass); } else { $this->addLocalVar($tmpVar, Type::VAR); } // 翻转赋值链 $chain = array_reverse($chain); $list = []; $list[] = $tmpVar . ' = ' . $this->parseExpr($next); $rightVar = new Variable($tmpVar); foreach ($chain as $var) { $list[] = $this->parseAssignFinally($var, $rightVar); } return '(' . implode(', ', $list) . ')'; } protected function parseAssign(Expr\Assign $v): string { $left = $v->var; $right = $v->expr; if ($this->isAssignExpr($right)) { return $this->parseRightAssociativeAssign($left, $right); } if ($left instanceof Expr\List_ && $v->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false)) { $optimized = $this->parseAssignToMultiReturn($left, $right); if ($optimized !== null) { return $optimized; } } return $this->parseAssignFinally($left, $right); } private function parseAssignToMultiReturn(Expr\List_ $left, Expr $right): ?string { if (!$right instanceof Expr\FuncCall || (!$this->isNameExpr($right->name) && !$this->isFullNameExpr($right->name))) { return null; } $nativeFunc = $this->findNativeFunction($this->parseIdentifier($right->name)); if ($nativeFunc === false) { return null; } $functionDef = $this->getFunction($nativeFunc); if (!$functionDef->hasMultiReturn() || $functionDef->multiReturnCount < count($left->items) || $this->shouldUseDynamicCallForNativeArgs($nativeFunc, $right->args)) { return null; } $variables = []; foreach ($left->items as $item) { if (!$item instanceof ArrayItem || $item->key !== null || $item->unpack || $item->byRef || !$this->isVarExpr($item->value) || !is_string($item->value->name)) { return null; } $name = $this->parseWritableIdentifier($item->value); if ($this->hasVar($name) && $this->getVarType($name) !== Type::VAR) { return null; } $variables[] = $name; } foreach ($variables as $variableName) { if (!$this->hasVar($variableName)) { $this->addLocalVar($variableName, Type::VAR); } } $tieItems = array_merge( $variables, array_fill(0, $functionDef->multiReturnCount - count($variables), 'std::ignore'), ); $right->setAttribute(self::ATTR_MULTI_RETURN_IMPL, true); return 'std::tie(' . implode(', ', $tieItems) . ') = ' . $this->parseFuncCall($right); } protected function parseAssignToList(Expr $left, Expr $right): string { $items = $left->items; $code = '{'; $this->indentLevel++; $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, Type::VAR); $code .= $this->getIndent() . $tmpVar . ' = ' . $this->parseExpr($right) . '; '; foreach ($items as $k => $item) { if (!$item) { continue; } if ($item instanceof ArrayItem) { $key = $item->key ? $this->parseArrayKey($item->key) : (string) $k; if ($item->value instanceof Expr\List_) { $nestedTmp = $this->genTmpVarName(); $this->addLocalVar($nestedTmp, Type::ARRAY); $code .= "{$nestedTmp} = {$tmpVar}.item({$key}); "; $code .= $this->parseAssignToList($item->value, new Variable($nestedTmp)); } else { $var = $this->parseWritableIdentifier($item->value); if ($this->isVarExpr($item->value) and !$this->hasVar($var)) { $this->addLocalVar($var, Type::VAR); } $code .= "{$var} = {$tmpVar}.item({$key}); "; } } else { abort($item); } } $this->indentLevel--; return $code . '}'; } protected function parseAssignFinally(Expr $left, Expr $right): string { $this->assertNotNullsafeWriteContext($left); $rightClass = $this->detectClassOfExpr($right); // A Native-element std container owns a PHPX Box but its raw pointer // elements are traced only by the function-local container root frame. // Copying the whole Box into any other value slot could outlive that // frame, so reject the escape once at the assignment boundary. Element // reads/writes do not pass the container variable itself as the RHS. if ($this->isVarExpr($right)) { $this->assertStdContainerDoesNotEscapeNativeObjects( $right, $this->parseIdentifier($right), ); } if ($left instanceof Expr\List_) { if ($this->isNativeObjectClass($rightClass)) { $this->fatalError($right, 'Native objects cannot be destructured into PHP values'); } return $this->parseAssignToList($left, $right); } if ($this->isNativeObjectClass($rightClass)) { $allowed = false; if ($this->isVarExpr($left)) { $leftName = $this->parseVariable($left); $declaredClass = $this->getDeclaredObjectType($leftName); if ($declaredClass !== '' && $this->isInterface($declaredClass)) { $this->fatalError( $left, 'Native objects cannot be assigned to interface-typed variables', ); } $allowed = !$this->hasVar($leftName) || $this->isNativeObjectVar($leftName) || $this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName); if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); } } elseif (($globalSlot = $this->getStaticGlobalsSlot($left)) !== null) { if (!$this->hasGlobalVar($globalSlot)) { $this->addGlobalVar($globalSlot, Type::VAR); } if (!$this->hasScopeGlobalVar($globalSlot)) { $this->addScopeGlobalVar($globalSlot, $this->globalVars[$globalSlot]); } $this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $right); $allowed = true; } elseif ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) { $receiverClass = $this->detectClassOfExpr($left->var); if ($this->isNativeObjectClass($receiverClass)) { $propertyName = $left->name->toString(); $resolution = $this->resolveNativeInstanceProperty($left, $propertyName, $receiverClass); if ($resolution !== null) { $this->applyNativePropertyAccessResult($left, $resolution); } $property = $resolution?->propertyDef; if ($property !== null && $property->class !== '' && $this->isInterface($property->class)) { $this->fatalError( $left, 'Native objects cannot be assigned to interface-typed properties', ); } $allowed = $property !== null && $property->type === Type::OBJECT && $this->isNativeObjectClass($property->class); } } elseif ($left instanceof Expr\ArrayDimFetch && $this->isStdContainerExpr($left)) { $info = $this->isStdArrayExpr($left) ? $this->getStdArrayInfo($left) : $this->getStdContainerInfo($left); // A std container with a concrete Native class element type is // compile-time storage, not a PHP array/Variant boundary. $allowed = $info !== null && isset($info['class']) && $this->isNativeObjectClass($info['class']); } if (!$allowed) { $this->fatalError( $left, 'Native objects cannot be stored in PHP arrays, PHP object properties, static properties, or mixed variables', ); } } // A direct assignment to readonly must go through write_property so // Zend can enforce scope, initialization state, type and clone rules. $propertyWriteTarget = $this->preparePropertyWriteTarget($left, true); $type = $this->detectTypeOfExpr($right); $finalVarType = $this->getNormalAssignType($type); $runtimeObjectAssignClass = ''; $assigningNullToTypedObject = false; $markNativeObjectNonNull = false; if ($type === Type::VOID) { $type = Type::VAR; } if ($left instanceof Expr\PropertyFetch && ($setter = $this->getPropertyHookSetter($left)) !== null) { return $this->parseAssignPropertyHook($left, $right, $propertyWriteTarget, $setter); } if ($left instanceof Expr\PropertyFetch && $this->isReadOnlyPropertyHook($left)) { $this->fatalError($left, 'Cannot write to read-only hooked property'); } if ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) { $receiverClass = $this->detectClassOfExpr($left->var); if ($this->isNativeObjectClass($receiverClass)) { $property = $left->name->toString(); $access = $this->getNativePropertyAccess($left); if ($access === null) { $resolution = $this->resolveNativeInstanceProperty($left, $property, $receiverClass); if ($resolution === null) { $this->fatalError($left, "Native class `{$receiverClass}` has no property `\${$property}`"); } $this->applyNativePropertyAccessResult($left, $resolution); $access = $this->getNativePropertyAccess($left); } $def = $access->getPropertyDef(); // Parse and materialize the receiver before the right-hand // expression. PHP evaluates an object/property target before // its assigned value, and C++ operand order must not decide it. $leftExpr = $this->parsePropertyFetch($left); if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) { if ($this->isNull($right)) { if (!$def->nullable) { $this->fatalError($right, "Cannot assign null to native property `{$receiverClass}::\${$property}`"); } return $leftExpr . ' = nullptr'; } $rightClass = $this->detectClassOfExpr($right); if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) { $this->fatalError($right, "Cannot assign value to native property `{$receiverClass}::\${$property}`"); } return $leftExpr . ' = ' . $this->parseExprAsValue($right); } $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); $rightExpr = $this->parseExprAsValue($right); $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); if ($def->type !== Type::VAR) { $rightExpr = $this->convertExprFromType($def->type, $rightExpr); } return $leftExpr . ' = ' . $rightExpr; } } if ($propertyWriteTarget !== null && $this->shouldUseDynamicNativePropertyWrite($left, $type)) { return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget); } if ($this->isVarExpr($left)) { $var = $this->parseWritableIdentifier($left); if ($var === 'this_') { $this->fatalError($left, 'Cannot re-assign $this'); } $assigningNullToTypedObject = $this->hasVar($var) && $this->getVarType($var) === Type::OBJECT && $this->isNull($right); if ($this->isStdContainer($var)) { $copyAssign = $this->parseStdContainerCopyAssign($var, $right); if ($copyAssign !== null) { return $copyAssign; } } // 类型推断,获取对象的类名,如果不是对象则返回空字符串 $rightClass = $this->detectClassOfExpr($right); $markNativeObjectNonNull = $this->context->scopeLevel <= 1 && !$this->hasScopeGlobalVar($var) && !$this->hasStaticVar($var) && $this->isNativeObjectClass($rightClass) && $this->isNativeObjectExpressionKnownNonNull($right); if ($this->isNativeObjectVar($var)) { // Assignment rebinds the local pointer slot. Even an // assignment nested in a conditional invalidates the simple // non-null proof; later reads fall back to nativeDeref(). $this->forgetNativeObjectNonNull($var); $leftClass = $this->getNativeObjectVarClass($var); if ($this->isNull($right)) { return $var . ' = nullptr'; } if ($rightClass === '' || !$this->isNativeObjectClass($rightClass)) { $this->fatalError($right, "Native object `\${$var}` cannot be converted to var/object"); } if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { $this->fatalError($right, "Cannot assign native object `{$rightClass}` to `{$leftClass}`"); } } // 右值是一个对象,已获得类的名称,左值必须与右值的类一致 if ($rightClass) { if (!$this->hasVar($var)) { if ($this->isNativeObjectClass($rightClass)) { $this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); $this->addNativeObject($var, $rightClass); } else { $this->addLocalVar($var, Type::OBJECT); $this->addObject($var, $rightClass); } } elseif (($leftClass = $this->getDeclaredObjectType($var)) !== '') { if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { // A child object can be assigned to a parent typed object. } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) { if ($this->isKnownConcreteObjectExpr($right, $rightClass)) { $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); } // Parent/interface/abstract declarations are not precise enough for a concrete typed object. $runtimeObjectAssignClass = $leftClass; } else { $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); } } else { $this->checkVarAssignExpr($left, $this->getVarType($var), Type::OBJECT); } } else { if ($this->isMethodCall($right) and $this->isNamedMethod($right->name)) { $methodName = $right->name->toString(); if (in_array($methodName, ['toStdArray', 'toStdVector', 'toStdMap', 'toStdOrderedMap'], true)) { if ($this->hasVar($var)) { $this->fatalError($left, "Cannot re-assign `\${$var}` to {$methodName}()"); } if ($this->context->scopeLevel > 1) { $this->fatalError($left, "Must use {$methodName}() in the top-level scope of the function"); } return $this->parseToStdAssign($var, $right); } } if ($this->isFuncCallExpr($right) and $this->isNameExpr($right->name)) { $type = $type === Type::VOID ? 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') { if (in_array($right->name->toString(), ['array', 'vector', 'map', 'ordered_map'], true)) { if ($this->hasScopeGlobalVar($var) || $this->hasStaticVar($var)) { $this->assertNativeStdContainerFunctionLocal($right); } if ($this->hasVar($var)) { $this->fatalError($left, "Cannot re-assign `\${$var}` to std::{$right->name->toString()}"); } if ($this->context->scopeLevel > 1) { $this->fatalError($left, "Must create std::{$right->name->toString()} in the top-level scope of the function"); } if ($right->name->toString() === 'array') { $this->addLocalVar($var, Type::STD_ARRAY); return $this->parseStdArray($var, $right); } if ($right->name->toString() === 'vector') { $this->addLocalVar($var, Type::STD_VECTOR); return $this->parseStdVector($var, $right); } if ($right->name->toString() === 'map') { $this->addLocalVar($var, Type::STD_MAP); return $this->parseStdMap($var, $right); } $this->addLocalVar($var, Type::STD_ORDERED_MAP); return $this->parseStdOrderedMap($var, $right); } else { $valueExpr = $this->parseStdCall($right); if (!$this->hasVar($var)) { $finalVarType = $right->getAttribute('nativeType'); $this->addLocalVar($var, $finalVarType); } return $var . ' = ' . $valueExpr; } } } elseif ($this->isVarExpr($right)) { $rightVar = $this->parseIdentifier($right); $this->assertStdContainerDoesNotEscapeNativeObjects($right, $rightVar); $type = $this->isStdContainer($rightVar) ? Type::ARRAY : $this->getVarType($rightVar); $finalVarType = $this->getNormalAssignType($type); $leftClass = $this->getDeclaredObjectType($var); $rightClass = $this->getDeclaredObjectType($rightVar); if ($leftClass !== '' and $rightClass !== '') { if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { // A child object can be assigned to a parent typed object. } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) { $runtimeObjectAssignClass = $leftClass; } else { $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); } } } // 变量第一次被赋值,确定其类型,由于 PHP 的变量作用域是 function 级的,在 for/while 块中声明的变量,可以在块外使用 if (!$this->hasVar($var)) { $finalVarType = $this->getNormalAssignType($type); $finalVarType = $this->isNativeType($finalVarType) ? $this->getNativeType($finalVarType) : $finalVarType; $this->addLocalVar($var, $finalVarType); } else { $finalVarType = $this->getVarType($var); $this->checkVarAssignExpr($left, $finalVarType, $type); $declaredObjectClass = $this->getDeclaredObjectType($var); if (!$assigningNullToTypedObject && $finalVarType === Type::OBJECT && $declaredObjectClass !== '' && ($type === Type::VAR || $type === Type::OBJECT)) { $runtimeObjectAssignClass = $declaredObjectClass; } } } } elseif ($this->isPropertyFetch($left) and !$this->isNativePropertyAccess($left)) { return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget); } elseif ($this->isArrayDimFetch($left) and $this->isVarExpr($left->var)) { $tmp = $this->parseIdentifier($left->var); if ($this->getVarType($tmp) === Type::STR and $left->dim === null) { $this->fatalError($left, 'Cannot use [] for strings'); } if ($this->isStdContainerExpr($left)) { return $this->parseStdContainerAssign($left, $right); } return $this->parseAssignArrayDim($left, $right); } elseif ($this->isArrayDimFetch($left) and $this->isPropertyFetch($left->var)) { return $this->parseAssignPropertyArrayDim($left, $right); } if ($propertyWriteTarget !== null) { $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); } $var = $this->parseWritableIdentifier($left); $rightExpr = $this->parseAssignRightExpr($right); if ($propertyWriteTarget !== null) { $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); } if ($assigningNullToTypedObject) { // php::Object intentionally keeps the inferred class as its C++ type, // while an empty/null value represents the same state as unset($var). $rightExpr = 'php::Object{' . $rightExpr . '}'; } if ($runtimeObjectAssignClass !== '') { // A typed object has two valid runtime states: the declared class or // null. Evaluate a dynamic RHS once, preserve null, and validate only // actual objects against the inferred class constraint. $checkedValue = 'typephp_nullable_object_value'; $rightExpr = '([&](php::Var ' . $checkedValue . ') -> php::Object {' . ' if (' . $checkedValue . '.isNull()) { return php::Object{' . $checkedValue . '}; }' . ' return php::toObject(' . $checkedValue . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ');' . ' })(' . $rightExpr . ')'; } if ($markNativeObjectNonNull) { // Parsing is sequential: this proof affects only subsequent source // expressions. Nested control flow is excluded above because its // 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->getNativeScalarPropertyTypeCheckHelper($propertyDef) !== null ? $propertyDef->type : $rightExprType; return $var . ' = ' . $this->convertNativePropertyWriteExpr($propertyDef->type, $effectiveRightType, $rightExpr); } if ($finalVarType === Type::VAR) { return $var . ' = ' . $rightExpr; } else { return $var . ' = ' . $this->convertExprType($rightExpr, $leftExprType, $rightExprType); } } protected function parseAssignPropertyHook( Expr\PropertyFetch $left, Expr $right, ?PropertyWriteTarget $target, string $setter, ): string { $property = $this->getNativePropertyDef($left); $nativeObjectClass = $property !== null && $property->type === Type::OBJECT && $this->isNativeObjectClass($property->class) ? $property->class : ''; if ($target !== null) { $this->assertCanAssignPropertyWrite($target, $right); } if ($nativeObjectClass !== '') { if ($this->isNull($right)) { if (!$property->nullable) { $this->fatalError($right, "Cannot assign null to native property `{$nativeObjectClass}`"); } $rightExpr = 'nullptr'; } else { $rightClass = $this->detectClassOfExpr($right); if ($rightClass === '' || !$this->isNativeObjectClass($rightClass) || !$this->isObjectClassStaticallyAssignableTo($rightClass, $nativeObjectClass) ) { $this->fatalError($right, "Cannot assign value to native property of type `{$nativeObjectClass}`"); } $rightExpr = $this->parseExprAsValue($right); } } else { $rightExpr = $this->parseExprAsValue($right); } if ($target !== null && $nativeObjectClass === '') { $rightExpr = $this->wrapPropertyWriteTypeCheck($target, $right, $rightExpr); } $tmp = $this->genTmpVarName(); if ($nativeObjectClass !== '') { // The synthesized value variable is also the assignment result and // the setter argument. Preserve its exact Native type so argument // validation and NativeRootFrame generation do not see a mixed // Zend value at this compiler-created boundary. $this->addLocalVar($tmp, $this->getNativeObjectPointerType($nativeObjectClass)); $this->addNativeObject($tmp, $nativeObjectClass); } else { $this->addLocalVar($tmp, Type::VAR); } $call = $this->emitPropertyHookSetterCall($left, $setter, new Expr\Variable($tmp)); return '((' . $tmp . ' = ' . $rightExpr . ', ' . $call . '), ' . $tmp . ')'; } protected function shouldUseDynamicNativePropertyWrite(Expr $left, string $rightType): bool { if (!$this->isPropertyFetch($left)) { return false; } $def = $this->getNativePropertyDef($left); if ($def === null) { return false; } if ($def->isReadonly()) { return true; } return !in_array($def->type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY], true) && $rightType === Type::VAR; } protected function parseStdContainerCopyAssign(string $leftVar, Expr $right): ?string { $rightInfo = $this->getStdContainerExprInfo($right); if ($rightInfo === null) { return null; } $leftInfo = $this->getStdContainerVarInfo($leftVar); if (!$this->isSameStdContainerInfo($leftInfo, $rightInfo)) { $this->fatalError($right, 'Cannot copy std container with different type'); } if (!$this->isStdArray($leftVar)) { $this->assertStdContainerStructureMutable($right, $leftVar); } return $leftVar . '_ref = ' . $this->parseStdContainerCopyExpr($right); } protected function parseAssignRightExpr(Expr $right): string { $rightExpr = $this->parseExprAsValue($right); if ($this->isVarExpr($right)) { $rightVar = $this->parseIdentifier($right); if ($this->isStdContainer($rightVar)) { return $this->convertArrayExpr($rightExpr); } } return $rightExpr; } protected function removeAssignOp(string $op): string { return str_replace('=', '', $op); } protected function parseAssignOp(Expr\AssignOp $node, string $op): string { $this->assertNativeObjectOperatorOperandSupported($node->var, $node, $op); $this->assertNotNullsafeWriteContext($node->var); $this->assertNativePropertyHookDirectWriteTarget($node->var); $pythonOperator = $this->parsePythonAssignOperator($node); if ($pythonOperator !== null) { return $pythonOperator; } $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); $this->guardLiteralDivisionByZero($node->expr, $op); if ($node->var instanceof Expr\PropertyFetch && $this->isNativeObjectPropertyHook($node->var)) { $this->fatalError( $node->var, 'Native property hooks only support direct reads and assignments', ); } if ($node->var instanceof Expr\PropertyFetch && $this->isReadOnlyPropertyHook($node->var)) { $this->fatalError($node->var, 'Cannot write to read-only hooked property'); } if ($node->var instanceof Expr\PropertyFetch && ($setter = $this->getPropertyHookSetter($node->var)) !== null && ($getter = $this->getPropertyHookGetter($node->var)) !== null) { $read = $this->emitPropertyHookGetterCall($node->var, $getter); $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); $binaryOp = $this->removeAssignOp($op); $value = match ($binaryOp) { '.' => $this->parseFlattenedConcat($node->expr, [ $this->prepareConcatOperand($read, $this->detectTypeOfExpr($node->var)), ]), '**' => 'php::fn::pow(' . $read . ', ' . $this->parseExprAsValue($node->expr) . ')', default => $read . ' ' . $binaryOp . ' (' . $this->parseExprAsValue($node->expr) . ')', }; $call = $this->emitPropertyHookSetterCall($node->var, $setter, new Expr\Variable($tmp)); return '((' . $tmp . ' = ' . $value . ', ' . $call . '), ' . $tmp . ')'; } $nativePropertyAssignOp = $this->parseNativePropertyAssignOp($node, $op); if ($nativePropertyAssignOp !== null) { return $nativePropertyAssignOp; } $var = $this->parseWritableIdentifier($node->var); $expr = $this->isAssignOpConcat($op) ? '' : (string) $this->parseIdentifier($node->expr); if ($this->isVarExpr($node->var)) { if (!$this->hasVar($var)) { $this->fatalError($node->var, 'Cannot assign to undefined variable'); } $type = $this->detectVarType($node->var); $rightType = $this->detectTypeOfExpr($node->expr); // Big* types: expand compound assignment to static method call. // BigInt/BigDecimal/BigFloat are immutable Box types stored inside // php::Var — Variant::operator+= calls ZendVM add_function which // cannot handle them. We must generate `$v = Type::add($v, $x)`. if ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) { return $this->parseBigAssignOp($node, $var, $type, $expr, $rightType, $op); } $rightExprStr = $this->convertExprType($expr, $type, $rightType); if ($this->isAssignOpConcat($op)) { if ($this->isArrayVar($node->var)) { $this->fatalError($node->var, 'Cannot concat string to array'); } return $var . ' = ' . $this->parseFlattenedConcat($node->expr, [ $this->prepareConcatOperand($var, $type), ]); } if ($this->isAssignOpPow($op)) { $powExpr = 'php::fn::pow(' . $var . ', ' . $rightExprStr . ')'; return $var . ' = ' . $this->convertVarType($var, $powExpr); } return $var . ' ' . $op . ' ' . $rightExprStr; } if ($this->isArrayDimFetch($node->var)) { if ($this->isStdContainerExpr($node->var)) { return $this->parseStdContainerAssignOp($node, $op); } /** * $count[$r] -= 1; * 需要转为下面语句: * $tmp_var = $count[$r] - 1; * $count[$r] = $tmp_var;. */ $type = $this->detectVarType($node->var); $rightType = $this->detectTypeOfExpr($node->expr); $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, $rightType); $dim = $this->parseIdentifier($node->var->dim); $readVar = $this->parseArrayDimFetchRead($node->var); $binaryOp = $this->removeAssignOp($op); if ($binaryOp === '.') { $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->parseFlattenedConcat($node->expr, [ $this->prepareConcatOperand($this->convertVarType($tmpVar, $readVar), $type), ]) . ';'; } elseif ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) { $bigAssign = $this->parseBigAssignOpExpr($readVar, $type, $expr, $rightType, $binaryOp, $node->var, $node->expr); $this->context->beforeStmtLines[] = "{$tmpVar} = {$bigAssign};"; } else { $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->convertVarType($tmpVar, $readVar) . ' ' . $binaryOp . ' ' . $this->convertExprType($expr, $type, $rightType) . ';'; } if ($this->isVarExpr($node->var->var) && $node->var->var->name === 'GLOBALS') { return $var . ' = ' . $tmpVar; } return '(' . $this->parseArrayDimStore($node->var->var, $dim, $tmpVar) . ', ' . $tmpVar . ')'; } if ($this->isPropertyFetch($node->var) and !$this->isNativePropertyAccess($node->var)) { if ($propertyWriteTarget !== null) { $this->assertCanAssignPropertyWrite($propertyWriteTarget, $node->expr); } $binaryOp = $this->removeAssignOp($op); $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, Type::VAR); $readProperty = $this->emitDynamicPropertyFetchRead($node->var, $propertyWriteTarget); if ($this->isAssignOpConcat($op)) { $this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->parseFlattenedConcat($node->expr, [ $this->prepareConcatOperand($readProperty, $this->detectTypeOfExpr($node->var)), ]) . ';'; } elseif ($this->isAssignOpPow($op)) { $this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$readProperty}, {$expr});"; } else { $this->context->beforeStmtLines[] = "{$tmpVar} = {$readProperty} {$binaryOp} ({$expr});"; } $this->context->afterStmtLines[] = $this->emitDynamicPropertyFetchWrite($node->var, $tmpVar, $propertyWriteTarget) . ';'; return $tmpVar; } if ($this->isAssignOpConcat($op)) { if (!($node->expr instanceof Expr\BinaryOp\Concat)) { return $var . '.append(' . $this->parseExprAsValue($node->expr) . ')'; } return $var . ' = php::toString(' . $this->parseFlattenedConcat($node->expr, [$var]) . ')'; } return $var . ' ' . $op . ' (' . $expr . ')'; } protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op): ?string { if (!$this->isPropertyFetch($node->var)) { return null; } $def = $this->getNativePropertyDef($node->var); if ($def === null) { return null; } if ($def->isReadonly()) { // A native scalar reference mutates the property zval directly and // bypasses Zend's readonly checks. Keep readonly properties on the // normal attr() path so no raw scalar reference escapes the wrapper. return null; } $rightType = $this->detectTypeOfExpr($node->expr); if (in_array($def->type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true)) { $binaryOp = $this->removeAssignOp($op); $leftExpr = $this->parseWritableIdentifier($node->var); $rightExpr = (string) $this->parseIdentifier($node->expr); $value = $this->parseBigAssignOpExpr( $leftExpr, $def->type, $rightExpr, $rightType, $binaryOp, $node->var, $node->expr, ); return $leftExpr . ' = ' . $value; } if ($this->isFixedObjectProp($def) && $rightType !== Type::VAR && !$this->canAssignStaticTypeToObjectProperty($def, $rightType)) { $this->fatalError( $node->var, 'Cannot assign ' . $this->getPropertyAssignmentTypeName($rightType) . ' to property ' . $this->getObjectPropertyTypeCheckDisplayName($node->var) . ' of type ' . $this->getObjectPropertyTypeCheckTypeString($def) ); } if (!$this->canUseNativePropertyAssignOp($def->type, $rightType, $op)) { return null; } $var = $this->parseWritableIdentifier($node->var); if (!$this->isNativePropertyTypedValue($node->var)) { $helper = $def->type === Type::FLOAT ? 'typephp_static_float_ref' : 'typephp_static_int_ref'; $var = $helper . '(' . $var . '.unwrap_ptr())'; } $rightExpr = $this->parseIdentifier($node->expr); if ($rightType === Type::VAR) { $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($node->var, $node->expr, $rightExpr); } $effectiveRightType = $rightType === Type::VAR && $this->getNativeScalarPropertyTypeCheckHelper($def) !== null ? $def->type : $rightType; return $var . ' ' . $op . ' (' . $this->convertNativePropertyWriteExpr($def->type, $effectiveRightType, $rightExpr) . ')'; } protected function convertNativePropertyWriteExpr(string $propertyType, string $rightType, string $rightExpr): string { if ($propertyType === $rightType) { return $rightExpr; } return $this->convertExprFromType($propertyType, $rightExpr); } protected function canUseNativePropertyAssignOp(string $propertyType, string $rightType, string $op): bool { if ($rightType !== Type::VAR && !($propertyType === $rightType || ($propertyType === Type::FLOAT && $rightType === Type::INT))) { return false; } return match ($propertyType) { Type::INT => in_array($op, ['+=', '-=', '*=', '%=', '<<=', '>>=', '&=', '|=', '^='], true), Type::FLOAT => in_array($op, ['+=', '-=', '*=', '/='], true), default => false, }; } protected function parseBigAssignOp(Expr\AssignOp $node, string $var, string $type, string $expr, string $rightType, string $op): string { $binaryOp = $this->removeAssignOp($op); $bigExpr = $this->parseBigAssignOpExpr($var, $type, $expr, $rightType, $binaryOp, $node->var, $node->expr); return $var . ' = ' . $bigExpr; } protected function parseBigAssignOpExpr(string $leftExpr, string $leftType, string $rightExpr, string $rightType, string $binaryOp, NodeAbstract $errorNode, ?NodeAbstract $rightNode = null): string { [$class, $opMap] = match ($leftType) { Type::BIGINT => ['BigInt', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '&' => 'bitAnd', '|' => 'bitOr', '^' => 'bitXor', '<<' => 'bitShiftLeft', '>>' => 'bitShiftRight']], Type::DECIMAL => ['Decimal', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod']], Type::BIGFLOAT => ['BigFloat', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div']], }; $method = $opMap[$binaryOp] ?? null; if ($method === null) { $this->fatalError($errorNode, "Unsupported compound assignment operator '{$binaryOp}' for type {$leftType}"); } // For bitwise shifts, the right operand is a shift amount (Int), not BigInt $isShift = ($binaryOp === '<<' || $binaryOp === '>>'); $convertedRight = match ($leftType) { Type::BIGINT => $isShift ? $rightExpr : $this->convertBigIntExpr($rightExpr, $rightType), Type::DECIMAL => $this->convertDecimalExpr($rightExpr, $rightType, $rightNode), Type::BIGFLOAT => $this->convertBigFloatExpr($rightExpr, $rightType), }; return 'php::' . $class . '::' . $method . '(' . $leftExpr . ', ' . $convertedRight . ')'; } protected function parseAssignOpConcat(Expr\AssignOp\Concat $expr): string { return $this->parseAssignOp($expr, '.='); } protected function parseAssignOpPlus(Expr\AssignOp\Plus $expr): string { return $this->parseAssignOp($expr, '+='); } protected function parseAssignOpMinus(Expr\AssignOp\Minus $expr): string { return $this->parseAssignOp($expr, '-='); } protected function parseAssignOpMod(Expr\AssignOp\Mod $expr): string { return $this->parseAssignOp($expr, '%='); } protected function parseAssignOpMul(Expr\AssignOp\Mul $expr): string { return $this->parseAssignOp($expr, '*='); } protected function parseAssignOpDiv(Expr\AssignOp\Div $expr): string { return $this->parseAssignOp($expr, '/='); } protected function parseAssignOpBitwiseAnd(Expr\AssignOp\BitwiseAnd $expr): string { return $this->parseAssignOp($expr, '&='); } protected function parseAssignOpBitwiseOr(Expr\AssignOp\BitwiseOr $expr): string { return $this->parseAssignOp($expr, '|='); } protected function parseAssignOpPow(Expr\AssignOp\Pow $expr): string { return $this->parseAssignOp($expr, '**='); } protected function parseArrayDimStore($array, $dim, $var): string { $id = $this->parseWritableIdentifier($array); return $id . '.offsetSet(' . $dim . ', ' . $var . ')'; } protected function parseAssignOpShiftLeft(Expr\AssignOp\ShiftLeft $node): string { return $this->parseAssignOp($node, '<<='); } protected function parseAssignOpShiftRight(Expr\AssignOp\ShiftRight $node): string { return $this->parseAssignOp($node, '>>='); } protected function parseAssignOpBitwiseXor(Expr\AssignOp\BitwiseXor $node): string { return $this->parseAssignOp($node, '^='); } protected function parseAssignRef(Expr\AssignRef $expr): string { $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->expr); if ($expr->expr instanceof Expr\NullsafePropertyFetch) { $this->fatalError($expr->expr, 'Cannot take reference of a nullsafe chain'); } $this->assertNativeObjectReferenceForbidden($expr->var, $expr); $this->assertNativeObjectReferenceForbidden($expr->expr, $expr); foreach ([$expr->var, $expr->expr] as $referenceOperand) { if ($this->isVarExpr($referenceOperand)) { $this->assertStdContainerDoesNotEscapeNativeObjects( $referenceOperand, $this->parseIdentifier($referenceOperand), ); } } // A reference would outlive the constructor-only write window and // make later mutations invisible to the compiler. It is therefore // forbidden on either side even inside the declaring constructor. $this->assertReadonlyPropertyReferenceForbidden($expr->var, $expr, true); $this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false); $left = $this->parseWritableIdentifier($expr->var); // Keep this write-context form for every RHS kind. Re-parsing it as a // read later breaks append and missing-key targets such as // `$array[] =& $source`. if ($this->isVarExpr($expr->var)) { if (!$this->hasVar($left)) { $this->addLocalVar($left, Type::REF); } else { $type = $this->getVarType($left); if ($type !== Type::REF) { $this->fatalError($expr, 'Cannot assign reference to variable of type ' . $type); } } } $tmpVar = $this->addTmpVar(Type::REF); $rightExpr = ''; if ($this->isVarExpr($expr->expr)) { $rightExpr = $tmpVar . ' = ' . $this->parseIdentifier($expr->expr) . '.toReference()'; } elseif ($expr->expr instanceof Expr\FuncCall && $this->isNameExpr($expr->expr->name)) { $name = $this->parseIdentifier($expr->expr->name); $function = $this->findNativeFunction($name); if ($function) { if (!$this->getFunction($function)->returnsByRef) { $this->fatalError($expr, 'Cannot assign reference to a function that does not return by reference'); } } else { $reflection = \TypePhp\Resolver\Reflection::getFunction(ltrim($this->getNamespacedFuncName($name), '\\')); if ($reflection === null || !$reflection->isInternal() || !$reflection->returnsReference()) { $this->fatalError($expr, 'Cannot assign reference to a function that does not return by reference'); } } $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); } elseif ($expr->expr instanceof Expr\FuncCall) { $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; } elseif ($expr->expr instanceof Expr\MethodCall) { if (!$this->isNamedMethod($expr->expr->name) || !$this->isVarExpr($expr->expr->var)) { $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; } else { $object = $this->parseIdentifier($expr->expr->var); $method = $this->parseIdentifier($expr->expr->name); $function = $this->findNativeMethod($expr->expr, $object, $method); if (!$function) { $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; } else { if (!$this->getFunction($function)->returnsByRef) { $this->fatalError($expr, 'Cannot assign reference to a method that does not return by reference'); } $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); } } } elseif ($expr->expr instanceof Expr\StaticCall) { if (!$this->isNameExpr($expr->expr->class) || !$this->isIdExpr($expr->expr->name)) { $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; } else { $class = $this->parseIdentifier($expr->expr->class); if ($class === 'self') { $class = $this->getFullClassName(); } elseif ($class === 'parent') { if (!$this->classDef || !$this->classDef->extends) { $this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class'); } $class = $this->classDef->extends; } elseif ($class !== 'static') { $class = $this->getNamespacedClassName($class); } $method = $this->parseIdentifier($expr->expr->name); $function = $class === 'static' ? false : $this->getNativeMethod($expr->expr, $class, $method); if (!$function) { $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; } else { if (!$this->getFunction($function)->returnsByRef) { $this->fatalError($expr, 'Cannot assign reference to a static method that does not return by reference'); } $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); } } } elseif ($this->isPropertyFetch($expr->expr)) { $rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr); } elseif ($this->isStaticPropertyFetch($expr->expr)) { $rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr); } elseif ($this->isArrayDimFetch($expr->expr)) { $array = $this->parseWritableIdentifier($expr->expr->var); if ($expr->expr->dim == null) { $this->fatalError($expr, 'Cannot assign reference to array dim fetch without dim'); } $rightExpr = $tmpVar . ' = ' . $array . '.itemRef(' . $this->parseIdentifier($expr->expr->dim) . ')'; } else { $this->fatalError($expr, 'Cannot assign reference to ' . $this->parseIdentifier($expr->expr)); } $this->context->beforeStmtLines[] = $rightExpr . ';'; if ($expr->var instanceof Expr\PropertyFetch && $this->isNativePropertyAccess($expr->var)) { return $left . '.rebindReference(' . $tmpVar . ')'; } return $left . ' = &' . $tmpVar; } protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string { $this->assertNativePropertyHookDirectWriteTarget($left); $propertyWriteTarget = $this->preparePropertyWriteTarget($left->var); $code = ''; $value = $this->parseExprAsValue($right); $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); if ($left->dim === null) { return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchAppendArray($left->var, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')'; } $dim = $this->parseIdentifier($left->dim); return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchUpdateArray($left->var, $dim, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')'; } protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string { $this->checkLeftValue($expr->var); $rightClass = $this->detectClassOfExpr($expr->expr); $nativeRight = $this->isNativeObjectClass($rightClass); // An undefined variable must exist before generating its isset check. // A Native RHS establishes a typed nullptr slot; it must never be // declared as Variant because boxing the raw pointer would coerce it // to bool. Other values retain the normal nullable Variant behavior. $var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; $globalSlot = $this->getStaticGlobalsSlot($expr->var); if ($globalSlot !== null) { if (!$this->hasGlobalVar($globalSlot)) { $this->addGlobalVar($globalSlot, Type::VAR); } if (!$this->hasScopeGlobalVar($globalSlot)) { $this->addScopeGlobalVar($globalSlot, $this->globalVars[$globalSlot]); } if ($nativeRight) { $this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $expr->expr); } $var = $globalSlot; } elseif ($nativeRight && $expr->var instanceof Expr\ArrayDimFetch && $this->isVarExpr($expr->var->var) && $expr->var->var->name === 'GLOBALS' ) { $this->fatalError( $expr->var, 'Native objects cannot be stored in dynamically addressed `$GLOBALS`', ); } if ($var !== null && !$this->hasVar($var)) { if ($nativeRight) { $this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); $this->addNativeObject($var, $rightClass); } else { $this->addLocalVar($var, Type::VAR); } } if ($var !== null && $this->isNativeObjectVar($var)) { $leftClass = $this->getNativeObjectVarClass($var); if ($this->isNull($expr->expr)) { // nullptr remains a valid nullable slot value. } elseif (!$nativeRight) { $this->fatalError($expr->expr, "Native object `\${$var}` cannot be converted to var/object"); } elseif (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { $this->fatalError( $expr->expr, "Cannot assign native object `{$rightClass}` to `{$leftClass}`", ); } } $isset = $var !== null && $this->isNativeObjectVar($var) ? $var . ' != nullptr' : $this->parseChainedExpr($expr->var, self::OP_ISSET); $var ??= $this->parseWritableIdentifier($expr->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); if ($propertyWriteTarget !== null) { $this->assertCanAssignPropertyWrite($propertyWriteTarget, $expr->expr); } $rightBeforeCount = count($this->context->beforeStmtLines); $rightAfterCount = count($this->context->afterStmtLines); $right = $this->parseExpr($expr->expr); $rightBefore = array_slice($this->context->beforeStmtLines, $rightBeforeCount); $rightAfter = array_slice($this->context->afterStmtLines, $rightAfterCount); $this->context->beforeStmtLines = array_slice( $this->context->beforeStmtLines, 0, $rightBeforeCount, ); $this->context->afterStmtLines = array_slice( $this->context->afterStmtLines, 0, $rightAfterCount, ); if ($propertyWriteTarget !== null) { $right = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $expr->expr, $right); } if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) { $this->errorUndefinedVariable($expr->expr); } $targetClass = $var !== null && $this->isNativeObjectVar($var) ? $this->getNativeObjectVarClass($var) : $this->detectClassOfExpr($expr->var); if (($rightBefore !== [] || $rightAfter !== []) && $this->isNativeObjectClass($targetClass)) { $tmp = $this->genTmpVarName(); $pointerType = $this->getNativeObjectPointerType($targetClass); $this->addLocalVar($tmp, $pointerType); $this->addNativeObject($tmp, $targetClass); $code = '[&]() -> ' . $pointerType . ' {' . PHP_EOL; $code .= $this->getIndent() . 'if (' . $isset . ') { return ' . $var . '; }' . PHP_EOL; $code .= $this->formatCapturedStmtLines($rightBefore); $code .= $this->getIndent() . $tmp . ' = ' . $right . ';' . PHP_EOL; $code .= $this->formatCapturedStmtLines($rightAfter); $code .= $this->getIndent() . $var . ' = ' . $tmp . ';' . PHP_EOL; $code .= $this->getIndent() . 'return ' . $var . ';' . PHP_EOL; $code .= $this->getIndent() . '}()'; return $code; } $this->appendCapturedStmtLinesToContext($rightBefore); foreach ($rightAfter as $stmt) { $this->context->afterStmtLines[] = $stmt; } return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))'; } protected function getNormalAssignType(string $type): string { return $type === Type::REF || $type === Type::VOID ? Type::VAR : $type; } }