diff --git a/lib/JIT.php b/lib/JIT.php index fc955a5ecdd..079507fac90 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -8550,6 +8550,7 @@ private function compileBlockInternal( case OpCode::TYPE_ARRAY_DIM_FETCH_WRITE: $forWrite = OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $op->type; $fetchIs = !$forWrite && $op->arrayDimFetchIs; + $warnUndefKeyIncDec = $forWrite && $this->varFetchDestUsedAsIncDec($block, $i, (int) $op->arg1); $value = $this->context->getVariableFromOp($block->getOperand($op->arg2)); // Zend: E_NOTICE + continue on non-object __get temp (#29231, re-#4673). if ( @@ -8888,7 +8889,7 @@ private function compileBlockInternal( break; } } - $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite, $emitFloatKeyDeprecation); + $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite, $emitFloatKeyDeprecation, $warnUndefKeyIncDec); if ($forWrite) { $this->context->setVariableOp($resultOp, $fetched); } elseif ($forceBranchMerge) { @@ -8936,7 +8937,7 @@ private function compileBlockInternal( break; } } - $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite, $emitFloatKeyDeprecation); + $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite, $emitFloatKeyDeprecation, $warnUndefKeyIncDec); if ($forWrite) { $this->context->setVariableOp($resultOp, $fetched); } elseif ($forceBranchMerge) { @@ -8980,7 +8981,7 @@ private function compileBlockInternal( Variable::KIND_VALUE, JIT\JitValueBox::alloc($this->context) ); - $fetched = $boxed->dimFetch($dim, $resultOp->type, $forWrite); + $fetched = $boxed->dimFetch($dim, $resultOp->type, $forWrite, true, $warnUndefKeyIncDec); if ($forWrite) { $this->context->setVariableOp($resultOp, $fetched); } elseif ($forceBranchMerge) { @@ -9024,7 +9025,7 @@ private function compileBlockInternal( $this->context->constantFromInteger($value->nextFreeElement) ); } - $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite); + $fetched = $value->dimFetch($dim, $resultOp->type, $forWrite, true, $warnUndefKeyIncDec); if ($forceBranchMerge && !$forWrite) { $this->assignOperand($resultOp, $fetched, true); } else { diff --git a/lib/JIT/HashTableHelper.php b/lib/JIT/HashTableHelper.php index 232c0938fef..f710d1444d9 100644 --- a/lib/JIT/HashTableHelper.php +++ b/lib/JIT/HashTableHelper.php @@ -125,6 +125,12 @@ public static function offsetIsSetDim(Context $context, Value $ht, Variable $dim return HashTableReadLlvm::offsetIsSetDim($context, $ht, $dim); } + /** ++/-- FETCH_DIM_W: warn when key missing (#30078). */ + public static function emitUndefinedArrayKeyWarningIfMissing(Context $context, Value $ht, Variable $dim): void + { + HashTableReadLlvm::emitUndefinedArrayKeyWarningIfMissing($context, $ht, $dim); + } + /** * Read an element into a stack {@see __value__} slot (string/int/object/boxed keys; issue #86). * diff --git a/lib/JIT/HashTableReadLlvm.php b/lib/JIT/HashTableReadLlvm.php index 6eb83bc8c54..a1643bd0f99 100644 --- a/lib/JIT/HashTableReadLlvm.php +++ b/lib/JIT/HashTableReadLlvm.php @@ -621,6 +621,130 @@ static function (Value $index) use ($context, $ht, $slot, $done): void { ); } + /** + * Zend E_WARNING when ++/-- FETCH_DIM_W reads a missing key (#30078, zend_vm_def.h). + */ + public static function emitUndefinedArrayKeyWarningIfMissing(Context $context, Value $ht, Variable $dim): void + { + $exists = self::offsetIsSetDim($context, $ht, $dim); + $tag = 'diuw'.(string) self::nextSeq(); + $hasKey = BasicBlockHelper::append($context, 'dim_inc_ukey_has_'.$tag); + $missKey = BasicBlockHelper::append($context, 'dim_inc_ukey_miss_'.$tag); + $done = BasicBlockHelper::append($context, 'dim_inc_ukey_done_'.$tag); + $context->builder->branchIf($exists, $hasKey, $missKey); + $context->builder->positionAtEnd($missKey); + self::emitUndefinedArrayKeyWarningForDim($context, $dim); + $context->builder->branch($done); + $context->builder->positionAtEnd($hasKey); + $context->builder->branch($done); + $context->builder->positionAtEnd($done); + } + + /** Emit {@see __compiler_undefined_array_key_warning_*} for a dim operand (#30078). */ + private static function emitUndefinedArrayKeyWarningForDim(Context $context, Variable $dim): void + { + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); + \PHPCompiler\ext\standard\StringTriggerErrorJit::implement($context); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } else { + BasicBlockHelper::ensureOpenInsertBlock($context, 'dim_inc_ukey_warn_setup'); + } + + if (Variable::TYPE_NATIVE_LONG === $dim->type) { + $context->builder->call( + $context->lookupFunction('__compiler_undefined_array_key_warning_long'), + $context->helper->loadValue($dim) + ); + + return; + } + if (Variable::TYPE_STRING === $dim->type) { + $keyStr = $context->helper->loadValue($dim); + $strMap = $context->structFieldMap['__string__']; + $i8p = $context->getTypeFromString('int8*'); + $keyLen = $context->builder->load($context->builder->structGep($keyStr, $strMap['length'])); + $keyBytes = $context->builder->structGep($keyStr, $strMap['value']); + $keyCStr = $context->builder->pointerCast($keyBytes, $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_undefined_array_key_warning_cstr'), + $keyCStr, + $keyLen + ); + + return; + } + if (Variable::TYPE_VALUE === $dim->type) { + $valPtr = self::valuePtrFromDim($context, $dim); + $valueMap = $context->structFieldMap['__value__']; + $i8 = $context->getTypeFromString('int8'); + $typeByte = $context->builder->load( + $context->builder->structGep($valPtr, $valueMap['type']) + ); + $fn = $context->builder->getInsertBlock()->getParent(); + $strBb = $fn->appendBasicBlock('dim_inc_ukey_str'); + $longBb = $fn->appendBasicBlock('dim_inc_ukey_long'); + $doneBb = $fn->appendBasicBlock('dim_inc_ukey_warn_done'); + $afterStr = $fn->appendBasicBlock('dim_inc_ukey_after_str'); + $context->builder->branchIf( + $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_STRING, false) + ), + $strBb, + $afterStr + ); + $context->builder->positionAtEnd($strBb); + $keyStr = $context->builder->call($context->lookupFunction('__value__readString'), $valPtr); + $strMap = $context->structFieldMap['__string__']; + $keyLen = $context->builder->load($context->builder->structGep($keyStr, $strMap['length'])); + $keyBytes = $context->builder->structGep($keyStr, $strMap['value']); + $i8p = $context->getTypeFromString('int8*'); + $keyCStr = $context->builder->pointerCast($keyBytes, $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_undefined_array_key_warning_cstr'), + $keyCStr, + $keyLen + ); + $context->builder->branch($doneBb); + $context->builder->positionAtEnd($afterStr); + $context->builder->branchIf( + $context->builder->icmp( + Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_NATIVE_LONG, false) + ), + $longBb, + $doneBb + ); + $context->builder->positionAtEnd($longBb); + $longKey = $context->builder->call($context->lookupFunction('__value__readLong'), $valPtr); + $context->builder->call( + $context->lookupFunction('__compiler_undefined_array_key_warning_long'), + $longKey + ); + $context->builder->branch($doneBb); + $context->builder->positionAtEnd($doneBb); + + return; + } + if (Variable::TYPE_NULL === $dim->type) { + DynamicPropertyDeprecationGuard::emitNullArrayOffset($context); + $emptyKey = $context->builder->load($context->constantStringFromString('')); + $strMap = $context->structFieldMap['__string__']; + $i8p = $context->getTypeFromString('int8*'); + $keyLen = $context->builder->load($context->builder->structGep($emptyKey, $strMap['length'])); + $keyBytes = $context->builder->structGep($emptyKey, $strMap['value']); + $keyCStr = $context->builder->pointerCast($keyBytes, $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_undefined_array_key_warning_cstr'), + $keyCStr, + $keyLen + ); + } + } + /** * Read an element into a stack {@see __value__} slot (string/int/object/boxed keys; #10031 v4). * diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index d7493dfd2e7..178ef85f423 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -984,7 +984,8 @@ public function dimFetch( self $dim, ?Type $expectedType = null, bool $forWrite = false, - bool $emitFloatKeyDeprecation = true + bool $emitFloatKeyDeprecation = true, + bool $warnUndefinedKeyForIncDec = false ): Variable { switch ($this->type) { case self::TYPE_STRING: @@ -1076,6 +1077,10 @@ public function dimFetch( $ht = HashTableHelper::loadHashtablePointer($this->context, $container); if (self::TYPE_VALUE === $dim->type) { if ($forWrite) { + if ($warnUndefinedKeyForIncDec) { + HashTableHelper::emitUndefinedArrayKeyWarningIfMissing($this->context, $ht, $dim); + } + return HashTableHelper::prepareValueBoxKeyWrite($this->context, $ht, $dim); } @@ -1103,6 +1108,10 @@ public function dimFetch( if (self::TYPE_STRING === $dim->type) { $key = $this->context->helper->loadValue($dim); if ($forWrite && (null === $expectedType || Type::TYPE_ARRAY !== $expectedType->type)) { + if ($warnUndefinedKeyForIncDec) { + HashTableHelper::emitUndefinedArrayKeyWarningIfMissing($this->context, $ht, $dim); + } + return HashTableHelper::prepareStringKeyWrite($this->context, $ht, $key); } if ('_FILES' === $container->superglobalName && !$forWrite) { @@ -1153,6 +1162,10 @@ public function dimFetch( // must return the live child HT so the inner write persists (#24011; string keys // already branch on TYPE_ARRAY above — zend_execute.c ZEND_FETCH_DIM_W). if ($forWrite && (null === $expectedType || Type::TYPE_ARRAY !== $expectedType->type)) { + if ($warnUndefinedKeyForIncDec) { + HashTableHelper::emitUndefinedArrayKeyWarningIfMissing($this->context, $ht, $dim); + } + return HashTableHelper::prepareIndexWrite($this->context, $ht, $index); } if ($forWrite && null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) { @@ -1211,7 +1224,7 @@ public function dimFetch( ); $htVar->borrowedHashtable = true; - return $htVar->dimFetch($dim, $expectedType, $forWrite); + return $htVar->dimFetch($dim, $expectedType, $forWrite, $emitFloatKeyDeprecation, $warnUndefinedKeyForIncDec); default: if (!($this->type & self::IS_NATIVE_ARRAY)) { throw new \LogicException("Unsupported dim fetch on " . self::getStringType($this->type)); diff --git a/lib/VM.php b/lib/VM.php index c07812670cf..7cd8fbbf088 100644 --- a/lib/VM.php +++ b/lib/VM.php @@ -5385,7 +5385,12 @@ static function () use ($arg3, $writeTarget, $dnfCtx, $strict): void { } $table = $container->toArray(); try { - if (!$forWrite && !$fetchIs && !$table->keyExists($arg3, false, $frame, false)) { + // ++/-- FETCH_DIM_W: warn on missing key then treat as null (#30078, zend_vm_def.h). + $forIncDec = $forWrite && $this->propertyFetchDestUsedAsIncDec($frame, $op); + if ( + (!$forWrite && !$fetchIs || $forIncDec) + && !$table->keyExists($arg3, false, $frame, false) + ) { $this->context->errors->undefinedArrayKey( $arg3, $this->context, diff --git a/test/compliance/cases/language/dim_inc_undefined_array_key_warning.phpt b/test/compliance/cases/language/dim_inc_undefined_array_key_warning.phpt new file mode 100644 index 00000000000..83f81adcca2 --- /dev/null +++ b/test/compliance/cases/language/dim_inc_undefined_array_key_warning.phpt @@ -0,0 +1,33 @@ +--TEST-- +Language: $a[missing]++ emits Undefined array key Warning then stores 1 (#30078, zend_vm_def.h) +--FILE-- + 1, +) +W:Automatic conversion of false to array is deprecated +W:Undefined array key 0 +array ( + 0 => 1, +)