Loading...
Searching...
No Matches
include.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_include Include
3!! Include file handling and resolution for the fpx Fortran preprocessor
4!!
5!! This module implements robust and standard-compliant processing of `#include` directives
6!! with full support for:
7!! - Both forms: `#include "file.h"` (local/user) and `#include <file.h>` (system)
8!! - Proper search order: quotes search source dir first, angle brackets skip source dir
9!! - Relative paths resolved against the directory of the parent source file
10!! - Search in user-defined include directories (`global%includedir`)
11!! - Search in system INCLUDE environment variable directories
12!! - Fallback to current working directory
13!! - Proper error reporting with file name and line number context
14!! - Recursion safety through integration with the main preprocessor loop
15!! - Seamless integration via the abstract `preprocess` procedure pointer
16!!
17!! The routine correctly strips quotes or angle brackets, performs path resolution,
18!! checks file existence, opens the file, and recursively invokes the main preprocessing
19!! engine on the included content using the same macro environment.
20!!
21!! @note
22!! For `#include "file"`:
23!! 1. Directory of the parent source file
24!! 2. Directories specified by the -I option (`global%includedir`)
25!! 3. Directories in INCLUDE environment variable
26!! 4. Current working directory
27!!
28!! @note
29!! For `#include <file>`:
30!! 1. Directories specified by the -I option (`global%includedir`)
31!! 2. Directories in INCLUDE environment variable
32!! 3. Current working directory
33!!
34!! @note
35!! Nested includes are supported. Relative paths inside included
36!! files are resolved relative to the directory containing the
37!! including file.
38!!
39!! @section include_examples Examples
40!!
41!! 1. Include a local header from the same directory using quotes:
42!! @code{.f90}
43!! #include "config.h"
44!! !> fpx will look for ./config.h relative to the current source file first
45!! @endcode
46!!
47!! 2. Include a system header using angle brackets:
48!! @code{.f90}
49!! #include <stdlib.h>
50!! !> fpx will skip the source directory and search -I paths, then INCLUDE
51!! @endcode
52!!
53!! 3. Using from the driver program (adding include paths):
54!! @code{.f90}
55!! global%includedir = ['/usr/include', './include', './headers']
56!! call preprocess('main.F90', 'main.f90')
57!! !> All #include <...> will search these directories in order
58!! @endcode
59!!
60!! 4. Verbose error reporting when a file is not found:
61!! @code{.txt}
62!! $ fpx -v src/utils.F90
63!! Error: Cannot find include file 'missing.h' at src/utils.F90:27
64!! @endcode
65module fpx_include
66 use iso_fortran_env, only : iostat_end
67 use fpx_constants
68 use fpx_logging
69 use fpx_path
70 use fpx_string
71 use fpx_macro
72 use fpx_global
73 use fpx_context
74
75 implicit none; private
76
77 public :: handle_include
78
79 ! Include directive types
80 integer, parameter, private :: INCLUDE_TYPE_SYSTEM = 1 ! < >
81 integer, parameter, private :: INCLUDE_TYPE_LOCAL = 2 ! " "
82#ifdef _WIN32
83 integer, parameter, private :: MAX_PATH_LEN = 256
84#else
85 integer, parameter, private :: MAX_PATH_LEN = 4096
86#endif
87
88contains
89
90 !> Process a #include directive encountered during preprocessing
91 !! Resolves the include file name (quoted or angle-bracketed), searches for the file
92 !! using standard C preprocessor rules:
93 !! - Quoted includes search: parent directory, -I paths, INCLUDE, cwd
94 !! - Angle bracket includes search: -I paths, INCLUDE, cwd (skips parent directory)
95 !! Opens the file and recursively preprocesses its contents into the output unit.
96 !!
97 !! @param[in] ctx Context line containing the #include directive
98 !! @param[in] ounit Output unit where preprocessed content is written
99 !! @param[in] preprocess Procedure pointer to the main line-by-line preprocessor
100 !! @param[inout] macros Current macro table (shared across recursion levels)
101 !! @param[in] token Usually 'include' - the directive keyword
102 !!
103 !! @ingroup group_include
104 recursive subroutine handle_include(ctx, ounit, preprocess, macros, token)
105 type(context), intent(in) :: ctx
106 integer, intent(in) :: ounit
107 procedure(read_unit) :: preprocess
108 type(macro), allocatable, intent(inout) :: macros(:)
109 character(*), intent(in) :: token
110 !private
111 type(string), allocatable, save :: include_stack(:)
112 character(:), allocatable :: include_file
113 character(:), allocatable :: dir, ifile
114 integer :: i, iunit, ierr, pos, include_type, closing
115 logical :: exists
116
117 ! Extract the directory of the parent file
118 dir = dirpath(ctx%path)
119 ! Find the position after the #include token
120 pos = index(lowercase(ctx%content), token) + len(token)
121 include_file = trim(adjustl(ctx%content(pos:)))
122
123 ! Determine include type and extract filename
124 if (include_file(1:1) == '"') then
125 include_type = include_type_local
126 closing = index(include_file(2:), '"')
127 if (closing == 0) then
128 call printf(render(diagnostic_report(level_error, &
129 message='Malformed #include directive', &
130 label=label_type('Missing closing quotation mark', &
131 index(ctx%content,'"'),1), &
132 source=trim(ctx%path)), &
133 ctx%content, ctx%line))
134 return
135 end if
136 include_file = include_file(2:closing)
137 else if (include_file(1:1) == '<') then
138 include_type = include_type_system
139 closing = index(include_file(2:), '>')
140 if (closing == 0) then
141 call printf(render(diagnostic_report(level_error, &
142 message='Malformed #include directive', &
143 label=label_type('Missing closing quotation mark', &
144 index(ctx%content,'"'),1), &
145 source=trim(ctx%path)), &
146 ctx%content, ctx%line))
147 return
148 end if
149 include_file = include_file(2:closing)
150 else
151 ! Malformed include directive
152 call printf(render(diagnostic_report(level_error, &
153 message='Malformed #include directive', &
154 label=label_type('Filepath should either be delimited by "<...>" or "..."', index(ctx%content, include_file), &
155 len(include_file)), &
156 source=trim(ctx%path)), &
157 ctx%content, ctx%line))
158 return
159 end if
160
161 ! Handle absolute/rooted paths (same for both types)
162 ifile = include_file
163 if (is_rooted(ifile)) then
164 inquire(file=ifile, exist=exists)
165 if (exists) then
166 include_file = ifile
167 else
168 call printf(render(diagnostic_report(level_error, &
169 message='File not found', &
170 label=label_type('Cannot find include file ' // trim(include_file), index(ctx%content, include_file), &
171 len(include_file)), &
172 source=trim(ctx%path)), &
173 ctx%content, ctx%line))
174 return
175 end if
176 else
177 ! Relative path - search according to include type
178 exists = .false.
179 ! For quoted includes (#include "file"), search parent directory first
180 if (include_type == include_type_local) then
181 ifile = join(dir, include_file)
182 inquire(file=ifile, exist=exists)
183 if (exists) then
184 include_file = ifile
185 end if
186 end if
187
188 ! If not found yet, search user-specified include directories (-I paths)
189 if (.not. exists .and. allocated(global%includedir)) then
190 do i = 1, size(global%includedir)
191 ifile = join(global%includedir(i), include_file)
192 inquire(file=ifile, exist=exists)
193 if (exists) then
194 include_file = ifile
195 exit
196 end if
197 end do
198 end if
199
200 ! If still not found, try the INCLUDE environmental variable
201 if (.not. exists) then
202 block
203 character(:), allocatable :: ipaths(:)
204
205 ipaths = get_system_paths()
206 do i = 1, size(ipaths)
207 ifile = join(ipaths(i), include_file)
208 inquire(file=ifile, exist=exists)
209 if (exists) then
210 include_file = ifile
211 end if
212 end do
213 end block
214 end if
215
216 ! If still not found, try current working directory as last resort
217 if (.not. exists) then
218 ifile = join(cwd(), include_file)
219 inquire(file=ifile, exist=exists)
220 if (exists) then
221 include_file = ifile
222 end if
223 end if
224
225 ! If file was not found anywhere, report error
226 if (.not. exists) then
227 call printf(render(diagnostic_report(level_error, &
228 message='File not found', &
229 label=label_type('Cannot find include file ' // trim(include_file), index(ctx%content, include_file), len(&
230 include_file)), &
231 source=trim(ctx%path)), &
232 ctx%content, ctx%line))
233 return
234 end if
235 end if
236
237 if (.not. allocated(include_stack)) allocate(include_stack(0))
238
239 if (include_stack .contains. include_file) then
240 call printf(render(diagnostic_report(level_error, &
241 message='Recursive include detected', &
242 label=label_type('File already included in current include chain', &
243 index(ctx%content, trim(include_file)), &
244 len_trim(include_file)), &
245 source=trim(ctx%path)), &
246 ctx%content, ctx%line))
247 return
248 end if
249
250 ! Open and preprocess the include file
251 open(newunit=iunit, file=include_file, status='old', action='read', iostat=ierr)
252 if (ierr /= 0) then
253 call printf(render(diagnostic_report(level_error, &
254 message='File not found', &
255 label=label_type('Cannot open include file ' // trim(include_file), index(ctx%content, include_file), len(&
256 include_file)), &
257 source=trim(ctx%path)), &
258 ctx%content, ctx%line))
259 return
260 end if
261
262 include_stack = [include_stack, string(include_file)]
263
264 call preprocess(iunit, ounit, macros, .true.)
265 close(iunit)
266 if (size(include_stack) > 1) then
267 include_stack = include_stack(:size(include_stack)-1)
268 else
269 deallocate(include_stack)
270 end if
271 end subroutine
272
273 !> Get system include paths from INCLUDE environment variable
274 !! Returns an array of directory paths found in INCLUDE
275 !! @return Array of path strings, empty if INCLUDE not set
276 !!
277 !! @ingroup group_include
278 function get_system_paths() result(paths)
279 character(:), allocatable :: paths(:)
280 !private
281 character(:), allocatable :: path_env, tmp(:)
282 integer :: lpath, i, n_paths, start_pos, end_pos, count
283 character(len=1) :: path_sep
284
285#ifdef _WIN32
286 path_sep = ';' ! Windows path separator
287#else
288 path_sep = ':' ! Unix/Linux/Mac path separator
289#endif
290
291 ! Get INCLUDE environment variable length
292 call get_environment_variable('INCLUDE', length=lpath)
293 if (lpath <= 0) then
294 allocate(character(len=0) :: paths(0)); return
295 end if
296
297 ! Allocate and retrieve INCLUDE value
298 allocate(character(len=lpath) :: path_env)
299 call get_environment_variable('INCLUDE', value=path_env)
300
301 ! Count number of paths (number of separators + 1)
302 n_paths = 1
303 do i = 1, len(path_env)
304 if (path_env(i:i) == path_sep) n_paths = n_paths + 1
305 end do
306
307 ! Allocate temporary array with maximum size
308 allocate(character(len=MAX_PATH_LEN) :: tmp(n_paths))
309
310 ! Split INCLUDE into individual directories
311 count = 0
312 start_pos = 1
313 do i = 1, len(path_env) + 1
314 if (i > len(path_env) .or. path_env(i:i) == path_sep) then
315 if (i > len(path_env)) then
316 end_pos = i - 1
317 else
318 end_pos = i - 1
319 end if
320
321 if (end_pos >= start_pos) then
322 count = count + 1
323 tmp(count) = trim(adjustl(path_env(start_pos:end_pos)))
324 end if
325 start_pos = i + 1
326 end if
327 end do
328
329 ! Allocate result array with actual count
330 if (count > 0) then
331 allocate(character(len=MAX_PATH_LEN) :: paths(count))
332 paths(:) = tmp(1:count)
333 else
334 allocate(character(len=0) :: paths(0))
335 end if
336 end function
337
338end module
type(global_settings), public global
Global preprocessor configuration instance.
Definition global.f90:192
recursive subroutine, public handle_include(ctx, ounit, preprocess, macros, token)
Process a include directive encountered during preprocessing Resolves the include file name (quoted o...
Definition include.f90:105
pure logical function, public is_rooted(filepath)
Returns .true. if the path is rooted (starts with a separator) or is absolute. A rooted path begins w...
Definition path.f90:189
character(:) function, allocatable, public cwd()
Returns the current working directory as a deferred-length character string. Returns an empty string ...
Definition path.f90:426
pure character(len_trim(str)) function, public lowercase(str)
Convert string to lower case (respects contents of quotes).
Definition string.f90:852
Generic renderer for diagnostics and source excerpts.
Definition logging.f90:208
Abstract interface to the top-level preprocessing routine.
Definition macro.f90:238
Join path components using the platform separator.
Definition path.f90:141
Locate the position of a substring.
Definition string.f90:395
Return the trimmed length of a string object.
Definition string.f90:201
Return the length of a string object.
Definition string.f90:164
Remove trailing blanks from a string object.
Definition string.f90:240
Snapshot of a source location within the preprocessing stream.
Definition context.f90:114
Structured compiler diagnostic.
Definition logging.f90:337
Diagnostic label identifying a region of source text.
Definition logging.f90:304
Representation of a preprocessor macro.
Definition macro.f90:135
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition string.f90:112