Loading...
Searching...
No Matches
logging.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_logging Logging
3!! Global logging, ANSI-colored diagnostics, and pretty error/warning reporting for fpx
4!!
5!! This module is the central place for all human-readable output in the fpx preprocessor.
6!! It provides:
7!! - Full ANSI color and style support (bold, underline, colors, etc.)
8!! - Structured diagnostic messages with source context, line numbers, and caret markers
9!! - Pretty-printed multi-line error/warning/help/note/info reports
10!! - Label-based highlighting of specific code ranges (like rustc-style diagnostics)
11!! - Recursive sub-diagnostic support for nested explanations
12!!
13!! Designed to produce modern, readable, IDE-friendly output similar to rustc, clang, or cargo.
14!! When `nocolor = .true.` (or terminal does not support ANSI), falls back to plain text.
15!!
16!! @section logging_examples Examples
17!!
18!! 1. Simple error message
19!! @code{.f90}
20!! print '(A)', render(diagnostic_report( &
21!! LEVEL_ERROR, &
22!! message='unexpected token', &
23!! label=label_type('expected expression', 8, 3)), &
24!! 'a = +')
25!! ...
26!! @endcode
27!!
28!! 2. Colored message (used internally for verbose logging):
29!! @code{.f90}
30!! use fpx_logging
31!!
32!! verbose = .true.
33!! print '(A)', render('Macro expanded: PI = 3.14159')
34!! ..
35!! @endcode
36!!
37!! 3. Full diagnostic report (like a compiler error):
38!! @code{.f90}
39!! character(*), parameter :: input = &
40!! '# This is a TOML document.' // nl // &
41!! 'title = "TOML Example"' // nl // &
42!! '[owner]' // nl // &
43!! 'name = "Tom Preston-Werner"' // nl // &
44!! 'dob = 1979-05-27T07:32:00-08:00 # First class dates' // nl // &
45!! '[database]' // nl // &
46!! 'server = "192.168.1.1"' // nl // &
47!! 'ports = [ 8001, 8001, 8002 ]' // nl // &
48!! 'connection_max = 5000' // nl // &
49!! 'enabled = true' // nl // &
50!! '[servers]' // nl // &
51!! ' # Indentation (tabs and/or spaces) is allowed but not required' // nl // &
52!! ' [servers.alpha]' // nl // &
53!! ' ip = "10.0.0.1"' // nl // &
54!! ' dc = "eqdc10"' // nl // &
55!! ' [servers.beta]' // nl // &
56!! ' ip = "10.0.0.2"' // nl // &
57!! ' dc = "eqdc10"' // nl // &
58!! '[title]' // nl // &
59!! 'data = [ ["gamma", "delta"], [1, 2] ]' // nl // &
60!! '# Line breaks are OK when inside arrays' // nl // &
61!! 'hosts = [' // nl // &
62!! ' "alpha",' // nl // &
63!! ' "omega"' // nl // &
64!! ']'
65!!
66!! print '(A)', render(diagnostic_report(level_error, &
67!! message="duplicated key 'title' found", &
68!! source="example.toml", &
69!! label=[label_type("table 'title' redefined here", 19, 2, 5, .true.), &
70!! label_type("first defined here", 2, 1, 5)]), &
71!! input)
72!! end
73!! ...
74!! @endcode
75!!
76!! Output might look like (colored in terminal):
77!! @code
78!! error: duplicated key 'title' found
79!! --> example.toml:19:2-6
80!! |
81!! 1 | # This is a TOML document.
82!! 2 | title = "TOML Example"
83!! | ----- first defined here
84!! 3 | [owner]
85!! :
86!! 18 | dc = "eqdc10"
87!! 19 | [title]
88!! | ^^^^^ table 'title' redefined here
89!! 20 | data = [ ["gamma", "delta"], [1, 2] ]
90!! |
91!! @endcode
92!!
93!! @par ANSI style & color reference (used internally)
94!! - Styles: BOLD_ON, UNDERLINE_ON, INVERSE_ON, STRIKETHROUGH_ON, ...
95!! - Foreground: RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, ...
96!! - Background: same as foreground but prefixed with BG_
97!!
98!! @note This code is adapted from [pretty-diagnostics](https://github.com/awvwgk/pretty-diagnostics).
99!! The visual presentation is inspired by modern compiler diagnostics
100!! such as rustc and clang, while being adapted for Fortran workflows.
101module fpx_logging
102 use iso_c_binding
103
104 implicit none; private
105
106 public :: render, &
107 printf, &
109 label_type, &
110 level_error, &
111 level_warning, &
112 level_help, &
113 level_note, &
114 level_info
115
116 !> @brief Master switch for verbose diagnostic output
117 !! Default value is `.false.` (quiet mode).
118 !! Set to `.true.` to get detailed step-by-step information about
119 !! preprocessing actions. Safe to modify at any time � the change takes
120 !! effect immediately for all subsequent operations.
121 !! @ingroup group_logging
122 logical, public :: verbose = .false.
123
124 !> @brief Switch for controling the ANSI color output
125 !! Default value is `.true.` (color mode on).
126 !! Set to `.false.` to get raw string output.
127 !! @ingroup group_logging
128 logical, public :: nocolor = .false.
129
130 !! @cond
131 character(1), parameter :: nl = new_line('a') !< New line character.
132 character(1), parameter :: escape = achar(27) !< '\' character.
133 character(2), parameter :: code_start = escape // '[' !< Start ansi code, "\‍[".
134 character(1), parameter :: code_end = 'm' !< End ansi code, "m".
135 character(4), parameter :: code_clear = code_start // '0' // code_end !< Clear all styles, "\‍[0m".
136
137 character(17), parameter :: styles(1:2, 1:16) = reshape([ &
138 'BOLD_ON ', '1 ', &
139 'ITALICS_ON ', '3 ', &
140 'UNDERLINE_ON ', '4 ', &
141 'INVERSE_ON ', '7 ', &
142 'STRIKETHROUGH_ON ', '9 ', &
143 'BOLD_OFF ', '22 ', &
144 'ITALICS_OFF ', '23 ', &
145 'UNDERLINE_OFF ', '24 ', &
146 'INVERSE_OFF ', '27 ', &
147 'STRIKETHROUGH_OFF', '29 ', &
148 'FRAMED_ON ', '51 ', &
149 'ENCIRCLED_ON ', '52 ', &
150 'OVERLINED_ON ', '53 ', &
151 'FRAMED_OFF ', '54 ', &
152 'ENCIRCLED_OFF ', '54 ', &
153 'OVERLINED_OFF ', '55 ' &
154 ], [2, 16]) !< Styles.
155
156 character(15), parameter :: colors_fg(1:2, 1:17) = reshape([ &
157 'BLACK ', '30 ', &
158 'RED ', '31 ', &
159 'GREEN ', '32 ', &
160 'YELLOW ', '33 ', &
161 'BLUE ', '34 ', &
162 'MAGENTA ', '35 ', &
163 'CYAN ', '36 ', &
164 'WHITE ', '37 ', &
165 'DEFAULT ', '39 ', &
166 'BLACK_INTENSE ', '90 ', &
167 'RED_INTENSE ', '91 ', &
168 'GREEN_INTENSE ', '92 ', &
169 'YELLOW_INTENSE ', '93 ', &
170 'BLUE_INTENSE ', '94 ', &
171 'MAGENTA_INTENSE', '95 ', &
172 'CYAN_INTENSE ', '96 ', &
173 'WHITE_INTENSE ', '97 ' &
174 ], [2, 17]) !< Foreground colors.
175
176 character(15), parameter :: colors_bg(1:2, 1:17) = reshape([ &
177 'BLACK ', '40 ', &
178 'RED ', '41 ', &
179 'GREEN ', '42 ', &
180 'YELLOW ', '43 ', &
181 'BLUE ', '44 ', &
182 'MAGENTA ', '45 ', &
183 'CYAN ', '46 ', &
184 'WHITE ', '47 ', &
185 'DEFAULT ', '49 ', &
186 'BLACK_INTENSE ', '100 ', &
187 'RED_INTENSE ', '101 ', &
188 'GREEN_INTENSE ', '102 ', &
189 'YELLOW_INTENSE ', '103 ', &
190 'BLUE_INTENSE ', '104 ', &
191 'MAGENTA_INTENSE', '105 ', &
192 'CYAN_INTENSE ', '106 ', &
193 'WHITE_INTENSE ', '107 ' &
194 ], [2, 17]) !< Background colors.
195 !! @endcond
196
197 !> Generic renderer for diagnostics and source excerpts.
198 !!
199 !! Supported overloads:
200 !! - render(diagnostic_report, source)
201 !! - render(character)
202 !! - render(character, label_type)
203 !! - render(character, label_type(:))
204 !!
205 !! Returns a formatted character string suitable for printing.
206 !!
207 !! @ingroup group_logging
208 interface render
209 module procedure :: render_diagnostic
210 module procedure :: render_text
211 module procedure :: render_text_with_label
212 module procedure :: render_text_with_labels
213 end interface
214
215 enum, bind(c)
216 enumerator :: LEVEL_ERROR = 0
217 enumerator :: LEVEL_WARNING = 1
218 enumerator :: LEVEL_HELP = 2
219 enumerator :: LEVEL_NOTE = 3
220 enumerator :: LEVEL_INFO = 4
221 end enum
222
223 !> Diagnostic label identifying a region of source text.
224 !!
225 !! A label highlights a specific character range within a source line
226 !! and may carry an explanatory message. Labels are used to produce
227 !! compiler-style diagnostics similar to those of rustc or clang.
228 !!
229 !! Labels may be primary or secondary:
230 !! - Primary labels identify the principal cause of the diagnostic.
231 !! - Secondary labels provide additional context.
232 !!
233 !! Primary labels determine the source location shown in the
234 !! diagnostic header and are rendered using '^' markers.
235 !!
236 !! Secondary labels are rendered using '-' markers and provide
237 !! supplementary context.
238 !! @note
239 !! Character positions are 1-based.
240 !! The highlighted range spans:
241 !!
242 !! first <= position < finish
243 !!
244 !! where `finish` is exclusive.
245 !!
246 !! @section label_type_examples Examples
247 !! @code{.f90}
248 !! type(string) :: s
249 !! s = 'foo'
250 !! ...
251 !! @endcode
252 !!
253 !! @section label_type_constructor Constructors
254 !! Initializes a new instance of the label_type class
255 !! @b Constructor
256 !! @code{.f90}
257 !! type(label_type) function string(character(*) text, integer first, integer length, (optional) integer level)
258 !! @endcode
259 !!
260 !! @param[in] text
261 !! Text displayed next to the label
262 !! @param[in] first
263 !! Position of the label
264 !! @param[in] length
265 !! Length of the label
266 !! @param[in] level
267 !! (optional) Level of the label
268 !!
269 !! @b Examples
270 !! @code{.f90}
271 !! type(label_type) :: label
272 !! label = label_type('Syntax error', 5, 7)
273 !! ...
274 !! @endcode
275 !! @return The constructed label_type object.
276 !!
277 !! @b Constructor
278 !! @code{.f90}
279 !! type(label_type) function label_type(integer line, character(*) text, integer first, integer length, (optional) integer level, (optional) logical primary)
280 !! @endcode
281 !!
282 !! @param[in] line
283 !! line number for the label
284 !! @param[in] text
285 !! Text displayed next to the label
286 !! @param[in] first
287 !! Position of the label
288 !! @param[in] length
289 !! Length of the label
290 !! @param[in] level
291 !! (optional) Level of the label
292 !! @param[in] primary
293 !! .true. if the label is the primary one
294 !!
295 !! @b Examples
296 !! @code{.f90}
297 !! type(label_type) :: label
298 !! label = label_type(1, 'Syntax error', 5, 7, LEVEL_ERROR, .true.)
299 !! ...
300 !! @endcode
301 !! @return The constructed label_type object.
302 !!
303 !! @ingroup group_logging
305 !> Level of message
306 integer, allocatable :: level
307 !> Primary message
308 logical :: primary
309 !> Line number of message
310 integer :: line
311 !> First character of message
312 integer :: first
313 !> Last character of message
314 integer :: finish
315 !> Message text
316 character(:), allocatable :: text
317 end type
318
319 interface label_type
320 module procedure :: label_new
321 module procedure :: label_new_with_line
322 end interface
323
324 !> Structured compiler diagnostic.
325 !!
326 !! A diagnostic report consists of:
327 !!
328 !! - a severity level,
329 !! - a primary message,
330 !! - one or more source labels,
331 !! - optional nested diagnostics.
332 !!
333 !! Reports can be rendered using the generic interface
334 !! `render(...)` to produce human-readable output.
335 !!
336 !! @ingroup group_logging
338 !> Level of message
339 integer :: level
340 !> Primary message
341 character(:), allocatable :: message
342 !> Context of the diagnostic source
343 character(:), allocatable :: source
344 !> Messages associated with this diagnostic
345 type(label_type), allocatable :: label(:)
346 !> Additional diagnostic information
347 type(diagnostic_report), allocatable :: sub(:)
348 end type
349
350 interface diagnostic_report
351 module procedure diagnostic_new
352 end interface
353
354 !! @private
355 type :: line_token
356 integer :: first, finish
357 end type
358
359contains
360
361 !> Colorize and stylize strings, DEFAULT kind.
362 !! @param[in] string Input string.
363 !! @param[in] foreground Foreground color definition.
364 !! @param[in] background Background color definition.
365 !! @param[in] style Style definition.
366 pure function colorize(string, foreground, background, style) result(res)
367 character(*), intent(in) :: string
368 character(*), intent(in), optional :: foreground
369 character(*), intent(in), optional :: background
370 character(*), intent(in), optional :: style
371 character(:), allocatable :: res
372 !private
373 integer :: i
374
375 res = string
376 if (nocolor) return
377 if (present(foreground)) then
378 i = color_index(upper(foreground))
379 if (i > 0) res = code_start // trim(colors_fg(2, i)) // code_end // res // code_clear
380 end if
381 if (present(background)) then
382 i = color_index(upper(background))
383 if (i > 0) res = code_start // trim(colors_bg(2, i)) // code_end // res // code_clear
384 end if
385 if (present(style)) then
386 i = style_index(upper(style))
387 if (i > 0) res = code_start // trim(styles(2, i)) // code_end // res // code_clear
388 end if
389 end function
390
391 !> Return the array-index corresponding to the queried color.
392 !! @note Because Foreground and backround colors lists share the same name,
393 !! no matter what array is used to find the color index.
394 !! Thus, the foreground array is used.
395 elemental integer function color_index(color) result(res)
396 character(*), intent(in) :: color !< Color definition.
397 !private
398 integer :: i
399
400 res = 0
401 do i = 1, size(colors_fg, dim=2)
402 if (trim(colors_fg(1, i)) == trim(adjustl(color))) then
403 res = i
404 exit
405 end if
406 end do
407 end function
408
409 !> Return the array-index corresponding to the queried style.
410 elemental integer function style_index(style) result(res)
411 character(*), intent(in) :: style !< Style definition.
412 !private
413 integer :: i
414
415 res = 0
416 do i = 1, size(styles, dim=2)
417 if (trim(styles(1, i)) == trim(adjustl(style))) then
418 res = i
419 exit
420 end if
421 end do
422 end function
423
424 !> Return a string with all uppercase characters.
425 elemental function upper(string)
426 character(*), intent(in) :: string !< Input string.
427 character(len(string)) :: upper !< Upper case string.
428 !private
429 integer, parameter :: a = iachar('a'), z = iachar('z'), case_diff = iachar('a') - iachar('A')
430 integer :: i, ichar
431
432 do i = 1, len(string)
433 ichar = iachar(string(i:i))
434 if (ichar >= a .and. ichar <= z) ichar = ichar - case_diff
435 upper(i:i) = achar(ichar)
436 end do
437 end function
438
439 type(label_type) pure function label_new(text, first, length, level) result(that)
440 character(*), intent(in) :: text
441 integer, intent(in) :: first
442 integer, intent(in) :: length
443 integer, intent(in), optional :: level
444
445 that%text = text
446 that%line = 1
447 that%first = max(1, first)
448 that%finish = that%first + length
449 that%primary = .true.
450 if (present(level)) that%level = level
451 end function
452
453 type(label_type) pure function label_new_with_line(line, text, first, length, primary, level) result(that)
454 integer, intent(in) :: line
455 character(*), intent(in) :: text
456 integer, intent(in) :: first
457 integer, intent(in) :: length
458 logical, intent(in), optional :: primary
459 integer, intent(in), optional :: level
460
461 that%text = text
462 that%line = line
463 that%first = max(1, first)
464 that%finish = that%first + length
465 if (present(primary)) then
466 that%primary = primary
467 else
468 that%primary = .true.
469 end if
470 if (present(level)) that%level = level
471 end function
472
473 !> Create new diagnostic message
474 !! @param[in] level Level of message
475 !! @param[in] message Primary message
476 !! @param[in] source Context of the diagnostic source
477 !! @param[in] label Messages associated with this diagnostic
478 !! @param[in] diagnostic Additional diagnostic information
479 type(diagnostic_report) function diagnostic_new(level, message, source, label, diagnostic) result(that)
480 integer, intent(in) :: level
481 character(*), intent(in), optional :: message
482 character(*), intent(in), optional :: source
483 type(label_type), intent(in), optional :: label(..)
484 type(diagnostic_report), intent(in), optional :: diagnostic(:)
485 !private
486 integer :: i
487
488 that%level = level
489 if (present(message)) that%message = message
490 if (present(source)) that%source = source
491 if (present(label)) then
492 if (allocated(that%label)) deallocate(that%label)
493 select rank (label)
494 rank(0)
495 allocate(that%label(1))
496 that%label(1) = label
497 if (.not. allocated(that%label(1)%level)) that%label(1)%level = level
498 rank(1)
499 allocate(that%label, source=label)
500 do i = 1, size(label)
501 if (.not. allocated(that%label(i)%level)) that%label(i)%level = level
502 end do
503 end select
504 end if
505 if (present(diagnostic)) that%sub = diagnostic
506
507 if (allocated(that%label)) then
508 if (.not. any(that%label(:)%primary)) then
509 that%label(1)%primary = .true.
510 end if
511 end if
512 end function
513
514 pure function line_tokens(input) result(res)
515 character(*), intent(in) :: input
516 type(line_token), allocatable :: res(:)
517 !private
518 integer :: first, finish
519
520 if (len(input) == 0) then
521 allocate(res(1))
522 res(1)=line_token(1,0)
523 return
524 end if
525
526 first = 1; finish = 0
527 allocate(res(0))
528 do while (first <= len(input))
529 finish = index(input(first + 1:), nl) + first - 1
530 if (finish < first) then
531 finish = len(input)
532 end if
533
534 res = [res, line_token(first, finish)]
535
536 first = finish + (1 + len(nl))
537 end do
538 end function
539
540 pure recursive function render_diagnostic(diag, input, linemum) result(res)
541 type(diagnostic_report), intent(in) :: diag
542 character(*), intent(in) :: input
543 integer, intent(in), optional :: linemum
544 character(:), allocatable :: res
545 !private
546 integer :: i
547
548 res = render_message(diag%level, diag%message)
549
550 if (allocated(diag%label)) then
551 res = res // nl // render_text_with_labels(input, diag%label, source=diag%source, linemum=linemum)
552 else
553 res = res // nl // render_text_with_labels(input, [label_type('', 1, len_trim(input))], source=diag%source, linemum=&
554 linemum)
555 end if
556
557 if (allocated(diag%sub)) then
558 do i = 1, size(diag%sub)
559 res = res // nl // render_diagnostic(diag%sub(i), input, linemum)
560 end do
561 end if
562 end function
563
564 pure function render_message(level, message) result(res)
565 integer, intent(in) :: level
566 character(*), intent(in), optional :: message
567 character(:), allocatable :: res
568
569 if (present(message)) then
570 res = level_name(level) // colorize(': ' // message, style='bold_on')
571 else
572 res = level_name(level)
573 end if
574 end function
575
576 pure function level_name(level) result(res)
577 integer, intent(in) :: level
578 character(:), allocatable :: res
579 !private
580 character(:), allocatable :: name, fg
581
582 select case(level)
583 case (level_error)
584 name='error'; fg='red'
585 case (level_warning)
586 name='warning'; fg='yellow'
587 case (level_help)
588 name='help'; fg='cyan'
589 case (level_note)
590 name='note'; fg='blue'
591 case (level_info)
592 name='info'; fg='magenta'
593 case default
594 name='unknown'; fg='blue'
595 end select
596
597 res = colorize(name, foreground=fg, style='bold_on')
598 end function
599
600 pure function render_source(source, offset) result(res)
601 character(*), intent(in) :: source
602 integer, intent(in) :: offset
603 character(:), allocatable :: res
604
605 res = repeat(' ', offset) // colorize('-->', foreground='blue') // ' ' // source
606 end function
607
608 pure function render_text(input, source, linenum) result(res)
609 character(*), intent(in) :: input
610 character(*), intent(in), optional :: source
611 integer, intent(in), optional :: linenum
612 character(:), allocatable :: res
613 !private
614 integer :: i, offset, iline
615 type(line_token), allocatable :: token(:)
616
617 iline = 1; if (present(linenum)) iline = linenum
618 token = line_tokens(input)
619 offset = integer_width(iline)
620
621 if (present(source)) then
622 res = render_source(source, offset) // nl // &
623 repeat(' ', offset + 1) // colorize('|', foreground='blue')
624 else
625 res = repeat(' ', offset + 1) // colorize('|', foreground='blue')
626 end if
627
628 do i = 1, size(token)
629 res = res // nl // render_line(input(token(i)%first:token(i)%finish), to_string(iline + i - 1, offset))
630 end do
631 res = res // nl // repeat(' ', offset + 1) // colorize('|', foreground='blue')
632 end function
633
634 pure function render_text_with_label(input, label, source, linenum) result(res)
635 character(*), intent(in) :: input
636 type(label_type), intent(in) :: label
637 character(*), intent(in), optional :: source
638 integer, intent(in), optional :: linenum
639 character(:), allocatable :: res
640
641 res = render_text_with_labels(input, [label], source, linenum)
642 end function
643
644 pure function render_text_with_labels(input, labels, source, linemum) result(res)
645 character(*), intent(in) :: input
646 type(label_type), intent(in) :: labels(:)
647 character(*), intent(in), optional :: source
648 integer, intent(in), optional :: linemum
649 character(:), allocatable :: res
650 !private
651 integer :: i, j, offset, first, finish, iline
652 type(line_token), allocatable :: token(:)
653 logical, allocatable :: display(:)
654
655 token = line_tokens(input)
656 first = max(1, minval(labels%line) - 1)
657 finish = min(size(token), maxval(labels%line) + 1)
658 iline = 1; if (present(linemum)) iline = linemum
659 offset = integer_width(iline)
660
661 i = 1 ! Without a primary we use the first label
662 do j = 1, size(labels)
663 if (labels(j)%primary) then
664 i = j
665 exit
666 end if
667 end do
668
669 if (present(source)) then
670 res = render_source(source, offset) // ':' // &
671 to_string(labels(i)%line) // ':' // &
672 to_string(labels(i)%first) // '-' // to_string(labels(i)%finish) // nl // &
673 repeat(' ', offset + 1) // colorize('|', foreground='blue')
674 else
675 res = repeat(' ', offset + 1) // colorize('|', foreground='blue')
676 end if
677
678 allocate(display(first:finish), source=.false.)
679 do j = 1, size(labels)
680 display(max(first, labels(j)%line - 1):min(finish, labels(j)%line + 1)) = .true.
681 end do
682
683 do i = first, finish
684 if (.not. display(i)) then
685 if (i > first) then
686 if (display(i - 1)) then
687 res = res // nl //&
688 repeat(' ', offset + 1) // colorize(':', foreground='blue')
689 end if
690 end if
691 cycle
692 end if
693
694 res = res // nl //&
695 & render_line(input(token(i)%first:token(i)%finish), &
696 & to_string(iline + i - 1, offset))
697 if (any(i == labels%line)) then
698 do j = 1, size(labels)
699 if (labels(j)%line /= i) cycle
700 res = res // nl //&
701 & repeat(' ', offset + 1) // colorize('|', foreground='blue') // &
702 & render_label(labels(j))
703 end do
704 end if
705 end do
706 res = res // nl // repeat(' ', offset + 1) // colorize('|', foreground='blue')
707 end function
708
709 pure function render_label(label) result(res)
710 type(label_type), intent(in) :: label
711 character(:), allocatable :: res
712 !private
713 integer :: width
714 character(1) :: marker
715 character(:), allocatable :: this_color, fg
716
717 marker = merge('^', '-', label%primary)
718 width = label%finish - label%first
719 fg = 'blue'
720
721 if (allocated(label%level)) then
722 select case (label%level)
723 case (level_error)
724 fg = 'red'
725 case (level_warning)
726 fg = 'yellow'
727 case (level_help)
728 fg = 'cyan'
729 case (level_info)
730 fg = 'magenta'
731 end select
732 res = repeat(' ', label%first) // colorize(repeat(marker, width), foreground=fg)
733 if (allocated(label%text)) then
734 res = res // ' ' // colorize(label%text, foreground=fg)
735 end if
736 else
737 res = repeat(' ', label%first) // repeat(marker, width)
738 if (allocated(label%text)) then
739 res = res // ' ' // colorize(label%text, foreground='blue')
740 end if
741 end if
742 end function
743
744 pure function render_line(input, line) result(res)
745 character(*), intent(in) :: input
746 character(*), intent(in) :: line
747 character(:), allocatable :: res
748
749 res = line // ' ' // colorize('|', foreground='blue') // ' ' // input
750 end function
751
752 pure integer function integer_width(input) result(res)
753 integer, value :: input
754
755 if (input == 0) then
756 res = 1
757 return
758 end if
759
760 res = 0
761 do while (input /= 0)
762 input = input / 10
763 res = res + 1
764 end do
765
766 end function
767
768 !> Represent an integer as character sequence.
769 pure function to_string(val, width) result(res)
770 integer, intent(in) :: val
771 integer, intent(in), optional :: width
772 character(:), allocatable :: res
773 !private
774 integer, parameter :: buffer_len = range(val) + 2
775 character(buffer_len) :: buffer
776 integer :: n, pos
777 character(1), parameter :: numbers(0:9) = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
778
779 if (val == 0) then
780 res = numbers(0)
781 return
782 end if
783
784 n = abs(val)
785 buffer = ''
786
787 pos = buffer_len + 1
788 do while (n > 0)
789 pos = pos - 1
790 buffer(pos:pos) = numbers(mod(n, 10))
791 n = n / 10
792 end do
793 if (val < 0) then
794 pos = pos - 1
795 buffer(pos:pos) = '-'
796 end if
797
798 if (present(width)) then
799 res = repeat(' ', max(width - (buffer_len + 1 - pos), 0)) // buffer(pos:)
800 else
801 res = buffer(pos:)
802 end if
803 end function
804
805 !> Conditionally print a message when verbose logging is enabled.
806 !!
807 !! This routine is intended for internal tracing and debugging output.
808 !! Compiler diagnostics should instead be constructed using
809 !! `diagnostic_report` and rendered explicitly.
810 !! @param[in] str Input string.
811 !! @param[in] fmt (optional) print format.
812 subroutine printf(str, fmt, unit)
813 character(*), intent(in) :: str
814 character(*), intent(in), optional :: fmt
815 integer, intent(in), optional :: unit
816
817 if (verbose) then
818 if (present(fmt)) then
819 if (present(unit)) then
820 write(unit, fmt) str
821 else
822 write(*, fmt) str
823 end if
824 else
825 if (present(unit)) then
826 write(unit, '(A)') str
827 else
828 write(*, '(A)') str
829 end if
830 end if
831 end if
832 end subroutine
833
834end module
logical, public nocolor
Switch for controling the ANSI color output Default value is .true. (color mode on)....
Definition logging.f90:128
logical, public verbose
Master switch for verbose diagnostic output Default value is .false. (quiet mode)....
Definition logging.f90:122
Generic renderer for diagnostics and source excerpts.
Definition logging.f90:208
Structured compiler diagnostic.
Definition logging.f90:337
Diagnostic label identifying a region of source text.
Definition logging.f90:304