From 94cf9f113f131a8395346fef4fc3e87894cc21a0 Mon Sep 17 00:00:00 2001 From: Nabeel Valley <36758308+nabeelvalley@users.noreply.github.com> Date: Fri, 19 Jul 2024 21:49:13 +0200 Subject: [PATCH] :sparkles: labeling and mapping over results --- src/parz/combinators.gleam | 81 ++++++++++++++++++++++++++----------- src/parz/parsers.gleam | 14 +++---- test/combinators_test.gleam | 76 +++++++++++++++++++++++++++++++++- 3 files changed, 139 insertions(+), 32 deletions(-) diff --git a/src/parz/combinators.gleam b/src/parz/combinators.gleam index 0a16764..849f168 100644 --- a/src/parz/combinators.gleam +++ b/src/parz/combinators.gleam @@ -1,37 +1,39 @@ -import gleam/list +import gleam/string import parz/types.{type Parser, type ParserState, ParserState} -import parz/util.{tap} -fn sequence_rec(parsers, state, acc) { +fn sequence_rec(parsers: List(Parser), input, acc) { case parsers { - [] -> Ok([]) + [] -> Ok(#([], input)) [first, ..rest] -> - case first(state) { + case first(input) { Error(err) -> Error(err) Ok(ok) -> - case sequence_rec(rest, ok, acc) { + case sequence_rec(rest, ok.remaining, acc) { Error(err) -> Error(err) - Ok(rec) -> Ok([ok, ..rec]) + Ok(rec) -> { + let #(matches, remaining) = rec + Ok(#([ok.matched, ..matches], remaining)) + } } } } } -pub fn sequence(parsers) { - fn(state) { sequence_rec(parsers, state, []) } +pub fn sequence(parsers: List(Parser)) { + fn(input) { sequence_rec(parsers, input, []) } } pub fn choice(parsers: List(Parser)) { - fn(state) { + fn(input) { case parsers { [] -> Error("No more choices provided") [first, ..rest] -> - case first(state) { + case first(input) { Ok(ok) -> Ok(ok) Error(err) -> case rest { [] -> Error(err) - _ -> choice(rest)(state) + _ -> choice(rest)(input) } } } @@ -39,8 +41,8 @@ pub fn choice(parsers: List(Parser)) { } pub fn right(l: Parser, r: Parser) -> Parser { - fn(state) { - case l(state) { + fn(input) { + case l(input) { Error(err) -> Error(err) Ok(okl) -> case r(okl.remaining) { @@ -52,8 +54,8 @@ pub fn right(l: Parser, r: Parser) -> Parser { } pub fn left(l: Parser, r: Parser) -> Parser { - fn(state) { - case l(state) { + fn(input) { + case l(input) { Error(err) -> Error(err) Ok(okl) -> case r(okl.remaining) { @@ -65,8 +67,8 @@ pub fn left(l: Parser, r: Parser) -> Parser { } pub fn between(l: Parser, keep: Parser, r: Parser) -> Parser { - fn(state) { - case l(state) { + fn(input) { + case l(input) { Error(err) -> Error(err) Ok(okl) -> case left(keep, r)(okl.remaining) { @@ -77,8 +79,8 @@ pub fn between(l: Parser, keep: Parser, r: Parser) -> Parser { } } -fn many_rec(parser: Parser, state, acc) { - case parser(state) { +fn many_rec(parser: Parser, input, acc) { + case parser(input) { Error(err) -> Error(err) Ok(ok) -> { case many_rec(parser, ok.remaining, acc) { @@ -93,14 +95,45 @@ fn many_rec(parser: Parser, state, acc) { } pub fn many1(parser: Parser) { - fn(state) { many_rec(parser, state, []) } + fn(input) { many_rec(parser, input, []) } } pub fn many(parser: Parser) { - fn(state) { - case many1(parser)(state) { - Error(_) -> Ok(#([], state)) + fn(input) { + case many1(parser)(input) { + Error(_) -> Ok(#([], input)) + Ok(ok) -> Ok(ok) + } + } +} + +pub fn concat(parser) { + fn(input) { + case parser(input) { + Error(err) -> Error(err) + Ok(ok) -> { + let #(parts, remainder) = ok + + Ok(ParserState(string.concat(parts), remainder)) + } + } + } +} + +pub fn label_error(parser, message) { + fn(input) { + case parser(input) { Ok(ok) -> Ok(ok) + Error(_) -> Error(message) + } + } +} + +pub fn map(parser, transform) { + fn(input) { + case parser(input) { + Error(err) -> Error(err) + Ok(ok) -> Ok(transform(ok)) } } } diff --git a/src/parz/parsers.gleam b/src/parz/parsers.gleam index 2e3a40f..b529da9 100644 --- a/src/parz/parsers.gleam +++ b/src/parz/parsers.gleam @@ -3,11 +3,11 @@ import gleam/string import parz/types.{type Parser, ParserState} pub fn str(start) -> Parser { - fn(state) { - case string.starts_with(state, start) { - False -> Error("Expected " <> start <> "But found " <> state) + fn(input) { + case string.starts_with(input, start) { + False -> Error("Expected " <> start <> " but found " <> input) True -> { - let remaining = string.drop_left(state, string.length(start)) + let remaining = string.drop_left(input, string.length(start)) Ok(ParserState(start, remaining)) } } @@ -15,15 +15,15 @@ pub fn str(start) -> Parser { } pub fn regex(regex) { - fn(state) { + fn(input) { case regex.from_string(regex) { Error(_) -> Error("Invalid Regex Provided " <> regex) Ok(re) -> { - case regex.scan(re, state) { + case regex.scan(re, input) { [] -> Error("String does not match regex " <> regex) [match, ..] -> { let remaining = - string.drop_left(state, string.length(match.content)) + string.drop_left(input, string.length(match.content)) Ok(ParserState(match.content, remaining)) } } diff --git a/test/combinators_test.gleam b/test/combinators_test.gleam index 7639ff9..4185cd9 100644 --- a/test/combinators_test.gleam +++ b/test/combinators_test.gleam @@ -1,6 +1,8 @@ import gleeunit/should import parz.{run} -import parz/combinators.{between, choice, left, many, many1, right, sequence} +import parz/combinators.{ + between, choice, concat, label_error, left, many, many1, map, right, sequence, +} import parz/parsers.{letters, str} import parz/types.{ParserState} @@ -113,3 +115,75 @@ pub fn many1_test() { |> should.be_ok |> should.equal(#(["x", "x"], "!")) } + +pub fn sequence_test() { + let parser = sequence([str("["), letters(), str("]")]) + + run(parser, "[hello]") + |> should.be_ok + |> should.equal(#(["[", "hello", "]"], "")) + + run(parser, "[hello]!") + |> should.be_ok + |> should.equal(#(["[", "hello", "]"], "!")) + + run(parser, "[hello") + |> should.be_error +} + +pub fn concat_test() { + let parser = concat(sequence([str("["), letters(), str("]")])) + + run(parser, "[hello]") + |> should.be_ok + |> should.equal(ParserState("[hello]", "")) + + run(parser, "[hello]!") + |> should.be_ok + |> should.equal(ParserState("[hello]", "!")) + + run(parser, "[hello") + |> should.be_error +} + +pub fn label_error_test() { + let message = "Expected [letters]" + let parser = + concat(sequence([str("["), letters(), str("]")])) |> label_error(message) + + run(parser, "[hello]") + |> should.be_ok + |> should.equal(ParserState("[hello]", "")) + + run(parser, "[hellox") + |> should.be_error + |> should.equal(message) +} + +type Transformed { + NoContent + Content(String) +} + +pub fn map_test() { + let parser = + concat(sequence([str("["), letters(), str("]")])) + |> map(fn(ok) { + let seq = ok.matched + case seq { + "" -> NoContent + content -> Content(content) + } + }) + + run(parser, "[hello]") + |> should.be_ok + |> should.equal(Content("[hello]")) + + run(parser, "[hello]x") + |> should.be_ok + |> should.equal(Content("[hello]")) + + run(parser, "[hellox") + |> should.be_error +}