diff --git a/.gitignore b/.gitignore index 94ab26f..70c3fca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /elm-stuff elm.js +examples/elm-stuff diff --git a/.travis.yml b/.travis.yml index 2a2acc0..3bbbd40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: make && make install; cd ..; fi - - npm install -g elm elm-test@0.18.7 + - npm install -g elm elm-test script: - $TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8bb2d..e4acac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,43 @@ # Changelog +## Version 3.0.2 (2019-01-09) + +I hope, it is possible to transfer this back to the community repo. This is a +simple port of the community/parser-combinators package to elm 0.19. +Unfortunatelly all operators had to be removed or in other words replaced. And +lazyness is not supported anymore, these things can only be used by native elm +modules :.(. + +**Basic changes:** + +* `*>` was replaced by the new function `ignore` +* `<*` was replaced by the function `keep` +* `$>` is now function `onsuccess` +* while `` is represented by the function `onerror` +* `lazy` can still be used, but in the manner, that it was replaced by the + function: + + ``` elm + lazy : (() -> Parser s a) -> Parser s a + lazy t = + succeed () |> andThen t + ``` + + which tries to circumvent the bad-recursion problem + +* Examples and test have been updated and a new the new function `modifyStream` + was added, which I personally use to inject code/macros at compile-time. +* Some minor optimization of regular expression ... + ## Version 2.0.0 (2017-12-05) -No API change, but a major change. The internal implementation how locations (line/columns) are working has changed. -Lines were previously 1-based, and columns sometimes had negative values. This is changed into zero-based lines and columns can never have negative values anymore. +No API change, but a major change. The internal implementation how locations +(line/columns) are working has changed. Lines were previously 1-based, and +columns sometimes had negative values. This is changed into zero-based lines and +columns can never have negative values anymore. -If your application/library did not rely on parse locations, the update is seamless. +If your application/library did not rely on parse locations, the update is +seamless. ## Version 1.0.0 (2017-02-09) @@ -13,7 +45,9 @@ If your application/library did not rely on parse locations, the update is seaml --- -> This repository is transferred from [Bogdanp/elm-combine](github.com/Bogdanp/elm-combine). The following changelog statements originate from that repository. +> This repository is transferred from +> [Bogdanp/elm-combine](github.com/Bogdanp/elm-combine). +> The following changelog statements originate from that repository. --- diff --git a/elm-package.json b/elm-package.json deleted file mode 100644 index 18e676f..0000000 --- a/elm-package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "2.0.0", - "summary": "A parser combinator library", - "repository": "https://github.com/elm-community/parser-combinators.git", - "license": "BSD3", - "source-directories": [ - "examples", - "src" - ], - "exposed-modules": [ - "Combine", - "Combine.Char", - "Combine.Num" - ], - "dependencies": { - "elm-lang/core": "5.0.0 <= v < 6.0.0", - "elm-lang/lazy": "2.0.0 <= v < 3.0.0" - }, - "elm-version": "0.18.0 <= v < 0.19.0" -} diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..394b748 --- /dev/null +++ b/elm.json @@ -0,0 +1,21 @@ +{ + "type": "package", + "name": "andre-dietrich/parser-combinators", + "summary": "Port of the community parser combinator to elm 0.19", + "license": "BSD-3-Clause", + "version": "7.0.2", + "exposed-modules": [ + "Combine", + "Combine.Char", + "Combine.Num" + ], + "elm-version": "0.19.0 <= v < 0.20.0", + "dependencies": { + "elm/core": "1.0.1 <= v < 2.0.0", + "elm/regex": "1.0.0 <= v < 2.0.0", + "pilatch/flip": "1.0.0 <= v < 2.0.0" + }, + "test-dependencies": { + "elm-explorations/test": "1.2.0 <= v < 2.0.0" + } +} diff --git a/examples/Calc.elm b/examples/Calc.elm index 992137c..7c7da3d 100644 --- a/examples/Calc.elm +++ b/examples/Calc.elm @@ -2,7 +2,14 @@ module Calc exposing (calc) {-| An example parser that computes arithmetic expressions. +To run this example, simply enter the examples and: + +1. run `elm repl` +2. type in `import Calc exposing (calc)` +3. try out some expressions like `calc "22*2+(3+18)"` + @docs calc + -} import Combine exposing (..) @@ -12,16 +19,16 @@ import Combine.Num exposing (int) addop : Parser s (Int -> Int -> Int) addop = choice - [ (+) <$ string "+" - , (-) <$ string "-" + [ string "+" |> onsuccess (+) + , string "-" |> onsuccess (-) ] mulop : Parser s (Int -> Int -> Int) mulop = choice - [ (*) <$ string "*" - , (//) <$ string "/" + [ string "*" |> onsuccess (*) + , string "/" |> onsuccess (//) ] @@ -31,7 +38,7 @@ expr = go () = chainl addop term in - lazy go + lazy go term : Parser s Int @@ -40,21 +47,44 @@ term = go () = chainl mulop factor in - lazy go + lazy go factor : Parser s Int factor = - whitespace *> (parens expr <|> int) <* whitespace + whitespace + |> keep (or (parens expr) int) + |> ignore whitespace {-| Compute the result of an expression. + + import Calc exposing (calc) + + calc "22*2+(3+18)" + -- Ok 65 + + calc "33 ** 12" + -- Err ("parse error: [ \"expected end of input\" ] , { data: 33 ** 12, input: ** 12, position: 3 }") + -} calc : String -> Result String Int calc s = - case parse (expr <* end) s of + case parse (expr |> ignore end) s of Ok ( _, _, n ) -> Ok n Err ( _, stream, ms ) -> - Err ("parse error: " ++ toString ms ++ ", " ++ toString stream) + Err + ("parse error: " + ++ "[ \"" + ++ (ms |> List.intersperse "\", \"" |> String.concat) + ++ "\" ] , " + ++ "{ data: " + ++ stream.data + ++ ", input: " + ++ stream.input + ++ ", position: " + ++ String.fromInt stream.position + ++ " }" + ) diff --git a/examples/Debugger.elm b/examples/Debugger.elm new file mode 100644 index 0000000..cd91215 --- /dev/null +++ b/examples/Debugger.elm @@ -0,0 +1,72 @@ +module Debugger exposing (runDebug) + +{-| An example parser defines a simple debugger, so that you are aware of the +current rule and location information ... state can be added accordingly... + +To run this example, simply enter the examples and: + +1. run `elm repl` +2. type in `import Debugger exposing (runDebug)` +3. try out some expressions like `runDebug "1234\nabcdef\nABCD"` + +@docs calc + +-} + +import Combine exposing (..) + + +debug : String -> Parser s a -> Parser s a +debug log p = + withLine + (\y -> + withColumn + (\x -> + withSourceLine + (\s -> + let + output = + Debug.log log + ( x + , y + , String.slice 0 x s + ++ "[" + ++ String.slice x (x + 1) s + ++ "]" + ++ String.slice (x + 1) -1 s + ) + in + p + ) + ) + ) + + +runDebug : String -> Result String String +runDebug s = + case + parse + ((regex "(.|\n)" + |> debug "regex" + ) + |> many + ) + s + of + Ok ( _, _, n ) -> + Ok <| String.concat n + + Err ( _, stream, ms ) -> + Err + ("parse error: " + ++ "[ \"" + ++ (ms |> List.intersperse "\", \"" |> String.concat) + ++ "\" ] , " + ++ "{ data: " + ++ stream.data + ++ ", input: " + ++ stream.input + ++ ", position: " + ++ String.fromInt stream.position + ++ " }" + ) diff --git a/examples/Python.elm b/examples/Python.elm index bbc9cd2..85e10a9 100644 --- a/examples/Python.elm +++ b/examples/Python.elm @@ -1,4 +1,19 @@ -module Python exposing (..) +module Python exposing + ( parse, test + , CompoundStatement(..), Expression(..), Indentation, Statement(..), addop, andExpr, andop, app, arithExpr, assertStmt, assignStmt, assignop, atom, attribute, block, blockStmt, bool, breakStmt, cmpExpr, cmpop, commaSep, comment, compoundStmt, continueStmt, dedent, delStmt, dict, dictSep, dropWhile, expr, exprList, exprStmt, factor, float, forStmt, formatError, funcStmt, globalStmt, identifier, importAs, importFromStmt, importStmt, indent, indentation, initIndentation, int, keyword, list, listSep, mulop, notExpr, orop, passStmt, printStmt, program, raiseStmt, returnStmt, set, simpleStmt, spaces, stmt, str, term, token, tuple, whileStmt, whitespace, withStmt + ) + +{-| An example parser for the Python programming language. + +To run this example, simply enter the examples and: + +1. run `elm repl` +2. type in `import Python` +3. try it out with `Python.test` or `Python.parse "a = 12 * 33"` + +@docs parse, test + +-} import Combine exposing (..) import Combine.Char exposing (..) @@ -71,6 +86,7 @@ dropWhile p xs = x :: ys -> if p x then dropWhile p ys + else xs @@ -87,7 +103,7 @@ spaces = whitespace : Parser s String whitespace = - comment <|> spaces "whitespace" + or comment spaces |> onerror "whitespace" token : Parser s res -> Parser s res @@ -97,63 +113,66 @@ token = keyword : String -> Parser s String keyword s = - string s <* spaces + string s |> ignore spaces bool : Parser s Expression bool = - EBool - <$> choice - [ False <$ string "False" - , True <$ string "True" - ] - "boolean" + or (string "False" |> onsuccess False) (string "True" |> onsuccess True) + |> map EBool + |> onerror "boolean" int : Parser s Expression int = - EInt <$> Combine.Num.int "integer" + map EInt Combine.Num.int |> onerror "integer" float : Parser s Expression float = - EFloat <$> Combine.Num.float "float" + map EFloat Combine.Num.float |> onerror "float" str : Parser s Expression str = - EString - <$> choice - [ string "'" *> regex "(\\\\'|[^'\n])*" <* string "'" - , string "\"" *> regex "(\\\\\"|[^\"\n])*" <* string "\"" - ] - "string" + or + (string "'" + |> keep (regex "(\\\\'|[^'\n])*") + |> ignore (string "'") + ) + (string "\"" + |> keep (regex "(\\\\\"|[^\"\n])*") + |> ignore (string "\"") + ) + |> map EString + |> onerror "string" identifier : Parser s Expression identifier = - EIdentifier <$> regex "[_a-zA-Z][_a-zA-Z0-9]*" "identifier" + regex "[_a-zA-Z][_a-zA-Z0-9]*" + |> map EIdentifier + |> onerror "identifier" attribute : Parser s Expression attribute = lazy <| \() -> - EAttribute - <$> identifier - <* string "." - <*> choice [ attribute, identifier ] - "attribute" + map EAttribute identifier + |> ignore (string ".") + |> andMap (or attribute identifier) + |> onerror "attribute" app : Parser s Expression app = lazy <| \() -> - EApp - <$> choice [ attribute, identifier ] - <*> parens exprList - "function call" + or attribute identifier + |> map EApp + |> andMap (parens exprList) + |> onerror "function call" commaSep : Parser s String @@ -163,48 +182,55 @@ commaSep = dictSep : Parser s String dictSep = - regex ":[ \t\x0D\n]*" + regex ":[ \t\n]*" listSep : Parser s String listSep = - regex ",[ \t\x0D\n]*" + regex ",[ \t\n]*" list : Parser s Expression list = lazy <| \() -> - EList - <$> brackets (sepBy listSep expr) - "list" + brackets (sepBy listSep expr) + |> map EList + |> onerror "error list" tuple : Parser s Expression tuple = lazy <| \() -> - ETuple - <$> parens (sepBy listSep expr) - "tuple" + sepBy listSep expr + |> parens + |> map ETuple + |> onerror "tuple" dict : Parser s Expression dict = lazy <| \() -> - EDict - <$> brackets (sepBy listSep ((,) <$> expr <* dictSep <*> expr)) - "dictionary" + expr + |> ignore dictSep + |> map Tuple.pair + |> andMap expr + |> sepBy listSep + |> brackets + |> map EDict + |> onerror "dictionary" set : Parser s Expression set = lazy <| \() -> - ESet - <$> brackets (sepBy listSep expr) - "set" + sepBy listSep expr + |> brackets + |> map ESet + |> onerror "set" atom : Parser s Expression @@ -228,7 +254,7 @@ notExpr : Parser s Expression notExpr = lazy <| \() -> - (token <| ENot <$> (string "not" *> notExpr)) <|> cmpExpr + or (string "not" |> keep notExpr |> map ENot |> token) cmpExpr cmpExpr : Parser s Expression @@ -248,50 +274,48 @@ term = factor : Parser s Expression factor = - lazy (\() -> token (parens expr <|> app <|> atom)) + lazy (\() -> token (choice [ parens expr, app, atom ])) orop : Parser s (Expression -> Expression -> Expression) orop = - EOr <$ string "or" + string "or" |> onsuccess EOr andop : Parser s (Expression -> Expression -> Expression) andop = - EAnd <$ string "and" + string "and" |> onsuccess EAnd cmpop : Parser s (Expression -> Expression -> Expression) cmpop = - ECmp - <$> choice - [ string "<" - , string ">" - , string "==" - , string "!=" - , string ">=" - , string "<=" - , string "in" - , (++) <$> keyword "not" <*> string "in" - , string "is" - , (++) <$> keyword "is" <*> string "not" - ] + [ string "<" + , string ">" + , string "==" + , string "!=" + , string ">=" + , string "<=" + , string "in" + , keyword "not" |> map (++) |> andMap (string "in") + , string "is" + , keyword "is" |> map (++) |> andMap (string "not") + ] + |> choice + |> map ECmp addop : Parser s (Expression -> Expression -> Expression) addop = - choice - [ EAdd <$ string "+" - , ESub <$ string "-" - ] + or + (string "+" |> onsuccess EAdd) + (string "-" |> onsuccess ESub) mulop : Parser s (Expression -> Expression -> Expression) mulop = - choice - [ EMul <$ string "*" - , EDiv <$ string "/" - ] + or + (string "*" |> onsuccess EMul) + (string "/" |> onsuccess EDiv) exprList : Parser s (List Expression) @@ -301,87 +325,112 @@ exprList = exprStmt : Parser s Statement exprStmt = - SExpr <$> expr "expression" + map SExpr expr |> onerror "expression" printStmt : Parser s Statement printStmt = - SPrint <$> (keyword "print" *> exprList) "print statement" + keyword "print" + |> keep exprList + |> map SPrint + |> onerror "print statement" delStmt : Parser s Statement delStmt = - SDel <$> (keyword "del" *> exprList) "del statement" + keyword "del" + |> keep exprList + |> map SDel + |> onerror "del statement" passStmt : Parser s Statement passStmt = - SPass <$ keyword "pass" "pass statement" + keyword "pass" + |> onsuccess SPass + |> onerror "pass statement" breakStmt : Parser s Statement breakStmt = - SBreak <$ keyword "break" "break statement" + keyword "break" + |> onsuccess SBreak + |> onerror "break statement" continueStmt : Parser s Statement continueStmt = - SContinue <$ keyword "continue" "continue statement" + keyword "continue" + |> onsuccess SContinue + |> onerror "continue statement" returnStmt : Parser s Statement returnStmt = - SReturn <$> (keyword "return" *> maybe expr) "return statement" + keyword "return" + |> keep (maybe expr) + |> map SReturn + |> onerror "return statement" raiseStmt : Parser s Statement raiseStmt = - SRaise <$> (keyword "raise" *> maybe exprList) "raise statement" + keyword "raise" + |> keep (maybe exprList) + |> map SRaise + |> onerror "raise statement" importAs : Parser s (List ( Expression, Maybe Expression )) importAs = - sepBy commaSep <| - (,) - <$> choice [ attribute, identifier ] - <*> maybe (whitespace *> keyword "as" *> identifier) + or attribute identifier + |> map Tuple.pair + |> andMap (whitespace |> ignore (keyword "as") |> keep identifier |> maybe) + |> sepBy commaSep importStmt : Parser s Statement importStmt = - SImport - <$> (keyword "import" *> importAs) - "import statement" + keyword "import" + |> keep importAs + |> map SImport + |> onerror "import statement" importFromStmt : Parser s Statement importFromStmt = - SImportFrom - <$> (keyword "from" *> choice [ attribute, identifier ] <* spaces) - <*> (keyword "import" *> importAs) - "from statement" + keyword "from" + |> keep (or attribute identifier) + |> ignore spaces + |> map SImportFrom + |> ignore (keyword "import") + |> andMap importAs + |> onerror "from statement" globalStmt : Parser s Statement globalStmt = - SGlobal <$> (keyword "global" *> sepBy commaSep identifier) + keyword "global" + |> keep (sepBy commaSep identifier) + |> map SGlobal assertStmt : Parser s Statement assertStmt = - SAssert - <$> (keyword "assert" *> expr) - <*> maybe (commaSep *> expr) + keyword "assert" + |> keep expr + |> map SAssert + |> andMap (commaSep |> keep expr |> maybe) assignop : Parser s (Expression -> Expression -> Expression) assignop = - EAssign <$ token (string "=") + token (string "=") |> onsuccess EAssign assignStmt : Parser s Statement assignStmt = - SAssign <$> chainr assignop expr + chainr assignop expr |> map SAssign indentation : Parser Indentation res -> Parser Indentation res @@ -395,17 +444,18 @@ indentation p = validate s = let - indent = + indent_ = String.length s in - if indent == current then - succeed () - else - fail ("expected " ++ toString current ++ " spaces of indentation") + if indent_ == current then + succeed () + + else + fail ("expected " ++ String.fromInt current ++ " spaces of indentation") in - spaces >>= validate + spaces |> andThen validate in - withState skipIndent *> p + withState skipIndent |> keep p indent : Parser Indentation () @@ -417,20 +467,21 @@ indent = withState <| \stack -> let - indent = + indent_ = String.length s in - case stack of - [] -> - fail "negative indentation" - - current :: _ -> - if indent > current then - putState (indent :: stack) - else - fail "expected indentation" + case stack of + [] -> + fail "negative indentation" + + current :: _ -> + if indent_ > current then + putState (indent_ :: stack) + + else + fail "expected indentation" in - lookAhead <| spaces >>= push + spaces |> andThen push |> lookAhead dedent : Parser Indentation () @@ -445,14 +496,14 @@ dedent = rem = dropWhile ((/=) (String.length s)) stack in - case rem of - _ :: _ -> - putState rem + case rem of + _ :: _ -> + putState rem - _ -> - fail "unindent does not match any outer indentation level" + _ -> + fail "unindent does not match any outer indentation level" in - spaces >>= pop + spaces |> andThen pop block : Parser Indentation (List CompoundStatement) @@ -460,11 +511,11 @@ block = lazy <| \() -> string ":" - *> whitespace - *> eol - *> indent - *> many1 stmt - <* dedent + |> ignore whitespace + |> ignore eol + |> ignore indent + |> keep (many1 stmt) + |> ignore dedent blockStmt : Parser Indentation (List CompoundStatement -> CompoundStatement) -> Parser Indentation CompoundStatement @@ -482,7 +533,7 @@ simpleStmt = lazy <| \() -> let - stmt = + stmt_ = choice [ assertStmt , globalStmt @@ -499,33 +550,41 @@ simpleStmt = , exprStmt ] in - indentation (CSimple <$> sepBy (string ";" <* whitespace) stmt <* (() <$ eol <|> end)) + sepBy (string ";" |> ignore whitespace) stmt_ + |> ignore (or (skip eol) end) + |> map CSimple + |> indentation whileStmt : Parser s (List CompoundStatement -> CompoundStatement) whileStmt = - CWhile <$> (keyword "while" *> expr) + keyword "while" + |> keep expr + |> map CWhile forStmt : Parser s (List CompoundStatement -> CompoundStatement) forStmt = - CFor - <$> (keyword "for" *> identifier) - <*> (spaces *> keyword "in" *> expr) + keyword "for" + |> keep identifier + |> map CFor + |> andMap (spaces |> ignore (keyword "in") |> keep expr) withStmt : Parser s (List CompoundStatement -> CompoundStatement) withStmt = - CWith - <$> (keyword "with" *> expr) - <*> maybe (keyword "as" *> identifier) + keyword "with" + |> keep expr + |> map CWith + |> andMap (keyword "as" |> keep identifier |> maybe) funcStmt : Parser s (List CompoundStatement -> CompoundStatement) funcStmt = - CFunc - <$> (keyword "def" *> identifier) - <*> parens (sepBy commaSep identifier) + keyword "def" + |> keep identifier + |> map CFunc + |> andMap (parens <| sepBy commaSep identifier) compoundStmt : Parser Indentation CompoundStatement @@ -541,14 +600,14 @@ compoundStmt = , funcStmt ] in - choice parsers + choice parsers stmt : Parser Indentation CompoundStatement stmt = lazy <| \() -> - compoundStmt <|> simpleStmt + or compoundStmt simpleStmt program : Parser Indentation (List CompoundStatement) @@ -577,17 +636,28 @@ formatError ms stream = padding = location.column + separatorOffset + 2 in - "Parse error around line:\n\n" - ++ toString location.line - ++ separator - ++ location.source - ++ "\n" - ++ String.padLeft padding ' ' "^" - ++ "\nI expected one of the following:\n" - ++ expectationSeparator - ++ String.join expectationSeparator ms + "Parse error around line:\n\n" + ++ String.fromInt location.line + ++ separator + ++ location.source + ++ "\n" + ++ String.padLeft padding ' ' "^" + ++ "\nI expected one of the following:\n" + ++ expectationSeparator + ++ String.join expectationSeparator ms + + +{-| Parse simple Python-code ... + + import Python exposing (parse) + parse "a = 33 * 12" + -- Ok [CSimple [SAssign (EAssign (EIdentifier "a") (EMul (EInt 33) (EInt 12)))]] + parse " = 33 * test(22)" + -- Err ("Parse error around line:\n\n0| = 33 * test(22)\n ^\nI expected one of the following:\n\n * expected end of input") + +-} parse : String -> Result String (List CompoundStatement) parse s = case Combine.runParser program initIndentation s of @@ -598,6 +668,14 @@ parse s = Err <| formatError ms stream +{-| Run the following example ... + + import Python exposing (test) + + test + -- Ok [CSimple [SImport [(EIdentifier "os",Nothing)]],CSimple [],CSimple [SAssign (EAssign (EIdentifier "a") (EAssign (EIdentifier "b") (EInt 1)))],CSimple [],CFunc (EIdentifier "rel") [EIdentifier "p"] [CSimple [SReturn (Just (EApp (EAttribute (EIdentifier "os") (EAttribute (EIdentifier "path") (EIdentifier "join"))) [EApp (EAttribute (EIdentifier "os") (EAttribute (EIdentifier "path") (EIdentifier "dirname"))) [EIdentifier "__file__"],EIdentifier "p"]))]],CSimple [],CFunc (EIdentifier "f") [EIdentifier "a",EIdentifier "b"] [CSimple [SReturn (Just (EAdd (EIdentifier "a") (EIdentifier "b")))]],CSimple [],CWith (EApp (EIdentifier "open") [EApp (EIdentifier "rel") [EString "Python.elm"]]) (Just (EIdentifier "f")) [CFor (EIdentifier "line") (EIdentifier "f") [CSimple [SPrint [EIdentifier "f"]]]]] + +-} test : Result String (List CompoundStatement) test = parse """import os diff --git a/examples/Scheme.elm b/examples/Scheme.elm index 5bffd78..dcd277a 100644 --- a/examples/Scheme.elm +++ b/examples/Scheme.elm @@ -1,4 +1,19 @@ -module Scheme exposing (..) +module Scheme exposing + ( parse, test + , E(..), bool, char, comment, expr, float, formatError, identifier, int, list, program, quasiquote, quote, str, unquote, unquoteSplice, vector + ) + +{-| An example parser for the Scheme programming language. + +To run this example, simply enter the examples and: + +1. run `elm repl` +2. type in `import Scheme` +3. try it out with `Scheme.test` or `Scheme.parse "(* 12 12 13)"` + +@docs parse, test + +-} import Combine exposing (..) import Combine.Char exposing (anyChar) @@ -24,37 +39,28 @@ type E comment : Parser s E comment = - EComment - <$> regex ";[^\n]+" - "comment" + regex ";[^\n]+" |> map EComment |> onerror "comment" bool : Parser s E bool = let boolLiteral = - choice - [ True <$ string "#t" - , False <$ string "#f" - ] + or + (string "#t" |> onsuccess True) + (string "#f" |> onsuccess False) in - EBool - <$> boolLiteral - "boolean literal" + map EBool boolLiteral |> onerror "boolean literal" int : Parser s E int = - EInt - <$> Combine.Num.int - "integer literal" + map EInt Combine.Num.int |> onerror "integer literal" float : Parser s E float = - EFloat - <$> Combine.Num.float - "float literal" + map EFloat Combine.Num.float |> onerror "float literal" char : Parser s E @@ -62,22 +68,22 @@ char = let charLiteral = string "#\\" - *> choice - [ ' ' <$ string "space" - , '\n' <$ string "newline" - , anyChar - ] + |> keep + (choice + [ string "space" |> onsuccess ' ' + , string "newline" |> onsuccess '\n' + , anyChar + ] + ) in - EChar - <$> charLiteral - "character literal" + map EChar charLiteral |> onerror "character literal" str : Parser s E str = - EString - <$> regex "\"(\\\"|[^\"])+\"" - "string literal" + regex "\"(\\\"|[^\"])+\"" + |> map EString + |> onerror "string literal" identifier : Parser s E @@ -110,49 +116,53 @@ identifier = identifierRe = initialRe ++ subsequentRe in - EIdentifier <$> regex identifierRe "identifier" + regex identifierRe |> map EIdentifier |> onerror "identifier" list : Parser s E list = - EList - <$> parens (many expr) - "list" + many expr |> parens |> map EList |> onerror "list" vector : Parser s E vector = - EVector - <$> (string "#(" *> many expr <* string ")") - "vector" + string "#(" + |> keep (many expr) + |> ignore (string ")") + |> map EVector + |> onerror "vector" quote : Parser s E quote = - EQuote - <$> (string "'" *> expr) - "quoted expression" + string "'" + |> keep expr + |> map EQuote + |> onerror "quoted expression" quasiquote : Parser s E quasiquote = - EQuasiquote - <$> (string "`" *> expr) - "quasiquoted expression" + string "`" + |> keep expr + |> map EQuasiquote + |> onerror "quasiquoted expression" unquote : Parser s E unquote = - EUnquote - <$> (string "," *> expr) - "unquoted expression" + string "," + |> keep expr + |> map EUnquote + |> onerror "unquoted expression" unquoteSplice : Parser s E unquoteSplice = - EUnquoteSplice - <$> (string ",@" *> expr) - "spliced expression" + string ",@" + |> keep expr + |> map EUnquoteSplice + |> onerror "spliced expression" expr : Parser s E @@ -161,22 +171,25 @@ expr = \() -> let parsers = - [ bool - , float - , int - , char - , str - , identifier - , list - , vector - , quote - , quasiquote - , unquote - , unquoteSplice - , comment - ] + choice + [ bool + , float + , int + , char + , str + , identifier + , list + , vector + , quote + , quasiquote + , unquote + , unquoteSplice + , comment + ] in - whitespace *> choice parsers <* whitespace + whitespace + |> keep parsers + |> ignore whitespace program : Parser s (List E) @@ -205,17 +218,29 @@ formatError ms stream = padding = location.column + separatorOffset + 2 in - "Parse error around line:\n\n" - ++ toString location.line - ++ separator - ++ location.source - ++ "\n" - ++ String.padLeft padding ' ' "^" - ++ "\nI expected one of the following:\n" - ++ expectationSeparator - ++ String.join expectationSeparator ms + "Parse error around line:\n\n" + ++ String.fromInt location.line + ++ separator + ++ location.source + ++ "\n" + ++ String.padLeft padding ' ' "^" + ++ "\nI expected one of the following:\n" + ++ expectationSeparator + ++ String.join expectationSeparator ms + + +{-| Parse simple Scheme-code ... + + import Scheme exposing (parse) + parse "(* 12 12 13)" + -- Ok [EList [EIdentifier "*",EInt 12,EInt 12,EInt 13]] + + parse "(* 12 12 13" + -- Err ("Parse error around line:\n\n0|> (* 12 12 13\n ^\nI expected one of the following:\n\n * expected end of input") + +-} parse : String -> Result String (List E) parse s = case Combine.parse program s of @@ -224,3 +249,21 @@ parse s = Err ( _, stream, ms ) -> Err <| formatError ms stream + + +{-| Run the following example ... + + import Scheme exposing (test) + + test + -- Ok [EList [EIdentifier "define",EList [EIdentifier "derivative",EIdentifier "f",EIdentifier "dx"],EList [EIdentifier "lambda",EList [EIdentifier "x"],EList [EIdentifier "/",EList [EIdentifier "-",EList [EIdentifier "f",EList [EIdentifier "+",EIdentifier "x",EIdentifier "dx"]],EList [EIdentifier "f",EIdentifier "x"]],EIdentifier "dx"]]]] + +-} +test : Result String (List E) +test = + parse """ + (define (derivative f dx) + (lambda (x) + (/ (- (f (+ x dx)) (f x)) + dx))) + """ diff --git a/examples/elm.json b/examples/elm.json new file mode 100644 index 0000000..0ef2aa8 --- /dev/null +++ b/examples/elm.json @@ -0,0 +1,25 @@ +{ + "type": "application", + "source-directories": [ + ".", + "../src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.5", + "elm/regex": "1.0.0", + "pilatch/flip": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/src/Combine.elm b/src/Combine.elm index 42aff92..96a6865 100644 --- a/src/Combine.elm +++ b/src/Combine.elm @@ -1,72 +1,15 @@ -module Combine - exposing - ( Parser - , InputStream - , ParseLocation - , ParseContext - , ParseResult - , ParseError - , ParseOk - , primitive - , app - , lazy - , parse - , runParser - , withState - , putState - , modifyState - , withLocation - , withLine - , withColumn - , currentLocation - , currentSourceLine - , currentLine - , currentColumn - , map - , mapError - , andThen - , andMap - , sequence - , fail - , succeed - , string - , regex - , end - , whitespace - , whitespace1 - , lookAhead - , while - , or - , choice - , optional - , maybe - , many - , many1 - , manyTill - , sepBy - , sepBy1 - , sepEndBy - , sepEndBy1 - , skip - , skipMany - , skipMany1 - , chainl - , chainr - , count - , between - , parens - , braces - , brackets - , () - , (>>=) - , (<$>) - , (<$) - , ($>) - , (<*>) - , (<*) - , (*>) - , (<|>) - ) +module Combine exposing + ( Parser, InputStream, ParseLocation, ParseContext, ParseResult, ParseError, ParseOk + , parse, runParser + , primitive, app, lazy, trackedLazy + , fail, succeed, string, end, whitespace, whitespace1 + , regex, regexSub, regexWith, regexWithSub + , map, onsuccess, mapError, onerror + , andThen, andMap, sequence + , keep, ignore, lookAhead, notFollowedBy, while, or, choice, optional, maybe, many, many1, manyTill, many1Till, sepBy, sepBy1, sepEndBy, sepEndBy1, skip, skipMany, skipMany1, skipUntil, skipWhile, atLeast, upTo, chainl, chainr, count, between, parens, braces, brackets + , withState, putState, modifyState, withLocation, withLine, withColumn, withSourceLine, modifyInput, putInput, modifyPosition, putPosition + , currentLocation, currentSourceLine, currentLine, currentColumn, currentStream + ) {-| This library provides facilities for parsing structured text data into concrete Elm values. @@ -97,12 +40,17 @@ into concrete Elm values. ## Constructing Parsers -@docs primitive, app, lazy +@docs primitive, app, lazy, trackedLazy ## Parsers -@docs fail, succeed, string, regex, end, whitespace, whitespace1 +@docs fail, succeed, string, end, whitespace, whitespace1 + + +### Regular Expressions + +@docs regex, regexSub, regexWith, regexWithSub ## Combinators @@ -110,27 +58,33 @@ into concrete Elm values. ### Transforming Parsers -@docs map, (<$>), (<$), ($>), mapError, () +@docs map, onsuccess, mapError, onerror ### Chaining Parsers -@docs andThen, (>>=), andMap, (<*>), (<*), (*>), sequence +@docs andThen, andMap, sequence ### Parser Combinators -@docs lookAhead, while, or, (<|>), choice, optional, maybe, many, many1, manyTill, sepBy, sepBy1, sepEndBy, sepEndBy1, skip, skipMany, skipMany1, chainl, chainr, count, between, parens, braces, brackets +@docs keep, ignore, lookAhead, notFollowedBy, while, or, choice, optional, maybe, many, many1, manyTill, many1Till, sepBy, sepBy1, sepEndBy, sepEndBy1, skip, skipMany, skipMany1, skipUntil, skipWhile, atLeast, upTo, chainl, chainr, count, between, parens, braces, brackets ### State Combinators -@docs withState, putState, modifyState, withLocation, withLine, withColumn, currentLocation, currentSourceLine, currentLine, currentColumn +@docs withState, putState, modifyState, withLocation, withLine, withColumn, withSourceLine, modifyInput, putInput, modifyPosition, putPosition + + +### Miscellaneous + +@docs currentLocation, currentSourceLine, currentLine, currentColumn, currentStream -} -import Lazy as L -import Regex exposing (Regex) +import Dict exposing (Dict) +import Flip exposing (flip) +import Regex import String @@ -139,18 +93,21 @@ import String - `data` is the initial input provided by the user - `input` is the remainder after running a parse - `position` is the starting position of `input` in `data` after a parse + - `lazyDepth` tracks consecutive lazy calls without input consumption (for infinite loop detection) + - `lazyTracking` tracks depth per unique lazy parser ID (for trackedLazy) -} type alias InputStream = { data : String , input : String , position : Int + , lazyTracking : Dict String Int } initStream : String -> InputStream initStream s = - InputStream s s 0 + InputStream s s 0 Dict.empty {-| A record representing the current parse location in an InputStream. @@ -214,7 +171,10 @@ remaining `InputStream` and a `ParseResult res`. -} type Parser state res = Parser (ParseFn state res) - | RecursiveParser (L.Lazy (ParseFn state res)) + + + +--| RecursiveParser (L.Lazy (ParseFn state res)) {-| Construct a new primitive Parser. @@ -223,7 +183,7 @@ If you find yourself reaching for this function often consider opening a [Github issue][issues] with the library to have your custom Parsers included in the standard distribution. -[issues]: https://github.com/elm-community/parser-combinators/issues +[issues]: https://github.com/andre-dietrich/parser-combinators/issues -} primitive : (state -> InputStream -> ParseContext state res) -> Parser state res @@ -256,17 +216,12 @@ Here's how you would implement a greedy version of `manyTill` using ( estate, estream, Err ms ) -> ( estate, estream, Err ms ) in - primitive <| accumulate [] + primitive <| accumulate [] -} app : Parser state res -> state -> InputStream -> ParseContext state res -app p = - case p of - Parser inner -> - inner - - RecursiveParser t -> - L.force t +app (Parser inner) = + inner {-| Parse a string. See `runParser` if your parser needs to manage @@ -308,10 +263,11 @@ parse p = statefulInt : Parse Int Int statefulInt = - -- Parse an int, then increment the state and return the parsed - -- int. It's important that we try to parse the int _first_ - -- since modifying the state will always succeed. - int <* modifyState ((+) 1) + -- Parse an int, then increment the state and return + -- the parsed int. It's important that we try to parse + -- the int _first_ since modifying the state will + -- always succeed. + int |> ignore (modifyState ((+) 1)) ints : Parse Int (List Int) ints = @@ -346,43 +302,89 @@ runParser p st s = Err ( state, stream, ms ) -{-| Defer running a parser until it's actually required. Use this -function to avoid "bad-recursion" errors. +{-| Unfortunately this is not a real lazy function anymore, since this +functionality is not accessible anymore by ordinary developers. Use this +function only to avoid "bad-recursion" errors or use the following example +snippet in your code to circumvent this problem: +recursion x = +() -> recursion x +-} +lazy : (() -> Parser s a) -> Parser s a +lazy t = + -- RecursiveParser (L.lazy (\() -> app (t ()))) + Parser <| \state stream -> app (t ()) state stream - type Expression - = ETerm String - | EList (List E) - name : Parser s String - name = whitespace *> regex "[a-zA-Z]+" <* whitespace +{-| A more granular version of lazy that tracks recursion depth per unique parser ID. +This allows different lazy parsers to have independent depth tracking and custom limits. - term : Parser s Expression - term = ETerm <$> name +Use this when you want fine-grained control over infinite loop detection: - list : Parser s Expression - list = - let - -- helper is itself a function so we avoid the case where the - -- value `list` tries to apply itself in its definition. - helper () = - EList <$> between (string "(") (string ")") (many (term <|> list)) - in - -- lazy defers calling helper until it's actually needed. - lazy helper + myRecursiveParser : Parser s Expr + myRecursiveParser = + trackedLazy "expr-parser" 50 <| + \() -> + choice + [ string "atom" + , myRecursiveParser -- Each ID has its own counter + ] - parse list "" - -- Err ["expected \"(\""] + parse myRecursiveParser "atom" + -- Ok "atom" - parse list "()" - -- Ok (EList []) +Parameters: - parse list "(a (b c))" - -- Ok (EList [ETerm "a", EList [ETerm "b", ETerm "c"]]) + - `id`: A unique identifier for this lazy parser (e.g., "json-object", "expression") + - `maxDepth`: Maximum recursion depth before triggering infinite loop detection + - `thunk`: The parser to evaluate lazily -} -lazy : (() -> Parser s a) -> Parser s a -lazy t = - RecursiveParser (L.lazy (\() -> app (t ()))) +trackedLazy : String -> Int -> (() -> Parser s a) -> Parser s a +trackedLazy id maxDepth t = + Parser <| + \state stream -> + let + currentDepth = + Dict.get id stream.lazyTracking |> Maybe.withDefault 0 + in + if currentDepth >= maxDepth then + ( state + , stream + , Err [ "infinite loop detected: lazy parser '" ++ id ++ "' exceeded depth limit of " ++ String.fromInt maxDepth ] + ) + + else + let + updatedTracking = + Dict.insert id (currentDepth + 1) stream.lazyTracking + + streamWithTracking = + { stream | lazyTracking = updatedTracking } + in + case app (t ()) state streamWithTracking of + ( rstate, rstream, Ok res ) -> + -- Always restore depth to what it was before this call + let + finalTracking = + if currentDepth == 0 then + Dict.remove id rstream.lazyTracking + + else + Dict.insert id currentDepth rstream.lazyTracking + in + ( rstate, { rstream | lazyTracking = finalTracking }, Ok res ) + + ( estate, estream, Err ms ) -> + -- Restore depth on error too + let + finalTracking = + if currentDepth == 0 then + Dict.remove id estream.lazyTracking + + else + Dict.insert id currentDepth estream.lazyTracking + in + ( estate, { estream | lazyTracking = finalTracking }, Err ms ) {-| Transform both the result and error message of a parser. @@ -409,6 +411,15 @@ bimap fok ferr p = {-| Get the parser's state and pipe it into a parser. + + let + parser = + string "a" + |> keep (withState (\state -> succeed state)) + in + parse parser "a" + -- Ok () + -} withState : (s -> Parser s a) -> Parser s a withState f = @@ -418,6 +429,15 @@ withState f = {-| Replace the parser's state. + + let + parser = + string "a" + |> andThen (\_ -> putState 42) + in + parse parser "a" + -- Ok () + -} putState : s -> Parser s () putState state = @@ -427,6 +447,15 @@ putState state = {-| Modify the parser's state. + + let + parser = + string "a" + |> andThen (\_ -> modifyState ((+) 1)) + in + parse parser "a" + -- Ok () + -} modifyState : (s -> s) -> Parser s () modifyState f = @@ -436,6 +465,15 @@ modifyState f = {-| Get the current position in the input stream and pipe it into a parser. + + let + parser = + string "a" + |> keep (withLocation succeed) + in + parse parser "a" + -- Ok { source = "a", line = 0, column = 1 } + -} withLocation : (ParseLocation -> Parser s a) -> Parser s a withLocation f = @@ -445,6 +483,15 @@ withLocation f = {-| Get the current line and pipe it into a parser. + + let + parser = + string "a\n\n" + |> keep (withLine succeed) + in + parse parser "a\n\n" + -- Ok 2 + -} withLine : (Int -> Parser s a) -> Parser s a withLine f = @@ -454,6 +501,15 @@ withLine f = {-| Get the current column and pipe it into a parser. + + let + parser = + string "aaa" + |> keep (withColumn succeed) + in + parse parser "aaa" + -- Ok 3 + -} withColumn : (Int -> Parser s a) -> Parser s a withColumn f = @@ -462,15 +518,34 @@ withColumn f = app (f <| currentColumn stream) state stream +{-| Get the current InputStream and pipe it into a parser, +only for debugging purposes ... + + let + parser = + string "a" + |> keep (withSourceLine succeed) + in + parse parser "abc" + -- Ok "bc" + +-} +withSourceLine : (String -> Parser s a) -> Parser s a +withSourceLine f = + Parser <| + \state stream -> + app (f <| currentSourceLine stream) state stream + + {-| Get the current `(line, column)` in the input stream. -} currentLocation : InputStream -> ParseLocation currentLocation stream = let - find position currentLine lines = + find position currentLine_ lines = case lines of [] -> - ParseLocation "" currentLine position + ParseLocation "" currentLine_ position line :: rest -> let @@ -480,14 +555,16 @@ currentLocation stream = lengthPlusNL = length + 1 in - if position == length then - ParseLocation line currentLine position - else if position > length then - find (position - lengthPlusNL) (currentLine + 1) rest - else - ParseLocation line currentLine position + if position == length then + ParseLocation line currentLine_ position + + else if position > length then + find (position - lengthPlusNL) (currentLine_ + 1) rest + + else + ParseLocation line currentLine_ position in - find stream.position 0 (String.split "\n" stream.data) + find stream.position 0 (String.split "\n" stream.data) {-| Get the current source line in the input stream. @@ -511,6 +588,80 @@ currentColumn = currentLocation >> .column +{-| Get the current string stream. That might be useful for applying memorization. +-} +currentStream : InputStream -> String +currentStream = + .input + + +{-| Modify the parser's current InputStream input (String). + + parse + (modifyInput String.toUpper + |> keep (many (string "A")) + ) + "aaa" + -- Ok ["A","A","A"] + +-} +modifyInput : (String -> String) -> Parser s () +modifyInput f = + Parser <| + \state stream -> + app (succeed ()) state { stream | input = f stream.input } + + +{-| Replace the remaining input with a new string. + + parse + (string "a" + |> ignore (putInput "AAA") + |> keep (many (string "A")) + ) + "aaa" + -- Ok ["A","A","A"] + +-} +putInput : String -> Parser s () +putInput i = + modifyInput (always i) + + +{-| Modify the parser's InputStream position (Int). + + let + parser = + string "a" + |> ignore (modifyPosition ((+) 1000)) + in + parse parser "a" + -- Ok ((),{ data = "a", input = "", position = 1001 },"a") + +-} +modifyPosition : (Int -> Int) -> Parser s () +modifyPosition f = + Parser <| + \state stream -> + app (succeed ()) state { stream | position = f stream.position } + + +{-| Replace the parser position. + + let + parser = + string "a" + |> ignore (putPosition 1000) + in + parse parser "a" + -- Ok ((),{ data = "a", input = "", position = 1000 },"a") + +-} +putPosition : Int -> Parser s () +putPosition i = + modifyPosition (always i) + + -- Transformers -- ------------ @@ -599,7 +750,7 @@ andThen f p = sum = int |> map (+) - |> andMap (plus *> int) + |> andMap (plus |> keep int) parse sum "1+2" -- Ok 3 @@ -607,7 +758,7 @@ andThen f p = -} andMap : Parser s a -> Parser s (a -> b) -> Parser s b andMap rp lp = - lp >>= flip map rp + lp |> andThen (flip map rp) {-| Run a list of parsers in sequence, accumulating the results. The @@ -638,9 +789,9 @@ sequence parsers = ( estate, estream, Err ms ) -> ( estate, estream, Err ms ) in - Parser <| - \state stream -> - accumulate [] parsers state stream + Parser <| + \state stream -> + accumulate [] parsers state stream @@ -705,9 +856,10 @@ string s = pos = stream.position + len in - ( state, { stream | input = rem, position = pos }, Ok s ) + ( state, { stream | input = rem, position = pos }, Ok s ) + else - ( state, stream, Err [ "expected " ++ toString s ] ) + ( state, stream, Err [ "expected \"" ++ s ++ "\"" ] ) {-| Parse a Regex match. @@ -719,34 +871,145 @@ every pattern unless one already exists. parse (regex "a+") "aaaaab" -- Ok "aaaaa" + parse (regex "a+") "Aaaaab" + -- Err ["expected input matching Regexp /^a+/"] + +Use `regexWith` for more options on allowing case-insensitive or multiline. + -} regex : String -> Parser s String -regex pat = +regex = + regexer Regex.fromString .match >> Parser + + +{-| Parse a Regex match. + +Same as regex, but returns also submatches as the second parameter in +the result tuple. + + parse (regexSub "(a+)(b+)") "aaaaab" + -- Ok ("aaaaab",[Just "aaaaa",Just "b"]) + + parse (regexSub "(?:a+)(b+)") "aaaaab" + -- Ok ("aaaaab",[Just "b"]) + +-} +regexSub : String -> Parser s ( String, List (Maybe String) ) +regexSub = + regexer Regex.fromString + (\m -> ( m.match, m.submatches )) + >> Parser + + +{-| Parse a Regex match. + +Since, Regex now also has support for more parameters, this option was +included into this package. Call `regexWith` with two additional parameters: +`caseInsensitive` and `multiline`, which allow you to tweak your expression. +The rest is as follows. Regular expressions must match from the beginning +of the input and their subgroups are ignored. A `^` is added implicitly to +the beginning of every pattern unless one already exists. + + parse + (regexWith + { caseInsensitive = True, multiline = False } + "a+" + ) + "AaaAAaAab" + -- Ok "AaaAAaAa" + + parse + (regexWith + { caseInsensitive = False, multiline = False } + "a+" + ) + "AaaAAaAab" + -- Err ["expected input matching Regexp /^a+/"] + +-} +regexWith : { caseInsensitive : Bool, multiline : Bool } -> String -> Parser s String +regexWith { caseInsensitive, multiline } = + regexer + (Regex.fromStringWith + { caseInsensitive = caseInsensitive + , multiline = multiline + } + ) + .match + >> Parser + + +{-| Parse a Regex match. + +Similar to `regexWith`, but a tuple is returned, with a list of additional +submatches. +The rest is as follows. Regular expressions must match from the beginning +of the input and their subgroups are ignored. A `^` is added implicitly to +the beginning of every pattern unless one already exists. + + parse + (regexWithSub + { caseInsensitive = True, multiline = False } + "a+" + ) + "AaaAAaAab" + -- Ok ("aaaAAaAa", []) + + parse + (regexWithSub + { caseInsensitive = False, multiline = False } + "a+" + ) + "AaaAAaAab" + -- Err ["expected input matching Regexp /^a+/"] + +-} +regexWithSub : { caseInsensitive : Bool, multiline : Bool } -> String -> Parser s ( String, List (Maybe String) ) +regexWithSub { caseInsensitive, multiline } = + regexer + (Regex.fromStringWith + { caseInsensitive = caseInsensitive + , multiline = multiline + } + ) + (\m -> ( m.match, m.submatches )) + >> Parser + + +regexer : + (String -> Maybe Regex.Regex) + -> (Regex.Match -> res) + -> String + -> (state -> InputStream -> ( state, InputStream, ParseResult res )) +regexer input output pat = let pattern = if String.startsWith "^" pat then pat + else "^" ++ pat + + compiledRegex = + input pattern |> Maybe.withDefault Regex.never in - Parser <| - \state stream -> - case Regex.find (Regex.AtMost 1) (Regex.regex pattern) stream.input of - [ match ] -> - let - len = - String.length match.match + \state stream -> + case Regex.findAtMost 1 compiledRegex stream.input of + [ match ] -> + let + len = + String.length match.match - rem = - String.dropLeft len stream.input + rem = + String.dropLeft len stream.input - pos = - stream.position + len - in - ( state, { stream | input = rem, position = pos }, Ok match.match ) + pos = + stream.position + len + in + ( state, { stream | input = rem, position = pos }, Ok (output match) ) - _ -> - ( state, stream, Err [ "expected input matching Regexp /" ++ pattern ++ "/" ] ) + _ -> + ( state, stream, Err [ "expected input matching Regexp /" ++ pattern ++ "/" ] ) {-| Consume input while the predicate matches. @@ -763,26 +1026,24 @@ while pred = Just ( h, rest ) -> if pred h then let - c = - String.cons h "" - pos = stream.position + 1 in - accumulate (acc ++ c) state { stream | input = rest, position = pos } + accumulate (h :: acc) state { stream | input = rest, position = pos } + else - ( state, stream, acc ) + ( state, stream, String.fromList (List.reverse acc) ) Nothing -> - ( state, stream, acc ) + ( state, stream, String.fromList (List.reverse acc) ) in - Parser <| - \state stream -> - let - ( rstate, rstream, res ) = - accumulate "" state stream - in - ( rstate, rstream, Ok res ) + Parser <| + \state stream -> + let + ( rstate, rstream, res ) = + accumulate [] state stream + in + ( rstate, rstream, Ok res ) {-| Fail when the input is not empty. @@ -800,11 +1061,22 @@ end = \state stream -> if stream.input == "" then ( state, stream, Ok () ) + else ( state, stream, Err [ "expected end of input" ] ) {-| Apply a parser without consuming any input on success. + + parse (lookAhead (string "a") |> keep (string "a")) "a" + -- Ok "a" + + parse (lookAhead (string "a") |> keep (string "b")) "a" + -- Err ["expected \"b\""] + + parse (lookAhead (string "a") |> keep (string "b")) "b" + -- Err ["expected \"a\""] + -} lookAhead : Parser s a -> Parser s a lookAhead p = @@ -818,6 +1090,35 @@ lookAhead p = err +{-| Succeed if the given parser fails, without consuming any input. +Useful for implementing keyword parsers that don't match prefixes. + + import Combine.Char exposing (alphaNum) + + keyword : String -> Parser s String + keyword kw = + string kw + |> ignore (notFollowedBy alphaNum) + + parse (keyword "if") "if x" + -- Ok "if" + + parse (keyword "if") "iffy" + -- Err ["unexpected alphanumeric character"] + +-} +notFollowedBy : Parser s a -> Parser s () +notFollowedBy p = + Parser <| + \state stream -> + case app p state stream of + ( _, _, Ok _ ) -> + ( state, stream, Err [ "unexpected input" ] ) + + ( _, _, Err _ ) -> + ( state, stream, Ok () ) + + {-| Choose between two parsers. parse (or (string "a") (string "b")) "a" @@ -844,7 +1145,7 @@ or lp rp = res ( _, _, Err rms ) -> - ( state, stream, Err (lms ++ rms) ) + ( state, stream, Err (List.foldl (::) lms rms |> List.reverse) ) {-| Choose between a list of parsers. @@ -858,7 +1159,21 @@ or lp rp = -} choice : List (Parser s a) -> Parser s a choice xs = - List.foldr or emptyErr xs + let + tryParsers parsers state stream errors = + case parsers of + [] -> + ( state, stream, Err (List.reverse errors) ) + + p :: rest -> + case app p state stream of + ( _, _, Ok _ ) as res -> + res + + ( _, _, Err ms ) -> + tryParsers rest state stream (List.foldl (::) errors ms) + in + Parser <| \state stream -> tryParsers xs state stream [] {-| Return a default value when the given parser fails. @@ -875,7 +1190,7 @@ choice xs = -} optional : a -> Parser s a -> Parser s a optional res p = - p <|> succeed res + succeed res |> or p {-| Wrap the return value into a `Maybe`. Returns `Nothing` on failure. @@ -917,21 +1232,22 @@ many p = accumulate acc state stream = case app p state stream of ( rstate, rstream, Ok res ) -> - if stream == rstream then + if stream.input == rstream.input then ( rstate, rstream, List.reverse acc ) + else accumulate (res :: acc) rstate rstream _ -> ( state, stream, List.reverse acc ) in - Parser <| - \state stream -> - let - ( rstate, rstream, res ) = - accumulate [] state stream - in - ( rstate, rstream, Ok res ) + Parser <| + \state stream -> + let + ( rstate, rstream, res ) = + accumulate [] state stream + in + ( rstate, rstream, Ok res ) {-| Parse at least one result. @@ -945,35 +1261,79 @@ many p = -} many1 : Parser s a -> Parser s (List a) many1 p = - (::) <$> p <*> many p + p |> map (::) |> andMap (many p) {-| Apply the first parser zero or more times until second parser succeeds. On success, the list of the first parser's results is returned. - string "") + parse + (string "") + |> map String.fromList + ) + ) + "" + -- Ok "foo bar" -} manyTill : Parser s a -> Parser s end -> Parser s (List a) -manyTill p end = +manyTill p end_ = let accumulate acc state stream = - case app end state stream of + case app end_ state stream of ( rstate, rstream, Ok _ ) -> ( rstate, rstream, Ok (List.reverse acc) ) ( estate, estream, Err ms ) -> case app p state stream of ( rstate, rstream, Ok res ) -> - accumulate (res :: acc) rstate rstream + if stream.input == rstream.input then + ( estate, estream, Err [ "manyTill: parser succeeded without consuming input" ] ) - _ -> - ( estate, estream, Err ms ) + else + accumulate (res :: acc) rstate rstream + + ( _, _, Err _ ) -> + if stream.input == "" then + ( estate, estream, Err [ "manyTill: reached end of input without finding end parser" ] ) + + else + ( estate, estream, Err ms ) in - Parser (accumulate []) + Parser (accumulate []) -{-| Parser zero or more occurences of one parser separated by another. +{-| Apply the first parser one or more times until second parser +succeeds. On success, the list of the first parser's results is returned. + + parse + (string "") + |> map String.fromList + ) + ) + "" + -- Ok "foo bar" + +-} +many1Till : Parser s a -> Parser s end -> Parser s (List a) +many1Till p = + manyTill p + >> andThen + (\result -> + case result of + [] -> + fail "not enough results" + + _ -> + succeed result + ) + + +{-| Parser zero or more occurrences of one parser separated by another. parse (sepBy (string ",") (string "a")) "b" -- Ok [] @@ -987,17 +1347,27 @@ manyTill p end = -} sepBy : Parser s x -> Parser s a -> Parser s (List a) sepBy sep p = - sepBy1 sep p <|> succeed [] + or (sepBy1 sep p) (succeed []) + +{-| Parse one or more occurrences of one parser separated by another. + + parse (sepBy1 (string ",") (string "a")) "" + -- Err ["expected \"a\""] + + parse (sepBy1 (string ",") (string "a")) "a" + -- Ok ["a"] + + parse (sepBy1 (string ",") (string "a")) "a," + -- Ok ["a"] -{-| Parse one or more occurences of one parser separated by another. -} sepBy1 : Parser s x -> Parser s a -> Parser s (List a) sepBy1 sep p = - (::) <$> p <*> many (sep *> p) + map (::) p |> andMap (many (sep |> keep p)) -{-| Parse zero or more occurences of one parser separated and +{-| Parse zero or more occurrences of one parser separated and optionally ended by another. parse (sepEndBy (string ",") (string "a")) "a,a,a," @@ -1006,10 +1376,10 @@ optionally ended by another. -} sepEndBy : Parser s x -> Parser s a -> Parser s (List a) sepEndBy sep p = - sepEndBy1 sep p <|> succeed [] + or (sepEndBy1 sep p) (succeed []) -{-| Parse one or more occurences of one parser separated and +{-| Parse one or more occurrences of one parser separated and optionally ended by another. parse (sepEndBy1 (string ",") (string "a")) "" @@ -1024,72 +1394,297 @@ optionally ended by another. -} sepEndBy1 : Parser s x -> Parser s a -> Parser s (List a) sepEndBy1 sep p = - sepBy1 sep p <* maybe sep + sepBy1 sep p |> ignore (maybe sep) {-| Apply a parser and skip its result. + + parse (skip (string "a")) "a" + -- Ok () + + parse (skip (string "a")) "b" + -- Err ["expected \"a\""] + -} skip : Parser s x -> Parser s () skip p = - () <$ p + p |> onsuccess () {-| Apply a parser and skip its result many times. + + parse (skipMany (string "a")) "aaa" + -- Ok () + + parse (skipMany (string "a")) "" + -- Ok () + -} skipMany : Parser s x -> Parser s () skipMany p = - () <$ many (skip p) + many (skip p) |> onsuccess () {-| Apply a parser and skip its result at least once. + + parse (skipMany1 (string "a")) "a" + -- Ok () + + parse (skipMany1 (string "a")) "" + -- Err ["expected \"a\""] + -} skipMany1 : Parser s x -> Parser s () skipMany1 p = - () <$ many1 (skip p) + many1 (skip p) |> onsuccess () + + +{-| Skip input until the given parser succeeds. +This is similar to `manyTill`, but more efficient as it doesn't +accumulate results. + + parse + (skipUntil (string "-->") |> keep (string "-->")) + "some text here-->" + -- Ok "-->" + +-} +skipUntil : Parser s end -> Parser s () +skipUntil end_ = + let + accumulate state stream = + case app end_ state stream of + ( rstate, rstream, Ok _ ) -> + ( rstate, rstream, Ok () ) + + ( estate, estream, Err _ ) -> + case String.uncons stream.input of + Just ( _, rest ) -> + accumulate state { stream | input = rest, position = stream.position + 1 } + + Nothing -> + ( estate, estream, Err [ "skipUntil: reached end of input without finding end parser" ] ) + in + Parser accumulate + + +{-| Skip characters while the predicate holds. +More efficient than `skipMany (satisfy pred)` as it doesn't build a list. + + import Combine.Char exposing (space) + + parse + (skipWhile ((==) ' ') |> keep (string "hello")) + " hello" + -- Ok "hello" + + parse (skipWhile Char.isDigit) "123abc" + -- Ok () + +-} +skipWhile : (Char -> Bool) -> Parser s () +skipWhile pred = + Parser <| + \state stream -> + let + skipChars input pos = + case String.uncons input of + Just ( c, rest ) -> + if pred c then + skipChars rest (pos + 1) + + else + ( input, pos ) + + Nothing -> + ( input, pos ) + + ( remainingInput, newPos ) = + skipChars stream.input stream.position + in + ( state, { stream | input = remainingInput, position = newPos }, Ok () ) + + +{-| Parse at least `n` occurrences of a parser. +Complements `upTo` for full bounded repetition control. + + parse (atLeast 2 (string "a")) "aaa" + -- Ok ["a", "a", "a"] + + parse (atLeast 2 (string "a")) "a" + -- Err ["expected \"a\""] + + parse (atLeast 0 (string "a")) "b" + -- Ok [] + +-} +atLeast : Int -> Parser s a -> Parser s (List a) +atLeast n p = + count n p + |> andThen (\initial -> many p |> map (\rest -> initial ++ rest)) + + +{-| Parse at most `n` occurrences of a parser. +Similar to `many`, but with an upper limit. + + parse (upTo 3 (string "a")) "aaaaa" + -- Ok ["a", "a", "a"] + + parse (upTo 3 (string "a")) "aa" + -- Ok ["a", "a"] + + parse (upTo 3 (string "a")) "b" + -- Ok [] + +Combine with `atLeast` for bounded repetition: + + between2And4 p = + atLeast 2 p |> andThen (\_ -> upTo 4 p) + +-} +upTo : Int -> Parser s a -> Parser s (List a) +upTo n p = + let + accumulate remaining acc state stream = + if remaining <= 0 then + ( state, stream, Ok (List.reverse acc) ) + + else + case app p state stream of + ( rstate, rstream, Ok res ) -> + if stream.input == rstream.input then + ( rstate, rstream, Ok (List.reverse acc) ) + else + accumulate (remaining - 1) (res :: acc) rstate rstream -{-| Parse one or more occurences of `p` separated by `op`, recursively + _ -> + ( state, stream, Ok (List.reverse acc) ) + in + Parser <| \state stream -> accumulate n [] state stream + + +{-| Parse one or more occurrences of `p` separated by `op`, recursively apply all functions returned by `op` to the values returned by `p`. See the `examples/Calc.elm` file for an example. + + let + addop = + choice + [ string "+" |> onsuccess (+) + , string "-" |> onsuccess (-) + ] + in + parse (chainl addop int) "1+2+3" + -- Ok 6 + + parse (chainl addop int) "1+2+3-X" + -- Ok 6 + -} chainl : Parser s (a -> a -> a) -> Parser s a -> Parser s a chainl op p = let - accumulate x = - (op - |> andThen - (\f -> - p - |> andThen (\y -> accumulate (f x y)) - ) - ) - <|> succeed x + accumulate x state stream = + case app op state stream of + ( opstate, opstream, Ok f ) -> + if stream.input == opstream.input then + ( opstate, opstream, Ok x ) + + else + case app p opstate opstream of + ( pstate, pstream, Ok y ) -> + if opstream.input == pstream.input then + ( pstate, pstream, Ok x ) + + else + accumulate (f x y) pstate pstream + + ( estate, estream, Err ms ) -> + ( estate, estream, Err ms ) + + ( _, _, Err _ ) -> + ( state, stream, Ok x ) in - andThen accumulate p + Parser <| + \state stream -> + case app p state stream of + ( pstate, pstream, Ok x ) -> + accumulate x pstate pstream + + ( estate, estream, Err ms ) -> + ( estate, estream, Err ms ) {-| Similar to `chainl` but functions of `op` are applied in right-associative order to the values of `p`. See the `examples/Python.elm` file for a usage example. + + let + addop = + choice + [ string "+" |> onsuccess (+) + , string "-" |> onsuccess (-) + ] + in + + parse (chainr addop int) "1-2-3" -- 1 - (2 - 3) + -- Ok 2 + + parse (chainl addop int) "1-2-3" -- 1 - 2 - 3 + -- Ok (-4) + -} chainr : Parser s (a -> a -> a) -> Parser s a -> Parser s a chainr op p = let - accumulate x = - (op - |> andThen - (\f -> - p - |> andThen accumulate - |> andThen (\y -> succeed (f x y)) - ) - ) - <|> succeed x + accumulate x state stream = + case app op state stream of + ( opstate, opstream, Ok f ) -> + if stream.input == opstream.input then + ( opstate, opstream, Ok x ) + + else + case app p opstate opstream of + ( pstate, pstream, Ok y ) -> + if opstream.input == pstream.input then + ( pstate, pstream, Ok x ) + + else + case accumulate y pstate pstream of + ( rstate, rstream, Ok z ) -> + ( rstate, rstream, Ok (f x z) ) + + err -> + err + + ( estate, estream, Err ms ) -> + ( estate, estream, Err ms ) + + ( _, _, Err _ ) -> + ( state, stream, Ok x ) in - andThen accumulate p + Parser <| + \state stream -> + case app p state stream of + ( pstate, pstream, Ok x ) -> + accumulate x pstate pstream + + ( estate, estream, Err ms ) -> + ( estate, estream, Err ms ) + +{-| Parse `n` occurrences of `p`. + + parse (count 3 (string "a")) "aaa" + -- Ok ["a", "a", "a"] + + parse (count 3 (string "a")) "aa" + -- Err ["expected \"a\""] + + parse (count 3 (string "a")) "aaaaa" + -- Ok ["a", "a", "a"] -{-| Parse `n` occurences of `p`. -} count : Int -> Parser s a -> Parser s (List a) count n p = @@ -1097,29 +1692,46 @@ count n p = accumulate x acc = if x <= 0 then succeed (List.reverse acc) + else andThen (\res -> accumulate (x - 1) (res :: acc)) p in - accumulate n [] + accumulate n [] {-| Parse something between two other parsers. The parser - between (string "(") (string ")") (string "a") + parse + (between + (string "(") + (string ")") + (string "a") + ) + "(a)" + -- Ok "a" is equivalent to the parser - string "(" *> string "a" <* string ")" + string "(" + |> keep (string "a") + |> ignore (string ")") -} between : Parser s l -> Parser s r -> Parser s a -> Parser s a between lp rp p = - lp *> p <* rp + lp |> keep p |> ignore rp {-| Parse something between parentheses. + + parse (parens (string "hello")) "(hello)" + -- Ok "hello" + + parse (parens (string "hello")) "(world)" + -- Err ["expected \"hello\""] + -} parens : Parser s a -> Parser s a parens = @@ -1127,6 +1739,13 @@ parens = {-| Parse something between braces `{}`. + + parse (braces (string "hello")) "{hello}" + -- Ok "hello" + + parse (braces (string "hello")) "{world}" + -- Err ["expected \"hello\""] + -} braces : Parser s a -> Parser s a braces = @@ -1134,6 +1753,13 @@ braces = {-| Parse something between square brackets `[]`. + + parse (brackets (string "hello")) "[hello]" + -- Ok "hello" + + parse (brackets (string "hello")) "[world]" + -- Err ["expected \"hello\""] + -} brackets : Parser s a -> Parser s a brackets = @@ -1142,136 +1768,72 @@ brackets = {-| Parse zero or more whitespace characters. - parse (whitespace *> string "hello") "hello" + parse + (whitespace + |> keep (string "hello") + ) + "hello" -- Ok "hello" - parse (whitespace *> string "hello") " hello" + parse + (whitespace + |> keep (string "hello") + ) + " hello" -- Ok "hello" -} whitespace : Parser s String whitespace = - regex "[ \t\x0D\n]*" "whitespace" + regex "\\s*" |> onerror "optional whitespace" {-| Parse one or more whitespace characters. - parse (whitespace1 *> string "hello") "hello" - -- Err ["whitespace"] + parse + (whitespace1 + |> keep (string "hello") + ) + "hello" + -- Err ["whitespace"] - parse (whitespace1 *> string "hello") " hello" - -- Ok "hello" + parse + (whitespace1 + |> keep (string "hello") + ) + " hello" + -- Ok "hello" -} whitespace1 : Parser s String whitespace1 = - regex "[ \t\x0D\n]+" "whitespace" - - - --- Infix operators --- --------------- + regex "\\s+" |> onerror "whitespace" {-| Variant of `mapError` that replaces the Parser's error with a List of a single string. - parse (string "a" "gimme an 'a'") "b" + parse (string "a" |> onerror "gimme an 'a'") "b" -- Err ["gimme an 'a'"] -} -() : Parser s a -> String -> Parser s a -() p m = +onerror : String -> Parser s a -> Parser s a +onerror m p = mapError (always [ m ]) p -{-| Infix version of `andThen`. - - import Combine.Num exposing (int) - - choosy : Parser s String - choosy = - let - createParser n = - if n % 2 == 0 then - string " is even" - else - string " is odd" - in - int >>= createParser - - parse choosy "1 is odd" - -- Ok " is odd" - - parse choosy "2 is even" - -- Ok " is even" - - parse choosy "1 is even" - -- Err ["expected \" is odd\""] - --} -(>>=) : Parser s a -> (a -> Parser s b) -> Parser s b -(>>=) = - flip andThen - - -{-| Infix version of `map`. - - parse (toString <$> int) "42" - -- Ok "42" - - parse (toString <$> int) "abc" - -- Err ["expected an integer"] - --} -(<$>) : (a -> b) -> Parser s a -> Parser s b -(<$>) = - map - - -{-| Run a parser and return the value on the left on success. - - parse (True <$ string "true") "true" - -- Ok True - - parse (True <$ string "true") "false" - -- Err ["expected \"true\""] - --} -(<$) : a -> Parser s x -> Parser s a -(<$) res = - map (always res) - - {-| Run a parser and return the value on the right on success. - parse (string "true" $> True) "true" + parse (string "true" |> onsuccess True) "true" -- Ok True - parse (string "true" $> True) "false" + parse (string "true" |> onsuccess True) "false" -- Err ["expected \"true\""] -} -($>) : Parser s x -> a -> Parser s a -($>) = - flip (<$) - - -{-| Infix version of `andMap`. - - add : Int -> Int -> Int - add = (+) - - plus : Parser s String - plus = string "+" - - parse (add <$> int <*> (plus *> int)) "1+1" - -- Ok 2 - --} -(<*>) : Parser s (a -> b) -> Parser s a -> Parser s b -(<*>) = - flip andMap +onsuccess : a -> Parser s x -> Parser s a +onsuccess res = + map (always res) {-| Join two parsers, ignoring the result of the one on the right. @@ -1279,17 +1841,17 @@ of a single string. unsuffix : Parser s String unsuffix = regex "[a-z]" - <* regex "[!?]" + |> keep (regex "[!?]") parse unsuffix "a!" -- Ok "a" -} -(<*) : Parser s a -> Parser s x -> Parser s a -(<*) lp rp = - lp - |> map always - |> andMap rp +keep : Parser s a -> Parser s x -> Parser s a +keep p1 p2 = + p2 + |> map (flip always) + |> andMap p1 {-| Join two parsers, ignoring the result of the one on the left. @@ -1297,50 +1859,15 @@ of a single string. unprefix : Parser s String unprefix = string ">" - *> while ((==) ' ') - *> while ((/=) ' ') + |> ignore (while ((==) ' ')) + |> ignore (while ((/=) ' ')) parse unprefix "> a" -- Ok "a" -} -(*>) : Parser s x -> Parser s a -> Parser s a -(*>) lp rp = - lp - |> map (flip always) - |> andMap rp - - -{-| Synonym for `or`. --} -(<|>) : Parser s a -> Parser s a -> Parser s a -(<|>) = - or - - - --- Fixities - - -infixl 1 >>= - - -infixr 1 <|> - - -infixl 4 <$> - - -infixl 4 <$ - - -infixl 4 $> - - -infixl 4 <*> - - -infixl 4 <* - - -infixl 4 *> +ignore : Parser s x -> Parser s a -> Parser s a +ignore p1 p2 = + p2 + |> map always + |> andMap p1 diff --git a/src/Combine/Char.elm b/src/Combine/Char.elm index 038537e..2c2166a 100644 --- a/src/Combine/Char.elm +++ b/src/Combine/Char.elm @@ -1,4 +1,4 @@ -module Combine.Char exposing (satisfy, char, anyChar, oneOf, noneOf, space, tab, newline, crlf, eol, lower, upper, digit, octDigit, hexDigit) +module Combine.Char exposing (satisfy, char, anyChar, peekChar, oneOf, noneOf, space, tab, newline, crlf, eol, lower, upper, digit, octDigit, hexDigit, alpha, alphaNum) {-| This module contains `Char`-specific Parsers. @@ -7,12 +7,16 @@ everything that you can do with this module by using `Combine.regex`, `Combine.string` or `Combine.primitive` and, in general, those will be much faster. + # Parsers -@docs satisfy, char, anyChar, oneOf, noneOf, space, tab, newline, crlf, eol, lower, upper, digit, octDigit, hexDigit + +@docs satisfy, char, anyChar, peekChar, oneOf, noneOf, space, tab, newline, crlf, eol, lower, upper, digit, octDigit, hexDigit, alpha, alphaNum + -} import Char -import Combine exposing (Parser, primitive, regex, (), (<$), (<|>)) +import Combine exposing (Parser, onerror, onsuccess, or, primitive, string) +import Flip exposing (flip) import String @@ -33,29 +37,45 @@ satisfy pred = message = "could not satisfy predicate" in - case String.uncons stream.input of - Just ( h, rest ) -> - if pred h then - ( state, { stream | input = rest, position = stream.position + 1 }, Ok h ) - else - ( state, stream, Err [ message ] ) - - Nothing -> + case String.uncons stream.input of + Just ( h, rest ) -> + if pred h then + ( state, { stream | input = rest, position = stream.position + 1 }, Ok h ) + + else ( state, stream, Err [ message ] ) + Nothing -> + ( state, stream, Err [ message ] ) + {-| Parse an exact character match. - parse (char 'a') "a" == - -- Ok 'a' + parse (char 'a') "a" --> Ok 'a' + + parse (char 'a') "b" --> Err ["expected 'a'"] + + -- You can write the expected result on the next line, + + add 41 1 + --> 42 + + -- You can write the expected result on the next line, - parse (char 'a') "b" == - -- Err ["expected 'a'"] + add 41 1 + --> 42 -} char : Char -> Parser s Char char c = - satisfy ((==) c) ("expected " ++ toString c) + satisfy ((==) c) |> onerror ("expected " ++ String.fromChar c) + + +charList : List Char -> String +charList chars = + "[" + ++ String.join ", " (List.map (\c -> "'" ++ String.fromChar c ++ "'") chars) + ++ "]" {-| Parse any character. @@ -69,7 +89,32 @@ char c = -} anyChar : Parser s Char anyChar = - satisfy (always True) "expected any character" + satisfy (always True) |> onerror "expected any character" + + +{-| Peek at the next character without consuming any input. +Returns `Nothing` if at end of input. + + parse peekChar "abc" == + -- Ok (Just 'a') + + parse (peekChar |> Combine.ignore (char 'a')) "abc" == + -- Ok (Just 'a') + + parse peekChar "" == + -- Ok Nothing + +-} +peekChar : Parser s (Maybe Char) +peekChar = + primitive <| + \state stream -> + case String.uncons stream.input of + Just ( c, _ ) -> + ( state, stream, Ok (Just c) ) + + Nothing -> + ( state, stream, Ok Nothing ) {-| Parse a character from the given list. @@ -83,7 +128,8 @@ anyChar = -} oneOf : List Char -> Parser s Char oneOf cs = - satisfy (flip List.member cs) ("expected one of " ++ toString cs) + satisfy (flip List.member cs) + |> onerror ("expected one of " ++ charList cs) {-| Parse a character that is not in the given list. @@ -97,74 +143,170 @@ oneOf cs = -} noneOf : List Char -> Parser s Char noneOf cs = - satisfy (not << flip List.member cs) ("expected none of " ++ toString cs) + satisfy (not << flip List.member cs) |> onerror ("expected none of " ++ charList cs) {-| Parse a space character. + + parse space " " == Ok ' ' + + parse space "a" == Err [ "expected a space" ] + -} space : Parser s Char space = - satisfy ((==) ' ') "expected space" + satisfy ((==) ' ') |> onerror "expected a space" {-| Parse a `\t` character. + + parse tab "\t" == Ok '\t' + + parse tab "a" == Err [ "expected a tab" ] + -} tab : Parser s Char tab = - satisfy ((==) '\t') "expected tab" + satisfy ((==) '\t') |> onerror "expected a tab" {-| Parse a `\n` character. + + parse newline "\n" == Ok '\n' + + parse newline "a" == Err [ "expected a newline" ] + -} newline : Parser s Char newline = - satisfy ((==) '\n') "expected newline" + satisfy ((==) '\n') |> onerror "expected a newline" {-| Parse a `\r\n` sequence, returning a `\n` character. + + parse crlf "\u{000D}\n" == Ok '\n' + + parse crlf "\n" == Err [ "expected CRLF" ] + + parse crlf "\u{000D}" == Err [ "expected CRLF" ] + -} crlf : Parser s Char crlf = - '\n' <$ regex "\x0D\n" "expected crlf" + string "\u{000D}\n" |> onsuccess '\n' |> onerror "expected CRLF" {-| Parse an end of line character or sequence, returning a `\n` character. + + parse eol "\n" == Ok '\n' + + parse eol "\u{000D}\n" == Ok '\n' + + parse eol "\u{000D}" == Ok '\n' + + parse eol "a" == Err [ "expected an end of line character" ] + -} eol : Parser s Char eol = - newline <|> crlf + or newline crlf {-| Parse any lowercase character. + + parse lower "a" == Ok 'a' + + parse lower "A" == Err [ "expected a lowercase character" ] + -} lower : Parser s Char lower = - satisfy Char.isLower "expected a lowercase character" + satisfy Char.isLower |> onerror "expected a lowercase character" {-| Parse any uppercase character. + + parse upper "A" == Ok 'A' + + parse upper "a" == Err [ "expected an uppercase character" ] + -} upper : Parser s Char upper = - satisfy Char.isUpper "expected an uppercase character" + satisfy Char.isUpper |> onerror "expected an uppercase character" {-| Parse any base 10 digit. + + parse digit "0" == Ok '0' + + parse digit "9" == Ok '9' + + parse digit "a" == Err [ "expected a digit" ] + -} digit : Parser s Char digit = - satisfy Char.isDigit "expected a digit" + satisfy Char.isDigit |> onerror "expected a digit" {-| Parse any base 8 digit. + + parse octDigit "0" == Ok '0' + + parse octDigit "7" == Ok '7' + + parse octDigit "8" == Err [ "expected an octal digit" ] + -} octDigit : Parser s Char octDigit = - satisfy Char.isOctDigit "expected an octal digit" + satisfy Char.isOctDigit |> onerror "expected an octal digit" {-| Parse any base 16 digit. + + parse hexDigit "0" == Ok '0' + + parse hexDigit "7" == Ok '7' + + parse hexDigit "a" == Ok 'a' + + parse hexDigit "f" == Ok 'f' + + parse hexDigit "g" == Err [ "expected a hexadecimal digit" ] + -} hexDigit : Parser s Char hexDigit = - satisfy Char.isHexDigit "expected a hexadecimal digit" + satisfy Char.isHexDigit |> onerror "expected a hexadecimal digit" + + +{-| Parse any alphabetic character. + + parse alpha "a" == Ok 'a' + + parse alpha "A" == Ok 'A' + + parse alpha "0" == Err [ "expected an alphabetic character" ] + +-} +alpha : Parser s Char +alpha = + satisfy Char.isAlpha |> onerror "expected an alphabetic character" + + +{-| Parse any alphanumeric character. + + parse alphaNum "a" == Ok 'a' + + parse alphaNum "A" == Ok 'A' + + parse alphaNum "0" == Ok '0' + + parse alphaNum "-" == Err [ "expected an alphanumeric character" ] + +-} +alphaNum : Parser s Char +alphaNum = + satisfy Char.isAlphaNum |> onerror "expected an alphanumeric character" diff --git a/src/Combine/Num.elm b/src/Combine/Num.elm index 2fe6a3f..7a46d8a 100644 --- a/src/Combine/Num.elm +++ b/src/Combine/Num.elm @@ -1,82 +1,91 @@ -module Combine.Num - exposing - ( sign - , digit - , int - , float - ) +module Combine.Num exposing (sign, digit, int, float) {-| This module contains Parsers specific to parsing numbers. + # Parsers + @docs sign, digit, int, float + -} import Char -import Combine exposing (..) +import Combine exposing (Parser, andThen, fail, map, onerror, onsuccess, optional, or, regex, string, succeed) import Combine.Char import String -unwrap : (String -> Result x res) -> String -> res -unwrap f s = - case f s of - Ok res -> - res - - Err m -> - Debug.crash ("impossible state in Combine.Num.unwrap: " ++ toString m) - - -toInt : String -> Int -toInt = - unwrap String.toInt +{-| Parse a numeric sign, returning `1` for positive numbers and `-1` +for negative numbers. + parse sign "+" == Ok 1 -toFloat : String -> Float -toFloat = - unwrap String.toFloat + parse sign "-" == Ok -1 + parse sign "a" == Err [ "expected a sign" ] -{-| Parse a numeric sign, returning `1` for positive numbers and `-1` -for negative numbers. -} sign : Parser s Int sign = optional 1 - (choice - [ 1 <$ string "+" - , -1 <$ string "-" - ] + (or + (string "+" |> onsuccess 1) + (string "-" |> onsuccess -1) ) {-| Parse a digit. + + parse digit "1" == Ok 1 + + parse digit "a" == Err [ "expected a digit" ] + -} digit : Parser s Int digit = - let - toDigit c = - Char.toCode c - Char.toCode '0' - in - toDigit <$> Combine.Char.digit "expected a digit" + Combine.Char.digit + -- 48 is the ASCII code for '0' + |> map (\c -> Char.toCode c - 48) + |> onerror "expected a digit" {-| Parse an integer. + + parse int "123" == Ok 123 + + parse int "-123" == Ok -123 + + parse int "abc" == Err [ "expected an int" ] + -} int : Parser s Int int = - (*) - <$> sign - <*> (toInt <$> regex "(0|[1-9][0-9]*)") - "expected an integer" + regex "-?(?:0|[1-9]\\d*)" + |> andThen (String.toInt >> unwrap) + |> onerror "expected an int" {-| Parse a float. + + parse float "123.456" == Ok 123.456 + + parse float "-123.456" == Ok -123.456 + + parse float "abc" == Err [ "expected a float" ] + -} float : Parser s Float float = - ((*) << Basics.toFloat) - <$> sign - <*> (toFloat <$> regex "(0|[1-9][0-9]*)(\\.[0-9]+)") - "expected a float" + regex "-?(?:0|[1-9]\\d*)\\.\\d+" + |> andThen (String.toFloat >> unwrap) + |> onerror "expected a float" + + +unwrap : Maybe v -> Parser s v +unwrap value = + case value of + Just v -> + succeed v + + Nothing -> + fail "impossible state in Combine.Num.unwrap" diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index b176e88..0000000 --- a/tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/elm-stuff -elm.js -index.html \ No newline at end of file diff --git a/tests/CurrentLocationTests.elm b/tests/CurrentLocationTests.elm index 8254ec3..2eb4d76 100644 --- a/tests/CurrentLocationTests.elm +++ b/tests/CurrentLocationTests.elm @@ -1,10 +1,18 @@ -module CurrentLocationTests exposing (..) +module CurrentLocationTests exposing (entryPoint, noNegativeValuesForColumn, noNegativeValuesForLine, specificLocationTests) -import Combine exposing (..) +import Combine import Combine.Char +import Dict import Expect import Fuzz -import Test exposing (..) +import Test + exposing + ( Test + , describe + , fuzz + , fuzz2 + , test + ) entryPoint : Test @@ -12,22 +20,22 @@ entryPoint = describe "entry point" [ test "column should be zero with empty input" <| \() -> - { data = "", input = "", position = 0 } + { data = "", input = "", position = 0, lazyTracking = Dict.empty } |> Combine.currentColumn |> Expect.equal 0 , test "column should be zero with two lines" <| \() -> - { data = "\n", input = "\n", position = 0 } + { data = "\n", input = "\n", position = 0, lazyTracking = Dict.empty } |> Combine.currentColumn |> Expect.equal 0 , fuzz Fuzz.string "column should be zero" <| \s -> - { data = s, input = s, position = 0 } + { data = s, input = s, position = 0, lazyTracking = Dict.empty } |> Combine.currentColumn |> Expect.equal 0 , fuzz Fuzz.string "line should be zero" <| \s -> - { data = s, input = s, position = 0 } + { data = s, input = s, position = 0, lazyTracking = Dict.empty } |> Combine.currentLine |> Expect.equal 0 ] @@ -38,12 +46,12 @@ specificLocationTests = describe "specific locations" [ test "should not skip to next line on eol" <| \() -> - { data = "x\ny", input = "x\ny", position = 1 } + { data = "x\ny", input = "x\ny", position = 1, lazyTracking = Dict.empty } |> Combine.currentLocation |> Expect.equal { source = "x", line = 0, column = 1 } , test "should skip to next line on eol + 1" <| \() -> - { data = "x\ny", input = "x\ny", position = 2 } + { data = "x\ny", input = "x\ny", position = 2, lazyTracking = Dict.empty } |> Combine.currentLocation |> Expect.equal { source = "y", line = 1, column = 0 } ] @@ -57,21 +65,22 @@ noNegativeValuesForColumn = c = if String.length s == 0 then 0 + else - (i % String.length s) + modBy (String.length s) i in - case - Combine.parse - (Combine.count c Combine.Char.anyChar - *> (Combine.withColumn Combine.succeed) - ) - s - of - Err _ -> - Expect.fail "Should always parse" + case + Combine.parse + (Combine.count c Combine.Char.anyChar + |> Combine.keep (Combine.withColumn Combine.succeed) + ) + s + of + Err _ -> + Expect.fail "Should always parse" - Ok ( _, _, v ) -> - Expect.greaterThan -1 v + Ok ( _, _, v ) -> + Expect.greaterThan -1 v noNegativeValuesForLine : Test @@ -82,18 +91,19 @@ noNegativeValuesForLine = c = if String.length s == 0 then 0 + else - (i % String.length s) + modBy (String.length s) i in - case - Combine.parse - (Combine.count c Combine.Char.anyChar - *> (Combine.withLine Combine.succeed) - ) - s - of - Err _ -> - Expect.fail "Should always parse" + case + Combine.parse + (Combine.count c Combine.Char.anyChar + |> Combine.keep (Combine.withLine Combine.succeed) + ) + s + of + Err _ -> + Expect.fail "Should always parse" - Ok ( _, _, v ) -> - Expect.greaterThan -1 v + Ok ( _, _, v ) -> + Expect.greaterThan -1 v diff --git a/tests/Parsers.elm b/tests/Parsers.elm index 1c37c1c..bcda447 100644 --- a/tests/Parsers.elm +++ b/tests/Parsers.elm @@ -1,8 +1,26 @@ -module Parsers exposing (..) +module Parsers exposing (manyTillSuite, sepEndBy1Suite, sepEndBySuite, sequenceSuite) -import Calc exposing (calc) -import Combine exposing (..) -import Combine.Char exposing (..) +--import Calc exposing (calc) + +import Combine + exposing + ( Parser + , keep + , many + , manyTill + , parse + , sepEndBy + , sepEndBy1 + , sequence + , string + ) +import Combine.Char + exposing + ( anyChar + , eol + , space + ) +import Dict import Expect import String import Test exposing (Test, describe, test) @@ -20,34 +38,38 @@ successful desc p s r = Expect.fail <| String.join ", " ms -calcSuite : Test -calcSuite = - let - equiv s x () = - Expect.equal (calc s) (Ok x) - in - describe "calc example tests" - [ test "Atoms" (equiv "1" 1) - , test "Atoms 2" (equiv "-1" -1) - , test "Parenthesized atoms" (equiv "(1)" 1) - , test "Addition" (equiv "1 + 1" 2) - , test "Subtraction" (equiv "1 - 1" 0) - , test "Multiplication" (equiv "1 * 1" 1) - , test "Division" (equiv "1 / 1" 1) - , test "Precedence 1" (equiv "1 + 2 * 3" 7) - , test "Precedence 2" (equiv "1 + 2 * 3 * 2" 13) - , test "Parenthesized precedence" (equiv "(1 + 2) * 3 * 2" 18) - ] + +{- + calcSuite : Test + calcSuite = + let + equiv s x () = + Expect.equal (calc s) (Ok x) + in + describe "calc example tests" + [ test "Atoms" (equiv "1" 1) + , test "Atoms 2" (equiv "-1" -1) + , test "Parenthesized atoms" (equiv "(1)" 1) + , test "Addition" (equiv "1 + 1" 2) + , test "Subtraction" (equiv "1 - 1" 0) + , test "Multiplication" (equiv "1 * 1" 1) + , test "Division" (equiv "1 / 1" 1) + , test "Precedence 1" (equiv "1 + 2 * 3" 7) + , test "Precedence 2" (equiv "1 + 2 * 3 * 2" 13) + , test "Parenthesized precedence" (equiv "(1 + 2) * 3 * 2" 18) + ] + +-} manyTillSuite : Test manyTillSuite = let comment = - string "") + string "")) line = - manyTill anyChar (many space *> eol) + manyTill anyChar (many space |> keep eol) in describe "manyTill tests" [ successful "Example" comment "" [ ' ', 't', 'e', 's', 't', ' ' ] @@ -81,22 +103,22 @@ sepEndBy1Suite = \() -> Expect.equal (parse commaSep "a,a,a") - (Ok ( (), { data = "a,a,a", input = "", position = 5 }, [ "a", "a", "a" ] )) + (Ok ( (), { data = "a,a,a", input = "", position = 5, lazyTracking = Dict.empty }, [ "a", "a", "a" ] )) , test "sepEndBy1 2" <| \() -> Expect.equal (parse commaSep "b") - (Err ( (), { data = "b", input = "b", position = 0 }, [ "expected \"a\"" ] )) + (Err ( (), { data = "b", input = "b", position = 0, lazyTracking = Dict.empty }, [ "expected \"a\"" ] )) , test "sepEndBy1 3" <| \() -> Expect.equal (parse commaSep "a,a,a,") - (Ok ( (), { data = "a,a,a,", input = "", position = 6 }, [ "a", "a", "a" ] )) + (Ok ( (), { data = "a,a,a,", input = "", position = 6, lazyTracking = Dict.empty }, [ "a", "a", "a" ] )) , test "sepEndBy1 4" <| \() -> Expect.equal (parse commaSep "a,a,b") - (Ok ( (), { data = "a,a,b", input = "b", position = 4 }, [ "a", "a" ] )) + (Ok ( (), { data = "a,a,b", input = "b", position = 4, lazyTracking = Dict.empty }, [ "a", "a" ] )) ] @@ -107,20 +129,20 @@ sequenceSuite = \() -> Expect.equal (parse (sequence []) "a") - (Ok ( (), { data = "a", input = "a", position = 0 }, [] )) + (Ok ( (), { data = "a", input = "a", position = 0, lazyTracking = Dict.empty }, [] )) , test "one parser" <| \() -> Expect.equal (parse (sequence [ many <| string "a" ]) "aaaab") - (Ok ( (), { data = "aaaab", input = "b", position = 4 }, [ [ "a", "a", "a", "a" ] ] )) + (Ok ( (), { data = "aaaab", input = "b", position = 4, lazyTracking = Dict.empty }, [ [ "a", "a", "a", "a" ] ] )) , test "many parsers" <| \() -> Expect.equal (parse (sequence [ string "a", string "b", string "c" ]) "abc") - (Ok ( (), { data = "abc", input = "", position = 3 }, [ "a", "b", "c" ] )) + (Ok ( (), { data = "abc", input = "", position = 3, lazyTracking = Dict.empty }, [ "a", "b", "c" ] )) , test "many parsers failure" <| \() -> Expect.equal (parse (sequence [ string "a", string "b", string "c" ]) "abd") - (Err ( (), { data = "abd", input = "d", position = 2 }, [ "expected \"c\"" ] )) + (Err ( (), { data = "abd", input = "d", position = 2, lazyTracking = Dict.empty }, [ "expected \"c\"" ] )) ] diff --git a/tests/elm-package.json b/tests/elm-package.json deleted file mode 100644 index 3fc7480..0000000 --- a/tests/elm-package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "1.0.0", - "summary": "parser-combinators test suite", - "repository": "https://github.com/elm-community/parser-combinators.git", - "license": "BSD3", - "source-directories": [".", "../examples", "../src"], - "exposed-modules": [ - "Calc", - "Combine", - "Combine.Char", - "Combine.Infix", - "Combine.Num" - ], - "dependencies": { - "elm-community/elm-test": "4.0.0 <= v < 5.0.0", - "elm-lang/core": "5.0.0 <= v < 6.0.0", - "elm-lang/lazy": "2.0.0 <= v < 3.0.0" - }, - "elm-version": "0.18.0 <= v < 0.19.0" -}