Loading...
Searching...
No Matches
parser.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_parser Parser
3!! Fortran Preprocessor (fpx) – core parsing and preprocessing module
4!!
5!! This module implements a full-featured, modern Fortran preprocessor supporting:
6!! - C-style line continuations with `\` and `\\`
7!! - Fortran-style `&` continuations
8!! - `#define`, `#undef`, object-like and function-like macros with variadic support
9!! - `#include` with proper path resolution and recursion guard
10!! - Conditional compilation: `#if`, `#ifdef`, `#ifndef`, `#elif`, `#else`, `#endif`
11!! - C-style `/* ... */` comments (nestable aware)
12!! - Macro expansion with argument substitution and stringification (`#`) / token-pasting (`##`)
13!! - Interactive REPL mode when reading from stdin
14!! - Multiple entry points for file-to-file, unit-to-unit, etc.
15!!
16!! The preprocessor is designed to be standards-conforming where possible while adding
17!! useful extensions (variadic macros, better diagnostics, include path handling).
18!!
19!! <h2 class="groupheader">Examples</h2>
20!!
21!! 1. Preprocess a file to stdout:
22!! @code{.f90}
23!! call preprocess('input.F90')
24!! @endcode
25!!
26!! 2. Preprocess a file and write to another file:
27!! @code{.f90}
28!! call preprocess('src/main.F90', 'preprocessed/main.F90')
29!! @endcode
30!!
31!! 3. Use in a build system with unit numbers:
32!! @code{.f90}
33!! integer :: iu, ou
34!! open(newunit=iu, file='input.F90')
35!! open(newunit=ou, file='output.F90')
36!! call preprocess(iu, ou)
37!! close(iu); close(ou)
38!! ...
39!! @endcode
40!!
41!! 4. Interactive mode (stdin to stdout):
42!! @code
43!! $ ./fpx
44!! [in] #define PI 3.1415926535
45!! [out]
46!! [in] real :: x = PI*2
47!! [out] real :: x = 3.1415926535*2
48!! [in] (empty line or 'quit' to exit)
49!! @endcode
50module fpx_parser
51 use, intrinsic :: iso_fortran_env, only: stdout => output_unit, iostat_end, stdin => input_unit
52 use, intrinsic :: iso_c_binding, only: c_char, c_size_t, c_ptr, c_null_ptr, c_associated
53 use fpx_constants
54 use fpx_string
55 use fpx_logging
56 use fpx_macro
57 use fpx_conditional
58 use fpx_define
59 use fpx_diagnostics
60 use fpx_include
61 use fpx_path
62 use fpx_global
63 use fpx_os
64
65 implicit none; private
66
67 public :: preprocess, &
68 global
69
70 !> Generic interface to start preprocessing from various sources/sinks
71 !!
72 !! Allows preprocessing:
73 !! - file to stdout
74 !! - file to file
75 !! - unit to file
76 !! - unit to unit (most flexible, used internally for #include)
77 !!
78 !! @b Remarks
79 !! @ingroup group_parser
80 interface preprocess
81 module procedure :: preprocess_file
82 module procedure :: preprocess_file_to_unit
83 module procedure :: preprocess_unit_to_file
84 module procedure :: preprocess_unit_to_unit
85 end interface
86
87 character(256) :: name !< Current source file name (without path)
88 logical :: c_continue !< Flags for C-style continuation
89 logical :: f_continue !< Flags for Fortran-style continuation
90 logical :: in_comment !< Internal state flags
91 logical :: reprocess !< Internal state flags
92 logical :: stitch !< Internal state flags
93 character(:), allocatable :: res !< Accumulated result line buffers
94 character(:), allocatable :: tmp !< Accumulated temporary line buffers
95 character(MAX_LINE_LEN) :: line !< Raw input line
96 character(MAX_LINE_LEN) :: continued_line !< Raw and continued input line
97 integer :: iline !< Current line number position
98 integer :: icontinuation !< Continuation position
99
100
101contains
102
103 !> Preprocess a file and write result to an optional output file (default: stdout)
104 !! Opens the input file, determines the base filename for error messages,
105 !! opens the output file if requested, and delegates to the unit-to-unit routine.
106 !! @param[in] filepath Path to the input source file
107 !! @param[in] outputfile Optional path to the output file; if absent output goes to stdout
108 !!
109 !! @b Remarks
110 !! @ingroup group_parser
111 subroutine preprocess_file(filepath, outputfile)
112 character(*), intent(in) :: filepath
113 character(*), intent(in), optional :: outputfile
114 !private
115 integer :: iunit, ierr, n, ounit
116 character(len=1, kind=c_char) :: buf(256)
117
118 open(newunit=iunit, file=filepath, status='old', action='read', iostat=ierr)
119 if (ierr /= 0) then
120 if (verbose) print *, "Error opening input file: ", trim(filepath)
121 return
122 else
123 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
124 n = findloc(buf, achar(0), 1)
125 name = filepath(n + 1:)
126 end if
127 end if
128
129 if (present(outputfile)) then
130 open(newunit=ounit, file=outputfile, status='replace', action='write', iostat=ierr)
131 if (ierr /= 0) then
132 if (verbose) print *, "Error opening output file: ", trim(outputfile)
133 close(iunit)
134 return
135 end if
136 else
137 ounit = stdout
138 end if
139
140 call preprocess(iunit, ounit)
141 if (iunit /= stdin) close(iunit)
142 if (ounit /= stdout) close(ounit)
143 end subroutine
144
145 !> Preprocess from an already-open input unit and write to a file
146 !! @param[in] iunit Input unit (must already be open for reading)
147 !! @param[in] ofile Output filename
148 !!
149 !! @b Remarks
150 !! @ingroup group_parser
151 subroutine preprocess_unit_to_file(iunit, ofile)
152 integer, intent(in) :: iunit
153 character(*), intent(in) :: ofile
154 !private
155 integer :: ierr, ounit
156
157 if (iunit /= stdin) then
158 inquire(unit = iunit, name=name)
159 end if
160
161 open(newunit=ounit, file=ofile, status='replace', action='write', iostat=ierr)
162 if (ierr /= 0) then
163 if (verbose) print *, "Error opening output file: ", trim(ofile)
164 close(iunit)
165 return
166 end if
167
168 call preprocess(iunit, ounit)
169 if (iunit /= stdin) close(iunit)
170 if (ounit /= stdout) close(ounit)
171 end subroutine
172
173 !> Preprocess a file and write to an already-open output unit
174 !! @param[in] ifile Input filename
175 !! @param[in] ounit Output unit (already open for writing)
176 !!
177 !! @b Remarks
178 !! @ingroup group_parser
179 subroutine preprocess_file_to_unit(ifile, ounit)
180 character(*), intent(in) :: ifile
181 integer, intent(in) :: ounit
182 !private
183 integer :: iunit, ierr, n
184 character(len=1, kind=c_char) :: buf(256)
185
186 open(newunit=iunit, file=ifile, status='old', action='read', iostat=ierr)
187 if (ierr /= 0) then
188 if (verbose) print *, "Error opening input file: ", trim(ifile)
189 return
190 else
191 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
192 n = findloc(buf, achar(0), 1)
193 name = ifile(n + 1:)
194 end if
195 end if
196
197 call preprocess(iunit, ounit)
198 if (iunit /= stdin) close(iunit)
199 if (ounit /= stdout) close(ounit)
200 end subroutine
201
202 !> Core preprocessing routine: read from iunit, write to ounit
203 !! Sets up a clean macro environment for the top-level file,
204 !! resets conditional compilation state, and calls the worker routine.
205 !! @param[in] iunit Input unit
206 !! @param[in] ounit Output unit
207 !!
208 !! @b Remarks
209 !! @ingroup group_parser
210 subroutine preprocess_unit_to_unit(iunit, ounit)
211 integer, intent(in) :: iunit
212 integer, intent(in) :: ounit
213 !private
214 type(macro), allocatable :: macros(:)
215
216 if (.not. allocated(global%macros)) allocate(global%macros(0))
217 allocate(macros(sizeof(global%macros)), source=global%macros)
218 if (.not. allocated(global%undef)) allocate(global%undef(0))
219 if (.not. allocated(global%includedir)) allocate(global%includedir(0))
220
221 call add(global%macros, macro('__STDF__','1'))
222 call add(global%macros, macro('__FPX__','1'))
223 associate(os => get_os_type())
224 if (os == os_windows .or. os == os_windowsx86) then
225 call add(global%macros, macro('_WIN32'))
226 if (os /= os_windowsx86) call add(global%macros, macro('_WIN64'))
227 end if
228 end associate
229
230 cond_depth = 0
231 cond_stack(1)%active = .true.
232 cond_stack(1)%has_met = .false.
233
234 reprocess = .false.; c_continue = .false.; f_continue = .false.
235 icontinuation = 1; iline = 0
236 continued_line = ''; res = ''
237
238 call preprocess_unit(iunit, ounit, macros, .false.)
239 deallocate(macros)
240 end subroutine
241
242 !> Worker routine that reads lines, handles continuations, comments and directives
243 !! This is the main loop that:
244 !! - reads lines with interactive prompt when iunit==stdin
245 !! - handles both `\` and `&` continuations
246 !! - strips or preserves comments appropriately
247 !! - calls process_line() for directive processing and macro expansion
248 !! - stitches lines when Fortran continuation (`&`) is active
249 !! @param[in] iunit Input unit
250 !! @param[in] ounit Output unit
251 !! @param[inout] macros(:) Current macro table (passed by value between include levels)
252 !! @param[in] from_include True if called recursively from #include
253 !!
254 !! @b Remarks
255 !! @ingroup group_parser
256 subroutine preprocess_unit(iunit, ounit, macros, from_include)
257 integer, intent(in) :: iunit
258 integer, intent(in) :: ounit
259 type(macro), allocatable, intent(inout) :: macros(:)
260 logical, intent(in) :: from_include
261 !private
262 integer :: ierr, n
263 character(:), allocatable :: uline
264 logical :: interactive
265
266 interactive = iunit == stdin
267
268 if (interactive) then
269 write(*, *)
270 write(*, *) ' Welcome to fpx, the extended Fortran preprocessor. '
271 write(*, *) ' The program can be exited at any time by hitting'
272 write(*, *) " 'Enter' at the prompt without entering any data, "
273 write(*, *) " or with the 'quit' command."
274 end if
275 do
276 if (interactive) write(*, '(/a)', advance='no') ' [in] ' ! Command line prompt
277 read(iunit, '(A)', iostat=ierr) line
278
279 if (interactive) then
280 if (line == '') exit
281 uline = uppercase(trim(adjustl(line)))
282 if (uline == 'QUIT') exit
283 end if
284 if (ierr /= 0) then
285 if (ierr == iostat_end .and. from_include) f_continue = tail(tmp) == '&'
286 exit
287 end if
288 if (.not. from_include) iline = iline + 1
289
290 if (c_continue) then
291 continued_line = continued_line(:icontinuation) // trim(adjustl(line))
292 else
293 continued_line = trim(adjustl(line))
294 end if
295 n = len_trim(continued_line); if (n == 0) cycle
296
297 ! Check for line continuation with '\'
298 if (verify(continued_line(n:n), '\') == 0) then
299 ! Check for line break with '\\'
300 if (continued_line(len_trim(continued_line) - 1:len_trim(continued_line)) == '\\' .and. global%line_break) then
301 c_continue = .true.
302 continued_line = continued_line(:len_trim(continued_line) - 2) // new_line('A') ! Strip '\\'
303 icontinuation = len_trim(continued_line)
304 else
305 c_continue = .true.
306 icontinuation = len_trim(continued_line) - 1
307 continued_line = continued_line(:icontinuation)
308 end if
309 cycle
310 else
311 c_continue = .false.
312
313 tmp = process_line(continued_line, ounit, name, iline, macros, stitch)
314 if (len_trim(tmp) == 0) cycle
315
316 in_comment = head(tmp) == '!'
317
318 if (merge(head(res) == '!', in_comment, len_trim(res) > 0)) then
319 f_continue = tail(tmp) == '&'
320 else
321 if (in_comment .and. f_continue) cycle
322 f_continue = .not. in_comment .and. tail(tmp) == '&'
323 end if
324
325 if (f_continue .or. stitch) then
326 reprocess = .true.
327 res = concat(res, tmp)
328 else
329 if (reprocess) then
330 if (.not. in_comment .and. head(res) == '!') then
331 write(ounit, '(A)') res
332 res = process_line(tmp, ounit, name, iline, macros, stitch)
333 else
334 res = process_line(concat(res, tmp), ounit, name, iline, macros, stitch)
335 end if
336 reprocess = .false.
337 else
338 res = trim(tmp)
339 end if
340 if (interactive) write(*, '(/a)', advance='no') ' [out] ' ! Command line prompt
341 write(ounit, '(A)') res
342 res = ''
343 end if
344 end if
345 end do
346
347 if (cond_depth > 0) then
348 if (verbose) print *, "Error: Unclosed conditional block at end of file ", trim(name)
349 end if
350 end subroutine
351
352 !> Process a single (possibly continued) line – handles directives and macro expansion
353 !! Responsibilities:
354 !! - Strip or terminate C-style block comments (`/* ... */`)
355 !! - Detect and delegate preprocessor directives (`#define`, `#include`, conditionals, etc.)
356 !! - Perform macro expansion when the line is in an active conditional block
357 !! - Return whether the next line should be stitched (for Fortran `&` continuation inside macros)
358 !! @param[in] current_line Input line (already continued and trimmed)
359 !! @param[in] ounit Output unit (used only for diagnostics inside called routines)
360 !! @param[in] filepath Current file name (for error messages)
361 !! @param[in] linenum Current line number (for error messages)
362 !! @param[inout] macros(:) Macro table
363 !! @param[out] stch Set to .true. if the expanded line ends with `&` (stitch next line)
364 !! @return Processed line (directives removed, macros expanded)
365 !!
366 !! @b Remarks
367 !! @ingroup group_parser
368 recursive function process_line(current_line, ounit, filepath, linenum, macros, stch) result(rst)
369 character(*), intent(in) :: current_line
370 integer, intent(in) :: ounit
371 character(*), intent(in) :: filepath
372 integer, intent(in) :: linenum
373 type(macro), allocatable, intent(inout) :: macros(:)
374 logical, intent(out) :: stch
375 character(:), allocatable :: rst
376 !private
377 character(:), allocatable :: trimmed_line
378 logical :: active
379 logical, save :: l_in_comment = .false.
380 integer :: idx, comment_start, comment_end, n
381
382 trimmed_line = trim(adjustl(current_line))
383 rst = ''
384 comment_end = index(trimmed_line, '*/')
385 if (l_in_comment .and. comment_end > 0) then
386 trimmed_line = trimmed_line(comment_end + 2:)
387 l_in_comment = .false.
388 end if
389
390 if (l_in_comment) return
391 comment_start = index(trimmed_line, '/*')
392 if (comment_start > 0) then
393 trimmed_line = trimmed_line(:comment_start - 1)
394 l_in_comment = comment_end == 0
395 end if
396 n = len(trimmed_line); if (n == 0) return
397
398 active = is_active()
399 if (verbose) print *, "Processing line ", linenum, ": '", trim(trimmed_line), "'"
400 if (verbose) print *, "is_active() = ", active, ", cond_depth = ", cond_depth
401 if (head(trimmed_line) == '#') then
402 if (len(trimmed_line) == 1) then
403 return !null directive
404 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'DEFINE') .and. active) then
405 call handle_define(trimmed_line, macros, 'DEFINE')
406 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'UNDEF') .and. active) then
407 call handle_undef(trimmed_line, macros, 'UNDEF')
408 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'WARNING') .and. active) then
409 call handle_warning(trimmed_line, macros, 'WARNING')
410 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ERROR') .and. active) then
411 call handle_error(trimmed_line, macros, 'ERROR')
412 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'INCLUDE') .and. active) then
413 call handle_include(trimmed_line, ounit, filepath, linenum, preprocess_unit, macros, 'INCLUDE')
414 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'IFDEF')) then
415 call handle_ifdef(trimmed_line, filepath, linenum, macros, 'IFDEF')
416 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'IFNDEF')) then
417 call handle_ifndef(trimmed_line, filepath, linenum, macros, 'IFNDEF')
418 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ELIFDEF')) then
419 call handle_elifdef(trimmed_line, filepath, linenum, macros, 'ELIFDEF')
420 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ELIFNDEF')) then
421 call handle_elifndef(trimmed_line, filepath, linenum, macros, 'ELIFNDEF')
422 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'IF')) then
423 call handle_if(trimmed_line, filepath, linenum, macros, 'IF')
424 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ELIF')) then
425 call handle_elif(trimmed_line, filepath, linenum, macros, 'ELIF')
426 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ELSE')) then
427 call handle_else(filepath, linenum)
428 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'ENDIF')) then
429 call handle_endif(filepath, linenum)
430 else if (starts_with(uppercase(adjustl(trimmed_line(2:))), 'PRAGMA') .and. active) then
431 rst = trimmed_line
432 end if
433 else if (active) then
434 if (.not. global%expand_macros) then
435 rst = trimmed_line
436 else
437 rst = adjustl(expand_all(trimmed_line, macros, filepath, linenum, stch, global%extra_macros))
438 if (verbose) print *, "Writing to output: '", trim(rst), "'"
439 end if
440 end if
441 end function
442end module
integer, public cond_depth
Current nesting depth of conditional directives (0 = outside any if)
type(cond_state), dimension(max_cond_depth), public cond_stack
Global stack of conditional states (depth-limited)
type(global_settings), public global
The single global instance used throughout fpx Initialized automatically with sensible defaults value...
Definition global.f90:92
logical, public verbose
Master switch for verbose diagnostic output Default value is .false. (quiet mode)....
Definition logging.f90:56
integer, parameter, public os_windowsx86
Microsoft Windows — explicitly 32-bit (x86) architecture.
Definition os.f90:75
integer, parameter, public os_windows
Microsoft Windows (native, 32-bit or 64-bit)
Definition os.f90:58
integer function, public get_os_type()
Determine the current operating system type Returns one of the OS_* constants. Detection is performed...
Definition os.f90:120
subroutine preprocess_file_to_unit(ifile, ounit)
Preprocess a file and write to an already-open output unit.
Definition parser.f90:180
subroutine preprocess_unit_to_unit(iunit, ounit)
Core preprocessing routine: read from iunit, write to ounit Sets up a clean macro environment for the...
Definition parser.f90:211
subroutine preprocess_unit(iunit, ounit, macros, from_include)
Worker routine that reads lines, handles continuations, comments and directives This is the main loop...
Definition parser.f90:257
subroutine preprocess_file(filepath, outputfile)
Preprocess a file and write result to an optional output file (default: stdout) Opens the input file,...
Definition parser.f90:112
subroutine preprocess_unit_to_file(iunit, ofile)
Preprocess from an already-open input unit and write to a file.
Definition parser.f90:152
recursive character(:) function, allocatable process_line(current_line, ounit, filepath, linenum, macros, stch)
Process a single (possibly continued) line – handles directives and macro expansion Responsibilities:
Definition parser.f90:369
Add one or more macros to a dynamic table.
Definition macro.f90:113
Return current number of stored macros.
Definition macro.f90:156
Generic interface to start preprocessing from various sources/sinks.
Definition parser.f90:80
Return the trimmed string.
Definition string.f90:146
Derived type representing a single preprocessor macro Extends string with macro-specific fields: rep...
Definition macro.f90:103