LCOV - code coverage report
Current view: top level - src/io - hsd_lexer.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 94.8 % 248 235
Test Date: 2026-02-04 13:26:36 Functions: 83.3 % 12 10

            Line data    Source code
       1              : !> Lexer (tokenizer) for HSD files
       2              : !>
       3              : !> This module provides the lexer that converts a character stream into
       4              : !> a sequence of tokens for the HSD parser.
       5              : module hsd_lexer
       6              :   use hsd_constants, only: hsd_max_line_length, &
       7              :       & CHAR_TAB, CHAR_BACKSLASH, CHAR_SPACE, CHAR_NEWLINE, CHAR_LBRACE, &
       8              :       & CHAR_RBRACE, CHAR_LBRACKET, CHAR_RBRACKET, CHAR_EQUAL, CHAR_SEMICOLON, &
       9              :       & CHAR_HASH, CHAR_DQUOTE, CHAR_SQUOTE, CHAR_LESS
      10              :   use hsd_utils, only: to_lower, string_buffer_t
      11              :   use hsd_token, only: hsd_token_t, TOKEN_EOF, TOKEN_NAME, TOKEN_STRING, &
      12              :     TOKEN_LBRACE, TOKEN_RBRACE, TOKEN_EQUAL, TOKEN_LBRACKET, TOKEN_RBRACKET, &
      13              :     TOKEN_INCLUDE_TXT, TOKEN_INCLUDE_HSD, TOKEN_SEMICOLON, TOKEN_COMMENT, &
      14              :     TOKEN_NEWLINE, TOKEN_TEXT
      15              :   use hsd_error, only: hsd_error_t, make_error, &
      16              :     HSD_STAT_OK, HSD_STAT_IO_ERROR, HSD_STAT_UNCLOSED_QUOTE, HSD_STAT_UNCLOSED_ATTRIB, &
      17              :     HSD_STAT_FILE_NOT_FOUND
      18              :   implicit none (type, external)
      19              :   private
      20              : 
      21              :   public :: hsd_lexer_t, new_lexer_from_file, new_lexer_from_string
      22              : 
      23              :   !> Lexer state
      24              :   type :: hsd_lexer_t
      25              :     !> Source filename (for error reporting)
      26              :     character(len=:), allocatable :: filename
      27              :     !> Source content
      28              :     character(len=:), allocatable :: source
      29              :     !> Current position in source
      30              :     integer :: pos = 1
      31              :     !> Current line number (1-based)
      32              :     integer :: line = 1
      33              :     !> Current column number (1-based)
      34              :     integer :: column = 1
      35              :     !> Length of source
      36              :     integer :: source_len = 0
      37              :     !> Whether we're inside an attribute context
      38              :     logical :: in_attrib = .false.
      39              :     !> Whether we're inside a quoted string
      40              :     logical :: in_quote = .false.
      41              :     !> Quote character being used
      42              :     character(len=1) :: quote_char = ''
      43              :   contains
      44              :     procedure :: next_token => lexer_next_token
      45              :     procedure :: peek_char => lexer_peek_char
      46              :     procedure :: advance => lexer_advance
      47              :     procedure :: skip_whitespace => lexer_skip_whitespace
      48              :     procedure :: read_string => lexer_read_string
      49              :     procedure :: read_text => lexer_read_text
      50              :     procedure :: read_comment => lexer_read_comment
      51              :     procedure :: is_eof => lexer_is_eof
      52              :   end type hsd_lexer_t
      53              : 
      54              : contains
      55              : 
      56              :   !> Create a new lexer from a file
      57           72 :   subroutine new_lexer_from_file(lexer, filename, error)
      58              :     type(hsd_lexer_t), intent(out) :: lexer
      59              :     character(len=*), intent(in) :: filename
      60              :     type(hsd_error_t), allocatable, intent(out), optional :: error
      61              : 
      62           36 :     integer :: unit_num, io_stat
      63           36 :     integer :: file_size
      64              :     character(len=256) :: io_msg
      65           36 :     logical :: file_exists
      66              : 
      67              :     ! Check if file exists
      68           36 :     inquire(file=filename, exist=file_exists)
      69           36 :     if (.not. file_exists) then
      70            5 :       if (present(error)) then
      71              :         call make_error(error, HSD_STAT_FILE_NOT_FOUND, &
      72            5 :           "File not found: " // trim(filename), filename)
      73              :       end if
      74            5 :       return
      75              :     end if
      76              : 
      77              :     ! Get file size
      78           31 :     inquire(file=filename, size=file_size)
      79              : 
      80              :     ! Open and read file
      81              :     open(newunit=unit_num, file=filename, status='old', action='read', &
      82           31 :          access='stream', form='unformatted', iostat=io_stat, iomsg=io_msg)
      83           31 :     if (io_stat /= 0) then
      84            0 :       if (present(error)) then
      85            0 :         call make_error(error, HSD_STAT_IO_ERROR, trim(io_msg), filename)
      86              :       end if
      87            0 :       return
      88              :     end if
      89              : 
      90              :     ! Allocate and read content
      91           31 :     allocate(character(len=file_size) :: lexer%source)
      92           31 :     read(unit_num, iostat=io_stat) lexer%source
      93           31 :     close(unit_num)
      94              : 
      95           31 :     if (io_stat /= 0 .and. io_stat /= -1) then  ! -1 is EOF, which is okay
      96            0 :       if (present(error)) then
      97            0 :         call make_error(error, HSD_STAT_IO_ERROR, "Error reading file", filename)
      98              :       end if
      99            0 :       return
     100              :     end if
     101              : 
     102           31 :     lexer%filename = filename
     103           31 :     lexer%source_len = len(lexer%source)
     104           31 :     lexer%pos = 1
     105           31 :     lexer%line = 1
     106           31 :     lexer%column = 1
     107              : 
     108           36 :   end subroutine new_lexer_from_file
     109              : 
     110              :   !> Create a new lexer from a string
     111         3152 :   subroutine new_lexer_from_string(lexer, source, filename)
     112              :     type(hsd_lexer_t), intent(out) :: lexer
     113              :     character(len=*), intent(in) :: source
     114              :     character(len=*), intent(in), optional :: filename
     115              : 
     116         1576 :     lexer%source = source
     117         1576 :     lexer%source_len = len(source)
     118         1576 :     lexer%pos = 1
     119         1576 :     lexer%line = 1
     120         1576 :     lexer%column = 1
     121              : 
     122         1576 :     if (present(filename)) then
     123            4 :       lexer%filename = filename
     124              :     else
     125         1572 :       lexer%filename = "<string>"
     126              :     end if
     127              : 
     128           36 :   end subroutine new_lexer_from_string
     129              : 
     130              :   !> Check if lexer is at end of file
     131      1150815 :   pure function lexer_is_eof(self) result(is_eof)
     132              :     class(hsd_lexer_t), intent(in) :: self
     133              :     logical :: is_eof
     134      1150815 :     is_eof = self%pos > self%source_len
     135         1576 :   end function lexer_is_eof
     136              : 
     137              :   !> Peek at current character without advancing
     138      1154644 :   pure function lexer_peek_char(self, offset) result(ch)
     139              :     class(hsd_lexer_t), intent(in) :: self
     140              :     integer, intent(in), optional :: offset
     141              :     character(len=1) :: ch
     142      1154644 :     integer :: peek_pos
     143              : 
     144      1154644 :     if (present(offset)) then
     145           76 :       peek_pos = self%pos + offset
     146              :     else
     147      1154568 :       peek_pos = self%pos
     148              :     end if
     149              : 
     150      1154644 :     if (peek_pos > 0 .and. peek_pos <= self%source_len) then
     151      1154639 :       ch = self%source(peek_pos:peek_pos)
     152              :     else
     153            5 :       ch = char(0)  ! NUL for EOF
     154              :     end if
     155              : 
     156      1150815 :   end function lexer_peek_char
     157              : 
     158              :   !> Advance position by n characters
     159      1007455 :   subroutine lexer_advance(self, n)
     160              :     class(hsd_lexer_t), intent(inout) :: self
     161              :     integer, intent(in), optional :: n
     162              : 
     163      1007455 :     integer :: i, steps
     164              :     character(len=1) :: ch
     165              : 
     166      1007455 :     if (present(n)) then
     167           31 :       steps = n
     168              :     else
     169      1007424 :       steps = 1
     170              :     end if
     171              : 
     172      2014972 :     do i = 1, steps
     173      2014972 :       if (self%pos <= self%source_len) then
     174      1007517 :         ch = self%source(self%pos:self%pos)
     175      1007517 :         if (ch == CHAR_NEWLINE) then
     176        20328 :           self%line = self%line + 1
     177        20328 :           self%column = 1
     178              :         else
     179       987189 :           self%column = self%column + 1
     180              :         end if
     181      1007517 :         self%pos = self%pos + 1
     182              :       end if
     183              :     end do
     184              : 
     185      1154644 :   end subroutine lexer_advance
     186              : 
     187              :   !> Skip whitespace characters (not newlines)
     188        86405 :   subroutine lexer_skip_whitespace(self)
     189              :     class(hsd_lexer_t), intent(inout) :: self
     190              :     character(len=1) :: ch
     191              : 
     192       146986 :     do while (.not. self%is_eof())
     193       145407 :       ch = self%peek_char()
     194       145407 :       if (ch == CHAR_SPACE .or. ch == CHAR_TAB) then
     195        60581 :         call self%advance()
     196              :       else
     197        84826 :         exit
     198              :       end if
     199              :     end do
     200              : 
     201      1007455 :   end subroutine lexer_skip_whitespace
     202              : 
     203              :   !> Read a quoted string
     204         7112 :   subroutine lexer_read_string(self, token)
     205              :     class(hsd_lexer_t), intent(inout) :: self
     206              :     type(hsd_token_t), intent(out) :: token
     207              : 
     208              :     character(len=1) :: quote_char, ch
     209         7112 :     type(string_buffer_t) :: buf
     210         7112 :     integer :: start_line, start_col
     211         7112 :     logical :: escaped
     212              : 
     213         7112 :     start_line = self%line
     214         7112 :     start_col = self%column
     215         7112 :     quote_char = self%peek_char()
     216         7112 :     call self%advance()  ! Skip opening quote
     217              : 
     218         7112 :     call buf%init()
     219         7112 :     escaped = .false.
     220              : 
     221        32157 :     do while (.not. self%is_eof())
     222        32152 :       ch = self%peek_char()
     223              : 
     224        32152 :       if (escaped) then
     225              :         ! Handle escape sequences
     226            2 :         select case (ch)
     227              :         case ('n')
     228            2 :           call buf%append_char(CHAR_NEWLINE)
     229              :         case ('t')
     230            2 :           call buf%append_char(CHAR_TAB)
     231              :         case ('\')
     232            4 :           call buf%append_char(CHAR_BACKSLASH)
     233              :         case ('"')
     234            1 :           call buf%append_char(CHAR_DQUOTE)
     235              :         case ("'")
     236            0 :           call buf%append_char(CHAR_SQUOTE)
     237              :         case default
     238           10 :           call buf%append_char(ch)
     239              :         end select
     240           10 :         escaped = .false.
     241           10 :         call self%advance()
     242        32142 :       else if (ch == CHAR_BACKSLASH) then
     243           10 :         escaped = .true.
     244           10 :         call self%advance()
     245        32132 :       else if (ch == quote_char) then
     246         7107 :         call self%advance()  ! Skip closing quote
     247         7107 :         exit
     248              :       else
     249        25025 :         call buf%append_char(ch)
     250        25025 :         call self%advance()
     251              :       end if
     252              :     end do
     253              : 
     254         7112 :     token%kind = TOKEN_STRING
     255         7112 :     token%value = buf%get_string()
     256         7112 :     token%line = start_line
     257         7112 :     token%column = start_col
     258              : 
     259        93517 :   end subroutine lexer_read_string
     260              : 
     261              :   !> Read unquoted text (identifier or value)
     262        27698 :   subroutine lexer_read_text(self, token, stop_chars)
     263              :     class(hsd_lexer_t), intent(inout) :: self
     264              :     type(hsd_token_t), intent(out) :: token
     265              :     character(len=*), intent(in) :: stop_chars
     266              : 
     267              :     character(len=1) :: ch, prev_ch
     268        27698 :     type(string_buffer_t) :: buf
     269        27698 :     integer :: start_line, start_col
     270              : 
     271        27698 :     start_line = self%line
     272        27698 :     start_col = self%column
     273        27698 :     call buf%init()
     274        27698 :     prev_ch = ''
     275              : 
     276       875153 :     do while (.not. self%is_eof())
     277       874895 :       ch = self%peek_char()
     278              : 
     279              :       ! Check for escape
     280       874895 :       if (prev_ch == CHAR_BACKSLASH .and. prev_ch /= CHAR_BACKSLASH) then
     281            0 :         call buf%append_char(ch)
     282            0 :         prev_ch = ch
     283            0 :         call self%advance()
     284            0 :         cycle
     285              :       end if
     286              : 
     287              :       ! Check for stop characters
     288       874895 :       if (index(stop_chars, ch) > 0) then
     289        21276 :         exit
     290              :       end if
     291              : 
     292              :       ! Check for newline
     293       853619 :       if (ch == CHAR_NEWLINE .or. ch == char(13)) then
     294         6164 :         exit
     295              :       end if
     296              : 
     297       847455 :       call buf%append_char(ch)
     298       847455 :       prev_ch = ch
     299       847455 :       call self%advance()
     300              :     end do
     301              : 
     302        27698 :     token%kind = TOKEN_TEXT
     303        27698 :     token%value = buf%get_string()
     304        27698 :     token%line = start_line
     305        27698 :     token%column = start_col
     306              : 
     307        34810 :   end subroutine lexer_read_text
     308              : 
     309              :   !> Read a comment (from # to end of line)
     310           10 :   subroutine lexer_read_comment(self, token)
     311              :     class(hsd_lexer_t), intent(inout) :: self
     312              :     type(hsd_token_t), intent(out) :: token
     313              : 
     314              :     character(len=1) :: ch
     315           10 :     type(string_buffer_t) :: buf
     316           10 :     integer :: start_line, start_col
     317              : 
     318           10 :     start_line = self%line
     319           10 :     start_col = self%column
     320           10 :     call self%advance()  ! Skip #
     321              : 
     322           10 :     call buf%init()
     323        10145 :     do while (.not. self%is_eof())
     324        10139 :       ch = self%peek_char()
     325        10139 :       if (ch == CHAR_NEWLINE) then
     326            4 :         exit
     327              :       end if
     328        10135 :       call buf%append_char(ch)
     329        10135 :       call self%advance()
     330              :     end do
     331              : 
     332           10 :     token%kind = TOKEN_COMMENT
     333           10 :     token%value = buf%get_string()
     334           10 :     token%line = start_line
     335           10 :     token%column = start_col
     336              : 
     337        27708 :   end subroutine lexer_read_comment
     338              : 
     339              :   !> Get the next token from the source
     340       121191 :   subroutine lexer_next_token(self, token, in_attrib)
     341              :     class(hsd_lexer_t), intent(inout) :: self
     342              :     type(hsd_token_t), intent(out) :: token
     343              :     logical, intent(in), optional :: in_attrib
     344              : 
     345              :     character(len=1) :: ch, ch2, ch3
     346              :     character(len=*), parameter :: general_stop = "{}[]<=""'#;"
     347              :     character(len=*), parameter :: attrib_stop = "]""'"
     348        86374 :     character(len=:), allocatable :: stop_chars
     349        86374 :     logical :: inside_attrib
     350              : 
     351        86374 :     if (present(in_attrib)) then
     352            0 :       inside_attrib = in_attrib
     353              :     else
     354        86374 :       inside_attrib = self%in_attrib
     355              :     end if
     356              : 
     357        86374 :     if (inside_attrib) then
     358            0 :       stop_chars = attrib_stop
     359              :     else
     360        86374 :       stop_chars = general_stop
     361              :     end if
     362              : 
     363              :     ! Skip whitespace
     364        86374 :     call self%skip_whitespace()
     365              : 
     366              :     ! Check for EOF
     367        86374 :     if (self%is_eof()) then
     368         1577 :       token%kind = TOKEN_EOF
     369         1577 :       token%line = self%line
     370         1577 :       token%column = self%column
     371         1577 :       return
     372              :     end if
     373              : 
     374        84797 :     ch = self%peek_char()
     375              : 
     376              :     ! Single character tokens
     377        20326 :     select case (ch)
     378              :     case (CHAR_NEWLINE)
     379        20326 :       token%kind = TOKEN_NEWLINE
     380        20326 :       token%line = self%line
     381        20326 :       token%column = self%column
     382        20326 :       call self%advance()
     383        20326 :       return
     384              : 
     385              :     case (char(13))  ! Carriage return
     386            3 :       call self%advance()
     387            3 :       if (self%peek_char() == CHAR_NEWLINE) then
     388            5 :         call self%advance()
     389              :       end if
     390            3 :       token%kind = TOKEN_NEWLINE
     391            3 :       token%line = self%line
     392            3 :       token%column = self%column
     393            3 :       return
     394              : 
     395              :     case (CHAR_LBRACE)
     396         6414 :       token%kind = TOKEN_LBRACE
     397         6414 :       token%line = self%line
     398         6414 :       token%column = self%column
     399         6414 :       call self%advance()
     400         6414 :       return
     401              : 
     402              :     case (CHAR_RBRACE)
     403         6409 :       token%kind = TOKEN_RBRACE
     404         6409 :       token%line = self%line
     405         6409 :       token%column = self%column
     406         6409 :       call self%advance()
     407         6409 :       return
     408              : 
     409              :     case (CHAR_LBRACKET)
     410         1032 :       token%kind = TOKEN_LBRACKET
     411         1032 :       token%line = self%line
     412         1032 :       token%column = self%column
     413         1032 :       call self%advance()
     414         1032 :       return
     415              : 
     416              :     case (CHAR_RBRACKET)
     417         1031 :       token%kind = TOKEN_RBRACKET
     418         1031 :       token%line = self%line
     419         1031 :       token%column = self%column
     420         1031 :       call self%advance()
     421         1031 :       return
     422              : 
     423              :     case (CHAR_EQUAL)
     424        14711 :       token%kind = TOKEN_EQUAL
     425        14711 :       token%line = self%line
     426        14711 :       token%column = self%column
     427        14711 :       call self%advance()
     428        14711 :       return
     429              : 
     430              :     case (CHAR_SEMICOLON)
     431           45 :       token%kind = TOKEN_SEMICOLON
     432           45 :       token%line = self%line
     433           45 :       token%column = self%column
     434           45 :       call self%advance()
     435           45 :       return
     436              : 
     437              :     case (CHAR_HASH)
     438           10 :       call self%read_comment(token)
     439           10 :       return
     440              : 
     441              :     case (CHAR_DQUOTE, CHAR_SQUOTE)
     442         7091 :       call self%read_string(token)
     443         7091 :       return
     444              : 
     445              :     case (CHAR_LESS)
     446              :       ! Check for include directives
     447           37 :       ch2 = self%peek_char(1)
     448           37 :       ch3 = self%peek_char(2)
     449        84834 :       if (ch2 == CHAR_LESS .and. ch3 == CHAR_LESS) then
     450              :         ! <<< text include
     451            7 :         token%kind = TOKEN_INCLUDE_TXT
     452            7 :         token%line = self%line
     453            7 :         token%column = self%column
     454            7 :         call self%advance(3)
     455              :         ! Read the filename
     456            7 :         call self%skip_whitespace()
     457            7 :         if (self%peek_char() == CHAR_DQUOTE .or. self%peek_char() == CHAR_SQUOTE) then
     458            4 :           call self%read_string(token)
     459            4 :           token%kind = TOKEN_INCLUDE_TXT
     460              :         else
     461            3 :           call self%read_text(token, general_stop)
     462           10 :           token%kind = TOKEN_INCLUDE_TXT
     463              :         end if
     464            7 :         return
     465           30 :       else if (ch2 == CHAR_LESS .and. ch3 == '+') then
     466              :         ! <<+ HSD include
     467           24 :         token%kind = TOKEN_INCLUDE_HSD
     468           24 :         token%line = self%line
     469           24 :         token%column = self%column
     470           24 :         call self%advance(3)
     471              :         ! Read the filename
     472           24 :         call self%skip_whitespace()
     473           24 :         if (self%peek_char() == CHAR_DQUOTE .or. self%peek_char() == CHAR_SQUOTE) then
     474           17 :           call self%read_string(token)
     475           17 :           token%kind = TOKEN_INCLUDE_HSD
     476              :         else
     477            7 :           call self%read_text(token, general_stop)
     478           31 :           token%kind = TOKEN_INCLUDE_HSD
     479              :         end if
     480           24 :         return
     481              :       else
     482              :         ! Standalone '<' - treat as single-character text token
     483            6 :         token%kind = TOKEN_TEXT
     484            6 :         token%value = "<"
     485            6 :         token%line = self%line
     486            6 :         token%column = self%column
     487            6 :         call self%advance()
     488            6 :         return
     489              :       end if
     490              : 
     491              :     case default
     492              :       ! Fall through to read as text
     493              : 
     494              :     end select
     495              : 
     496              :     ! Default: read as text
     497        27688 :     call self%read_text(token, stop_chars)
     498              : 
     499        86384 :   end subroutine lexer_next_token
     500              : 
     501        86374 : end module hsd_lexer
        

Generated by: LCOV version 2.0-1