diff --git a/gleam.toml b/gleam.toml index ce0de42..04d1794 100644 --- a/gleam.toml +++ b/gleam.toml @@ -10,7 +10,6 @@ links = [ [dependencies] gleam_stdlib = "~> 0.34 or ~> 1.0" -pears = "~> 0.3" party = "~> 1.0" [dev-dependencies] diff --git a/manifest.toml b/manifest.toml index 54f9714..73df247 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,13 +3,11 @@ packages = [ { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, - { name = "party", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "party", source = "hex", outer_checksum = "13D70704E008F1AB163E6E5AED717014473898100B26852E158BD6E9BAE583BC" }, - { name = "pears", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "pears", source = "hex", outer_checksum = "F823EDF5C7F8606A0B7764C071B6EE8515FC341D6FC69902E81120633DF7E983" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, + { name = "party", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "party", source = "hex", outer_checksum = "6169240A11DE6793FDFB77E7375E79441B6D7FB3D3EF8274B3C109ABAFF1A8FD" }, ] [requirements] gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } -party = { version = "~> 1.0"} -pears = { version = "~> 0.3" } +party = { version = "~> 1.0" } diff --git a/src/jasper.gleam b/src/jasper.gleam index 4691319..7c6d713 100644 --- a/src/jasper.gleam +++ b/src/jasper.gleam @@ -4,21 +4,16 @@ import gleam/string import gleam/result import gleam/dict.{type Dict} import gleam/list -import jasper/internal/parsing.{ - type ParseError, type Parser, Parser, Position, Unexpected, between, char, - choice, choice_char, concat, digit, do, either, end, go, lazy, left, letter, - many, many1_concat, many_concat, map, pair, perhaps, perhaps_default, - perhaps_empty, return, right, satisfy, sep, string, to, +import party.{ + type ParseError, type Parser, char, choice, digit, do, either, end, go, lazy, + letter, many1_concat, many_concat, map, perhaps, return, satisfy, sep, string, + until, } -pub type JsonParseError = - Nil - -pub type JsonParserError = - ParseError(JsonParseError) - -pub type JsonParser(a) = - Parser(a, JsonParseError) +pub fn to(p: Parser(a, e), v: b) -> Parser(b, e) { + use _ <- do(p) + return(v) +} pub type JsonObject = Dict(String, JsonValue) @@ -35,8 +30,26 @@ pub type JsonValue { Null } -fn ws(options: JsonParseOptions) -> JsonParser(String) { - many_concat( +fn multiline_comment() -> Parser(String, e) { + use _ <- do(string("/*")) + use comment <- do(until(satisfy(fn(_) { True }), string("*/"))) + return(string.concat(comment)) +} + +fn singleline_comment() -> Parser(String, e) { + use _ <- do(string("//")) + use comment <- do( + many_concat(satisfy(fn(c) { !string.contains("\r\n\u{2028}\u{2029}", c) })), + ) + return(comment) +} + +fn comment() -> Parser(String, e) { + either(multiline_comment(), singleline_comment()) +} + +fn ws(options: JsonParseOptions) -> Parser(String, e) { + let ws = satisfy(fn(c) { string.contains( case options.ecma_whitespace { @@ -46,52 +59,46 @@ fn ws(options: JsonParseOptions) -> JsonParser(String) { }, c, ) - }), - ) + }) + many_concat(case options.comments { + True -> either(ws, comment()) + False -> ws + }) } -fn padded(p: JsonParser(a), options: JsonParseOptions) -> JsonParser(a) { - left(p, ws(options)) +fn symbol(s: String, options: JsonParseOptions) -> Parser(String, e) { + use _ <- do(ws(options)) + use c <- do(char(s)) + return(c) } -fn symbol(s: String, options: JsonParseOptions) -> JsonParser(String) { - char(s) - |> padded(options) +fn json_null() -> Parser(Nil, e) { + to(string("null"), Nil) } -fn json_null_parser() -> JsonParser(Nil) { - string("null") - |> to(Nil) -} - -fn json_boolean_parser() -> JsonParser(Bool) { +fn json_boolean() -> Parser(Bool, e) { either(to(string("true"), True), to(string("false"), False)) } -fn hex_digit_parser() -> JsonParser(String) { - choice_char([ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", - "f", "A", "B", "C", "D", "E", "F", - ]) +fn hex_digit() -> Parser(String, e) { + satisfy(fn(c) { string.contains("0123456789abcdefABCDEF", c) }) } -fn json_number_parser(options: JsonParseOptions) -> JsonParser(Float) { - let sign_parser = - either(char("+"), char("-")) - |> perhaps_empty +fn json_number(options: JsonParseOptions) -> Parser(Float, e) { + let sign_parser = perhaps(either(char("+"), char("-"))) let int_parser = - either( - char("0"), - concat( - choice_char(["1", "2", "3", "4", "5", "6", "7", "8", "9"]), - many_concat(digit()), - ), - ) + either(char("0"), { + use d <- do(satisfy(fn(c) { string.contains("123456789", c) })) + use ds <- do(many_concat(digit())) + return(d <> ds) + }) let exp_parser = - either(char("e"), char("E")) - |> concat(sign_parser) - |> concat(many1_concat(digit())) - |> perhaps_empty + perhaps({ + use e <- do(either(char("e"), char("E"))) + use sign <- do(sign_parser) + use digits <- do(many1_concat(digit())) + return(e <> result.unwrap(sign, "") <> digits) + }) let min_double = -1.7976931348623158e308 let max_double = 1.7976931348623158e308 @@ -99,38 +106,41 @@ fn json_number_parser(options: JsonParseOptions) -> JsonParser(Float) { True -> { use sign <- do(sign_parser) let inf = case sign { - "-" -> min_double + Ok("-") -> min_double _ -> max_double } choice([ to(string("Infinity"), inf), to(string("NaN"), 0.0), { - use hex <- do( - either(string("0x"), string("0X")) - |> right(many1_concat(hex_digit_parser())), - ) - let assert Ok(n) = int.base_parse(sign <> hex, 16) + use _ <- do(either(string("0x"), string("0X"))) + use hex <- do(many1_concat(hex_digit())) + let assert Ok(n) = int.base_parse(result.unwrap(sign, "") <> hex, 16) return(int.to_float(n)) }, { - use n <- do(either( - { - use ns <- do(int_parser) - use ds <- do( - right( - char("."), - many1_concat(digit()) - |> perhaps_default("0"), + use n <- do( + either( + { + use ns <- do(int_parser) + use ds <- do( + perhaps({ + use _ <- do(char(".")) + use ds <- do(perhaps(many1_concat(digit()))) + return(result.unwrap(ds, "0")) + }), ) - |> perhaps_default("0"), - ) - return(ns <> "." <> ds) - }, - concat(to(char("."), "0."), many1_concat(digit())), - )) + return(ns <> "." <> result.unwrap(ds, "0")) + }, + { + use _ <- do(char(".")) + use ds <- do(many1_concat(digit())) + return("0." <> ds) + }, + ), + ) use ex <- do(exp_parser) - { sign <> n <> ex } + { result.unwrap(sign, "") <> n <> result.unwrap(ex, "") } |> float.parse |> result.unwrap(inf) |> return @@ -138,21 +148,27 @@ fn json_number_parser(options: JsonParseOptions) -> JsonParser(Float) { ]) } False -> { - use sign <- do( - char("-") - |> perhaps_empty, - ) + use sign <- do(perhaps(char("-"))) let inf = case sign { - "-" -> min_double + Ok("-") -> min_double _ -> max_double } use ns <- do(int_parser) use ds <- do( - right(char("."), many1_concat(digit())) - |> perhaps_default("0"), + perhaps({ + use _ <- do(char(".")) + use ds <- do(many1_concat(digit())) + return(ds) + }), ) use ex <- do(exp_parser) - { sign <> ns <> "." <> ds <> ex } + { + result.unwrap(sign, "") + <> ns + <> "." + <> result.unwrap(ds, "0") + <> result.unwrap(ex, "") + } |> float.parse |> result.unwrap(inf) |> return @@ -172,29 +188,27 @@ fn valid_string_char(c: String) -> Bool { i >= 0x20 && i <= 0x10FFFF } -fn json_string_parser(options: JsonParseOptions) -> JsonParser(String) { - let hex_digit = hex_digit_parser() - let unicode_escape = - map( - right( - char("u"), - hex_digit - |> concat(hex_digit) - |> concat(hex_digit) - |> concat(hex_digit), - ), - string_to_codepoint, - ) +fn json_string(options: JsonParseOptions) -> Parser(String, e) { + let unicode_escape = { + use _ <- do(char("u")) + use a <- do(hex_digit()) + use b <- do(hex_digit()) + use c <- do(hex_digit()) + use d <- do(hex_digit()) + return(string_to_codepoint(a <> b <> c <> d)) + } - let escape = - char("\\") - |> right( + let escape = { + use _ <- do(char("\\")) + use c <- do( choice(case options.ecma_strings { True -> [ - map( - right(char("x"), concat(hex_digit, hex_digit)), - string_to_codepoint, - ), + { + use _ <- do(char("x")) + use a <- do(hex_digit()) + use b <- do(hex_digit()) + return(string_to_codepoint(a <> b)) + }, unicode_escape, char("\\"), char("/"), @@ -232,12 +246,19 @@ fn json_string_parser(options: JsonParseOptions) -> JsonParser(String) { ] }), ) + return(c) + } let str = fn(q) { - satisfy(fn(c) { c != q && c != "\\" && valid_string_char(c) }) - |> either(escape) - |> many_concat() - |> between(char(q), char(q)) + use _ <- do(char(q)) + use str <- do( + many_concat(either( + escape, + satisfy(fn(c) { c != q && c != "\\" && valid_string_char(c) }), + )), + ) + use _ <- do(char(q)) + return(str) } case options.ecma_strings { @@ -246,106 +267,67 @@ fn json_string_parser(options: JsonParseOptions) -> JsonParser(String) { } } -fn parse_multiline_comment() -> JsonParser(String) { - Parser(fn(source, pos) { - let assert Position(row, col) = pos - case source { - [h, ..t] -> - case h { - "\n" -> Ok(#(h, t, Position(row + 1, 0))) - "*" -> - case t { - ["/", ..] -> Error(Unexpected(pos, h)) - _ -> Ok(#(h, t, Position(row, col + 1))) - } - _ -> Ok(#(h, t, Position(row, col + 1))) - } - [] -> Error(Unexpected(pos, "EOF")) - } - }) - |> many_concat - |> between(string("/*"), string("*/")) -} - -fn parse_singleline_comment() -> JsonParser(String) { - right( - string("//"), - many_concat(satisfy(fn(c) { !string.contains("\r\n\u{2028}\u{2029}", c) })), - ) -} - -fn comment_parser(options: JsonParseOptions) -> JsonParser(String) { - either(parse_multiline_comment(), parse_singleline_comment()) - |> padded(options) -} - -fn allow_comments(options: JsonParseOptions, p: Parser(_, _)) { - case options.comments { - True -> left(p, many(comment_parser(options))) - False -> p - } -} - -fn allow_trailing_comma(options: JsonParseOptions, p: Parser(_, _)) { +fn allow_trailing_comma(p: Parser(a, e), options: JsonParseOptions) { case options.trailing_comma { - True -> right(perhaps(symbol(",", options)), p) + True -> { + use _ <- do(perhaps(symbol(",", options))) + use v <- do(p) + return(v) + } False -> p } } -fn json_array_parser(options: JsonParseOptions) -> JsonParser(JsonArray) { - let json_value_parser = lazy(fn() { json_value_parser(options) }) - let allow_comments = allow_comments(options, _) - sep(json_value_parser, allow_comments(symbol(",", options))) - |> between( - allow_comments(symbol("[", options)), - allow_trailing_comma(options, symbol("]", options)), - ) +fn json_array(options: JsonParseOptions) -> Parser(JsonArray, e) { + use _ <- do(symbol("[", options)) + use values <- do(sep(lazy(fn() { json_value(options) }), symbol(",", options))) + use _ <- do(allow_trailing_comma(symbol("]", options), options)) + return(values) } // todo -fn ecmascript_identifier_parser() -> JsonParser(String) { - letter() - |> concat( - either(letter(), digit()) - |> many_concat, - ) +fn ecmascript_identifier() -> Parser(String, e) { + use c <- do(letter()) + use cs <- do(many_concat(either(letter(), digit()))) + return(c <> cs) } -fn json_object_parser(options: JsonParseOptions) -> JsonParser(JsonObject) { - let json_value_parser = lazy(fn() { json_value_parser(options) }) - let allow_comments = allow_comments(options, _) - let key_parser = case options.ecma_object_keys { - True -> either(json_string_parser(options), ecmascript_identifier_parser()) - False -> json_string_parser(options) - } - allow_comments(key_parser) - |> left(allow_comments(symbol(":", options))) - |> pair(json_value_parser) - |> sep(allow_comments(symbol(",", options))) - |> map(dict.from_list) - |> between( - allow_comments(symbol("{", options)), - allow_trailing_comma(options, symbol("}", options)), - ) +fn json_object(options: JsonParseOptions) -> Parser(JsonObject, e) { + use _ <- do(symbol("{", options)) + use key_value_pairs <- do(sep( + { + use _ <- do(ws(options)) + use key <- do(case options.ecma_object_keys { + True -> either(json_string(options), ecmascript_identifier()) + False -> json_string(options) + }) + use _ <- do(symbol(":", options)) + use value <- do(lazy(fn() { json_value(options) })) + return(#(key, value)) + }, + symbol(",", options), + )) + use _ <- do(allow_trailing_comma(symbol("}", options), options)) + return(dict.from_list(key_value_pairs)) } -fn json_value_parser(options: JsonParseOptions) -> JsonParser(JsonValue) { +fn json_value(options: JsonParseOptions) -> Parser(JsonValue, e) { + use _ <- do(ws(options)) choice([ - to(json_null_parser(), Null), - map(json_boolean_parser(), Boolean), - map(json_number_parser(options), Number), - map(json_string_parser(options), String), - map(json_array_parser(options), Array), - map(json_object_parser(options), Object), + to(json_null(), Null), + map(json_boolean(), Boolean), + map(json_number(options), Number), + map(json_string(options), String), + map(json_array(options), Array), + map(json_object(options), Object), ]) - |> padded(options) - |> allow_comments(options, _) } -fn json_parser(options: JsonParseOptions) -> JsonParser(JsonValue) { - json_value_parser(options) - |> between(allow_comments(options, ws(options)), end()) +fn json_parser(options: JsonParseOptions) -> Parser(JsonValue, e) { + use value <- do(json_value(options)) + use _ <- do(ws(options)) + use _ <- do(end()) + return(value) } pub type JsonParseOptions { @@ -391,19 +373,19 @@ const jsonl_options = json_options pub fn parse_json_custom( value: String, options: JsonParseOptions, -) -> Result(JsonValue, JsonParserError) { +) -> Result(JsonValue, ParseError(e)) { go(json_parser(options), value) } -pub fn parse_json(value: String) -> Result(JsonValue, JsonParserError) { +pub fn parse_json(value: String) -> Result(JsonValue, ParseError(e)) { parse_json_custom(value, json_options) } -pub fn parse_jsonc(value: String) -> Result(JsonValue, JsonParserError) { +pub fn parse_jsonc(value: String) -> Result(JsonValue, ParseError(e)) { parse_json_custom(value, jsonc_options) } -pub fn parse_json5(value: String) -> Result(JsonValue, JsonParserError) { +pub fn parse_json5(value: String) -> Result(JsonValue, ParseError(e)) { parse_json_custom(value, json5_options) } @@ -415,14 +397,12 @@ fn split_jsonl(value: String) -> List(String) { |> string.split("\n") } -pub fn parse_jsonl(value: String) -> Result(List(JsonValue), JsonParserError) { +pub fn parse_jsonl(value: String) -> Result(List(JsonValue), ParseError(e)) { let parse = go(json_parser(jsonl_options), _) list.try_map(split_jsonl(value), parse) } -pub fn parse_jsonl_all( - value: String, -) -> List(Result(JsonValue, JsonParserError)) { +pub fn parse_jsonl_all(value: String) -> List(Result(JsonValue, ParseError(e))) { let parse = go(json_parser(jsonl_options), _) list.map(split_jsonl(value), parse) } diff --git a/src/jasper/internal/parsing.gleam b/src/jasper/internal/parsing.gleam deleted file mode 100644 index 3bd287a..0000000 --- a/src/jasper/internal/parsing.gleam +++ /dev/null @@ -1,417 +0,0 @@ -import gleam/string -import gleam/result -import gleam/list - -/// The custom error type for the parser, -/// which can itself be parameterized by a user-defined error type. -/// The user-defined error type is useful for, for example, -/// adding a `int.parse` call into your parser pipeline. -/// See `try` for using this feature. -pub type ParseError(e) { - Unexpected(pos: Position, error: String) - UserError(pos: Position, error: e) -} - -/// The type for positions within a string. -pub type Position { - Position(row: Int, col: Int) -} - -/// The parser type, parameterized by the type it parses and -/// the user-defined error type it can return. -pub type Parser(a, e) { - Parser( - parse: fn(List(String), Position) -> - Result(#(a, List(String), Position), ParseError(e)), - ) -} - -/// Apply a parser to a list of graphemes (holding on to extra result info that is hidden from the library user). -fn run( - p: Parser(a, e), - src: List(String), - pos: Position, -) -> Result(#(a, List(String), Position), ParseError(e)) { - case p { - Parser(f) -> f(src, pos) - } -} - -/// Apply a parser to a string. -pub fn go(p: Parser(a, e), src: String) -> Result(a, ParseError(e)) { - case run(p, string.to_graphemes(src), Position(1, 1)) { - Ok(#(x, _, _)) -> Ok(x) - Error(e) -> Error(e) - } -} - -/// Get the current parser position. -pub fn pos() -> Parser(Position, e) { - Parser(fn(source, p) { Ok(#(p, source, p)) }) -} - -/// Parse a character if it matches the predicate. -pub fn satisfy(when pred: fn(String) -> Bool) -> Parser(String, e) { - Parser(fn(source, pos) { - let assert Position(row, col) = pos - case source { - [h, ..t] -> - case pred(h) { - True -> - case h { - "\n" -> Ok(#(h, t, Position(row + 1, 0))) - _ -> Ok(#(h, t, Position(row, col + 1))) - } - False -> Error(Unexpected(pos, h)) - } - [] -> Error(Unexpected(pos, "EOF")) - } - }) -} - -/// Parse a lowercase letter. -pub fn lowercase_letter() -> Parser(String, e) { - satisfy(when: fn(c) { string.contains("abcdefghijklmnopqrstuvwxyz", c) }) -} - -/// Parse an uppercase letter. -pub fn uppercase_letter() -> Parser(String, e) { - satisfy(when: fn(c) { string.contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", c) }) -} - -/// Parse a lowercase or uppercase letter. -pub fn letter() -> Parser(String, e) { - either(lowercase_letter(), uppercase_letter()) -} - -/// Parse a specific character. -pub fn char(c) -> Parser(String, e) { - satisfy(when: fn(c2) { c == c2 }) -} - -/// Parse a digit. -pub fn digit() -> Parser(String, e) { - satisfy(fn(c) { string.contains("0123456789", c) }) -} - -/// Parse a sequence of digits. -pub fn digits() -> Parser(String, e) { - many1_concat(digit()) -} - -/// Parse the first parser, or the second if the first fails. -pub fn either(p: Parser(a, e), q: Parser(a, e)) -> Parser(a, e) { - Parser(fn(source, pos) { result.or(run(p, source, pos), run(q, source, pos)) }) -} - -/// Parse with the first parser in the list that doesn't fail. -pub fn choice(ps: List(Parser(a, e))) -> Parser(a, e) { - Parser(fn(source, pos) { - case ps { - [] -> panic as "choice doesn't accept an empty list of parsers" - // TODO: should this be an Unexpected instead? - [p] -> run(p, source, pos) - [p, ..t] -> - case run(p, source, pos) { - Ok(#(x, r, pos2)) -> Ok(#(x, r, pos2)) - Error(_) -> run(choice(t), source, pos) - } - } - }) -} - -/// Parse an alphanumeric character. -pub fn alphanum() -> Parser(String, e) { - either(digit(), letter()) -} - -/// Parse zero or more whitespace characters. -pub fn whitespace() -> Parser(String, e) { - many_concat(choice([char(" "), char("\t"), char("\n")])) -} - -/// Parse one or more whitespace characters. -pub fn whitespace1() -> Parser(String, e) { - many1_concat(choice([char(" "), char("\t"), char("\n")])) -} - -/// Keep trying the parser until it fails, and return the array of parsed results. -/// This cannot fail because it parses zero or more times! -pub fn many(p: Parser(a, e)) -> Parser(List(a), e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Error(_) -> Ok(#([], source, pos)) - Ok(#(x, r, pos2)) -> - result.map(run(many(p), r, pos2), fn(res) { - #([x, ..res.0], res.1, res.2) - }) - } - }) -} - -/// Parse a certain string as many times as possible, returning everything that was parsed. -/// This cannot fail because it parses zero or more times! -pub fn many_concat(p: Parser(String, e)) -> Parser(String, e) { - many(p) - |> map(string.concat) -} - -/// Keep trying the parser until it fails, and return the array of parsed results. -/// This can fail, because it must parse successfully at least once! -pub fn many1(p: Parser(a, e)) -> Parser(List(a), e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Error(e) -> Error(e) - Ok(#(x, r, pos2)) -> - result.map(run(many(p), r, pos2), fn(res) { - #([x, ..res.0], res.1, res.2) - }) - } - }) -} - -/// Parse a certain string as many times as possible, returning everything that was parsed. -/// This can fail, because it must parse successfully at least once! -pub fn many1_concat(p: Parser(String, e)) -> Parser(String, e) { - many1(p) - |> map(string.concat) -} - -/// Do the first parser, ignore its result, then do the second parser. -pub fn seq(p: Parser(a, e), q: Parser(b, e)) -> Parser(b, e) { - use _ <- do(p) - q -} - -/// Parse a sequence separated by the given separator parser. -pub fn sep(parser: Parser(a, e), by s: Parser(b, e)) -> Parser(List(a), e) { - use mb_a <- do(perhaps(parser)) - case mb_a { - Ok(a) -> { - use rest <- do(many(right(s, parser))) - return([a, ..rest]) - } - Error(Nil) -> return([]) - } -} - -/// Parse a sequence separated by the given separator parser. -/// This only succeeds if at least one element of the sequence was parsed. -pub fn sep1(parser: Parser(a, e), by s: Parser(b, e)) -> Parser(List(a), e) { - use sequence <- do(sep(parser, by: s)) - case sequence { - [] -> fail() - _ -> return(sequence) - } -} - -/// Do `p`, then apply `f` to the result if it succeeded. -pub fn map(p: Parser(a, e), f: fn(a) -> b) -> Parser(b, e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(#(x, r, pos2)) -> Ok(#(f(x), r, pos2)) - Error(e) -> Error(e) - } - }) -} - -/// Do `p`, the apply `f` to the result if it succeeded. -/// `f` itself can fail with the user-defined error type, -/// and if it does the result is a `UserError` with the error. -pub fn try(p: Parser(a, e), f: fn(a) -> Result(b, e)) -> Parser(b, e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(#(x, r, pos2)) -> - case f(x) { - Ok(a) -> Ok(#(a, r, pos2)) - Error(e) -> Error(UserError(pos2, e)) - } - Error(e) -> Error(e) - } - }) -} - -/// Transform the user-defined error type -/// with a user-provided conversion function. -pub fn error_map(p: Parser(a, e), f: fn(e) -> f) -> Parser(a, f) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(res) -> Ok(res) - Error(e) -> - case e { - UserError(pos, e) -> Error(UserError(pos, f(e))) - Unexpected(pos, s) -> Error(Unexpected(pos, s)) - } - } - }) -} - -/// Try running a parser, but still succeed (with `Error(Nil)`) if it failed. -pub fn perhaps(p: Parser(a, e)) -> Parser(Result(a, Nil), e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(#(x, r, pos2)) -> Ok(#(Ok(x), r, pos2)) - Error(_) -> Ok(#(Error(Nil), source, pos)) - } - }) -} - -/// Do each parser in the list, returning the result of the last parser. -pub fn all(ps: List(Parser(a, e))) -> Parser(a, e) { - case ps { - [p] -> p - [h, ..t] -> { - use _ <- do(h) - all(t) - } - _ -> panic as "all(parsers) doesn't accept an empty list of parsers" - } - // TODO: should this be an Unexpected instead? -} - -/// Parse an exact string of characters. -pub fn string(s: String) -> Parser(String, e) { - case string.pop_grapheme(s) { - Ok(#(h, t)) -> { - use c <- do(char(h)) - use rest <- do(string(t)) - return(c <> rest) - } - Error(_) -> return("") - } -} - -/// Negate a parser: if it succeeds, this fails, and vice versa. -/// Example: `seq(string("if"), not(either(alphanum(), char("_"))))` -pub fn not(p: Parser(a, e)) -> Parser(Nil, e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(_) -> Error(Unexpected(pos, "")) - // todo: better error message here (add a label system) - Error(_) -> Ok(#(Nil, source, pos)) - } - }) -} - -/// Parses successfully only when at the end of the input string. -pub fn end() -> Parser(Nil, e) { - Parser(fn(source, pos) { - case source { - [] -> Ok(#(Nil, source, pos)) - [h, ..] -> Error(Unexpected(pos, h)) - } - }) -} - -/// Run a parser as normal, but the parser itself isn't evaluated until it is used. -/// This is needed for recursive grammars, such as `E := n | E + E` where `n` is a number. -/// Example: `lazy(digit)` instead of `digit()`. -pub fn lazy(p: fn() -> Parser(a, e)) -> Parser(a, e) { - Parser(fn(source, pos) { run(p(), source, pos) }) -} - -/// A monadic bind for pleasant interplay with gleam's `use` syntax. -/// example: -/// ``` -/// fn identifier() -> Parser(String, e) { -/// use pos <- do(pos()) -/// use first <- do(lowercase_letter()) -/// use rest <- do(many(alt(alphanum(), char("_")))) -/// return(Ident(pos, first <> string.concat(rest))) -/// } -/// ``` -pub fn do(p: Parser(a, e), f: fn(a) -> Parser(b, e)) -> Parser(b, e) { - Parser(fn(source, pos) { - case run(p, source, pos) { - Ok(#(x, r, pos2)) -> run(f(x), r, pos2) - Error(e) -> Error(e) - } - }) -} - -/// A monadic return for pleasant interplay with gleam's `use` syntax. -/// see `do` for more details and an example. -/// This is redundant if the last `do` is a `map` instead. -/// But I prefer using it, stylistically. -pub fn return(x) { - Parser(fn(source, pos) { Ok(#(x, source, pos)) }) -} - -/// Immediately fail regardless of the next input -pub fn fail() -> Parser(a, e) { - Parser(fn(source, pos) { - case source { - [] -> Error(Unexpected(pos, "EOF")) - [h, ..] -> Error(Unexpected(pos, h)) - } - }) -} - -pub fn left(p: Parser(a, e), q: Parser(b, e)) -> Parser(a, e) { - use x <- do(p) - use _ <- do(q) - return(x) -} - -pub fn right(p: Parser(a, e), q: Parser(b, e)) -> Parser(b, e) { - use _ <- do(p) - use x <- do(q) - return(x) -} - -pub fn pair(p: Parser(a, e), q: Parser(b, e)) -> Parser(#(a, b), e) { - use x <- do(p) - use y <- do(q) - return(#(x, y)) -} - -pub fn between( - p: Parser(a, e), - q: Parser(b, e), - r: Parser(c, e), -) -> Parser(a, e) { - use _ <- do(q) - use x <- do(p) - use _ <- do(r) - return(x) -} - -pub fn concat(p: Parser(String, e), q: Parser(String, e)) -> Parser(String, e) { - use x <- do(p) - use y <- do(q) - return(x <> y) -} - -pub fn choice_char(vs: List(String)) -> Parser(String, e) { - vs - |> list.map(char) - |> choice -} - -pub fn perhaps_default(p: Parser(a, e), d: a) -> Parser(a, e) { - use x <- do(perhaps(p)) - return(result.unwrap(x, d)) -} - -pub fn perhaps_empty(p: Parser(String, e)) -> Parser(String, e) { - perhaps_default(p, "") -} - -pub fn to(p: Parser(a, e), v: b) -> Parser(b, e) { - use _ <- do(p) - return(v) -} - -pub fn any() -> Parser(String, e) { - Parser(fn(source, pos) { - let assert Position(row, col) = pos - case source { - [h, ..t] -> - case h { - "\n" -> Ok(#(h, t, Position(row + 1, 0))) - _ -> Ok(#(h, t, Position(row, col + 1))) - } - [] -> Error(Unexpected(pos, "EOF")) - } - }) -}