def _assign_names(self, fragment: Fragment, hierarchy: "tuple[str]"):
        """Assign names to signals and IO ports used in a given fragment, as well as its
        subfragments.

        The signal mapping is set in ``self.signal_names``, and the IO port mapping is set in
        ``self.io_port_names``.  Because names are deduplicated using local information only,
        the same signal used in a different fragment may get a different name.

        Subfragments may not necessarily have a name. This method assigns every such subfragment
        a name, ``U$<number>``, where ``<number>`` is based on its location in the hierarchy.

        Subfragment names may collide with signal names safely in Amaranth, but this may confuse
        backends. This method assigns every such subfragment a new name.

        Arguments
        ---------
        hierarchy : tuple of str
            Name of this fragment.
        """

        frag_info = self.fragments[fragment]
        frag_info.name = hierarchy

        if fragment.origins is not None:
            for origin in fragment.origins:
                if origin in self.elaboratables:
                    other_hierarchy = self.fragments[self.elaboratables[origin]].name
                    raise DuplicateElaboratable(f"Elaboratable {origin!r} is included twice "
                                                f"in the hierarchy, as {'.'.join(other_hierarchy)} "
                                                f"and {'.'.join(hierarchy)}")
                self.elaboratables[origin] = fragment

        if fragment is self.fragment:
            # Reserve names for top-level ports. If equal to the signal name, let the signal share it.
            for name, conn, _dir in self.ports:
                frag_info.assigned_names.add(name)
                if isinstance(conn, _ast.Signal) and conn.name == name:
                    frag_info.signal_names[conn] = name
                elif isinstance(conn, _ast.IOPort) and conn.name == name:
                    frag_info.io_port_names[conn] = name

        for signal in frag_info.used_signals:
            if signal not in frag_info.signal_names and signal.name != "": # Private name shouldn't be added.
                frag_info.signal_names[signal] = _add_name(frag_info.assigned_names, signal.name)
        for port in frag_info.used_io_ports:
            if port not in frag_info.io_port_names:
                frag_info.io_port_names[port] = _add_name(frag_info.assigned_names, port.name)

        for subfragment_index, (subfragment, subfragment_name, subfragment_src_loc) in enumerate(fragment.subfragments):
            if subfragment_name is None:
                subfragment_name = f"U${subfragment_index}"
            subfragment_name = _add_name(frag_info.assigned_names, subfragment_name)
            self._assign_names(subfragment, hierarchy=(*hierarchy, subfragment_name))
Assign names to signals and IO ports used in a given fragment, as well as its
        subfragments.

        The signal mapping is set in ``self.signal_names``, and the IO port mapping is set in
        ``self.io_port_names``.  Because names are deduplicated using local information only,
        the same signal used in a different fragment may get a different name.

        Subfragments may not necessarily have a name. This method assigns every such subfragment
        a name, ``U$<number>``, where ``<number>`` is based on its location in the hierarchy.

        Subfragment names may collide with signal names safely in Amaranth, but this may confuse
        backends. This method assigns every such subfragment a new name.

        Arguments
        ---------
        hierarchy : tuple of str
            Name of this fragment.
        
amaranth/hdl/_ir.py