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!! - Parameter stringification (`#param`) and token pasting (`##`)
9!! - Built-in predefined macros: `__FILE__`, `__FILENAME__`, `__LINE__`, `__DATE__`, `__TIME__`, `__TIMESTAMP__`
10!! - Recursive expansion with circular dependency detection via digraph analysis
11!! - Dynamic macro table of `macro` objects with efficient addition, lookup, removal
12!! - Full support for nested macro calls and proper argument handling
13!!
14!! The design allows safe, repeated expansion while preventing infinite recursion.
15!! All operations are container-agnostic using allocatable dynamic arrays.
16!!
17!! <h2 class="groupheader">Examples</h2>
18!!
19!! 1. Define and use simple macros:
20!! @code{.f90}
21!! type(macro), allocatable :: macros(:)
22!! call add(macros, macro('PI', '3.1415926535'))
23!! call add(macros, macro('MSG(x)', 'print *, ″Hello ″, x'))
24!! print *, expand_all(context('area = PI * r**2', 10, './circle.F90', 'circle'), macros, stitch, .false., .false.)
25!! !> prints: area = 3.1415926535 * r**2
26!! @endcode
27!!
28!! 2. Variadic macro with stringification and pasting:
29!! @code{.f90}
30!! call add(macros, macro('DEBUG_PRINT(...)', 'print *, ″DEBUG[″, __FILE__, ″:″, __LINE__, ″]: ″, __VA_ARGS__'))
31!! print *, expand_all(context('DEBUG_PRINT(″value =″, x)', 42, 'test.F90', 'text'), macros, stitch, .false., false)
32!! !> prints: print *, 'DEBUG[', 'test.F90', ':', 42, ']: ', 'value =', x
33!! @endcode
34!!
35!! 3. Token pasting with ##:
36!! @code{.f90}
37!! call add(macros, macro('MAKE_VAR(name,num)', 'var_name_##num'))
38!! print *, expand_all(context('real :: MAKE_VAR(temp,42)', 5, 'file.F90', 'file'), macros, stitch, .false., .false.)
39!! !> prints: real :: var_name_42
40!! @endcode
41module fpx_macro
42 use fpx_constants
43 use fpx_logging
44 use fpx_path
45 use fpx_graph
46 use fpx_string
47 use fpx_date
48 use fpx_logging
49 use fpx_context
50
51 implicit none; private
52
53 public :: macro, &
54 add, &
55 get, &
56 insert, &
57 clear, &
58 remove, &
59 sizeof
60
61 public :: expand_macros, &
62 expand_all, &
64
65 !> @brief Default buffer size
66 !! @ingroup group_macro
67 integer, parameter :: buffer_size = 256
68
69 !> Derived type representing a single preprocessor macro
70 !! Extends @link fpx_string::string string @endlink with macro-specific fields: replacement value, parameters,
71 !! variadic flag, and cyclic self-reference detection.
72 !! <h2 class="groupheader">Examples</h2>
73 !! @code{.f90}
74 !! type(macro), allocatable :: macros(:)
75 !! call add(macros, macro('PI', '3.1415926535'))
76 !! @endcode
77 !! <h2 class="groupheader">Constructors</h2>
78 !! Initializes a new instance of the @ref macro class
79 !! <h3>macro(character(*), character(*))</h3>
80 !! @verbatim type(macro) function macro(character(*) name, (optional) character(*) val) @endverbatim
81 !!
82 !! @param[in] name macro name
83 !! @param[in] val (optional) value of the macro
84 !!
85 !! @b Examples
86 !! @code{.f90}
87 !! type(macro) :: m
88 !! m = macro('_WIN32')
89 !! @endcode
90 !! @return The constructed macro object.
91 !!
92 !! <h2 class="groupheader">Remarks</h2>
93 !! @ingroup group_macro
94 type, extends(string) :: macro
95 character(:), allocatable :: value !< Value of the macro
96 type(string), allocatable :: params(:) !< List of parameter for function like macros
97 logical :: is_variadic !< Indicate whether the macro is variadic or not.
98 logical :: is_cyclic !< Indicates whether the macro has cyclic dependencies or not.
99 end type
100
101 !> @brief Constructor interface for macro type
102 !!
103 !! @b Remarks
104 !! @ingroup group_macro
105 interface macro
106 !! @cond
107 module procedure :: macro_new
108 !! @endcond
109 end interface
110
111 !> Add one or more macros to a dynamic table
112 !!
113 !! @b Remarks
114 !! @ingroup group_macro
115 interface add
116 module procedure :: add_item
117 module procedure :: add_item_from_name
118 module procedure :: add_item_from_name_and_value
119 module procedure :: add_range
120 end interface
121
122 !> Remove all macros from a table
123 !!
124 !! @b Remarks
125 !! @ingroup group_macro
126 interface clear
127 module procedure :: clear_item
128 end interface
129
130 !> Retrieve a macro by index
131 !!
132 !! @b Remarks
133 !! @ingroup group_macro
134 interface get
135 module procedure :: get_item
136 end interface
137
138 !> Insert more macro to a dynamic table
139 !!
140 !! @b Remarks
141 !! @ingroup group_macro
142 interface insert
143 module procedure :: insert_item
144 end interface
145
146 !> Remove a macro at given index
147 !!
148 !! @b Remarks
149 !! @ingroup group_macro
150 interface remove
151 module procedure :: remove_item
152 end interface
153
154 !> Return current number of stored macros
155 !!
156 !! @b Remarks
157 !! @ingroup group_macro
158 interface sizeof
159 module procedure :: size_item
160 end interface
161
162contains
163
164 !> Construct a new macro object
165 !! @param[in] name Mandatory macro name
166 !! @param[in] val Optional replacement text (default: empty)
167 !! @return Initialized macro object
168 type(macro) function macro_new(name, val) result(that)
169 character(*), intent(in) :: name
170 character(*), intent(in), optional :: val
171
172 that = trim(name)
173 if (present(val)) then
174 that%value = val
175 else
176 that%value = ''
177 end if
178 allocate(that%params(0))
179 that%is_variadic = .false.
180 that%is_cyclic = that == that%value
181 end function
182
183 !> Fully expand a line including predefined macros (__FILE__, __LINE__, etc.)
184 !! First performs normal macro expansion via expand_macros(), then substitutes
185 !! standard predefined tokens with current file/line/date information.
186 !! @param[in] ctx Context
187 !! @param[in] macros Current macro table
188 !! @param[out] stitch Set to .true.true. if result ends with '&' (Fortran continuation)
189 !! @param[in] has_extra Has extra macros (non-standard) like __FILENAME__ and __TIMESTAMP__
190 !! @param[in] implicit_conti If .true., implicit continuation is permitted
191 !! @return Expanded line with all macros and predefined tokens replaced
192 !!
193 !! @b Remarks
194 !! @ingroup group_macro
195 function expand_all(ctx, macros, stitch, has_extra, implicit_conti) result(expanded)
196 type(context), intent(in) :: ctx
197 type(macro), intent(in) :: macros(:)
198 logical, intent(out) :: stitch
199 logical, intent(in) :: has_extra
200 logical, intent(in) :: implicit_conti
201 character(:), allocatable :: expanded
202 !private
203 integer :: pos, start, sep, dot
204 type(datetime) :: date
205
206 expanded = expand_macros(ctx%content, macros, stitch, implicit_conti, ctx)
207 date = now()
208
209 ! Substitute __FILE__ (relative path to working directory)
210 pos = 1
211 do while (pos > 0)
212 pos = index(expanded, '__FILE__')
213 if (pos > 0) then
214 start = pos + len('__FILE__')
215 expanded = trim(expanded(:pos - 1) // '"' // trim(ctx%path) // '"' // trim(expanded(start:)))
216 end if
217 end do
218
219 ! Substitute __LINE__
220 pos = 1
221 do while (pos > 0)
222 pos = index(expanded, '__LINE__')
223 if (pos > 0) then
224 if (pos > 0) then
225 start = pos + len('__LINE__')
226 expanded = trim(expanded(:pos - 1) // tostring(ctx%line) // trim(expanded(start:)))
227 end if
228 end if
229 end do
230
231 ! Substitute __DATE__
232 pos = 1
233 do while (pos > 0)
234 pos = index(expanded, '__DATE__')
235 if (pos > 0) then
236 if (pos > 0) then
237 start = pos + len('__DATE__')
238 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('MMM-dd-yyyy') // '"' // trim(expanded(start:)))
239 end if
240 end if
241 end do
242
243 ! Substitute __TIME__
244 pos = 1
245 do while (pos > 0)
246 pos = index(expanded, '__TIME__')
247 if (pos > 0) then
248 if (pos > 0) then
249 start = pos + len('__TIME__')
250 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('HH:mm:ss') // '"' // trim(expanded(start:)))
251 end if
252 end if
253 end do
254
255 if (has_extra) then
256 ! Substitute __FILENAME__
257 pos = 1; do while (pos > 0)
258 pos = index(expanded, '__FILENAME__')
259 if (pos > 0) then
260 start = pos + len('__FILENAME__')
261 expanded = trim(expanded(:pos - 1) // '"' // filename(ctx%path, .true.) // '"' // trim(expanded(start:)))
262 end if
263 end do
264
265 ! Substitute __TIMESTAMP__
266 pos = 1; do while (pos > 0)
267 pos = index(expanded, '__TIMESTAMP__')
268 if (pos > 0) then
269 if (pos > 0) then
270 start = pos + len('__TIMESTAMP__')
271 expanded = trim(expanded(:pos - 1) // '"' // date%to_string('ddd MM yyyy') // ' ' // date%to_string(&
272 'HH:mm:ss'&
273 &) // '"' // trim(expanded(start:)))
274 end if
275 end if
276 end do
277 end if
278 end function
279
280 !> Core recursive macro expander (handles function-like, variadic, #, ##)
281 !!
282 !! Performs actual macro replacement with full support for:
283 !! - Function-like macros with argument collection
284 !! - Stringification (`#param`)
285 !! - Token pasting (`##`)
286 !! - Variadic macros and `__VA_ARGS__`, `__VA_OPT__`
287 !! - Recursion with cycle detection via digraph
288 !! - Proper handling of nested parentheses and quoted strings
289 !!
290 !! @param[in] line Line to be expanded
291 !! @param[in] macros Current macro table
292 !! @param[out] stitch .true. if final line ends with '&'
293 !! @param[in] implicit_conti If .true., implicit continuation is permitted
294 !! @param[in] ctx Context
295 !! @return Line with user-defined macros expanded (predefined tokens untouched)
296 !!
297 !! @b Remarks
298 !! @ingroup group_macro
299 function expand_macros(line, macros, stitch, implicit_conti, ctx) result(expanded)
300 character(*), intent(in) :: line
301 type(macro), intent(in) :: macros(:)
302 logical, intent(out) :: stitch
303 logical, intent(in) :: implicit_conti
304 type(context), intent(in) :: ctx
305 character(:), allocatable :: expanded
306 !private
307 integer :: imacro, paren_level
308 type(digraph) :: graph
309
310 imacro = 0; paren_level = 0
311 graph = digraph(size(macros))
312 stitch = .false.
313
314 expanded = expand_macros_internal(line, imacro, macros)
315
316 if (implicit_conti) then
317 stitch = (tail(expanded) == '&') .or. paren_level > 0
318 else
319 stitch = (tail(expanded) == '&') .and. paren_level > 0
320 end if
321 contains
322 !> @private
323 recursive function expand_macros_internal(line, imacro, macros) result(expanded)
324 character(*), intent(in) :: line
325 integer, intent(in) :: imacro
326 type(macro), intent(in) :: macros(:)
327 character(:), allocatable :: expanded
328 !private
329 character(:), allocatable :: args_str, temp, va_args
330 character(:), allocatable :: token1, token2, prefix, suffix
331 type(string) :: arg_values(max_params)
332 integer :: c, i, j, k, n, pos, start, arg_start, nargs
333 integer :: m_start, m_end, token1_start, token2_stop
334 logical :: isopened, found
335 character :: quote
336 integer, allocatable :: indexes(:)
337 logical :: exists
338
339 expanded = line
340 if (size(macros) == 0) return
341 isopened = .false.
342
343 do i = 1, size(macros)
344 n = len_trim(macros(i)); if (n == 0) cycle
345 c = 0
346 do while (c < len_trim(expanded))
347 c = c + 1
348 if (expanded(c:c) == '"' .or. expanded(c:c) == "'") then
349 if (.not. isopened) then
350 isopened = .true.
351 quote = expanded(c:c)
352 else
353 if (expanded(c:c) == quote) isopened = .false.
354 end if
355 end if
356 if (isopened) cycle
357 if (c + n - 1 > len_trim(expanded)) exit
358
359 found = .false.
360 if (expanded(c:c + n - 1) == macros(i)) then
361 found = .true.
362 if (len_trim(expanded(c:)) > n) then
363 found = verify(expanded(c + n:c + n), ' ()[]<>&;.,^~!/*-+\="' // "'") == 0
364 end if
365 if (found .and. c > 1) then
366 found = verify(expanded(c - 1:c - 1), ' ()[]<>&;.,^~!/*-+\="' // "'") == 0
367 end if
368 end if
369
370 if (found) then
371 pos = c
372 c = c + n - 1
373 m_start = pos
374 start = pos + n
375 if (size(macros(i)%params) > 0 .or. macros(i)%is_variadic) then
376 if (start <= len(expanded)) then
377 if (expanded(start:start) == '(') then
378 paren_level = 1
379 arg_start = start + 1
380 nargs = 0
381 j = arg_start
382 do while (j <= len(expanded) .and. paren_level > 0)
383 if (expanded(j:j) == '(') paren_level = paren_level + 1
384 if (expanded(j:j) == ')') paren_level = paren_level - 1
385 if (paren_level == 1 .and. expanded(j:j) == ',' .or. paren_level == 0) then
386 if (nargs < max_params) then
387 nargs = nargs + 1
388 arg_values(nargs) = trim(adjustl(expanded(arg_start:j - 1)))
389 arg_start = j + 1
390 end if
391 end if
392 j = j + 1
393 end do
394 m_end = j - 1
395 args_str = expanded(start:m_end)
396 temp = trim(macros(i)%value)
397
398 if (macros(i)%is_variadic) then
399 if (nargs < size(macros(i)%params)) then
400 call printf(render(diagnostic_report(level_error, &
401 message='Variadic macro issue', &
402 label=label_type('Too few arguments for macro ' // macros(i), start, m_end - start), &
403 source=trim(ctx%path)), &
404 expanded, ctx%line))
405 cycle
406 end if
407 va_args = ''
408 do j = size(macros(i)%params) + 1, nargs
409 if (j > size(macros(i)%params) + 1) va_args = va_args // ', '
410 va_args = va_args // arg_values(j)
411 end do
412 else if (nargs /= size(macros(i)%params)) then
413 call printf(render(diagnostic_report(level_error, &
414 message='Function-like macro issue', &
415 label=label_type('Incorrect number of arguments for macro ' // macros(i), start, m_end - start), &
416 source=trim(ctx%path)), &
417 expanded, ctx%line))
418 cycle
419 end if
420
421 ! Substitute regular parameters
422 argbck :block
423 integer :: c1, h1
424 logical :: opened
425
426 opened = .false.
427 jloop: do j = 1, size(macros(i)%params)
428 c1 = 0
429 wloop: do while (c1 < len_trim(temp))
430 c1 = c1 + 1
431 if (temp(c1:c1) == '"') opened = .not. opened
432 if (opened) cycle wloop
433 if (c1 + len_trim(macros(i)%params(j)) - 1 > len(temp)) cycle wloop
434
435 if (temp(c1:c1 + len_trim(macros(i)%params(j)) - 1) == trim(macros(i)%params(j))) &
436 then
437 checkbck:block
438 integer :: cend, l
439
440 cend = c1 + len_trim(macros(i)%params(j))
441 l = len(temp)
442 if (c1 == 1 .and. cend == l + 1) then
443 exit checkbck
444 else if (c1 > 1 .and. l == cend - 1) then
445 if (verify(temp(c1 - 1:c1 - 1), ' #()[]<>&;.,!/*-+\="' // "'") /= 0) &
446 cycle wloop
447 else if (c1 <= 1 .and. cend <= l) then
448 if (verify(temp(cend:cend), ' #()[]<>&;.,!/*-+\="' // "'") /= 0) cycle &
449 wloop
450 else
451 if (verify(temp(c1 - 1:c1 - 1), ' #()[]<>&;.,!/*-+\="' // "'") /= 0 &
452 .or. verify(temp(cend:cend), ' #()[]<>$&;.,!/*-+\="' // "'") /=&
453 & 0) cycle wloop
454 end if
455 end block checkbck
456 pos = c1
457 c1 = c1 + len_trim(macros(i)%params(j)) - 1
458 start = pos + len_trim(macros(i)%params(j))
459 if (pos == 2) then
460 if (temp(pos - 1:pos - 1) == '#') then
461 temp = trim(temp(:pos - 2) // '"' // arg_values(j) // '"' // trim(temp(&
462 start:)))
463 else
464 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
465 end if
466 elseif (pos > 2) then
467 h1 = pos - 1
468 if (previous(temp, h1) == '#') then
469 if (h1 == 1) then
470 temp = trim(temp(:h1 - 1) // '"' // arg_values(j) // '"' // trim(&
471 temp(start:)))
472 else
473 if (temp(h1 - 1:h1 - 1) /= '#') then
474 temp = trim(temp(:h1 - 1) // '"' // arg_values(j) // '"' // &
475 trim(temp(start:)))
476 else
477 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:&
478 )))
479 end if
480 end if
481 else
482 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
483 end if
484 else
485 temp = trim(temp(:pos - 1) // arg_values(j) // trim(temp(start:)))
486 end if
487 end if
488 end do wloop
489 end do jloop
490 end block argbck
491
492 ! Handle concatenation (##) first with immediate substitution
493 block
494 pos = 1
495 do while (pos > 0)
496 pos = index(temp, '##')
497 if (pos > 0) then
498 ! Find token1 (before ##)
499 k = pos - 1
500 if (k <= 0) then
501 call printf(render(diagnostic_report(level_error, &
502 message='Synthax error', &
503 label=label_type('No token before ##', pos, 2), &
504 source=trim(ctx%path)), &
505 temp, ctx%line))
506 cycle
507 end if
508
509 token1 = adjustr(temp(:k))
510 prefix = ''
511 token1_start = index(token1, ' ')
512 if (token1_start > 0) then
513 prefix = token1(:token1_start)
514 token1 = token1(token1_start + 1:)
515 end if
516
517 ! Find token2 (after ##)
518 k = pos + 2
519 if (k > len(temp)) then
520 call printf(render(diagnostic_report(level_error, &
521 message='Synthax error', &
522 label=label_type('No token after ##', pos, 2), &
523 source=trim(ctx%path)), &
524 temp, ctx%line))
525 cycle
526 end if
527
528 suffix = ''
529 token2 = adjustl(temp(k:))
530 token2_stop = index(token2, ' ')
531 if (token2_stop > 0) then
532 suffix = token2(token2_stop:)
533 token2 = token2(:token2_stop - 1)
534 end if
535
536 ! Concatenate, replacing the full 'token1 ## token2' pattern
537 if (is_defined(token1, macros, idx=k)) &
538 token1 = expand_macros_internal(token1, imacro, macros)
539 if (is_defined(token2, macros, idx=k)) &
540 token2 = expand_macros_internal(token2, imacro, macros)
541
542 temp = trim(prefix // trim(token1) // trim(token2) // suffix)
543 end if
544 end do
545 end block
546
547 ! Substitute __VA_ARGS__
548 block
549 if (macros(i)%is_variadic) then
550 pos = 1
551 do while (pos > 0)
552 pos = index(temp, '__VA_ARGS__')
553 if (pos > 0) then
554 start = pos + len('__VA_ARGS__') - 1
555 if (start < len(temp) .and. temp(start:start) == '_' &
556 .and. temp(start + 1:start + 1) == ')') then
557 temp = trim(temp(:pos - 1) // trim(va_args) // ')')
558 else
559 temp = trim(temp(:pos - 1) // trim(va_args) // trim(temp(start + 1:)))
560 end if
561
562 ! Substitute __VA_OPT__
563 pos = index(temp, '__VA_OPT__')
564 if (pos > 0) then
565 start = pos + index(temp(pos:), ')') - 1
566 if (len_trim(va_args) > 0) then
567 temp = trim(temp(:pos - 1)) // temp(pos + index(temp(pos:), '('):start &
568 - 1) // trim(temp(start + 1:))
569 else
570 temp = trim(temp(:pos - 1)) // trim(temp(start + 1:))
571 end if
572 end if
573 end if
574 end do
575 end if
576 end block
577
578 call graph%add_edge(imacro, i)
579 if (.not. graph%is_circular(i)) then
580 temp = expand_macros_internal(temp, i, macros) ! Only for nested macros
581 else
582 call printf(render(diagnostic_report(level_error, &
583 message='Failed macro expansion', &
584 label=label_type('Circular macro detected', index(temp, macros(i)), len(macros(i))), &
585 source=trim(ctx%path)), &
586 temp, ctx%line))
587 cycle
588 end if
589 expanded = trim(expanded(:m_start - 1) // trim(temp) // expanded(m_end + 1:))
590 end if
591 end if
592 else
593 temp = trim(macros(i)%value)
594 m_end = start - 1
595 call graph%add_edge(imacro, i)
596 if ((.not. graph%is_circular(i)) .and. (.not. macros(i)%is_cyclic)) then
597 expanded = trim(expanded(:m_start - 1) // trim(temp) // expanded(m_end + 1:))
598 expanded = expand_macros_internal(expanded, imacro, macros)
599 else
600 call printf(render(diagnostic_report(level_error, &
601 message='Failed macro expansion', &
602 label=label_type('Circular macro detected', index(temp, macros(i)), len(macros(i))), &
603 source=trim(ctx%path)), &
604 temp, ctx%line))
605 cycle
606 end if
607 end if
608 end if
609 end do
610 end do
611 pos = index(expanded, '&')
612 if (index(expanded, '!') > pos .and. pos > 0) expanded = expanded(:pos + 1)
613 end function
614 end function
615
616 !> Check if a macro with given name exists in table
617 !! @param[in] name Macro name to test
618 !! @param[in] macros Current macro table
619 !! @param[inout] idx Optional: returns index (1-based) if found
620 !! @return .true. if macro is defined
621 !!
622 !! @b Remarks
623 !! @ingroup group_macro
624 logical function is_defined(name, macros, idx) result(res)
625 character(*), intent(in) :: name
626 type(macro), intent(in) :: macros(:)
627 integer, intent(inout), optional :: idx
628 !private
629 integer :: i
630
631 res = .false.
632 do i = 1, size(macros)
633 if (macros(i) == trim(name)) then
634 res = .true.
635 if (present(idx)) idx = i
636 exit
637 end if
638 end do
639 end function
640
641 !> Generic conversion of polymorphic value to string
642 !! Used internally during macro argument stringification and debugging.
643 !! Supports integers, reals, logicals, characters, and complex.
644 !!
645 !! @b Remarks
646 !! @ingroup group_macro
647 function tostring(any)
648 class(*), intent(in) :: any
649 !private
650 character(:), allocatable :: tostring
651 character(4096) :: line
652
653 call print_any(any); tostring = trim(line)
654 contains
655 !> @private
656 subroutine print_any(any)
657 use, intrinsic :: iso_fortran_env, only: int8, int16, int32, int64, real32, real64, real128
658 class(*), intent(in) :: any
659
660 select type (any)
661 type is (integer(kind=int8)); write(line, '(i0)') any
662 type is (integer(kind=int16)); write(line, '(i0)') any
663 type is (integer(kind=int32)); write(line, '(i0)') any
664 type is (integer(kind=int64)); write(line, '(i0)') any
665 type is (real(kind=real32)); write(line, '(1pg0)') any
666 type is (real(kind=real64)); write(line, '(1pg0)') any
667 type is (real(kind=real128)); write(line, '(1pg0)') any
668 type is (logical); write(line, '(1l)') any
669 type is (character(*)); write(line, '(a)') any
670 type is (complex(kind=real32)); write(line, '("(",1pg0,",",1pg0,")")') any
671 type is (complex(kind=real64)); write(line, '("(",1pg0,",",1pg0,")")') any
672 type is (complex(kind=real128)); write(line, '("(",1pg0,",",1pg0,")")') any
673 end select
674 end subroutine
675 end function
676
677 !> Internal helper: grow dynamic macro array in chunks for efficiency
678 !! Adds a new macro to the allocatable array, growing in BUFFER_SIZE increments.
679 !! Also detects direct self-references (A → A) and marks both sides as cyclic.
680 !!
681 !! @b Remarks
682 subroutine add_to(array, val)
683 type(macro), allocatable, intent(inout) :: array(:)
684 type(macro), intent(in) :: val(..)
685 !private
686 type(macro), allocatable :: tmp(:)
687 logical, allocatable :: isdef(:)
688 integer :: i, j, n
689
690 n = merge(size(array), 0, allocated(array))
691
692 select rank (val)
693 rank(0)
694 allocate(isdef(1), source=.false.)
695 do i = 1, n
696 if (array(i) == val) then
697 array(i) = val
698 isdef(1) = .true.
699 end if
700 end do
701 if (.not. isdef(1)) then
702 allocate(tmp(n + 1))
703 tmp(1:n) = array
704 tmp(n + 1) = val
705 call move_alloc(tmp, array)
706 if (allocated(tmp)) deallocate(tmp)
707 end if
708 rank(1)
709 allocate(isdef(size(val)), source=.false.)
710 do concurrent(i = 1:n, j = 1:size(val))
711 if (array(i) == val(j)) then
712 array(i) = val(j)
713 isdef(j) = .true.
714 end if
715 end do
716 n = merge(size(array), 0, allocated(array)); allocate(tmp(n + count(isdef)))
717 tmp(1:n) = array
718 tmp(n + 1:) = pack(val, isdef)
719 call move_alloc(tmp, array)
720 if (allocated(tmp)) deallocate(tmp)
721 end select
722
723 do i = 1, size(array)
724 do j = n + 1, size(array)
725 if (i == j) cycle
726 if (array(i) == array(j)%value .and. array(i)%value == array(j)) then
727 array(i)%is_cyclic = .true.
728 end if
729 end do
730 end do
731 end subroutine
732
733 !> Add a complete macro object to the table
734 !!
735 !! @b Remarks
736 subroutine add_item(this, m)
737 type(macro), intent(inout), allocatable :: this(:)
738 type(macro), intent(in) :: m
739
740 call add_to(this, m)
741 end subroutine
742
743 !> Add macro by name only (value = empty)
744 !!
745 !! @b Remarks
746 subroutine add_item_from_name(this, name)
747 type(macro), intent(inout), allocatable :: this(:)
748 character(*), intent(in) :: name
749
750 if (.not. allocated(this)) allocate(this(0))
751 call add_to(this, macro(name))
752 end subroutine
753
754 !> Add macro with name and replacement text
755 !!
756 !! @b Remarks
757 subroutine add_item_from_name_and_value(this, name, value)
758 type(macro), intent(inout), allocatable :: this(:)
759 character(*), intent(in) :: name
760 character(*), intent(in) :: value
761
762 if (.not. allocated(this)) allocate(this(0))
763 call add_to(this, macro(name, value))
764 end subroutine
765
766 !> Add multiple macros at once
767 !!
768 !! @b Remarks
769 subroutine add_range(this, m)
770 type(macro), intent(inout), allocatable :: this(:)
771 type(macro), intent(in) :: m(:)
772
773 if (.not. allocated(this)) allocate(this(0))
774 call add_to(this, m)
775 end subroutine
776
777 !> Remove all macros from table
778 !!
779 !! @b Remarks
780 subroutine clear_item(this)
781 type(macro), intent(inout), allocatable :: this(:)
782
783 if (allocated(this)) deallocate(this)
784 allocate(this(0))
785 end subroutine
786
787 !> Retrieve macro by 1-based index
788 !!
789 !! @b Remarks
790 function get_item(this, key) result(res)
791 type(macro), intent(inout) :: this(:)
792 integer, intent(in) :: key
793 type(macro), allocatable :: res
794 !private
795 integer :: n
796
797 n = sizeof(this)
798 if (key > 0 .and. key <= n) then
799 res = this(key)
800 end if
801 end function
802
803 !> Insert macro at specific position
804 !!
805 !! @b Remarks
806 subroutine insert_item(this, i, m)
807 type(macro), intent(inout), allocatable :: this(:)
808 integer, intent(in) :: i
809 type(macro), intent(in) :: m
810 !private
811 integer :: j, count
812
813 if (.not. allocated(this)) allocate(this(0))
814 count = size(this)
815 call add_to(this, m)
816
817 do j = count, i + 1, -1
818 this(j) = this(j - 1)
819 end do
820 this(i) = m
821 end subroutine
822
823 !> Return number of defined macros
824 !!
825 !! @b Remarks
826 integer function size_item(this) result(res)
827 type(macro), intent(inout), allocatable :: this(:)
828
829 res = merge(size(this), 0, allocated(this))
830 end function
831
832 !> Remove macro at given index
833 !!
834 !! @b Remarks
835 subroutine remove_item(this, i)
836 type(macro), intent(inout), allocatable :: this(:)
837 integer, intent(in) :: i
838 !private
839 type(macro), allocatable :: tmp(:)
840 integer :: k, j, n
841
842 if (.not. allocated(this)) allocate(this(0))
843 n = size(this)
844 if (allocated(this(i)%params)) deallocate(this(i)%params)
845 if (n > 1) then
846 this(i:n - 1) = this(i + 1:n)
847 allocate(tmp(n - 1))
848 tmp = this(:n - 1)
849 deallocate(this)
850 call move_alloc(tmp, this)
851
852 this(:)%is_cyclic = .false.
853 do k = 1, size(this)
854 do j = 1, size(this)
855 if (this(k) == this(j)%value .and. this(k)%value == this(j)) then
856 this(i)%is_cyclic = .true.
857 this(j)%is_cyclic = .true.
858 end if
859 end do
860 end do
861 else
862 deallocate(this); allocate(this(0))
863 end if
864 end subroutine
865end module
integer, parameter buffer_size
Default buffer size.
Definition macro.f90:67
character(:) function, allocatable, public expand_all(ctx, macros, stitch, has_extra, implicit_conti)
Fully expand a line including predefined macros (FILE, LINE, etc.) First performs normal macro expans...
Definition macro.f90:196
character(:) function, allocatable, public expand_macros(line, macros, stitch, implicit_conti, ctx)
Core recursive macro expander (handles function-like, variadic, #, ##).
Definition macro.f90:300
character(:) function, allocatable tostring(any)
Generic conversion of polymorphic value to string Used internally during macro argument stringificati...
Definition macro.f90:648
logical function, public is_defined(name, macros, idx)
Check if a macro with given name exists in table.
Definition macro.f90:625
Add one or more macros to a dynamic table.
Definition macro.f90:115
Remove all macros from a table.
Definition macro.f90:126
Retrieve a macro by index.
Definition macro.f90:134
Insert more macro to a dynamic table.
Definition macro.f90:142
Remove a macro at given index.
Definition macro.f90:150
Return current number of stored macros.
Definition macro.f90:158
Return the trimmed string.
Definition string.f90:149
Derived type representing a single preprocessor macro Extends string with macro-specific fields: rep...
Definition macro.f90:94
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition string.f90:110