File 1882-stdlib-New-module-graph-a-functional-equivalent-to-d.patch of Package erlang

From c99fd5c594f6265560150da34b50a91eb85f4731 Mon Sep 17 00:00:00 2001
From: Richard Carlsson <carlsson.richard@gmail.com>
Date: Sun, 28 Dec 2025 12:09:17 +0100
Subject: [PATCH 2/3] stdlib: New module `graph`, a functional equivalent to
 digraph

Based on the beam_digraph module, but staying mostly compatible with the
digraph module to make it easy to switch implementations.
---
 lib/stdlib/doc/docs.exs         |    1 +
 lib/stdlib/src/Makefile         |    1 +
 lib/stdlib/src/graph.erl        | 1318 +++++++++++++++++++++++++++++++
 lib/stdlib/src/stdlib.app.src   |    1 +
 lib/stdlib/test/Makefile        |    1 +
 lib/stdlib/test/graph_SUITE.erl |  830 +++++++++++++++++++
 6 files changed, 2152 insertions(+)
 create mode 100644 lib/stdlib/src/graph.erl
 create mode 100644 lib/stdlib/test/graph_SUITE.erl

diff --git a/lib/stdlib/doc/docs.exs b/lib/stdlib/doc/docs.exs
index 29ccd07875..1253d7191b 100644
--- a/lib/stdlib/doc/docs.exs
+++ b/lib/stdlib/doc/docs.exs
@@ -69,6 +69,7 @@
         :digraph_utils,
         :gb_sets,
         :gb_trees,
+        :graph,
         :json,
         :orddict,
         :ordsets,
diff --git a/lib/stdlib/src/Makefile b/lib/stdlib/src/Makefile
index 6a4379af37..07614bd4eb 100644
--- a/lib/stdlib/src/Makefile
+++ b/lib/stdlib/src/Makefile
@@ -56,6 +56,7 @@ MODULES= \
 	dets_utils \
 	dets_v9 \
 	dict \
+	graph \
 	digraph \
 	digraph_utils \
 	edlin \
