From 1f48eca09fd8a620ed8ebf6f7e2b897de6e7ea98 Mon Sep 17 00:00:00 2001 From: Oliver Marienfeld Date: Thu, 5 Feb 2026 11:36:23 +0100 Subject: [PATCH] feat: not combinator --- src/atto/ops.gleam | 61 ++++++++++++++++++++++++++++++++++++++++++++- test/ops_test.gleam | 21 ++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/atto/ops.gleam b/src/atto/ops.gleam index c5a6679..355fa6a 100644 --- a/src/atto/ops.gleam +++ b/src/atto/ops.gleam @@ -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. /// @@ -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(), + )) + } + } + }) +} diff --git a/test/ops_test.gleam b/test/ops_test.gleam index a0c5b01..1549d86 100644 --- a/test/ops_test.gleam +++ b/test/ops_test.gleam @@ -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]) +}