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_context
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
100contains
101
102 !> Preprocess a file and write result to an optional output file (default: stdout)
103 !! Opens the input file, determines the base filename for error messages,
104 !! opens the output file if requested, and delegates to the unit-to-unit routine.
105 !! @param[in] filepath Path to the input source file
106 !! @param[in] outputfile Optional path to the output file; if absent output goes to stdout
107 !!
108 !! @b Remarks
109 !! @ingroup group_parser
110 subroutine preprocess_file(filepath, outputfile)
111 character(*), intent(in) :: filepath
112 character(*), intent(in), optional :: outputfile
113 !private
114 integer :: iunit, ierr, n, ounit
115 character(len=1, kind=c_char) :: buf(256)
116
117 open(newunit=iunit, file=filepath, status='old', action='read', iostat=ierr)
118 if (ierr /= 0) then
119 call printf(render(diagnostic_report(level_error, &
120 message='Error opening input file: ' // trim(filepath), &
121 source=name), &
122 ''))
123 return
124 else
125 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
126 n = findloc(buf, achar(0), 1)
127 name = filepath(n + 1:)
128 end if
129 end if
130
131 if (present(outputfile)) then
132 open(newunit=ounit, file=outputfile, status='replace', action='write', iostat=ierr)
133 if (ierr /= 0) then
134 call printf(render(diagnostic_report(level_error, &
135 message='Error opening input file: ' // trim(outputfile), &
136 source=name), &
137 ''))
138 close(iunit)
139 return
140 end if
141 else
142 ounit = stdout
143 end if
144
145 call preprocess(iunit, ounit)
146 if (iunit /= stdin) close(iunit)
147 if (ounit /= stdout) close(ounit)
148 end subroutine
149
150 !> Preprocess from an already-open input unit and write to a file
151 !! @param[in] iunit Input unit (must already be open for reading)
152 !! @param[in] ofile Output filename
153 !!
154 !! @b Remarks
155 !! @ingroup group_parser
156 subroutine preprocess_unit_to_file(iunit, ofile)
157 integer, intent(in) :: iunit
158 character(*), intent(in) :: ofile
159 !private
160 integer :: ierr, ounit
161
162 if (iunit /= stdin) then
163 inquire(unit = iunit, name=name)
164 end if
165
166 open(newunit=ounit, file=ofile, status='replace', action='write', iostat=ierr)
167 if (ierr /= 0) then
168 call printf(render(diagnostic_report(level_error, &
169 message='Error opening input file: ' // trim(ofile), &
170 source=name), &
171 ''))
172 close(iunit)
173 return
174 end if
175
176 call preprocess(iunit, ounit)
177 if (iunit /= stdin) close(iunit)
178 if (ounit /= stdout) close(ounit)
179 end subroutine
180
181 !> Preprocess a file and write to an already-open output unit
182 !! @param[in] ifile Input filename
183 !! @param[in] ounit Output unit (already open for writing)
184 !!
185 !! @b Remarks
186 !! @ingroup group_parser
187 subroutine preprocess_file_to_unit(ifile, ounit)
188 character(*), intent(in) :: ifile
189 integer, intent(in) :: ounit
190 !private
191 integer :: iunit, ierr, n
192 character(len=1, kind=c_char) :: buf(256)
193
194 open(newunit=iunit, file=ifile, status='old', action='read', iostat=ierr)
195 if (ierr /= 0) then
196 call printf(render(diagnostic_report(level_error, &
197 message='Error opening input file: ' // trim(ifile), &
198 source=name), &
199 ''))
200 return
201 else
202 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
203 n = findloc(buf, achar(0), 1)
204 name = ifile(n + 1:)
205 end if
206 end if
207
208 call preprocess(iunit, ounit)
209 if (iunit /= stdin) close(iunit)
210 if (ounit /= stdout) close(ounit)
211 end subroutine
212
213 !> Core preprocessing routine: read from iunit, write to ounit
214 !! Sets up a clean macro environment for the top-level file,
215 !! resets conditional compilation state, and calls the worker routine.
216 !! @param[in] iunit Input unit
217 !! @param[in] ounit Output unit
218 !!
219 !! @b Remarks
220 !! @ingroup group_parser
221 subroutine preprocess_unit_to_unit(iunit, ounit)
222 integer, intent(in) :: iunit
223 integer, intent(in) :: ounit
224 !private
225 type(macro), allocatable :: macros(:)
226
227 if (.not. allocated(global%macros)) allocate(global%macros(0))
228 allocate(macros(sizeof(global%macros)), source=global%macros)
229 if (.not. allocated(global%undef)) allocate(global%undef(0))
230 if (.not. allocated(global%includedir)) allocate(global%includedir(0))
231
232 cond_depth = 0
233 cond_stack(1)%active = .true.
234 cond_stack(1)%has_met = .false.
235
236 reprocess = .false.; c_continue = .false.; f_continue = .false.
237 icontinuation = 1; iline = 0
238 continued_line = ''; res = ''
239
240 call preprocess_unit(iunit, ounit, macros, .false.)
241 deallocate(macros)
242 end subroutine
243
244 !> Worker routine that reads lines, handles continuations, comments and directives
245 !! This is the main loop that:
246 !! - reads lines with interactive prompt when iunit==stdin
247 !! - handles both `\` and `&` continuations
248 !! - strips or preserves comments appropriately
249 !! - calls process_line() for directive processing and macro expansion
250 !! - stitches lines when Fortran continuation (`&`) is active
251 !! @param[in] iunit Input unit
252 !! @param[in] ounit Output unit
253 !! @param[inout] macros(:) Current macro table (passed by value between include levels)
254 !! @param[in] from_include True if called recursively from #include
255 !!
256 !! @b Remarks
257 !! @ingroup group_parser
258 subroutine preprocess_unit(iunit, ounit, macros, from_include)
259 integer, intent(in) :: iunit
260 integer, intent(in) :: ounit
261 type(macro), allocatable, intent(inout) :: macros(:)
262 logical, intent(in) :: from_include
263 !private
264 integer :: ierr, n
265
266 do
267 if (global%interactive) write(*, '(/a)', advance='no') ' [in] ' ! Command line prompt
268 read(iunit, '(A)', iostat=ierr) line
269
270 if (global%interactive) then
271 if (line == '') exit
272 if (lowercase(trim(adjustl(line))) == 'quit') exit
273 end if
274 if (ierr /= 0) then
275 if (ierr == iostat_end .and. from_include) f_continue = tail(tmp) == '&'
276 exit
277 end if
278 if (.not. from_include) iline = iline + 1
279
280 if (c_continue) then
281 continued_line = continued_line(:icontinuation) // trim(adjustl(line))
282 else
283 continued_line = trim(adjustl(line))
284 end if
285 n = len_trim(continued_line); if (n == 0) cycle
286
287 ! Check for line continuation with '\'
288 if (verify(continued_line(n:n), '\') == 0) then
289 ! Check for line break with '\\'
290 if (continued_line(len_trim(continued_line) - 1:len_trim(continued_line)) == '\\' .and. global%line_break) then
291 c_continue = .true.
292 continued_line = continued_line(:len_trim(continued_line) - 2) // new_line('A') ! Strip '\\'
293 icontinuation = len_trim(continued_line)
294 else
295 c_continue = .true.
296 icontinuation = len_trim(continued_line) - 1
297 continued_line = continued_line(:icontinuation)
298 end if
299 cycle
300 else
301 c_continue = .false.
302
303 tmp = process_line(continued_line, ounit, name, iline, macros, stitch)
304 if (len_trim(tmp) == 0) cycle
305
306 in_comment = head(tmp) == '!'
307
308 if (merge(head(res) == '!', in_comment, len_trim(res) > 0)) then
309 f_continue = tail(tmp) == '&'
310 else
311 if (in_comment .and. f_continue) cycle
312 f_continue = .not. in_comment .and. tail(tmp) == '&'
313 end if
314
315 if (f_continue .or. stitch) then
316 reprocess = .true.
317 res = concat(res, tmp)
318 else
319 if (reprocess) then
320 if (.not. in_comment .and. head(res) == '!') then
321 write(ounit, '(A)') res
322 res = process_line(tmp, ounit, name, iline, macros, stitch)
323 else
324 res = process_line(concat(res, tmp), ounit, name, iline, macros, stitch)
325 end if
326 reprocess = .false.
327 else
328 res = trim(tmp)
329 end if
330 if (global%interactive) write(*, '(/a)', advance='no') ' [out] ' ! Command line prompt
331 write(ounit, '(A)') res
332 res = ''
333 end if
334 end if
335 end do
336
337 if (cond_depth > 0) then
338 call printf(render(diagnostic_report(level_error, &
339 message='Unclosed conditional block at end of file', &
340 label=label_type('Missing conditional statement #endif', 1, 1), &
341 source=name), &
342 trim(line), iline))
343 else if (c_continue) then
344 call printf(render(diagnostic_report(level_error, &
345 message='Unexpected character', &
346 label=label_type('Trailing new line "\"', len(trim(line)), 1), &
347 source=name), &
348 trim(line), iline))
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(inout) :: filepath
372 integer, intent(inout) :: 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 type(context) :: ctx
382
383 trimmed_line = trim(adjustl(current_line))
384 rst = ''
385 comment_end = index(trimmed_line, '*/')
386 if (l_in_comment .and. comment_end > 0) then
387 trimmed_line = trimmed_line(comment_end + 2:)
388 l_in_comment = .false.
389 end if
390
391 if (l_in_comment) return
392 comment_start = index(trimmed_line, '/*')
393 if (comment_start > 0) then
394 trimmed_line = trimmed_line(:comment_start - 1)
395 l_in_comment = comment_end == 0
396 end if
397 n = len(trimmed_line); if (n == 0) return
398
399 active = is_active()
400 ctx = context(trimmed_line, linenum, filepath)
401 if (head(trimmed_line) == '#') then
402 if (len(trimmed_line) == 1) then
403 return !null directive
404 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'define') .and. active) then
405 call handle_define(ctx, macros, 'define')
406 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'undef') .and. active) then
407 call handle_undef(ctx, macros, 'undef')
408 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'warning') .and. active) then
409 call handle_warning(ctx, macros, 'warning')
410 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'error') .and. active) then
411 call handle_error(ctx, macros, 'error')
412 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'include') .and. active) then
413 call handle_include(ctx, ounit, preprocess_unit, macros, 'include')
414 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'line')) then
415 call handle_line(ctx, 'line')
416 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifdef')) then
417 call handle_ifdef(ctx, macros, 'ifdef')
418 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifndef')) then
419 call handle_ifndef(ctx, macros, 'ifndef')
420 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifdef')) then
421 call handle_elifdef(ctx, macros, 'elifdef')
422 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifndef')) then
423 call handle_elifndef(ctx, macros, 'elifndef')
424 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'if')) then
425 call handle_if(ctx, macros, 'if')
426 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elif')) then
427 call handle_elif(ctx, macros, 'elif')
428 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'else')) then
429 call handle_else(ctx)
430 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'endif')) then
431 call handle_endif(ctx)
432 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'pragma') .and. active) then
433 rst = ctx%content
434 else
435 return
436 end if
437 else if (active) then
438 if (.not. global%expand_macros) then
439 rst = trimmed_line
440 else
441 rst = adjustl(expand_all(ctx, macros, stch, global%extra_macros, global%&
442 implicit_continuation))
443 end if
444 end if
445 end function
446end 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:208
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:78
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...
type(global_settings), public global
The single global instance used throughout fpx Initialized automatically with sensible defaults value...
Definition global.f90:93
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:117
character(:) function, allocatable, public expand_all(ctx, macros, stitch, has_extra, implicit_conti)
Fully expand a line including predefined macros (FILE, LINE, etc.) First performs normal macro expans...
Definition macro.f90:196
subroutine preprocess_file_to_unit(ifile, ounit)
Preprocess a file and write to an already-open output unit.
Definition parser.f90:188
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:222
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:259
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:111
subroutine preprocess_unit_to_file(iunit, ofile)
Preprocess from an already-open input unit and write to a file.
Definition parser.f90:157
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
character function, public tail(str)
Returns the last non-blank character of a string.
Definition string.f90:531
pure character(len_trim(str)) function, public lowercase(str)
Convert string to lower case (respects contents of quotes).
Definition string.f90:640
character(:) function, allocatable, public concat(str1, str2)
Smart concatenation that removes continuation markers (&) and handles line-continuation rules.
Definition string.f90:549
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:498
character function, public head(str)
Returns the first non-blank character of a string.
Definition string.f90:516
Interface to render diagnostic messages and labels.
Definition logging.f90:185
Return current number of stored macros.
Definition macro.f90:158
Generic interface to start preprocessing from various sources/sinks.
Definition parser.f90:80
Index operator.
Definition string.f90:178
Return the trimmed length of a string.
Definition string.f90:141
Return the length of a string.
Definition string.f90:133
Return the trimmed string.
Definition string.f90:149
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:94