Loading...
Searching...
No Matches
graph.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_graph Graph
3!! Directed graph utilities used for macro dependency analysis.
4!!
5!! This module provides a lightweight directed graph implementation used
6!! internally by the fpx preprocessor to detect cyclic dependencies during
7!! macro expansion.
8!!
9!! Unlike general-purpose graph libraries, this implementation is optimized
10!! for the small graphs typically encountered during preprocessing:
11!!
12!! - Vertices are represented by 1-based integer identifiers.
13!! - Edges are stored in a dense adjacency structure for fast traversal.
14!! - Cycle detection uses depth-first search (DFS) with a recursion stack.
15!! - Invalid vertices are ignored gracefully.
16!! - Memory management is automatic through a finalizer.
17!!
18!! The primary use case is preventing infinite recursion caused by macros
19!! expanding, directly or indirectly, to themselves:
20!!
21!! @code{.f90}
22!! #define A B
23!! #define B C
24!! #define C A
25!! ...
26!! @endcode
27!!
28!! Before expanding a macro, fpx records dependencies in a graph and checks
29!! whether introducing a new dependency would create a cycle.
30!!
31!! @section graph_examples Examples
32!!
33!! 1. Detecting a circular dependency:
34!! @code{.f90}
35!! type(digraph) :: g
36!! logical :: cycle
37!!
38!! g = digraph(3)
39!!
40!! call g%add_edge(1, 2)
41!! call g%add_edge(2, 3)
42!! call g%add_edge(3, 1)
43!!
44!! cycle = g%is_circular(1)
45!! print *, cycle ! prints .true.
46!! ...
47!! @endcode
48!!
49!! 2. Detecting an acyclic dependency chain:
50!! @code{.f90}
51!! type(digraph) :: g
52!!
53!! g = digraph(4)
54!!
55!! call g%add_edge(1, 2)
56!! call g%add_edge(2, 3)
57!! call g%add_edge(3, 4)
58!!
59!! print *, g%is_circular(1) ! .false.
60!! ...
61!! @endcode
62!!
63!! 3. Internal usage during macro expansion:
64!! @code{.f90}
65!! call graph%add_edge(current_macro, referenced_macro)
66!!
67!! if (graph%is_circular(referenced_macro)) then
68!! ! Prevent recursive expansion
69!! end if
70!! ...
71!! @endcode
72module fpx_graph
73 implicit none; private
74
75 !> Directed graph supporting efficient cycle detection.
76 !!
77 !! The graph stores a fixed number of vertices identified by
78 !! integers in the range `[1, vertices]`.
79 !!
80 !! Edges are represented internally using a dense adjacency
81 !! structure together with per-vertex occupancy counters.
82 !! This approach avoids repeated allocations and is well suited
83 !! to the relatively small dependency graphs encountered by fpx.
84 !!
85 !! @section digraph_type_examples Examples
86 !! @code{.f90}
87 !! type(digraph) :: g
88 !!
89 !! g = digraph(2)
90 !! call g%add_edge(1, 2)
91 !!
92 !! print *, g%is_circular(1)
93 !! ...
94 !! @endcode
95 !!
96 !! @section digraph_type_constructors Constructors
97 !! Initializes a new directed graph.
98 !!
99 !! @b Constructor
100 !! @code{.f90}
101 !! type(digraph) function digraph(integer vertices)
102 !! @endcode
103 !!
104 !! @param[in] vertices
105 !! Number of vertices in the graph.
106 !!
107 !! @return A newly constructed directed graph.
108 !!
109 !! @section digraph_type_remarks Remarks
110 !! - Vertices are numbered from 1.
111 !! - The number of vertices is fixed after construction.
112 !! - Intended primarily for internal use by the macro expander.
113 !!
114 !! @ingroup group_graph
115 type, public :: digraph
116 integer, private :: vertices !< Number of vertices
117 integer, allocatable, private :: adjacency_list(:, :) !< Adjacency list containing the connection information between the vertices.
118 integer, allocatable, private :: list_sizes(:) !< Actually used portion of each row of @ref adjacency_list.
119 contains
120 private
121 procedure, pass(this), public :: add_edge => graph_add_edge
122 procedure, pass(this), public :: is_circular => graph_has_cycle_dfs
123 final :: graph_final
124 end type
125
126 !> Construct a directed graph with a fixed number of vertices.
127 !!
128 !! Allocates the internal adjacency structures and initializes
129 !! the graph without any edges.
130 !!
131 !! @param[in] vertices
132 !! Number of vertices.
133 !!
134 !! @return Newly initialized graph.
135 !!
136 !! @ingroup group_graph
137 interface digraph
138 !! @cond
139 module procedure :: graph_new
140 !! @endcond
141 end interface
142
143contains
144
145 type(digraph) function graph_new(vertices) result(that)
146 integer, intent(in) :: vertices
147 integer :: i
148
149 that%vertices = vertices
150 allocate(that%adjacency_list(vertices, vertices), source=0)
151 allocate(that%list_sizes(vertices), source=0)
152 end function
153
154 !> Add a directed edge to the graph.
155 !!
156 !! Inserts an edge from `source` to `destination`.
157 !! If either vertex lies outside the valid range,
158 !! the request is ignored.
159 !!
160 !! @param[inout] this Graph instance.
161 !! @param[in] source Source vertex (1-based).
162 !! @param[in] destination Destination vertex (1-based).
163 !! @param[out] overflow Optional flag indicating whether the
164 !! insertion position was already occupied.
165 !!
166 !! @note Duplicate edges are not explicitly filtered.
167 !!
168 !! @ingroup group_graph
169 subroutine graph_add_edge(this, source, destination, overflow)
170 class(digraph), intent(inout) :: this
171 integer, intent(in) :: source
172 integer, intent(in) :: destination
173 logical, intent(out), optional :: overflow
174
175 if (source < 1 .or. source > this%vertices .or. &
176 destination < 1 .or. destination > this%vertices) then
177 return ! Skip invalid edges
178 end if
179
180 this%list_sizes(source) = this%list_sizes(source) + 1
181 if (this%list_sizes(source) <= this%vertices) then
182 if (present(overflow)) overflow = this%adjacency_list(source, this%list_sizes(source)) /= 0
183 this%adjacency_list(source, this%list_sizes(source)) = destination
184 end if
185 end subroutine
186
187 !> Determine whether a cycle is reachable from a vertex.
188 !!
189 !! Performs a depth-first traversal starting from
190 !! `start_vertex` and detects back edges using a
191 !! recursion stack.
192 !!
193 !! @param[in] this
194 !! Graph instance.
195 !! @param[in] start_vertex
196 !! Vertex from which the search begins.
197 !!
198 !! @return `.true.` if a cycle exists in the reachable component;
199 !! `.false.` otherwise.
200 !!
201 !! @ingroup group_graph
202 logical function graph_has_cycle_dfs(this, start_vertex) result(has_cycle)
203 class(digraph), intent(in) :: this
204 integer, intent(in) :: start_vertex
205 !private
206 logical, allocatable :: visited(:), recursion_stack(:)
207
208 if (start_vertex < 1 .or. start_vertex > this%vertices) then
209 has_cycle = .false.
210 return
211 end if
212
213 allocate(visited(this%vertices), source=.false.)
214 allocate(recursion_stack(this%vertices), source=.false.)
215
216 has_cycle = dfs_recursive(this, start_vertex, visited, recursion_stack)
217
218 deallocate(visited, recursion_stack)
219 end function
220
221 !> Recursive DFS worker used for cycle detection.
222 !!
223 !! This routine implements the actual traversal algorithm used
224 !! by @ref graph_has_cycle_dfs. It maintains both a visited set
225 !! and a recursion stack in order to identify back edges.
226 !!
227 !! @ingroup group_graph
228 recursive logical function dfs_recursive(this, vertex, visited, recursion_stack) result(has_cycle)
229 class(digraph), intent(in) :: this
230 integer, intent(in) :: vertex
231 logical, intent(inout) :: visited(:), recursion_stack(:)
232 integer :: neighbor, i
233
234 visited(vertex) = .true.
235 recursion_stack(vertex) = .true.
236
237 do i = 1, this%list_sizes(vertex)
238 neighbor = this%adjacency_list(vertex, i)
239 if (neighbor < 1 .or. neighbor > this%vertices) cycle ! Skip invalid neighbors
240 if (.not. visited(neighbor)) then
241 if (dfs_recursive(this, neighbor, visited, recursion_stack)) then
242 has_cycle = .true.
243 return
244 end if
245 else if (recursion_stack(neighbor)) then
246 has_cycle = .true.
247 return
248 end if
249 end do
250
251 recursion_stack(vertex) = .false.
252 has_cycle = .false.
253 end function
254
255 !> Finalizer for the directed graph.
256 !!
257 !! Releases all dynamically allocated storage associated with
258 !! the graph when it leaves scope.
259 !!
260 !! @ingroup group_graph
261 subroutine graph_final(this)
262 type(digraph), intent(inout) :: this
263 if (allocated(this%adjacency_list)) deallocate(this%adjacency_list)
264 if (allocated(this%list_sizes)) deallocate(this%list_sizes)
265 end subroutine
266
267end module
Directed graph supporting efficient cycle detection.
Definition graph.f90:115