Lower the IR into an IR form that can be represented in MATCH queries.

    Args:
        ir_blocks: list of IR blocks to lower into MATCH-compatible form
        query_metadata_table: QueryMetadataTable object containing all metadata collected during
                              query processing, including location metadata (e.g. which locations
                              are folded or optional).
        type_equivalence_hints: optional dict of GraphQL interface or type -> GraphQL union.
                                Used as a workaround for GraphQL's lack of support for
                                inheritance across "types" (i.e. non-interfaces), as well as a
                                workaround for Gremlin's total lack of inheritance-awareness.
                                The key-value pairs in the dict specify that the "key" type
                                is equivalent to the "value" type, i.e. that the GraphQL type or
                                interface in the key is the most-derived common supertype
                                of every GraphQL type in the "value" GraphQL union.
                                Recursive expansion of type equivalence hints is not performed,
                                and only type-level correctness of this argument is enforced.
                                See README.md for more details on everything this parameter does.
                                *****
                                Be very careful with this option, as bad input here will
                                lead to incorrect output queries being generated.
                                *****

    Returns:
        MatchQuery object containing the IR blocks organized in a MATCH-like structure

def lower_ir(ir_blocks, query_metadata_table, type_equivalence_hints=None):
    """Lower the IR into an IR form that can be represented in MATCH queries.

    Args:
        ir_blocks: list of IR blocks to lower into MATCH-compatible form
        query_metadata_table: QueryMetadataTable object containing all metadata collected during
                              query processing, including location metadata (e.g. which locations
                              are folded or optional).
        type_equivalence_hints: optional dict of GraphQL interface or type -> GraphQL union.
                                Used as a workaround for GraphQL's lack of support for
                                inheritance across "types" (i.e. non-interfaces), as well as a
                                workaround for Gremlin's total lack of inheritance-awareness.
                                The key-value pairs in the dict specify that the "key" type
                                is equivalent to the "value" type, i.e. that the GraphQL type or
                                interface in the key is the most-derived common supertype
                                of every GraphQL type in the "value" GraphQL union.
                                Recursive expansion of type equivalence hints is not performed,
                                and only type-level correctness of this argument is enforced.
                                See README.md for more details on everything this parameter does.
                                *****
                                Be very careful with this option, as bad input here will
                                lead to incorrect output queries being generated.
                                *****

    Returns:
        MatchQuery object containing the IR blocks organized in a MATCH-like structure
    """
    sanity_check_ir_blocks_from_frontend(ir_blocks, query_metadata_table)

    # Construct the mapping of each location to its corresponding GraphQL type.
    location_types = {
        location: location_info.type
        for location, location_info in query_metadata_table.registered_locations
    }

    # Compute the set of all locations that have associated type coercions.
    coerced_locations = {
        location
        for location, location_info in query_metadata_table.registered_locations
        if location_info.coerced_from_type is not None
    }

    # Extract information for both simple and complex @optional traverses
    location_to_optional_results = extract_optional_location_root_info(ir_blocks)
    complex_optional_roots, location_to_optional_roots = location_to_optional_results
    simple_optional_root_info = extract_simple_optional_location_info(
        ir_blocks, complex_optional_roots, location_to_optional_roots)
    ir_blocks = remove_end_optionals(ir_blocks)

    # Append global operation block(s) to filter out incorrect results
    # from simple optional match traverses (using a WHERE statement)
    if len(simple_optional_root_info) > 0:
        where_filter_predicate = construct_where_filter_predicate(
            query_metadata_table, simple_optional_root_info)
        ir_blocks.insert(-1, GlobalOperationsStart())
        ir_blocks.insert(-1, Filter(where_filter_predicate))

    # These lowering / optimization passes work on IR blocks.
    ir_blocks = lower_context_field_existence(ir_blocks, query_metadata_table)
    ir_blocks = optimize_boolean_expression_comparisons(ir_blocks)
    ir_blocks = rewrite_binary_composition_inside_ternary_conditional(ir_blocks)
    ir_blocks = merge_consecutive_filter_clauses(ir_blocks)
    ir_blocks = lower_has_substring_binary_compositions(ir_blocks)
    ir_blocks = orientdb_eval_scheduling.workaround_lowering_pass(ir_blocks, query_metadata_table)

    # Here, we lower from raw IR blocks into a MatchQuery object.
    # From this point on, the lowering / optimization passes work on the MatchQuery representation.
    match_query = convert_to_match_query(ir_blocks)

    match_query = lower_comparisons_to_between(match_query)

    match_query = lower_backtrack_blocks(match_query, location_types)
    match_query = truncate_repeated_single_step_traversals(match_query)
    match_query = orientdb_class_with_while.workaround_type_coercions_in_recursions(match_query)

    # Optimize and lower the IR blocks inside @fold scopes.
    new_folds = {
        key: merge_consecutive_filter_clauses(
            remove_backtrack_blocks_from_fold(
                lower_folded_coerce_types_into_filter_blocks(folded_ir_blocks)
            )
        )
        for key, folded_ir_blocks in six.iteritems(match_query.folds)
    }
    match_query = match_query._replace(folds=new_folds)

    compound_match_query = convert_optional_traversals_to_compound_match_query(
        match_query, complex_optional_roots, location_to_optional_roots)
    compound_match_query = prune_non_existent_outputs(compound_match_query)
    compound_match_query = collect_filters_to_first_location_occurrence(compound_match_query)
    compound_match_query = lower_context_field_expressions(compound_match_query)

    compound_match_query = truncate_repeated_single_step_traversals_in_sub_queries(
        compound_match_query)
    compound_match_query = orientdb_query_execution.expose_ideal_query_execution_start_points(
        compound_match_query, location_types, coerced_locations)

    return compound_match_query