diff --git a/bytes/regexp.go b/bytes/regexp.go index 8b7a762..7155fca 100644 --- a/bytes/regexp.go +++ b/bytes/regexp.go @@ -3,6 +3,7 @@ package bytes import ( + "bytes" "errors" "fmt" "io" @@ -87,3 +88,22 @@ func Regexp(pattern string) gigaparsec.Parser[byte, string] { return gigaparsec.Succeed(true, string(dst), next, gigaparsec.MessageOK(input.Pos())), nil } } + +func MatchString(s string) gigaparsec.Parser[byte, string] { + expected := fmt.Sprintf("%q", s) + b := []byte(s) + return func(input gigaparsec.State[byte]) (gigaparsec.Result[byte, string], error) { + dst := make([]byte, len(s)) + _, next, err := input.Read(dst) + if errors.Is(err, io.EOF) { + return gigaparsec.Fail[byte, string](false, gigaparsec.MessageEnd(input.Pos(), expected)), nil + } + if err != nil { + return gigaparsec.Result[byte, string]{}, fmt.Errorf("MatchString: %w", err) + } + if !bytes.Equal(dst, b) { + return gigaparsec.Fail[byte, string](false, gigaparsec.MakeMessage(input.Pos(), string(dst), expected)), nil + } + return gigaparsec.Succeed(true, s, next, gigaparsec.MessageOK(input.Pos())), nil + } +} diff --git a/bytes/regexp_test.go b/bytes/regexp_test.go index 6e9640f..e59abe1 100644 --- a/bytes/regexp_test.go +++ b/bytes/regexp_test.go @@ -3,11 +3,14 @@ package bytes_test import ( + "bytes" "testing" "git.codemonkeysoftware.net/b/gigaparsec" - "git.codemonkeysoftware.net/b/gigaparsec/bytes" + pbytes "git.codemonkeysoftware.net/b/gigaparsec/bytes" "git.codemonkeysoftware.net/b/gigaparsec/cursor" + ptest "git.codemonkeysoftware.net/b/gigaparsec/test" + pgen "git.codemonkeysoftware.net/b/gigaparsec/test/generator" "github.com/shoenig/test" "github.com/shoenig/test/must" "pgregory.net/rapid" @@ -25,7 +28,7 @@ func TestRegexp(t *testing.T) { })) t.Run("basically works", func(t *testing.T) { - result, err := bytes.Regexp("a")(gigaparsec.MakeState(cursor.NewSlice([]byte("a")))) + result, err := pbytes.Regexp("a")(gigaparsec.MakeState(cursor.NewSlice([]byte("a")))) must.NoError(t, err) failed, _, msg := result.Failed() must.False(t, failed, must.Sprint(msg)) @@ -38,7 +41,7 @@ func TestRegexp(t *testing.T) { func TestRuneReader(t *testing.T) { var s = []byte("abcdefghijklmnopqrstuvwxyz") - rr := bytes.NewRuneReader(cursor.NewSlice(s)) + rr := pbytes.NewRuneReader(cursor.NewSlice(s)) for i, b := range s { r, n, err := rr.ReadRune() test.NoError(t, err) @@ -49,3 +52,39 @@ func TestRuneReader(t *testing.T) { } } } + +func TestMatchString(t *testing.T) { + t.Run("fails on unexpected error", rapid.MakeCheck(func(t *rapid.T) { + s := rapid.StringN(-1, -1, 100).Draw(t, "s") + readErr := pgen.Error().Draw(t, "readErr") + result, err := pbytes.MatchString(s)(gigaparsec.MakeState(cursor.NewReaderAt(ptest.ErrReaderAt(readErr)))) + test.ErrorIs(t, err, readErr) + failed, consumed, _ := result.Failed() + test.True(t, failed) + test.False(t, consumed) + })) + t.Run("does not succeed or consume on mismatch", rapid.MakeCheck(func(t *rapid.T) { + bgen := rapid.SliceOfN(rapid.Byte(), 1, 100) + input := bgen.Draw(t, "input") + notPrefix := func(b []byte) bool { return !bytes.HasPrefix(input, b) } + s := string(bgen.Filter(notPrefix).Draw(t, "s")) + + result, err := pbytes.MatchString(s)(gigaparsec.MakeState(cursor.NewSlice(input))) + must.NoError(t, err) + failed, consumed, _ := result.Failed() + must.True(t, failed) + test.False(t, consumed) + })) + t.Run("succeeds with correct value, consumption, and position", rapid.MakeCheck(func(t *rapid.T) { + input := rapid.SliceOfN(rapid.Byte(), 1, 100).Draw(t, "input") + slen := rapid.IntRange(0, len(input)).Draw(t, "slen") + s := string(input[:slen]) + result, err := pbytes.MatchString(s)(gigaparsec.MakeState(cursor.NewSlice(input))) + must.NoError(t, err) + succeeded, consumed, value, next, _ := result.Succeeded() + must.True(t, succeeded) + test.True(t, consumed) + test.EqOp(t, s, value) + ptest.StateIsAt(t, next, uint64(slen)) + })) +} diff --git a/test/readerat.go b/test/readerat.go index 502cb51..14d0136 100644 --- a/test/readerat.go +++ b/test/readerat.go @@ -6,7 +6,9 @@ package test import ( "io" + "git.codemonkeysoftware.net/b/gigaparsec" "git.codemonkeysoftware.net/b/gigaparsec/cursor" + "github.com/shoenig/test" ) type errReaderAt struct { @@ -44,3 +46,7 @@ func (c errCursor[T]) Pos() uint64 { func ErrCursor[T any](err error) cursor.Cursor[T] { return errCursor[T]{err: err} } + +func StateIsAt[Input any](t test.T, s gigaparsec.State[Input], pos uint64) { + test.EqOp(t, pos, s.Pos(), test.Sprintf("expected parser state to be at position %d, got %d", pos, s.Pos())) +}