LCOV - code coverage report
Current view: top level - src - hsd_types.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.5 % 732 721
Test Date: 2026-02-04 13:26:36 Functions: 92.9 % 56 52

            Line data    Source code
       1              : !> Data types for HSD nodes
       2              : !>
       3              : !> This module provides the tree structure for representing parsed HSD data.
       4              : !> The main types are:
       5              : !> - hsd_node   - Abstract base type for all nodes
       6              : !> - hsd_table  - Table (container) node for nested structures
       7              : !> - hsd_value  - Value (leaf) node for scalar and array data
       8              : !> - hsd_iterator - Iterator for traversing table children
       9              : !>
      10              : !> ## Cache-on-Read Mutation Behavior
      11              : !>
      12              : !> **IMPORTANT:** Some "read" operations on `hsd_value` (such as `value_get_int_array`,
      13              : !> `value_get_real_array`, etc.) use `intent(inout)` and mutate the internal state by
      14              : !> caching parsed array results.
      15              : !>
      16              : !> - The first call parses the raw text and stores it in a cache (e.g., `self%int_array`).
      17              : !> - Subsequent calls return the cached array without reparsing.
      18              : !> - This means these logically read-only operations have side effects, requiring
      19              : !>   `intent(inout)`.
      20              : !>
      21              : !> ### Thread Safety Implications
      22              : !>
      23              : !> - **Not thread-safe for concurrent reads:** If multiple threads access the same
      24              : !>   `hsd_value` concurrently, a race may occur on first access (cache population).
      25              : !> - **Safe after first access:** Once populated, concurrent reads are safe (immutable).
      26              : !> - **Workaround:** If thread safety is required, populate caches in a single-threaded
      27              : !>   context before concurrent access, or use external synchronization.
      28              : !>
      29              : !> ### Rationale
      30              : !>
      31              : !> - This design avoids repeated parsing and improves performance for repeated access.
      32              : !> - Purely read-only (side-effect-free) variants could be added in the future if needed.
      33              : !>
      34              : !> See also: [AGENTS.md](../AGENTS.md) for design notes and thread safety summary.
      35              : !> ## Memory Ownership Semantics
      36              : !>
      37              : !> The HSD tree uses a **copy-on-add** ownership model:
      38              : !>
      39              : !> - **table_add_child**: Creates a deep copy of the node via `allocate(source=child)`.
      40              : !>   The caller retains ownership of the original node and is responsible for
      41              : !>   deallocating it. The table owns the copy and will deallocate it when the
      42              : !>   table is destroyed or the child is removed.
      43              : !>
      44              : !> - **table_get_child, table_get_child_by_name**: Return pointers to nodes owned
      45              : !>   by the table. These pointers become invalid if the child is removed or the
      46              : !>   table is destroyed. Do NOT deallocate returned pointers.
      47              : !>
      48              : !> - **table_remove_child**: Deallocates the removed node. Any pointers previously
      49              : !>   obtained via get_child become invalid.
      50              : !>
      51              : !> - **table_destroy**: Recursively deallocates all children. Must be called
      52              : !>   explicitly to avoid memory leaks (Fortran finalizers are not used).
      53              : !>
      54              : !> ### Example - Proper Memory Management
      55              : !>
      56              : !> ```fortran
      57              : !> type(hsd_table) :: root, child_table
      58              : !> type(hsd_value) :: val
      59              : !>
      60              : !> call new_table(root, "root")
      61              : !> call new_value(val, "key")
      62              : !> call val%set_string("value")
      63              : !> call root%add_child(val)  ! root now owns a COPY of val
      64              : !> ! val can be reused or will be cleaned up when it goes out of scope
      65              : !>
      66              : !> call new_table(child_table, "section")
      67              : !> call root%add_child(child_table)  ! root owns a COPY
      68              : !>
      69              : !> ! When done, destroy the root (also destroys all children):
      70              : !> call root%destroy()
      71              : !> ```
      72              : module hsd_types
      73              :   use hsd_constants, only: dp, sp
      74              :   use hsd_utils, only: to_lower
      75              :   use hsd_error, only: HSD_STAT_OK, HSD_STAT_TYPE_ERROR, HSD_STAT_NOT_FOUND
      76              :   use hsd_hash_table, only: hsd_name_index_t
      77              :   implicit none (type, external)
      78              :   private
      79              : 
      80              :   public :: hsd_node, hsd_table, hsd_value, hsd_node_ptr, hsd_iterator
      81              :   public :: new_table, new_value
      82              :   public :: VALUE_TYPE_NONE, VALUE_TYPE_STRING, VALUE_TYPE_INTEGER
      83              :   public :: VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_ARRAY
      84              :   public :: VALUE_TYPE_COMPLEX
      85              : 
      86              :   !> Value type enumeration
      87              :   integer, parameter :: VALUE_TYPE_NONE = 0
      88              :   integer, parameter :: VALUE_TYPE_STRING = 1
      89              :   integer, parameter :: VALUE_TYPE_INTEGER = 2
      90              :   integer, parameter :: VALUE_TYPE_REAL = 3
      91              :   integer, parameter :: VALUE_TYPE_LOGICAL = 4
      92              :   integer, parameter :: VALUE_TYPE_ARRAY = 5
      93              :   integer, parameter :: VALUE_TYPE_COMPLEX = 6
      94              : 
      95              :   !> Abstract base type for all HSD nodes
      96              :   type, abstract :: hsd_node
      97              :     !> Node name (tag name)
      98              :     character(len=:), allocatable :: name
      99              :     !> Optional attribute (e.g., unit)
     100              :     character(len=:), allocatable :: attrib
     101              :     !> Line number where this node was defined (for error messages)
     102              :     integer :: line = 0
     103              :   contains
     104              :     procedure :: has_attrib => node_has_attrib
     105              :     procedure :: get_attrib => node_get_attrib
     106              :     procedure(node_destroy), deferred :: destroy
     107              :   end type hsd_node
     108              : 
     109              :   abstract interface
     110              :     subroutine node_destroy(self)
     111              :       import :: hsd_node
     112              :       implicit none (type, external)
     113              :       class(hsd_node), intent(inout) :: self
     114              :     end subroutine node_destroy
     115              :   end interface
     116              : 
     117              :   !> Pointer wrapper for polymorphic node storage
     118              :   type :: hsd_node_ptr
     119              :     class(hsd_node), allocatable :: node
     120              :   end type hsd_node_ptr
     121              : 
     122              :   !> Iterator for traversing table children
     123              :   type :: hsd_iterator
     124              :     !> Reference to the table being iterated
     125              :     type(hsd_table), pointer :: table => null()
     126              :     !> Current position (0 = before first)
     127              :     integer :: pos = 0
     128              :   contains
     129              :     procedure :: init => iterator_init
     130              :     procedure :: next => iterator_next
     131              :     procedure :: reset => iterator_reset
     132              :     procedure :: has_next => iterator_has_next
     133              :   end type hsd_iterator
     134              : 
     135              :   !> Table node (container for child nodes)
     136              :   type, extends(hsd_node) :: hsd_table
     137              :     !> Child nodes
     138              :     type(hsd_node_ptr), allocatable :: children(:)
     139              :     !> Number of children
     140              :     integer :: num_children = 0
     141              :     !> Allocated capacity
     142              :     integer :: capacity = 0
     143              :     !> Optional hash index for O(1) child lookup (built when num_children >= threshold)
     144              :     type(hsd_name_index_t) :: name_index
     145              :     !> Whether the hash index is active
     146              :     logical :: index_active = .false.
     147              :   contains
     148              :     procedure :: build_index => table_build_index
     149              :     procedure :: invalidate_index => table_invalidate_index
     150              :     procedure :: add_child => table_add_child
     151              :     procedure :: get_child => table_get_child
     152              :     procedure :: get_child_by_name => table_get_child_by_name
     153              :     procedure :: has_child => table_has_child
     154              :     procedure :: num_children_func => table_num_children
     155              :     procedure :: remove_child => table_remove_child
     156              :     procedure :: remove_child_by_name => table_remove_child_by_name
     157              :     procedure :: destroy => table_destroy
     158              :     procedure :: get_keys => table_get_keys
     159              :   end type hsd_table
     160              : 
     161              :   !> Value node (leaf node with data)
     162              :   type, extends(hsd_node) :: hsd_value
     163              :     !> Type of value stored
     164              :     integer :: value_type = VALUE_TYPE_NONE
     165              :     !> String value
     166              :     character(len=:), allocatable :: string_value
     167              :     !> Integer value
     168              :     integer :: int_value = 0
     169              :     !> Real value
     170              :     real(dp) :: real_value = 0.0_dp
     171              :     !> Logical value
     172              :     logical :: logical_value = .false.
     173              :     !> Complex value
     174              :     complex(dp) :: complex_value = (0.0_dp, 0.0_dp)
     175              :     !> Complex array values
     176              :     complex(dp), allocatable :: complex_array(:)
     177              :     !> String array (for multi-value or matrix data)
     178              :     character(len=:), allocatable :: raw_text
     179              :     !> Integer array values
     180              :     integer, allocatable :: int_array(:)
     181              :     !> Real array values
     182              :     real(dp), allocatable :: real_array(:)
     183              :     !> Logical array values
     184              :     logical, allocatable :: logical_array(:)
     185              :     !> String array values
     186              :     character(len=:), allocatable :: string_array(:)
     187              :     !> 2D integer matrix
     188              :     integer, allocatable :: int_matrix(:,:)
     189              :     !> 2D real matrix
     190              :     real(dp), allocatable :: real_matrix(:,:)
     191              :     !> Number of rows (for matrix data)
     192              :     integer :: nrows = 0
     193              :     !> Number of columns (for matrix data)
     194              :     integer :: ncols = 0
     195              :   contains
     196              :     procedure :: set_string => value_set_string
     197              :     procedure :: set_integer => value_set_integer
     198              :     procedure :: set_real => value_set_real
     199              :     procedure :: set_logical => value_set_logical
     200              :     procedure :: set_complex => value_set_complex
     201              :     procedure :: set_raw => value_set_raw
     202              :     procedure :: get_string => value_get_string
     203              :     procedure :: get_integer => value_get_integer
     204              :     procedure :: get_real => value_get_real
     205              :     procedure :: get_logical => value_get_logical
     206              :     procedure :: get_complex => value_get_complex
     207              :     procedure :: get_int_array => value_get_int_array
     208              :     procedure :: get_real_array => value_get_real_array
     209              :     procedure :: get_logical_array => value_get_logical_array
     210              :     procedure :: get_string_array => value_get_string_array
     211              :     procedure :: get_complex_array => value_get_complex_array
     212              :     procedure :: get_int_matrix => value_get_int_matrix
     213              :     procedure :: get_real_matrix => value_get_real_matrix
     214              :     procedure :: destroy => value_destroy
     215              :   end type hsd_value
     216              : 
     217              : contains
     218              : 
     219              :   !> Check if node has an attribute
     220        21161 :   pure function node_has_attrib(self) result(has)
     221              :     class(hsd_node), intent(in) :: self
     222              :     logical :: has
     223        21161 :     has = allocated(self%attrib)
     224        42322 :   end function node_has_attrib
     225              : 
     226              :   !> Get node attribute (empty string if not set)
     227         1010 :   pure function node_get_attrib(self) result(attrib)
     228              :     class(hsd_node), intent(in) :: self
     229              :     character(len=:), allocatable :: attrib
     230         1010 :     if (allocated(self%attrib)) then
     231         1009 :       attrib = self%attrib
     232              :     else
     233            1 :       attrib = ""
     234              :     end if
     235        21161 :   end function node_get_attrib
     236              : 
     237              :   !> Create a new table
     238       162475 :   subroutine new_table(table, name, attrib, line)
     239              :     type(hsd_table), intent(out) :: table
     240              :     character(len=*), intent(in), optional :: name
     241              :     character(len=*), intent(in), optional :: attrib
     242              :     integer, intent(in), optional :: line
     243              : 
     244        78605 :     if (present(name)) table%name = name
     245        78605 :     if (present(attrib)) then
     246         9422 :       if (len_trim(attrib) > 0) table%attrib = attrib
     247              :     end if
     248        78605 :     if (present(line)) table%line = line
     249              : 
     250        78605 :     table%capacity = 8
     251       707445 :     allocate(table%children(table%capacity))
     252        78605 :     table%num_children = 0
     253              : 
     254         1010 :   end subroutine new_table
     255              : 
     256              :   !> Create a new value node
     257       975626 :   subroutine new_value(val, name, attrib, line)
     258              :     type(hsd_value), intent(out) :: val
     259              :     character(len=*), intent(in), optional :: name
     260              :     character(len=*), intent(in), optional :: attrib
     261              :     integer, intent(in), optional :: line
     262              : 
     263       487813 :     if (present(name)) val%name = name
     264       487813 :     if (present(attrib)) then
     265        11701 :       if (len_trim(attrib) > 0) val%attrib = attrib
     266              :     end if
     267       487813 :     if (present(line)) val%line = line
     268       487813 :     val%value_type = VALUE_TYPE_NONE
     269              : 
     270        78605 :   end subroutine new_value
     271              : 
     272              :   !> Build the hash index for O(1) child lookup
     273              :   !>
     274              :   !> This is called automatically when adding children.
     275              :   !> Can also be called explicitly to pre-build the index.
     276        78548 :   subroutine table_build_index(self)
     277              :     class(hsd_table), intent(inout) :: self
     278              : 
     279        78548 :     integer :: i
     280              : 
     281        78548 :     call self%name_index%init(self%num_children * 2)
     282              : 
     283       157100 :     do i = 1, self%num_children
     284       157100 :       if (allocated(self%children(i)%node)) then
     285        78552 :         if (allocated(self%children(i)%node%name)) then
     286        78538 :           call self%name_index%insert(self%children(i)%node%name, i)
     287              :         end if
     288              :       end if
     289              :     end do
     290              : 
     291        78548 :     self%index_active = .true.
     292              : 
     293       487813 :   end subroutine table_build_index
     294              : 
     295              :   !> Invalidate the hash index (called when children are removed)
     296            1 :   subroutine table_invalidate_index(self)
     297              :     class(hsd_table), intent(inout) :: self
     298              : 
     299            1 :     if (self%index_active) then
     300            1 :       call self%name_index%clear()
     301            1 :       self%index_active = .false.
     302              :     end if
     303              : 
     304        78548 :   end subroutine table_invalidate_index
     305              : 
     306              :   !> Add a child node to the table
     307              :   !>
     308              :   !> Creates a deep copy of the child node and adds it to the table.
     309              :   !> The table takes ownership of the copy and will deallocate it when
     310              :   !> the table is destroyed or the child is removed.
     311              :   !>
     312              :   !> @param[inout] self  The table to add the child to
     313              :   !> @param[in]    child The child node to copy and add
     314              :   !>
     315              :   !> @note The caller retains ownership of the original `child` argument.
     316              :   !>       The copy mechanism uses `allocate(source=child)` which performs
     317              :   !>       a deep copy of all components, including allocatable arrays.
     318              :   !>
     319              :   !> ## Performance
     320              :   !>
     321              :   !> Uses a hash index for O(1) name lookups.
     322       564274 :   subroutine table_add_child(self, child)
     323              :     class(hsd_table), intent(inout) :: self
     324              :     class(hsd_node), intent(in) :: child
     325              : 
     326       564274 :     type(hsd_node_ptr), allocatable :: tmp(:)
     327       564274 :     integer :: new_capacity
     328              : 
     329              :     ! Grow array if needed
     330       564274 :     if (self%num_children >= self%capacity) then
     331           46 :       new_capacity = self%capacity * 2
     332         6462 :       allocate(tmp(new_capacity))
     333         3254 :       tmp(1:self%num_children) = self%children(1:self%num_children)
     334         3300 :       call move_alloc(tmp, self%children)
     335           46 :       self%capacity = new_capacity
     336              :     end if
     337              : 
     338              :     ! Add child
     339       564274 :     self%num_children = self%num_children + 1
     340       564274 :     allocate(self%children(self%num_children)%node, source=child)
     341              : 
     342              :     ! Update hash index
     343       564274 :     if (.not. self%index_active) then
     344        78542 :       call self%build_index()
     345       485732 :     else if (allocated(child%name)) then
     346       485732 :       call self%name_index%insert(child%name, self%num_children)
     347              :     end if
     348              : 
     349       564275 :   end subroutine table_add_child
     350              : 
     351              :   !> Get child by index
     352              :   !>
     353              :   !> Returns a pointer to the child at the given index. The pointer is owned
     354              :   !> by the table - do NOT deallocate it. The pointer becomes invalid if the
     355              :   !> child is removed or the table is destroyed.
     356              :   !>
     357              :   !> @param[in]  self   The table to search
     358              :   !> @param[in]  index  1-based index of the child (1 to num_children)
     359              :   !> @param[out] child  Pointer to the child, or null() if index is out of range
     360       557358 :   subroutine table_get_child(self, index, child)
     361              :     class(hsd_table), intent(in), target :: self
     362              :     integer, intent(in) :: index
     363              :     class(hsd_node), pointer, intent(out) :: child
     364              : 
     365       557358 :     child => null()
     366       557358 :     if (index >= 1 .and. index <= self%num_children) then
     367       557357 :       if (allocated(self%children(index)%node)) then
     368       557357 :         child => self%children(index)%node
     369              :       end if
     370              :     end if
     371              : 
     372       564274 :   end subroutine table_get_child
     373              : 
     374              :   !> Get child by name
     375              :   !>
     376              :   !> Returns a pointer to the first child with the given name. The pointer is
     377              :   !> owned by the table - do NOT deallocate it. The pointer becomes invalid if
     378              :   !> the child is removed or the table is destroyed.
     379              :   !>
     380              :   !> @param[in]  self             The table to search
     381              :   !> @param[in]  name             Name to search for
     382              :   !> @param[out] child            Pointer to the child, or null() if not found
     383              :   !> @param[in]  case_insensitive If .true., ignore case when comparing names
     384              :   !>
     385              :   !> ## Performance
     386              :   !>
     387              :   !> Uses O(1) hash lookup for all table sizes.
     388      1811719 :   subroutine table_get_child_by_name(self, name, child, case_insensitive)
     389              :     class(hsd_table), intent(in), target :: self
     390              :     character(len=*), intent(in) :: name
     391              :     class(hsd_node), pointer, intent(out) :: child
     392              :     logical, intent(in), optional :: case_insensitive
     393              : 
     394      1811719 :     integer :: idx
     395      1811719 :     logical :: ignore_case, found
     396              : 
     397      1811719 :     child => null()
     398      1811719 :     ignore_case = .false.
     399       611697 :     if (present(case_insensitive)) ignore_case = case_insensitive
     400              : 
     401              :     ! Ensure index is built if there are children but index isn't active
     402              :     ! (Should normally be active if added via add_child, but safety first)
     403              :     if (.not. self%index_active .and. self%num_children > 0) then
     404              :       ! Deeply constant intent(in) self prevents calling build_index directly
     405              :       ! but we can use a select type or just assume it's active.
     406              :       ! Since this is intent(in), we can't build it here.
     407              :       ! Let's assume it IS active if num_children > 0.
     408              :     end if
     409              : 
     410      1811719 :     if (self%index_active) then
     411      1811687 :       if (ignore_case) then
     412       611666 :         idx = self%name_index%lookup_case_insensitive(name, found)
     413              :       else
     414      1200021 :         idx = self%name_index%lookup(name, found)
     415              :       end if
     416              : 
     417      1811687 :       if (found .and. idx >= 1 .and. idx <= self%num_children) then
     418      1810592 :         if (allocated(self%children(idx)%node)) then
     419      1810592 :           child => self%children(idx)%node
     420              :         end if
     421              :       end if
     422              :     end if
     423              : 
     424       557358 :   end subroutine table_get_child_by_name
     425              : 
     426              :   !> Check if table has a child with given name
     427           15 :   function table_has_child(self, name, case_insensitive) result(has)
     428              :     class(hsd_table), intent(in) :: self
     429              :     character(len=*), intent(in) :: name
     430              :     logical, intent(in), optional :: case_insensitive
     431              :     logical :: has
     432              : 
     433              :     class(hsd_node), pointer :: child
     434              : 
     435           15 :     call self%get_child_by_name(name, child, case_insensitive)
     436           15 :     has = associated(child)
     437              : 
     438      1811734 :   end function table_has_child
     439              : 
     440              :   !> Get number of children
     441            2 :   pure function table_num_children(self) result(n)
     442              :     class(hsd_table), intent(in) :: self
     443              :     integer :: n
     444            2 :     n = self%num_children
     445           15 :   end function table_num_children
     446              : 
     447              :   !> Get list of all child names
     448            9 :   subroutine table_get_keys(self, keys)
     449              :     class(hsd_table), intent(in) :: self
     450              :     character(len=:), allocatable, intent(out) :: keys(:)
     451              : 
     452            9 :     integer :: i, max_len
     453              : 
     454              :     ! Find maximum key length
     455            9 :     max_len = 0
     456           23 :     do i = 1, self%num_children
     457           23 :       if (allocated(self%children(i)%node)) then
     458           14 :         if (allocated(self%children(i)%node%name)) then
     459           12 :           max_len = max(max_len, len(self%children(i)%node%name))
     460              :         end if
     461              :       end if
     462              :     end do
     463              : 
     464              :     ! Allocate and fill keys
     465            9 :     if (max_len > 0) then
     466            6 :       allocate(character(len=max_len) :: keys(self%num_children))
     467           20 :       do i = 1, self%num_children
     468           20 :         if (allocated(self%children(i)%node)) then
     469           14 :           if (allocated(self%children(i)%node%name)) then
     470           12 :             keys(i) = self%children(i)%node%name
     471              :           else
     472            2 :             keys(i) = ""
     473              :           end if
     474              :         end if
     475              :       end do
     476              :     else
     477            3 :       allocate(character(len=1) :: keys(0))
     478              :     end if
     479              : 
     480            2 :   end subroutine table_get_keys
     481              : 
     482              :   !> Remove child at given index
     483              :   !>
     484              :   !> Removes and deallocates the child at the given index. Children after
     485              :   !> the removed one are shifted to fill the gap. Any pointers to the removed
     486              :   !> child (obtained via get_child) become invalid after this call.
     487              :   !>
     488              :   !> @param[inout] self   The table to modify
     489              :   !> @param[in]    index  1-based index of the child to remove
     490              :   !> @param[out]   stat   Optional status: HSD_STAT_OK on success,
     491              :   !>                      HSD_STAT_NOT_FOUND if index is out of range
     492           11 :   subroutine table_remove_child(self, index, stat)
     493              :     class(hsd_table), intent(inout) :: self
     494              :     integer, intent(in) :: index
     495              :     integer, intent(out), optional :: stat
     496              : 
     497           11 :     integer :: i
     498              : 
     499           11 :     if (index < 1 .or. index > self%num_children) then
     500            4 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     501            4 :       return
     502              :     end if
     503              : 
     504              :     ! Destroy the child node
     505            7 :     if (allocated(self%children(index)%node)) then
     506            7 :       call self%children(index)%node%destroy()
     507           14 :       deallocate(self%children(index)%node)
     508              :     end if
     509              : 
     510              :     ! Shift remaining children down
     511           14 :     do i = index, self%num_children - 1
     512           14 :       call move_alloc(self%children(i + 1)%node, self%children(i)%node)
     513              :     end do
     514              : 
     515            7 :     self%num_children = self%num_children - 1
     516              : 
     517              :     ! Rebuild index as indices have shifted
     518            7 :     if (self%num_children > 0) then
     519            6 :       call self%build_index()
     520              :     else
     521            1 :       call self%invalidate_index()
     522              :     end if
     523              : 
     524            7 :     if (present(stat)) stat = HSD_STAT_OK
     525              : 
     526           11 :   end subroutine table_remove_child
     527              : 
     528              :   !> Remove child by name
     529           10 :   subroutine table_remove_child_by_name(self, name, stat, case_insensitive)
     530              :     class(hsd_table), intent(inout) :: self
     531              :     character(len=*), intent(in) :: name
     532              :     integer, intent(out), optional :: stat
     533              :     logical, intent(in), optional :: case_insensitive
     534              : 
     535           10 :     integer :: idx
     536           10 :     logical :: ignore_case, found
     537              : 
     538           10 :     ignore_case = .false.
     539            1 :     if (present(case_insensitive)) ignore_case = case_insensitive
     540              : 
     541           10 :     if (self%index_active) then
     542           10 :       if (ignore_case) then
     543            1 :         idx = self%name_index%lookup_case_insensitive(name, found)
     544              :       else
     545            9 :         idx = self%name_index%lookup(name, found)
     546              :       end if
     547           10 :       if (found) then
     548            6 :         call self%remove_child(idx, stat)
     549            6 :         return
     550              :       end if
     551              :     end if
     552              : 
     553            4 :     if (present(stat)) stat = HSD_STAT_NOT_FOUND
     554              : 
     555           21 :   end subroutine table_remove_child_by_name
     556              : 
     557              :   !> Destroy table and all children
     558              :   !>
     559              :   !> Recursively deallocates all child nodes and frees all allocated memory.
     560              :   !> This must be called explicitly to avoid memory leaks - Fortran finalizers
     561              :   !> are not used for performance reasons. After calling destroy(), the table
     562              :   !> can be reused by calling new_table().
     563              :   !>
     564              :   !> @param[inout] self  The table to destroy
     565              :   !>
     566              :   !> @warning Any pointers to children obtained via get_child become invalid.
     567        78596 :   recursive subroutine table_destroy(self)
     568              :     class(hsd_table), intent(inout) :: self
     569        78596 :     integer :: i
     570              : 
     571              :     ! Destroy hash index
     572        78596 :     call self%name_index%destroy()
     573        78596 :     self%index_active = .false.
     574              : 
     575       642860 :     do i = 1, self%num_children
     576       642860 :       if (allocated(self%children(i)%node)) then
     577       564264 :         call self%children(i)%node%destroy()
     578      1128528 :         deallocate(self%children(i)%node)
     579              :       end if
     580              :     end do
     581              : 
     582       789159 :     if (allocated(self%children)) deallocate(self%children)
     583        78596 :     if (allocated(self%name)) deallocate(self%name)
     584        78596 :     if (allocated(self%attrib)) deallocate(self%attrib)
     585              : 
     586        78596 :     self%num_children = 0
     587        78596 :     self%capacity = 0
     588              : 
     589           10 :   end subroutine table_destroy
     590              : 
     591              :   !> Initialize iterator for a table
     592            7 :   subroutine iterator_init(self, table)
     593              :     class(hsd_iterator), intent(inout) :: self
     594              :     type(hsd_table), target, intent(in) :: table
     595              : 
     596            7 :     self%table => table
     597            7 :     self%pos = 0
     598              : 
     599            7 :   end subroutine iterator_init
     600              : 
     601              :   !> Advance to next child and return it
     602              :   !> Returns .false. if no more children
     603           22 :   function iterator_next(self, child) result(has_more)
     604              :     class(hsd_iterator), intent(inout) :: self
     605              :     class(hsd_node), pointer, intent(out) :: child
     606              :     logical :: has_more
     607              : 
     608           22 :     child => null()
     609           22 :     has_more = .false.
     610              : 
     611            1 :     if (.not. associated(self%table)) return
     612              : 
     613           21 :     self%pos = self%pos + 1
     614           21 :     if (self%pos <= self%table%num_children) then
     615           19 :       if (allocated(self%table%children(self%pos)%node)) then
     616           19 :         child => self%table%children(self%pos)%node
     617           19 :         has_more = .true.
     618              :       end if
     619              :     end if
     620              : 
     621           22 :   end function iterator_next
     622              : 
     623              :   !> Reset iterator to beginning
     624            3 :   subroutine iterator_reset(self)
     625              :     class(hsd_iterator), intent(inout) :: self
     626            3 :     self%pos = 0
     627           22 :   end subroutine iterator_reset
     628              : 
     629              :   !> Check if there are more children without advancing
     630           26 :   function iterator_has_next(self) result(has_more)
     631              :     class(hsd_iterator), intent(in) :: self
     632              :     logical :: has_more
     633              : 
     634           26 :     has_more = .false.
     635           26 :     if (associated(self%table)) then
     636           26 :       has_more = self%pos < self%table%num_children
     637              :     end if
     638              : 
     639           26 :   end function iterator_has_next
     640              : 
     641              :   !> Set string value
     642        10609 :   subroutine value_set_string(self, val)
     643              :     class(hsd_value), intent(inout) :: self
     644              :     character(len=*), intent(in) :: val
     645        10609 :     self%value_type = VALUE_TYPE_STRING
     646        10609 :     self%string_value = val
     647           26 :   end subroutine value_set_string
     648              : 
     649              :   !> Set integer value
     650         5854 :   subroutine value_set_integer(self, val)
     651              :     class(hsd_value), intent(inout) :: self
     652              :     integer, intent(in) :: val
     653         5854 :     self%value_type = VALUE_TYPE_INTEGER
     654         5854 :     self%int_value = val
     655        10609 :   end subroutine value_set_integer
     656              : 
     657              :   !> Set real value
     658           19 :   subroutine value_set_real(self, val)
     659              :     class(hsd_value), intent(inout) :: self
     660              :     real(dp), intent(in) :: val
     661           19 :     self%value_type = VALUE_TYPE_REAL
     662           19 :     self%real_value = val
     663         5873 :   end subroutine value_set_real
     664              : 
     665              :   !> Set logical value
     666            8 :   subroutine value_set_logical(self, val)
     667              :     class(hsd_value), intent(inout) :: self
     668              :     logical, intent(in) :: val
     669            8 :     self%value_type = VALUE_TYPE_LOGICAL
     670            8 :     self%logical_value = val
     671           27 :   end subroutine value_set_logical
     672              : 
     673              :   !> Set complex value
     674            3 :   subroutine value_set_complex(self, val)
     675              :     class(hsd_value), intent(inout) :: self
     676              :     complex(dp), intent(in) :: val
     677            3 :     self%value_type = VALUE_TYPE_COMPLEX
     678            3 :     self%complex_value = val
     679           11 :   end subroutine value_set_complex
     680              : 
     681              :   !> Set raw text (for arrays/matrices)
     682         1168 :   subroutine value_set_raw(self, text)
     683              :     class(hsd_value), intent(inout) :: self
     684              :     character(len=*), intent(in) :: text
     685         1168 :     self%value_type = VALUE_TYPE_STRING
     686         1168 :     self%raw_text = text
     687         1168 :     self%string_value = text
     688         2339 :   end subroutine value_set_raw
     689              : 
     690              :   !> Get string value
     691           55 :   subroutine value_get_string(self, val, stat)
     692              :     class(hsd_value), intent(in) :: self
     693              :     character(len=:), allocatable, intent(out) :: val
     694              :     integer, intent(out), optional :: stat
     695              : 
     696           55 :     if (allocated(self%string_value)) then
     697           52 :       val = self%string_value
     698           52 :       if (present(stat)) stat = HSD_STAT_OK
     699            3 :     else if (allocated(self%raw_text)) then
     700            2 :       val = self%raw_text
     701            2 :       if (present(stat)) stat = HSD_STAT_OK
     702              :     else
     703            1 :       val = ""
     704            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     705              :     end if
     706              : 
     707         1168 :   end subroutine value_get_string
     708              : 
     709              :   !> Get integer value
     710       100076 :   subroutine value_get_integer(self, val, stat)
     711              :     class(hsd_value), intent(in) :: self
     712              :     integer, intent(out) :: val
     713              :     integer, intent(out), optional :: stat
     714              : 
     715       100076 :     integer :: io_stat
     716              : 
     717       100076 :     if (self%value_type == VALUE_TYPE_INTEGER) then
     718            8 :       val = self%int_value
     719            8 :       if (present(stat)) stat = HSD_STAT_OK
     720       100068 :     else if (allocated(self%string_value)) then
     721       100067 :       read(self%string_value, *, iostat=io_stat) val
     722       100067 :       if (io_stat /= 0) then
     723            4 :         if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     724              :       else
     725       100063 :         if (present(stat)) stat = HSD_STAT_OK
     726              :       end if
     727              :     else
     728            1 :       val = 0
     729            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     730              :     end if
     731              : 
     732           55 :   end subroutine value_get_integer
     733              : 
     734              :   !> Get real value
     735           47 :   subroutine value_get_real(self, val, stat)
     736              :     class(hsd_value), intent(in) :: self
     737              :     real(dp), intent(out) :: val
     738              :     integer, intent(out), optional :: stat
     739              : 
     740           47 :     integer :: io_stat
     741              : 
     742           47 :     if (self%value_type == VALUE_TYPE_REAL) then
     743            5 :       val = self%real_value
     744            5 :       if (present(stat)) stat = HSD_STAT_OK
     745           42 :     else if (self%value_type == VALUE_TYPE_INTEGER) then
     746            1 :       val = real(self%int_value, dp)
     747            1 :       if (present(stat)) stat = HSD_STAT_OK
     748           41 :     else if (allocated(self%string_value)) then
     749           40 :       read(self%string_value, *, iostat=io_stat) val
     750           40 :       if (io_stat /= 0) then
     751            7 :         if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     752              :       else
     753           33 :         if (present(stat)) stat = HSD_STAT_OK
     754              :       end if
     755              :     else
     756            1 :       val = 0.0_dp
     757            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     758              :     end if
     759              : 
     760       100076 :   end subroutine value_get_real
     761              : 
     762              :   !> Get logical value
     763           26 :   subroutine value_get_logical(self, val, stat)
     764              :     class(hsd_value), intent(in) :: self
     765              :     logical, intent(out) :: val
     766              :     integer, intent(out), optional :: stat
     767              : 
     768           26 :     character(len=:), allocatable :: lower_val
     769              : 
     770           26 :     if (self%value_type == VALUE_TYPE_LOGICAL) then
     771            3 :       val = self%logical_value
     772            3 :       if (present(stat)) stat = HSD_STAT_OK
     773           23 :     else if (allocated(self%string_value)) then
     774           22 :       lower_val = to_lower(trim(self%string_value))
     775           12 :       select case (lower_val)
     776              :       case ("yes", "on", "1", "true", ".true.")
     777           12 :         val = .true.
     778           12 :         if (present(stat)) stat = HSD_STAT_OK
     779              :       case ("no", "off", "0", "false", ".false.")
     780            7 :         val = .false.
     781            7 :         if (present(stat)) stat = HSD_STAT_OK
     782              :       case default
     783            3 :         val = .false.
     784           22 :         if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     785              :       end select
     786              :     else
     787            1 :       val = .false.
     788            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     789              :     end if
     790              : 
     791           73 :   end subroutine value_get_logical
     792              : 
     793              :   !> Get complex value
     794              :   !> Parses formats like: 4.0+9.0i, 2.0-3.0i, (1.0,2.0), 5.0+2.0j
     795           46 :   subroutine value_get_complex(self, val, stat)
     796              :     class(hsd_value), intent(in) :: self
     797              :     complex(dp), intent(out) :: val
     798              :     integer, intent(out), optional :: stat
     799              : 
     800           46 :     if (self%value_type == VALUE_TYPE_COMPLEX) then
     801            1 :       val = self%complex_value
     802            1 :       if (present(stat)) stat = HSD_STAT_OK
     803           45 :     else if (allocated(self%string_value)) then
     804           44 :       call parse_complex(trim(self%string_value), val, stat)
     805              :     else
     806            1 :       val = (0.0_dp, 0.0_dp)
     807            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     808              :     end if
     809              : 
     810           26 :   end subroutine value_get_complex
     811              : 
     812              :   !> Get integer array from raw text (parses space/comma/newline separated values)
     813              :   !> Caches the parsed result for subsequent calls
     814           20 :   subroutine value_get_int_array(self, val, stat)
     815              :     class(hsd_value), intent(inout) :: self
     816              :     integer, allocatable, intent(out) :: val(:)
     817              :     integer, intent(out), optional :: stat
     818              : 
     819           20 :     character(len=:), allocatable :: text
     820           20 :     integer :: io_stat
     821              : 
     822              :     ! If already parsed, return cached array
     823           20 :     if (allocated(self%int_array)) then
     824            4 :       val = self%int_array
     825            1 :       if (present(stat)) stat = HSD_STAT_OK
     826            1 :       return
     827              :     end if
     828              : 
     829              :     ! Get source text
     830           19 :     if (allocated(self%raw_text)) then
     831            2 :       text = self%raw_text
     832           17 :     else if (allocated(self%string_value)) then
     833           15 :       text = self%string_value
     834              :     else
     835            2 :       allocate(val(0))
     836            2 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     837            2 :       return
     838              :     end if
     839              : 
     840              :     ! Count and parse values
     841           17 :     call parse_int_array(text, val, io_stat)
     842           17 :     if (present(stat)) stat = io_stat
     843              : 
     844              :     ! Cache result for next access
     845           17 :     if (io_stat == 0) then
     846         1577 :       self%int_array = val
     847              :     end if
     848              : 
     849           66 :   end subroutine value_get_int_array
     850              : 
     851              :   !> Get real array from raw text
     852              :   !> Caches the parsed result for subsequent calls
     853           19 :   subroutine value_get_real_array(self, val, stat)
     854              :     class(hsd_value), intent(inout) :: self
     855              :     real(dp), allocatable, intent(out) :: val(:)
     856              :     integer, intent(out), optional :: stat
     857              : 
     858           19 :     character(len=:), allocatable :: text
     859           19 :     integer :: io_stat
     860              : 
     861              :     ! If already parsed, return cached array
     862           19 :     if (allocated(self%real_array)) then
     863            4 :       val = self%real_array
     864            1 :       if (present(stat)) stat = HSD_STAT_OK
     865            1 :       return
     866              :     end if
     867              : 
     868              :     ! Get source text
     869           18 :     if (allocated(self%raw_text)) then
     870            5 :       text = self%raw_text
     871           13 :     else if (allocated(self%string_value)) then
     872           11 :       text = self%string_value
     873              :     else
     874            2 :       allocate(val(0))
     875            2 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     876            2 :       return
     877              :     end if
     878              : 
     879              :     ! Count and parse values
     880           16 :     call parse_real_array(text, val, io_stat)
     881           16 :     if (present(stat)) stat = io_stat
     882              : 
     883              :     ! Cache result for next access
     884           16 :     if (io_stat == 0) then
     885           60 :       self%real_array = val
     886              :     end if
     887              : 
     888           39 :   end subroutine value_get_real_array
     889              : 
     890              :   !> Get logical array from raw text
     891              :   !> Caches the parsed result for subsequent calls
     892            9 :   subroutine value_get_logical_array(self, val, stat)
     893              :     class(hsd_value), intent(inout) :: self
     894              :     logical, allocatable, intent(out) :: val(:)
     895              :     integer, intent(out), optional :: stat
     896              : 
     897            9 :     character(len=:), allocatable :: text, tokens(:)
     898            9 :     integer :: i, n
     899            9 :     logical :: parse_ok
     900              : 
     901            9 :     if (allocated(self%logical_array)) then
     902            3 :       val = self%logical_array
     903            1 :       if (present(stat)) stat = HSD_STAT_OK
     904            1 :       return
     905              :     end if
     906              : 
     907            8 :     if (allocated(self%raw_text)) then
     908            2 :       text = self%raw_text
     909            6 :     else if (allocated(self%string_value)) then
     910            5 :       text = self%string_value
     911              :     else
     912            1 :       allocate(val(0))
     913            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     914            1 :       return
     915              :     end if
     916              : 
     917            7 :     call tokenize_string(text, tokens)
     918            7 :     n = size(tokens)
     919            7 :     allocate(val(n))
     920            7 :     parse_ok = .true.
     921              : 
     922           31 :     do i = 1, n
     923           56 :       select case (to_lower(trim(tokens(i))))
     924              :       case ("yes", "on", "1", "true", ".true.")
     925           12 :         val(i) = .true.
     926              :       case ("no", "off", "0", "false", ".false.")
     927           12 :         val(i) = .false.
     928              :       case default
     929            1 :         val(i) = .false.
     930            1 :         parse_ok = .false.
     931            1 :         if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     932           51 :         return
     933              :       end select
     934              :     end do
     935              : 
     936            6 :     if (present(stat)) stat = HSD_STAT_OK
     937              : 
     938              :     ! Cache result for next access
     939            6 :     if (parse_ok) then
     940           28 :       self%logical_array = val
     941              :     end if
     942              : 
     943           35 :   end subroutine value_get_logical_array
     944              : 
     945              :   !> Get complex array from raw text (parses space/comma separated complex values)
     946              :   !> Caches the parsed result for subsequent calls
     947            9 :   subroutine value_get_complex_array(self, val, stat)
     948              :     class(hsd_value), intent(inout) :: self
     949              :     complex(dp), allocatable, intent(out) :: val(:)
     950              :     integer, intent(out), optional :: stat
     951              : 
     952            9 :     character(len=:), allocatable :: text
     953            9 :     integer :: io_stat
     954              : 
     955              :     ! If already parsed, return cached array
     956            9 :     if (allocated(self%complex_array)) then
     957            3 :       val = self%complex_array
     958            1 :       if (present(stat)) stat = HSD_STAT_OK
     959            1 :       return
     960              :     end if
     961              : 
     962              :     ! Get source text
     963            8 :     if (allocated(self%raw_text)) then
     964            3 :       text = self%raw_text
     965            5 :     else if (allocated(self%string_value)) then
     966            4 :       text = self%string_value
     967              :     else
     968            1 :       allocate(val(0))
     969            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     970            1 :       return
     971              :     end if
     972              : 
     973              :     ! Count and parse values
     974            7 :     call parse_complex_array(text, val, io_stat)
     975            7 :     if (present(stat)) stat = io_stat
     976              : 
     977              :     ! Cache result for next access
     978            7 :     if (io_stat == 0) then
     979           19 :       self%complex_array = val
     980              :     end if
     981              : 
     982           18 :   end subroutine value_get_complex_array
     983              : 
     984              :   !> Get string array from raw text (space-separated, quoted strings preserved)
     985              :   !> Caches the parsed result for subsequent calls
     986           24 :   subroutine value_get_string_array(self, val, stat)
     987              :     class(hsd_value), intent(inout) :: self
     988              :     character(len=:), allocatable, intent(out) :: val(:)
     989              :     integer, intent(out), optional :: stat
     990              : 
     991           24 :     character(len=:), allocatable :: text
     992              : 
     993           24 :     if (allocated(self%string_array)) then
     994            9 :       val = self%string_array
     995            5 :       if (present(stat)) stat = HSD_STAT_OK
     996            5 :       return
     997              :     end if
     998              : 
     999           19 :     if (allocated(self%raw_text)) then
    1000            1 :       text = self%raw_text
    1001           18 :     else if (allocated(self%string_value)) then
    1002           16 :       text = self%string_value
    1003              :     else
    1004            2 :       allocate(character(len=1) :: val(0))
    1005            2 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
    1006            2 :       return
    1007              :     end if
    1008              : 
    1009           17 :     call tokenize_quoted_string(text, val)
    1010           17 :     if (present(stat)) stat = HSD_STAT_OK
    1011              : 
    1012              :     ! Cache result for next access
    1013           80 :     self%string_array = val
    1014              : 
    1015           33 :   end subroutine value_get_string_array
    1016              : 
    1017              :   !> Get 2D integer matrix from raw text (rows separated by newlines or semicolons)
    1018              :   !> Caches the parsed result for subsequent calls
    1019           16 :   subroutine value_get_int_matrix(self, val, nrows, ncols, stat)
    1020              :     class(hsd_value), intent(inout) :: self
    1021              :     integer, allocatable, intent(out) :: val(:,:)
    1022              :     integer, intent(out) :: nrows, ncols
    1023              :     integer, intent(out), optional :: stat
    1024              : 
    1025           16 :     character(len=:), allocatable :: text
    1026           16 :     integer :: io_stat
    1027              : 
    1028           16 :     if (allocated(self%int_matrix)) then
    1029           10 :       val = self%int_matrix
    1030            1 :       nrows = self%nrows
    1031            1 :       ncols = self%ncols
    1032            1 :       if (present(stat)) stat = HSD_STAT_OK
    1033            1 :       return
    1034              :     end if
    1035              : 
    1036           15 :     if (allocated(self%raw_text)) then
    1037            7 :       text = self%raw_text
    1038            8 :     else if (allocated(self%string_value)) then
    1039            7 :       text = self%string_value
    1040              :     else
    1041            1 :       allocate(val(0,0))
    1042            1 :       nrows = 0
    1043            1 :       ncols = 0
    1044            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
    1045            1 :       return
    1046              :     end if
    1047              : 
    1048           14 :     call parse_int_matrix(text, val, nrows, ncols, io_stat)
    1049           14 :     if (present(stat)) stat = io_stat
    1050              : 
    1051              :     ! Cache result for next access
    1052           14 :     if (io_stat == 0) then
    1053          126 :       self%int_matrix = val
    1054           10 :       self%nrows = nrows
    1055           10 :       self%ncols = ncols
    1056              :     end if
    1057              : 
    1058           16 :   end subroutine value_get_int_matrix
    1059              : 
    1060              :   !> Get 2D real matrix from raw text
    1061              :   !> Caches the parsed result for subsequent calls
    1062           14 :   subroutine value_get_real_matrix(self, val, nrows, ncols, stat)
    1063              :     class(hsd_value), intent(inout) :: self
    1064              :     real(dp), allocatable, intent(out) :: val(:,:)
    1065              :     integer, intent(out) :: nrows, ncols
    1066              :     integer, intent(out), optional :: stat
    1067              : 
    1068           14 :     character(len=:), allocatable :: text
    1069           14 :     integer :: io_stat
    1070              : 
    1071           14 :     if (allocated(self%real_matrix)) then
    1072            7 :       val = self%real_matrix
    1073            1 :       nrows = self%nrows
    1074            1 :       ncols = self%ncols
    1075            1 :       if (present(stat)) stat = HSD_STAT_OK
    1076            1 :       return
    1077              :     end if
    1078              : 
    1079           13 :     if (allocated(self%raw_text)) then
    1080            7 :       text = self%raw_text
    1081            6 :     else if (allocated(self%string_value)) then
    1082            5 :       text = self%string_value
    1083              :     else
    1084            1 :       allocate(val(0,0))
    1085            1 :       nrows = 0
    1086            1 :       ncols = 0
    1087            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
    1088            1 :       return
    1089              :     end if
    1090              : 
    1091           12 :     call parse_real_matrix(text, val, nrows, ncols, io_stat)
    1092           12 :     if (present(stat)) stat = io_stat
    1093              : 
    1094              :     ! Cache result for next access
    1095           12 :     if (io_stat == 0) then
    1096           81 :       self%real_matrix = val
    1097            8 :       self%nrows = nrows
    1098            8 :       self%ncols = ncols
    1099              :     end if
    1100              : 
    1101           30 :   end subroutine value_get_real_matrix
    1102              : 
    1103              :   !> Parse space/comma-separated integers from text (dynamically sized)
    1104          111 :   subroutine parse_int_array(text, arr, stat)
    1105              :     character(len=*), intent(in) :: text
    1106              :     integer, allocatable, intent(out) :: arr(:)
    1107              :     integer, intent(out) :: stat
    1108              : 
    1109           33 :     character(len=:), allocatable :: tokens(:)
    1110           33 :     integer :: i, n, val, io_stat
    1111              : 
    1112            0 :     call tokenize_string(text, tokens)
    1113           33 :     n = size(tokens)
    1114              : 
    1115           33 :     allocate(arr(n))
    1116         1657 :     do i = 1, n
    1117         1630 :       read(tokens(i), *, iostat=io_stat) val
    1118         1630 :       if (io_stat /= 0) then
    1119            6 :         deallocate(arr)
    1120            6 :         allocate(arr(0))
    1121            6 :         stat = io_stat
    1122            6 :         return
    1123              :       end if
    1124         1651 :       arr(i) = val
    1125              :     end do
    1126              : 
    1127           27 :     stat = 0
    1128              : 
    1129           80 :   end subroutine parse_int_array
    1130              : 
    1131              :   !> Parse space/comma-separated reals from text (dynamically sized)
    1132           28 :   subroutine parse_real_array(text, arr, stat)
    1133              :     character(len=*), intent(in) :: text
    1134              :     real(dp), allocatable, intent(out) :: arr(:)
    1135              :     integer, intent(out) :: stat
    1136              : 
    1137           28 :     character(len=:), allocatable :: tokens(:)
    1138           28 :     integer :: i, n, io_stat
    1139           28 :     real(dp) :: val
    1140              : 
    1141            0 :     call tokenize_string(text, tokens)
    1142           28 :     n = size(tokens)
    1143              : 
    1144           28 :     allocate(arr(n))
    1145          114 :     do i = 1, n
    1146           93 :       read(tokens(i), *, iostat=io_stat) val
    1147           93 :       if (io_stat /= 0) then
    1148            7 :         deallocate(arr)
    1149            7 :         allocate(arr(0))
    1150            7 :         stat = io_stat
    1151            7 :         return
    1152              :       end if
    1153          107 :       arr(i) = val
    1154              :     end do
    1155              : 
    1156           21 :     stat = 0
    1157              : 
    1158           89 :   end subroutine parse_real_array
    1159              : 
    1160              :   !> Tokenize string by whitespace and commas
    1161          105 :   subroutine tokenize_string(text, tokens)
    1162              :     character(len=*), intent(in) :: text
    1163              :     character(len=:), allocatable, intent(out) :: tokens(:)
    1164              : 
    1165          105 :     integer :: i, start, max_len, token_count
    1166            0 :     character(len=len(text)) :: temp_tokens(len(text))
    1167          105 :     logical :: in_token
    1168              : 
    1169              :     ! First pass: count tokens and find max length
    1170          105 :     token_count = 0
    1171          105 :     max_len = 0
    1172          105 :     in_token = .false.
    1173          105 :     start = 1
    1174              : 
    1175         8148 :     do i = 1, len(text)
    1176         8148 :       if (is_separator(text(i:i))) then
    1177         1832 :         if (in_token) then
    1178         1809 :           token_count = token_count + 1
    1179         1809 :           max_len = max(max_len, i - start)
    1180         1809 :           temp_tokens(token_count) = text(start:i-1)
    1181         1809 :           in_token = .false.
    1182              :         end if
    1183              :       else
    1184         6211 :         if (.not. in_token) then
    1185         1906 :           start = i
    1186         1906 :           in_token = .true.
    1187              :         end if
    1188              :       end if
    1189              :     end do
    1190              : 
    1191              :     ! Handle last token
    1192          105 :     if (in_token) then
    1193           97 :       token_count = token_count + 1
    1194           97 :       max_len = max(max_len, len(text) - start + 1)
    1195           97 :       temp_tokens(token_count) = text(start:len(text))
    1196              :     end if
    1197              : 
    1198              :     ! Allocate and copy
    1199          105 :     if (token_count > 0 .and. max_len > 0) then
    1200          104 :       allocate(character(len=max_len) :: tokens(token_count))
    1201         2010 :       do i = 1, token_count
    1202         2010 :         tokens(i) = trim(temp_tokens(i))
    1203              :       end do
    1204              :     else
    1205            1 :       allocate(character(len=1) :: tokens(0))
    1206              :     end if
    1207              : 
    1208           28 :   end subroutine tokenize_string
    1209              : 
    1210              :   !> Check if character is a separator (whitespace, comma, semicolon)
    1211         8153 :   pure function is_separator(ch) result(is_sep)
    1212              :     character(len=1), intent(in) :: ch
    1213              :     logical :: is_sep
    1214              :     is_sep = (ch == ' ' .or. ch == char(9) .or. ch == char(10) .or. &
    1215         8153 :               ch == char(13) .or. ch == ',' .or. ch == ';')
    1216         8258 :   end function is_separator
    1217              : 
    1218              :   !> Tokenize string preserving quoted sections
    1219           17 :   subroutine tokenize_quoted_string(text, tokens)
    1220              :     character(len=*), intent(in) :: text
    1221              :     character(len=:), allocatable, intent(out) :: tokens(:)
    1222              : 
    1223           17 :     integer :: i, start, max_len, token_count, tlen
    1224            0 :     character(len=len(text)) :: temp_tokens(len(text))
    1225              :     character(len=1) :: quote_char
    1226           17 :     logical :: in_token, in_quote
    1227              : 
    1228           17 :     token_count = 0
    1229           17 :     max_len = 0
    1230           17 :     in_token = .false.
    1231           17 :     in_quote = .false.
    1232           17 :     quote_char = ' '
    1233           17 :     start = 1
    1234           17 :     tlen = len_trim(text)
    1235              : 
    1236           17 :     i = 1
    1237          143 :     do while (i <= tlen)
    1238          126 :       if (in_quote) then
    1239              :         ! Look for closing quote
    1240           13 :         if (text(i:i) == quote_char) then
    1241            3 :           token_count = token_count + 1
    1242            3 :           max_len = max(max_len, i - start - 1)
    1243            3 :           if (i > start + 1) then
    1244            2 :             temp_tokens(token_count) = text(start+1:i-1)
    1245              :           else
    1246            1 :             temp_tokens(token_count) = ""
    1247              :           end if
    1248            3 :           in_quote = .false.
    1249            3 :           in_token = .false.
    1250              :         end if
    1251          113 :       else if (text(i:i) == '"' .or. text(i:i) == "'") then
    1252            3 :         quote_char = text(i:i)
    1253            3 :         in_quote = .true.
    1254            3 :         start = i
    1255            3 :         in_token = .true.
    1256          110 :       else if (is_separator(text(i:i))) then
    1257           17 :         if (in_token) then
    1258           16 :           token_count = token_count + 1
    1259           16 :           max_len = max(max_len, i - start)
    1260           16 :           temp_tokens(token_count) = text(start:i-1)
    1261           16 :           in_token = .false.
    1262              :         end if
    1263              :       else
    1264           93 :         if (.not. in_token) then
    1265           27 :           start = i
    1266           27 :           in_token = .true.
    1267              :         end if
    1268              :       end if
    1269          126 :       i = i + 1
    1270              :     end do
    1271              : 
    1272              :     ! Handle last token
    1273           17 :     if (in_token .and. .not. in_quote) then
    1274           11 :       token_count = token_count + 1
    1275           11 :       max_len = max(max_len, tlen - start + 1)
    1276           11 :       temp_tokens(token_count) = text(start:tlen)
    1277              :     end if
    1278              : 
    1279              :     ! Allocate and copy
    1280           17 :     if (token_count > 0 .and. max_len > 0) then
    1281           12 :       allocate(character(len=max_len) :: tokens(token_count))
    1282           41 :       do i = 1, token_count
    1283           41 :         tokens(i) = trim(temp_tokens(i))
    1284              :       end do
    1285              :     else
    1286            5 :       allocate(character(len=1) :: tokens(0))
    1287              :     end if
    1288              : 
    1289         8153 :   end subroutine tokenize_quoted_string
    1290              : 
    1291              :   !> Parse 2D integer matrix (rows separated by newlines or semicolons)
    1292           14 :   subroutine parse_int_matrix(text, mat, nrows, ncols, stat)
    1293              :     character(len=*), intent(in) :: text
    1294              :     integer, allocatable, intent(out) :: mat(:,:)
    1295              :     integer, intent(out) :: nrows, ncols, stat
    1296              : 
    1297           14 :     character(len=:), allocatable :: rows(:), tokens(:)
    1298           14 :     integer, allocatable :: row_vals(:)
    1299           14 :     integer :: i, j, row_count, col_count, first_cols
    1300              : 
    1301              :     ! Split into rows by newlines
    1302            0 :     call split_by_newlines(text, rows)
    1303           14 :     row_count = size(rows)
    1304              : 
    1305              :     ! Count non-empty rows and determine column count
    1306           14 :     nrows = 0
    1307           14 :     ncols = 0
    1308           14 :     first_cols = -1
    1309              : 
    1310           33 :     do i = 1, row_count
    1311           33 :       if (len_trim(rows(i)) > 0) then
    1312           17 :         call tokenize_string(rows(i), tokens)
    1313           17 :         col_count = size(tokens)
    1314           17 :         if (col_count > 0) then
    1315           17 :           nrows = nrows + 1
    1316           17 :           if (first_cols < 0) then
    1317           13 :             first_cols = col_count
    1318           13 :             ncols = col_count
    1319            4 :           else if (col_count /= first_cols) then
    1320              :             ! Inconsistent column count - use max
    1321            2 :             ncols = max(ncols, col_count)
    1322              :           end if
    1323              :         end if
    1324              :       end if
    1325              :     end do
    1326              : 
    1327           14 :     if (nrows == 0 .or. ncols == 0) then
    1328            1 :       allocate(mat(0,0))
    1329            1 :       nrows = 0
    1330            1 :       ncols = 0
    1331            1 :       stat = 0
    1332            1 :       return
    1333              :     end if
    1334              : 
    1335           13 :     allocate(mat(nrows, ncols))
    1336          152 :     mat = 0
    1337              : 
    1338           13 :     j = 0
    1339           26 :     do i = 1, row_count
    1340           26 :       if (len_trim(rows(i)) > 0) then
    1341           16 :         call parse_int_array(rows(i), row_vals, stat)
    1342           16 :         if (stat /= 0) then
    1343            4 :           deallocate(mat)
    1344            4 :           allocate(mat(0,0))
    1345            4 :           nrows = 0
    1346            4 :           ncols = 0
    1347            4 :           return
    1348              :         end if
    1349           12 :         if (size(row_vals) > 0) then
    1350           12 :           j = j + 1
    1351           72 :           mat(j, 1:min(size(row_vals), ncols)) = row_vals(1:min(size(row_vals), ncols))
    1352              :         end if
    1353              :       end if
    1354              :     end do
    1355              : 
    1356            9 :     stat = 0
    1357              : 
    1358           45 :   end subroutine parse_int_matrix
    1359              : 
    1360              :   !> Parse 2D real matrix
    1361           12 :   subroutine parse_real_matrix(text, mat, nrows, ncols, stat)
    1362              :     character(len=*), intent(in) :: text
    1363              :     real(dp), allocatable, intent(out) :: mat(:,:)
    1364              :     integer, intent(out) :: nrows, ncols, stat
    1365              : 
    1366           12 :     character(len=:), allocatable :: rows(:), tokens(:)
    1367           12 :     real(dp), allocatable :: row_vals(:)
    1368           12 :     integer :: i, j, row_count, col_count, first_cols
    1369              : 
    1370            0 :     call split_by_newlines(text, rows)
    1371           12 :     row_count = size(rows)
    1372              : 
    1373           12 :     nrows = 0
    1374           12 :     ncols = 0
    1375           12 :     first_cols = -1
    1376              : 
    1377           26 :     do i = 1, row_count
    1378           26 :       if (len_trim(rows(i)) > 0) then
    1379           13 :         call tokenize_string(rows(i), tokens)
    1380           13 :         col_count = size(tokens)
    1381           13 :         if (col_count > 0) then
    1382           13 :           nrows = nrows + 1
    1383           13 :           if (first_cols < 0) then
    1384           11 :             first_cols = col_count
    1385           11 :             ncols = col_count
    1386            2 :           else if (col_count /= first_cols) then
    1387            1 :             ncols = max(ncols, col_count)
    1388              :           end if
    1389              :         end if
    1390              :       end if
    1391              :     end do
    1392              : 
    1393           12 :     if (nrows == 0 .or. ncols == 0) then
    1394            1 :       allocate(mat(0,0))
    1395            1 :       nrows = 0
    1396            1 :       ncols = 0
    1397            1 :       stat = 0
    1398            1 :       return
    1399              :     end if
    1400              : 
    1401           11 :     allocate(mat(nrows, ncols))
    1402          109 :     mat = 0.0_dp
    1403              : 
    1404           11 :     j = 0
    1405           19 :     do i = 1, row_count
    1406           19 :       if (len_trim(rows(i)) > 0) then
    1407           12 :         call parse_real_array(rows(i), row_vals, stat)
    1408           12 :         if (stat /= 0) then
    1409            4 :           deallocate(mat)
    1410            4 :           allocate(mat(0,0))
    1411            4 :           nrows = 0
    1412            4 :           ncols = 0
    1413            4 :           return
    1414              :         end if
    1415            8 :         if (size(row_vals) > 0) then
    1416            8 :           j = j + 1
    1417           45 :           mat(j, 1:min(size(row_vals), ncols)) = row_vals(1:min(size(row_vals), ncols))
    1418              :         end if
    1419              :       end if
    1420              :     end do
    1421              : 
    1422            7 :     stat = 0
    1423              : 
    1424           38 :   end subroutine parse_real_matrix
    1425              : 
    1426              :   !> Split text by newlines
    1427           26 :   subroutine split_by_newlines(text, lines)
    1428              :     character(len=*), intent(in) :: text
    1429              :     character(len=:), allocatable, intent(out) :: lines(:)
    1430              : 
    1431           26 :     integer :: i, start, line_count, max_len, tlen
    1432            0 :     character(len=len(text)) :: temp_lines(len(text))
    1433              : 
    1434           26 :     line_count = 0
    1435           26 :     max_len = 0
    1436           26 :     start = 1
    1437           26 :     tlen = len(text)
    1438              : 
    1439          381 :     do i = 1, tlen
    1440          381 :       if (text(i:i) == char(10) .or. text(i:i) == ';') then
    1441            7 :         line_count = line_count + 1
    1442            7 :         max_len = max(max_len, i - start)
    1443            7 :         if (i > start) then
    1444            6 :           temp_lines(line_count) = text(start:i-1)
    1445              :         else
    1446            1 :           temp_lines(line_count) = ""
    1447              :         end if
    1448            7 :         start = i + 1
    1449              :       end if
    1450              :     end do
    1451              : 
    1452              :     ! Handle last line
    1453           26 :     if (start <= tlen) then
    1454           25 :       line_count = line_count + 1
    1455           25 :       max_len = max(max_len, tlen - start + 1)
    1456           25 :       temp_lines(line_count) = text(start:tlen)
    1457              :     end if
    1458              : 
    1459           26 :     if (line_count > 0 .and. max_len > 0) then
    1460           25 :       allocate(character(len=max_len) :: lines(line_count))
    1461           57 :       do i = 1, line_count
    1462           57 :         lines(i) = trim(temp_lines(i))
    1463              :       end do
    1464              :     else
    1465            1 :       allocate(character(len=1) :: lines(1))
    1466            1 :       lines(1) = text
    1467              :     end if
    1468              : 
    1469           12 :   end subroutine split_by_newlines
    1470              : 
    1471              :   !> Destroy value
    1472       494543 :   subroutine value_destroy(self)
    1473              :     class(hsd_value), intent(inout) :: self
    1474              : 
    1475       494506 :     if (allocated(self%name)) deallocate(self%name)
    1476       494543 :     if (allocated(self%attrib)) deallocate(self%attrib)
    1477       494543 :     if (allocated(self%string_value)) deallocate(self%string_value)
    1478       494543 :     if (allocated(self%raw_text)) deallocate(self%raw_text)
    1479       494543 :     if (allocated(self%int_array)) deallocate(self%int_array)
    1480       494543 :     if (allocated(self%real_array)) deallocate(self%real_array)
    1481       494543 :     if (allocated(self%logical_array)) deallocate(self%logical_array)
    1482       494543 :     if (allocated(self%string_array)) deallocate(self%string_array)
    1483       494543 :     if (allocated(self%complex_array)) deallocate(self%complex_array)
    1484       494543 :     if (allocated(self%int_matrix)) deallocate(self%int_matrix)
    1485       494543 :     if (allocated(self%real_matrix)) deallocate(self%real_matrix)
    1486              : 
    1487       494543 :     self%value_type = VALUE_TYPE_NONE
    1488       494543 :     self%nrows = 0
    1489       494543 :     self%ncols = 0
    1490              : 
    1491           26 :   end subroutine value_destroy
    1492              : 
    1493              :   !> Parse a single complex number from string
    1494              :   !> Supports formats: 4.0+9.0i, 2.0-3.0i, (1.0,2.0), 5.0+2.0j, 3.5, pure imaginary 2.0i
    1495           61 :   subroutine parse_complex(str, val, stat)
    1496              :     character(len=*), intent(in) :: str
    1497              :     complex(dp), intent(out) :: val
    1498              :     integer, intent(out), optional :: stat
    1499              : 
    1500           61 :     character(len=:), allocatable :: work
    1501           61 :     integer :: i, sign_pos, io_stat
    1502           61 :     real(dp) :: re, im
    1503              :     character(len=1) :: ch
    1504              : 
    1505           61 :     work = adjustl(trim(str))
    1506              : 
    1507              :     ! Handle empty string
    1508           61 :     if (len_trim(work) == 0) then
    1509            1 :       val = (0.0_dp, 0.0_dp)
    1510            1 :       if (present(stat)) stat = HSD_STAT_TYPE_ERROR
    1511            1 :       return
    1512              :     end if
    1513              : 
    1514              :     ! Handle Fortran-style (re,im) format
    1515           60 :     if (work(1:1) == '(') then
    1516            7 :       i = index(work, ')')
    1517            7 :       if (i > 2) then
    1518            6 :         work = work(2:i-1)
    1519            6 :         i = index(work, ',')
    1520            6 :         if (i > 0) then
    1521            6 :           read(work(1:i-1), *, iostat=io_stat) re
    1522            6 :           if (io_stat /= 0) then
    1523            3 :             val = (0.0_dp, 0.0_dp)
    1524            3 :             if (present(stat)) stat = io_stat
    1525            3 :             return
    1526              :           end if
    1527            3 :           read(work(i+1:), *, iostat=io_stat) im
    1528            3 :           if (io_stat /= 0) then
    1529            1 :             val = (0.0_dp, 0.0_dp)
    1530            1 :             if (present(stat)) stat = io_stat
    1531            1 :             return
    1532              :           end if
    1533            2 :           val = cmplx(re, im, dp)
    1534            2 :           if (present(stat)) stat = HSD_STAT_OK
    1535            2 :           return
    1536              :         end if
    1537              :       end if
    1538              :     end if
    1539              : 
    1540              :     ! Handle a+bi or a-bi format (also handles j instead of i)
    1541              :     ! Find the + or - that separates real and imaginary parts
    1542              :     ! (must skip the first char and any exponent signs)
    1543           54 :     sign_pos = 0
    1544          545 :     do i = 2, len_trim(work)
    1545          491 :       ch = work(i:i)
    1546          545 :       if ((ch == '+' .or. ch == '-')) then
    1547              :         ! Make sure this isn't part of an exponent
    1548           39 :         if (i > 1) then
    1549              :           if (work(i-1:i-1) /= 'e' .and. work(i-1:i-1) /= 'E' .and. &
    1550           39 :               work(i-1:i-1) /= 'd' .and. work(i-1:i-1) /= 'D') then
    1551           39 :             sign_pos = i
    1552              :           end if
    1553              :         end if
    1554              :       end if
    1555              :     end do
    1556              : 
    1557              :     ! Check if last character is 'i' or 'j' (imaginary marker)
    1558           54 :     ch = work(len_trim(work):len_trim(work))
    1559           54 :     if (ch == 'i' .or. ch == 'I' .or. ch == 'j' .or. ch == 'J') then
    1560           47 :       if (sign_pos > 0) then
    1561              :         ! Format: a+bi or a-bi
    1562           38 :         read(work(1:sign_pos-1), *, iostat=io_stat) re
    1563           38 :         if (io_stat /= 0) then
    1564            2 :           val = (0.0_dp, 0.0_dp)
    1565            2 :           if (present(stat)) stat = io_stat
    1566            2 :           return
    1567              :         end if
    1568           36 :         read(work(sign_pos:len_trim(work)-1), *, iostat=io_stat) im
    1569           36 :         if (io_stat /= 0) then
    1570            2 :           val = (0.0_dp, 0.0_dp)
    1571            2 :           if (present(stat)) stat = io_stat
    1572            2 :           return
    1573              :         end if
    1574           34 :         val = cmplx(re, im, dp)
    1575           34 :         if (present(stat)) stat = HSD_STAT_OK
    1576           34 :         return
    1577              :       else
    1578              :         ! Pure imaginary: bi
    1579            9 :         read(work(1:len_trim(work)-1), *, iostat=io_stat) im
    1580            9 :         if (io_stat /= 0) then
    1581            0 :           val = (0.0_dp, 0.0_dp)
    1582            0 :           if (present(stat)) stat = io_stat
    1583            0 :           return
    1584              :         end if
    1585            9 :         val = cmplx(0.0_dp, im, dp)
    1586            9 :         if (present(stat)) stat = HSD_STAT_OK
    1587            9 :         return
    1588              :       end if
    1589              :     else
    1590              :       ! Pure real number
    1591            7 :       read(work, *, iostat=io_stat) re
    1592            7 :       if (io_stat /= 0) then
    1593            4 :         val = (0.0_dp, 0.0_dp)
    1594            4 :         if (present(stat)) stat = io_stat
    1595            4 :         return
    1596              :       end if
    1597            3 :       val = cmplx(re, 0.0_dp, dp)
    1598            3 :       if (present(stat)) stat = HSD_STAT_OK
    1599              :     end if
    1600              : 
    1601       494604 :   end subroutine parse_complex
    1602              : 
    1603              :   !> Parse an array of complex numbers from text
    1604            7 :   subroutine parse_complex_array(text, arr, stat)
    1605              :     character(len=*), intent(in) :: text
    1606              :     complex(dp), allocatable, intent(out) :: arr(:)
    1607              :     integer, intent(out) :: stat
    1608              : 
    1609            7 :     character(len=:), allocatable :: tokens(:)
    1610            7 :     integer :: i, n, io_stat
    1611            7 :     complex(dp) :: val
    1612              : 
    1613            0 :     call tokenize_string(text, tokens)
    1614            7 :     n = size(tokens)
    1615              : 
    1616            7 :     allocate(arr(n))
    1617           22 :     do i = 1, n
    1618           17 :       call parse_complex(tokens(i), val, io_stat)
    1619           17 :       if (io_stat /= 0) then
    1620            2 :         deallocate(arr)
    1621            2 :         allocate(arr(0))
    1622            2 :         stat = io_stat
    1623            2 :         return
    1624              :       end if
    1625           37 :       arr(i) = val
    1626              :     end do
    1627              : 
    1628            5 :     stat = 0
    1629              : 
    1630           75 :   end subroutine parse_complex_array
    1631              : 
    1632     17395784 : end module hsd_types
        

Generated by: LCOV version 2.0-1