diff --git a/lib/stdlib/src/graph.erl b/lib/stdlib/src/graph.erl
new file mode 100644
index 0000000000..0dfa541514
--- /dev/null
+++ b/lib/stdlib/src/graph.erl
@@ -0,0 +1,1318 @@
+%%
+%% %CopyrightBegin%
+%%
+%% SPDX-License-Identifier: Apache-2.0
+%%
+%% Copyright Ericsson AB 2019-2026. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+-module(graph).
+-moduledoc """
+A functional implementation of labeled directed graphs.
+
+This module is closely modelled on the `digraph` and `digraph_utils`
+modules, which represent graphs using mutable ETS tables. This functional
+implementation is more lightweight and does not involve mutable state and
+table ownership, and makes it easy to keep multiple versions of a graph, but
+for large tables with a long lifetime, the ETS based implementation can be
+more suitable. In the context of this module, we only use the term "digraph"
+when referring to the `digraph` implementation, and not to directed graphs
+in general.
+
+When rewriting code from using `digraph` to using `graph`, keep in mind that:
+
+- Graphs are immutable: each modifying operation returns the new graph,
+  which needs to be saved in a variable and passed to the next operation,
+  for example `G0 = graph:new(), G1 = graph:add_vertex(G0, v1), G2 =
+  graph:add_vertex(G1, v2)`.
+- Graphs are garbage collected and do not need to be explicitly deleted.
+- There are no `protected` or `private` options and no `memory` info key.
+- Edges are not objects with identity and state. An edge is uniquely
+  identified by the triple `{From, To, Label}` where the default label is
+  `[]`. There can be multiple edges with the same `From` and `To` but only
+  if they have different values for `Label`.
+- Vertices, however, have a unique identifier just as in `digraph`. The
+  label of an existing vertex can be replaced by a new call to
+  `add_vertex(Id, Label)`. The label defaults to `[]`.
+- The functions in `digraph_utils` have been included directly in the
+  `graph` module for simplicity.
+
+Some graph theoretical definitions:
+
+- A _directed graph_{: #graph } (here simply called "graph") is a pair (V, E)
+  of a finite set V of _vertices_{: #vertex } and a finite set E of _directed
+  edges_{: #edge } (here simply called "edges"). The set of edges E is a
+  subset of V × V (the Cartesian product of V with itself).
+
+  In this module, V is allowed to be empty. The so obtained unique graph is
+  called the _empty graph_{: #empty_graph }. Each vertex has a unique Erlang
+  term as identifier.
+
+- Graphs can be annotated with more information. Such information can be
+  attached to the vertices and to the edges of the graph. An annotated graph
+  is called a _labeled graph_, and the information attached to a vertex or an
+  edge is called a _label_{: #label }. Labels are Erlang terms.
+
+- An edge e = (v, w) is said to _emanate_{: #emanate } from vertex v and to be
+  _incident_{: #incident } on vertex w.
+
+- The _out-degree_{: #out_degree } of a vertex is the number of edges emanating
+  from that vertex.
+
+- The _in-degree_{: #in_degree } of a vertex is the number of edges incident on
+  that vertex.
+
+- If an edge is emanating from v and incident on w, then w is said to be an
+  _out-neighbor_{: #out_neighbour } of v, and v is said to be an _in-neighbor_{:
+  #in_neighbour } of w.
+
+- A _subgraph_{: #subgraph } G' of G is a graph whose vertices and edges form
+  subsets of the vertices and edges of G.
+
+- G' is _maximal_ with respect to a property P if all other subgraphs that
+  include the vertices of G' do not have property P.
+
+- A _path_{: #path } P from v\[1] to v\[k] in a graph (V, E) is a non-empty
+  sequence v\[1], v\[2], ..., v\[k] of vertices in V such that there is an edge
+  (v\[i], v\[i+1]) in E for 1 <= i < k.
+
+- The _length_{: #length } of path P is k-1.
+
+- Path P is _simple_{: #simple_path } if all vertices are distinct, except that
+  the first and the last vertices can be the same.
+
+- Path P is a _cycle_{: #cycle } if the length of P is not zero and v\[1] =
+  v\[k].
+
+- A _loop_{: #loop } is a cycle of length one.
+
+- A _simple cycle_{: #simple_cycle } is a path that is both a cycle and simple.
+
+- An _acyclic graph_{: #acyclic_graph } is a graph without cycles.
+
+- A _tree_{: #tree } is an acyclic non-empty graph such that there is a unique
+  path between every pair of vertices, considering all edges undirected. In an
+  undirected tree, any vertex can be used as root. Informally however, "tree"
+  is often used to refer to mean an _out-tree_ (arborescence), in particular
+  in computer science.
+
+- An _arborescence_{: #arborescence } or _directed rooted tree_ or _out-tree_
+  is an acyclic directed graph with a vertex V, the _root_{: #root }, such that
+  there is a unique path from V to every other vertex of G.
+
+- A _forest_{: #forest } is a disjoint union of trees.
+
+- A [_strongly connected component_](https://en.wikipedia.org/wiki/Strongly_connected_component) {: #strong_components }
+  is a maximal subgraph such that there is a path between each pair of vertices.
+
+- A _connected component_{: #components } is a maximal subgraph such that there
+  is a path between each pair of vertices, considering all edges undirected.
+
+This module also provides algorithms based on depth-first traversal of
+directed graphs.
+
+- A _depth-first traversal_{: #depth_first_traversal } of a directed graph can
+  be viewed as a process that visits all vertices of the graph. Initially, all
+  vertices are marked as unvisited. The traversal starts with an arbitrarily
+  chosen vertex, which is marked as visited, and follows an edge to an unmarked
+  vertex, marking that vertex. The search then proceeds from that vertex in the
+  same fashion, until there is no edge leading to an unvisited vertex. At that
+  point the process backtracks, and the traversal continues as long as there are
+  unexamined edges. If unvisited vertices remain when all edges from the first
+  vertex have been examined, some so far unvisited vertex is chosen, and the
+  process is repeated.
+- A _partial ordering_{: #partial_ordering } of a set S is a transitive,
+  antisymmetric, and reflexive relation between the objects of S.
+- The problem of [_topological sorting_](https://en.wikipedia.org/wiki/Topological_sorting) {: #topsort }
+  is to find a total ordering of S that is a superset of the partial ordering.
+  A graph G = (V, E) is equivalent to a relation E on V (we neglect that
+  the version of directed graphs provided by the `digraph` module allows
+  multiple edges between vertices). If the graph has no cycles of length
+  two or more, the reflexive and transitive closure of E is a partial ordering.
+""".
+-moduledoc(#{ since => ~"OTP 29.0"}).
+
+%% Basic functionality
+-export([new/0, new/1, info/1,
+         no_edges/1, source_vertices/1, sink_vertices/1,
+         add_vertex/1, add_vertex/2, add_vertex/3,
+         add_edge/3, add_edge/4,
+         del_edge/2, del_edges/2, del_edges/3,
+         del_vertex/2, del_vertices/2,
+         edges/1, edges/2, edges/3,
+         has_vertex/2, has_edge/2, has_edge/3,
+         has_path/3, del_path/3, get_path/3, get_cycle/2,
+         get_short_path/3, get_short_cycle/2,
+         in_degree/2, in_edges/2, in_neighbours/2,
+         no_vertices/1,
+         out_degree/2, out_edges/2, out_neighbours/2,
+         vertex/2, vertex/3, vertices/1, vertices_with_labels/1,
+         fold_vertices/3
+         ]).
+
+%% Utilities
+-export([components/1, strong_components/1, cyclic_strong_components/1,
+         reachable/2, reachable_via_neighbours/2,
+         reaching/2, reaching_via_neighbours/2,
+         topsort/1, is_acyclic/1, roots/1,
+         arborescence_root/1, is_arborescence/1, is_tree/1,
+         loop_vertices/1,
+         subgraph/2, subgraph/3, condensation/1,
+         preorder/1, preorder/2, postorder/1, postorder/2,
+         reverse_postorder/1, reverse_postorder/2]).
+
+-export_type([graph/0, graph_type/0, graph_cyclicity/0,
+              vertex/0, edge/0, label/0]).
+
+%% Debugging.
+-define(DEBUG, false).
+-if(?DEBUG).
+-export([dump/1,dump/2,dump/3]).
+-endif.
+
+-import(lists, [foldl/3, reverse/1]).
+
+-type edge_map() :: #{ vertex() => ordsets:ordset(vertex()) }.
+-type vertice_map() :: #{ vertex() => label() }.
+
+-record(graph, {vs = #{} :: vertice_map(),
+                in_es = #{} :: edge_map(),
+                out_es = #{} :: edge_map(),
+                cyclic = true :: boolean(),
+                next_vid = 0 :: non_neg_integer()}).
+
+-type graph() :: #graph{}.
+
+-type vertex() :: term().
+-type label() :: term().
+-type edge() :: {vertex(), vertex(), label()}.
+
+-type graph_cyclicity()  :: 'acyclic' | 'cyclic'.
+-type graph_type()       :: graph_cyclicity().
+
+-doc(#{ equiv => new([]) }).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec new() -> graph().
+new() ->
+    new([]).
+
+-doc """
+Creates a new graph.
+
+Returns an [empty graph](`m:graph#empty_graph`) with properties according to
+the options in `Options`:
+
+- **`cyclic`** - Allows [cycles](`m:graph#cycle`) in the graph (default).
+
+- **`acyclic`** - The graph is to be kept [acyclic](`m:graph#acyclic_graph`).
+  Attempting to add an edge that would introduce a cycle will raise an error
+  `{bad_edge, {From, To}}`. Note that this slows down the adding of edges.
+
+If an unrecognized option is specified or `Options` is not a proper list, a
+`badarg` exception is raised.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec new([graph_type()]) -> graph().
+new(Options) when is_list(Options) ->
+    new_1(Options, #graph{}).
+
+new_1([cyclic|Opts], G) ->
+    new_1(Opts, G#graph{cyclic = true});
+new_1([acyclic|Opts], G) ->
+    new_1(Opts, G#graph{cyclic = false});
+new_1([], G) ->
+    G;
+new_1(_, _) ->
+    error(badarg).
+
+-doc """
+Returns a list of `{Tag, Value}` pairs describing graph `G`.
+
+The following pairs are returned:
+
+- `{cyclicity, Cyclicity}`, where `Cyclicity` is `cyclic` or `acyclic`,
+  according to the options given to `new`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec info(graph()) -> [{'cyclicity', graph_cyclicity()}].
+info(G) ->
+    Cyclicity = case G#graph.cyclic of
+                    true  -> cyclic;
+                    false -> acyclic
+                end,
+    [{cyclicity, Cyclicity}].
+
+-doc """
+Adds a new vertex to graph `G`, returning the created vertex id.
+
+The new vertex will have the empty list `[]` as [label](`m:graph#label`).
+
+Note: Vertex ID:s are assigned as integers in increasing order starting from
+zero. If you use `add_vertex/2` or `add_vertex/3` to insert vertices with
+your own identifiers, this function could generate an ID that already exists
+in the graph.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec add_vertex(graph()) -> {vertex(), graph()}.
+add_vertex(G) ->
+    {V, G1} = new_vertex_id(G),
+    {V, add_vertex(G1, V)}.
+
+new_vertex_id(#graph{next_vid=V}=G) ->
+    {V, G#graph{next_vid=V+1}}.
+
+-doc(#{equiv => add_vertex(G, V, [])}).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec add_vertex(graph(), vertex()) -> graph().
+add_vertex(G, V) ->
+    add_vertex(G, V, []).
+
+-doc """
+Creates or modifies vertex `V` of graph `G`, using `L` as the (new)
+[label](`m:graph#label`) of the vertex.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec add_vertex(graph(), vertex(), label()) -> graph().
+add_vertex(G, V, L) ->
+    #graph{vs=Vs0} = G,
+    Vs = Vs0#{V=>L},
+    G#graph{vs=Vs}.
+
+-doc """
+Deletes vertex `V` from graph `G`.
+
+Any edges [emanating](`m:graph#emanate`) from `V` or
+[incident](`m:graph#incident`) on `V` are also deleted.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_vertex(graph(), vertex()) -> graph().
+del_vertex(G, V) ->
+    #graph{vs=Vs0,in_es=InEsMap0,out_es=OutEsMap0} = G,
+    InEs = maps:get(V, InEsMap0, []),
+    OutEsMap = foldl(fun({From,_,_}=E, A) -> edge_map_del(From, E, A) end,
+                     maps:remove(V, OutEsMap0), InEs),
+    OutEs = maps:get(V, OutEsMap0, []),
+    InEsMap = foldl(fun({_,To,_}=E, A) -> edge_map_del(To, E, A) end,
+                    maps:remove(V, InEsMap0), OutEs),
+    Vs = maps:remove(V, Vs0),
+    G#graph{vs=Vs,in_es=InEsMap,out_es=OutEsMap}.
+
+-doc "Deletes the vertices in list `Vs` from graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_vertices(G::graph(), Vs::[vertex()]) -> graph().
+del_vertices(G, [V | Vs]) ->
+    del_vertices(del_vertex(G, V), Vs);
+del_vertices(G, []) -> G.
+
+-doc """
+Returns the [label](`m:graph#label`) of the vertex `V` of graph `G`.
+
+An exception is raised if `V` does not exist in `G`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec vertex(G::graph(), V::vertex()) -> label().
+vertex(#graph{vs=Vs}, V) ->
+    map_get(V, Vs).
+
+-doc """
+Returns the [label](`m:graph#label`) of the vertex `V` of graph `G`,
+or returns `Default` if `V` does not exist in `G`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec vertex(G::graph(), V::vertex(), Default::label()) -> label().
+vertex(#graph{vs=Vs}, V, Default) ->
+    maps:get(V, Vs, Default).
+
+-doc "Returns the number of vertices of graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec no_vertices(G::graph()) -> non_neg_integer().
+no_vertices(#graph{vs=Vs}) ->
+    map_size(Vs).
+
+-doc "Returns a list of all vertices of graph `G`, in some unspecified order.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec vertices(G::graph()) -> [vertex()].
+vertices(#graph{vs=Vs}) ->
+    maps:keys(Vs).
+
+-doc """
+Returns a list of all pairs `{V, L}` of vertices of graph `G` and their
+respective labels, in some unspecified order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec vertices_with_labels(G::graph()) -> [{vertex(), label()}].
+vertices_with_labels(#graph{vs=Vs}) ->
+    maps:to_list(Vs).
+
+-doc """
+Returns a list of all vertices of graph `G` with
+[in-degree](`m:graph#in_degree`) zero.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec source_vertices(G::graph()) -> [vertex()].
+source_vertices(#graph{vs=Vs, in_es=InEsMap}) ->
+    maps:fold(fun(V, _, Acc) ->
+                      case maps:get(V, InEsMap, []) of
+                          [] -> [V | Acc];
+                          _ -> Acc
+                      end
+              end, [], Vs).
+
+-doc """
+Returns a list of all vertices of graph `G` with
+[out-degree](`m:graph#in_degree`) zero.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec sink_vertices(G::graph()) -> [vertex()].
+sink_vertices(#graph{vs=Vs, out_es=OutEsMap}) ->
+    maps:fold(fun(V, _, Acc) ->
+                      case maps:get(V, OutEsMap, []) of
+                          [] -> [V | Acc];
+                          _ -> Acc
+                      end
+              end, [], Vs).
+
+-doc "Returns the [in-degree](`m:graph#in_degree`) of vertex `V` of graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec in_degree(G::graph(), V::vertex()) -> non_neg_integer().
+in_degree(#graph{in_es=InEsMap}, V) ->
+    length(maps:get(V, InEsMap, [])).
+
+-doc """
+Returns a list of all [in-neighbors](`m:graph#in_neighbour`) of `V` of graph
+`G`, in some unspecified order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec in_neighbours(G::graph(), V::vertex()) -> [vertex()].
+in_neighbours(#graph{in_es=InEsMap}, V) ->
+    [From || {From,_,_} <:- maps:get(V, InEsMap, [])].
+
+-doc """
+Returns a list of all edges [incident](`m:graph#incident`) on `V` of graph
+`G`, in some unspecified order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec in_edges(G::graph(), V::vertex()) -> [edge()].
+in_edges(#graph{in_es=InEsMap}, V) ->
+    maps:get(V, InEsMap, []).
+
+-doc "Returns the [out-degree](`m:graph#out_degree`) of vertex `V` of graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec out_degree(G::graph(), V::vertex()) -> non_neg_integer().
+out_degree(#graph{out_es=OutEsMap}, V) ->
+    length(maps:get(V, OutEsMap, [])).
+
+-doc """
+Returns a list of all [out-neighbors](`m:graph#out_neighbour`) of `V` of
+graph `G`, in some unspecified order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec out_neighbours(G::graph(), V::vertex()) -> [vertex()].
+out_neighbours(#graph{out_es=OutEsMap}, V) ->
+    [To || {_,To,_} <:- maps:get(V, OutEsMap, [])].
+
+-doc """
+Returns a list of all edges [emanating](`m:graph#emanate`) from `V` of graph
+`G`, in some unspecified order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec out_edges(G::graph(), V::vertex()) -> [edge()].
+out_edges(#graph{out_es=OutEsMap}, V) ->
+    maps:get(V, OutEsMap, []).
+
+-doc(#{equiv => add_edge(G, V1, V2, [])}).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec add_edge(graph(), vertex(), vertex()) -> graph().
+add_edge(G, V1, V2) ->
+    add_edge(G, V1, V2, []).
+
+-doc """
+Creates an edge `{V1, V2, L}` in graph `G`.
+
+The edge is [emanating](`m:graph#emanate`) from `V1` and
+[incident](`m:graph#incident`) on `V2`, and has [label](`m:graph#label`)
+`L`. The edge is uniquely identified by this triple. A graph can have
+multiple edges between the same vertices `V1` and `V2` but only if the
+edges have different labels.
+
+If `G` was created with option `acyclic`, then attempting to add an edge that
+would introduce a cycle will raise an error `{bad_edge, {From, To}}`. Note
+that checking for cyclicity slows down the adding of edges.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec add_edge(G, V1, V2, L) -> graph() when
+      G :: graph(),
+      V1 :: vertex(),
+      V2 :: vertex(),
+      L :: label().
+add_edge(#graph{cyclic=true}=G, From, To, Label) ->
+    do_add_edge(G, From, To, Label);
+add_edge(#graph{cyclic=false}=G, From, To, Label) ->
+    acyclic_add_edge(G, From, To, Label).
+
+do_add_edge(#graph{vs=Vs}=G, From, To, Label) ->
+    maps:is_key(From, Vs) orelse error({bad_vertex, From}),
+    maps:is_key(To, Vs) orelse error({bad_vertex, To}),
+    #graph{in_es=InEsMap0,out_es=OutEsMap0} = G,
+    Name = {From,To,Label},  % note: edge tuple is shared between the maps
+    InEsMap = edge_map_add(To, Name, InEsMap0),
+    OutEsMap = edge_map_add(From, Name, OutEsMap0),
+    G#graph{in_es=InEsMap,out_es=OutEsMap}.
+
+edge_map_add(V, E, EsMap) ->
+    Es0 = maps:get(V, EsMap, []),
+    Es = ordsets:add_element(E, Es0),
+    EsMap#{V=>Es}.
+
+acyclic_add_edge(_G, From, To, _Label) when From =:= To ->
+    error({bad_edge, {From,To}});
+acyclic_add_edge(G, From, To, Label) ->
+    case get_path(G, To, From) of
+        false ->
+            false = has_path(G, To, From),  % assert - remove me
+            do_add_edge(G, From, To, Label);
+        _ ->
+            true = has_path(G, To, From),  % assert - remove me
+            error({bad_edge, {From,To}})
+    end.
+
+-doc "Deletes edge `E` from graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_edge(G::graph(), E::edge()) -> graph().
+del_edge(G, {From,To,_}=E) ->
+    #graph{in_es=InEsMap0,out_es=OutEsMap0} = G,
+    InEsMap = edge_map_del(To, E, InEsMap0),
+    OutEsMap = edge_map_del(From, E, OutEsMap0),
+    G#graph{in_es=InEsMap,out_es=OutEsMap}.
+
+edge_map_del(V, E, EsMap) ->
+    case maps:find(V, EsMap) of
+        error -> EsMap;
+        {ok, Es0} ->
+            Es = Es0 -- [E],
+            EsMap#{V:=Es}
+    end.
+
+-doc "Deletes the edges in list `Es` from graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_edges(graph(), [edge()]) -> graph().
+del_edges(G, Es) when is_list(Es) ->
+    foldl(fun(E, A) -> del_edge(A, E) end, G, Es).
+
+-doc "Deletes all edges from vertex `V1` to vertex `V2` in graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_edges(graph(), vertex(), vertex()) -> graph().
+del_edges(G, V1, V2) ->
+    Es = out_edges(G, V1),
+    del_edges(G, V1, V2, Es).
+
+del_edges(G, V1, V2, [{Va,Vb,_}=E | Es]) when Va =:= V1, Vb =:= V2->
+    del_edges(del_edge(G, E), V1, V2, Es);
+del_edges(G, V1, V2, [_|Es]) ->
+    del_edges(G, V1, V2, Es);
+del_edges(G, _, _, []) -> G.
+
+-doc "Returns the number of edges of graph `G`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec no_edges(G::graph()) -> non_neg_integer().
+no_edges(#graph{out_es=OutEsMap}) ->
+    maps:fold(fun (_, Es, Acc) -> length(Es) + Acc end,
+              0, OutEsMap).
+
+-doc "Returns a list of all edges of graph `G`, in some unspecified order.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec edges(G::graph()) -> [edge()].
+edges(#graph{out_es=OutEsMap}) ->
+    maps:fold(fun(_, Es, Acc) ->
+                      Es ++ Acc
+              end, [], OutEsMap).
+
+-doc """
+Returns a list of all edges [emanating](`m:graph#emanate`) from or
+[incident](`m:graph#incident`) on `V` of graph `G`, in some unspecified
+order.
+
+Edges may occur twice in the list. Use `ordsets:from_list/1` on the
+result if you need to remove duplicates.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec edges(G::graph(), V::vertex()) -> [edge()].
+edges(#graph{in_es=InEsMap, out_es=OutEsMap}, V) ->
+    maps:get(V, OutEsMap, [])
+        ++ maps:get(V, InEsMap, []).
+
+-doc "Returns the ordered set of edges from V1 to V2.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec edges(G::graph(), V1::vertex(), V2::vertex()) -> ordsets:ordset(edge()).
+edges(#graph{out_es=OutEsMap}, V1, V2) ->
+    case OutEsMap of
+       #{V1 := Es} -> [E || {Va, Vb, _}=E <- Es, Va =:= V1, Vb =:= V2];
+       #{} -> []
+   end.
+
+-doc """
+Returns `true` if and only if `G` contains edge `E`.
+
+Note that the identity of an edge includes its label. To check for an
+arbitrary edge between two vertices, use `has_edge/3`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec has_edge(G::graph(), E::edge()) -> boolean().
+has_edge(#graph{out_es=OutEsMap}, {V1, _, _}=E) ->
+    case OutEsMap of
+       #{V1 := Es} -> ordsets:is_element(E, Es);
+       #{} -> false
+   end.
+
+-doc "Returns `true` if and only if `G` contains some edge from `V1` to `V2`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec has_edge(G::graph(), V1::vertex(), V2::vertex()) -> boolean().
+has_edge(#graph{out_es=OutEsMap}, V1, V2) ->
+    case OutEsMap of
+       #{V1 := Es} -> has_edge_1(Es, V1, V2);
+       #{} -> false
+   end.
+
+has_edge_1([{V1, V2, _L} | _], V1, V2) ->
+    true;
+has_edge_1([_ | Es], V1, V2) ->
+    has_edge_1(Es, V1, V2);
+has_edge_1([], _, _) ->
+    false.
+
+-doc "Fold `Fun` over the vertices of graph `G`, in some unspecified order.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec fold_vertices(G, Fun, Acc) -> any() when
+      G :: graph(),
+      Fun :: fun((vertex(), label(), any()) -> any()),
+      Acc :: any().
+fold_vertices(#graph{vs=Vs}, Fun, Acc) ->
+    maps:fold(Fun, Acc, Vs).
+
+-doc "Returns `true` if and only if `G` contains vertex `V`.".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec has_vertex(G::graph(), V::vertex()) -> boolean().
+has_vertex(#graph{vs=Vs}, V) ->
+    is_map_key(V, Vs).
+
+-doc """
+Returns `true` if and only if there is a [path](`m:graph#path`) in `G` from
+vertex `V1` to vertex `V2`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec has_path(G::graph(), V1::vertex(), V2::vertex()) -> boolean().
+has_path(G, From, To) ->
+    Seen = sets:new(),
+    try
+        _ = has_path_1([From], To, G, Seen),
+        false
+    catch
+        throw:true ->
+            true
+    end.
+
+has_path_1([To|_], To, _G, _Seen) ->
+    throw(true);
+has_path_1([V|Vs], To, G, Seen0) ->
+    case sets:is_element(V, Seen0) of
+        true ->
+            has_path_1(Vs, To, G, Seen0);
+        false ->
+            Seen1 = sets:add_element(V, Seen0),
+            Successors = out_neighbours(G, V),
+            Seen = has_path_1(Successors, To, G, Seen1),
+            has_path_1(Vs, To, G, Seen)
+    end;
+has_path_1([], _To, _G, Seen) ->
+    Seen.
+
+-doc """
+Deletes edges from graph `G` until there are no [paths](`m:graph#path`) from
+vertex `V1` to vertex `V2`.
+
+A sketch of the procedure employed:
+
+- Find an arbitrary [simple path](`m:graph#simple_path`)
+  v\[1], v\[2], ..., v\[k] from `V1` to `V2` in `G`.
+- Remove all edges of `G` [emanating](`m:graph#emanate`) from v\[i] and
+  [incident](`m:graph#incident`) to v\[i+1] for 1 <= i < k (including multiple
+  edges).
+- Repeat until there is no path between `V1` and `V2`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec del_path(graph(), vertex(), vertex()) -> graph().
+del_path(G, V1, V2) ->
+    case get_path(G, V1, V2) of
+        false -> G;
+        Path ->
+            del_path(del_path_edges(G, Path), V1, V2)
+    end.
+
+del_path_edges(G, [V1, V2 | Vs]) ->
+    del_path_edges(del_edges(G, V1, V2), [V2 | Vs]);
+del_path_edges(G, _) -> G.
+
+-doc """
+Tries to find a cycle in `G` which includes vertex `V`.
+
+If a [simple cycle](`m:graph#simple_cycle`) of length two or more exists
+through vertex `V`, the cycle is returned as a list `[V, ..., V]` of vertices.
+If a [loop](`m:graph#loop`) through `V` exists, the loop is returned as a list
+`[V]`. If no cycles through `V` exist, `false` is returned.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec get_cycle(graph(), vertex()) -> [vertex(),...] | 'false'.
+get_cycle(G, V) ->
+    case one_path(out_neighbours(G, V), V, [], [V], [V], 2, G, 1) of
+        false ->
+            case lists:member(V, out_neighbours(G, V)) of
+                true -> [V];
+                false -> false
+            end;
+        Vs -> Vs
+    end.
+
+-doc """
+Tries to find a [simple path](`m:graph#simple_path`) from vertex `V1` to
+vertex `V2` of graph `G`.
+
+Returns the path as a list `[V1, ..., V2]` of vertices, or `false` if no
+simple path from `V1` to `V2` of length one or more exists.
+
+The graph is traversed in a depth-first manner, and the first found path is
+returned.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec get_path(graph(), vertex(), vertex()) -> [vertex(),...] | 'false'.
+get_path(G, V1, V2) ->
+    one_path(out_neighbours(G, V1), V2, [], [V1], [V1], 1, G, 1).
+
+one_path([W|Ws], W, Cont, Xs, Ps, Prune, G, Counter) ->
+    if Counter < Prune -> one_path(Ws, W, Cont, Xs, Ps, Prune, G, Counter);
+        true -> reverse([W|Ps])
+    end;
+one_path([V|Vs], W, Cont, Xs, Ps, Prune, G, Counter) ->
+    case lists:member(V, Xs) of
+        true ->  one_path(Vs, W, Cont, Xs, Ps, Prune, G, Counter);
+        false -> one_path(out_neighbours(G, V), W,
+                          [{Vs,Ps} | Cont], [V|Xs], [V|Ps],
+                          Prune, G, Counter+1)
+    end;
+one_path([], W, [{Vs,Ps}|Cont], Xs, _, Prune, G, Counter) ->
+    one_path(Vs, W, Cont, Xs, Ps, Prune, G, Counter-1);
+one_path([], _, [], _, _, _, _, _Counter) -> false.
+
+-doc """
+Like `get_cycle/2`, but a cycle of length one is preferred.
+
+Tries to find an as short as possible [simple
+cycle](`m:graph#simple_cycle`) through vertex `V` of graph `G`. Returns
+the cycle as a list `[V, ..., V]` of vertices, or `false` if no simple
+cycle through `V` exists. Notice that a [loop](`m:graph#loop`) through
+`V` is returned as list `[V, V]`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec get_short_cycle(graph(), vertex()) -> [vertex(),...] | 'false'.
+get_short_cycle(G, V) ->
+    get_short_path(G, V, V).
+
+-doc """
+Like `get_path/3`, but using a breadth-first search to find a short path.
+
+Tries to find an as short as possible [simple path](`m:graph#simple_path`)
+from vertex `V1` to vertex `V2` of graph `G`. Returns the path as a list
+`[V1, ..., V2]` of vertices, or `false` if no simple path from `V1` to `V2` of
+length one or more exists.
+
+Graph `G` is traversed in a breadth-first manner, and the first found path is
+returned.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec get_short_path(graph(), vertex(), vertex()) -> [vertex(),...] | 'false'.
+get_short_path(G, V1, V2) ->
+    T = new(),
+    T1 = add_vertex(T, V1),
+    Q = queue:new(),
+    Q1 = queue_out_neighbours(V1, G, Q),
+    spath(Q1, G, V2, T1).
+
+spath(Q, G, Sink, T) ->
+    case queue:out(Q) of
+        {{value, {V1, V2, _Label}}, Q1} ->
+            if
+                Sink =:= V2 ->
+                    follow_path(V1, T, [V2]);
+                true ->
+                    case has_vertex(T, V2) of
+                        false ->
+                            T1 = add_vertex(T, V2),
+                            T2 = add_edge(T1, V2, V1),
+                            NQ = queue_out_neighbours(V2, G, Q1),
+                            spath(NQ, G, Sink, T2);
+                        true ->
+                            spath(Q1, G, Sink, T)
+                    end
+            end;
+        {empty, _Q1} ->
+            false
+    end.
+
+follow_path(V, T, P) ->
+    P1 = [V | P],
+    case out_neighbours(T, V) of
+        [N] ->
+            follow_path(N, T, P1);
+        [] ->
+            P1
+    end.
+
+queue_out_neighbours(V, G, Q0) ->
+    foldl(fun(E, Q) -> queue:in(E, Q) end, Q0, out_edges(G, V)).
+
+-if(?DEBUG).
+
+%%
+%% Dumps the graph as a string in dot (graphviz) format.
+%%
+%% Use dot(1) to convert to an image:
+%%
+%%    dot [input] -T[format]
+%%    dot graph_file -Tsvg > graph.svg
+
+-spec dump(any()) -> any().
+dump(G) ->
+    Formatter = fun(Node) -> io_lib:format("~p", [Node]) end,
+    io:format("~s", [dump_1(G, Formatter)]).
+
+-spec dump(any(), any()) -> any().
+dump(G, FileName) ->
+    Formatter = fun(Node) -> io_lib:format("~p", [Node]) end,
+    dump(G, FileName, Formatter).
+
+-spec dump(any(), any(), any()) -> any().
+dump(G, FileName, Formatter) ->
+    {ok, Fd} = file:open(FileName, [write]),
+    io:fwrite(Fd, "~s", [dump_1(G, Formatter)]),
+    file:close(Fd).
+
+dump_1(G, Formatter) ->
+    Vs = maps:keys(G#graph.vs),
+
+    {Map, Vertices} = dump_vertices(Vs, 0, Formatter,#{}, []),
+    Edges = dump_edges(Vs, G, Map, []),
+
+    io_lib:format("graph g {~n~s~n~s~n}~n", [Vertices, Edges]).
+
+dump_vertices([V | Vs], Counter, Formatter, Map, Acc) ->
+    VerticeSlug = io_lib:format("    ~p [label=\"~s\"]~n",
+                                [Counter, Formatter(V)]),
+    dump_vertices(Vs, Counter + 1, Formatter,
+                  Map#{ V => Counter }, [VerticeSlug | Acc]);
+dump_vertices([], _Counter, _Formatter, Map, Acc) ->
+    {Map, Acc}.
+
+dump_edges([V | Vs], G, Map, Acc) ->
+    SelfId = map_get(V, Map),
+    EdgeSlug = [io_lib:format("    ~p -> ~p~n", [SelfId, map_get(To, Map)]) ||
+                {_, To, _} <- out_edges(G, V)],
+    dump_edges(Vs, G, Map, [EdgeSlug | Acc]);
+dump_edges([], _G, _Map, Acc) ->
+    Acc.
+
+-endif.
+
+
+%% ------------------------------------------------------------------------
+%% Graph utilities
+
+%%% Operations on directed (and undirected) graphs.
+%%%
+%%% Implementation based on Launchbury, John: Graph Algorithms with a
+%%% Functional Flavour, in Jeuring, Johan, and Meijer, Erik (Eds.):
+%%% Advanced Functional Programming, Lecture Notes in Computer
+%%% Science 925, Springer Verlag, 1995.
+
+%%
+%%  Exported functions
+%%
+
+-doc """
+Returns a list of [connected components](`m:graph#components`).
+
+Each component is represented by its vertices. The order of the vertices
+and the order of the components are arbitrary. Each vertex of graph `G`
+occurs in exactly one component.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec components(graph()) -> [[vertex()]].
+components(G) ->
+    revpreorders(G, fun inout/3).
+
+-doc """
+Returns a list of [strongly connected
+components](`m:graph#strong_components`).
+
+Each strongly component is represented by its vertices. The order of the
+vertices and the order of the components are arbitrary. Each vertex of
+graph `G` occurs in exactly one strong component.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec strong_components(graph()) -> [[vertex()]].
+strong_components(G) ->
+    revpreorders(G, fun in/3, reverse_postorder(G, vertices(G))).
+
+-doc """
+Returns a list of cyclic [strongly connected
+components](`m:graph#strong_components`).
+
+Each strongly component is represented by its vertices. The order of the
+vertices and the order of the components are arbitrary. Only vertices
+that are included in some [cycle](`m:graph#cycle`) in `G` are returned,
+otherwise the returned list is equal to that returned by
+`strong_components/1`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec cyclic_strong_components(graph()) -> [[vertex()]].
+cyclic_strong_components(G) ->
+    remove_singletons(strong_components(G), G, []).
+
+-doc """
+Returns an unsorted list of graph vertices such that for each vertex in the
+list, there is a [path](`m:graph#path`) in `G` from some vertex of `Vs` to
+the vertex.
+
+In particular, as paths can have length zero, the vertices of `Vs` are all
+included in the returned list.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reachable(graph(), [vertex()]) -> [vertex()].
+reachable(G, Vs) when is_list(Vs) ->
+    lists:append(revpreorders(G, fun out/3, Vs, first)).
+
+-doc """
+Returns an unsorted list of graph vertices such that for each vertex in the
+list, there is a [path](`m:graph#path`) in `G` of length one or more from
+some vertex of `Vs` to the vertex.
+
+Hence, vertices in `Vs` will only be included in the result if they are part
+of some [cycle](`m:graph#cycle`).
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reachable_via_neighbours(graph(), [vertex()]) -> [vertex()].
+reachable_via_neighbours(G, Vs) when is_list(Vs) ->
+    lists:append(revpreorders(G, fun out/3, Vs, not_first)).
+
+-doc """
+Returns an unsorted list of graph vertices such that for each vertex in the
+list, there is a [path](`m:graph#path`) from the vertex to some vertex of
+`Vs`.
+
+In particular, as paths can have length zero, the vertices of `Vs` are all
+included in the returned list.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reaching(graph(), [vertex()]) -> [vertex()].
+reaching(G, Vs) when is_list(Vs) ->
+    lists:append(revpreorders(G, fun in/3, Vs, first)).
+
+-doc """
+Returns an unsorted list of graph vertices such that for each vertex in the
+list, there is a [path](`m:graph#path`) of length one or more from the
+vertex to some vertex of `Vs`.
+
+Hence, vertices in `Vs` will only be included in the result if they are part
+of some [cycle](`m:graph#cycle`).
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reaching_via_neighbours(graph(), [vertex()]) -> [vertex()].
+reaching_via_neighbours(G, Vs) when is_list(Vs) ->
+    lists:append(revpreorders(G, fun in/3, Vs, not_first)).
+
+-doc """
+Returns a [topological ordering](`m:graph#topsort`) of the vertices of graph
+`G` if such an ordering exists, otherwise `false`.
+
+For each vertex in the returned list, no
+[out-neighbors](`m:graph#out_neighbour`) occur earlier in the list.
+
+This is currently implemented simply as `reverse_postorder(G)`, but this
+detail is subject to change and should not be relied on.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec topsort(graph()) -> [vertex()].
+topsort(G) ->
+    reverse_postorder(G).
+
+-doc """
+Returns `true` if and only if graph `G` is
+[acyclic](`m:graph#acyclic_graph`).
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec is_acyclic(graph()) -> boolean().
+is_acyclic(G) ->
+    cyclic_strong_components(G) =:= [].
+
+-doc """
+Returns a minimal list of vertices of `G` from which all vertices of `G` can
+be reached.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec roots(graph()) -> [vertex()].
+roots(G) ->
+    R1 = [V || V <- vertices(G), in_degree(G, V) =:= 0],
+    R2 = [X || [X|_] <- cyclic_strong_components(G)],
+    R1 ++ R2.
+
+-doc """
+Returns `{yes, V}` if `G` is an [arborescence](`m:graph#arborescence`) (a
+directed tree) with vertex `V` as the [root](`m:graph#root`), otherwise `no`.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec arborescence_root(graph()) -> 'no' | {'yes', vertex()}.
+arborescence_root(G) ->
+    case no_edges(G) =:= no_vertices(G) - 1 of
+        true ->
+            try
+                F = fun(V, Z) ->
+                            case in_degree(G, V) of
+                                1 -> Z;
+                                0 when Z =:= [] -> [V]
+                            end
+                    end,
+                [Root] = foldl(F, [], vertices(G)),
+                {yes, Root}
+            catch _:_ ->
+                no
+            end;
+        false ->
+            no
+    end.
+
+-doc """
+Returns `true` if and only if graph `G` is an
+[arborescence](`m:graph#arborescence`) (a directed tree with a unique root).
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec is_arborescence(graph()) -> boolean().
+is_arborescence(G) ->
+    arborescence_root(G) =/= no.
+
+-doc """
+Returns `true` if and only if graph `G` is a
+[tree](`m:graph#tree`), considering all edges undirected.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec is_tree(graph()) -> boolean().
+is_tree(G) ->
+    (no_edges(G) =:= no_vertices(G) - 1)
+    andalso (length(components(G)) =:= 1).
+
+-doc """
+Returns a list of all vertices of `G` that are included in some
+[loop](`m:graph#loop`).
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec loop_vertices(graph()) -> [vertex()].
+loop_vertices(G) ->
+    [V || V <- vertices(G), is_reflexive_vertex(G, V)].
+
+-doc(#{equiv => subgraph(G, Vs, [])}).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec subgraph(graph(), [vertex()]) -> graph().
+subgraph(G, Vs) ->
+    try
+        subgraph_opts(G, Vs, [])
+    catch
+        throw:badarg ->
+            erlang:error(badarg)
+    end.
+
+-doc """
+Creates a maximal [subgraph](`m:graph#subgraph`) of `G` restricted to the
+vertices listed in `Vs`.
+
+If the value of option `type` is `inherit`, which is the default, the type
+of `G` is used for the subgraph as well (for example, whether the graph
+allows cycles). Otherwise the value of the `type` option is used as argument
+to `new/1`.
+
+If the value of option `keep_labels` is `true`, which is the default, the
+[labels](`m:graph#label`) of vertices and edges of `G` are used for the
+subgraph as well. If the value is `false`, the vertices and edges of the
+subgraph will have the default labels.
+
+If any of the arguments are invalid, a `badarg` exception is raised.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec subgraph(graph(), [vertex()], Options) -> graph() when
+      Options :: [{'type', SubgraphType} | {'keep_labels', boolean()}],
+      SubgraphType :: 'inherit' | [graph_type()].
+subgraph(G, Vs, Options) ->
+    try
+        subgraph_opts(G, Vs, Options)
+    catch
+        throw:badarg ->
+            erlang:error(badarg)
+    end.
+
+-doc """
+Creates a graph where the vertices are the [strongly connected
+components](`m:graph#strong_components`) of `G` as returned by
+`strong_components/1`.
+
+If X and Y are two different strongly connected components, and vertices x
+and y exist in X and Y, respectively, such that there is an edge
+[emanating](`m:graph#emanate`) from x and [incident](`m:graph#incident`) on
+y, then an edge emanating from X and incident on Y is created.
+
+The created graph has the same type as `G`. All vertices and edges have
+the default [label](`m:graph#label`) `[]`.
+
+Each [cycle](`m:graph#cycle`) is included in some strongly connected
+component, which implies that a
+[topological ordering](`m:graph#topsort`) of the created graph always
+exists.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec condensation(graph()) -> graph().
+condensation(G) ->
+    SCs = strong_components(G),
+    %% Each component is assigned a number.
+    %% V2I: from vertex to number.
+    %% I2C: from number to component.
+    CFun = fun(SC, {N, V2I0, I2C0}) ->
+                   V2I1 = foldl(fun(V, Map) -> Map#{V => N} end,
+                                V2I0,
+                                SC),
+                   I2C1 = I2C0#{N => SC},
+                   {N + 1, V2I1, I2C1}
+           end,
+    {_, V2I, I2C} = foldl(CFun, {1, #{}, #{}}, SCs),
+    G0 = subgraph_opts(G, [], []),
+    foldl(fun(SC, SCG) -> condense(SC, G, SCG, V2I, I2C) end,
+          G0, SCs).
+
+-doc(#{ equiv => preorder(G, roots(G)) }).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec preorder(graph()) -> [vertex()].
+preorder(G) ->
+    preorder(G, roots(G)).
+
+-doc """
+Returns all vertices of graph `G` reachable from `Vs`, listed in pre-order.
+
+The order is given by a [depth-first
+traversal](`m:graph#depth_first_traversal`) of the graph, collecting visited
+vertices in preorder.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec preorder(graph(), [vertex()]) -> [vertex()].
+preorder(G, Vs) ->
+    T = sets:new(),
+    {_, Acc} = pretraverse_1(Vs, fun out/3, G, T, [], []),
+    reverse(lists:append(Acc)).
+
+-doc(#{ equiv => postorder(G, roots(G)) }).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec postorder(graph()) -> [vertex()].
+postorder(G) ->
+    postorder(G, roots(G)).
+
+-doc """
+Returns the vertices of graph `G` reachable from `Vs`, listed in post-order.
+
+The order is given by a [depth-first
+traversal](`m:graph#depth_first_traversal`) of the graph, collecting visited
+vertices in postorder. More precisely, the vertices visited while searching
+from an arbitrarily chosen vertex are collected in postorder, and all those
+collected vertices are placed before the subsequently visited vertices.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec postorder(graph(), [vertex()]) -> [vertex()].
+postorder(G, Vs) ->
+    T = sets:new(),
+    {Acc, _} = posttraverse(Vs, G, T, []),
+    reverse(Acc).
+
+-doc(#{ equiv => reverse_postorder(G, roots(G)) }).
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reverse_postorder(graph()) -> [vertex()].
+reverse_postorder(G) ->
+    reverse_postorder(G, roots(G)).
+
+-doc """
+Returns the vertices of graph `G` reachable from `Vs`, listed in reverse
+post-order.
+
+This effectively performs a topological sort of the reachable nodes.
+
+The graph is traversed as for `postorder/2`, but producing the result in
+reverse order.
+""".
+-doc(#{ since => ~"OTP 29.0"}).
+-spec reverse_postorder(graph(), [vertex()]) -> [vertex()].
+reverse_postorder(G, Vs) ->
+    T = sets:new(),
+    {L, _} = posttraverse(Vs, G, T, []),
+    L.
+
+%%
+%%  Local functions
+%%
+
+revpreorders(G, SF) ->
+    revpreorders(G, SF, vertices(G)).
+
+revpreorders(G, SF, Vs) ->
+    revpreorders(G, SF, Vs, first).
+
+%% returns a list of reverse preorder traversals using the given Vs as
+%% starting points (if a starting point V has already become visited by
+%% a previous traversal it will not be included again)
+revpreorders(G, SF, Vs, HandleFirst) ->
+    T = sets:new(),
+    F = fun(V, {T0, LL}) -> pretraverse(HandleFirst, V, SF, G, T0, LL) end,
+    {_, LL} = foldl(F, {T, []}, Vs),
+    LL.
+
+pretraverse(first, V, SF, G, T, LL) ->
+    pretraverse_1([V], SF, G, T, [], LL);
+pretraverse(not_first, V, SF, G, T, LL) ->
+    %% used by reachable_neighbours/2 and reaching_neighbours/2
+    case sets:is_element(V, T) of
+        false -> pretraverse_1(SF(G, V, []), SF, G, T, [], LL);
+        true  -> {T, LL}
+    end.
+
+%% generic preorder traversal loop; given a starting set Vs of vertices
+%% of G, T tracks seen vertices, the SF function queues up neighbour
+%% vertexes, and the resulting list Rs of reached vertices (in reverse
+%% preorder) is prepended onto LL unless it is empty
+pretraverse_1([V | Vs], SF, G, T0, Rs, LL) ->
+    case sets:is_element(V, T0) of
+        false ->
+            T1 = sets:add_element(V, T0),
+            pretraverse_1(SF(G, V, Vs), SF, G, T1, [V | Rs], LL);
+        true ->
+            pretraverse_1(Vs, SF, G, T0, Rs, LL)
+    end;
+pretraverse_1([], _SF, _G, T, [], LL) ->
+    {T, LL};
+pretraverse_1([], _SF, _G, T, Rs, LL) ->
+    {T, [Rs | LL]}.
+
+%% similar to pretraverse_1 but accumulates onto a single list, in reverse
+%% postorder, and only for out-edges
+posttraverse([V | Vs], G, T0, Acc0) ->
+    case sets:is_element(V, T0) of
+        false ->
+            T1 = sets:add_element(V, T0),
+            {Acc1, T2} = posttraverse(out(G, V, []), G, T1, Acc0),
+            posttraverse(Vs, G, T2, [V|Acc1]);
+        true ->
+            posttraverse(Vs, G, T0, Acc0)
+    end;
+posttraverse([], _G, T, Acc) ->
+    {Acc, T}.
+
+in(G, V, Vs) ->
+    in_neighbours(G, V) ++ Vs.
+
+out(G, V, Vs) ->
+    out_neighbours(G, V) ++ Vs.
+
+inout(G, V, Vs) ->
+    in(G, V, out(G, V, Vs)).
+
+remove_singletons([C=[V] | Cs], G, L) ->
+    case is_reflexive_vertex(G, V) of
+        true  -> remove_singletons(Cs, G, [C | L]);
+        false -> remove_singletons(Cs, G, L)
+    end;
+remove_singletons([C | Cs], G, L) ->
+    remove_singletons(Cs, G, [C | L]);
+remove_singletons([], _G, L) ->
+    L.
+
+is_reflexive_vertex(G, V) ->
+    lists:member(V, out_neighbours(G, V)).
+
+subgraph_opts(G, Vs, Opts) ->
+    subgraph_opts(Opts, inherit, true, G, Vs).
+
+subgraph_opts([{type, Type} | Opts], _Type0, Keep, G, Vs)
+  when Type =:= inherit; is_list(Type) ->
+    subgraph_opts(Opts, Type, Keep, G, Vs);
+subgraph_opts([{keep_labels, Keep} | Opts], Type, _Keep0, G, Vs)
+  when is_boolean(Keep) ->
+    subgraph_opts(Opts, Type, Keep, G, Vs);
+subgraph_opts([], inherit, Keep, G, Vs) ->
+    Info = info(G),
+    {_, {_, Cyclicity}} = lists:keysearch(cyclicity, 1, Info),
+    subgraph(G, Vs, [Cyclicity], Keep);
+subgraph_opts([], Type, Keep, G, Vs) ->
+    subgraph(G, Vs, Type, Keep);
+subgraph_opts(_, _Type, _Keep, _G, _Vs) ->
+    throw(badarg).
+
+subgraph(G, Vs, Type, Keep) ->
+    try new(Type) of
+        SG0 ->
+            SG1 = foldl(fun(V, SG) -> subgraph_vertex(V, G, SG, Keep) end,
+                        SG0, Vs),
+            EFun = fun(V, SGv) -> foldl(fun(E, SG) ->
+                                                subgraph_edge(E, SG, Keep)
+                                        end,
+                                        SGv,
+                                        out_edges(G, V))
+                   end,
+            foldl(EFun, SG1, vertices(SG1))
+    catch
+        error:badarg ->
+            throw(badarg)
+    end.
+
+subgraph_vertex(V, G, SG, Keep) ->
+    case has_vertex(G, V) of
+        false -> SG;
+        true when not Keep -> add_vertex(SG, V);
+        true when Keep -> add_vertex(SG, V, vertex(G, V))
+    end.
+
+subgraph_edge({V1, V2, Label}, SG, Keep) ->
+    case has_vertex(SG, V2) of
+        false -> SG;
+        true when not Keep -> add_edge(SG, V1, V2);
+        true when Keep -> add_edge(SG, V1, V2, Label)
+    end.
+
+condense(SC, G, SCG, V2I, I2C) ->
+    NFun = fun(Neighbour, T0) ->
+                   I = maps:get(Neighbour, V2I),
+                   T0#{I => true}
+           end,
+    VFun = fun(V, T0) -> foldl(NFun, T0, out_neighbours(G, V)) end,
+    T = foldl(VFun, #{}, SC),
+    maps:fold(fun (I, true, SCG0) ->
+                      C = maps:get(I, I2C),
+                      SCG1 = add_vertex(SCG0, C),
+                      if C =/= SC ->
+                              add_edge(SCG1, SC, C);
+                         true ->
+                              SCG1
+                      end
+              end,
+              add_vertex(SCG, SC),
+              T).
diff --git a/lib/stdlib/src/stdlib.app.src b/lib/stdlib/src/stdlib.app.src
index 7f42f85737..85d1298e40 100644
--- a/lib/stdlib/src/stdlib.app.src
+++ b/lib/stdlib/src/stdlib.app.src
@@ -75,6 +75,7 @@
 	     gen_fsm,
 	     gen_server,
 	     gen_statem,
+	     graph,
 	     io,
 	     io_lib,
 	     io_lib_format,
diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile
index 7df2b43eac..f05e67ded4 100644
--- a/lib/stdlib/test/Makefile
+++ b/lib/stdlib/test/Makefile
@@ -40,6 +40,7 @@ MODULES= \
 	dets_SUITE \
 	dict_SUITE \
 	dict_test_lib \
+	graph_SUITE \
 	digraph_SUITE \
 	digraph_utils_SUITE \
 	dummy1_h \
diff --git a/lib/stdlib/test/graph_SUITE.erl b/lib/stdlib/test/graph_SUITE.erl
new file mode 100644
index 0000000000..d95e2e9e58
--- /dev/null
+++ b/lib/stdlib/test/graph_SUITE.erl
@@ -0,0 +1,830 @@
+%%
+%% %CopyrightBegin%
+%%
+%% SPDX-License-Identifier: Apache-2.0
+%%
+%% Copyright Ericsson AB 1996-2026. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(graph_SUITE).
+
+%%-define(STANDALONE,1).
+
+-ifdef(STANDALONE).
+-define(line, put(line, ?LINE), ).
+-else.
+-include_lib("common_test/include/ct.hrl").
+-endif.
+
+-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
+         init_per_group/2,end_per_group/2]).
+
+-export([opts/1, degree/1, path/1, cycle/1, vertices/1,
+         edges/1, data/1, otp_3522/1, otp_3630/1, otp_8066/1, vertex_names/1]).
+
+-export([simple/1, loop/1, roots/1, isolated/1, topsort/1, subgraph/1,
+         condensation/1, tree/1, traversals/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+suite() -> [{ct_hooks,[ts_install_cth]}].
+
+all() ->
+    [opts, degree, path, cycle, {group, misc},
+     {group, tickets}, {group, utils}].
+
+groups() ->
+    [{misc, [], [vertices, edges, data, vertex_names]},
+     {utils, [], [simple, loop, roots, isolated, topsort, subgraph,
+                  condensation, tree, traversals]},
+     {tickets, [], [otp_3522, otp_3630, otp_8066]}].
+
+init_per_suite(Config) ->
+    Config.
+
+end_per_suite(_Config) ->
+    ok.
+
+init_per_group(_GroupName, Config) ->
+    Config.
+
+end_per_group(_GroupName, Config) ->
+    Config.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+opts(Config) when is_list(Config) ->
+    Template = [{v1,[v2]}, {v2,[v3]}, {v3,[v4]}, {v4,[]}],
+    G4 = build_graph([], Template),
+    graph:add_edge(G4, v4, v1, []),
+    G5 = build_graph([cyclic], Template),
+    graph:add_edge(G5, v4, v1, []),
+    G6 = build_graph([acyclic], Template),
+    acyclic = info(G6, cyclicity),
+    try graph:add_edge(G6, v4, v1) of
+        _ -> error(cycle_not_detected)
+    catch
+        error: {bad_edge,{v4,v1}} -> ok
+    end,
+    ok.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+degree(Config) when is_list(Config) ->
+    G = build_graph([], [{x1,[]}, {x2,[x1]}, {x3,[x1,x2]},
+                         {x4,[x1,x2,x3]}, {x5,[x1,x2,x3,x4]}]),
+    %% out degree
+    0 = graph:out_degree(G, x1),
+    1 = graph:out_degree(G, x2),
+    2 = graph:out_degree(G, x3),
+    3 = graph:out_degree(G, x4),
+    4 = graph:out_degree(G, x5),
+    %% out neighbours
+    [] = check(graph:out_neighbours(G, x1), []),
+    [] = check(graph:out_neighbours(G, x2), [x1]),
+    [] = check(graph:out_neighbours(G, x3), [x1,x2]),
+    [] = check(graph:out_neighbours(G, x4), [x1,x2,x3]),
+    [] = check(graph:out_neighbours(G, x5), [x1,x2,x3,x4]),
+
+    %% in degree
+    4 = graph:in_degree(G, x1),
+    3 = graph:in_degree(G, x2),
+    2 = graph:in_degree(G, x3),
+    1 = graph:in_degree(G, x4),
+    0 = graph:in_degree(G, x5),
+    %% in neighbours
+    [] = check(graph:in_neighbours(G, x1), [x2,x3,x4,x5]),
+    [] = check(graph:in_neighbours(G, x2), [x3,x4,x5]),
+    [] = check(graph:in_neighbours(G, x3), [x4,x5]),
+    [] = check(graph:in_neighbours(G, x4), [x5]),
+    [] = check(graph:in_neighbours(G, x5), []),
+    ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+path(Config) when is_list(Config) ->
+    G = build_graph([], [{x1,[x2,x3]}, {x2,[x4]}, {x3,[x4]},
+                         {x4,[x5,x6]}, {x5,[x7]}, {x6,[x7]}]),
+    {G1, Vi} = case graph:get_path(G, x1, x7) of
+             [x1,x2,x4,x5,x7] -> {graph:del_vertex(G, x5), x6};
+             [x1,x2,x4,x6,x7] -> {graph:del_vertex(G, x6), x5};
+             [x1,x3,x4,x5,x7] -> {graph:del_vertex(G, x5), x6};
+             [x1,x3,x4,x6,x7] -> {graph:del_vertex(G, x6), x5}
+         end,
+    {G2, Vj} = case graph:get_path(G1, x1, x7) of
+             [x1,x2,x4,Vi,x7] -> {graph:del_vertex(G1,x2), x3};
+             [x1,x3,x4,Vi,x7] -> {graph:del_vertex(G1,x3), x2}
+         end,
+    [x1,Vj,x4,Vi,x7] = graph:get_path(G2, x1, x7),
+    G3 = graph:del_vertex(G2, Vj),
+    false = graph:get_path(G3, x1, x7),
+    [] = check(graph:vertices(G3), [x1,x4,Vi,x7]),
+    ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+cycle(Config) when is_list(Config) ->
+    G = build_graph([], [{x1,[x2,x3]}, {x2,[x4]}, {x3,[x4]},
+                         {x4,[x5,x6]}, {x5,[x7]}, {x6,[x7,x8]},
+                         {x8,[x3,x8]}]),
+    false = graph:get_cycle(G, x1),
+    false = graph:get_cycle(G, x2),
+    false = graph:get_cycle(G, x5),
+    false = graph:get_cycle(G, x7),
+    [x3,x4,x6,x8,x3] = graph:get_cycle(G, x3),
+    [x4,x6,x8,x3,x4] = graph:get_cycle(G, x4),
+    [x6,x8,x3,x4,x6] = graph:get_cycle(G, x6),
+    [x8,x3,x4,x6,x8] = graph:get_cycle(G, x8),
+    G1 = graph:del_vertex(G, x4),
+    [x8] = graph:get_cycle(G1, x8),
+    ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+
+vertices(Config) when is_list(Config) ->
+    G = build_graph([], [{x,[]}, {y,[]}]),
+    [] = check(graph:vertices(G), [x,y]),
+    G1 = graph:del_vertices(G, [x,y]),
+    [] = graph:vertices(G1),
+    ok.
+
+edges(Config) when is_list(Config) ->
+    G = build_graph([], [{x, [{exy,y},{exx,x}]},
+                         {y, [{eyx,x}]}
+                        ]),
+    [] = check(labels(graph:edges(G)), [exy, eyx, exx]),
+    [] = check(labels(graph:out_edges(G, x)), [exy,exx]),
+    [] = check(labels(graph:in_edges(G, x)), [eyx,exx]),
+    [] = check(labels(graph:out_edges(G, y)), [eyx]),
+    [] = check(labels(graph:in_edges(G, y)), [exy]),
+    G1 = del_edges(G, [exy, eyx, does_not_exist]),
+    [exx] = labels(graph:edges(G1)),
+    [] = check(labels(graph:out_edges(G1, x)), [exx]),
+    [] = check(labels(graph:in_edges(G1, x)), [exx]),
+    [] = check(labels(graph:out_edges(G1, y)), []),
+    [] = check(labels(graph:in_edges(G1, y)), []),
+    G2 = graph:del_vertices(G1, [x,y]),
+    [] = graph:edges(G2),
+    [] = graph:vertices(G2),
+    ok.
+
+del_edges(G, Ls) ->
+    Es = lists:usort(graph:edges(G)),
+    lists:foldl(fun({_,_,L}=E, Dg) ->
+                        case lists:member(L, Ls) of
+                            false -> Dg;
+                            true -> graph:del_edge(Dg, E)
+                        end
+                end,
+                G, Es).
+
+data(Config) when is_list(Config) ->
+    G = build_graph([], [{x, [{exy, y}]}, {y, []}]),
+
+    [] = graph:vertex(G, x),
+    [] = graph:vertex(G, y),
+    [E] = graph:edges(G),
+    {x,y,exy} = E,
+
+    G1 = graph:add_edge(G, x, y, label_1),
+    G2 = graph:add_edge(G1, x, y, label_2), %E
+    G3 = graph:add_vertex(G2, x, {any}),
+    G4 = graph:add_vertex(G3, y, '_'), % not a wildcard
+
+    {any} = graph:vertex(G4, x),
+    '_' = graph:vertex(G4, y),
+    [E,{x,y,label_1},{x,y,label_2}] = lists:sort(graph:edges(G4)),
+    G5 = graph:del_edge(G4, {x,y,label_1}),
+    [E,{x,y,label_2}] = lists:sort(graph:edges(G5)),
+    true = sane(G5),
+    ok.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+
+otp_3522(Config) when is_list(Config) ->
+    G0 = build_graph([acyclic], [{x, []}]),
+    try graph:add_edge(G0, x, x) of
+        _ -> error(cycle_not_detected)
+    catch
+        error: {bad_edge,{x,x}} -> ok
+    end,
+
+    G = graph:new(),
+    0 = graph:no_vertices(G),
+    0 = graph:no_edges(G),
+    {V1, G1} = graph:add_vertex(G),
+    G2 = graph:add_vertex(G1, '$vid'),
+    {V2, G3} = graph:add_vertex(G2),
+    G4 = graph:add_edge(G3, V1, V2, l1),
+    G5 = graph:add_edge(G4, V1, V2),
+    3 = graph:no_vertices(G5),
+    2 = graph:no_edges(G5),
+    cyclic = info(G5, cyclicity),
+
+    [] = check(labels(graph:in_edges(G5, V2)), [l1, []]),
+    [] = check(labels(graph:out_edges(G5, V1)), [l1, []]),
+    [] = check(graph:vertices(G5), [V1,V2,'$vid']),
+    [] = check(labels(graph:edges(G5)), [[], l1]),
+    true = sane(G5),
+    ok.
+
+otp_3630(Config) when is_list(Config) ->
+    G = build_graph([], [{x, [{exy,y},{exx,x}]},
+                         {y, [{eyy,y},{eyx,x}]}
+                        ]),
+    [x,y] = graph:get_path(G, x, y),
+    [y,x] = graph:get_path(G, y, x),
+
+    [x,x] = graph:get_short_path(G, x, x),
+    [y,y] = graph:get_short_path(G, y, y),
+
+    G1 = build_graph([], [{1, [{12,2},{13,3},{11,1}]},
+                          {2, [{23,3}]},
+                          {3, [{34,4},{35,5}]},
+                          {4, [{45,5}]},
+                          {5, [{56,6},{57,7}]},
+                          {6, [{67,7}]},
+                          {7, [{71,1}]}
+                         ]),
+
+    [1,3,5,7] = graph:get_short_path(G1, 1, 7),
+    [3,5,7,1,3] = graph:get_short_cycle(G1, 3),
+    [1,1] = graph:get_short_cycle(G1, 1),
+
+    F = 0.0, I = round(F),
+    G2 = graph:new([acyclic]),
+    G3 = graph:add_vertex(G2, F),
+    G4 = graph:add_vertex(G3, I),
+    G5 = graph:add_edge(G4, F, I),
+    true = sane(G5),
+
+    ok.
+
+otp_8066(Config) when is_list(Config) ->
+    fun() ->
+            D = graph:new(),
+            {V1, D1} = graph:add_vertex(D),
+            {V2, D2} = graph:add_vertex(D1),
+            D3 = graph:add_edge(D2, V1, V2),
+            [V1, V2] = graph:get_path(D3, V1, V2),
+            true = sane(D3),
+            D4 = graph:del_path(D3, V1, V2),
+            true = sane(D4),
+            false = graph:get_path(D4, V1, V2),
+            graph:del_path(D4, V1, V2)
+    end(),
+
+    fun() ->
+            D = graph:new(),
+            {V1, D1} = graph:add_vertex(D),
+            {V2, D2} = graph:add_vertex(D1),
+            D3 = graph:add_edge(D2, V1, V2),
+            D4 = graph:add_edge(D3, V1, V2),
+            D5 = graph:add_edge(D4, V1, V1),
+            D6 = graph:add_edge(D5, V2, V2),
+            [V1, V2] = graph:get_path(D6, V1, V2),
+            true = sane(D6),
+            D7 = graph:del_path(D6, V1, V2),
+            false = graph:get_short_path(D7, V2, V1),
+
+            true = sane(D7),
+            false = graph:get_path(D7, V1, V2),
+            graph:del_path(D7, V1, V2)
+    end(),
+
+    fun() ->
+            G = graph:new(),
+            {W1, G1} = graph:add_vertex(G),
+            {W2, G2} = graph:add_vertex(G1),
+            {_W3, G3} = graph:add_vertex(G2),
+            {_W4, G4} = graph:add_vertex(G3),
+            G5 = graph:add_edge(G4, W1, W2, ['$e'|0]),
+            try graph:add_edge(G5, bv, W1) of
+                _ -> error(bad_edge_not_detected)
+            catch
+                error: {bad_vertex, bv} -> ok
+            end,
+            try graph:add_edge(G5, W1, bv) of
+                _ -> error(bad_edge_not_detected)
+            catch
+                error: {bad_vertex, bv} -> ok
+            end,
+            false = graph:get_short_cycle(G5, W1),
+            true = sane(G5)
+    end(),
+    ok.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+vertex_names(Config) when is_list(Config) ->
+    G = graph:new([acyclic]),
+    A = 'A',
+    B = '_',
+    G1 = graph:add_vertex(G, A),
+    G2 = graph:add_vertex(G1, B),
+    G3 = graph:add_edge(G2, A, B),
+    AB = {A,B,[]},
+
+    %% Link A -> B
+    1 = graph:out_degree(G3, A),
+    1 = graph:in_degree(G3, B),
+    0 = graph:out_degree(G3, B),
+    0 = graph:in_degree(G3, A),
+    [B] = graph:out_neighbours(G3, A),
+    [A] = graph:in_neighbours(G3, B),
+    [] = graph:out_neighbours(G3, B),
+    [] = graph:in_neighbours(G3, A),
+    [AB] = graph:out_edges(G3, A),
+    [AB] = graph:in_edges(G3, B),
+    [] = graph:out_edges(G3, B),
+    [] = graph:in_edges(G3, A),
+
+    %% Reverse the edge
+    G4 = graph:del_edge(G3, AB),
+    G5 = graph:add_edge(G4, B, A),
+    BA = {B,A,[]},
+
+    1 = graph:out_degree(G5, B),
+    1 = graph:in_degree(G5, A),
+    0 = graph:out_degree(G5, A),
+    0 = graph:in_degree(G5, B),
+    [A] = graph:out_neighbours(G5, B),
+    [B] = graph:in_neighbours(G5, A),
+    [] = graph:out_neighbours(G5, A),
+    [] = graph:in_neighbours(G5, B),
+    [BA] = graph:out_edges(G5, B),
+    [BA] = graph:in_edges(G5, A),
+    [] = graph:out_edges(G5, A),
+    [] = graph:in_edges(G5, B),
+
+    ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+sane(G) ->
+    sane1(G),
+    erase(sane) =:= undefined.
+
+sane1(G) ->
+    Es = graph:edges(G),
+    Vs = graph:vertices(G),
+    VEs = lists:flatmap(fun(V) -> graph:edges(G, V) end, Vs),
+    case lists:sort(Es++Es) =:= lists:sort(VEs) of
+        true -> ok;
+        false ->
+            io:format("Bad edges~n", []), put(sane, no)
+    end,
+
+    lists:foreach(
+      fun({V1, V2, _L}=Edge) ->
+              case {graph:vertex(G, V1, none),
+                    graph:vertex(G, V2, none)} of
+                  {L1, L2} when L1 =/= none, L2 =/= none -> ok;
+                  _ -> io:format("Missing vertex ~p~n", [Edge]), put(sane, no)
+              end,
+              In = graph:in_edges(G, V2),
+              case lists:member(Edge, In) of
+                  true -> ok;
+                  false ->
+                      io:format("Missing in-neighbour ~p~n", [Edge]),
+                      put(sane, no)
+              end,
+              Out = graph:out_edges(G, V1),
+              case lists:member(Edge, Out) of
+                  true -> ok;
+                  false ->
+                      io:format("Missing out-neighbour ~p~n", [Edge]),
+                      put(sane, no)
+              end
+      end, Es),
+
+    lists:foreach(
+      fun(V) ->
+              InEs = graph:in_edges(G, V),
+              %% *All* in-edges of V
+              lists:foreach(
+                fun(E) ->
+                        case E of
+                            {_, V, _} -> ok;
+                            _ ->
+                                io:format("Bad in-edge ~p: ~p~n", [V, E]),
+                                put(sane, no)
+                        end
+                end, InEs),
+              OutEs = graph:out_edges(G, V),
+              lists:foreach(
+                fun(E) ->
+                        case E of
+                            {V, _, _} -> ok;
+                            _ ->
+                                io:format("Bad out-edge ~p: ~p~n", [V, E]),
+                                put(sane, no)
+                        end
+                end, OutEs)
+      end, Vs),
+
+    InEs = lists:flatmap(fun(V) -> graph:in_edges(G, V) end, Vs),
+    OutEs = lists:flatmap(fun(V) -> graph:out_edges(G, V) end, Vs),
+    lists:foreach(
+      fun(E) ->
+              case E of
+                  {_, _, _} -> ok;
+                  _ ->
+                      io:format("Unknown edge (neighbour) ~p~n", [E]),
+                      put(sane, no)
+              end
+      end, InEs++OutEs),
+
+    N_in = length(InEs),
+    N_out = length(OutEs),
+    N_edges = graph:no_edges(G),
+    if
+        N_in =/= N_out ->
+            io:format("Number of in- and out-edges differs~n", []),
+            put(sane, no);
+        N_in+N_out =/= N_edges+N_edges  ->
+            io:format("Invalid number of edges (~p+~p =/= 2*~p)~n",
+                      [N_in, N_out, N_edges]),
+            put(sane, no);
+        true -> ok
+    end,
+    EVs = lists:usort([V || {V, _, _} <- Es] ++
+                          [V || {_, V, _} <- Es]),
+    lists:foreach(
+      fun(V) ->
+              case graph:vertex(G, V, none) of
+                  none ->
+                      io:format("Unknown vertex in edge: ~p~n", [V]),
+                      put(sane, no);
+                  _ -> ok
+              end
+      end, EVs),
+
+    %% sink: a vertex with no outgoing edges
+    SinkVs = [V || V <- Vs, graph:out_edges(G, V) =:= [] ],
+    case lists:sort(SinkVs) =:=  lists:sort(graph:sink_vertices(G)) of
+        true -> ok;
+        false ->
+            io:format("Bad sinks~n"), put(sane, no)
+    end,
+    %% source: a vertex with no incoming edges
+    SourceVs = [V || V <- Vs, graph:in_edges(G, V) =:= [] ],
+    case lists:sort(SourceVs) =:=  lists:sort(graph:source_vertices(G)) of
+        true -> ok;
+        false ->
+            io:format("Bad sources~n"), put(sane, no)
+    end,
+
+    true.
+
+build_graph(Opts, Gs) ->
+    G = graph:new(Opts),
+    build_g(G, Gs).
+
+build_g(G, [{V,Ns} | Gs]) ->
+    G1 = graph:add_vertex(G, V),
+    G2 = build_ns(G1, V, Ns),
+    build_g(G2, Gs);
+build_g(G, []) ->
+    true = sane(G),
+    G.
+
+build_ns(G, V, [{L,W} | Ns]) ->
+    G1 = graph:add_vertex(G, W),
+    G2 = graph:add_edge(G1, V, W, L),
+    build_ns(G2, V, Ns);
+build_ns(G, V, [W | Ns]) ->
+    G1 = graph:add_vertex(G, W),
+    G2 = graph:add_edge(G1, V, W),
+    build_ns(G2, V, Ns);
+build_ns(G, _V, []) ->
+    G.
+
+info(G, What) ->
+    case lists:keysearch(What, 1, graph:info(G)) of
+        {value, {What, Value}} -> Value;
+        false -> []
+    end.
+
+labels(List) ->
+    [label(X) || X <- List].
+
+label({_V1,_V2,L}) -> L;
+label({_V, L}) -> L.
+
+check(R0, E0) ->
+    R = lists:sort(R0),
+    E = lists:sort(E0),
+    case R of
+        E ->
+            [];
+        _ ->
+            (R -- E) ++ (E -- R)
+    end.
+
+
+%% ------------------------------------------------------------------------
+%% Graph utilities tests
+
+simple(Config) when is_list(Config) ->
+    G0 = graph:new(),
+    G1 = add_vertices(G0, [a]),
+    G = add_edges(G1, [{b,c},{b,d},{e,f},{f,g},{g,e},{h,h},
+                       {i,i},{i,ij1,j},{i,ij2,j}]),
+    10 = length(graph:postorder(G)),
+    10 = length(graph:preorder(G)),
+    ok = evall(graph:components(G),
+               [[a],[b,c,d],[e,f,g],[h],[i,j]]),
+    ok = evall(graph:strong_components(G),
+               [[a],[b],[c],[d],[e,f,g],[h],[i],[j]]),
+    ok = evall(graph:cyclic_strong_components(G),
+               [[e,f,g],[h],[i]]),
+    true = path(G, e, e),
+    false = path(G, e, j),
+    false = path(G, a, a),
+    ok = eval(graph:topsort(G), [a,b,c,d,e,f,g,h,i,j]),
+    false = graph:is_acyclic(G),
+    true = graph:has_vertex(G, h),
+    false = graph:has_vertex(G, z),
+    true = graph:has_edge(G, {i, j, ij1}),
+    false = graph:has_edge(G, {j, i, ij1}),
+    true = graph:has_edge(G, {i, j, ij2}),
+    false = graph:has_edge(G, {j, i, ij2}),
+    true = graph:has_edge(G, i, j),
+    false = graph:has_edge(G, j, i),
+    [{i,j,ij1},{i,j,ij2}] = graph:edges(G, i, j),
+    [] = graph:edges(G, j, i),
+    ok = eval(graph:loop_vertices(G), [h,i]),
+    ok = eval(graph:reaching(G, [e]), [e,f,g]),
+    ok = eval(graph:reaching_via_neighbours(G, [e]), [e,f,g]),
+    ok = eval(graph:reachable(G, [e]), [e,f,g]),
+    ok = eval(graph:reachable_via_neighbours(G, [e]), [e,f,g]),
+    ok = eval(graph:reaching(G, [b]), [b]),
+    ok = eval(graph:reaching_via_neighbours(G, [b]), []),
+    ok = eval(graph:reachable(G, [b]), [b,c,d]),
+    ok = eval(graph:reachable_via_neighbours(G, [b]), [c,d]),
+    ok = eval(graph:reaching(G, [h]), [h]),
+    ok = eval(graph:reaching_via_neighbours(G, [h]), [h]),
+    ok = eval(graph:reachable(G, [h]), [h]),
+    ok = eval(graph:reachable_via_neighbours(G, [h]), [h]),
+    ok = eval(graph:reachable(G, [e,f]), [e,f,g]),
+    ok = eval(graph:reachable_via_neighbours(G, [e,f]), [e,f,g]),
+    ok = eval(graph:reachable(G, [h,h,h]), [h]),
+    ok.
+
+roots(Config) when is_list(Config) ->
+    G0 = graph:new(),
+    G1 = add_vertices(G0, [a]),
+    G = add_edges(G1, [{a,b},{b,c},{c,a},{c,d},{j,j},{j,k},{j,l}]),
+    7 = length(graph:postorder(G)),
+    7 = length(graph:preorder(G)),
+    ok.
+
+loop(Config) when is_list(Config) ->
+    G0 = graph:new(),
+    G1 = add_vertices(G0, [a,b]),
+    G = add_edges(G1, [{a,a},{b,b}]),
+    ok = evall(graph:components(G), [[a],[b]]),
+    ok = evall(graph:strong_components(G), [[a],[b]]),
+    ok = evall(graph:cyclic_strong_components(G), [[a],[b]]),
+    [_,_] = graph:topsort(G),
+    false = graph:is_acyclic(G),
+    ok = eval(graph:loop_vertices(G), [a,b]),
+    [_,_] = graph:preorder(G),
+    [_,_] = graph:postorder(G),
+    ok = eval(graph:reaching(G, [b]), [b]),
+    ok = eval(graph:reaching_via_neighbours(G, [b]), [b]),
+    ok = eval(graph:reachable(G, [b]), [b]),
+    ok = eval(graph:reachable_via_neighbours(G, [b]), [b]),
+    true = path(G, a, a),
+    ok.
+
+isolated(Config) when is_list(Config) ->
+    G0 = graph:new(),
+    G = add_vertices(G0, [a,b]),
+    ok = evall(graph:components(G), [[a],[b]]),
+    ok = evall(graph:strong_components(G), [[a],[b]]),
+    ok = evall(graph:cyclic_strong_components(G), []),
+    [_,_] = graph:topsort(G),
+    true = graph:is_acyclic(G),
+    ok = eval(graph:loop_vertices(G), []),
+    [_,_] = graph:preorder(G),
+    [_,_] = graph:postorder(G),
+    ok = eval(graph:reaching(G, [b]), [b]),
+    ok = eval(graph:reaching_via_neighbours(G, [b]), []),
+    ok = eval(graph:reachable(G, [b]), [b]),
+    ok = eval(graph:reachable_via_neighbours(G, [b]), []),
+    false = path(G, a, a),
+    ok.
+
+topsort(Config) when is_list(Config) ->
+    G0 = graph:new(),
+    G = add_edges(G0, [{a,b},{b,c},{c,d},{d,e},{e,f}]),
+    ok = eval(graph:topsort(G), [a,b,c,d,e,f]),
+    ok.
+
+subgraph(Config) when is_list(Config) ->
+    G0 = graph:new([acyclic]),
+    G1 = add_edges(G0, [{b,c},{b,d},{e,f},{f,fg,g},{f,fg2,g},{h,i},{i,j}]),
+    G = add_vertices(G1, [{b,bl},{f,fl}]),
+    SG = graph:subgraph(G, [u1,b,c,u2,f,g,i,u3]),
+    [b,c,f,g,i] = lists:sort(graph:vertices(SG)),
+    bl = graph:vertex(SG, b),
+    [] = graph:vertex(SG, c),
+    [{f,g,fg},{f,g,fg2}] = graph:edges(SG, f, g),
+    {_, {_, acyclic}} = lists:keysearch(cyclicity, 1, graph:info(SG)),
+
+    SG1 = graph:subgraph(G, [f, g, h],
+                                 [{type, []}, {keep_labels, false}]),
+    [f,g,h] = lists:sort(graph:vertices(SG1)),
+    [] = graph:vertex(SG1, f),
+    [{f,g,[]}] = graph:edges(SG1, f, g),
+    {_, {_, cyclic}} = lists:keysearch(cyclicity, 1, graph:info(SG1)),
+
+    SG2 = graph:subgraph(G, [f, g, h],
+                                 [{type, [acyclic]},
+                                  {keep_labels, true}]),
+    [f,g,h] = lists:sort(graph:vertices(SG2)),
+    fl = graph:vertex(SG2, f),
+    [{f,g,fg},{f,g,fg2}] = graph:edges(SG2, f, g),
+    {_, {_, acyclic}} = lists:keysearch(cyclicity, 1, graph:info(SG2)),
+
+    {'EXIT',{badarg,_}} =
+        (catch graph:subgraph(G, [f], [{invalid, opt}])),
+    {'EXIT',{badarg,_}} =
+        (catch graph:subgraph(G, [f], [{keep_labels, not_Bool}])),
+    {'EXIT',{badarg,_}} =
+        (catch graph:subgraph(G, [f], [{type, not_type}])),
+    {'EXIT',{badarg,_}} =
+        (catch graph:subgraph(G, [f], [{type, [not_type]}])),
+    {'EXIT',{badarg,_}} =
+        (catch graph:subgraph(G, [f], not_a_list)),
+
+    ok.
+
+condensation(Config) when is_list(Config) ->
+    G0 = graph:new([]),
+    G1 = add_edges(G0, [{b,c},{b,d},{e,f},{f,fgl,g},{f,fgl2,g},{g,e},
+                        {h,h},{j,i},{i,j}]),
+    G = add_vertices(G1, [q]),
+    CG = graph:condensation(G),
+    Vs = sort_2(graph:vertices(CG)),
+    [[b],[c],[d],[e,f,g],[h],[i,j],[q]] = Vs,
+    Fun = fun(E) ->
+                  {V1, V2, _L} = E,
+                  {lists:sort(V1), lists:sort(V2)}
+          end,
+    Es = lists:map(Fun, graph:edges(CG)),
+    [{[b],[c]},{[b],[d]}] = lists:sort(Es),
+    ok.
+
+%% OTP-7081
+tree(Config) when is_list(Config) ->
+    false = is_tree([], []),
+    true = is_tree([a], []),
+    false = is_tree([a,b], []),
+    true = is_tree([{a,b}]),
+    false = is_tree([{a,b},{b,a}]),
+    true = is_tree([{a,b},{a,c},{b,d},{b,e}]),
+    false = is_tree([{a,b},{a,c},{b,d},{b,e},{d,e}]),
+    false = is_tree([{a,b},{a,c},{b,d},{b,l1,e},{b,l2,e}]),
+    true = is_tree([{a,c},{c,b}]),
+    true = is_tree([{b,a},{c,a}]),
+    %% Parallel edges. Acyclic and with one component
+    false = is_tree([{a,l1,b},{a,l2,b}]),
+
+    no = arborescence_root([], []),
+    {yes, a} = arborescence_root([a], []),
+    no = arborescence_root([a,b], []),
+    {yes, a} = arborescence_root([{a,b}]),
+    no = arborescence_root([{a,b},{b,a}]),
+    {yes, a} = arborescence_root([{a,b},{a,c},{b,d},{b,e}]),
+    no = arborescence_root([{a,b},{a,c},{b,d},{b,e},{d,e}]),
+    no = arborescence_root([{a,b},{a,c},{b,d},{b,l1,e},{b,l2,e}]),
+    {yes, a} = arborescence_root([{a,c},{c,b}]),
+    no = arborescence_root([{b,a},{c,a}]),
+
+    false = is_arborescence([], []),
+    true = is_arborescence([a], []),
+    false = is_arborescence([a,b], []),
+    true = is_arborescence([{a,b}]),
+    false = is_arborescence([{a,b},{b,a}]),
+    true = is_arborescence([{a,b},{a,c},{b,d},{b,e}]),
+    false = is_arborescence([{a,b},{a,c},{b,d},{b,e},{d,e}]),
+    false = is_arborescence([{a,b},{a,c},{b,d},{b,l1,e},{b,l2,e}]),
+    true = is_arborescence([{a,c},{c,b}]),
+    false = is_arborescence([{b,a},{c,a}]),
+
+    %% Parallel edges.
+    false = is_arborescence([{a,l1,b},{a,l2,b}]),
+
+    ok.
+
+%% OTP-9040
+traversals(Config) when is_list(Config) ->
+    G = graph:new([]),
+    [] = graph:preorder(G),
+    [] = graph:postorder(G),
+    G1 = add_edges(G, [{a,b},{b,c},{c,d},{d,e}]),
+    [a,b,c,d,e] = graph:preorder(G1),
+    [e,d,c,b,a] = graph:postorder(G1),
+    G2 = add_edges(G1, [{0,1},{1,2},{2,0}]),
+    [a,b,c,d,e,1,2,0] = graph:preorder(G2),
+    [e,d,c,b,a,0,2,1] = graph:postorder(G2),
+    G3 = add_edges(G1, [{x,0},{y,1},{z,2}]),
+    [x,0,y,1,a,b,c,d,e,z,2] = graph:preorder(G3),
+    [0,x,1,y,e,d,c,b,a,2,z] = graph:postorder(G3),
+    ok.
+
+is_tree(Es) ->
+    is_tree([], Es).
+
+is_tree(Vs, Es) ->
+    gu(Vs, Es, fun graph:is_tree/1).
+
+is_arborescence(Es) ->
+    is_arborescence([], Es).
+
+is_arborescence(Vs, Es) ->
+    gu(Vs, Es, fun graph:is_arborescence/1).
+
+arborescence_root(Es) ->
+    arborescence_root([], Es).
+
+arborescence_root(Vs, Es) ->
+    gu(Vs, Es, fun graph:arborescence_root/1).
+
+gu(Vs, Es, F) ->
+    G = graph:new(),
+    G1 = add_vertices(G, Vs),
+    G2 = add_edges(G1, Es),
+    F(G2).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+sort_2(L) ->
+    lists:sort(lists:map(fun(V) -> lists:sort(V) end, L)).
+
+path(G, V1, V2) ->
+    graph:get_path(G, V1, V2) /= false.
+
+add_vertices(G, Vs) ->
+    lists:foldl(fun({V, Label}, G0) -> graph:add_vertex(G0, V, Label);
+                   (V, G0) -> graph:add_vertex(G0, V)
+                end, G, Vs).
+
+add_edges(G, L) ->
+    Fun = fun({From, To}, G0) ->
+                  G1 = graph:add_vertex(G0, From),
+                  G2 = graph:add_vertex(G1, To),
+                  graph:add_edge(G2, From, To);
+             ({From, Label, To}, G0) ->
+                  G1 = graph:add_vertex(G0, From),
+                  G2 = graph:add_vertex(G1, To),
+                  graph:add_edge(G2, From, To, Label)
+          end,
+    lists:foldl(Fun, G, L).
+
+eval(L, E) ->
+    Expected = lists:sort(E),
+    Got = lists:sort(L),
+    if
+        Expected == Got ->
+            ok;
+        true ->
+            not_ok
+    end.
+
+evall(L, E) ->
+    F = fun(L1) -> lists:sort(L1) end,
+    Fun = fun(LL) -> F(lists:map(F, LL)) end,
+
+    Expected = Fun(E),
+    Got = Fun(L),
+    if
+        Expected == Got ->
+            ok;
+        true ->
+            not_ok
+    end.
-- 
2.51.0

openSUSE Build Service is sponsored by