Loading...
Searching...
No Matches
graph.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_graph Graph
3!! Lightweight directed graph implementation for cycle detection in macro expansion
4!! This module provides a compact, allocation-efficient directed graph (@link fpx_graph::digraph digraph@endlink) specifically
5!! designed for detecting circular dependencies during macro expansion in the fpx preprocessor.
6!!
7!! Features:
8!! - Fixed-size adjacency list using a dense 2D integer array (fast access, no pointers)
9!! - Dynamic edge insertion with automatic per-vertex size tracking
10!! - Depth-first search (DFS) based) cycle detection starting from any vertex
11!! - Automatic cleanup via finalizer
12!! - No dynamic memory fragmentation – ideal for frequent creation/destruction during preprocessing
13!!
14!! Used internally by @link fpx_macro fpx_macro@endlink to prevent infinite recursion when a macro expands
15!! (directly or indirectly) to itself (e.g., `#define A B`, `#define B A`).
16!!
17!! <h2 class="groupheader">Examples</h2>
18!!
19!! 1. Detect circular macro dependency:
20!! @code{.f90}
21!! type(digraph) :: g
22!! logical :: cycle
23!!
24!! g = digraph(3) ! 3 macros indexed 1..3
25!! call g%add_edge(1, 2) ! macro1 depends on macro2
26!! call g%add_edge(2, 3) ! macro2 depends on macro3
27!! call g%add_edge(3, 1) ! macro3 depends on macro1 → cycle!
28!!
29!! cycle = g%is_circular(1) ! returns .true.
30!! print *, 'Circular macro chain detected:', cycle
31!! ...
32!! @endcode
33!!
34!! 2. Safe expansion (used inside @link fpx_macro fpx_macro@endlink):
35!! @code{.f90}
36!! type(digraph) :: expansion_graph
37!! expansion_graph = digraph(size(macros))
38!! call expansion_graph%add_edge(current_macro_idx, referenced_macro_idx)
39!! if (expansion_graph%is_circular(referenced_macro_idx)) then
40!! ! Skip expansion to prevent infinite loop
41!! end if
42!! ...
43!! @endcode
44module fpx_graph
45 implicit none; private
46
47 !> Directed graph with fixed vertex count and efficient cycle detection
48 !! Stores edges in a dense adjacency matrix slice per vertex.
49 !! Only the actually used portion of each row is tracked via `list_sizes`.
50 !! <h2 class="groupheader">Examples</h2>
51 !! @code{.f90}
52 !! type(digraph) :: g
53 !! logical :: cycle
54 !!
55 !! g = digraph(3) ! 3 macros indexed 1..3
56 !! call g%add_edge(1, 2) ! macro1 depends on macro2
57 !! call g%add_edge(2, 3) ! macro2 depends on macro3
58 !! call g%add_edge(3, 1) ! macro3 depends on macro1 → cycle!
59 !!
60 !! cycle = g%is_circular(1) ! returns .true.
61 !! print *, 'Circular macro chain detected:', cycle
62 !! ...
63 !! @endcode
64 !! <h2 class="groupheader">Constructors</h2>
65 !! Initializes a new instance of the @ref digraph class
66 !! <h3>digraph(integer)</h3>
67 !! @verbatim type(digraph) function digraph(integer vertices) @endverbatim
68 !!
69 !! @param[in] vertices Number of vertices (usually number of currently defined macros)
70 !!
71 !! @b Examples
72 !! @code{.f90}
73 !! type(digraph) :: g
74 !! g = digraph(3)
75 !! @endcode
76 !! @return The constructed digraph object.
77 !!
78 !!
79 !! <h2 class="groupheader">Remarks</h2>
80 !! @ingroup group_graph
81 type, public :: digraph
82 integer, private :: vertices !< Number of vertices
83 integer, allocatable, private :: adjacency_list(:, :) !< Adjacency list containing the connection information between the vertices.
84 integer, allocatable, private :: list_sizes(:) !< Actually used portion of each row of @ref adjacency_list.
85 contains
86 private
87 procedure, pass(this), public :: add_edge => graph_add_edge
88 procedure, pass(this), public :: is_circular => graph_has_cycle_dfs
89 final :: graph_final
90 end type
91
92 !> Constructor interface for @ref digraph type
93 !!
94 !! @b Remarks
95 !! @ingroup group_graph
96 interface digraph
97 !! @cond
98 module procedure :: graph_new
99 !! @endcond
100 end interface
101
102contains
103
104 !> Constructor
105 type(digraph) function graph_new(vertices) result(that)
106 integer, intent(in) :: vertices
107 integer :: i
108
109 that%vertices = vertices
110 allocate(that%adjacency_list(vertices, vertices), source=0)
111 allocate(that%list_sizes(vertices), source=0)
112 end function
113
114 !> Add a directed edge from source → destination
115 !! Silently ignores invalid indices. Optional `exists` flag indicates if edge was already present.
116 !! @param[inout] this digraph object
117 !! @param[in] source Source vertex (1-based)
118 !! @param[in] destination Target vertex (1-based)
119 !! @param[out] exists (optional) .true. if edge already existed
120 !!
121 !! @b Remarks
122 subroutine graph_add_edge(this, source, destination, exists)
123 class(digraph), intent(inout) :: this
124 integer, intent(in) :: source
125 integer, intent(in) :: destination
126 logical, intent(out), optional :: exists
127
128 if (source < 1 .or. source > this%vertices .or. &
129 destination < 1 .or. destination > this%vertices) then
130 return ! Skip invalid edges
131 end if
132
133 this%list_sizes(source) = this%list_sizes(source) + 1
134 if (this%list_sizes(source) <= this%vertices) then
135 if (present(exists)) exists = this%adjacency_list(source, this%list_sizes(source)) /= 0
136 this%adjacency_list(source, this%list_sizes(source)) = destination
137 end if
138 end subroutine
139
140 !> Check whether a cycle exists in the graph reachable from start_vertex
141 !! Uses standard DFS with recursion stack (back-edge detection).
142 !! @param[in] this digraph object
143 !! @param[in] start_vertex Vertex from which to begin cycle search
144 !! @return .true. if a cycle is found in the component reachable from start_vertex
145 !!
146 !! @b Remarks
147 logical function graph_has_cycle_dfs(this, start_vertex) result(has_cycle)
148 class(digraph), intent(in) :: this
149 integer, intent(in) :: start_vertex
150 !private
151 logical, allocatable :: visited(:), recursion_stack(:)
152
153 if (start_vertex < 1 .or. start_vertex > this%vertices) then
154 has_cycle = .false.
155 return
156 end if
157
158 allocate(visited(this%vertices), source=.false.)
159 allocate(recursion_stack(this%vertices), source=.false.)
160
161 has_cycle = dfs_recursive(this, start_vertex, visited, recursion_stack)
162
163 deallocate(visited, recursion_stack)
164 end function
165
166 !> Internal recursive DFS worker for cycle detection
167 !! @ingroup group_graph
168 recursive logical function dfs_recursive(this, vertex, visited, recursion_stack) result(has_cycle)
169 class(digraph), intent(in) :: this
170 integer, intent(in) :: vertex
171 logical, intent(inout) :: visited(:), recursion_stack(:)
172 integer :: neighbor, i
173
174 visited(vertex) = .true.
175 recursion_stack(vertex) = .true.
176
177 do i = 1, this%list_sizes(vertex)
178 neighbor = this%adjacency_list(vertex, i)
179 if (neighbor < 1 .or. neighbor > this%vertices) cycle ! Skip invalid neighbors
180 if (.not. visited(neighbor)) then
181 if (dfs_recursive(this, neighbor, visited, recursion_stack)) then
182 has_cycle = .true.
183 return
184 end if
185 else if (recursion_stack(neighbor)) then
186 has_cycle = .true.
187 return
188 end if
189 end do
190
191 recursion_stack(vertex) = .false.
192 has_cycle = .false.
193 end function
194
195 !> Finalizer – automatically deallocate internal arrays when graph goes out of scope
196 subroutine graph_final(this)
197 type(digraph), intent(inout) :: this
198 if (allocated(this%adjacency_list)) deallocate(this%adjacency_list)
199 if (allocated(this%list_sizes)) deallocate(this%list_sizes)
200 end subroutine
201
202end module
recursive logical function dfs_recursive(this, vertex, visited, recursion_stack)
Internal recursive DFS worker for cycle detection.
Definition graph.f90:169
Directed graph with fixed vertex count and efficient cycle detection Stores edges in a dense adjacenc...
Definition graph.f90:96