def METHOD_NAME(instructions, cache=None):
    """Merge indices across an instruction stream.
    Indices are candidates for merging if they have the same extent as
    an already seen index in the instruction stream, and appear at the
    same level of the loop nest.
    :arg instructions:  Iterable of nodes to merge indices across.
    :returns: a memoized callable suitable for index merging.
    """
    if cache is None:
        cache = {}
    appeared = {}
    subst = []
    index_replacer = Memoizer(replace_indices)
    for insn in instructions:
        if isinstance(insn, FunctionCall):
            continue
        indices = tuple(i for i in collect_indices([insn]))
        runtime = tuple(i for i in indices if not isinstance(i, Index))
        free = tuple(i for i in indices if isinstance(i, Index))
        indices = runtime + free
        key = runtime + tuple(i.extent for i in free)
        full_key = key
        # Look for matching key prefix
        while key not in cache and len(key):
            key = key[:-1]
        if key in cache:
            new_indices = cache[key] + indices[len(key):]
        else:
            new_indices = indices
        for i in range(len(key), len(full_key) + 1):
            cache[full_key[:i]] = new_indices[:i]
        for i, ni in zip(indices, new_indices):
            if i in appeared:
                if isinstance(i, (Index)) and i.extent != 1 or isinstance(i, (RuntimeIndex)):
                    subst.append((i, appeared[i]))
            if i != ni:
                if i in appeared:
                    assert appeared[i] == ni
                appeared[i] = ni
                if isinstance(i, (Index)) and i.extent != 1 or isinstance(i, (RuntimeIndex)):
                    subst.append((i, ni))
    index_replacer.subst = dict(subst)
    return index_replacer