class_docstr: If there are multiple ops with "identical" inputs, then they are redundant and all but one of them can be removed.
  This pass checks and removes such ops.
  
  Since all inputs to ops in MIL are named, two ops with same ``op_types`` can be compared by comparing their
  correspondingly named inputs. Inputs are treated as identical if one of the following is true:
  
  - The input is a constant var, in which case its value should have the same dtype and numerical value.
  - The input is a non constant var, in which case it should be the same var object.
  
  This pass iterates over the ops, takes its first output var, and then builds a candidate op list from the child
  ops of this var.
  This candidate ops list contains ops of the same ``op_type``, arranged in topological order.
  From each of these candidate ops in the list, the second, third, and subsequent ops are pairwise compared with the first op,
  and if identical to it, they are removed. For example:
  
  .. code-block::
  
      Input:
          %0 = op0(...)
          %1 = op1(...)
          %2 = const(val=4.5)
          %3 = const(val=4.5)
          %4 = op2(%1, %0, %2)
          %5 = op3(%1, %0, %3)
  
      Output:
          %0 = op0(...)
          %1 = op1(...)
          %2 = const(val=4.5)
          %3 = const(val=4.5) # this will get removed later by dead code elimination pass
          %4 = op2(%1, %0, %2)
  
  In the example above, ``op3`` is removed and all uses of ``%5`` is replaced by ``%4``.
  For more examples, see "TestRemoveRedundantOpsPass".
code: def _remove_redundant_ops_in_block_wrapper(self, block):
      def _remove_redundant_ops_in_block(block):
          if isinstance(block.inputs, dict):
              block_input_var_list = list(block.inputs.values())
          elif isinstance(block.inputs, (list, tuple)):
              block_input_var_list = block.inputs
          else:
              raise ValueError(
                  "Unrecognized type of block.inputs, its neither a list nor dict."
              )
  
          # iterate over the block inputs
          for input_var in block_input_var_list:
              if len(input_var.child_ops) > 1:
                  self._try_to_transform(input_var)
  
          # iterate over the ops in the block
          graph_updated = False
          for op in block.operations:
              if op.op_type == "const":
                  continue
  
              for b in op.blocks:
                  block_changed = True
                  while block_changed:
                      block_changed = _remove_redundant_ops_in_block(b)
  
              if (
                  len(op.outputs) > 0
                  and len(op.outputs[0].child_ops) > 1
              ):
                  # currently, we only check the first output of the op
                  # this can be extended, if required, to check for other outputs.
                  graph_updated = self._try_to_transform(op.outputs[0])
                  # has to break as the downstream iterator is affected.
                  if graph_updated:
                      return graph_updated
          return graph_updated
  
      block_changed = True
      while block_changed:
          block_changed = _remove_redundant_ops_in_block(block)
  
file_path: coremltools/coremltools/converters/mil/mil/passes/defs/cleanup/remove_redundant_ops.py
imports: import collections
  import numpy
  from coremltools.converters.mil.mil import Var
  from coremltools.converters.mil.mil.passes.graph_pass import AbstractGraphPass
  from coremltools.converters.mil.mil.passes.helper import block_context_manager
  from coremltools.converters.mil.mil.passes.pass_registry import register_pass
label_desc: Use image node_id 8 for calling the remove_redundant_ops obj's underlying member method code with example usage: obj._remove_redundant_ops_in_block_wrapper(block) and returns: graph_updated, graph_updated