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!! - Non-standard `#for` directive
12!! - C-style `/* ... */` comments (nestable aware)
13!! - Macro expansion with argument substitution and stringification (`#`) / token-pasting (`##`)
14!! - Interactive REPL mode when reading from stdin
15!! - Multiple entry points for file-to-file, unit-to-unit, etc.
16!! - Support ${x} for substituting macro name
17!!
18!! The preprocessor is designed to be standards-conforming where possible while adding
19!! useful extensions (variadic macros, better diagnostics, include path handling).
20!!
21!! @section parser_examples Examples
22!!
23!! 1. Preprocess a file to stdout:
24!! @code{.f90}
25!! call preprocess('input.F90')
26!! @endcode
27!!
28!! 2. Preprocess a file and write to another file:
29!! @code{.f90}
30!! call preprocess('src/main.F90', 'preprocessed/main.F90')
31!! @endcode
32!!
33!! 3. Use in a build system with unit numbers:
34!! @code{.f90}
35!! integer :: iu, ou
36!! open(newunit=iu, file='input.F90')
37!! open(newunit=ou, file='output.F90')
38!! call preprocess(iu, ou)
39!! close(iu); close(ou)
40!! ...
41!! @endcode
42!!
43!! 4. Interactive mode (stdin to stdout):
44!! @code
45!! $ ./fpx
46!! [in] #define PI 3.1415926535
47!! [out]
48!! [in] real :: x = PI*2
49!! [out] real :: x = 3.1415926535*2
50!! [in] (empty line or 'quit' to exit)
51!! @endcode
52module fpx_parser
53 use, intrinsic :: iso_fortran_env, only: stdout => output_unit, iostat_end, stdin => input_unit
54 use, intrinsic :: iso_c_binding, only: c_char, c_size_t, c_ptr, c_null_ptr, c_associated, c_funloc
55 use fpx_constants
56 use fpx_string
57 use fpx_logging
58 use fpx_macro
59 use fpx_conditional
60 use fpx_define
61 use fpx_diagnostics
62 use fpx_include
63 use fpx_path
64 use fpx_global
65 use fpx_context
66 use fpx_line
67 use fpx_for
68
69 implicit none; private
70
71 public :: preprocess, &
72 global
73
74 !> Generic interface to start preprocessing from various sources/sinks
75 !!
76 !! Allows preprocessing:
77 !! - file to stdout
78 !! - file to file
79 !! - unit to file
80 !! - unit to unit (most flexible, used internally for #include)
81 !!
82 !! @b Remarks
83 !! @ingroup group_parser
84 interface preprocess
85 module procedure :: preprocess_file
86 module procedure :: preprocess_file_to_unit
87 module procedure :: preprocess_unit_to_file
88 module procedure :: preprocess_unit_to_unit
89 end interface
90
91 character(256) :: name !< Current source file name (without path)
92 logical :: c_continue !< Flags for C-style continuation
93 logical :: f_continue !< Flags for Fortran-style continuation
94 logical :: in_comment !< Internal state flags
95 logical :: reprocess !< Internal state flags
96 logical :: stitch !< Internal state flags
97 character(:), allocatable :: res !< Accumulated result line buffers
98 character(:), allocatable :: tmp !< Accumulated temporary line buffers
99 character(MAX_LINE_LEN) :: line !< Raw input line
100 character(MAX_LINE_LEN) :: continued_line !< Raw and continued input line
101 integer :: iline !< Current line number position
102 integer :: icontinuation !< Continuation position
103
104contains
105
106 !> Preprocess a file and write result to an optional output file (default: stdout)
107 !! Opens the input file, determines the base filename for error messages,
108 !! opens the output file if requested, and delegates to the unit-to-unit routine.
109 !! @param[in] filepath Path to the input source file
110 !! @param[in] outputfile Optional path to the output file; if absent output goes to stdout
111 !!
112 !! @b Remarks
113 !! @ingroup group_parser
114 subroutine preprocess_file(filepath, outputfile)
115 character(*), intent(in) :: filepath
116 character(*), intent(in), optional :: outputfile
117 !private
118 integer :: iunit, ierr, n, ounit
119 character(len=1, kind=c_char) :: buf(256)
120
121 open(newunit=iunit, file=filepath, status='old', action='read', iostat=ierr)
122
123 if (ierr /= 0) then
124 call printf(render(diagnostic_report(level_error, &
125 message='Error opening input file: ' // trim(filepath), &
126 source=name), &
127 ''))
128 return
129 else
130 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
131 n = findloc(buf, achar(0), 1)
132 name = filepath(n + 1:)
133 end if
134 end if
135
136 if (present(outputfile)) then
137 open(newunit=ounit, file=outputfile, status='replace', action='write', iostat=ierr)
138 if (ierr /= 0) then
139 call printf(render(diagnostic_report(level_error, &
140 message='Error opening input file: ' // trim(outputfile), &
141 source=name), &
142 ''))
143 close(iunit)
144 return
145 end if
146 else
147 ounit = stdout
148 end if
149
150 call preprocess(iunit, ounit)
151 if (iunit /= stdin) close(iunit)
152 if (ounit /= stdout) close(ounit)
153 end subroutine
154
155 !> Preprocess from an already-open input unit and write to a file
156 !! @param[in] iunit Input unit (must already be open for reading)
157 !! @param[in] ofile Output filename
158 !!
159 !! @b Remarks
160 !! @ingroup group_parser
161 subroutine preprocess_unit_to_file(iunit, ofile)
162 integer, intent(in) :: iunit
163 character(*), intent(in) :: ofile
164 !private
165 integer :: ierr, ounit
166
167 if (iunit /= stdin) then
168 inquire(unit = iunit, name=name)
169 end if
170
171 open(newunit=ounit, file=ofile, status='replace', action='write', iostat=ierr)
172 if (ierr /= 0) then
173 call printf(render(diagnostic_report(level_error, &
174 message='Error opening input file: ' // trim(ofile), &
175 source=name), &
176 ''))
177 close(iunit)
178 return
179 end if
180
181 call preprocess(iunit, ounit)
182 if (iunit /= stdin) close(iunit)
183 if (ounit /= stdout) close(ounit)
184 end subroutine
185
186 !> Preprocess a file and write to an already-open output unit
187 !! @param[in] ifile Input filename
188 !! @param[in] ounit Output unit (already open for writing)
189 !!
190 !! @b Remarks
191 !! @ingroup group_parser
192 subroutine preprocess_file_to_unit(ifile, ounit)
193 character(*), intent(in) :: ifile
194 integer, intent(in) :: ounit
195 !private
196 integer :: iunit, ierr, n
197 character(len=1, kind=c_char) :: buf(256)
198
199 open(newunit=iunit, file=ifile, status='old', action='read', iostat=ierr)
200 if (ierr /= 0) then
201 call printf(render(diagnostic_report(level_error, &
202 message='Error opening input file: ' // trim(ifile), &
203 source=name), &
204 ''))
205 return
206 else
207 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
208 n = findloc(buf, achar(0), 1)
209 name = ifile(n + 1:)
210 end if
211 end if
212
213 call preprocess(iunit, ounit)
214 if (iunit /= stdin) close(iunit)
215 if (ounit /= stdout) close(ounit)
216 end subroutine
217
218 !> Core preprocessing routine: read from iunit, write to ounit
219 !! Sets up a clean macro environment for the top-level file,
220 !! resets conditional compilation state, and calls the worker routine.
221 !! @param[in] iunit Input unit
222 !! @param[in] ounit Output unit
223 !!
224 !! @b Remarks
225 !! @ingroup group_parser
226 subroutine preprocess_unit_to_unit(iunit, ounit)
227 integer, intent(in) :: iunit
228 integer, intent(in) :: ounit
229 !private
230 type(macro), allocatable :: macros(:)
231
232 if (.not. allocated(global%macros)) allocate(global%macros(0))
233 allocate(macros(size_of(global%macros)), source=global%macros)
234 if (.not. allocated(global%undef)) allocate(global%undef(0))
235 if (.not. allocated(global%includedir)) allocate(global%includedir(0))
236
237 cond_depth = 0
238 cond_stack(1)%active = .true.
239 cond_stack(1)%has_met = .false.
240
241 reprocess = .false.; c_continue = .false.; f_continue = .false.
242 icontinuation = 1; iline = 0
243 continued_line = ''; res = ''
244
245 call preprocess_unit(iunit, ounit, macros, .false.)
246 deallocate(macros)
247 end subroutine
248
249 !> Worker routine that reads lines, handles continuations, comments and directives
250 !! This is the main loop that:
251 !! - reads lines with interactive prompt when iunit==stdin
252 !! - handles both `\` and `&` continuations
253 !! - strips or preserves comments appropriately
254 !! - calls process_line() for directive processing and macro expansion
255 !! - stitches lines when Fortran continuation (`&`) is active
256 !! @param[in] iunit Input unit
257 !! @param[in] ounit Output unit
258 !! @param[inout] macros(:) Current macro table (passed by value between include levels)
259 !! @param[in] from_include True if called recursively from #include
260 !!
261 !! @b Remarks
262 !! @ingroup group_parser
263 subroutine preprocess_unit(iunit, ounit, macros, from_include)
264 integer, intent(in) :: iunit
265 integer, intent(in) :: ounit
266 type(macro), allocatable, intent(inout) :: macros(:)
267 logical, intent(in) :: from_include
268 !private
269 integer :: ierr, n
270
271 do
272 if (global%interactive) write(*, '(/a)', advance='no') ' [in] ' ! Command line prompt
273 read(iunit, '(A)', iostat=ierr) line
274
275 if (global%interactive) then
276 if (line == '') exit
277 if (lowercase(trim(adjustl(line))) == 'quit') exit
278 end if
279 if (ierr /= 0) then
280 if (ierr == iostat_end .and. from_include) f_continue = tail(tmp) == '&'
281 exit
282 end if
283 if (.not. from_include) iline = iline + 1
284
285 if (c_continue) then
286 continued_line = continued_line(:icontinuation) // trim(adjustl(line))
287 else
288 continued_line = trim(adjustl(line))
289 end if
290 n = len_trim(continued_line); if (n == 0) cycle
291
292 ! Check for line continuation with '\'
293 if (verify(continued_line(n:n), '\') == 0) then
294 ! Check for line break with '\\'
295 if (continued_line(len_trim(continued_line) - 1:len_trim(continued_line)) == '\\' .and. global%line_break) then
296 c_continue = .true.
297 continued_line = continued_line(:len_trim(continued_line) - 2) // new_line('A') ! Strip '\\'
298 icontinuation = len_trim(continued_line)
299 else
300 c_continue = .true.
301 icontinuation = len_trim(continued_line) - 1
302 continued_line = continued_line(:icontinuation)
303 end if
304 cycle
305 else
306 c_continue = .false.
307
308 tmp = process_line(continued_line, ounit, name, iline, macros, stitch)
309 if (len_trim(tmp) == 0) cycle
310
311 in_comment = head(tmp) == '!'
312
313 if (merge(head(res) == '!', in_comment, len_trim(res) > 0)) then
314 f_continue = tail(tmp) == '&'
315 else
316 if (in_comment .and. f_continue) cycle
317 f_continue = .not. in_comment .and. tail(tmp) == '&'
318 end if
319
320 if ((.not. global%disable_continuation) .and. (f_continue .or. stitch)) then
321 reprocess = .true.
322 res = concat(res, tmp)
323 else
324 if (reprocess) then
325 if (.not. in_comment .and. head(res) == '!') then
326 if (is_in_forloop()) then
327 call add_to_loop(res)
328 else
329 write(ounit, '(A)') res
330 end if
331 res = process_line(tmp, ounit, name, iline, macros, stitch)
332 else
333 res = process_line(concat(res, tmp), ounit, name, iline, macros, stitch)
334 end if
335 reprocess = .false.
336 else
337 res = trim(tmp)
338 end if
339
340 if (is_in_forloop()) then
341 call add_to_loop(res)
342 else
343 if (global%interactive) write(*, '(/a)', advance='no') ' [out] ' ! Command line prompt
344 write(ounit, '(A)') res
345 end if
346 res = ''
347 end if
348 end if
349 end do
350
351 if (cond_depth > 0) then
352 call printf(render(diagnostic_report(level_error, &
353 message='Unclosed conditional block at end of file', &
354 source=name), &
355 trim(line), iline))
356 else if (c_continue) then
357 call printf(render(diagnostic_report(level_error, &
358 message='Unexpected character', &
359 label=label_type('Trailing new line "\"', len(trim(line)), 1), &
360 source=name), &
361 trim(line), iline))
362 end if
363 end subroutine
364
365 !> Process a single (possibly continued) line – handles directives and macro expansion
366 !! Responsibilities:
367 !! - Strip or terminate C-style block comments (`/* ... */`)
368 !! - Detect and delegate preprocessor directives (`#define`, `#include`, conditionals, etc.)
369 !! - Perform macro expansion when the line is in an active conditional block
370 !! - Return whether the next line should be stitched (for Fortran `&` continuation inside macros)
371 !! @param[in] current_line Input line (already continued and trimmed)
372 !! @param[in] ounit Output unit (used only for diagnostics inside called routines)
373 !! @param[in] filepath Current file name (for error messages)
374 !! @param[in] linenum Current line number (for error messages)
375 !! @param[inout] macros(:) Macro table
376 !! @param[out] stch Set to .true. if the expanded line ends with `&` (stitch next line)
377 !! @return Processed line (directives removed, macros expanded)
378 !!
379 !! @b Remarks
380 !! @ingroup group_parser
381 recursive function process_line(current_line, ounit, filepath, linenum, macros, stch) result(rst)
382 character(*), intent(in) :: current_line
383 integer, intent(in) :: ounit
384 character(*), intent(inout) :: filepath
385 integer, intent(inout) :: linenum
386 type(macro), allocatable, intent(inout) :: macros(:)
387 logical, intent(out) :: stch
388 character(:), allocatable :: rst
389 !private
390 character(:), allocatable :: trimmed_line
391 logical :: active
392 logical, save :: l_in_comment = .false., l_in_loop = .false.
393 integer :: idx, comment_start, comment_end, n
394 type(context) :: ctx
395
396 trimmed_line = trim(adjustl(current_line))
397 rst = ''
398 comment_end = index(trimmed_line, '*/')
399 if (l_in_comment .and. comment_end > 0) then
400 trimmed_line = trimmed_line(comment_end + 2:)
401 l_in_comment = .false.
402 end if
403
404 if (l_in_comment) return
405 comment_start = index(trimmed_line, '/*')
406 if (comment_start > 0) then
407 trimmed_line = trimmed_line(:comment_start - 1)
408 l_in_comment = comment_end == 0
409 end if
410 n = len(trimmed_line); if (n == 0) return
411
412 active = is_active()
413 ctx = context(trimmed_line, linenum, filepath)
414 if (head(trimmed_line) == '#') then
415 if (len(trimmed_line) == 1) then
416 return !null directive
417 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'for')) then
418 l_in_loop = .true.
419 if (global%support_forloop) call handle_for(ctx, macros, 'for')
420 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'endfor')) then
421 l_in_loop = .false.
422 if (global%support_forloop) call handle_endfor(ctx, ounit, c_funloc(process_line), macros, 'endfor')
423 l_in_loop = is_in_forloop()
424 else if (l_in_loop) then
425 rst = trimmed_line
426 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'define') .and. active) then
427 call handle_define(ctx, macros, 'define')
428 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'undef') .and. active) then
429 call handle_undef(ctx, macros, 'undef')
430 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'warning') .and. active) then
431 call handle_warning(ctx, macros, 'warning')
432 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'error') .and. active) then
433 call handle_error(ctx, macros, 'error')
434 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'include') .and. active) then
435 call handle_include(ctx, ounit, preprocess_unit, macros, 'include')
436 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'line')) then
437 call handle_line(ctx, 'line')
438 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifdef')) then
439 call handle_ifdef(ctx, macros, 'ifdef')
440 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifndef')) then
441 call handle_ifndef(ctx, macros, 'ifndef')
442 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifdef')) then
443 call handle_elifdef(ctx, macros, 'elifdef')
444 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifndef')) then
445 call handle_elifndef(ctx, macros, 'elifndef')
446 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'if')) then
447 call handle_if(ctx, macros, 'if')
448 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elif')) then
449 call handle_elif(ctx, macros, 'elif')
450 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'else')) then
451 call handle_else(ctx)
452 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'endif')) then
453 call handle_endif(ctx)
454 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'pragma') .and. active) then
455 rst = ctx%content
456 else
457 return
458 end if
459 else if (active) then
460 if (.not. global%expand_macros .or. is_in_forloop()) then
461 rst = trimmed_line
462 else
463 rst = adjustl(expand_all(ctx, macros, stch, global%extra_macros, global%implicit_continuation, &
464 global%implicit_continuation))
465 end if
466 end if
467 end function
468end module
subroutine, public handle_ifndef(ctx, macros, token)
Process ifndef – test if a macro is NOT defined.
logical function, public is_active()
Determine if current line is inside an active conditional block.
subroutine, public handle_ifdef(ctx, macros, token)
Process ifdef – test if a macro is defined.
integer, public cond_depth
Current nesting depth of conditional directives (0 = outside any if).
subroutine, public handle_elif(ctx, macros, token)
Process elif – alternative branch after if/elif Only activates if no previous branch in the group was...
subroutine, public handle_elifndef(ctx, macros, token)
Process elifndef – test if a macro is not defined.
subroutine, public handle_else(ctx)
Process else – final fallback branch Activates only if no previous if/elif branch was true.
type(cond_state), dimension(max_cond_depth), public cond_stack
Global stack of conditional states (depth-limited).
subroutine, public handle_if(ctx, macros, token)
Process a if directive with constant expression evaluation Evaluates the expression after if using ev...
subroutine, public handle_elifdef(ctx, macros, token)
Process elifdef – test if a macro is defined.
subroutine, public handle_endif(ctx)
Process endif – end of conditional block Pops the top state from the stack. Reports error on unmatche...
subroutine, public handle_undef(ctx, macros, token)
Process a undef directive and remove a macro from the table Finds the named macro in the current tabl...
Definition define.f90:222
subroutine, public handle_define(ctx, macros, token)
Process a define directive and register or update a macro Parses the line after #define,...
Definition define.f90:80
subroutine, public handle_error(ctx, macros, token)
Process a error directive. It causes the preprocessor to report a fatal error that stops the preproce...
subroutine, public handle_warning(ctx, macros, token)
Process a warning directive. It causes the preprocessor to report a warning that does not stop the pr...
subroutine, public handle_for(ctx, macros, token)
Process a #for directive and initialize a new loop context.
Definition loop.f90:129
subroutine, public add_to_loop(line)
Append a source line to the currently active loop body.
Definition loop.f90:347
subroutine, public handle_endfor(ctx, ounit, p, macros, token)
Finalize a loop and emit all expanded iterations.
Definition loop.f90:252
logical function, public is_in_forloop()
Query whether parsing is currently inside a #for block.
Definition loop.f90:360
type(global_settings), public global
The single global instance used throughout fpx Initialized automatically with sensible defaults value...
Definition global.f90:96
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:101
subroutine, public handle_line(ctx, token)
Handle the standard line directive Supports two standard forms: line <number> line <number> "<filenam...
Definition line.f90:69
character(:) function, allocatable, public expand_all(ctx, macros, stitch, has_extra, implicit_conti, dollar_insert)
Fully expand a line including predefined macros (FILE, LINE, etc.) First performs normal macro expans...
Definition macro.f90:231
character function, public tail(str)
Returns the last non-blank character of a string.
Definition string.f90:533
pure character(len_trim(str)) function, public lowercase(str)
Convert string to lower case (respects contents of quotes).
Definition string.f90:642
character(:) function, allocatable, public concat(str1, str2)
Smart concatenation that removes continuation markers (&) and handles line-continuation rules.
Definition string.f90:551
logical function, public starts_with(str, arg1, idx)
Checks if a string starts with a given prefix Returns .true. if the string str (after trimming leadin...
Definition string.f90:500
character function, public head(str)
Returns the first non-blank character of a string.
Definition string.f90:518
Interface to render diagnostic messages and labels.
Definition logging.f90:185
Return current number of stored macros.
Definition macro.f90:163
Generic interface to start preprocessing from various sources/sinks.
Definition parser.f90:84
Index operator.
Definition string.f90:180
Return the trimmed length of a string.
Definition string.f90:143
Return the length of a string.
Definition string.f90:135
Return the trimmed string.
Definition string.f90:151
Source location and content snapshot for precise diagnostics Instances of this type are created for e...
Definition context.f90:99
Definition of diagnostic message.
Definition logging.f90:269
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition logging.f90:246
Derived type representing a single preprocessor macro Extends string with macro-specific fields: rep...
Definition macro.f90:98