8n8
8n8

Reputation: 1382

How to get string parser to terminate

I have a parser for double-quoted strings that mostly works fine, but when the end quote is missing it loops forever and crashes the app.

It is part of a web app written in Elm, and uses elm/parser.

It is based on an example from the Elm Github.

Here is a minimal example (run it in Ellie; change toParse to "\"" and it crashes the tab).

module Main exposing (main)

import Html
import Parser as P exposing ((|.), (|=))
import Debug


stringP : P.Parser String                                             
stringP =
    P.succeed identity 
        |. P.token "\""                                               
        |= P.loop [] stringHelp


stringHelp : List String -> P.Parser (P.Step (List String) String)    
stringHelp revChunks =                                                
    P.oneOf        
        [ P.token "\""                                                
            |> P.map (\_ -> P.Done (String.join "" (List.reverse revChunks)))  
        , P.chompWhile isUninteresting                                
            |> P.getChompedString                                     
            |> P.map (\chunk -> P.Loop (chunk :: revChunks))          
        ]                                                             


isUninteresting : Char -> Bool
isUninteresting char =
    char /= '\\' && char /= '"'


toParse =
    "\""


main =
    Html.text <| Debug.toString <| P.run stringP toParse

I can see what's wrong in a way - the chompWhile bit succeeds even if the end of input is reached. I need something like this, but can't quite work out how to do it in this case.

Upvotes: 4

Views: 228

Answers (2)

Jack Leow
Jack Leow

Reputation: 22497

Here's an alternative solution that checks for the end token if a string terminates prematurely (Ellie):

module Main exposing (main)

import Html
import Parser as P exposing ((|.), (|=))
import Debug


stringP : P.Parser String                                             
stringP =
    ( P.succeed identity 
        |. P.token "\""                                               
        |= P.loop [] stringHelp
    ) |> P.andThen
    ( \res ->
      case res of
        Ok str -> P.succeed str
        Err msg -> P.problem msg
    )
                                                                      
        
stringHelp : List String -> P.Parser (P.Step (List String) (Result String String))
stringHelp revChunks =                                                
    P.oneOf        
        [ P.end
            |> P.map (\_ -> P.Done (Err "string ended prematurely"))
        , P.token "\""                                                
            |> P.map (\_ -> P.Done (Ok (String.join "" (List.reverse revChunks))))
        , P.chompWhile isUninteresting                                
            |> P.getChompedString                                     
            |> P.map (\chunk -> P.Loop (chunk :: revChunks))          
        ]                                                             


isUninteresting : Char -> Bool
isUninteresting char =
    char /= '\\' && char /= '"'


toParse =
    "\"hi\""


main =
    Html.text <| Debug.toString <| P.run stringP toParse

Upvotes: 0

8n8
8n8

Reputation: 1382

I have found that a solution is to track the parsing position, and quit the loop if the position has not increased. It's not very pretty though; maybe someone will come up with something better.

The code is as the example in the question, but with these changes (new Ellie):

stringP : P.Parser String                                             
stringP =
    P.succeed identity 
        |. P.token "\""                                               
        |= P.loop ([], 0) stringHelp
        |. P.token "\""


stringHelp : (List String, Int) -> P.Parser (P.Step (List String, Int) String)
stringHelp (revChunks, offset) =
    P.succeed (stepHelp offset)
        |= stringHelp2 revChunks
        |= P.getOffset


stepHelp : Int -> (P.Step (List String) String) -> Int -> P.Step (List String, Int) String
stepHelp oldOffset step newOffset =
    case step of
        P.Done str ->
            P.Done str

        P.Loop revChunks ->
            if newOffset > oldOffset then                              
                P.Loop (revChunks, newOffset)
            else
                P.Done <| String.join "" <| List.reverse revChunks    



stringHelp2 : List String -> P.Parser (P.Step (List String) String)    
stringHelp2 revChunks =                                                
    P.chompWhile isUninteresting                                
        |> P.getChompedString                                     
        |> P.map (\chunk -> P.Loop (chunk :: revChunks))          

This ends with the desired parser error that there is a missing quote.

Upvotes: 3

Related Questions