Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion src/atto/ops.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import gleam/list
import gleam/result
import gleam/set

import atto.{type Parser, do, drop, pure}
import atto.{type Parser, type ParserInput, type Pos, Parser, do, drop, pure}

/// Try to apply a parser, returning `Nil` if it fails without consuming input.
///
Expand Down Expand Up @@ -166,3 +166,62 @@ pub fn between(
use <- drop(close)
pure(x)
}

/// Fails and backtracks if `p` succeeds, otherwise returns the _token_.
///
/// **Please note:** If `p` maps to some type `a`, then `not(p)` must also
/// map to `a` (see examples).
///
/// ## Examples
///
/// ```gleam
/// ops.not(token("b"))
/// |> run(text.new("a"), Nil)
/// // -> Ok("b")
///
/// ops.not(token("a"))
/// |> run(text.new("a"), Nil)
/// // -> Error(ParseError(Pos(0, 1), Token("a"), set.new()))
///
/// // If p maps to Int, not(p) must also map to Int:
/// let a = token("a") |> atto.map(fn(_) { 0 })
/// let not_a = ops.not(a) |> atto.map(fn(_) { 1 })
/// ops.choice([a, not_a])
/// |> run(text.new("b"), Nil)
/// // -> Ok(1)
/// ```
pub fn not(that p: Parser(a, t, s, c, e)) -> Parser(t, t, s, c, e) {
Parser(run: fn(in: ParserInput(t, s), pos: Pos, ctx: c) -> Result(
#(t, ParserInput(t, s), Pos, c),
atto.ParseError(e, t),
) {
case p.run(in, pos, ctx) {
Ok(#(_res, _in2, pos2, _ctx2)) ->
case atto.get_token(in, pos) {
Ok(#(token, _, _)) ->
Error(atto.ParseError(
span: atto.Span(pos, pos2),
got: atto.Token(token),
expected: set.new(),
))
Error(_) ->
Error(atto.ParseError(
span: atto.Span(pos, pos2),
got: atto.Msg("EOF"),
expected: set.new(),
))
}

Error(_) ->
case atto.get_token(in, pos) {
Ok(#(token, in2, pos2)) -> Ok(#(token, in2, pos2, ctx))
Error(_) ->
Error(atto.ParseError(
span: atto.Span(pos, pos),
got: atto.Msg("EOF"),
expected: set.new(),
))
}
}
})
}
21 changes: 21 additions & 0 deletions test/ops_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,24 @@ pub fn between_test() {
)),
)
}

pub fn not_test() {
let a = atto.token("a")
let not_a = ops.not(a)

assert atto.run(not_a, text.new("b"), Nil) == Ok("b")
assert atto.run(not_a, text.new("a"), Nil)
== Error(atto.ParseError(
span: span_char(0, 1, 1),
got: atto.Token("a"),
expected: set.new(),
))
}

pub fn not_and_backtrack_test() {
let a = atto.token("a") |> atto.map(fn(_) { 1 })
let not_a = ops.not(a) |> atto.map(fn(_) { 0 })
let parser = ops.some(ops.choice([a, not_a]))

assert atto.run(parser, text.new("abba"), Nil) == Ok([1, 0, 0, 1])
}