docs: def _handle_successor(self, job, successor, all_successors):
          """
          Process each successor generated by the job, and return a new list of succeeding jobs.
  
          :param VFGJob job: The VFGJob instance.
          :param SimState successor: The succeeding state.
          :param list all_successors:  A list of all successors.
          :return: A list of newly created jobs from the successor.
          :rtype: list
          """
  
          # Initialize parameters
          addr = job.addr
          jumpkind = successor.history.jumpkind
  
          #
          # Get instruction pointer
          #
  
          if job.is_return_jump:
              ret_target = job.call_stack.current_return_target
              if ret_target is None:
                  # We have no where to go according to our call stack. However, the callstack might be corrupted
                  l.debug("According to the call stack, we have nowhere to return to.")
                  return [ ]
  
              successor.ip = ret_target
  
          # this try-except block is to handle cases where the instruction pointer is symbolic
          try:
              successor_addrs = successor.solver.eval_upto(successor.ip, 2)
          except SimValueError:
              # TODO: Should fall back to reading targets from CFG
              # It cannot be concretized currently. Maybe we could handle
              # it later, maybe it just cannot be concretized
              return [ ]
  
          if len(successor_addrs) > 1:
              # multiple concrete targets
              if job.is_return_jump:
                  # It might be caused by state merging
                  # We may retrieve the correct ip from call stack
                  successor.ip = job.call_stack.current_return_target
  
              else:
                  return self._handle_successor_multitargets(job, successor, all_successors)
  
          # Now there should be one single target for the successor
          successor_addr = successor.solver.eval_one(successor.ip)
  
          # Get the fake ret successor
          fakeret_successor = None
          if self._is_call_jumpkind(jumpkind):
              fakeret_successor = all_successors[-1]
  
              # If the function we're calling into doesn't return, we should discard it
              if self._cfg is not None:
                  func = self.kb.functions.function(addr=job.call_target)
                  if func is not None and func.returning is False and len(all_successors) == 2:
                      del all_successors[-1]
                      fakeret_successor = None
  
          if self._is_call_jumpkind(jumpkind):
              # Create a new call stack for the successor
              new_call_stack = self._create_callstack(job, successor_addr, jumpkind, fakeret_successor)
              if new_call_stack is None:
                  l.debug("Cannot create a new callstack for address %#x", successor_addr)
                  job.dbg_exit_status[successor] = ""
                  return [ ]
              new_call_stack_suffix = new_call_stack.stack_suffix(self._context_sensitivity_level)
  
              new_function_key = FunctionKey.new(successor_addr, new_call_stack_suffix)
              # Save the initial state for the function
              self._save_function_initial_state(new_function_key, successor_addr, successor.copy())
  
              # bail out if we hit the interfunction_level cap
              if len(job.call_stack) >= self._interfunction_level:
                  l.debug('We are not tracing into a new function %#08x as we hit interfunction_level limit', successor_addr)
  
                  # mark it as skipped
                  job.dbg_exit_status[successor] = "Skipped"
  
                  job.call_skipped = True
                  job.call_function_key = new_function_key
  
                  job.call_task.skipped = True
  
                  return [ ]
  
          elif jumpkind == 'Ijk_Ret':
              # Pop the current function out from the call stack
              new_call_stack = self._create_callstack(job, successor_addr, jumpkind, fakeret_successor)
              if new_call_stack is None:
                  l.debug("Cannot create a new callstack for address %#x", successor_addr)
                  job.dbg_exit_status[successor] = ""
                  return [ ]
              new_call_stack_suffix = new_call_stack.stack_suffix(self._context_sensitivity_level)
  
          else:
              new_call_stack = job.call_stack
              new_call_stack_suffix = job.call_stack_suffix
  
          # Generate the new block ID
          new_block_id = BlockID.new(successor_addr, new_call_stack_suffix, jumpkind)
  
          #
          # Generate new VFG jobs
          #
  
          if jumpkind == "Ijk_Ret":
              assert not job.is_call_jump
  
              # Record this return
              self._return_target_sources[successor_addr].append(job.call_stack_suffix + (addr,))
  
              # Check if this return is inside our pending returns list
              if new_block_id in self._pending_returns:
                  del self._pending_returns[new_block_id]
  
          # Check if we have reached a fix-point
          if jumpkind != 'Ijk_FakeRet' and \
                  new_block_id in self._nodes:
              last_state = self._nodes[new_block_id].state
  
              _, _, merged = last_state.merge(successor, plugin_whitelist=self._mergeable_plugins)
  
              if merged:
                  l.debug("%s didn't reach a fix-point", new_block_id)
              else:
                  l.debug("%s reaches a fix-point.", new_block_id)
                  job.dbg_exit_status[successor] = "Merged due to reaching a fix-point"
                  return [ ]
  
          new_jobs = self._create_new_jobs(job, successor, new_block_id, new_call_stack)
  
          return new_jobs