def replace_uses_of_var_after_op(
        self,
        anchor_op: Operation,
        old_var: Var,
        new_var: Var,
        end_op: Optional[Operation] = None,
        no_check_var_types: Optional[bool] = False,
        force_replace: Optional[bool] = False,
    ):
        """
        Replace all uses of `old_var` with `new_var` after `anchor_op`,
        and before `end_op` (inclusive).

        That is all the ops that use `old_var` will now use `new_var`.
        The op that produces the `old_var` will continue to produce it, its output
        won't be replaced by `new_var`.

        If `anchor_op` is None, replace all input occurrences of `old_var` in the block. If
        `end_op` is None, all occurrences of `old_var` are replaced in the block starting from
        the op just after `anchor_op`

        no_check_var_types: An error will be raised if the type of new_var is not same as the
        old_var, unless `no_check_var_types` is set to True. Normally type inference is
        re-invoked for all the child ops of `old_var` after updating it to `new_var`. However,
        this is skipped if `no_check_var_types` is set to True.

        old_var, new_var must meet the following conditions:

        - old_var, new_var both existing within the block. This implies that
          the op generating new_var must be inserted prior to this
          replacement.

        - Affected ops (i.e., Operation after anchor_op that take old_var as
          input) must generate the same type inference results as before.

        - new_var must be visible at or before anchor_op in the order of
          self.operations.

        Given:   %2 = op0(%1, %1)
                 %3 = op1(%1, %2)
                 %4 = op2(%1)
                 %6 = op3(%4, %4)

        Execute: replace_uses_of_var_after_op(op2, %4, %3)

        Result:  %2 = op0(%1, %1)
                 %3 = op1(%1, %2)
                 %4 = op2(%1)
                 %6 = op3(%3, %3)     # type inference check against %6


        Comment: Execute: replace_uses_of_var_after_op(op1, %4, %3) would lead to
        identical results, as op2 does not take %4 as input.

        Comment: replace_uses_of_var_after_op(op0, %4, %3) would cause error as %3 is
        after op0

        Comment: To avoid clutter, we drop the names of arguments and return
        Var in the illustration above.


        Another example, usage of "end_op":

        Given:   %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %2)
                 %5 = op3(%2)

        if execute replace_uses_of_var_after_op(anchor_op=op0, old_var=%2, new_var=%3)

        Result:  %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %3)
                 %5 = op3(%3)

        if execute replace_uses_of_var_after_op(anchor_op=op0, old_var=%2, new_var=%3, end_op=op2)

        Result:  %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %3)           # %2 is replaced with %3 till here
                 %5 = op3(%2)               # will continue using %2

        """
        if not force_replace and old_var.op is not None and new_var.op is not None:
            if not old_var.can_be_replaced_by_var(new_var):
                old_nonreplaceable_vars = old_var.nonreplaceable_vars_upstream
                new_nonreplaceable_vars = new_var.nonreplaceable_vars_upstream
                err_var = None
                for _var in old_nonreplaceable_vars:
                    if _var not in new_nonreplaceable_vars:
                        err_var = _var
                        break
                msg = (
                    "var {} cannot be replaced by {}. Since the nonreplaceable var {} might "
                    "potentially "
                    "be removed during the replacement of those vars."
                ).format(old_var, new_var, err_var)
                raise ValueError(msg)

        # It is expensive to check the var visibility, and it should only be done while debugging.
        if DEBUG:
            self.validate()

            visibility_error_msg = (
                    "new_var '{}' is not visible in block '{}' at or before "
                    + "anchor_op '{}'"
            )
            anchor_op_name = "None" if anchor_op is None else anchor_op.name

            if isinstance(new_var, ComplexVar):
                # For ComplexVar, as it's just a temp wrapper to transit the real and imag data, we
                # check the visibility of its real and imaginary Var instead.
                if not self.is_var_visible_in_block(new_var.real, upto_op=anchor_op):
                    raise ValueError(
                        visibility_error_msg.format(new_var.real.name, self.name, anchor_op_name)
                    )
                if not self.is_var_visible_in_block(new_var.imag, upto_op=anchor_op):
                    raise ValueError(
                        visibility_error_msg.format(new_var.imag.name, self.name, anchor_op_name)
                    )
            else:
                if not self.is_var_visible_in_block(new_var, upto_op=anchor_op):
                    raise ValueError(
                        visibility_error_msg.format(new_var.name, self.name, anchor_op_name)
                    )
            start = self.find_op_id_in_block(anchor_op) + 1 if anchor_op is not None else 0
            end_id = self.find_op_id_in_block(end_op) if end_op is not None else -1

            if end_id != -1 and end_id < start:
                msg = "end_op '{}' comes before the anchor_op '{}'"
                raise ValueError(msg.format(end_op.name, anchor_op.name))

        num_ops_affected = self._replace_var(
            old_var,
            new_var,
            anchor_op=anchor_op,
            end_op=end_op,
            no_check_var_types=no_check_var_types,
        )

        logger.debug("Num ops affected in replacing var: {}".format(num_ops_affected))

        Replace all uses of `old_var` with `new_var` after `anchor_op`,
        and before `end_op` (inclusive).

        That is all the ops that use `old_var` will now use `new_var`.
        The op that produces the `old_var` will continue to produce it, its output
        won't be replaced by `new_var`.

        If `anchor_op` is None, replace all input occurrences of `old_var` in the block. If
        `end_op` is None, all occurrences of `old_var` are replaced in the block starting from
        the op just after `anchor_op`

        no_check_var_types: An error will be raised if the type of new_var is not same as the
        old_var, unless `no_check_var_types` is set to True. Normally type inference is
        re-invoked for all the child ops of `old_var` after updating it to `new_var`. However,
        this is skipped if `no_check_var_types` is set to True.

        old_var, new_var must meet the following conditions:

        - old_var, new_var both existing within the block. This implies that
          the op generating new_var must be inserted prior to this
          replacement.

        - Affected ops (i.e., Operation after anchor_op that take old_var as
          input) must generate the same type inference results as before.

        - new_var must be visible at or before anchor_op in the order of
          self.operations.

        Given:   %2 = op0(%1, %1)
                 %3 = op1(%1, %2)
                 %4 = op2(%1)
                 %6 = op3(%4, %4)

        Execute: replace_uses_of_var_after_op(op2, %4, %3)

        Result:  %2 = op0(%1, %1)
                 %3 = op1(%1, %2)
                 %4 = op2(%1)
                 %6 = op3(%3, %3)     # type inference check against %6


        Comment: Execute: replace_uses_of_var_after_op(op1, %4, %3) would lead to
        identical results, as op2 does not take %4 as input.

        Comment: replace_uses_of_var_after_op(op0, %4, %3) would cause error as %3 is
        after op0

        Comment: To avoid clutter, we drop the names of arguments and return
        Var in the illustration above.


        Another example, usage of "end_op":

        Given:   %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %2)
                 %5 = op3(%2)

        if execute replace_uses_of_var_after_op(anchor_op=op0, old_var=%2, new_var=%3)

        Result:  %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %3)
                 %5 = op3(%3)

        if execute replace_uses_of_var_after_op(anchor_op=op0, old_var=%2, new_var=%3, end_op=op2)

        Result:  %2 = op0(%1, %1)
                 %3 = op1()
                 %4 = op2(%1, %3)           # %2 is replaced with %3 till here
                 %5 = op3(%2)               # will continue using %2

        
coremltools/converters/mil/mil/block.py