src/Experimental/Executor/CoroutineExecutor.php
private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
    {
        $nonNull     = false;
        $returnValue = null;

        if ($type instanceof NonNull) {
            $nonNull = true;
            $type    = $type->getWrappedType();
        } else {
            $nullFence = $path;
        }

        // !!! $value might be promise, yield to resolve
        try {
            if ($this->promiseAdapter->isThenable($value)) {
                $value = yield $value;
            }
        } catch (Throwable $reason) {
            $this->addError(Error::createLocatedError(
                $reason,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($value === null) {
            $returnValue = $value;
            goto CHECKED_RETURN;
        } elseif ($value instanceof Throwable) {
            // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
            $this->addError(Error::createLocatedError(
                $value,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($type instanceof ListOfType) {
            $returnValue = [];
            $index       = -1;
            $itemType    = $type->getWrappedType();
            foreach ($value as $itemValue) {
                ++$index;

                $itemPath   = $path;
                $itemPath[] = $index; // !!! use arrays COW semantics

                try {
                    if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
                        $itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
                    }
                } catch (Throwable $reason) {
                    $this->addError(Error::createLocatedError(
                        $reason,
                        $ctx->shared->fieldNodes,
                        $itemPath
                    ));
                    $itemReturnValue = null;
                }
                if ($itemReturnValue === self::$undefined) {
                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }
                $returnValue[$index] = $itemReturnValue;
            }

            goto CHECKED_RETURN;
        } else {
            if ($type !== $this->schema->getType($type->name)) {
                $hint = '';
                if ($this->schema->getConfig()->typeLoader) {
                    $hint = sprintf(
                        'Make sure that type loader returns the same instance as defined in %s.%s',
                        $ctx->type,
                        $ctx->shared->fieldName
                    );
                }
                $this->addError(Error::createLocatedError(
                    new InvariantViolation(
                        sprintf(
                            'Schema must contain unique named types but contains multiple types named "%s". %s ' .
                            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                            $type->name,
                            $hint
                        )
                    ),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }

            if ($type instanceof LeafType) {
                try {
                    $returnValue = $type->serialize($value);
                } catch (Throwable $error) {
                    $this->addError(Error::createLocatedError(
                        new InvariantViolation(
                            'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
                            0,
                            $error
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));
                    $returnValue = null;
                }
                goto CHECKED_RETURN;
            } elseif ($type instanceof CompositeType) {
                /** @var ObjectType|null $objectType */
                $objectType = null;
                if ($type instanceof InterfaceType || $type instanceof UnionType) {
                    $objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);

                    if ($objectType === null) {
                        $objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
                    }

                    // !!! $objectType->resolveType() might return promise, yield to resolve
                    $objectType = yield $objectType;
                    if (is_string($objectType)) {
                        $objectType = $this->schema->getType($objectType);
                    }

                    if ($objectType === null) {
                        $this->addError(Error::createLocatedError(
                            sprintf(
                                'Composite type "%s" did not resolve concrete object type for value: %s.',
                                $type->name,
                                Utils::printSafe($value)
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = self::$undefined;
                        goto CHECKED_RETURN;
                    } elseif (! $objectType instanceof ObjectType) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Abstract type %s must resolve to an Object type at ' .
                                'runtime for field %s.%s with value "%s", received "%s". ' .
                                'Either the %s type should provide a "resolveType" ' .
                                'function or each possible type should provide an "isTypeOf" function.',
                                $type,
                                $ctx->resolveInfo->parentType,
                                $ctx->resolveInfo->fieldName,
                                Utils::printSafe($value),
                                Utils::printSafe($objectType),
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif (! $this->schema->isPossibleType($type, $objectType)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Runtime Object type "%s" is not a possible type for "%s".',
                                $objectType,
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif ($objectType !== $this->schema->getType($objectType->name)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(
                                sprintf(
                                    'Schema must contain unique named types but contains multiple types named "%s". ' .
                                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
                                    'type instance as referenced anywhere else within the schema ' .
                                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                                    $objectType,
                                    $type
                                )
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                } elseif ($type instanceof ObjectType) {
                    $objectType = $type;
                } else {
                    $this->addError(Error::createLocatedError(
                        sprintf(
                            'Unexpected field type "%s".',
                            Utils::printSafe($type)
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));

                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }

                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
                if ($typeCheck !== null) {
                    // !!! $objectType->isTypeOf() might return promise, yield to resolve
                    $typeCheck = yield $typeCheck;
                    if (! $typeCheck) {
                        $this->addError(Error::createLocatedError(
                            sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                }

                $returnValue = new stdClass();

                if ($ctx->shared->typeGuard2 === $objectType) {
                    foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
                        $childCtx              = clone $childCtx;
                        $childCtx->type        = $objectType;
                        $childCtx->value       = $value;
                        $childCtx->result      = $returnValue;
                        $childCtx->path        = $path;
                        $childCtx->path[]      = $childCtx->shared->resultName; // !!! uses array COW semantics
                        $childCtx->nullFence   = $nullFence;
                        $childCtx->resolveInfo = null;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childCtx->shared->resultName} = null;
                    }
                } else {
                    $childContexts = [];

                    foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
                        /** @var CoroutineContextShared $childShared */

                        $childPath   = $path;
                        $childPath[] = $childShared->resultName; // !!! uses array COW semantics
                        $childCtx    = new CoroutineContext(
                            $childShared,
                            $objectType,
                            $value,
                            $returnValue,
                            $childPath,
                            $nullFence
                        );

                        $childContexts[] = $childCtx;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childShared->resultName} = null;
                    }

                    $ctx->shared->typeGuard2           = $objectType;
                    $ctx->shared->childContextsIfType2 = $childContexts;
                }

                goto CHECKED_RETURN;
            } else {
                $this->addError(Error::createLocatedError(
                    sprintf('Unhandled type "%s".', Utils::printSafe($type)),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }
        }

        CHECKED_RETURN:
        if ($nonNull && $returnValue === null) {
            $this->addError(Error::createLocatedError(
                new InvariantViolation(sprintf(
                    'Cannot return null for non-nullable field %s.%s.',
                    $ctx->type->name,
                    $ctx->shared->fieldName
                )),
                $ctx->shared->fieldNodes,
                $path
            ));

            return self::$undefined;
        }

        return $returnValue;
    }
private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
    {
        $nonNull     = false;
        $returnValue = null;

        if ($type instanceof NonNull) {
            $nonNull = true;
            $type    = $type->getWrappedType();
        } else {
            $nullFence = $path;
        }

        // !!! $value might be promise, yield to resolve
        try {
            if ($this->promiseAdapter->isThenable($value)) {
                $value = yield $value;
            }
        } catch (Throwable $reason) {
            $this->addError(Error::createLocatedError(
                $reason,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($value === null) {
            $returnValue = $value;
            goto CHECKED_RETURN;
        } elseif ($value instanceof Throwable) {
            // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
            $this->addError(Error::createLocatedError(
                $value,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($type instanceof ListOfType) {
            $returnValue = [];
            $index       = -1;
            $itemType    = $type->getWrappedType();
            foreach ($value as $itemValue) {
                ++$index;

                $itemPath   = $path;
                $itemPath[] = $index; // !!! use arrays COW semantics

                try {
                    if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
                        $itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
                    }
                } catch (Throwable $reason) {
                    $this->addError(Error::createLocatedError(
                        $reason,
                        $ctx->shared->fieldNodes,
                        $itemPath
                    ));
                    $itemReturnValue = null;
                }
                if ($itemReturnValue === self::$undefined) {
                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }
                $returnValue[$index] = $itemReturnValue;
            }

            goto CHECKED_RETURN;
        } else {
            if ($type !== $this->schema->getType($type->name)) {
                $hint = '';
                if ($this->schema->getConfig()->typeLoader) {
                    $hint = sprintf(
                        'Make sure that type loader returns the same instance as defined in %s.%s',
                        $ctx->type,
                        $ctx->shared->fieldName
                    );
                }
                $this->addError(Error::createLocatedError(
                    new InvariantViolation(
                        sprintf(
                            'Schema must contain unique named types but contains multiple types named "%s". %s ' .
                            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                            $type->name,
                            $hint
                        )
                    ),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }

            if ($type instanceof LeafType) {
                try {
                    $returnValue = $type->serialize($value);
                } catch (Throwable $error) {
                    $this->addError(Error::createLocatedError(
                        new InvariantViolation(
                            'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
                            0,
                            $error
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));
                    $returnValue = null;
                }
                goto CHECKED_RETURN;
            } elseif ($type instanceof CompositeType) {
                /** @var ObjectType|null $objectType */
                $objectType = null;
                if ($type instanceof InterfaceType || $type instanceof UnionType) {
                    $objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);

                    if ($objectType === null) {
                        $objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
                    }

                    // !!! $objectType->resolveType() might return promise, yield to resolve
                    $objectType = yield $objectType;
                    if (is_string($objectType)) {
                        $objectType = $this->schema->getType($objectType);
                    }

                    if ($objectType === null) {
                        $this->addError(Error::createLocatedError(
                            sprintf(
                                'Composite type "%s" did not resolve concrete object type for value: %s.',
                                $type->name,
                                Utils::printSafe($value)
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = self::$undefined;
                        goto CHECKED_RETURN;
                    } elseif (! $objectType instanceof ObjectType) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Abstract type %s must resolve to an Object type at ' .
                                'runtime for field %s.%s with value "%s", received "%s". ' .
                                'Either the %s type should provide a "resolveType" ' .
                                'function or each possible type should provide an "isTypeOf" function.',
                                $type,
                                $ctx->resolveInfo->parentType,
                                $ctx->resolveInfo->fieldName,
                                Utils::printSafe($value),
                                Utils::printSafe($objectType),
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif (! $this->schema->isPossibleType($type, $objectType)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Runtime Object type "%s" is not a possible type for "%s".',
                                $objectType,
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif ($objectType !== $this->schema->getType($objectType->name)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(
                                sprintf(
                                    'Schema must contain unique named types but contains multiple types named "%s". ' .
                                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
                                    'type instance as referenced anywhere else within the schema ' .
                                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                                    $objectType,
                                    $type
                                )
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                } elseif ($type instanceof ObjectType) {
                    $objectType = $type;
                } else {
                    $this->addError(Error::createLocatedError(
                        sprintf(
                            'Unexpected field type "%s".',
                            Utils::printSafe($type)
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));

                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }

                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
                if ($typeCheck !== null) {
                    // !!! $objectType->isTypeOf() might return promise, yield to resolve
                    $typeCheck = yield $typeCheck;
                    if (! $typeCheck) {
                        $this->addError(Error::createLocatedError(
                            sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                }

                $returnValue = new stdClass();

                if ($ctx->shared->typeGuard2 === $objectType) {
                    foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
                        $childCtx              = clone $childCtx;
                        $childCtx->type        = $objectType;
                        $childCtx->value       = $value;
                        $childCtx->result      = $returnValue;
                        $childCtx->path        = $path;
                        $childCtx->path[]      = $childCtx->shared->resultName; // !!! uses array COW semantics
                        $childCtx->nullFence   = $nullFence;
                        $childCtx->resolveInfo = null;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childCtx->shared->resultName} = null;
                    }
                } else {
                    $childContexts = [];

                    foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
                        /** @var CoroutineContextShared $childShared */

                        $childPath   = $path;
                        $childPath[] = $childShared->resultName; // !!! uses array COW semantics
                        $childCtx    = new CoroutineContext(
                            $childShared,
                            $objectType,
                            $value,
                            $returnValue,
                            $childPath,
                            $nullFence
                        );

                        $childContexts[] = $childCtx;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childShared->resultName} = null;
                    }

                    $ctx->shared->typeGuard2           = $objectType;
                    $ctx->shared->childContextsIfType2 = $childContexts;
                }

                goto CHECKED_RETURN;
            } else {
                $this->addError(Error::createLocatedError(
                    sprintf('Unhandled type "%s".', Utils::printSafe($type)),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }
        }

        CHECKED_RETURN:
        if ($nonNull && $returnValue === null) {
            $this->addError(Error::createLocatedError(
                new InvariantViolation(sprintf(
                    'Cannot return null for non-nullable field %s.%s.',
                    $ctx->type->name,
                    $ctx->shared->fieldName
                )),
                $ctx->shared->fieldNodes,
                $path
            ));

            return self::$undefined;
        }

        return $returnValue;
    }
@param mixed         $value
@param string[]      $path
@param string[]|null $nullFence

@return mixed