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