Compile a full GraphQL abstract syntax tree (AST) to intermediate representation.

    Args:
        schema: GraphQL schema object, obtained from the graphql library
        ast: the root GraphQL AST node for the query, obtained from the graphql library,
             and already validated against the schema for type-correctness
        type_equivalence_hints: optional dict of GraphQL type to equivalent GraphQL union

    Returns:
        IrAndMetadata named tuple, containing fields:
        - ir_blocks: a list of IR basic block objects
        - input_metadata: a dict of expected input parameters (string) -> inferred GraphQL type
        - output_metadata: a dict of output name (string) -> OutputMetadata object
        - location_types: a dict of location objects -> GraphQL type objects at that location
        - coerced_locations: a set of location objects indicating where type coercions have happened

def _compile_root_ast_to_ir(schema, ast, type_equivalence_hints=None):
    """Compile a full GraphQL abstract syntax tree (AST) to intermediate representation.

    Args:
        schema: GraphQL schema object, obtained from the graphql library
        ast: the root GraphQL AST node for the query, obtained from the graphql library,
             and already validated against the schema for type-correctness
        type_equivalence_hints: optional dict of GraphQL type to equivalent GraphQL union

    Returns:
        IrAndMetadata named tuple, containing fields:
        - ir_blocks: a list of IR basic block objects
        - input_metadata: a dict of expected input parameters (string) -> inferred GraphQL type
        - output_metadata: a dict of output name (string) -> OutputMetadata object
        - location_types: a dict of location objects -> GraphQL type objects at that location
        - coerced_locations: a set of location objects indicating where type coercions have happened
    """
    if len(ast.selection_set.selections) != 1:
        raise GraphQLCompilationError(u'Cannot process AST with more than one root selection!')

    base_ast = ast.selection_set.selections[0]
    base_start_type = get_ast_field_name(base_ast)  # This is the type at which querying starts.

    # Validation passed, so the base_start_type must exist as a field of the root query.
    current_schema_type = get_field_type_from_schema(schema.get_query_type(), base_start_type)

    # Construct the start location of the query and its associated metadata.
    location = Location((base_start_type,))
    base_location_info = LocationInfo(
        parent_location=None,
        type=current_schema_type,
        coerced_from_type=None,
        optional_scopes_depth=0,
        recursive_scopes_depth=0,
        is_within_fold=False,
    )
    query_metadata_table = QueryMetadataTable(location, base_location_info)

    # Default argument value is empty dict
    if not type_equivalence_hints:
        type_equivalence_hints = dict()

    # Construct the starting context object.
    context = {
        # 'metadata' is the QueryMetadataTable describing all the metadata collected during query
        # processing, including location metadata (e.g. which locations are folded or optional).
        'metadata': query_metadata_table,
        # 'tags' is a dict containing
        #  - location: Location where the tag was defined
        #  - optional: boolean representing whether the tag was defined within an @optional scope
        #  - type: GraphQLType of the tagged value
        'tags': dict(),
        # 'global_filters' is a list that may contain Filter blocks that are generated during
        # query processing, but apply to the global query scope and should be appended to the
        # IR blocks only after the GlobalOperationsStart block has been emitted.
        'global_filters': [],
        # 'outputs' is a dict mapping each output name to another dict which contains
        #  - location: Location where to output from
        #  - optional: boolean representing whether the output was defined within an @optional scope
        #  - type: GraphQLType of the output
        #  - fold: FoldScopeLocation object if the current output was defined within a fold scope,
        #          and None otherwise
        'outputs': dict(),
        # 'inputs' is a dict mapping input parameter names to their respective expected GraphQL
        # types, as automatically inferred by inspecting the query structure
        'inputs': dict(),
        # 'type_equivalence_hints' is a dict mapping GraphQL types to equivalent GraphQL unions
        'type_equivalence_hints': type_equivalence_hints,
        # 'type_equivalence_hints_inverse' is the inverse of type_equivalence_hints,
        # which is always invertible.
        'type_equivalence_hints_inverse': invert_dict(type_equivalence_hints),
    }

    # Add the query root basic block to the output.
    basic_blocks = [
        blocks.QueryRoot({base_start_type})
    ]

    # Ensure the GraphQL query root doesn't immediately have a fragment (type coercion).
    # Instead of starting at one type and coercing to another,
    # users should simply start at the type to which they are coercing.
    immediate_fragment = _get_inline_fragment(base_ast)
    if immediate_fragment is not None:
        msg_args = {
            'coerce_to': immediate_fragment.type_condition.name.value,
            'type_from': base_start_type,
        }
        raise GraphQLCompilationError(u'Found inline fragment coercing to type {coerce_to}, '
                                      u'immediately inside query root asking for type {type_from}. '
                                      u'This is a contrived pattern -- you should simply start '
                                      u'your query at {coerce_to}.'.format(**msg_args))

    # Ensure the GraphQL query root doesn't have any vertex directives
    # that are disallowed on the root node.
    validate_root_vertex_directives(base_ast)

    # Compile and add the basic blocks for the query's base AST vertex.
    new_basic_blocks = _compile_ast_node_to_ir(
        schema, current_schema_type, base_ast, location, context)
    basic_blocks.extend(new_basic_blocks)

    _validate_all_tags_are_used(context['metadata'])

    # All operations after this point affect the global query scope, and are not related to
    # the "current" location in the query produced by the sequence of Traverse/Backtrack blocks.
    basic_blocks.append(blocks.GlobalOperationsStart())

    # Add any filters that apply to the global query scope.
    basic_blocks.extend(context['global_filters'])

    # Based on the outputs context data, add an output step and construct the output metadata.
    outputs_context = context['outputs']
    basic_blocks.append(_compile_output_step(outputs_context))
    output_metadata = {
        name: OutputMetadata(type=value['type'], optional=value['optional'])
        for name, value in six.iteritems(outputs_context)
    }

    return IrAndMetadata(
        ir_blocks=basic_blocks,
        input_metadata=context['inputs'],
        output_metadata=output_metadata,
        query_metadata_table=context['metadata'])