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