Repository: winnow-rs/winnow (Rust)

Issue:
the trait SliceLen is not implemented for char
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/winnow-rs/winnow/discussions)
- [X] I have searched the [open](https://github.com/winnow-rs/winnow/issues) and [rejected](https://github.com/winnow-rs/winnow/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### rust version

rustc 1.74.0 (79e9716c9 2023-11-13)

### winnow version

0.5.31

### Minimal reproducible code

```rust
use std::num::{NonZeroU32, NonZeroU8};
use winnow::{Parser, PResult};
use winnow::ascii::space1;
use winnow::error::{ContextError, ParseError, ParserError};
use winnow::token::{tag, take};

fn main() {
    assert!(parse_human_format("2022-07-01 00:40:05 +08:00").is_ok());
    assert!(parse_human_format("2022-07-00 00:40:05 +08:00").is_err());
}


fn u8_len2(input: &mut &str) -> PResult<u8> {
    take(2_usize).try_map(str::parse).parse_next(input)
}

fn non_zero_u8_len2(input: &mut &str) -> PResult<NonZeroU8> {
    take(2_usize).try_map(str::parse).parse_next(input)
}

//
fn non_zero_u32_len4(input: &mut &str) -> PResult<NonZeroU32> {
    take(4_usize).try_map(str::parse).parse_next(input)
}

// 2022-07-14 00:40:05 +08:00
fn parse_human_format(input: &str) -> Result<(), ParseError<&str, ContextError>> {
    (non_zero_u32_len4,
     tag('-'),
     non_zero_u8_len2,
     tag('-'),
     non_zero_u8_len2,
     space1,
     u8_len2,
     tag(":"),
     u8_len2,
     tag(":"),
     u8_len2,
     space1,
     tag("+"),
     u8_len2,
     tag(":"),
     u8_len2,
    ).parse(input)?;
    Ok(())
}
```

* the  complier detail error
```diff
error[E0277]: the trait bound `char: SliceLen` is not satisfied
   --> src/main.rs:29:10
    |
29  |      tag('-'),
+    |      --- ^^^ the trait `SliceLen` is not implemented for `char`
    |      |
    |      required by a bound introduced by this call
    |
    = help: the following other types implement trait `SliceLen`:
              Located<I>
              Stateful<I, S>
              Partial<I>
              Caseless<S>
              [T; LEN]
              &'a [T]
              &'a [T; LEN]
              &'a str
            and 3 others
note: required by a bound in `winnow::token::tag`
   --> ~/.cargo/registry/src/winnow-0.5.30/src/token/mod.rs:142:8
    |
138 | pub fn tag<T, I, Error: ParserError<I>>(tag: T) -> impl Parser<I, <I as Stream>::Slice, Error>
    |        --- required by a bound in this function
...
142 |     T: SliceLen + Clone,
    |        ^^^^^^^^ required by this bound in `tag`


```


### Steps to reproduce the bug with the above code

exec above rust code,cause error

### Actual Behaviour

Maybe tag support `char`

### Expected Behaviour

-

### Additional Context

_No response_


Discussion:
Is there a reason you are using `tag`, rather than
- `one_of`
- `char` directly (as it implements `Parser`)
For this example, both `one_of` and `char` are suitable. 
However, in certain scenarios, semantically,
`one_of` tends to imply a multiple choice selection where only one option is chosen.
Using the `char` type requires hardcoding, and it does not  passing parameters into a tag.



Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 0dcaef768..c3078a8ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -144,6 +144,10 @@ harness = false
 name = "iter"
 harness = false
 
+[[bench]]
+name = "next_slice"
+harness = false
+
 [[bench]]
 name = "number"
 harness = false
diff --git a/benches/next_slice.rs b/benches/next_slice.rs
new file mode 100644
index 000000000..6c25d23a2
--- /dev/null
+++ b/benches/next_slice.rs
@@ -0,0 +1,133 @@
+use criterion::black_box;
+
+use winnow::combinator::repeat;
+use winnow::prelude::*;
+use winnow::token::one_of;
+use winnow::token::tag;
+
+fn next_slice(c: &mut criterion::Criterion) {
+    let mut group = c.benchmark_group("next_slice");
+
+    let name = "ascii";
+    let sample = "h".repeat(100);
+    let sample = sample.as_str();
+    group.bench_with_input(
+        criterion::BenchmarkId::new("char", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_ascii_char.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("str", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_ascii_str.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("one_of", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_ascii_one_of.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("tag_char", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_ascii_tag_char.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("tag_str", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_ascii_tag_str.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+
+    let name = "utf8";
+    let sample = "🧑".repeat(100);
+    let sample = sample.as_str();
+    group.bench_with_input(
+        criterion::BenchmarkId::new("char", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_utf8_char.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("str", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_utf8_str.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("one_of", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_utf8_one_of.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("tag_char", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_utf8_tag_char.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+    group.bench_with_input(
+        criterion::BenchmarkId::new("tag_str", name),
+        sample,
+        |b, sample| {
+            b.iter(|| black_box(parser_utf8_tag_str.parse_peek(black_box(sample)).unwrap()));
+        },
+    );
+
+    group.finish();
+}
+
+fn parser_ascii_char(input: &mut &str) -> PResult<usize> {
+    repeat(0.., 'h').parse_next(input)
+}
+
+fn parser_ascii_str(input: &mut &str) -> PResult<usize> {
+    repeat(0.., "h").parse_next(input)
+}
+
+fn parser_ascii_one_of(input: &mut &str) -> PResult<usize> {
+    repeat(0.., one_of('h')).parse_next(input)
+}
+
+fn parser_ascii_tag_char(input: &mut &str) -> PResult<usize> {
+    repeat(0.., tag('h')).parse_next(input)
+}
+
+fn parser_ascii_tag_str(input: &mut &str) -> PResult<usize> {
+    repeat(0.., tag("h")).parse_next(input)
+}
+
+fn parser_utf8_char(input: &mut &str) -> PResult<usize> {
+    repeat(0.., '🧑').parse_next(input)
+}
+
+fn parser_utf8_str(input: &mut &str) -> PResult<usize> {
+    repeat(0.., "🧑").parse_next(input)
+}
+
+fn parser_utf8_one_of(input: &mut &str) -> PResult<usize> {
+    repeat(0.., one_of('🧑')).parse_next(input)
+}
+
+fn parser_utf8_tag_char(input: &mut &str) -> PResult<usize> {
+    repeat(0.., tag('🧑')).parse_next(input)
+}
+
+fn parser_utf8_tag_str(input: &mut &str) -> PResult<usize> {
+    repeat(0.., tag("🧑")).parse_next(input)
+}
+
+criterion::criterion_group!(benches, next_slice);
+criterion::criterion_main!(benches);
diff --git a/src/stream/mod.rs b/src/stream/mod.rs
index 39a1e0d48..d5d11aa3d 100644
--- a/src/stream/mod.rs
+++ b/src/stream/mod.rs
@@ -374,6 +374,13 @@ impl<'a> SliceLen for &'a str {
     }
 }
 
+impl SliceLen for char {
+    #[inline]
+    fn slice_len(&self) -> usize {
+        self.len_utf8()
+    }
+}
+
 impl<'a> SliceLen for &'a Bytes {
     #[inline]
     fn slice_len(&self) -> usize {
@@ -1717,6 +1724,32 @@ impl<'a, 'b> Compare<AsciiCaseless<&'b str>> for &'a [u8] {
     }
 }
 
+impl<'a> Compare<char> for &'a [u8] {
+    #[inline(always)]
+    fn compare(&self, t: char) -> CompareResult {
+        self.compare(t.encode_utf8(&mut [0; 4]).as_bytes())
+    }
+
+    #[inline(always)]
+    #[allow(deprecated)]
+    fn compare_no_case(&self, t: char) -> CompareResult {
+        self.compare_no_case(t.encode_utf8(&mut [0; 4]).as_bytes())
+    }
+}
+
+impl<'a> Compare<AsciiCaseless<char>> for &'a [u8] {
+    #[inline]
+    fn compare(&self, t: AsciiCaseless<char>) -> CompareResult {
+        self.compare(AsciiCaseless(t.0.encode_utf8(&mut [0; 4]).as_bytes()))
+    }
+
+    #[inline(always)]
+    #[allow(deprecated)]
+    fn compare_no_case(&self, t: AsciiCaseless<char>) -> CompareResult {
+        self.compare_no_case(AsciiCaseless(t.0.encode_utf8(&mut [0; 4]).as_bytes()))
+    }
+}
+
 impl<'a, 'b> Compare<&'b str> for &'a str {
     #[inline(always)]
     fn compare(&self, t: &'b str) -> CompareResult {
@@ -1743,6 +1776,32 @@ impl<'a, 'b> Compare<AsciiCaseless<&'b str>> for &'a str {
     }
 }
 
+impl<'a> Compare<char> for &'a str {
+    #[inline(always)]
+    fn compare(&self, t: char) -> CompareResult {
+        self.compare(t.encode_utf8(&mut [0; 4]).as_bytes())
+    }
+
+    #[inline(always)]
+    #[allow(deprecated)]
+    fn compare_no_case(&self, t: char) -> CompareResult {
+        self.compare_no_case(t.encode_utf8(&mut [0; 4]).as_bytes())
+    }
+}
+
+impl<'a> Compare<AsciiCaseless<char>> for &'a str {
+    #[inline]
+    fn compare(&self, t: AsciiCaseless<char>) -> CompareResult {
+        self.compare(AsciiCaseless(t.0.encode_utf8(&mut [0; 4]).as_bytes()))
+    }
+
+    #[inline(always)]
+    #[allow(deprecated)]
+    fn compare_no_case(&self, t: AsciiCaseless<char>) -> CompareResult {
+        self.compare_no_case(AsciiCaseless(t.0.encode_utf8(&mut [0; 4]).as_bytes()))
+    }
+}
+
 impl<'a, 'b> Compare<&'b [u8]> for &'a str {
     #[inline(always)]
     fn compare(&self, t: &'b [u8]) -> CompareResult {
@@ -2620,6 +2679,7 @@ impl AsChar for u8 {
         self == b'\n'
     }
 }
+
 impl<'a> AsChar for &'a u8 {
     #[inline(always)]
     fn as_char(self) -> char {


Test patch:
diff --git a/src/stream/tests.rs b/src/stream/tests.rs
index 0129f6e3f..06047dfd4 100644
--- a/src/stream/tests.rs
+++ b/src/stream/tests.rs
@@ -1,6 +1,9 @@
 #[cfg(feature = "std")]
 use proptest::prelude::*;
 
+use crate::error::ErrMode::Backtrack;
+use crate::error::{ErrorKind, InputError};
+use crate::token::tag;
 use crate::{
     combinator::{separated, separated_pair},
     PResult, Parser,
@@ -146,3 +149,80 @@ fn test_custom_slice() {
     let offset = input.offset_from(&start);
     assert_eq!(offset, 2);
 }
+
+#[test]
+fn test_tag_support_char() {
+    assert_eq!(
+        tag::<_, _, InputError<_>>('π').parse_peek("π"),
+        Ok(("", "π"))
+    );
+    assert_eq!(
+        tag::<_, _, InputError<_>>('π').parse_peek("π3.14"),
+        Ok(("3.14", "π"))
+    );
+
+    assert_eq!(
+        tag::<_, _, InputError<_>>("π").parse_peek("π3.14"),
+        Ok(("3.14", "π"))
+    );
+
+    assert_eq!(
+        tag::<_, _, InputError<_>>('-').parse_peek("π"),
+        Err(Backtrack(InputError::new("π", ErrorKind::Tag)))
+    );
+
+    assert_eq!(
+        tag::<_, Partial<&[u8]>, InputError<_>>('π').parse_peek(Partial::new(b"\xCF\x80")),
+        Ok((Partial::new(Default::default()), "π".as_bytes()))
+    );
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>('π').parse_peek(b"\xCF\x80"),
+        Ok((Default::default(), "π".as_bytes()))
+    );
+
+    assert_eq!(
+        tag::<_, Partial<&[u8]>, InputError<_>>('π').parse_peek(Partial::new(b"\xCF\x803.14")),
+        Ok((Partial::new(&b"3.14"[..]), "π".as_bytes()))
+    );
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>('π').parse_peek(b"\xCF\x80"),
+        Ok((Default::default(), "π".as_bytes()))
+    );
+
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>('π').parse_peek(b"\xCF\x803.14"),
+        Ok((&b"3.14"[..], "π".as_bytes()))
+    );
+
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>(AsciiCaseless('a')).parse_peek(b"ABCxyz"),
+        Ok((&b"BCxyz"[..], &b"A"[..]))
+    );
+
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>('a').parse_peek(b"ABCxyz"),
+        Err(Backtrack(InputError::new(&b"ABCxyz"[..], ErrorKind::Tag)))
+    );
+
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>(AsciiCaseless('π')).parse_peek(b"\xCF\x803.14"),
+        Ok((&b"3.14"[..], "π".as_bytes()))
+    );
+
+    assert_eq!(
+        tag::<_, _, InputError<_>>(AsciiCaseless('🧑')).parse_peek("🧑你好"),
+        Ok(("你好", "🧑"))
+    );
+
+    let mut buffer = [0; 4];
+    let input = '\u{241b}'.encode_utf8(&mut buffer);
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>(AsciiCaseless('␛')).parse_peek(input.as_bytes()),
+        Ok((&b""[..], [226, 144, 155].as_slice()))
+    );
+
+    assert_eq!(
+        tag::<_, &[u8], InputError<_>>('-').parse_peek(b"\xCF\x80"),
+        Err(Backtrack(InputError::new(&b"\xCF\x80"[..], ErrorKind::Tag)))
+    );
+}