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 use fpx_line
65
66 implicit none; private
67
68 public :: preprocess, &
69 global
70
71 !> Generic interface to start preprocessing from various sources/sinks
72 !!
73 !! Allows preprocessing:
74 !! - file to stdout
75 !! - file to file
76 !! - unit to file
77 !! - unit to unit (most flexible, used internally for #include)
78 !!
79 !! @b Remarks
80 !! @ingroup group_parser
81 interface preprocess
82 module procedure :: preprocess_file
83 module procedure :: preprocess_file_to_unit
84 module procedure :: preprocess_unit_to_file
85 module procedure :: preprocess_unit_to_unit
86 end interface
87
88 character(256) :: name !< Current source file name (without path)
89 logical :: c_continue !< Flags for C-style continuation
90 logical :: f_continue !< Flags for Fortran-style continuation
91 logical :: in_comment !< Internal state flags
92 logical :: reprocess !< Internal state flags
93 logical :: stitch !< Internal state flags
94 character(:), allocatable :: res !< Accumulated result line buffers
95 character(:), allocatable :: tmp !< Accumulated temporary line buffers
96 character(MAX_LINE_LEN) :: line !< Raw input line
97 character(MAX_LINE_LEN) :: continued_line !< Raw and continued input line
98 integer :: iline !< Current line number position
99 integer :: icontinuation !< Continuation position
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 call printf(render(diagnostic_report(level_error, &
121 message='Error opening input file: ' // trim(filepath), &
122 source=name), &
123 ''))
124 return
125 else
126 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
127 n = findloc(buf, achar(0), 1)
128 name = filepath(n + 1:)
129 end if
130 end if
131
132 if (present(outputfile)) then
133 open(newunit=ounit, file=outputfile, status='replace', action='write', iostat=ierr)
134 if (ierr /= 0) then
135 call printf(render(diagnostic_report(level_error, &
136 message='Error opening input file: ' // trim(outputfile), &
137 source=name), &
138 ''))
139 close(iunit)
140 return
141 end if
142 else
143 ounit = stdout
144 end if
145
146 call preprocess(iunit, ounit)
147 if (iunit /= stdin) close(iunit)
148 if (ounit /= stdout) close(ounit)
149 end subroutine
150
151 !> Preprocess from an already-open input unit and write to a file
152 !! @param[in] iunit Input unit (must already be open for reading)
153 !! @param[in] ofile Output filename
154 !!
155 !! @b Remarks
156 !! @ingroup group_parser
157 subroutine preprocess_unit_to_file(iunit, ofile)
158 integer, intent(in) :: iunit
159 character(*), intent(in) :: ofile
160 !private
161 integer :: ierr, ounit
162
163 if (iunit /= stdin) then
164 inquire(unit = iunit, name=name)
165 end if
166
167 open(newunit=ounit, file=ofile, status='replace', action='write', iostat=ierr)
168 if (ierr /= 0) then
169 call printf(render(diagnostic_report(level_error, &
170 message='Error opening input file: ' // trim(ofile), &
171 source=name), &
172 ''))
173 close(iunit)
174 return
175 end if
176
177 call preprocess(iunit, ounit)
178 if (iunit /= stdin) close(iunit)
179 if (ounit /= stdout) close(ounit)
180 end subroutine
181
182 !> Preprocess a file and write to an already-open output unit
183 !! @param[in] ifile Input filename
184 !! @param[in] ounit Output unit (already open for writing)
185 !!
186 !! @b Remarks
187 !! @ingroup group_parser
188 subroutine preprocess_file_to_unit(ifile, ounit)
189 character(*), intent(in) :: ifile
190 integer, intent(in) :: ounit
191 !private
192 integer :: iunit, ierr, n
193 character(len=1, kind=c_char) :: buf(256)
194
195 open(newunit=iunit, file=ifile, status='old', action='read', iostat=ierr)
196 if (ierr /= 0) then
197 call printf(render(diagnostic_report(level_error, &
198 message='Error opening input file: ' // trim(ifile), &
199 source=name), &
200 ''))
201 return
202 else
203 if (c_associated(getcwd_c(buf, size(buf, kind=c_size_t)))) then
204 n = findloc(buf, achar(0), 1)
205 name = ifile(n + 1:)
206 end if
207 end if
208
209 call preprocess(iunit, ounit)
210 if (iunit /= stdin) close(iunit)
211 if (ounit /= stdout) close(ounit)
212 end subroutine
213
214 !> Core preprocessing routine: read from iunit, write to ounit
215 !! Sets up a clean macro environment for the top-level file,
216 !! resets conditional compilation state, and calls the worker routine.
217 !! @param[in] iunit Input unit
218 !! @param[in] ounit Output unit
219 !!
220 !! @b Remarks
221 !! @ingroup group_parser
222 subroutine preprocess_unit_to_unit(iunit, ounit)
223 integer, intent(in) :: iunit
224 integer, intent(in) :: ounit
225 !private
226 type(macro), allocatable :: macros(:)
227
228 if (.not. allocated(global%macros)) allocate(global%macros(0))
229 allocate(macros(sizeof(global%macros)), source=global%macros)
230 if (.not. allocated(global%undef)) allocate(global%undef(0))
231 if (.not. allocated(global%includedir)) allocate(global%includedir(0))
232
233 cond_depth = 0
234 cond_stack(1)%active = .true.
235 cond_stack(1)%has_met = .false.
236
237 reprocess = .false.; c_continue = .false.; f_continue = .false.
238 icontinuation = 1; iline = 0
239 continued_line = ''; res = ''
240
241 call preprocess_unit(iunit, ounit, macros, .false.)
242 deallocate(macros)
243 end subroutine
244
245 !> Worker routine that reads lines, handles continuations, comments and directives
246 !! This is the main loop that:
247 !! - reads lines with interactive prompt when iunit==stdin
248 !! - handles both `\` and `&` continuations
249 !! - strips or preserves comments appropriately
250 !! - calls process_line() for directive processing and macro expansion
251 !! - stitches lines when Fortran continuation (`&`) is active
252 !! @param[in] iunit Input unit
253 !! @param[in] ounit Output unit
254 !! @param[inout] macros(:) Current macro table (passed by value between include levels)
255 !! @param[in] from_include True if called recursively from #include
256 !!
257 !! @b Remarks
258 !! @ingroup group_parser
259 subroutine preprocess_unit(iunit, ounit, macros, from_include)
260 integer, intent(in) :: iunit
261 integer, intent(in) :: ounit
262 type(macro), allocatable, intent(inout) :: macros(:)
263 logical, intent(in) :: from_include
264 !private
265 integer :: ierr, n
266
267 do
268 if (global%interactive) write(*, '(/a)', advance='no') ' [in] ' ! Command line prompt
269 read(iunit, '(A)', iostat=ierr) line
270
271 if (global%interactive) then
272 if (line == '') exit
273 if (lowercase(trim(adjustl(line))) == 'quit') exit
274 end if
275 if (ierr /= 0) then
276 if (ierr == iostat_end .and. from_include) f_continue = tail(tmp) == '&'
277 exit
278 end if
279 if (.not. from_include) iline = iline + 1
280
281 if (c_continue) then
282 continued_line = continued_line(:icontinuation) // trim(adjustl(line))
283 else
284 continued_line = trim(adjustl(line))
285 end if
286 n = len_trim(continued_line); if (n == 0) cycle
287
288 ! Check for line continuation with '\'
289 if (verify(continued_line(n:n), '\') == 0) then
290 ! Check for line break with '\\'
291 if (continued_line(len_trim(continued_line) - 1:len_trim(continued_line)) == '\\' .and. global%line_break) then
292 c_continue = .true.
293 continued_line = continued_line(:len_trim(continued_line) - 2) // new_line('A') ! Strip '\\'
294 icontinuation = len_trim(continued_line)
295 else
296 c_continue = .true.
297 icontinuation = len_trim(continued_line) - 1
298 continued_line = continued_line(:icontinuation)
299 end if
300 cycle
301 else
302 c_continue = .false.
303
304 tmp = process_line(continued_line, ounit, name, iline, macros, stitch)
305 if (len_trim(tmp) == 0) cycle
306
307 in_comment = head(tmp) == '!'
308
309 if (merge(head(res) == '!', in_comment, len_trim(res) > 0)) then
310 f_continue = tail(tmp) == '&'
311 else
312 if (in_comment .and. f_continue) cycle
313 f_continue = .not. in_comment .and. tail(tmp) == '&'
314 end if
315
316 if (f_continue .or. stitch) then
317 reprocess = .true.
318 res = concat(res, tmp)
319 else
320 if (reprocess) then
321 if (.not. in_comment .and. head(res) == '!') then
322 write(ounit, '(A)') res
323 res = process_line(tmp, ounit, name, iline, macros, stitch)
324 else
325 res = process_line(concat(res, tmp), ounit, name, iline, macros, stitch)
326 end if
327 reprocess = .false.
328 else
329 res = trim(tmp)
330 end if
331 if (global%interactive) write(*, '(/a)', advance='no') ' [out] ' ! Command line prompt
332 write(ounit, '(A)') res
333 res = ''
334 end if
335 end if
336 end do
337
338 if (cond_depth > 0) then
339 call printf(render(diagnostic_report(level_error, &
340 message='Unclosed conditional block at end of file', &
341 label=label_type('Missing conditional statement #endif', 1, 1), &
342 source=name), &
343 trim(line), iline))
344 else if (c_continue) then
345 call printf(render(diagnostic_report(level_error, &
346 message='Unexpected character', &
347 label=label_type('Trailing new line "\"', len(trim(line)), 1), &
348 source=name), &
349 trim(line), iline))
350 end if
351 end subroutine
352
353 !> Process a single (possibly continued) line – handles directives and macro expansion
354 !! Responsibilities:
355 !! - Strip or terminate C-style block comments (`/* ... */`)
356 !! - Detect and delegate preprocessor directives (`#define`, `#include`, conditionals, etc.)
357 !! - Perform macro expansion when the line is in an active conditional block
358 !! - Return whether the next line should be stitched (for Fortran `&` continuation inside macros)
359 !! @param[in] current_line Input line (already continued and trimmed)
360 !! @param[in] ounit Output unit (used only for diagnostics inside called routines)
361 !! @param[in] filepath Current file name (for error messages)
362 !! @param[in] linenum Current line number (for error messages)
363 !! @param[inout] macros(:) Macro table
364 !! @param[out] stch Set to .true. if the expanded line ends with `&` (stitch next line)
365 !! @return Processed line (directives removed, macros expanded)
366 !!
367 !! @b Remarks
368 !! @ingroup group_parser
369 recursive function process_line(current_line, ounit, filepath, linenum, macros, stch) result(rst)
370 character(*), intent(in) :: current_line
371 integer, intent(in) :: ounit
372 character(*), intent(inout) :: filepath
373 integer, intent(inout) :: linenum
374 type(macro), allocatable, intent(inout) :: macros(:)
375 logical, intent(out) :: stch
376 character(:), allocatable :: rst
377 !private
378 character(:), allocatable :: trimmed_line
379 logical :: active
380 logical, save :: l_in_comment = .false.
381 integer :: idx, comment_start, comment_end, n
382 type(context) :: ctx
383
384 trimmed_line = trim(adjustl(current_line))
385 rst = ''
386 comment_end = index(trimmed_line, '*/')
387 if (l_in_comment .and. comment_end > 0) then
388 trimmed_line = trimmed_line(comment_end + 2:)
389 l_in_comment = .false.
390 end if
391
392 if (l_in_comment) return
393 comment_start = index(trimmed_line, '/*')
394 if (comment_start > 0) then
395 trimmed_line = trimmed_line(:comment_start - 1)
396 l_in_comment = comment_end == 0
397 end if
398 n = len(trimmed_line); if (n == 0) return
399
400 active = is_active()
401 ctx = context(trimmed_line, linenum, filepath)
402 if (head(trimmed_line) == '#') then
403 if (len(trimmed_line) == 1) then
404 return !null directive
405 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'define') .and. active) then
406 call handle_define(ctx, macros, 'define')
407 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'undef') .and. active) then
408 call handle_undef(ctx, macros, 'undef')
409 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'warning') .and. active) then
410 call handle_warning(ctx, macros, 'warning')
411 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'error') .and. active) then
412 call handle_error(ctx, macros, 'error')
413 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'include') .and. active) then
414 call handle_include(ctx, ounit, preprocess_unit, macros, 'include')
415 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'line')) then
416 call handle_line(ctx, 'line')
417 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifdef')) then
418 call handle_ifdef(ctx, macros, 'ifdef')
419 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'ifndef')) then
420 call handle_ifndef(ctx, macros, 'ifndef')
421 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifdef')) then
422 call handle_elifdef(ctx, macros, 'elifdef')
423 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elifndef')) then
424 call handle_elifndef(ctx, macros, 'elifndef')
425 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'if')) then
426 call handle_if(ctx, macros, 'if')
427 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'elif')) then
428 call handle_elif(ctx, macros, 'elif')
429 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'else')) then
430 call handle_else(ctx)
431 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'endif')) then
432 call handle_endif(ctx)
433 else if (starts_with(lowercase(adjustl(trimmed_line(2:))), 'pragma') .and. active) then
434 rst = ctx%content
435 else
436 return
437 end if
438 else if (active) then
439 if (.not. global%expand_macros) then
440 rst = trimmed_line
441 else
442 rst = adjustl(expand_all(ctx, macros, stch, global%extra_macros, global%&
443 implicit_continuation))
444 end if
445 end if
446 end function
447end 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
subroutine, public handle_line(ctx, token)
Handle the standard line directive Supports two standard forms: line <number> line <number> "<filenam...
Definition line.f90:70
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:189
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:223
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:260
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:158
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:370
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:81
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