LCOV - code coverage report
Current view: top level - src/backends - hsd_data_toml.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 75.4 % 541 408
Test Date: 2026-02-15 21:36:29 Functions: 92.6 % 27 25

            Line data    Source code
       1              : !> TOML backend — read/write hsd_table trees using toml-f.
       2              : !>
       3              : !> Mapping (per SPECIFICATION.md §3.4):
       4              : !>   hsd_table            → TOML [section] or inline table
       5              : !>   hsd_value (string)   → TOML string
       6              : !>   hsd_value (integer)  → TOML integer
       7              : !>   hsd_value (real)     → TOML float
       8              : !>   hsd_value (logical)  → TOML boolean
       9              : !>   hsd_value (complex)  → inline table {re = r, im = i}
      10              : !>   hsd_value (array)    → TOML array [1, 2, 3]
      11              : !>   node%attrib          → sibling key "name__attrib"
      12              : !>   anonymous value      → "_value" key
      13              : !>
      14              : !> Requires toml-f.  Compiled only when WITH_TOML is defined.
      15              : module hsd_data_toml
      16              :   use hsd, only: hsd_table, hsd_value, hsd_node, hsd_node_ptr, hsd_error_t, &
      17              :       & new_table, new_value, dp, &
      18              :       & VALUE_TYPE_NONE, VALUE_TYPE_STRING, VALUE_TYPE_INTEGER, &
      19              :       & VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_ARRAY, &
      20              :       & VALUE_TYPE_COMPLEX, HSD_STAT_IO_ERROR, HSD_STAT_SYNTAX_ERROR
      21              :   use tomlf, only: toml_table, toml_array, toml_keyval, toml_key, &
      22              :       & toml_value, toml_error, toml_load, toml_loads, &
      23              :       & toml_serialize, get_value, set_value, add_table, add_array, &
      24              :       & new_table_ => new_table, len, toml_stat
      25              :   implicit none(type, external)
      26              :   private
      27              : 
      28              :   public :: toml_backend_load, toml_backend_load_string
      29              :   public :: toml_backend_dump, toml_backend_dump_to_string
      30              : 
      31              :   !> Suffix for attribute sibling keys (must match JSON backend)
      32              :   character(len=*), parameter :: ATTRIB_SUFFIX = "__attrib"
      33              : 
      34              :   !> Key for anonymous values (must match JSON backend)
      35              :   character(len=*), parameter :: ANON_VALUE_KEY = "_value"
      36              : 
      37              : contains
      38              : 
      39              :   ! ---------------------------------------------------------------------------
      40              :   !  Loading (TOML → hsd_table)
      41              :   ! ---------------------------------------------------------------------------
      42              : 
      43              :   !> Load a TOML file into an hsd_table tree.
      44           32 :   subroutine toml_backend_load(filename, root, error)
      45              :     character(len=*), intent(in) :: filename
      46              :     type(hsd_table), intent(out) :: root
      47              :     type(hsd_error_t), allocatable, intent(out), optional :: error
      48              : 
      49           16 :     type(toml_table), allocatable :: toml_root
      50           16 :     type(toml_error), allocatable :: toml_err
      51              : 
      52           16 :     call toml_load(toml_root, filename, error=toml_err)
      53           16 :     if (allocated(toml_err)) then
      54            0 :       if (present(error)) then
      55            0 :         allocate(error)
      56            0 :         error%code = HSD_STAT_SYNTAX_ERROR
      57            0 :         error%message = toml_err%message
      58              :       end if
      59            0 :       return
      60              :     end if
      61           16 :     if (.not. allocated(toml_root)) then
      62            0 :       if (present(error)) then
      63            0 :         allocate(error)
      64            0 :         error%code = HSD_STAT_IO_ERROR
      65            0 :         error%message = "Failed to parse TOML file: " // trim(filename)
      66              :       end if
      67            0 :       return
      68              :     end if
      69              : 
      70           16 :     call new_table(root)
      71           16 :     call toml_table_to_hsd(toml_root, root)
      72              : 
      73           16 :   end subroutine toml_backend_load
      74              : 
      75              :   !> Load a TOML string into an hsd_table tree.
      76           56 :   subroutine toml_backend_load_string(source, root, error, filename)
      77              :     character(len=*), intent(in) :: source
      78              :     type(hsd_table), intent(out) :: root
      79              :     type(hsd_error_t), allocatable, intent(out), optional :: error
      80              :     character(len=*), intent(in), optional :: filename
      81              : 
      82           28 :     type(toml_table), allocatable :: toml_root
      83           28 :     type(toml_error), allocatable :: toml_err
      84              : 
      85           28 :     call toml_loads(toml_root, source, error=toml_err)
      86           28 :     if (allocated(toml_err)) then
      87            0 :       if (present(error)) then
      88            0 :         allocate(error)
      89            0 :         error%code = HSD_STAT_SYNTAX_ERROR
      90            0 :         error%message = toml_err%message
      91              :       end if
      92            0 :       return
      93              :     end if
      94           28 :     if (.not. allocated(toml_root)) then
      95            0 :       if (present(error)) then
      96            0 :         allocate(error)
      97            0 :         error%code = HSD_STAT_IO_ERROR
      98            0 :         if (present(filename)) then
      99            0 :           error%message = "Failed to parse TOML from: " // trim(filename)
     100              :         else
     101            0 :           error%message = "Failed to parse TOML string"
     102              :         end if
     103              :       end if
     104            0 :       return
     105              :     end if
     106              : 
     107           28 :     call new_table(root)
     108           28 :     call toml_table_to_hsd(toml_root, root)
     109              : 
     110           44 :   end subroutine toml_backend_load_string
     111              : 
     112              :   !> Recursively convert a toml_table to an hsd_table.
     113              :   !> Children of the TOML table become children of the HSD table.
     114          232 :   recursive subroutine toml_table_to_hsd(tt, ht)
     115              :     type(toml_table), intent(inout) :: tt
     116              :     type(hsd_table), intent(inout) :: ht
     117              : 
     118          232 :     type(toml_key), allocatable :: keys(:)
     119          232 :     integer :: ii, nkeys
     120          232 :     character(len=:), allocatable :: key_str, attrib_val
     121              :     type(toml_table), pointer :: child_tt
     122              :     type(toml_array), pointer :: child_arr
     123              :     type(toml_keyval), pointer :: child_kv
     124          232 :     integer :: stat
     125              : 
     126          232 :     call tt%get_keys(keys)
     127          232 :     nkeys = size(keys)
     128              : 
     129          703 :     do ii = 1, nkeys
     130          471 :       key_str = keys(ii)%key
     131              : 
     132              :       ! Skip __attrib keys — they are handled by their sibling
     133          471 :       if (is_attrib_key(key_str)) cycle
     134              : 
     135              :       ! Try to get as table first
     136          425 :       call get_value(tt, key_str, child_tt, requested=.false., stat=stat)
     137          425 :       if (associated(child_tt)) then
     138              :         ! Check if this is a complex value {re = ..., im = ...}
     139          376 :         if (is_complex_table(child_tt)) then
     140            0 :           call add_complex_value(child_tt, ht, key_str)
     141              :         else
     142          188 :           call add_hsd_table_child(child_tt, ht, key_str)
     143              :         end if
     144              :         ! Check for attrib sibling
     145          188 :         call get_attrib_from_toml(tt, key_str, attrib_val)
     146          196 :         if (allocated(attrib_val)) then
     147            8 :           call set_last_child_attrib(ht, attrib_val)
     148              :         end if
     149          188 :         cycle
     150              :       end if
     151              : 
     152              :       ! Try to get as array
     153          237 :       call get_value(tt, key_str, child_arr, requested=.false., stat=stat)
     154          266 :       if (associated(child_arr)) then
     155           29 :         call add_hsd_from_array(child_arr, ht, key_str)
     156              :         ! Check for attrib sibling
     157           29 :         call get_attrib_from_toml(tt, key_str, attrib_val)
     158           29 :         if (allocated(attrib_val)) then
     159            0 :           call set_last_child_attrib(ht, attrib_val)
     160              :         end if
     161           29 :         cycle
     162              :       end if
     163              : 
     164              :       ! Must be a keyval (scalar)
     165          208 :       call get_value(tt, key_str, child_kv, requested=.false., stat=stat)
     166          648 :       if (associated(child_kv)) then
     167          208 :         call add_hsd_from_keyval(child_kv, ht, key_str)
     168              :         ! Check for attrib sibling
     169          208 :         call get_attrib_from_toml(tt, key_str, attrib_val)
     170          246 :         if (allocated(attrib_val)) then
     171           38 :           call set_last_child_attrib(ht, attrib_val)
     172              :         end if
     173              :       end if
     174              :     end do
     175              : 
     176          731 :   end subroutine toml_table_to_hsd
     177              : 
     178              :   !> Add a child hsd_table from a toml_table.
     179          188 :   recursive subroutine add_hsd_table_child(tt, ht, key)
     180              :     type(toml_table), intent(inout), pointer :: tt
     181              :     type(hsd_table), intent(inout) :: ht
     182              :     character(len=*), intent(in) :: key
     183              : 
     184          188 :     type(hsd_table), allocatable :: child_ht
     185          188 :     character(len=:), allocatable :: child_name
     186              : 
     187          188 :     if (key == ANON_VALUE_KEY) then
     188            0 :       child_name = ""
     189              :     else
     190          188 :       child_name = key
     191              :     end if
     192              : 
     193          188 :     allocate(child_ht)
     194          188 :     call new_table(child_ht, name=child_name)
     195          188 :     call toml_table_to_hsd(tt, child_ht)
     196          188 :     call ht%add_child(child_ht)
     197              : 
     198         3384 :   end subroutine add_hsd_table_child
     199              : 
     200              :   !> Add a complex value from a toml inline table {re = ..., im = ...}.
     201            0 :   subroutine add_complex_value(tt, ht, key)
     202              :     type(toml_table), intent(inout), pointer :: tt
     203              :     type(hsd_table), intent(inout) :: ht
     204              :     character(len=*), intent(in) :: key
     205              : 
     206            0 :     type(hsd_value), allocatable :: child_val
     207            0 :     real(dp) :: re_part, im_part
     208            0 :     integer :: stat
     209            0 :     character(len=:), allocatable :: child_name
     210              : 
     211            0 :     if (key == ANON_VALUE_KEY) then
     212            0 :       child_name = ""
     213              :     else
     214            0 :       child_name = key
     215              :     end if
     216              : 
     217            0 :     re_part = 0.0_dp
     218            0 :     im_part = 0.0_dp
     219            0 :     call get_value(tt, "re", re_part, stat=stat)
     220            0 :     call get_value(tt, "im", im_part, stat=stat)
     221              : 
     222            0 :     allocate(child_val)
     223            0 :     call new_value(child_val, name=child_name)
     224            0 :     call child_val%set_complex(cmplx(re_part, im_part, dp))
     225            0 :     call ht%add_child(child_val)
     226              : 
     227            0 :   end subroutine add_complex_value
     228              : 
     229              :   !> Add an hsd child from a TOML scalar keyval.
     230          208 :   subroutine add_hsd_from_keyval(kv, ht, key)
     231              :     type(toml_keyval), intent(inout), pointer :: kv
     232              :     type(hsd_table), intent(inout) :: ht
     233              :     character(len=*), intent(in) :: key
     234              : 
     235          208 :     type(hsd_value), allocatable :: child_val
     236          208 :     character(len=:), allocatable :: str_val, child_name
     237          208 :     integer :: int_val, stat
     238          208 :     real(dp) :: real_val
     239          208 :     logical :: bool_val
     240              : 
     241          208 :     if (key == ANON_VALUE_KEY) then
     242            5 :       child_name = ""
     243              :     else
     244          203 :       child_name = key
     245              :     end if
     246              : 
     247          208 :     allocate(child_val)
     248          208 :     call new_value(child_val, name=child_name)
     249              : 
     250              :     ! Try boolean first
     251          208 :     call get_value(kv, bool_val, stat=stat)
     252          208 :     if (stat == toml_stat%success) then
     253           40 :       if (bool_val) then
     254           34 :         call child_val%set_string("Yes")
     255              :       else
     256            6 :         call child_val%set_string("No")
     257              :       end if
     258           40 :       call ht%add_child(child_val)
     259           40 :       return
     260              :     end if
     261              : 
     262              :     ! Try integer
     263          168 :     call get_value(kv, int_val, stat=stat)
     264          168 :     if (stat == toml_stat%success) then
     265           27 :       call child_val%set_string(int_to_string(int_val))
     266           27 :       call ht%add_child(child_val)
     267           27 :       return
     268              :     end if
     269              : 
     270              :     ! Try real
     271          141 :     call get_value(kv, real_val, stat=stat)
     272          141 :     if (stat == toml_stat%success) then
     273           54 :       call child_val%set_string(real_to_string(real_val))
     274           54 :       call ht%add_child(child_val)
     275           54 :       return
     276              :     end if
     277              : 
     278              :     ! Fall back to string
     279           87 :     call get_value(kv, str_val, stat=stat)
     280           87 :     if (stat == toml_stat%success) then
     281           87 :       call child_val%set_string(str_val)
     282              :     else
     283            0 :       call child_val%set_string("")
     284              :     end if
     285           87 :     call ht%add_child(child_val)
     286              : 
     287          208 :   end subroutine add_hsd_from_keyval
     288              : 
     289              :   !> Add HSD children from a TOML array.
     290              :   !> If the array contains tables → multiple same-named hsd_table children.
     291              :   !> If the array contains scalars → single hsd_value with space-separated text.
     292              :   !> If the array contains arrays → matrix (newline-separated rows).
     293           29 :   recursive subroutine add_hsd_from_array(arr, ht, key)
     294              :     type(toml_array), intent(inout), pointer :: arr
     295              :     type(hsd_table), intent(inout) :: ht
     296              :     character(len=*), intent(in) :: key
     297              : 
     298           29 :     integer :: nn, jj, stat
     299              :     type(toml_table), pointer :: elem_tt
     300              :     type(toml_array), pointer :: elem_arr
     301           29 :     type(hsd_table), allocatable :: child_ht
     302           29 :     type(hsd_value), allocatable :: child_val
     303           29 :     character(len=:), allocatable :: row_str, full_str, child_name
     304           29 :     logical :: first
     305              : 
     306            0 :     nn = len(arr)
     307           29 :     if (nn == 0) return
     308              : 
     309           29 :     if (key == ANON_VALUE_KEY) then
     310           10 :       child_name = ""
     311              :     else
     312           19 :       child_name = key
     313              :     end if
     314              : 
     315              :     ! Check first element type — try table
     316           29 :     call get_value(arr, 1, elem_tt, stat=stat)
     317           29 :     if (stat == toml_stat%success .and. associated(elem_tt)) then
     318              :       ! Array of tables → multiple same-named children
     319            0 :       do jj = 1, nn
     320            0 :         call get_value(arr, jj, elem_tt, stat=stat)
     321            0 :         if (stat /= toml_stat%success .or. .not. associated(elem_tt)) cycle
     322            0 :         allocate(child_ht)
     323            0 :         call new_table(child_ht, name=child_name)
     324            0 :         call toml_table_to_hsd(elem_tt, child_ht)
     325            0 :         call ht%add_child(child_ht)
     326            0 :         deallocate(child_ht)
     327              :       end do
     328            0 :       return
     329              :     end if
     330              : 
     331              :     ! Check first element type — try sub-array (matrix)
     332           29 :     call get_value(arr, 1, elem_arr, stat=stat)
     333           38 :     if (stat == toml_stat%success .and. associated(elem_arr)) then
     334              :       ! Array of arrays → matrix (newline-separated rows)
     335            9 :       full_str = ""
     336            9 :       first = .true.
     337           33 :       do jj = 1, nn
     338           24 :         call get_value(arr, jj, elem_arr, stat=stat)
     339           24 :         if (stat /= toml_stat%success .or. .not. associated(elem_arr)) cycle
     340           24 :         call array_to_space_string(elem_arr, row_str)
     341           33 :         if (first) then
     342            9 :           full_str = row_str
     343            9 :           first = .false.
     344              :         else
     345           15 :           full_str = full_str // new_line("a") // row_str
     346              :         end if
     347              :       end do
     348            9 :       allocate(child_val)
     349            9 :       call new_value(child_val, name=child_name)
     350            9 :       call child_val%set_raw(full_str)
     351            9 :       call ht%add_child(child_val)
     352            9 :       return
     353              :     end if
     354              : 
     355              :     ! Flat scalar array → space-separated string
     356           20 :     call array_to_space_string(arr, full_str)
     357           20 :     allocate(child_val)
     358           20 :     call new_value(child_val, name=child_name)
     359           20 :     call child_val%set_raw(full_str)
     360           20 :     call ht%add_child(child_val)
     361              : 
     362          266 :   end subroutine add_hsd_from_array
     363              : 
     364              :   !> Convert a flat TOML array of scalars to a space-separated string.
     365           44 :   subroutine array_to_space_string(arr, result_str)
     366              :     type(toml_array), intent(inout), pointer :: arr
     367              :     character(len=:), allocatable, intent(out) :: result_str
     368              : 
     369           44 :     integer :: nn, jj, stat
     370           44 :     integer :: int_val
     371           44 :     real(dp) :: real_val
     372           44 :     logical :: bool_val
     373           44 :     character(len=:), allocatable :: str_val, elem_str
     374           44 :     logical :: first
     375              : 
     376            0 :     nn = len(arr)
     377           44 :     result_str = ""
     378           44 :     first = .true.
     379              : 
     380          230 :     do jj = 1, nn
     381              :       ! Try integer
     382          186 :       call get_value(arr, jj, int_val, stat=stat)
     383          186 :       if (stat == toml_stat%success) then
     384           37 :         elem_str = int_to_string(int_val)
     385              :       else
     386              :         ! Try real
     387          149 :         call get_value(arr, jj, real_val, stat=stat)
     388          149 :         if (stat == toml_stat%success) then
     389          133 :           elem_str = real_to_string(real_val)
     390              :         else
     391              :           ! Try boolean
     392           16 :           call get_value(arr, jj, bool_val, stat=stat)
     393           16 :           if (stat == toml_stat%success) then
     394            0 :             if (bool_val) then
     395            0 :               elem_str = "Yes"
     396              :             else
     397            0 :               elem_str = "No"
     398              :             end if
     399              :           else
     400              :             ! Fall back to string
     401           16 :             call get_value(arr, jj, str_val, stat=stat)
     402           16 :             if (stat == toml_stat%success) then
     403           16 :               elem_str = str_val
     404              :             else
     405            0 :               elem_str = ""
     406              :             end if
     407              :           end if
     408              :         end if
     409              :       end if
     410              : 
     411          230 :       if (first) then
     412           44 :         result_str = elem_str
     413           44 :         first = .false.
     414              :       else
     415          142 :         result_str = result_str // " " // elem_str
     416              :       end if
     417              :     end do
     418              : 
     419           44 :   end subroutine array_to_space_string
     420              : 
     421              :   ! ---------------------------------------------------------------------------
     422              :   !  Dumping (hsd_table → TOML)
     423              :   ! ---------------------------------------------------------------------------
     424              : 
     425              :   !> Dump an hsd_table tree to a TOML string.
     426           50 :   subroutine toml_backend_dump_to_string(root, output, pretty)
     427              :     type(hsd_table), intent(in) :: root
     428              :     character(len=:), allocatable, intent(out) :: output
     429              :     logical, intent(in), optional :: pretty
     430              : 
     431           50 :     type(toml_table), allocatable :: toml_root
     432              : 
     433              :     ! TOML has no meaningful compact form — it always uses key = value lines
     434              :     ! and [section] headers.  The pretty argument is accepted for API
     435              :     ! consistency with other backends but has no effect on the output.
     436              :     if (.false. .and. present(pretty)) continue
     437              : 
     438           50 :     allocate(toml_root)
     439           50 :     call new_table_(toml_root)
     440           50 :     call hsd_to_toml_table(root, toml_root)
     441           50 :     output = toml_serialize(toml_root)
     442              : 
     443           94 :   end subroutine toml_backend_dump_to_string
     444              : 
     445              :   !> Dump an hsd_table tree to a TOML file.
     446            1 :   subroutine toml_backend_dump(root, filename, error, pretty)
     447              :     type(hsd_table), intent(in) :: root
     448              :     character(len=*), intent(in) :: filename
     449              :     type(hsd_error_t), allocatable, intent(out), optional :: error
     450              :     logical, intent(in), optional :: pretty
     451              : 
     452            1 :     character(len=:), allocatable :: output
     453            1 :     integer :: unit_num, ios
     454              : 
     455            1 :     call toml_backend_dump_to_string(root, output, pretty)
     456              : 
     457              :     open(newunit=unit_num, file=filename, status="replace", action="write", &
     458            1 :         & iostat=ios)
     459            1 :     if (ios /= 0) then
     460            0 :       if (present(error)) then
     461            0 :         allocate(error)
     462            0 :         error%code = HSD_STAT_IO_ERROR
     463            0 :         error%message = "Failed to open file for writing: " // trim(filename)
     464              :       end if
     465            0 :       return
     466              :     end if
     467            1 :     write(unit_num, "(a)", iostat=ios) output
     468            1 :     close(unit_num)
     469              : 
     470            1 :     if (ios /= 0 .and. present(error)) then
     471            0 :       allocate(error)
     472            0 :       error%code = HSD_STAT_IO_ERROR
     473            0 :       error%message = "Failed to write to file: " // trim(filename)
     474              :     end if
     475              : 
     476           51 :   end subroutine toml_backend_dump
     477              : 
     478              :   !> Recursively convert an hsd_table to a toml_table.
     479          263 :   recursive subroutine hsd_to_toml_table(ht, tt)
     480              :     type(hsd_table), intent(in) :: ht
     481              :     type(toml_table), intent(inout) :: tt
     482              : 
     483          263 :     integer :: ii, jj, name_count
     484          263 :     character(len=:), allocatable :: child_name
     485          263 :     logical, allocatable :: emitted(:)
     486              :     type(toml_table), pointer :: child_tt
     487              : 
     488          263 :     allocate(emitted(ht%num_children))
     489          734 :     emitted = .false.
     490              : 
     491              :     ! --- Pass 1a: value children and their attribs ---
     492              :     ! Emit all scalar key-value pairs before any table sections so that
     493              :     ! the output matches what toml-f produces after parsing.
     494          734 :     do ii = 1, ht%num_children
     495          471 :       if (.not. associated(ht%children(ii)%node)) cycle
     496              : 
     497          263 :       select type (child => ht%children(ii)%node)
     498              :       type is (hsd_value)
     499          258 :         child_name = get_hsd_child_name(child)
     500              :         ! Check for same-named siblings (arrays handled in pass 2)
     501          258 :         name_count = 0
     502          719 :         do jj = ii, ht%num_children
     503          461 :           if (.not. associated(ht%children(jj)%node)) cycle
     504         1180 :           if (get_hsd_child_name(ht%children(jj)%node) == child_name) then
     505          719 :             name_count = name_count + 1
     506              :           end if
     507              :         end do
     508          258 :         if (name_count > 1 .or. emitted(ii)) cycle
     509          258 :         emitted(ii) = .true.
     510          258 :         call write_hsd_value_to_toml(child, child_name, tt)
     511          516 :         if (allocated(child%attrib)) then
     512           37 :           if (len_trim(child%attrib) > 0) then
     513           37 :             call set_value(tt, child_name // ATTRIB_SUFFIX, child%attrib)
     514              :           end if
     515              :         end if
     516              :       end select
     517              :     end do
     518              : 
     519              :     ! --- Pass 1b: table-child attribs (scalar keys for table attributes) ---
     520              :     ! These must appear after value scalars but before table sections.
     521          734 :     do ii = 1, ht%num_children
     522          471 :       if (.not. associated(ht%children(ii)%node)) cycle
     523              : 
     524          263 :       select type (child => ht%children(ii)%node)
     525              :       type is (hsd_table)
     526          213 :         if (allocated(child%attrib)) then
     527           10 :           if (len_trim(child%attrib) > 0) then
     528           10 :             child_name = get_hsd_child_name(child)
     529           10 :             call set_value(tt, child_name // ATTRIB_SUFFIX, child%attrib)
     530              :           end if
     531              :         end if
     532              :       end select
     533              :     end do
     534              : 
     535              :     ! --- Pass 2: table sections and array-of-tables ---
     536          734 :     do ii = 1, ht%num_children
     537          471 :       if (.not. associated(ht%children(ii)%node)) cycle
     538          471 :       if (emitted(ii)) cycle
     539              : 
     540          213 :       child_name = get_hsd_child_name(ht%children(ii)%node)
     541              : 
     542              :       ! Count same-named siblings
     543          213 :       name_count = 0
     544          574 :       do jj = ii, ht%num_children
     545          361 :         if (.not. associated(ht%children(jj)%node)) cycle
     546          935 :         if (get_hsd_child_name(ht%children(jj)%node) == child_name) then
     547          574 :           name_count = name_count + 1
     548              :         end if
     549              :       end do
     550              : 
     551          476 :       if (name_count > 1) then
     552            0 :         call write_array_of_tables(ht, child_name, ii, emitted, tt)
     553              :       else
     554          213 :         emitted(ii) = .true.
     555            0 :         select type (child => ht%children(ii)%node)
     556              :         type is (hsd_table)
     557          213 :           call get_value(tt, child_name, child_tt)
     558          426 :           call hsd_to_toml_table(child, child_tt)
     559              :         type is (hsd_value)
     560              :           ! Should not reach here — values are handled in pass 1a
     561            0 :           call write_hsd_value_to_toml(child, child_name, tt)
     562              :         end select
     563              :       end if
     564              :     end do
     565              : 
     566          264 :   end subroutine hsd_to_toml_table
     567              : 
     568              :   !> Write multiple same-named HSD children as a TOML array of tables.
     569            0 :   recursive subroutine write_array_of_tables(ht, name, start_idx, emitted, tt)
     570              :     type(hsd_table), intent(in) :: ht
     571              :     character(len=*), intent(in) :: name
     572              :     integer, intent(in) :: start_idx
     573              :     logical, intent(inout) :: emitted(:)
     574              :     type(toml_table), intent(inout) :: tt
     575              : 
     576            0 :     integer :: jj
     577              :     type(toml_array), pointer :: arr
     578              :     type(toml_table), pointer :: elem_tt
     579              : 
     580            0 :     call add_array(tt, name, arr)
     581              : 
     582            0 :     do jj = start_idx, ht%num_children
     583            0 :       if (.not. associated(ht%children(jj)%node)) cycle
     584            0 :       if (get_hsd_child_name(ht%children(jj)%node) /= name) cycle
     585            0 :       emitted(jj) = .true.
     586              : 
     587            0 :       select type (child => ht%children(jj)%node)
     588              :       type is (hsd_table)
     589            0 :         call get_value(arr, len(arr) + 1, elem_tt)
     590            0 :         call hsd_to_toml_table(child, elem_tt)
     591              :       type is (hsd_value)
     592              :         ! Scalar in duplicate-named group — store as string in array
     593            0 :         call get_value(arr, len(arr) + 1, elem_tt)
     594            0 :         call write_hsd_value_to_toml(child, ANON_VALUE_KEY, elem_tt)
     595              :       end select
     596              :     end do
     597              : 
     598            0 :   end subroutine write_array_of_tables
     599              : 
     600              :   !> Write a single hsd_value into a TOML table.
     601          258 :   subroutine write_hsd_value_to_toml(val, key, tt)
     602              :     type(hsd_value), intent(in) :: val
     603              :     character(len=*), intent(in) :: key
     604              :     type(toml_table), intent(inout) :: tt
     605              : 
     606          258 :     real(dp) :: re_part, im_part
     607              :     type(toml_table), pointer :: cpx_tt
     608          258 :     character(len=:), allocatable :: text
     609              : 
     610          258 :     select case (val%value_type)
     611              :     case (VALUE_TYPE_INTEGER)
     612            0 :       call set_value(tt, key, val%int_value)
     613              : 
     614              :     case (VALUE_TYPE_REAL)
     615            0 :       call set_value(tt, key, val%real_value)
     616              : 
     617              :     case (VALUE_TYPE_LOGICAL)
     618            0 :       call set_value(tt, key, val%logical_value)
     619              : 
     620              :     case (VALUE_TYPE_COMPLEX)
     621            0 :       re_part = real(val%complex_value, dp)
     622            0 :       im_part = aimag(val%complex_value)
     623            0 :       call get_value(tt, key, cpx_tt)
     624            0 :       cpx_tt%inline = .true.
     625            0 :       call set_value(cpx_tt, "re", re_part)
     626            0 :       call set_value(cpx_tt, "im", im_part)
     627              : 
     628              :     case (VALUE_TYPE_ARRAY)
     629           26 :       if (allocated(val%string_value)) then
     630           26 :         text = val%string_value
     631            0 :       else if (allocated(val%raw_text)) then
     632            0 :         text = val%raw_text
     633              :       else
     634            0 :         text = ""
     635              :       end if
     636           52 :       call write_array_text_to_toml(text, key, tt)
     637              : 
     638              :     case (VALUE_TYPE_STRING)
     639          464 :       if (allocated(val%string_value)) then
     640              :         ! Sniff for numeric/boolean strings that came from HSD
     641          319 :         if (looks_like_number(val%string_value)) then
     642           87 :           call write_numeric_string_to_toml(val%string_value, key, tt)
     643          145 :         else if (is_hsd_boolean(val%string_value)) then
     644           42 :           call set_value(tt, key, hsd_bool_to_logical(val%string_value))
     645              :         else
     646          103 :           call set_value(tt, key, val%string_value)
     647              :         end if
     648              :       else
     649            0 :         call set_value(tt, key, "")
     650              :       end if
     651              : 
     652              :     case (VALUE_TYPE_NONE)
     653            0 :       if (allocated(val%string_value)) then
     654            0 :         if (len(val%string_value) > 0) then
     655            0 :           call set_value(tt, key, val%string_value)
     656              :         else
     657            0 :           call set_value(tt, key, "")
     658              :         end if
     659              :       else
     660            0 :         call set_value(tt, key, "")
     661              :       end if
     662              : 
     663              :     case default
     664           26 :       if (allocated(val%string_value)) then
     665            0 :         call set_value(tt, key, val%string_value)
     666              :       else
     667            0 :         call set_value(tt, key, "")
     668              :       end if
     669              :     end select
     670              : 
     671          258 :   end subroutine write_hsd_value_to_toml
     672              : 
     673              :   !> Write array text (space-separated, possibly multi-line) as TOML array.
     674           26 :   subroutine write_array_text_to_toml(text, key, tt)
     675              :     character(len=*), intent(in) :: text
     676              :     character(len=*), intent(in) :: key
     677              :     type(toml_table), intent(inout) :: tt
     678              : 
     679              :     type(toml_array), pointer :: arr, sub_arr
     680           26 :     integer :: ii, line_start, line_end
     681           26 :     logical :: has_newlines, is_nl
     682              : 
     683           26 :     if (len_trim(text) == 0) then
     684              :       ! Empty array
     685            0 :       call add_array(tt, key, arr)
     686            0 :       return
     687              :     end if
     688              : 
     689              :     ! Check for multi-line (matrix)
     690           26 :     has_newlines = .false.
     691          649 :     do ii = 1, len(text)
     692          649 :       if (text(ii:ii) == new_line("a")) then
     693           16 :         has_newlines = .true.
     694           16 :         exit
     695              :       end if
     696              :     end do
     697              : 
     698           26 :     if (has_newlines) then
     699              :       ! Matrix: array of arrays
     700           16 :       call add_array(tt, key, arr)
     701           16 :       line_start = 1
     702         1220 :       do ii = 1, len(text) + 1
     703         1204 :         if (ii > len(text)) then
     704           16 :           is_nl = .true.
     705              :         else
     706         1188 :           is_nl = (text(ii:ii) == new_line("a"))
     707              :         end if
     708         1220 :         if (is_nl) then
     709           43 :           line_end = ii - 1
     710           43 :           if (line_start <= line_end .and. len_trim(text(line_start:line_end)) > 0) then
     711           43 :             call add_array(arr, sub_arr)
     712           43 :             call add_tokens_to_array(text(line_start:line_end), sub_arr)
     713              :           end if
     714           43 :           line_start = ii + 1
     715              :         end if
     716              :       end do
     717              :     else
     718              :       ! Flat array
     719           10 :       call add_array(tt, key, arr)
     720           10 :       call add_tokens_to_array(text, arr)
     721              :     end if
     722              : 
     723          284 :   end subroutine write_array_text_to_toml
     724              : 
     725              :   !> Add space-separated tokens to a TOML array.
     726           53 :   subroutine add_tokens_to_array(line, arr)
     727              :     character(len=*), intent(in) :: line
     728              :     type(toml_array), intent(inout), pointer :: arr
     729              : 
     730           53 :     integer :: ii, tok_start, tok_count, stat
     731           53 :     logical :: in_token, is_sep
     732           53 :     character(len=:), allocatable :: token
     733           53 :     integer :: int_val
     734           53 :     real(dp) :: real_val
     735              : 
     736           53 :     tok_count = 0
     737           53 :     in_token = .false.
     738           53 :     tok_start = 1
     739              : 
     740         1471 :     do ii = 1, len(line) + 1
     741         1418 :       if (ii > len(line)) then
     742           53 :         is_sep = .true.
     743              :       else
     744         2730 :         is_sep = (line(ii:ii) == " " .or. line(ii:ii) == achar(9) &
     745         4095 :             & .or. line(ii:ii) == ",")
     746              :       end if
     747              : 
     748         1471 :       if (is_sep) then
     749          193 :         if (in_token) then
     750          163 :           token = line(tok_start:ii - 1)
     751              :           ! Try integer first
     752          163 :           read(token, *, iostat=stat) int_val
     753          163 :           if (stat == 0 .and. is_pure_integer(token)) then
     754           36 :             call set_value(arr, len(arr) + 1, int_val)
     755              :           else
     756              :             ! Try real
     757          127 :             read(token, *, iostat=stat) real_val
     758          127 :             if (stat == 0 .and. looks_like_number(token)) then
     759          115 :               call set_value(arr, len(arr) + 1, real_val)
     760              :             else
     761              :               ! Store as string
     762           12 :               call set_value(arr, len(arr) + 1, token)
     763              :             end if
     764              :           end if
     765          163 :           tok_count = tok_count + 1
     766          163 :           in_token = .false.
     767              :         end if
     768              :       else
     769         1225 :         if (.not. in_token) then
     770          163 :           tok_start = ii
     771          163 :           in_token = .true.
     772              :         end if
     773              :       end if
     774              :     end do
     775              : 
     776           79 :   end subroutine add_tokens_to_array
     777              : 
     778              :   !> Write a numeric string to TOML as the appropriate type.
     779           87 :   subroutine write_numeric_string_to_toml(str, key, tt)
     780              :     character(len=*), intent(in) :: str
     781              :     character(len=*), intent(in) :: key
     782              :     type(toml_table), intent(inout) :: tt
     783              : 
     784           87 :     integer :: int_val, ios
     785           87 :     real(dp) :: real_val
     786              : 
     787              :     ! Try integer first
     788           87 :     if (is_pure_integer(str)) then
     789           30 :       read(str, *, iostat=ios) int_val
     790           30 :       if (ios == 0) then
     791           30 :         call set_value(tt, key, int_val)
     792           30 :         return
     793              :       end if
     794              :     end if
     795              : 
     796              :     ! Must be real
     797           57 :     read(str, *, iostat=ios) real_val
     798           57 :     if (ios == 0) then
     799           57 :       call set_value(tt, key, real_val)
     800              :     else
     801              :       ! Cannot parse — store as string
     802            0 :       call set_value(tt, key, str)
     803              :     end if
     804              : 
     805          140 :   end subroutine write_numeric_string_to_toml
     806              : 
     807              :   ! ---------------------------------------------------------------------------
     808              :   !  Utility routines
     809              :   ! ---------------------------------------------------------------------------
     810              : 
     811              :   !> Get the effective name of an HSD child node.
     812         1303 :   function get_hsd_child_name(node) result(name)
     813              :     class(hsd_node), intent(in) :: node
     814              :     character(len=:), allocatable :: name
     815              : 
     816              :     select type (node)
     817              :     type is (hsd_table)
     818          667 :       if (allocated(node%name)) then
     819          667 :         if (len_trim(node%name) > 0) then
     820          667 :           name = node%name
     821              :         else
     822            0 :           name = ANON_VALUE_KEY
     823              :         end if
     824              :       else
     825            0 :         name = ANON_VALUE_KEY
     826              :       end if
     827              :     type is (hsd_value)
     828          636 :       if (allocated(node%name)) then
     829          636 :         if (len_trim(node%name) > 0) then
     830          616 :           name = node%name
     831              :         else
     832           20 :           name = ANON_VALUE_KEY
     833              :         end if
     834              :       else
     835            0 :         name = ANON_VALUE_KEY
     836              :       end if
     837              :     class default
     838            0 :       name = ANON_VALUE_KEY
     839              :     end select
     840              : 
     841           87 :   end function get_hsd_child_name
     842              : 
     843              :   !> Check if key ends with __attrib.
     844          471 :   pure function is_attrib_key(key) result(is_attr)
     845              :     character(len=*), intent(in) :: key
     846              :     logical :: is_attr
     847              : 
     848          471 :     integer :: klen, slen
     849              : 
     850          471 :     is_attr = .false.
     851          471 :     klen = len(key)
     852          471 :     slen = len(ATTRIB_SUFFIX)
     853          250 :     if (klen <= slen) return
     854          221 :     is_attr = (key(klen - slen + 1:klen) == ATTRIB_SUFFIX)
     855              : 
     856          471 :   end function is_attrib_key
     857              : 
     858              :   !> Check if a TOML table is a complex number {re = ..., im = ...}.
     859          188 :   function is_complex_table(tt) result(is_cpx)
     860              :     type(toml_table), intent(inout), pointer :: tt
     861              :     logical :: is_cpx
     862              : 
     863          188 :     type(toml_key), allocatable :: keys(:)
     864          188 :     integer :: nkeys
     865              : 
     866          188 :     is_cpx = .false.
     867          188 :     call tt%get_keys(keys)
     868          188 :     nkeys = size(keys)
     869          188 :     if (nkeys /= 2) return
     870          100 :     if ((keys(1)%key == "re" .and. keys(2)%key == "im") .or. &
     871           50 :         & (keys(1)%key == "im" .and. keys(2)%key == "re")) then
     872            0 :       is_cpx = .true.
     873              :     end if
     874              : 
     875         1022 :   end function is_complex_table
     876              : 
     877              :   !> Look for a "key__attrib" sibling in the TOML table.
     878          425 :   subroutine get_attrib_from_toml(tt, key, attrib_val)
     879              :     type(toml_table), intent(inout) :: tt
     880              :     character(len=*), intent(in) :: key
     881              :     character(len=:), allocatable, intent(out) :: attrib_val
     882              : 
     883          425 :     integer :: stat
     884              : 
     885          425 :     call get_value(tt, key // ATTRIB_SUFFIX, attrib_val, stat=stat)
     886          425 :     if (stat /= toml_stat%success) then
     887          379 :       if (allocated(attrib_val)) deallocate(attrib_val)
     888              :     end if
     889              : 
     890          188 :   end subroutine get_attrib_from_toml
     891              : 
     892              :   !> Set attrib on the most recently added child of an hsd_table.
     893           46 :   subroutine set_last_child_attrib(ht, attrib_val)
     894              :     type(hsd_table), intent(inout) :: ht
     895              :     character(len=*), intent(in) :: attrib_val
     896              : 
     897            0 :     if (ht%num_children < 1) return
     898              : 
     899            0 :     select type (child => ht%children(ht%num_children)%node)
     900              :     type is (hsd_table)
     901            8 :       child%attrib = attrib_val
     902              :     type is (hsd_value)
     903           38 :       child%attrib = attrib_val
     904              :     end select
     905              : 
     906          471 :   end subroutine set_last_child_attrib
     907              : 
     908              :   !> Check if string looks like a number.
     909          359 :   pure function looks_like_number(str) result(is_num)
     910              :     character(len=*), intent(in) :: str
     911              :     logical :: is_num
     912              : 
     913          359 :     integer :: ii, slen
     914              : 
     915          359 :     is_num = .false.
     916          359 :     slen = len_trim(str)
     917            0 :     if (slen == 0) return
     918              : 
     919          359 :     ii = 1
     920          359 :     if (str(ii:ii) == "-" .or. str(ii:ii) == "+") then
     921            4 :       ii = ii + 1
     922            4 :       if (ii > slen) return
     923              :     end if
     924              : 
     925              :     ! Accept leading dot (e.g., ".2" from some compilers' g0 format)
     926          359 :     if (str(ii:ii) == ".") then
     927            0 :       ii = ii + 1
     928            0 :       if (ii > slen) return
     929              :       ! Must have at least one digit after the dot
     930            0 :       if (str(ii:ii) < "0" .or. str(ii:ii) > "9") return
     931            0 :       do while (ii <= slen)
     932            0 :         if (str(ii:ii) < "0" .or. str(ii:ii) > "9") exit
     933            0 :         ii = ii + 1
     934              :       end do
     935              :       ! Check for exponent part
     936            0 :       if (ii <= slen) then
     937            0 :         if (str(ii:ii) == "e" .or. str(ii:ii) == "E" &
     938            0 :             & .or. str(ii:ii) == "d" .or. str(ii:ii) == "D") then
     939            0 :           ii = ii + 1
     940            0 :           if (ii <= slen) then
     941            0 :             if (str(ii:ii) == "+" .or. str(ii:ii) == "-") ii = ii + 1
     942              :           end if
     943            0 :           if (ii > slen) return
     944            0 :           do while (ii <= slen)
     945            0 :             if (str(ii:ii) < "0" .or. str(ii:ii) > "9") exit
     946            0 :             ii = ii + 1
     947              :           end do
     948              :         end if
     949              :       end if
     950            0 :       is_num = (ii > slen)
     951            0 :       return
     952              :     end if
     953              : 
     954          359 :     if (str(ii:ii) < "0" .or. str(ii:ii) > "9") return
     955              : 
     956          578 :     do while (ii <= slen)
     957          548 :       if (str(ii:ii) < "0" .or. str(ii:ii) > "9") exit
     958          332 :       ii = ii + 1
     959              :     end do
     960              : 
     961          246 :     if (ii <= slen) then
     962          216 :       if (str(ii:ii) == ".") then
     963          184 :         ii = ii + 1
     964         1613 :         do while (ii <= slen)
     965         1449 :           if (str(ii:ii) < "0" .or. str(ii:ii) > "9") exit
     966         1429 :           ii = ii + 1
     967              :         end do
     968              :       end if
     969              :     end if
     970              : 
     971          246 :     if (ii <= slen) then
     972          104 :       if (str(ii:ii) == "e" .or. str(ii:ii) == "E" &
     973          156 :           & .or. str(ii:ii) == "d" .or. str(ii:ii) == "D") then
     974            8 :         ii = ii + 1
     975            8 :         if (ii <= slen) then
     976            8 :           if (str(ii:ii) == "+" .or. str(ii:ii) == "-") ii = ii + 1
     977              :         end if
     978            8 :         if (ii > slen) return
     979           16 :         do while (ii <= slen)
     980            8 :           if (str(ii:ii) < "0" .or. str(ii:ii) > "9") exit
     981            8 :           ii = ii + 1
     982              :         end do
     983              :       end if
     984              :     end if
     985              : 
     986          246 :     is_num = (ii > slen)
     987              : 
     988          405 :   end function looks_like_number
     989              : 
     990              :   !> Check if string is a pure integer (no decimal point or exponent).
     991          250 :   pure function is_pure_integer(str) result(is_int)
     992              :     character(len=*), intent(in) :: str
     993              :     logical :: is_int
     994              : 
     995          250 :     integer :: ii, slen
     996              : 
     997          250 :     is_int = .false.
     998          250 :     slen = len_trim(str)
     999            0 :     if (slen == 0) return
    1000              : 
    1001          250 :     ii = 1
    1002          250 :     if (str(ii:ii) == "-" .or. str(ii:ii) == "+") then
    1003            4 :       ii = ii + 1
    1004            4 :       if (ii > slen) return
    1005              :     end if
    1006              : 
    1007          250 :     if (str(ii:ii) < "0" .or. str(ii:ii) > "9") return
    1008              : 
    1009          562 :     do while (ii <= slen)
    1010          496 :       if (str(ii:ii) < "0" .or. str(ii:ii) > "9") return
    1011          324 :       ii = ii + 1
    1012              :     end do
    1013              : 
    1014           66 :     is_int = .true.
    1015              : 
    1016          609 :   end function is_pure_integer
    1017              : 
    1018              :   !> Check if string is an HSD boolean.
    1019          145 :   pure function is_hsd_boolean(str) result(is_bool)
    1020              :     character(len=*), intent(in) :: str
    1021              :     logical :: is_bool
    1022              : 
    1023          145 :     character(len=:), allocatable :: lower
    1024          145 :     integer :: ii, slen
    1025              : 
    1026          145 :     is_bool = .false.
    1027          145 :     slen = len_trim(str)
    1028            0 :     if (slen == 0) return
    1029              : 
    1030          145 :     allocate(character(len=slen) :: lower)
    1031         1881 :     do ii = 1, slen
    1032         1881 :       if (str(ii:ii) >= "A" .and. str(ii:ii) <= "Z") then
    1033           76 :         lower(ii:ii) = achar(iachar(str(ii:ii)) + 32)
    1034              :       else
    1035         1660 :         lower(ii:ii) = str(ii:ii)
    1036              :       end if
    1037              :     end do
    1038              : 
    1039              :     is_bool = (lower == "yes" .or. lower == "no" .or. lower == "true" &
    1040          145 :         & .or. lower == "false" .or. lower == ".true." .or. lower == ".false.")
    1041              : 
    1042          395 :   end function is_hsd_boolean
    1043              : 
    1044              :   !> Convert HSD boolean string to Fortran logical.
    1045           42 :   pure function hsd_bool_to_logical(str) result(val)
    1046              :     character(len=*), intent(in) :: str
    1047              :     logical :: val
    1048              : 
    1049           42 :     character(len=:), allocatable :: lower
    1050           42 :     integer :: ii, slen
    1051              : 
    1052           42 :     slen = len_trim(str)
    1053           42 :     allocate(character(len=slen) :: lower)
    1054          161 :     do ii = 1, slen
    1055          161 :       if (str(ii:ii) >= "A" .and. str(ii:ii) <= "Z") then
    1056           42 :         lower(ii:ii) = achar(iachar(str(ii:ii)) + 32)
    1057              :       else
    1058           77 :         lower(ii:ii) = str(ii:ii)
    1059              :       end if
    1060              :     end do
    1061              : 
    1062           42 :     val = (lower == "yes" .or. lower == "true" .or. lower == ".true.")
    1063              : 
    1064          187 :   end function hsd_bool_to_logical
    1065              : 
    1066              :   !> Convert integer to string.
    1067           64 :   function int_to_string(ival) result(str)
    1068              :     integer, intent(in) :: ival
    1069              :     character(len=:), allocatable :: str
    1070              : 
    1071              :     character(len=32) :: buf
    1072              : 
    1073           64 :     write(buf, "(i0)") ival
    1074           64 :     str = trim(adjustl(buf))
    1075              : 
    1076           42 :   end function int_to_string
    1077              : 
    1078              :   !> Convert real to string.
    1079          187 :   function real_to_string(rval) result(str)
    1080              :     real(dp), intent(in) :: rval
    1081              :     character(len=:), allocatable :: str
    1082              : 
    1083              :     character(len=64) :: buf
    1084              : 
    1085          187 :     write(buf, "(g0)") rval
    1086          187 :     str = trim(adjustl(buf))
    1087              : 
    1088           64 :   end function real_to_string
    1089              : 
    1090         3132 : end module hsd_data_toml
        

Generated by: LCOV version 2.0-1