String.Parser.Advanced
Functions for turning unstructured strings into structured data. This module extends String.Parser with custom errors and payloads, so you can better explain to end users what went wrong.
Parsers
An advanced Parser gives two ways to improve your error messages:
problem— Instead of all errors being aString, you can create a custom type liketype Problem = BadIndent | BadKeyword Stringand track problems much more precisely.payload— Sometimes you want to track information while you're parsing. This could be things like comments for when you want to format code, indentation levels, or what you're currently trying to parse. The payload gives you a place to store arbitrary data, should you need it.
I recommend starting with the simpler [String.Parser][String.Parser] module though, and when you feel comfortable and want better error messages, you can create a type alias like this:
import Parser.Advanced
type alias MyParser a =
Parser.Advanced.Parser Payload Problem a
type Payload = Definition String | List | Record
type Problem = BadIndent | BadKeyword String
All of the functions from String.Parser should exist in String.Parser.Advanced in some
form, allowing you to switch over pretty easily.
This works just like String.Parser.run, except it requires an intial payload value and will return more precise information for each dead end.
Say you are parsing a function named viewHealthData that contains a list.
You might get a DeadEnd like this:
{ row = 18
, col = 22
, problem = UnexpectedComma
, payload = List
}
We're using the payload to keep track of what we were parsing when the error
occurred. So in the error message, we can say that "I ran into an issue when
parsing a list. It looks like there is an extra comma." Or maybe something even
better!
By tracking the row and col where the problem occurred along with the payload,
we can provide helpful context about what the parser was doing. This is much
better than just marking where the problem manifested without any additional
information about the parsing state.
Note: Rows and columns are counted like a text editor. The beginning is row=1
and col=1. The col increments as characters are chomped. When a \n is chomped,
row is incremented and col starts over again at 1.
Payload
This allows you to extract the payload from the parser. You might use this during parsing to consult the current, or previous, indentation level. Maybe you want the payload at the end of parsing to report some interesting statistics, or to include some optional constructs in certain cases (like comments while code formatting).
intWithPayload : Parser Payload error { int : Int, payload : Payload } intWithPayload = succeed (\integer payload -> { int = integer, payload = payload }) |> keep int |> keep getPayload
This is how you set the payload. For example, here is a rough outline of some code
that uses setPayload to mark when you are parsing a specific definition:
type Payload
= Definition String
| List
definition : Parser Payload Problem Expr
definition =
functionName
|> andThen definitionBody
definitionBody : String -> Parser Payload Problem Expr
definitionBody name =
setPayload (Definition name) <|
succeed (Function name)
|> keep arguments
|> skip (token "=" ExpectingEquals)
|> keep expression
functionName : Parser c Problem String
functionName =
variable
{ start = Char.isLower
, inner = Char.isAlphaNum
, expecting = ExpectingFunctionName
}
First we parse the function name, and then we parse the rest of the definition.
Importantly, we call setPayload so that any dead end that occurs in
definitionBody will get this extra payload information. That way you can say
things like, "I was expecting an equals sign in the view definition".
Keep in mind that the payload will be changed for the entire duration of
parsing, and not just for this particular parser. Take a look at scopedPayloadUpdate
for more info.
Like setPayload, but with the option of modifying a subset of the payload
instead of replacing it entirely. This might be helpful when you're using the
payload to keep track of multiple things.
Like setPayload, the payload changes aren't automaticly reverted.
This works like updatePayload, except it allows you to revert the payload back to it's original state when a parser is done. Effectively, it allows you to scope a payload value to a specific parser.
definitionBody : String -> Parser Payload Problem Expr
definitionBody name =
setPayload (Definition name) <|
succeed (Function name)
|> keep arguments
|> skip (token "=" ExpectingEquals)
|> keep
( Parser.scopedUpdatePayload
(\payload -> payload.indentLevel + 1)
.indentLevel
(\nextValue payload -> { payload | indentLevel = nextValue })
expression
)
In the above expression, the indentLevel is increased for parsing the expression, but
reverted to its original value after.
Error formatting
Render an array of DeadEnds.
The String should be the same that was passed to the failing parser.
This returns a list of renderable "pieces", explaining what went wrong and where.
Describes how to output the various parts of the error message.
Describes how to get the context stack from a DeadEnd and how to extract expectations
information from a problem.
A problem is often of the form "expected something". This type is used to group those
together.
Everything past here works just like in the
String.Parser module, except that
you need to provide a Problem for certain scenarios.
Building Blocks
Just like String.Parser.int where you have to handle negation yourself. The only difference is that you provide a problem in case the parser fails.
Just like String.Parser.token except you provide a problem in case the parser fails.
Just like String.Parser.keyword except you provide a problem in case the parser fails:
let_ : Parser Payload Problem {}
let_ =
keyword "let" ExpectingLet
Note: this would fail to chomp letter because of the subsequent
characters. Use token if you do not want that last letter check.
Just like String.Parser.variable except you specify the problem yourself.
Just like String.Parser.end except you provide the problem that arises when the parser is not at the end of the input.
Pipelines
Just like String.Parser.succeed
Just like keep from the String.Parser module.
Just like skip from the String.Parser module.
Just like String.Parser.lazy
Just like String.Parser.andThen
Just like String.Parser.problem except you provide a custom type for your problem.
Branches
Just like String.Parser.oneOf
Just like String.Parser.map
Transform the Error of a parser.
Just like String.Parser.backtrackable
Just like String.Parser.commit
Loops
Just like String.Parser.sequence except with Token records for
the start, separator, and end. That way you can specify your custom type of
problem for when something is not found.
Contains a string that we expect to chomp and the problem we'll return in case the parser fails.
What’s the deal with trailing commas? Are they Forbidden?
Are they Optional? Are they Mandatory? Welcome to shapes
club!
Just like String.Parser.loop
Just like String.Parser.Step
Whitespace
Just like String.Parser.spaces
Just like String.Parser.lineComment except you provide a problem in case the parser fails.
Just like String.Parser.multiComment except with a
Token for the open and close symbols.
Works just like String.Parser.Nestable to help distinguish between unnestable and nestable comments.
Chompers
Just like String.Parser.getChompedString
Just like String.Parser.chompIf except you provide a problem in case a character cannot be chomped.
Just like String.Parser.chompChar except you provide a problem in case a character cannot be chomped.
Just like String.Parser.chompWhile
Just like String.Parser.chompUntil except you provide the problem in case you chomp all the way to the end of the input without finding what you need.
Just like String.Parser.chompUntilEndOr
Just like Parser.mapChompedString
Positions
Just like String.Parser.getPosition
Just like String.Parser.getRow
Just like String.Parser.getCol
Just like String.Parser.getOffset
Just like String.Parser.getSource