def connect(m, *args, **kwargs):
    """Connect interface objects to each other.

    This function creates connections between ports of several interface objects. (Any number of
    interface objects may be provided; in most cases it is two.)

    The connections can be made only if all of the objects satisfy a number of requirements:

    * Every interface object must have the same set of port members, and they must have the same
      :meth:`dimensions <Member.dimensions>`.
    * For each path, the port members of every interface object must have the same width and initial
      value (for port members corresponding to signals) or constant value (for port members
      corresponding to constants). Signedness may differ.
    * For each path, at most one interface object must have the corresponding port member be
      an output.
    * For a given path, if any of the interface objects has an input port member corresponding
      to a constant value, then the rest of the interface objects must have output port members
      corresponding to the same constant value.
    * When connecting multiple interface objects, at least one connection must be made.

    For example, if :py:`obj1` is being connected to :py:`obj2` and :py:`obj3`, and :py:`obj1.a.b`
    is an output, then :py:`obj2.a.b` and :py:`obj2.a.b` must exist and be inputs. If :py:`obj2.c`
    is an input and its value is :py:`Const(1)`, then :py:`obj1.c` and :py:`obj3.c` must be outputs
    whose value is also :py:`Const(1)`. If no ports besides :py:`obj1.a.b` and :py:`obj1.c` exist,
    then no ports except for those two must exist on :py:`obj2` and :py:`obj3` either.

    Once it is determined that the interface objects can be connected, this function performs
    an equivalent of:

    .. code::

        m.d.comb += [
            in1.eq(out1),
            in2.eq(out1),
            ...
        ]

    Where :py:`out1` is an output and :py:`in1`, :py:`in2`, ... are the inputs that have the same
    path. (If no interface object has an output for a given path, **no connection at all** is made.)

    The positions (within :py:`args`) or names (within :py:`kwargs`) of the arguments do not affect
    the connections that are made. There is no difference in behavior between :py:`connect(m, a, b)`
    and :py:`connect(m, b, a)` or :py:`connect(m, arbiter=a, decoder=b)`. The names of the keyword
    arguments serve only a documentation purpose: they clarify the diagnostic messages when
    a connection cannot be made.
    """

    if not isinstance(m, Module):
        raise TypeError(f"The initial argument must be a module, not {m!r}")

    src_loc_at = 1

    objects = {
        **{index:   arg for index,   arg in enumerate(args)},
        **{keyword: arg for keyword, arg in kwargs.items()}
    }

    # Extract signatures from arguments.
    signatures = {}
    for handle, obj in objects.items():
        if not hasattr(obj, "signature"):
            raise AttributeError(f"Argument {handle!r} must have a 'signature' attribute")
        if not isinstance(obj.signature, Signature):
            raise TypeError(f"Signature of argument {handle!r} must be a signature, "
                            f"not {obj.signature!r}")
        if not obj.signature.is_compliant(obj):
            reasons = []
            obj.signature.is_compliant(obj, reasons=reasons, path=(handle,))
            reasons_as_string = "".join("\n- " + reason for reason in reasons)
            raise ConnectionError(f"Argument {handle!r} does not match its signature:" +
                                  reasons_as_string)
        signatures[handle] = obj.signature

    # Connecting zero or one signatures is OK.
    if len(signatures) <= 1:
        return

    # Collate signatures, build connections, track whether we see any input or output.
    flattens = {handle: iter(sorted(signature.members.flatten()))
                for handle, signature in signatures.items()}
    connections = []
    any_in, any_out = False, False
    # Each iteration of the outer loop is intended to connect several (usually a pair) members
    # to each other, e.g. an out member `[0].a` to an in member `[1].a`. However, because we
    # do not just check signatures for equality (in order to improve diagnostics), it is possible
    # that we will find that in `[0]`, the first member is `a`, and in `[1]`, the first member
    # is completely unrelated `[b]`. Since the assumption that all signatures are equal, or even
    # of equal length, cannot be made, it is necessary to simultaneously iterate (like with `zip`)
    # the signature of every object being connected, making sure each set of next members is
    # compliant with each other.
    while True:
        # Classify the members by kind and flow: signature, In, Out. Flow of signature members is
        # implied in the flow of each port member, so the signature members are only classified
        # here to ensure they are not connected to port members.
        is_first = True
        first_path = None
        sig_kind, out_kind, in_kind = [], [], []
        for handle, flattened_members in flattens.items():
            path_for_handle, member = next(flattened_members, (None, None))
            # First, ensure that the paths are equal (i.e. that the hierarchy matches for all of
            # the objects up to this point).
            if is_first:
                is_first = False
                first_path = path_for_handle
            else:
                first_handle = next(iter(flattens))
                if first_path != path_for_handle:
                    # The paths are inequal. It is ambiguous how exactly the diagnostic should be
                    # displayed, and the choices of which other member to use below is arbitrary.
                    # Signature members are iterated in ascending lexicographical order, so the path
                    # that sorts greater corresponds to the handle that's missing a member.
                    if (path_for_handle is None or
                            (first_path is not None and path_for_handle > first_path)):
                        first_path_as_string = _format_path(first_path)
                        raise ConnectionError(f"Member {first_path_as_string} is present in "
                                              f"{first_handle!r}, but not in {handle!r}")
                    if (first_path is None or
                            (path_for_handle is not None and path_for_handle < first_path)):
                        path_for_handle_as_string = _format_path(path_for_handle)
                        raise ConnectionError(f"Member {path_for_handle_as_string} is present in "
                                              f"{handle!r}, but not in {first_handle!r}")
                    assert False # :nocov:
            # If there is no actual member, the signature has been fully iterated through.
            # Other signatures may still have extraneous members, so continue iterating until
            # a diagnostic is returned.
            if member is None:
                continue
            # At this point we know the paths are equal, but the members can still have
            # incompliant flow, kind (signature or port), signature, or shape. Collect all of
            # these for later evaluation.
            if member.is_port:
                if member.flow == Out:
                    out_kind.append(((handle, *path_for_handle), member))
                if member.flow == In:
                    in_kind.append(((handle, *path_for_handle), member))
            if member.is_signature:
                sig_kind.append(((handle, *path_for_handle), member))
        # If there's no path and an error wasn't raised above, we're done!
        if first_path is None:
            break
        # At this point, valid possibilities are:
        # - All of the members are signature members. In this case, we move on to their contents,
        #   and ignore the signatures themselves.
        # - There are no signature members, and there is exactly one Out flow member. In this case,
        #   this member is connected to the remaining In members, of which there may be any amount.
        # All other cases must be rejected with a diagnostic.
        if sig_kind and (out_kind or in_kind):
            sig_member_paths_as_string = \
                ", ".join(_format_path(h) for h, m in sig_kind)
            port_member_paths_as_string = \
                ", ".join(_format_path(h) for h, m in out_kind + in_kind)
            raise ConnectionError(
                f"Cannot connect signature member(s) {sig_member_paths_as_string} with "
                f"port member(s) {port_member_paths_as_string}")
        if sig_kind:
            # There are no port members at this point; we're done with this path.
            continue
        # There are only port members after this point.
        any_in = any_in or bool(in_kind)
        any_out = any_out or bool(out_kind)
        is_first = True
        for (path, member) in in_kind + out_kind:
            member_shape = member.shape
            if is_first:
                is_first = False
                first_path = path
                first_member_shape = member.shape
                first_member_init = member.init
                first_member_init_as_const = member._init_as_const
                continue
            if Shape.cast(first_member_shape).width != Shape.cast(member_shape).width:
                raise ConnectionError(
                    f"Cannot connect the member {_format_path(first_path)} with shape "
                    f"{_format_shape(first_member_shape)} to the member {_format_path(path)} with "
                    f"shape {_format_shape(member_shape)} because the shape widths "
                    f"({Shape.cast(first_member_shape).width} and "
                    f"{Shape.cast(member_shape).width}) do not match")
            if first_member_init_as_const.value != member._init_as_const.value:
                raise ConnectionError(
                    f"Cannot connect together the member {_format_path(first_path)} with initial "
                    f"value {first_member_init!r} and the member {_format_path(path)} with initial "
                    f"value {member.init} because the initial values do not match")
        # If there are no Out members, there is nothing to connect. The In members, while not
        # explicitly connected, will stay at the same value since we ensured their initial values
        # are all identical.
        if len(out_kind) == 0:
            continue
        # Check that there is only one Out member. In the future we could extend connection to
        # handle wired-OR and wired-AND, and this check may go away.
        if len(out_kind) != 1:
            out_member_paths_as_string = \
                ", ".join(_format_path(h) for h, m in out_kind)
            raise ConnectionError(
                f"Cannot connect several output members {out_member_paths_as_string} together")
        # There is exactly one Out member after this point, and any amount of In members.
        # Traversing the paths to all of them should always succeed, since the signature check
        # at the beginning of `connect()` passed, and so should casting the result to a Value.
        (out_path, out_member), = out_kind
        for (in_path, in_member) in in_kind:
            def connect_value(*, out_path, in_path, src_loc_at):
                in_value = Value.cast(_traverse_path(in_path, objects))
                out_value = Value.cast(_traverse_path(out_path, objects))
                assert type(in_value) in (Const, Signal)
                # If the input is a constant, only a constant may be connected to it. Ensure that
                # this is the case.
                if type(in_value) is Const:
                    # If the output is not a constant, the connection is illegal.
                    if type(out_value) is not Const:
                        raise ConnectionError(
                            f"Cannot connect input member {_format_path(in_path)} that has "
                            f"a constant value {in_value.value!r} to an output member "
                            f"{_format_path(out_path)} that has a varying value")
                    # If the output is a constant, the connection is legal only if the value is
                    # the same for both the input and the output.
                    if type(out_value) is Const and in_value.value != out_value.value:
                        raise ConnectionError(
                            f"Cannot connect input member {_format_path(in_path)} that has "
                            f"a constant value {in_value.value!r} to an output member "
                            f"{_format_path(out_path)} that has a different constant value "
                            f"{out_value.value!r}")
                    # We never actually connect anything to the constant input; we only ensure its
                    # value (which is constant) is consistent with a connection that would have
                    # been made.
                    return
                # A connection that is made at this point is guaranteed to be valid.
                connections.append(in_value.eq(out_value, src_loc_at=src_loc_at + 1))
            def connect_dimensions(dimensions, *, out_path, in_path, src_loc_at):
                if not dimensions:
                    return connect_value(out_path=out_path, in_path=in_path, src_loc_at=src_loc_at)
                dimension, *rest_of_dimensions = dimensions
                for index in range(dimension):
                    connect_dimensions(rest_of_dimensions,
                        out_path=(*out_path, index), in_path=(*in_path, index),
                        src_loc_at=src_loc_at + 1)
            assert out_member.dimensions == in_member.dimensions
            connect_dimensions(out_member.dimensions,
                out_path=out_path, in_path=in_path, src_loc_at=src_loc_at + 1)

    # If no connections were made, and there were inputs but no outputs in the
    # signatures, issue a diagnostic as this is most likely in error.
    if len(connections) == 0 and any_in and not any_out:
        raise ConnectionError(f"Only input to input connections have been made between several "
                              f"interfaces; should one of them have been flipped?")


    # Now that we know all of the connections are legal, add them to the module. This is done
    # instead of returning them because adding them to a non-comb domain would subtly violate
    # assumptions that `connect()` is intended to provide.
    m.d.comb += connections
