Loading...
Searching...
No Matches
macro.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_macro Macro
3!! Macro management and expansion core of the fpx Fortran preprocessor
4!!
5!! This module implements a complete, standards-inspired macro system supporting:
6!! - Object-like and function-like macros
7!! - Variadic macros (`...` and `__VA_ARGS__`)
8!! - C++20/C23-style `__VA_OPT__` handling for optional variadic content
9!! - Parameter stringification (`#param`) and token pasting (`##`)
10!! - Built-in predefined macros: `__FILE__`, `__FILENAME__`, `__LINE__`, `__DATE__`, `__TIME__`, `__TIMESTAMP__`, `__FUNC__`
11!! - Recursive expansion with circular dependency detection via digraph analysis
12!! - Dynamic macro table of `macro` objects with efficient addition, lookup, removal
13!! - Full support for nested macro calls and proper argument handling
14!!
15!! The design allows safe, repeated expansion while preventing infinite recursion.
16!! All operations are container-agnostic using allocatable dynamic arrays.
17!!
18!! @par Expansion Model
19!! Macros are expanded recursively.
20!! Circular dependencies are detected through dependency graph analysis.
21!! Macro lookup is currently linear in the number of defined macros.
22!!
23!! @par Expansion Pipeline
24!! Macro processing occurs in two stages:
25!! - @link fpx_macro::expand_macros expand_macros @endlink performs recursive expansion of user-defined macros,
26!! including function-like macros, variadic substitutions, token
27!! pasting, stringification, and cycle detection.
28!! - @link fpx_macro::expand_all expand_all @endlink subsequently substitutes predefined macros such as
29!! `__FILE__`, `__LINE__`, `__DATE__`, and related extensions.
30!!
31!! This separation allows internal preprocessing routines to reuse the
32!! core expansion engine while selectively enabling predefined tokens.
33!!
34!! @section macro_examples Examples
35!!
36!! 1. Define and use simple macros:
37!! @code{.f90}
38!! type(macro), allocatable :: macros(:)
39!! call add(macros, macro('PI', '3.1415926535'))
40!! call add(macros, macro('MSG(x)', 'print *, ″Hello ″, x'))
41!! print *, expand_all(context('area = PI * r**2', 10, './circle.F90', 'circle'), macros, stitch, .false., .false., .true., .false.)
42!! !> prints: area = 3.1415926535 * r**2
43!! @endcode
44!!
45!! 2. Variadic macro with stringification and pasting:
46!! @code{.f90}
47!! call add(macros, macro('DEBUG_PRINT(...)', 'print *, ″DEBUG[″, __FILE__, ″:″, __LINE__, ″]: ″, __VA_ARGS__'))
48!! print *, expand_all(context('DEBUG_PRINT(″value =″, x)', 42, 'test.F90', 'text'), macros, stitch, .false., .false., .true., .false.)
49!! !> prints: print *, 'DEBUG[', 'test.F90', ':', 42, ']: ', 'value =', x
50!! @endcode
51!!
52!! 3. Token pasting with ##:
53!! @code{.f90}
54!! call add(macros, macro('MAKE_VAR(name,num)', 'var_name_##num'))
55!! print *, expand_all(context('real :: MAKE_VAR(temp,42)', 5, 'file.F90', 'file'), macros, stitch, .false., .false., .false.)
56!! !> prints: real :: var_name_42
57!! @endcode
58module fpx_macro
59 use fpx_constants
60 use fpx_logging
61 use fpx_path
62 use fpx_graph
63 use fpx_string
64 use fpx_date
65 use fpx_logging
66 use fpx_context
67 use fpx_compiler
68
69 implicit none; private
70
71 public :: macro, &
72 add, &
73 get, &
74 insert, &
75 clear, &
76 remove, &
78
79 public :: expand_macros, &
80 expand_all, &
81 is_defined, &
82 read_unit, &
84
85 !> Representation of a preprocessor macro.
86 !!
87 !! A macro stores its identifier together with the metadata required
88 !! during expansion:
89 !! - replacement text,
90 !! - formal parameter list,
91 !! - variadic status,
92 !! - cycle detection flags,
93 !! - temporary activation state.
94 !!
95 !! The type extends @link fpx_string::string string @endlink so that the
96 !! macro name itself behaves as a string value.
97 !!
98 !! @section macro_type_examples Examples
99 !!
100 !! Object-like macro:
101 !! @code{.f90}
102 !! type(macro) :: m
103 !! m = macro('PI', '3.1415926535')
104 !! ...
105 !! @endcode
106 !!
107 !! Function-like macro:
108 !! @code{.f90}
109 !! type(macro) :: m
110 !! m = macro('SQR(x)', '((x)*(x))')
111 !! ...
112 !! @endcode
113 !!
114 !! @section macro_type_constructor Constructors
115 !! Initializes a new instance of the @ref macro class
116 !!
117 !! @b Constructor
118 !! @code{.f90}
119 !! type(macro) function macro(character(*) name, (optional) character(*) val)
120 !! @endcode
121 !!
122 !! @param[in] name
123 !! macro name
124 !! @param[in] val
125 !! (optional) value of the macro
126 !!
127 !! @b Examples
128 !! @code{.f90}
129 !! type(macro) :: m
130 !! m = macro('_WIN32')
131 !! ...
132 !! @endcode
133 !! @return The constructed macro object.
134 !!
135 !! @ingroup group_macro
136 type, extends(string) :: macro
137 character(:), allocatable :: value !< Value of the macro
138 type(string), allocatable :: params(:) !< List of parameter for function like macros
139 logical :: is_variadic !< Indicate whether the macro is variadic or not.
140 logical :: is_cyclic !< Indicates whether the macro has cyclic dependencies or not.
141 logical :: active = .true.
142 end type
143
144 !> Construct a new macro definition.
145 !!
146 !! Creates an initialized @ref macro object with the specified name
147 !! and optional replacement text.
148 !!
149 !! Parameter lists are initialized to empty, variadic expansion is
150 !! disabled, and direct self-references are marked as cyclic.
151 !!
152 !! @param[in] name Macro identifier.
153 !! @param[in] val Replacement text (default: empty).
154 !!
155 !! @return Initialized macro object.
156 !!
157 !! @ingroup group_macro
158 interface macro
159 !! @cond
160 module procedure :: macro_new
161 !! @endcond
162 end interface
163
164 !> Append macros to a macro table.
165 !!
166 !! Existing definitions with the same name are replaced, while new
167 !! definitions are appended to the dynamic array.
168 !!
169 !! Overloads support:
170 !! - insertion of a single @ref macro object,
171 !! - insertion by name only,
172 !! - insertion by name and replacement text,
173 !! - insertion of a range of macros.
174 !!
175 !! @ingroup group_macro
176 interface add
177 module procedure :: add_item
178 module procedure :: add_item_from_name
179 module procedure :: add_item_from_name_and_value
180 module procedure :: add_range
181 end interface
182
183 !> Remove all macro definitions from a table.
184 !!
185 !! The table remains allocated as an empty array.
186 !!
187 !! @ingroup group_macro
188 interface clear
189 module procedure :: clear_item
190 end interface
191
192 !> Retrieve a macro by index
193 !!
194 !! @ingroup group_macro
195 interface get
196 module procedure :: get_item
197 end interface
198
199 !> Insert a macro at a specified position.
200 !!
201 !! Existing elements are shifted to preserve ordering.
202 !!
203 !! @ingroup group_macro
204 interface insert
205 module procedure :: insert_item
206 end interface
207
208 !> Remove a macro definition from a table.
209 !!
210 !! The array is compacted after removal and cyclic dependency
211 !! markers are recomputed.
212 !!
213 !! @ingroup group_macro
214 interface remove
215 module procedure :: remove_item
216 end interface
217
218 !> Return the number of stored macro definitions.
219 !!
220 !! Convenience wrapper around the intrinsic `size` function that
221 !! safely handles non allocated arrays.
222 !!
223 !! @ingroup group_macro
224 interface size_of
225 module procedure :: size_item
226 end interface
227
228 !> Abstract interface to the top-level preprocessing routine.
229 !!
230 !! This callback allows modules such as the include handler to invoke
231 !! recursive preprocessing of additional source units without creating
232 !! circular module dependencies.
233 !!
234 !! Implementations are expected to preprocess the contents of the
235 !! input unit and emit the resulting output to the specified unit.
236 !!
237 !! @ingroup group_include
238 interface
239 subroutine read_unit(iunit, ounit, macros, from_include)
240 import macro; implicit none
241 integer, intent(in) :: iunit
242 integer, intent(in) :: ounit
243 type(macro), allocatable, intent(inout) :: macros(:)
244 logical, intent(in) :: from_include
245 end subroutine
246 end interface
247
248 !> Abstract interface for line preprocessing callbacks.
249 !!
250 !! Implementations process a single source line after directive
251 !! handling and macro substitution.
252 !!
253 !! The callback mechanism is primarily used by nested constructs such
254 !! as `#for` expansion, allowing generated lines to re-enter the main
255 !! preprocessing pipeline.
256 !!
257 !! @ingroup group_macro
258 interface
259 recursive function preprocess_line(current_line, ounit, filepath, linenum, macros, stch) result(rst)
260 import macro; implicit none
261 character(*), intent(in) :: current_line
262 integer, intent(in) :: ounit
263 character(*), intent(inout) :: filepath
264 integer, intent(inout) :: linenum
265 type(macro), allocatable, intent(inout) :: macros(:)
266 logical, intent(out) :: stch
267 character(:), allocatable :: rst
268 end function
269 end interface
270contains
271
272 !> Construct a new macro object
273 !! @param[in] name Mandatory macro name
274 !! @param[in] val Optional replacement text (default: empty)
275 !! @return Initialized macro object
276 type(macro) function macro_new(name, val) result(that)
277 character(*), intent(in) :: name
278 character(*), intent(in), optional :: val
279
280 that = trim(name)
281 if (present(val)) then
282 that%value = val
283 else
284 that%value = ''
285 end if
286 allocate(that%params(0))
287 that%is_variadic = .false.
288 that%is_cyclic = that == that%value
289 that%active = .true.
290 end function
291
292 !> Expand a source line including predefined macros.
293 !!
294 !! This routine represents the complete user-visible expansion phase.
295 !!
296 !! Expansion proceeds in two steps:
297 !! 1. User-defined macros are expanded recursively through
298 !! @ref expand_macros.
299 !! 2. Built-in predefined macros are substituted using the current
300 !! preprocessing context.
301 !!
302 !! Supported predefined macros include:
303 !! - `__FILE__`
304 !! - `__LINE__`
305 !! - `__DATE__`
306 !! - `__TIME__`
307 !! - `__FUNC__`
308 !! - `__FILENAME__` (extension)
309 !! - `__TIMESTAMP__` (extension)
310 !!
311 !! @param[in] ctx
312 !! Context
313 !! @param[inout] macros
314 !! Current macro table
315 !! @param[out] stitch
316 !! Set to .true.true. if result ends with '&' (Fortran continuation)
317 !! @param[in] has_extra
318 !! Has extra macros (non-standard) like __FILENAME__ and __TIMESTAMP__
319 !! @param[in] implicit_conti
320 !! If .true., implicit continuation is permitted
321 !! @param[in] dollar_insert
322 !! If .true., the syntax ${} is supported for macro insertion
323 !! @return Expanded line with all macros and predefined tokens replaced
324 !!
325 !! @ingroup group_macro
326 function expand_all(ctx, macros, stitch, has_extra, implicit_conti, dollar_insert, is_standalone) result(expanded)
327 type(context), intent(in) :: ctx
328 type(macro), allocatable, intent(inout) :: macros(:)
329 logical, intent(out) :: stitch
330 logical, intent(in) :: has_extra
331 logical, intent(in) :: implicit_conti
332 logical, intent(in) :: dollar_insert
333 logical, intent(in) :: is_standalone
334 character(:), allocatable :: expanded
335 !private
336 integer :: pos, start, sep, dot, imacro
337 type(datetime) :: date
338
339 if (has_extra) then
340 if (.not. is_defined('__FUNC__', macros, imacro)) then
341 call add(macros, '__FUNC__', '')
342 end if
343
344 if (.not. is_defined('__COMPILER__', macros, imacro)) then
345 call add(macros, '__COMPILER__', get_compiler(.false.))
346 end if
347 end if
348
349 expanded = expand_macros(ctx%content, macros, stitch, implicit_conti, dollar_insert, ctx)
350
351 date = now()
352
353 ! Substitute __FILE__ (relative path to working directory)
354 pos = 1
355 do while (pos > 0)
356 pos = index(expanded, '__FILE__')
357 if (pos > 0) then
358 start = pos + len('__FILE__')
359 expanded = trim(expanded(:pos - 1) // '"' // trim(ctx%path) // '"' // trim(expanded(start:)))
360 end if
361 end do
362
363 ! Substitute __LINE__
364 pos = 1
365 do while (pos > 0)
366 pos = index(expanded, '__LINE__')
367 if (pos > 0) then
368 if (pos > 0) then
369 start = pos + len('__LINE__')
370 expanded = trim(expanded(:pos - 1) // tostring(ctx%line) // trim(expanded(start:)))
371 end if
372 end if
373 end do
374
375 ! Substitute __DATE__
376 pos = 1
377 do while (pos > 0)
378 pos = index(expanded, '__DATE__')
379 if (pos > 0) then
380 if (pos > 0) then
381 start = pos + len('__DATE__')
382 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('MMM-dd-yyyy') // '"' // trim(expanded(start:)))
383 end if
384 end if
385 end do
386
387 ! Substitute __TIME__
388 pos = 1
389 do while (pos > 0)
390 pos = index(expanded, '__TIME__')
391 if (pos > 0) then
392 if (pos > 0) then
393 start = pos + len('__TIME__')
394 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('HH:mm:ss') // '"' // trim(expanded(start:)))
395 end if
396 end if
397 end do
398
399 if (has_extra) then
400 ! Substitute __FILENAME__
401 pos = 1; do while (pos > 0)
402 pos = index(expanded, '__FILENAME__')
403 if (pos > 0) then
404 start = pos + len('__FILENAME__')
405 expanded = trim(expanded(:pos - 1) // '"' // filename(ctx%path, .true.) // '"' // trim(expanded(start:)))
406 end if
407 end do
408
409 ! Substitute __TIMESTAMP__
410 pos = 1; do while (pos > 0)
411 pos = index(expanded, '__TIMESTAMP__')
412 if (pos > 0) then
413 if (pos > 0) then
414 start = pos + len('__TIMESTAMP__')
415 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('ddd MM yyyy') // ' ' // date%to_string(&
416 'HH:mm:ss'&
417 &) // '"' // trim(expanded(start:)))
418 end if
419 end if
420 end do
421 end if
422 end function
423
424 !> Recursively expand user-defined macros.
425 !!
426 !! Implements the core expansion engine used throughout fpx.
427 !!
428 !! Supported features include:
429 !! - object-like macros,
430 !! - function-like macros,
431 !! - variadic macros,
432 !! - `__VA_ARGS__`,
433 !! - `__VA_OPT__`,
434 !! - parameter stringification,
435 !! - token pasting,
436 !! - nested expansion,
437 !! - optional `${...}` substitutions,
438 !! - circular dependency detection.
439 !!
440 !! Recursive expansion terminates automatically when cyclic
441 !! dependencies are detected.
442 !!
443 !! @param[in] line
444 !! Line to be expanded
445 !! @param[inout] macros
446 !! Current macro table
447 !! @param[out] stitch
448 !! .true. if final line ends with '&'
449 !! @param[in] implicit_conti
450 !! If .true., implicit continuation is permitted
451 !! @param[in] dollar_insert
452 !! If .true., ${} macro substitution is supported
453 !! @param[in] ctx
454 !! Context
455 !! @return Line with user-defined macros expanded (predefined tokens untouched)
456 !!
457 !! @ingroup group_macro
458 function expand_macros(line, macros, stitch, implicit_conti, dollar_insert, ctx) result(expanded)
459 character(*), intent(in) :: line
460 type(macro), allocatable, intent(inout) :: macros(:)
461 logical, intent(out) :: stitch
462 logical, intent(in) :: implicit_conti
463 logical, intent(in) :: dollar_insert
464 type(context), intent(in) :: ctx
465 character(:), allocatable :: expanded
466 !private
467 integer :: imacro, paren_level
468 type(digraph) :: graph
469
470 imacro = 0; paren_level = 0
471 graph = digraph(size(macros))
472 stitch = .false.
473
474 expanded = expand_macros_internal(line, imacro, macros)
475
476 if (implicit_conti) then
477 stitch = (tail(expanded) == '&') .or. paren_level > 0
478 else
479 stitch = (tail(expanded) == '&') .and. paren_level > 0
480 end if
481 contains
482 !> @private
483 recursive function expand_macros_internal(line, imacro, macros) result(expanded)
484 character(*), intent(in) :: line
485 integer, intent(in) :: imacro
486 type(macro), allocatable, intent(inout) :: macros(:)
487 character(:), allocatable :: expanded
488 !private
489 character(:), allocatable :: args_str, temp, va_args
490 character(:), allocatable :: token1, token2, prefix, suffix
491 type(string) :: arg_values(max_params)
492 integer :: c, i, j, k, n, pos, start, arg_start, nargs
493 integer :: m_start, m_end, token1_start, token2_stop
494 logical :: isopened, found
495 character :: quote
496 integer, allocatable :: indexes(:)
497 logical :: exists, ok, hasfunc
498
499 expanded = line
500 if (size(macros) == 0) return
501 isopened = .false.; hasfunc = .false.
502
503 do i = 1, size(macros)
504 n = len_trim(macros(i)); if (n == 0) cycle
505 c = 0
506 do while (c < len_trim(expanded))
507 c = c + 1
508 if (expanded(c:c) == '"' .or. expanded(c:c) == "'") then
509 if (.not. isopened) then
510 isopened = .true.
511 quote = expanded(c:c)
512 else
513 if (expanded(c:c) == quote) isopened = .false.
514 end if
515 end if
516 if (isopened) cycle
517 if (c + n - 1 > len_trim(expanded)) exit
518
519 if (.not. hasfunc) then
520 call update_func_macro(expanded, macros)
521 hasfunc = .true.
522 end if
523
524 ! Placeholder expansion: ${NAME}
525 if (dollar_insert) then
526 if (expanded(c:c) == '$') then
527 if (c < len_trim(expanded)) then
528 if (expanded(c + 1:c + 1) == '{') then
529 j = c + 2
530 do while (j <= len_trim(expanded))
531 if (expanded(j:j) == '}') exit
532 j = j + 1
533 end do
534
535 if (j <= len_trim(expanded)) then
536 token1 = trim(expanded(c + 2:j - 1))
537 if (is_defined(token1, macros, idx=k)) then
538 temp = macros(k)%value
539 if (len(temp) == 0 .and. .not. macros(k)%active) then
540 c = j
541 else
542 expanded = expanded(:c - 1) // temp // expanded(j + 1:)
543 if (len(temp) /= 0) then
544 c = c + len_trim(temp) - 1
545 end if
546 end if
547 cycle
548 end if
549 end if
550 end if
551 end if
552 end if
553 end if
554
555 found = .false.
556 if (expanded(c:c + n - 1) == macros(i)) then
557 found = .true.
558 if (len_trim(expanded(c:)) > n) then
559 found = verify(expanded(c + n:c + n), ' ()[]<>&;.,^~!/*-+\="' // "'") == 0
560 end if
561 if (found .and. c > 1) then
562 found = verify(expanded(c - 1:c - 1), ' ()[]<>&;.,^~!/*-+\="' // "'") == 0
563 end if
564 end if
565
566 if (found) then
567 pos = c
568 c = c + n - 1
569 m_start = pos
570 start = pos + n
571 ok = allocated(macros(i)%params); if (ok) ok = size(macros(i)%params) > 0
572 if (ok .or. macros(i)%is_variadic) then
573 if (start <= len(expanded)) then
574 if (expanded(start:start) == '(') then
575 paren_level = 1
576 arg_start = start + 1
577 nargs = 0
578 j = arg_start
579 do while (j <= len(expanded) .and. paren_level > 0)
580 if (expanded(j:j) == '(') paren_level = paren_level + 1
581 if (expanded(j:j) == ')') paren_level = paren_level - 1
582 if (paren_level == 1 .and. expanded(j:j) == ',' .or. paren_level == 0) then
583 if (nargs < max_params) then
584 nargs = nargs + 1
585 arg_values(nargs) = trim(adjustl(expanded(arg_start:j - 1)))
586 arg_start = j + 1
587 end if
588 end if
589 j = j + 1
590 end do
591 m_end = j - 1
592 args_str = expanded(start:m_end)
593 temp = trim(macros(i)%value)
594
595 if (macros(i)%is_variadic) then
596 if (nargs < size(macros(i)%params)) then
597 call printf(render(diagnostic_report(level_error, &
598 message='Variadic macro issue', &
599 label=label_type('Too few arguments for macro ' // macros(i), start, m_end - &
600 start), &
601 source=trim(ctx%path)), &
602 expanded, ctx%line))
603 cycle
604 end if
605 va_args = ''
606 do j = size(macros(i)%params) + 1, nargs
607 if (j > size(macros(i)%params) + 1) va_args = va_args // ', '
608 va_args = va_args // arg_values(j)
609 end do
610 else if (nargs /= size(macros(i)%params)) then
611 call printf(render(diagnostic_report(level_error, &
612 message='Function-like macro issue', &
613 label=label_type('Incorrect number of arguments for macro ' // macros(i), start, &
614 m_end - start), &
615 source=trim(ctx%path)), &
616 expanded, ctx%line))
617 cycle
618 end if
619
620 ! Substitute regular parameters
621 argbck :block
622 integer :: c1, h1
623 logical :: opened
624
625 opened = .false.
626 jloop: do j = 1, size(macros(i)%params)
627 c1 = 0
628 wloop: do while (c1 < len_trim(temp))
629 c1 = c1 + 1
630 if (temp(c1:c1) == '"') opened = .not. opened
631 if (opened) cycle wloop
632 if (c1 + len_trim(macros(i)%params(j)) - 1 > len(temp)) cycle wloop
633
634 if (temp(c1:c1 + len_trim(macros(i)%params(j)) - 1) == trim(macros(i)%params(j))) &
635 then
636 checkbck:block
637 integer :: cend, l
638
639 cend = c1 + len_trim(macros(i)%params(j))
640 l = len(temp)
641 if (c1 == 1 .and. cend == l + 1) then
642 exit checkbck
643 else if (c1 > 1 .and. l == cend - 1) then
644 if (verify(temp(c1 - 1:c1 - 1), ' #()[]<>&;.,!/*-+\="' // "'") /= 0) &
645 cycle wloop
646 else if (c1 <= 1 .and. cend <= l) then
647 if (verify(temp(cend:cend), ' #()[]<>&;.,!/*-+\="' // "'") /= 0) cycle &
648 wloop
649 else
650 if (verify(temp(c1 - 1:c1 - 1), ' #()[]<>&;.,!/*-+\="' // "'") /= 0 &
651 .or. verify(temp(cend:cend), ' #()[]<>$&;.,!/*-+\="' // "'") /=&
652 & 0) cycle wloop
653 end if
654 end block checkbck
655 pos = c1
656 c1 = c1 + len_trim(macros(i)%params(j)) - 1
657 start = pos + len_trim(macros(i)%params(j))
658 if (pos == 2) then
659 if (temp(pos - 1:pos - 1) == '#') then
660 temp = trim(temp(:pos - 2) // '"' // arg_values(j) // '"' // trim(temp(&
661 start:)))
662 else
663 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
664 end if
665 elseif (pos > 2) then
666 h1 = pos - 1
667 if (previous(temp, h1) == '#') then
668 if (h1 == 1) then
669 temp = trim(temp(:h1 - 1) // '"' // arg_values(j) // '"' // trim(&
670 temp(start:)))
671 else
672 if (temp(h1 - 1:h1 - 1) /= '#') then
673 temp = trim(temp(:h1 - 1) // '"' // arg_values(j) // '"' // &
674 trim(temp(start:)))
675 else
676 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:&
677 )))
678 end if
679 end if
680 else
681 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
682 end if
683 else
684 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
685 end if
686 end if
687 end do wloop
688 end do jloop
689 end block argbck
690
691 ! Handle concatenation (##) first with immediate substitution
692 block
693 pos = 1
694 do while (pos > 0)
695 pos = index(temp, '##')
696 if (pos > 0) then
697 ! Find token1 (before ##)
698 k = pos - 1
699 if (k <= 0) then
700 call printf(render(diagnostic_report(level_error, &
701 message='Syntax error', &
702 label=label_type('No token before ##', pos, 2), &
703 source=trim(ctx%path)), &
704 temp, ctx%line))
705 cycle
706 end if
707
708 token1 = adjustr(temp(:k))
709 prefix = ''
710 token1_start = index(token1, ' ')
711 if (token1_start > 0) then
712 prefix = token1(:token1_start)
713 token1 = token1(token1_start + 1:)
714 end if
715
716 ! Find token2 (after ##)
717 k = pos + 2
718 if (k > len(temp)) then
719 call printf(render(diagnostic_report(level_error, &
720 message='Syntax error', &
721 label=label_type('No token after ##', pos, 2), &
722 source=trim(ctx%path)), &
723 temp, ctx%line))
724 cycle
725 end if
726
727 suffix = ''
728 token2 = adjustl(temp(k:))
729 token2_stop = index(token2, ' ')
730 if (token2_stop > 0) then
731 suffix = token2(token2_stop:)
732 token2 = token2(:token2_stop - 1)
733 end if
734
735 ! Concatenate, replacing the full 'token1 ## token2' pattern
736 if (is_defined(token1, macros, idx=k)) &
737 token1 = expand_macros_internal(token1, imacro, macros)
738 if (is_defined(token2, macros, idx=k)) &
739 token2 = expand_macros_internal(token2, imacro, macros)
740
741 temp = trim(prefix // trim(token1) // trim(token2) // suffix)
742 end if
743 end do
744 end block
745
746 ! Substitute __VA_ARGS__
747 block
748 if (macros(i)%is_variadic) then
749 pos = 1
750 do while (pos > 0)
751 pos = index(temp, '__VA_ARGS__')
752 if (pos > 0) then
753 start = pos + len('__VA_ARGS__') - 1
754 if (start < len(temp) .and. temp(start:start) == '_' &
755 .and. temp(start + 1:start + 1) == ')') then
756 temp = trim(temp(:pos - 1) // trim(va_args) // ')')
757 else
758 temp = trim(temp(:pos - 1) // trim(va_args) // trim(temp(start + 1:)))
759 end if
760
761 ! Substitute __VA_OPT__
762 pos = index(temp, '__VA_OPT__')
763 if (pos > 0) then
764 start = pos + index(temp(pos:), ')') - 1
765 if (len_trim(va_args) > 0) then
766 temp = trim(temp(:pos - 1)) // temp(pos + index(temp(pos:), '('):start &
767 - 1) // trim(temp(start + 1:))
768 else
769 temp = trim(temp(:pos - 1)) // trim(temp(start + 1:))
770 end if
771 end if
772 end if
773 end do
774 end if
775 end block
776
777 call graph%add_edge(imacro, i)
778 if (.not. graph%is_circular(i)) then
779 temp = expand_macros_internal(temp, i, macros) ! Only for nested macros
780 else
781 call printf(render(diagnostic_report(level_error, &
782 message='Failed macro expansion', &
783 label=label_type('Circular macro detected', index(temp, macros(i)), len(macros(i)))&
784 , &
785 source=trim(ctx%path)), &
786 temp, ctx%line))
787 cycle
788 end if
789 expanded = trim(expanded(:m_start - 1) // trim(temp) // expanded(m_end + 1:))
790 end if
791 end if
792 else
793 temp = trim(macros(i)%value)
794 m_end = start - 1
795 call graph%add_edge(imacro, i)
796 if ((.not. graph%is_circular(i)) .and. (.not. macros(i)%is_cyclic)) then
797 expanded = trim(expanded(:m_start - 1) // trim(temp) // expanded(m_end + 1:))
798 expanded = expand_macros_internal(expanded, imacro, macros)
799 else
800 call printf(render(diagnostic_report(level_error, &
801 message='Failed macro expansion', &
802 label=label_type('Circular macro detected', index(temp, macros(i)), len(macros(i))), &
803 source=trim(ctx%path)), &
804 temp, ctx%line))
805 cycle
806 end if
807 end if
808 end if
809 end do
810 end do
811 pos = index(expanded, '&')
812 if (index(expanded, '!') > pos .and. pos > 0) expanded = expanded(:pos + 1)
813 end function
814 end function
815
816 !> Determine whether a macro is currently defined.
817 !!
818 !! Performs a linear search through the macro table and optionally
819 !! returns the corresponding index.
820 !!
821 !! @param[in] name
822 !! Macro identifier.
823 !! @param[in] macros
824 !! Macro table.
825 !! @param[out] idx
826 !! Position of the matching entry, if present.
827 !! @return `.true.` if the macro exists.
828 !!
829 !! @ingroup group_macro
830 logical function is_defined(name, macros, idx) result(res)
831 character(*), intent(in) :: name
832 type(macro), intent(in) :: macros(:)
833 integer, intent(inout), optional :: idx
834 !private
835 integer :: i
836
837 res = .false.
838 do i = 1, size(macros)
839 if (macros(i) == trim(name)) then
840 res = .true.
841 if (present(idx)) idx = i
842 exit
843 end if
844 end do
845 end function
846
847 !> Convert a scalar value to its textual representation.
848 !!
849 !! Supports intrinsic integer, real, logical, character, and complex
850 !! values of common kinds.
851 !!
852 !! Primarily intended for internal diagnostics and macro processing.
853 !! @private
854 !! @ingroup group_macro
855 function tostring(any)
856 class(*), intent(in) :: any
857 !private
858 character(:), allocatable :: tostring
859 character(4096) :: line
860
861 call print_any(any); tostring = trim(line)
862 contains
863 !> @private
864 subroutine print_any(any)
865 use, intrinsic :: iso_fortran_env, only: int8, &
866 int16, &
867 int32, &
868 int64, &
869 real32, &
870 real64, &
871 real128
872 class(*), intent(in) :: any
873
874 select type (any)
875 type is (integer(kind=int8)); write(line, '(i0)') any
876 type is (integer(kind=int16)); write(line, '(i0)') any
877 type is (integer(kind=int32)); write(line, '(i0)') any
878 type is (integer(kind=int64)); write(line, '(i0)') any
879 type is (real(kind=real32)); write(line, '(1pg0)') any
880 type is (real(kind=real64)); write(line, '(1pg0)') any
881 type is (real(kind=real128)); write(line, '(1pg0)') any
882 type is (logical); write(line, '(1l)') any
883 type is (character(*)); write(line, '(a)') any
884 type is (complex(kind=real32)); write(line, '("(",1pg0,",",1pg0,")")') any
885 type is (complex(kind=real64)); write(line, '("(",1pg0,",",1pg0,")")') any
886 type is (complex(kind=real128)); write(line, '("(",1pg0,",",1pg0,")")') any
887 end select
888 end subroutine
889 end function
890
891 !> Internal helper: grow dynamic macro array in chunks for efficiency
892 !! Adds a new macro to the allocatable array.
893 !! Also detects direct self-references (A -> A) and marks both sides as cyclic.
894 !!
895 subroutine add_to(array, val)
896 type(macro), allocatable, intent(inout) :: array(:)
897 type(macro), intent(in) :: val(..)
898 !private
899 type(macro), allocatable :: tmp(:)
900 logical, allocatable :: isdef(:)
901 integer :: i, j, n
902
903 n = size_of(array)
904
905 select rank (val)
906 rank(0)
907 allocate(isdef(1), source=.false.)
908 do i = 1, n
909 if (array(i) == val) then
910 array(i) = val
911 isdef(1) = .true.
912 end if
913 end do
914 if (.not. isdef(1)) then
915 allocate(tmp(n + 1))
916 if (n > 0) tmp(1:n) = array
917 tmp(n + 1) = val
918 call move_alloc(tmp, array)
919 if (allocated(tmp)) deallocate(tmp)
920 end if
921 rank(1)
922 allocate(isdef(size(val)), source=.false.)
923 do concurrent(i = 1:n, j = 1:size(val))
924 if (array(i) == val(j)) then
925 array(i) = val(j)
926 isdef(j) = .true.
927 end if
928 end do
929 n = size_of(array); allocate(tmp(n + count(isdef)))
930 if (n > 0) tmp(1:n) = array
931 tmp(n + 1:) = pack(val, isdef)
932 call move_alloc(tmp, array)
933 if (allocated(tmp)) deallocate(tmp)
934 end select
935
936 do i = 1, size_of(array)
937 do j = n + 1, size(array)
938 if (i == j) cycle
939 if (array(i) == array(j)%value .and. array(i)%value == array(j)) then
940 array(i)%is_cyclic = .true.
941 end if
942 end do
943 end do
944 end subroutine
945
946 !> Add a complete macro object to the table
947 subroutine add_item(this, m)
948 type(macro), intent(inout), allocatable :: this(:)
949 type(macro), intent(in) :: m
950
951 call add_to(this, m)
952 end subroutine
953
954 !> Add macro by name only (value = empty)
955 subroutine add_item_from_name(this, name)
956 type(macro), intent(inout), allocatable :: this(:)
957 character(*), intent(in) :: name
958
959 if (.not. allocated(this)) allocate(this(0))
960 call add_to(this, macro(name))
961 end subroutine
962
963 !> Add macro with name and replacement text
964 subroutine add_item_from_name_and_value(this, name, value)
965 type(macro), intent(inout), allocatable :: this(:)
966 character(*), intent(in) :: name
967 character(*), intent(in) :: value
968
969 if (.not. allocated(this)) allocate(this(0))
970 call add_to(this, macro(name, value))
971 end subroutine
972
973 !> Add multiple macros at once
974 subroutine add_range(this, m)
975 type(macro), intent(inout), allocatable :: this(:)
976 type(macro), intent(in) :: m(:)
977
978 if (.not. allocated(this)) allocate(this(0))
979 call add_to(this, m)
980 end subroutine
981
982 !> Remove all macros from table
983 subroutine clear_item(this)
984 type(macro), intent(inout), allocatable :: this(:)
985
986 if (allocated(this)) deallocate(this)
987 allocate(this(0))
988 end subroutine
989
990 !> Retrieve macro by 1-based index
991 function get_item(this, key) result(res)
992 type(macro), intent(inout) :: this(:)
993 integer, intent(in) :: key
994 type(macro), allocatable :: res
995 !private
996 integer :: n
997
998 n = size(this)
999 if (key > 0 .and. key <= n) then
1000 res = this(key)
1001 end if
1002 end function
1003
1004 !> Insert macro at specific position
1005 subroutine insert_item(this, i, m)
1006 type(macro), intent(inout), allocatable :: this(:)
1007 integer, intent(in) :: i
1008 type(macro), intent(in) :: m
1009 !private
1010 integer :: j, count
1011
1012 if (.not. allocated(this)) allocate(this(0))
1013 count = size(this)
1014 call add_to(this, m)
1015
1016 do j = count, i + 1, -1
1017 this(j) = this(j - 1)
1018 end do
1019 this(i) = m
1020 end subroutine
1021
1022 !> Return number of defined macros
1023 pure integer function size_item(x) result(res)
1024 class(*), dimension(..), intent(in), optional :: x
1025 res = 0
1026 if (present(x)) res = size(x)
1027 end function
1028
1029 !> Remove macro at given index
1030 subroutine remove_item(this, i)
1031 type(macro), intent(inout), allocatable :: this(:)
1032 integer, intent(in) :: i
1033 !private
1034 type(macro), allocatable :: tmp(:)
1035 integer :: k, j, n
1036
1037 if (.not. allocated(this)) allocate(this(0))
1038 n = size(this)
1039 if (allocated(this(i)%params)) deallocate(this(i)%params)
1040 if (n > 1) then
1041 this(i:n - 1) = this(i + 1:n)
1042 allocate(tmp(n - 1))
1043 tmp = this(:n - 1)
1044 deallocate(this)
1045 call move_alloc(tmp, this)
1046
1047 this(:)%is_cyclic = .false.
1048 do k = 1, size(this)
1049 do j = 1, size(this)
1050 if (this(k) == this(j)%value .and. this(k)%value == this(j)) then
1051 this(i)%is_cyclic = .true.
1052 this(j)%is_cyclic = .true.
1053 end if
1054 end do
1055 end do
1056 else
1057 deallocate(this); allocate(this(0))
1058 end if
1059 end subroutine
1060
1061 !> Update the special predefined macro __FUNC__
1062 !!
1063 !! Examines the current source line and detects whether it introduces
1064 !! a Fortran procedure definition (`function` or `subroutine`).
1065 !! When a procedure declaration is found, the macro `__FUNC__` is
1066 !! created or updated with the procedure name.
1067 !!
1068 !! When an `end function`, `endfunction`, `end subroutine`, or
1069 !! `endsubroutine` statement is encountered, the macro value is
1070 !! cleared.
1071 !!
1072 !! Detection is token based and therefore supports arbitrary valid
1073 !! Fortran declaration prefixes such as:
1074 !! - `recursive function foo()`
1075 !! - `pure elemental function bar()`
1076 !! - `type(string) function baz() result(res)`
1077 !! - `module subroutine solve()`
1078 !!
1079 !! The macro value reflects the innermost active procedure and is
1080 !! automatically cleared when leaving the corresponding scope.
1081 !!
1082 !! @param[in] line
1083 !! Current source line after continuation handling
1084 !! @param[inout] macros
1085 !! Current macro table (updated in-place)
1086 !!
1087 !! @ingroup group_macro
1088 subroutine update_func_macro(line, macros)
1089 character(*), intent(in) :: line
1090 type(macro), allocatable, intent(inout) :: macros(:)
1091 !private
1092 character(:), allocatable :: txt
1093 character(:), allocatable :: procname
1094 logical :: leaving
1095 integer :: imacro
1096
1097 if (.not. is_defined('__FUNC__', macros, imacro)) return
1098
1099 txt = lowercase(adjustl(trim(line)))
1100 procname = extract_proc_name(txt, leaving)
1101
1102 if (len_trim(procname) > 0) then
1103 macros(imacro)%value = procname
1104 return
1105 end if
1106
1107 ! Leaving a procedure
1108 if (starts_with(txt, 'end function') .or. &
1109 starts_with(txt, 'endfunction') .or. &
1110 starts_with(txt, 'end subroutine') .or. &
1111 starts_with(txt, 'endsubroutine')) then
1112
1113 if (.not. is_defined('__FUNC__', macros, imacro)) then
1114 call add(macros, '__FUNC__', '')
1115 else
1116 macros(imacro)%value = ''
1117 end if
1118 end if
1119 end subroutine
1120
1121 !> Extract the procedure name from a Fortran procedure declaration
1122 !!
1123 !! Searches a source line for a standalone `function` or `subroutine`
1124 !! token and returns the identifier immediately following it.
1125 !!
1126 !! The parser is intentionally independent of declaration prefixes,
1127 !! allowing valid declarations such as:
1128 !! @code{.f90}
1129 !! function foo()
1130 !! recursive function foo()
1131 !! pure elemental function foo()
1132 !! type(string) function foo() result(res)
1133 !! module subroutine solve()
1134 !! @endcode
1135 !!
1136 !! End statements (`end function`, `endfunction`,
1137 !! `end subroutine`, `endsubroutine`) are ignored and return
1138 !! an unallocated result.
1139 !!
1140 !! @param[in] txt
1141 !! Source line to analyze
1142 !! @return Extracted procedure name, or an empty string when no
1143 !! procedure declaration is detected.
1144 !!
1145 !! @ingroup group_macro
1146 function extract_proc_name(txt, leaving) result(name)
1147 character(*), intent(in) :: txt
1148 logical, intent(out) :: leaving
1149 character(:), allocatable :: name
1150 !private
1151 integer :: pos, istart, iend
1152 character(:), allocatable :: tmp
1153
1154 name = ''
1155 tmp = lowercase(adjustl(trim(txt)))
1156
1157 ! Ignore END FUNCTION / END SUBROUTINE
1158 if (index(tmp, 'end function') > 0) then
1159 leaving = .true.
1160 return
1161 elseif (index(tmp, 'endfunction') > 0) then
1162 leaving = .true.
1163 return
1164 elseif (index(tmp, 'end subroutine') > 0) then
1165 leaving = .true.
1166 return
1167 elseif (index(tmp, 'endsubroutine') > 0) then
1168 leaving = .true.
1169 return
1170 end if
1171
1172 ! Search FUNCTION token
1173 pos = find_token(tmp, 'function')
1174
1175 if (pos > 0) then
1176 istart = pos + len('function')
1177 else
1178 pos = find_token(tmp, 'subroutine')
1179 if (pos == 0) return
1180 istart = pos + len('subroutine')
1181 end if
1182
1183 ! Skip whitespace
1184 do while (istart <= len(tmp))
1185 if (tmp(istart:istart) /= ' ') exit
1186 istart = istart + 1
1187 end do
1188
1189 if (istart > len(tmp)) return
1190
1191 iend = istart
1192
1193 do while (iend <= len(tmp))
1194 select case (tmp(iend:iend))
1195 case ('a':'z', 'A':'Z', '0':'9', '_')
1196 iend = iend + 1
1197 case default
1198 exit
1199 end select
1200 end do
1201
1202 name = tmp(istart:iend - 1)
1203 contains
1204 !> Locate a standalone token within a source line
1205 !! Searches for a token delimited by non-identifier characters.
1206 !! The token must not appear as part of a larger identifier.
1207 !!
1208 !! Examples:
1209 !! @code{.f90}
1210 !! function foo() ! match "function"
1211 !! subroutine bar() ! match "subroutine"
1212 !! myfunction() ! no match
1213 !! subroutine_name ! no match
1214 !! @endcode
1215 !!
1216 !! @param[in] line Source line to search
1217 !! @param[in] token Token to locate
1218 !! @return Position of the first valid token occurrence,
1219 !! or zero if not found
1220 !!
1221 !! @private
1222 !! @ingroup group_macro
1223 integer function find_token(line, token) result(pos)
1224 character(*), intent(in) :: line
1225 character(*), intent(in) :: token
1226 !private
1227 integer :: i, ltok, lline
1228 logical :: left_ok, right_ok
1229
1230 pos = 0
1231 lline = len_trim(line); ltok = len_trim(token)
1232
1233 if (ltok == 0 .or. lline < ltok) return
1234
1235 do i = 1, lline - ltok + 1
1236 if (lowercase(line(i:i + ltok - 1)) /= lowercase(token)) cycle
1237
1238 ! Check left boundary
1239 if (i == 1) then
1240 left_ok = .true.
1241 else
1242 left_ok = .not. is_ident(line(i - 1:i - 1))
1243 end if
1244
1245 ! Check right boundary
1246 if (i + ltok - 1 == lline) then
1247 right_ok = .true.
1248 else
1249 right_ok = .not. is_ident(line(i + ltok:i + ltok))
1250 end if
1251
1252 if (left_ok .and. right_ok) then
1253 pos = i
1254 return
1255 end if
1256 end do
1257 end function
1258
1259 !> Determine whether a character is a valid identifier character
1260 !!
1261 !! Returns `.true.` for characters that may appear in a Fortran
1262 !! identifier:
1263 !! - letters (`A-Z`, `a-z`)
1264 !! - digits (`0-9`)
1265 !! - underscore (`_`)
1266 !!
1267 !! Used internally by token matching routines to verify identifier
1268 !! boundaries.
1269 !!
1270 !! @param[in] ch
1271 !! Character to test
1272 !! @return `.true.` if the character is a valid identifier character
1273 !!
1274 !! @private
1275 !! @ingroup group_macro
1276 logical function is_ident(ch)
1277 character(1), intent(in) :: ch
1278
1279 select case (ch)
1280 case ('a':'z', 'A':'Z', '0':'9', '_')
1281 is_ident = .true.
1282 case default
1283 is_ident = .false.
1284 end select
1285 end function
1286 end function
1287end module
character(:) function, allocatable, public expand_macros(line, macros, stitch, implicit_conti, dollar_insert, ctx)
Recursively expand user-defined macros.
Definition macro.f90:459
character(:) function, allocatable, public expand_all(ctx, macros, stitch, has_extra, implicit_conti, dollar_insert, is_standalone)
Expand a source line including predefined macros.
Definition macro.f90:327
logical function, public is_defined(name, macros, idx)
Determine whether a macro is currently defined.
Definition macro.f90:831
Append macros to a macro table.
Definition macro.f90:176
Remove all macro definitions from a table.
Definition macro.f90:188
Retrieve a macro by index.
Definition macro.f90:195
Insert a macro at a specified position.
Definition macro.f90:204
Abstract interface for line preprocessing callbacks.
Definition macro.f90:259
Abstract interface to the top-level preprocessing routine.
Definition macro.f90:239
Remove a macro definition from a table.
Definition macro.f90:214
Return the number of stored macro definitions.
Definition macro.f90:224
Remove trailing blanks from a string object.
Definition string.f90:240
Representation of a preprocessor macro.
Definition macro.f90:136
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition string.f90:112