let find_free_group (graph : graph) : (string hashset * string hashset) option =
  let not_visited = Hashset.create (Hashtbl.length graph) in
  let _           = Hashtbl.iter (fun x _ -> add not_visited x) graph in
  let cur_group   = Hashset.create (Hashtbl.length graph) in
  let cur_inbound = Hashset.create (Hashtbl.length graph) in

  let is_in_isect x = match x with
    | InIsect _ -> true
    | _ -> false in
  let is_in_concat x = match x with
    | InConcat (_,_) -> true
    | _ -> false in
  let bad_edge edge = match edge with
    | InIsect source -> 
        let source_node = find_node graph source in
          not (free source_node)
    | _ -> false in

  let queueadd = ref [] in
  let enqueue_out edge = match edge with
    | OutConcatLeft  (rhs,target) -> queueadd := (target,false)::!queueadd
    | OutConcatRight (lhs,target) -> queueadd := (target,false)::!queueadd
    | OutIsect target -> () in
  let enqueue_in edge = match edge with 
    | InConcat (lhs, rhs) -> () (* do nothing *)
    | InIsect source -> add cur_inbound source in
  let enqueue_in_backw edge = match edge with
    | InConcat (lhs, rhs) -> queueadd := (lhs,true)::(rhs, true)::!queueadd
    | _ -> () (* do nothing *) in

  let visit x   = remove not_visited x in
  let visited x = not (mem not_visited x) in

  let rec walk (queue : (string * bool) list) : bool = match queue with
    | (cur,backw)::qs ->
        if visited cur then walk qs else enqueue_neighbors cur backw queue
    | _ -> size cur_group > 1
  and enqueue_neighbors cur backw queue : bool =
    let cur_node = find_node graph cur in
    let any_nonfree = exists bad_edge cur_node.inb in
      if any_nonfree then
        (clear cur_group;
         clear cur_inbound;
         false)
      else begin
        visit cur; add cur_group cur;
        (* If we are going backwards already or if the current node
           has an intersect constraint, and if the current node has an
           inbound concatenation constraint, then add the operands of
           that concatenation. Note that we assume at most one inbound
           concat constraint per node; we enforce this elsewhere. *)

        if backw || (exists is_in_isect cur_node.inb) then
          (match filter is_in_concat cur_node.inb with
             | edge::nothing -> enqueue_in_backw edge
             | _ -> ()); (* nothing *)
        iter enqueue_out cur_node.outb;
        iter enqueue_in  cur_node.inb;
        let toadd = List.filter (fun (x,_) -> not (visited x)) !queueadd in
        let newqueue = (List.tl queue) @ toadd in
          walk newqueue
      end
  in
  let consider_node id id_node =
    if not (visited id) && (exists is_in_isect id_node.inb) &&
      (exists is_in_concat id_node.inb) then
        if (walk [ (id, false) ]) then raise Done_finding
  in
    try
      Hashtbl.iter consider_node graph;
      None (* No groups found *)
    with Done_finding -> 
      Some (cur_group, cur_inbound)