Connect interface objects to each other.

    This function creates connections between ports of several interface objects. (Any number of
    interface objects may be provided; in most cases it is two.)

    The connections can be made only if all of the objects satisfy a number of requirements:

    * Every interface object must have the same set of port members, and they must have the same
      :meth:`dimensions <Member.dimensions>`.
    * For each path, the port members of every interface object must have the same width and initial
      value (for port members corresponding to signals) or constant value (for port members
      corresponding to constants). Signedness may differ.
    * For each path, at most one interface object must have the corresponding port member be
      an output.
    * For a given path, if any of the interface objects has an input port member corresponding
      to a constant value, then the rest of the interface objects must have output port members
      corresponding to the same constant value.
    * When connecting multiple interface objects, at least one connection must be made.

    For example, if :py:`obj1` is being connected to :py:`obj2` and :py:`obj3`, and :py:`obj1.a.b`
    is an output, then :py:`obj2.a.b` and :py:`obj2.a.b` must exist and be inputs. If :py:`obj2.c`
    is an input and its value is :py:`Const(1)`, then :py:`obj1.c` and :py:`obj3.c` must be outputs
    whose value is also :py:`Const(1)`. If no ports besides :py:`obj1.a.b` and :py:`obj1.c` exist,
    then no ports except for those two must exist on :py:`obj2` and :py:`obj3` either.

    Once it is determined that the interface objects can be connected, this function performs
    an equivalent of:

    .. code::

        m.d.comb += [
            in1.eq(out1),
            in2.eq(out1),
            ...
        ]

    Where :py:`out1` is an output and :py:`in1`, :py:`in2`, ... are the inputs that have the same
    path. (If no interface object has an output for a given path, **no connection at all** is made.)

    The positions (within :py:`args`) or names (within :py:`kwargs`) of the arguments do not affect
    the connections that are made. There is no difference in behavior between :py:`connect(m, a, b)`
    and :py:`connect(m, b, a)` or :py:`connect(m, arbiter=a, decoder=b)`. The names of the keyword
    arguments serve only a documentation purpose: they clarify the diagnostic messages when
    a connection cannot be made.
    
amaranth/lib/wiring.py