Repository: toml-rs/toml (language: rust)

Issue:
`toml_edit` is pretty slow to compile
`toml_edit` is pretty slow to compile, one of the slowest dependencies of `cargo`.  My suspicion is the number of macros for `combine` is the core problem (due to the extra parsing steps the compiler has to do).  I want to explore using `nom` once some ergonomic improvements are made since it doesn't use macros.

_Originally posted by @epage in https://github.com/ordian/toml_edit/issues/323#issuecomment-1182006418_
 


Discussion / hints:
It's funny I migrated the parser from nom to combine a while ago (#26) because of too many macros. Combine was mainly based on traits at the time. It would be ironic to go full circle.

Did some profiling:
![Screenshot from 2022-07-31 17-42-35](https://user-images.githubusercontent.com/4211399/182034234-4ebddbad-c89a-4f22-b08d-5fa5436b3af8.png)

```
$ cargo +nightly rustc -- -Z self-profile
$ summarize summarize toml_edit-554834.mm_profdata | head -20
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| Item                                             | Self time | % of total time | Time     | Item count | Incremental result hashing time |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| LLVM_passes                                      | 17.92s    | 18.750          | 17.95s   | 1          | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| finish_ongoing_codegen                           | 16.27s    | 17.022          | 16.28s   | 1          | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| LLVM_module_codegen_emit_obj                     | 16.20s    | 16.949          | 16.20s   | 121        | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| LLVM_lto_optimize                                | 13.10s    | 13.699          | 13.10s   | 121        | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| LLVM_module_optimize                             | 12.63s    | 13.216          | 12.63s   | 121        | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| LLVM_thin_lto_import                             | 3.39s     | 3.545           | 3.39s    | 121        | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| typeck                                           | 3.15s     | 3.296           | 6.77s    | 1516       | 10.39ms                         |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| codegen_module_optimize                          | 1.86s     | 1.949           | 14.50s   | 121        | 0.00ns                          |
+--------------------------------------------------+-----------+-----------------+----------+------------+---------------------------------+
| codegen_module_perform_lto                       | 1.86s     | 1.948           | 35.04s   | 121        | 0.00ns                          |

```
![Screenshot from 2022-07-31 17-28-08](https://user-images.githubusercontent.com/4211399/182033688-7e67aa06-1fa5-432b-a046-17e36b875577.png)
It spends a lot of time with monomorphization and linking which suggests generics explosion.
```
❯ cargo llvm-lines | head -20
  Lines          Copies        Function name
  -----          ------        -------------
  884919 (100%)  15631 (100%)  (TOTAL)
   81961 (9.3%)    120 (0.8%)  combine::parser::sequence::<impl combine::parser::Parser<Input> for (A,B)>::parse_mode_impl
   59663 (6.7%)     79 (0.5%)  <combine::parser::combinator::AndThen<P,F> as combine::parser::Parser<Input>>::parse_mode_impl
   56629 (6.4%)     59 (0.4%)  <(Y,Z) as combine::parser::choice::ChoiceParser<Input>>::parse_mode_choice
   51181 (5.8%)    335 (2.1%)  <combine::parser::combinator::Map<P,F> as combine::parser::Parser<Input>>::parse_mode_impl
   42252 (4.8%)     37 (0.2%)  combine::parser::sequence::<impl combine::parser::Parser<Input> for (A,B,C)>::parse_mode_impl
   31767 (3.6%)    256 (1.6%)  combine::error::ParseResult<T,E>::map
   26308 (3.0%)     62 (0.4%)  combine::parser::ParseMode::parse_committed
   25442 (2.9%)     50 (0.3%)  <combine::parser::range::RecognizeWithValue<P> as combine::parser::Parser<Input>>::parse_mode
   21348 (2.4%)     68 (0.4%)  <combine::parser::repeat::Iter<Input,P,S,M> as core::iter::traits::iterator::Iterator>::next
   20884 (2.4%)   1402 (9.0%)  combine::parser::Parser::parse_mode
   14444 (1.6%)    628 (4.0%)  <combine::parser::PartialMode as combine::parser::ParseMode>::parse
   14369 (1.6%)     67 (0.4%)  combine::parser::sequence::PartialState2<A,B>::add_errors
   12562 (1.4%)     17 (0.1%)  combine::parser::range::parse_partial_range
   12328 (1.4%)    134 (0.9%)  combine::parser::sequence::PartialState2<A,B>::add_errors::{{closure}}
   11365 (1.3%)      6 (0.0%)  <(V,X,Y,Z) as combine::parser::choice::ChoiceParser<Input>>::parse_mode_choice
   11342 (1.3%)      8 (0.1%)  <(X,Y,Z) as combine::parser::choice::ChoiceParser<Input>>::parse_mode_choice
   10836 (1.2%)    774 (5.0%)  <combine::parser::FirstMode as combine::parser::ParseMode>::parse
```

I'd say rewriting the parser manually is still on the table and might be beneficial long-term (e.g. precise control over errors). Short-term we could try to reduce the number of macros and generics in toml_edit itself.
> It's funny I migrated the parser from nom to combine a while ago (https://github.com/ordian/toml_edit/pull/26) because of too many macros. Combine was mainly based on traits at the time. It would be ironic to go full circle.

And `combine` can still be light on macros except in some specific cases.

>  Short-term we could try to reduce the number of macros and generics in toml_edit itself.

I had looked at trying to drop the macros where possible but combine's API is such a mess to work with because everything is a combinator, returning a generic parser, rather than nom's model of operating on the actual parsers themselves (which also simplifies some parser cases in my experience).

> I'd say rewriting the parser manually is still on the table and might be beneficial long-term (e.g. precise control over errors). 

Its interesting to see what perspectives exist in the community.  I've heard from some that the use of a parser library in toml_edit made it more favorable to toml-rs.  I've also seen others say to just write a parser by hand.  I lean towards preferring parser libraries as I feel it makes the parsing code more maintainable unless there is a very specific and critical need otherwise.
> I had looked at trying to drop the macros where possible but combine's API is such a mess to work with because everything is a combinator, returning a generic parser, rather than nom's model of operating on the actual parsers themselves (which also simplifies some parser cases in my experience).

Fair point, I haven't looked at the current state of nom. Feel free to explore this approach.

> Its interesting to see what perspectives exist in the community. I've heard from some that the use of a parser library in toml_edit made it more favorable to toml-rs. I've also seen others say to just write a parser by hand. I lean towards preferring parser libraries as I feel it makes the parsing code more maintainable unless there is a very specific and critical need otherwise.

Interesting indeed. I do agree that using parser libraries result in more readable and maintainable code. The downside is that we lose precise control. Control over compile times, control over error messages and when things don't work as expected we're at the mercy of the library developers if they are complex enough. Another concern is security/vulnerability response.

That being said, I'm not sure whether the problem is with combine or how we (ab)use it (probably a combination of both). But if you think nom's approach will have fewer downsides, go for it :)
> I want to explore using nom once some ergonomic improvements are made since it doesn't use macros.

As a follow up to my earlier comment, like most parser libraries, the development of nom has slowed down, so we do not yet have these ergonomic improvements.  I am coordinator with the maintainer for getting access to make these improvements.  They will be back from vacation soon which will hopefully get things moving forward.
btw I had forgotten to call out that I made a [parser benchmark repo](https://github.com/epage/parse-benchmarks-rs).  This was in part to test my combine theory.   While combine performs fairly well, that is for an old version of combine (all I could find a json implementation for) and only uses one macro rather than our 2 layers of macros for every parser approach.

Also, for more background on why I had originally suspected combine and our use of it: When nnethercote was analyzing hot spots for compiling, he found that [tt munchers](https://veykril.github.io/tlborm/decl-macros/patterns/tt-muncher.html) were slow to compile because it needed to re-parse at each layer of recursion to check what pattern it matched.  While we aren't doing tt munching, we put the entire body of every parser function into two layers of macros.

However, if this is all done during typecheck, then the numbers earlier in this thread blow that theory out of the water.  However, the use of generics that combine forces on us and the complexities by only working through combinators are both reasons for us to still re-evaluate our use of combine.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5a5b9d88..3f2033ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -67,12 +67,6 @@ version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
 
-[[package]]
-name = "bytes"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
-
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -167,16 +161,6 @@ dependencies = [
  "os_str_bytes",
 ]
 
-[[package]]
-name = "combine"
-version = "4.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
-dependencies = [
- "bytes",
- "memchr",
-]
-
 [[package]]
 name = "concolor"
 version = "0.0.11"
@@ -532,6 +516,22 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom8"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75d908f0297c3526d34e478d438b07eefe3d7b0416494d7ffccb17f1c7f7262c"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "normalize-line-endings"
 version = "0.3.0"
@@ -924,11 +924,11 @@ dependencies = [
 name = "toml_edit"
 version = "0.15.0"
 dependencies = [
- "combine",
  "criterion",
  "indexmap",
  "itertools",
  "kstring",
+ "nom8",
  "serde",
  "serde_json",
  "snapbox",
diff --git a/crates/toml_edit/Cargo.toml b/crates/toml_edit/Cargo.toml
index 0e72e3b2..a3a608e6 100644
--- a/crates/toml_edit/Cargo.toml
+++ b/crates/toml_edit/Cargo.toml
@@ -42,7 +42,7 @@ serde = ["dep:serde", "toml_datetime/serde"]
 
 [dependencies]
 indexmap = "1.9.1"
-combine = "4.6.6"
+nom8 = "0.1.0"
 itertools = "0.10.5"
 serde = { version = "1.0.145", features = ["derive"], optional = true }
 kstring = { version = "2.0.0", features = ["max_inline"], optional = true }
diff --git a/crates/toml_edit/src/document.rs b/crates/toml_edit/src/document.rs
index d2898cde..a0319d44 100644
--- a/crates/toml_edit/src/document.rs
+++ b/crates/toml_edit/src/document.rs
@@ -70,7 +70,13 @@ impl FromStr for Document {
 
     /// Parses a document from a &str
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        parser::document(s.as_bytes())
+        use nom8::prelude::*;
+
+        let b = s.as_bytes();
+        parser::document::document
+            .parse(b)
+            .finish()
+            .map_err(|e| Self::Err::new(e, b))
     }
 }
 
diff --git a/crates/toml_edit/src/key.rs b/crates/toml_edit/src/key.rs
index f9cce7ce..bb640b82 100644
--- a/crates/toml_edit/src/key.rs
+++ b/crates/toml_edit/src/key.rs
@@ -1,11 +1,9 @@
 use std::borrow::Cow;
 use std::str::FromStr;
 
-use combine::stream::position::Stream;
-
 use crate::encode::{to_string_repr, StringStyle};
 use crate::parser;
-use crate::parser::is_unquoted_char;
+use crate::parser::key::is_unquoted_char;
 use crate::repr::{Decor, Repr};
 use crate::InternalString;
 
@@ -105,37 +103,23 @@ impl Key {
     }
 
     fn try_parse_simple(s: &str) -> Result<Key, parser::TomlError> {
-        use combine::stream::position::{IndexPositioner, Positioner};
-        use combine::EasyParser;
+        use nom8::prelude::*;
 
         let b = s.as_bytes();
-        let result = parser::simple_key().easy_parse(Stream::new(b));
+        let result = parser::key::simple_key.parse(b).finish();
         match result {
-            Ok((_, ref rest)) if !rest.input.is_empty() => Err(parser::TomlError::from_unparsed(
-                (&rest.positioner
-                    as &dyn Positioner<usize, Position = usize, Checkpoint = IndexPositioner>)
-                    .position(),
-                b,
-            )),
-            Ok(((raw, key), _)) => Ok(Key::new(key).with_repr_unchecked(Repr::new_unchecked(raw))),
+            Ok((raw, key)) => Ok(Key::new(key).with_repr_unchecked(Repr::new_unchecked(raw))),
             Err(e) => Err(parser::TomlError::new(e, b)),
         }
     }
 
     fn try_parse_path(s: &str) -> Result<Vec<Key>, parser::TomlError> {
-        use combine::stream::position::{IndexPositioner, Positioner};
-        use combine::EasyParser;
+        use nom8::prelude::*;
 
         let b = s.as_bytes();
-        let result = parser::key_path().easy_parse(Stream::new(b));
+        let result = parser::key::key.parse(b).finish();
         match result {
-            Ok((_, ref rest)) if !rest.input.is_empty() => Err(parser::TomlError::from_unparsed(
-                (&rest.positioner
-                    as &dyn Positioner<usize, Position = usize, Checkpoint = IndexPositioner>)
-                    .position(),
-                b,
-            )),
-            Ok((keys, _)) => Ok(keys),
+            Ok(keys) => Ok(keys),
             Err(e) => Err(parser::TomlError::new(e, b)),
         }
     }
diff --git a/crates/toml_edit/src/parser/array.rs b/crates/toml_edit/src/parser/array.rs
index 95604ecc..885c5b58 100644
--- a/crates/toml_edit/src/parser/array.rs
+++ b/crates/toml_edit/src/parser/array.rs
@@ -1,19 +1,27 @@
-use combine::parser::byte::byte;
-use combine::parser::range::recognize_with_value;
-use combine::stream::RangeStream;
-use combine::*;
+use nom8::combinator::cut;
+use nom8::combinator::opt;
+use nom8::multi::separated_list1;
+use nom8::sequence::delimited;
 
 use crate::parser::trivia::ws_comment_newline;
 use crate::parser::value::value;
-use crate::{Array, Value};
+use crate::{Array, Item, Value};
+
+use crate::parser::prelude::*;
 
 // ;; Array
 
 // array = array-open array-values array-close
-parse!(array() -> Array, {
-    between(byte(ARRAY_OPEN), byte(ARRAY_CLOSE),
-            array_values())
-});
+pub(crate) fn array(input: Input<'_>) -> IResult<Input<'_>, Array, ParserError<'_>> {
+    delimited(
+        ARRAY_OPEN,
+        cut(array_values),
+        cut(ARRAY_CLOSE)
+            .context(Context::Expression("array"))
+            .context(Context::Expected(ParserValue::CharLiteral(']'))),
+    )
+    .parse(input)
+}
 
 // note: we're omitting ws and newlines here, because
 // they should be part of the formatted values
@@ -27,42 +35,42 @@ const ARRAY_SEP: u8 = b',';
 // note: this rule is modified
 // array-values = [ ( array-value array-sep array-values ) /
 //                  array-value / ws-comment-newline ]
-parse!(array_values() -> Array, {
+pub(crate) fn array_values(input: Input<'_>) -> IResult<Input<'_>, Array, ParserError<'_>> {
     (
-        optional(
-            recognize_with_value(
-                sep_end_by1(array_value(), byte(ARRAY_SEP))
-            ).map(|(r, v): (&'a [u8], Array)| (v, r[r.len() - 1] == b','))
+        opt(
+            (separated_list1(ARRAY_SEP, array_value), opt(ARRAY_SEP)).map(
+                |(v, trailing): (Vec<Value>, Option<u8>)| {
+                    (
+                        Array::with_vec(v.into_iter().map(Item::Value).collect()),
+                        trailing.is_some(),
+                    )
+                },
+            ),
         ),
-        ws_comment_newline(),
-    ).and_then::<_, _, std::str::Utf8Error>(|(array, trailing)| {
-        let (mut array, comma) = array.unwrap_or_default();
-        array.set_trailing_comma(comma);
-        array.set_trailing(std::str::from_utf8(trailing)?);
-        Ok(array)
-    })
-});
+        ws_comment_newline,
+    )
+        .map_res::<_, _, std::str::Utf8Error>(|(array, trailing)| {
+            let (mut array, comma) = array.unwrap_or_default();
+            array.set_trailing_comma(comma);
+            array.set_trailing(std::str::from_utf8(trailing)?);
+            Ok(array)
+        })
+        .parse(input)
+}
 
-parse!(array_value() -> Value, {
-    attempt((
-        ws_comment_newline(),
-        value(),
-        ws_comment_newline(),
-    )).and_then::<_, _, std::str::Utf8Error>(|(ws1, v, ws2)| {
-        let v = v.decorated(
-            std::str::from_utf8(ws1)?,
-            std::str::from_utf8(ws2)?,
-        );
-        Ok(v)
-    })
-});
+pub(crate) fn array_value(input: Input<'_>) -> IResult<Input<'_>, Value, ParserError<'_>> {
+    (ws_comment_newline, value, ws_comment_newline)
+        .map_res::<_, _, std::str::Utf8Error>(|(ws1, v, ws2)| {
+            let v = v.decorated(std::str::from_utf8(ws1)?, std::str::from_utf8(ws2)?);
+            Ok(v)
+        })
+        .parse(input)
+}
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use combine::stream::position::Stream;
-
     #[test]
     fn arrays() {
         let inputs = [
@@ -101,12 +109,13 @@ mod test {
             r#"[ { x = 1, a = "2" }, {a = "a",b = "b",     c =    "c"} ]"#,
         ];
         for input in inputs {
-            parsed_value_eq!(input);
+            let parsed = array.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
 
         let invalid_inputs = [r#"["#, r#"[,]"#, r#"[,2]"#, r#"[1e165,,]"#];
         for input in invalid_inputs {
-            let parsed = array().easy_parse(Stream::new(input.as_bytes()));
+            let parsed = array.parse(input.as_bytes()).finish();
             assert!(parsed.is_err());
         }
     }
diff --git a/crates/toml_edit/src/parser/datetime.rs b/crates/toml_edit/src/parser/datetime.rs
index b3360c04..bdf7c027 100644
--- a/crates/toml_edit/src/parser/datetime.rs
+++ b/crates/toml_edit/src/parser/datetime.rs
@@ -1,94 +1,82 @@
-use combine::parser::byte::byte;
-use combine::parser::range::{recognize, take_while1};
-use combine::stream::RangeStream;
-use combine::*;
-use toml_datetime::*;
+use std::ops::RangeInclusive;
 
 use crate::parser::errors::CustomError;
+use crate::parser::prelude::*;
 use crate::parser::trivia::from_utf8_unchecked;
 
+use nom8::branch::alt;
+use nom8::bytes::one_of;
+use nom8::bytes::take_while_m_n;
+use nom8::combinator::cut;
+use nom8::combinator::opt;
+use nom8::sequence::preceded;
+use toml_datetime::*;
+
 // ;; Date and Time (as defined in RFC 3339)
 
 // date-time = offset-date-time / local-date-time / local-date / local-time
-// offset-date-time = full-date "T" full-time
-// local-date-time = full-date "T" partial-time
+// offset-date-time = full-date time-delim full-time
+// local-date-time = full-date time-delim partial-time
 // local-date = full-date
 // local-time = partial-time
 // full-time = partial-time time-offset
-parse!(date_time() -> Datetime, {
-    choice!(
-        (
-            full_date(),
-            optional((
-                attempt((
-                    satisfy(is_time_delim),
-                    look_ahead(time_hour())
-                )),
-                partial_time(),
-                optional(time_offset()),
-            ))
-        )
+pub(crate) fn date_time(input: Input<'_>) -> IResult<Input<'_>, Datetime, ParserError<'_>> {
+    alt((
+        (full_date, opt((time_delim, partial_time, opt(time_offset))))
             .map(|(date, opt)| {
                 match opt {
                     // Offset Date-Time
-                    Some((_, time, offset)) => {
-                        Datetime { date: Some(date), time: Some(time), offset }
-                    }
+                    Some((_, time, offset)) => Datetime {
+                        date: Some(date),
+                        time: Some(time),
+                        offset,
+                    },
                     // Local Date
-                    None => {
-                        Datetime { date: Some(date), time: None, offset: None}
+                    None => Datetime {
+                        date: Some(date),
+                        time: None,
+                        offset: None,
                     },
                 }
-            }),
-        // Local Time
-        partial_time()
-            .message("While parsing a Time")
-            .map(|t| {
-                t.into()
             })
-    )
-        .message("While parsing a Date-Time")
-});
+            .context(Context::Expression("date-time")),
+        partial_time
+            .map(|t| t.into())
+            .context(Context::Expression("time")),
+    ))
+    .parse(input)
+}
 
 // full-date      = date-fullyear "-" date-month "-" date-mday
-parse!(full_date() -> Date, {
-    (
-        attempt((date_fullyear(), byte(b'-'))),
-        date_month(),
-        byte(b'-'),
-        date_mday(),
-    ).map(|((year, _), month, _, day)| {
-        Date { year, month, day }
-    })
-});
+pub(crate) fn full_date(input: Input<'_>) -> IResult<Input<'_>, Date, ParserError<'_>> {
+    (date_fullyear, b'-', cut((date_month, b'-', date_mday)))
+        .map(|(year, _, (month, _, day))| Date { year, month, day })
+        .parse(input)
+}
 
 // partial-time   = time-hour ":" time-minute ":" time-second [time-secfrac]
-parse!(partial_time() -> Time, {
+pub(crate) fn partial_time(input: Input<'_>) -> IResult<Input<'_>, Time, ParserError<'_>> {
     (
-        attempt((
-            time_hour(),
-            byte(b':'),
-        )),
-        time_minute(),
-        byte(b':'),
-        time_second(),
-        optional(attempt(time_secfrac())),
-    ).map(|((hour, _), minute, _, second, nanosecond)| {
-        Time { hour, minute, second, nanosecond: nanosecond.unwrap_or_default() }
-    })
-});
+        time_hour,
+        b':',
+        cut((time_minute, b':', time_second, opt(time_secfrac))),
+    )
+        .map(|(hour, _, (minute, _, second, nanosecond))| Time {
+            hour,
+            minute,
+            second,
+            nanosecond: nanosecond.unwrap_or_default(),
+        })
+        .parse(input)
+}
 
 // time-offset    = "Z" / time-numoffset
 // time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
-parse!(time_offset() -> Offset, {
-    attempt(satisfy(|c| c == b'Z' || c == b'z')).map(|_| Offset::Z)
-        .or(
-            (
-                attempt(choice([byte(b'+'), byte(b'-')])),
-                time_hour(),
-                byte(b':'),
-                time_minute(),
-            ).map(|(sign, hours, _, minutes)| {
+pub(crate) fn time_offset(input: Input<'_>) -> IResult<Input<'_>, Offset, ParserError<'_>> {
+    alt((
+        one_of((b'Z', b'z')).value(Offset::Z),
+        (one_of((b'+', b'-')), cut((time_hour, b':', time_minute))).map(
+            |(sign, (hours, _, minutes))| {
                 let hours = hours as i8;
                 let hours = match sign {
                     b'+' => hours,
@@ -96,121 +84,148 @@ parse!(time_offset() -> Offset, {
                     _ => unreachable!("Parser prevents this"),
                 };
                 Offset::Custom { hours, minutes }
-            })
-        ).message("While parsing a Time Offset")
-});
+            },
+        ),
+    ))
+    .context(Context::Expression("time offset"))
+    .parse(input)
+}
 
 // date-fullyear  = 4DIGIT
-parse!(date_fullyear() -> u16, {
-    signed_digits(4).map(|d| d as u16)
-});
+pub(crate) fn date_fullyear(input: Input<'_>) -> IResult<Input<'_>, u16, ParserError<'_>> {
+    unsigned_digits::<4, 4>
+        .map(|s: &str| s.parse::<u16>().expect("4DIGIT should match u8"))
+        .parse(input)
+}
 
 // date-month     = 2DIGIT  ; 01-12
-parse!(date_month() -> u8, {
-    unsigned_digits(2).map(|d| d as u8).and_then(|v| {
-        if (1..=12).contains(&v) {
-            Ok(v)
-        } else {
-            Err(CustomError::OutOfRange)
-        }
-    })
-});
+pub(crate) fn date_month(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    unsigned_digits::<2, 2>
+        .map_res(|s: &str| {
+            let d = s.parse::<u8>().expect("2DIGIT should match u8");
+            if (1..=12).contains(&d) {
+                Ok(d)
+            } else {
+                Err(CustomError::OutOfRange)
+            }
+        })
+        .parse(input)
+}
 
 // date-mday      = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on month/year
-parse!(date_mday() -> u8, {
-    unsigned_digits(2).map(|d| d as u8).and_then(|v| {
-        if (1..=31).contains(&v) {
-            Ok(v)
-        } else {
-            Err(CustomError::OutOfRange)
-        }
-    })
-});
+pub(crate) fn date_mday(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    unsigned_digits::<2, 2>
+        .map_res(|s: &str| {
+            let d = s.parse::<u8>().expect("2DIGIT should match u8");
+            if (1..=31).contains(&d) {
+                Ok(d)
+            } else {
+                Err(CustomError::OutOfRange)
+            }
+        })
+        .parse(input)
+}
 
 // time-delim     = "T" / %x20 ; T, t, or space
-fn is_time_delim(c: u8) -> bool {
-    matches!(c, b'T' | b't' | b' ')
+pub(crate) fn time_delim(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    one_of(TIME_DELIM).parse(input)
 }
 
+const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
+
 // time-hour      = 2DIGIT  ; 00-23
-parse!(time_hour() -> u8, {
-    unsigned_digits(2).map(|d| d as u8).and_then(|v| {
-        if (0..=23).contains(&v) {
-            Ok(v)
-        } else {
-            Err(CustomError::OutOfRange)
-        }
-    })
-});
+pub(crate) fn time_hour(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    unsigned_digits::<2, 2>
+        .map_res(|s: &str| {
+            let d = s.parse::<u8>().expect("2DIGIT should match u8");
+            if (0..=23).contains(&d) {
+                Ok(d)
+            } else {
+                Err(CustomError::OutOfRange)
+            }
+        })
+        .parse(input)
+}
 
 // time-minute    = 2DIGIT  ; 00-59
-parse!(time_minute() -> u8, {
-    unsigned_digits(2).map(|d| d as u8).and_then(|v| {
-        if (0..=59).contains(&v) {
-            Ok(v)
-        } else {
-            Err(CustomError::OutOfRange)
-        }
-    })
-});
+pub(crate) fn time_minute(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    unsigned_digits::<2, 2>
+        .map_res(|s: &str| {
+            let d = s.parse::<u8>().expect("2DIGIT should match u8");
+            if (0..=59).contains(&d) {
+                Ok(d)
+            } else {
+                Err(CustomError::OutOfRange)
+            }
+        })
+        .parse(input)
+}
 
 // time-second    = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second rules
-parse!(time_second() -> u8, {
-    unsigned_digits(2).map(|d| d as u8).and_then(|v| {
-        if (0..=60).contains(&v) {
-            Ok(v)
-        } else {
-            Err(CustomError::OutOfRange)
-        }
-    })
-});
+pub(crate) fn time_second(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    unsigned_digits::<2, 2>
+        .map_res(|s: &str| {
+            let d = s.parse::<u8>().expect("2DIGIT should match u8");
+            if (0..=60).contains(&d) {
+                Ok(d)
+            } else {
+                Err(CustomError::OutOfRange)
+            }
+        })
+        .parse(input)
+}
 
 // time-secfrac   = "." 1*DIGIT
-parse!(time_secfrac() -> u32, {
-    static SCALE: [u32; 10] =
-        [0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
-    byte(b'.').and(take_while1(|c: u8| c.is_ascii_digit())).and_then::<_, _, CustomError>(|(_, repr): (u8, &[u8])| {
-        let mut repr = unsafe { from_utf8_unchecked(repr, "`is_ascii_digit` filters out on-ASCII") };
-        let max_digits = SCALE.len() - 1;
-        if max_digits < repr.len() {
-            // Millisecond precision is required. Further precision of fractional seconds is
-            // implementation-specific. If the value contains greater precision than the
-            // implementation can support, the additional precision must be truncated, not rounded.
-            repr = &repr[0..max_digits];
-        }
+pub(crate) fn time_secfrac(input: Input<'_>) -> IResult<Input<'_>, u32, ParserError<'_>> {
+    static SCALE: [u32; 10] = [
+        0,
+        100_000_000,
+        10_000_000,
+        1_000_000,
+        100_000,
+        10_000,
+        1_000,
+        100,
+        10,
+        1,
+    ];
+    const INF: usize = usize::MAX;
+    preceded(b'.', unsigned_digits::<1, INF>)
+        .map_res(|mut repr: &str| -> Result<u32, CustomError> {
+            let max_digits = SCALE.len() - 1;
+            if max_digits < repr.len() {
+                // Millisecond precision is required. Further precision of fractional seconds is
+                // implementation-specific. If the value contains greater precision than the
+                // implementation can support, the additional precision must be truncated, not rounded.
+                repr = &repr[0..max_digits];
+            }
+
+            let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
+            let num_digits = repr.len();
 
-        let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
-        let num_digits = repr.len();
-
-        // scale the number accordingly.
-        let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
-        let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
-        Ok(v)
-    })
-});
-
-parse!(signed_digits(count: usize) -> i32, {
-    recognize(skip_count_min_max(
-        *count, *count,
-        satisfy(|c: u8| c.is_ascii_digit()),
-    )).and_then(|b: &[u8]| {
-        let s = unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") };
-        s.parse::<i32>()
-    })
-});
-
-parse!(unsigned_digits(count: usize) -> u32, {
-    recognize(skip_count_min_max(
-        *count, *count,
-        satisfy(|c: u8| c.is_ascii_digit()),
-    )).and_then(|b: &[u8]| {
-        let s = unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") };
-        s.parse::<u32>()
-    })
-});
+            // scale the number accordingly.
+            let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
+            let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
+            Ok(v)
+        })
+        .parse(input)
+}
+
+pub(crate) fn unsigned_digits<const MIN: usize, const MAX: usize>(
+    input: Input<'_>,
+) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    take_while_m_n(MIN, MAX, DIGIT)
+        .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
+        .parse(input)
+}
+
+// DIGIT = %x30-39 ; 0-9
+const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
 
 #[cfg(test)]
 mod test {
+    use super::*;
+
     #[test]
     fn offset_date_time() {
         let inputs = [
@@ -219,7 +234,7 @@ mod test {
             "1979-05-27T00:32:00.999999-07:00",
         ];
         for input in inputs {
-            parsed_date_time_eq!(input, is_datetime);
+            date_time.parse(input.as_bytes()).finish().unwrap();
         }
     }
 
@@ -227,7 +242,7 @@ mod test {
     fn local_date_time() {
         let inputs = ["1979-05-27T07:32:00", "1979-05-27T00:32:00.999999"];
         for input in inputs {
-            parsed_date_time_eq!(input, is_datetime);
+            date_time.parse(input.as_bytes()).finish().unwrap();
         }
     }
 
@@ -235,7 +250,7 @@ mod test {
     fn local_date() {
         let inputs = ["1979-05-27", "2017-07-20"];
         for input in inputs {
-            parsed_date_time_eq!(input, is_datetime);
+            date_time.parse(input.as_bytes()).finish().unwrap();
         }
     }
 
@@ -243,13 +258,13 @@ mod test {
     fn local_time() {
         let inputs = ["07:32:00", "00:32:00.999999"];
         for input in inputs {
-            parsed_date_time_eq!(input, is_datetime);
+            date_time.parse(input.as_bytes()).finish().unwrap();
         }
     }
 
     #[test]
     fn time_fraction_truncated() {
         let input = "1987-07-05T17:45:00.123456789012345Z";
-        parsed_date_time_eq!(input, is_datetime);
+        date_time.parse(input.as_bytes()).finish().unwrap();
     }
 }
diff --git a/crates/toml_edit/src/parser/document.rs b/crates/toml_edit/src/parser/document.rs
index 16032ffe..77a43d00 100644
--- a/crates/toml_edit/src/parser/document.rs
+++ b/crates/toml_edit/src/parser/document.rs
@@ -1,19 +1,23 @@
 use std::cell::RefCell;
 
-use combine::parser::byte::byte;
-use combine::stream::position::{IndexPositioner, Positioner, Stream};
-use combine::stream::RangeStream;
-use combine::Parser;
-use combine::*;
+use nom8::bytes::any;
+use nom8::bytes::one_of;
+use nom8::combinator::cut;
+use nom8::combinator::eof;
+use nom8::combinator::opt;
+use nom8::combinator::peek;
+use nom8::error::FromExternalError;
+use nom8::multi::many0_count;
 
 use crate::document::Document;
 use crate::key::Key;
 use crate::parser::inline_table::KEYVAL_SEP;
 use crate::parser::key::key;
+use crate::parser::prelude::*;
+use crate::parser::state::ParseState;
 use crate::parser::table::table;
 use crate::parser::trivia::{comment, line_ending, line_trailing, newline, ws};
 use crate::parser::value::value;
-use crate::parser::{ParseState, TomlError};
 use crate::table::TableKeyValue;
 use crate::Item;
 
@@ -25,86 +29,96 @@ use crate::Item;
 //                ( ws keyval ws [ comment ] ) /
 //                ( ws table ws [ comment ] ) /
 //                  ws )
-pub(crate) fn document(s: &[u8]) -> Result<Document, TomlError> {
-    // Remove BOM if present
-    let s = s.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(s);
-
-    let parser = RefCell::new(ParseState::default());
-    let input = Stream::new(s);
-
-    let parsed = parse_ws(&parser)
-        .with(choice((
-            eof(),
-            skip_many1(
-                look_ahead(any())
-                    .then(|e| {
-                        dispatch!(e;
-                            crate::parser::trivia::COMMENT_START_SYMBOL => parse_comment(&parser),
-                            crate::parser::table::STD_TABLE_OPEN => table(&parser),
-                            crate::parser::trivia::LF |
-                            crate::parser::trivia::CR => parse_newline(&parser),
-                            _ => keyval(&parser),
-                        )
-                    })
-                    .skip(parse_ws(&parser)),
-            ),
-        )))
-        .easy_parse(input);
-    match parsed {
-        Ok((_, ref rest)) if !rest.input.is_empty() => Err(TomlError::from_unparsed(
-            (&rest.positioner
-                as &dyn Positioner<usize, Position = usize, Checkpoint = IndexPositioner>)
-                .position(),
-            s,
+pub(crate) fn document(input: Input<'_>) -> IResult<Input<'_>, Document, ParserError<'_>> {
+    let state = RefCell::new(ParseState::default());
+    let state_ref = &state;
+
+    let (i, _o) = (
+        // Remove BOM if present
+        opt(b"\xEF\xBB\xBF"),
+        parse_ws(state_ref),
+        many0_count((
+            dispatch! {peek(any);
+                crate::parser::trivia::COMMENT_START_SYMBOL => cut(parse_comment(state_ref)),
+                crate::parser::table::STD_TABLE_OPEN => cut(table(state_ref)),
+                crate::parser::trivia::LF |
+                crate::parser::trivia::CR => parse_newline(state_ref),
+                _ => cut(keyval(state_ref)),
+            },
+            parse_ws(state_ref),
         )),
-        Ok(..) => {
-            let doc = parser
-                .into_inner()
-                .into_document()
-                .map_err(|e| TomlError::custom(e.to_string()))?;
-            Ok(doc)
-        }
-        Err(e) => Err(TomlError::new(e, s)),
-    }
+        eof,
+    )
+        .parse(input)?;
+    state
+        .into_inner()
+        .into_document()
+        .map(|document| (i, document))
+        .map_err(|err| {
+            nom8::Err::Error(ParserError::from_external_error(
+                i,
+                nom8::error::ErrorKind::MapRes,
+                err,
+            ))
+        })
 }
 
-toml_parser!(parse_comment, parser, {
-    (comment(), line_ending()).and_then::<_, _, std::str::Utf8Error>(|(c, e)| {
-        let c = std::str::from_utf8(c)?;
-        parser.borrow_mut().on_comment(c, e);
-        Ok(())
-    })
-});
+pub(crate) fn parse_comment<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'_>> + 's {
+    move |i| {
+        (comment, line_ending)
+            .map_res::<_, _, std::str::Utf8Error>(|(c, e)| {
+                let c = std::str::from_utf8(c)?;
+                state.borrow_mut().on_comment(c, e);
+                Ok(())
+            })
+            .parse(i)
+    }
+}
 
-toml_parser!(parse_ws, parser, ws().map(|w| parser.borrow_mut().on_ws(w)));
+pub(crate) fn parse_ws<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| ws.map(|w| state.borrow_mut().on_ws(w)).parse(i)
+}
 
-toml_parser!(parse_newline, parser, {
-    newline().map(|_| parser.borrow_mut().on_ws("\n"))
-});
+pub(crate) fn parse_newline<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| newline.map(|_| state.borrow_mut().on_ws("\n")).parse(i)
+}
 
-toml_parser!(keyval, parser, {
-    parse_keyval().and_then(|(p, kv)| parser.borrow_mut().on_keyval(p, kv))
-});
+pub(crate) fn keyval<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| {
+        parse_keyval
+            .map_res(|(p, kv)| state.borrow_mut().on_keyval(p, kv))
+            .parse(i)
+    }
+}
 
 // keyval = key keyval-sep val
-parser! {
-    fn parse_keyval['a, I]()(I) -> (Vec<Key>, TableKeyValue)
-    where
-        [I: RangeStream<
-         Range = &'a [u8],
-         Token = u8>,
-         I::Error: ParseError<u8, &'a [u8], <I as StreamOnce>::Position>,
-         <I::Error as ParseError<u8, &'a [u8], <I as StreamOnce>::Position>>::StreamError:
-         From<std::num::ParseIntError> +
-         From<std::num::ParseFloatError> +
-         From<std::str::Utf8Error> +
-         From<crate::parser::errors::CustomError>
-    ] {
-        (
-            key(),
-            byte(KEYVAL_SEP),
-            (ws(), value(), line_trailing())
-        ).and_then::<_, _, std::str::Utf8Error>(|(key, _, v)| {
+pub(crate) fn parse_keyval(
+    input: Input<'_>,
+) -> IResult<Input<'_>, (Vec<Key>, TableKeyValue), ParserError<'_>> {
+    (
+        key,
+        cut((
+            one_of(KEYVAL_SEP)
+                .context(Context::Expected(ParserValue::CharLiteral('.')))
+                .context(Context::Expected(ParserValue::CharLiteral('='))),
+            (
+                ws,
+                value,
+                line_trailing
+                    .context(Context::Expected(ParserValue::CharLiteral('\n')))
+                    .context(Context::Expected(ParserValue::CharLiteral('#'))),
+            ),
+        )),
+    )
+        .map_res::<_, _, std::str::Utf8Error>(|(key, (_, v))| {
             let mut path = key;
             let key = path.pop().expect("grammar ensures at least 1");
 
@@ -116,21 +130,20 @@ parser! {
                 TableKeyValue {
                     key,
                     value: Item::Value(v),
-                }
+                },
             ))
         })
-    }
+        .parse(input)
 }
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use snapbox::assert_eq;
-
     #[test]
     fn documents() {
         let documents = [
+            "",
             r#"
 # This is a TOML document.
 
@@ -188,20 +201,18 @@ key = "value"
 "#,
         ];
         for input in documents {
-            let doc = document(input.as_bytes());
-            let doc = match doc {
+            let parsed = document.parse(input.as_bytes()).finish();
+            let doc = match parsed {
                 Ok(doc) => doc,
                 Err(err) => {
                     panic!(
-                        "Parse error: {}\nFailed to parse:\n```\n{}\n```",
+                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
                         err, input
                     )
                 }
             };
 
-            dbg!(doc.to_string());
-            dbg!(input);
-            assert_eq(input, doc.to_string());
+            snapbox::assert_eq(input, doc.to_string());
         }
 
         let parse_only = ["\u{FEFF}
@@ -211,12 +222,12 @@ version = \"0.0.1\"
 authors = []
 "];
         for input in parse_only {
-            let doc = document(input.as_bytes());
-            match doc {
+            let parsed = document.parse(input.as_bytes()).finish();
+            match parsed {
                 Ok(_) => (),
                 Err(err) => {
                     panic!(
-                        "Parse error: {}\nFailed to parse:\n```\n{}\n```",
+                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
                         err, input
                     )
                 }
@@ -226,9 +237,8 @@ authors = []
         let invalid_inputs = [r#" hello = 'darkness' # my old friend
 $"#];
         for input in invalid_inputs {
-            let doc = document(input.as_bytes());
-
-            assert!(doc.is_err());
+            let parsed = document.parse(input.as_bytes()).finish();
+            assert!(parsed.is_err(), "Input: {:?}", input);
         }
     }
 }
diff --git a/crates/toml_edit/src/parser/errors.rs b/crates/toml_edit/src/parser/errors.rs
index a2ff053d..ac09094b 100644
--- a/crates/toml_edit/src/parser/errors.rs
+++ b/crates/toml_edit/src/parser/errors.rs
@@ -1,11 +1,9 @@
 use std::error::Error as StdError;
 use std::fmt::{Display, Formatter, Result};
 
-use combine::easy::Errors as ParseError;
-use combine::stream::easy::Error;
-use combine::stream::position::SourcePosition;
 use itertools::Itertools;
 
+use crate::parser::prelude::*;
 use crate::Key;
 
 /// Type representing a TOML parse error
@@ -16,20 +14,21 @@ pub struct TomlError {
 }
 
 impl TomlError {
-    pub(crate) fn new(error: ParseError<u8, &[u8], usize>, input: &[u8]) -> Self {
-        let fancy = FancyError::new(error, input);
-        let message = fancy.to_string();
-        let line_col = Some((fancy.position.line as usize, fancy.position.column as usize));
+    pub(crate) fn new(error: ParserError<'_>, original: Input<'_>) -> Self {
+        use nom8::input::Offset;
+        let offset = original.offset(error.input);
+        let position = translate_position(original, offset);
+        let message = ParserErrorDisplay {
+            error: &error,
+            original,
+            position,
+        }
+        .to_string();
+        let line_col = Some(position);
         Self { message, line_col }
     }
 
-    pub(crate) fn from_unparsed(pos: usize, input: &[u8]) -> Self {
-        Self::new(
-            ParseError::new(pos, CustomError::UnparsedLine.into()),
-            input,
-        )
-    }
-
+    #[cfg(feature = "serde")]
     pub(crate) fn custom(message: String) -> Self {
         Self {
             message,
@@ -70,53 +69,103 @@ impl StdError for TomlError {
 }
 
 #[derive(Debug)]
-pub(crate) struct FancyError<'a> {
-    errors: Vec<Error<char, String>>,
-    position: SourcePosition,
-    input: &'a [u8],
+pub(crate) struct ParserError<'b> {
+    input: Input<'b>,
+    context: Vec<Context>,
+    cause: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
 }
 
-impl<'a> FancyError<'a> {
-    pub(crate) fn new(error: ParseError<u8, &'a [u8], usize>, input: &'a [u8]) -> Self {
-        let position = translate_position(input, error.position);
-        let errors: Vec<_> = error
-            .errors
-            .into_iter()
-            .map(|e| {
-                e.map_token(char::from)
-                    .map_range(|s| String::from_utf8_lossy(s).into_owned())
-            })
-            .collect();
+impl<'b> nom8::error::ParseError<Input<'b>> for ParserError<'b> {
+    fn from_error_kind(input: Input<'b>, _kind: nom8::error::ErrorKind) -> Self {
+        Self {
+            input,
+            context: Default::default(),
+            cause: Default::default(),
+        }
+    }
+
+    fn append(_input: Input<'b>, _kind: nom8::error::ErrorKind, other: Self) -> Self {
+        other
+    }
+
+    fn from_char(_input: Input<'b>, _: char) -> Self {
+        unimplemented!("this shouldn't be called with a binary parser")
+    }
+
+    fn or(self, other: Self) -> Self {
+        other
+    }
+}
+
+impl<'b> nom8::error::ContextError<Input<'b>, Context> for ParserError<'b> {
+    fn add_context(_input: Input<'b>, ctx: Context, mut other: Self) -> Self {
+        other.context.push(ctx);
+        other
+    }
+}
+
+impl<'b, E: std::error::Error + Send + Sync + 'static> nom8::error::FromExternalError<Input<'b>, E>
+    for ParserError<'b>
+{
+    fn from_external_error(input: Input<'b>, _kind: nom8::error::ErrorKind, e: E) -> Self {
         Self {
-            errors,
-            position,
             input,
+            context: Default::default(),
+            cause: Some(Box::new(e)),
         }
     }
 }
 
-impl<'a> Display for FancyError<'a> {
-    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
-        let SourcePosition { line, column } = self.position;
+// For tests
+impl<'b> std::cmp::PartialEq for ParserError<'b> {
+    fn eq(&self, other: &Self) -> bool {
+        self.input == other.input
+            && self.context == other.context
+            && self.cause.as_ref().map(ToString::to_string)
+                == other.cause.as_ref().map(ToString::to_string)
+    }
+}
+
+struct ParserErrorDisplay<'a> {
+    error: &'a ParserError<'a>,
+    original: Input<'a>,
+    position: (usize, usize),
+}
 
+impl<'a> std::fmt::Display for ParserErrorDisplay<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let (line, column) = self.position;
         let line_num = line + 1;
         let col_num = column + 1;
-        let offset = line_num.to_string().len();
+        let gutter = line_num.to_string().len();
         let content = self
-            .input
+            .original
             .split(|b| *b == b'\n')
-            .nth((line) as usize)
+            .nth(line)
             .expect("valid line number");
         let content = String::from_utf8_lossy(content);
 
+        let expression = self.error.context.iter().find_map(|c| match c {
+            Context::Expression(c) => Some(c),
+            _ => None,
+        });
+        let expected = self
+            .error
+            .context
+            .iter()
+            .filter_map(|c| match c {
+                Context::Expected(c) => Some(c),
+                _ => None,
+            })
+            .collect::<Vec<_>>();
+
         writeln!(
             f,
             "TOML parse error at line {}, column {}",
             line_num, col_num
         )?;
-
         //   |
-        for _ in 0..=offset {
+        for _ in 0..=gutter {
             write!(f, " ")?;
         }
         writeln!(f, "|")?;
@@ -126,7 +175,7 @@ impl<'a> Display for FancyError<'a> {
         writeln!(f, "{}", content)?;
 
         //   |          ^
-        for _ in 0..=offset {
+        for _ in 0..=gutter {
             write!(f, " ")?;
         }
         write!(f, "|")?;
@@ -135,16 +184,59 @@ impl<'a> Display for FancyError<'a> {
         }
         writeln!(f, "^")?;
 
-        Error::fmt_errors(self.errors.as_ref(), f)
+        if let Some(expression) = expression {
+            writeln!(f, "Invalid {}", expression)?;
+        }
+
+        if !expected.is_empty() {
+            write!(f, "Expected ")?;
+            for (i, expected) in expected.iter().enumerate() {
+                if i != 0 {
+                    write!(f, ", ")?;
+                }
+                write!(f, "{}", expected)?;
+            }
+            writeln!(f)?;
+        }
+        if let Some(cause) = &self.error.cause {
+            write!(f, "{}", cause)?;
+        }
+
+        Ok(())
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub(crate) enum Context {
+    Expression(&'static str),
+    Expected(ParserValue),
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub(crate) enum ParserValue {
+    CharLiteral(char),
+    StringLiteral(&'static str),
+    Description(&'static str),
+}
+
+impl std::fmt::Display for ParserValue {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParserValue::CharLiteral('\n') => "newline".fmt(f),
+            ParserValue::CharLiteral('`') => "'`'".fmt(f),
+            ParserValue::CharLiteral(c) if c.is_ascii_control() => {
+                write!(f, "`{}`", c.escape_debug())
+            }
+            ParserValue::CharLiteral(c) => write!(f, "`{}`", c),
+            ParserValue::StringLiteral(c) => write!(f, "`{}`", c),
+            ParserValue::Description(c) => write!(f, "{}", c),
+        }
     }
 }
 
-fn translate_position(input: &[u8], index: usize) -> SourcePosition {
+fn translate_position(input: &[u8], index: usize) -> (usize, usize) {
     if input.is_empty() {
-        return SourcePosition {
-            line: 0,
-            column: index as i32,
-        };
+        return (0, index);
     }
 
     let safe_index = index.min(input.len() - 1);
@@ -162,14 +254,14 @@ fn translate_position(input: &[u8], index: usize) -> SourcePosition {
         None => 0,
     };
     let line = input[0..line_start].iter().filter(|b| **b == b'\n').count();
-    let line = line as i32;
+    let line = line;
 
     let column = std::str::from_utf8(&input[line_start..=index])
         .map(|s| s.chars().count() - 1)
         .unwrap_or_else(|_| index - line_start);
-    let column = (column + column_offset) as i32;
+    let column = column + column_offset;
 
-    SourcePosition { line, column }
+    (line, column)
 }
 
 #[cfg(test)]
@@ -181,7 +273,7 @@ mod test_translate_position {
         let input = b"";
         let index = 0;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 0, column: 0 });
+        assert_eq!(position, (0, 0));
     }
 
     #[test]
@@ -189,7 +281,7 @@ mod test_translate_position {
         let input = b"Hello";
         let index = 0;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 0, column: 0 });
+        assert_eq!(position, (0, 0));
     }
 
     #[test]
@@ -197,13 +289,7 @@ mod test_translate_position {
         let input = b"Hello";
         let index = input.len() - 1;
         let position = translate_position(&input[..], index);
-        assert_eq!(
-            position,
-            SourcePosition {
-                line: 0,
-                column: input.len() as i32 - 1
-            }
-        );
+        assert_eq!(position, (0, input.len() - 1));
     }
 
     #[test]
@@ -211,13 +297,7 @@ mod test_translate_position {
         let input = b"Hello";
         let index = input.len();
         let position = translate_position(&input[..], index);
-        assert_eq!(
-            position,
-            SourcePosition {
-                line: 0,
-                column: input.len() as i32
-            }
-        );
+        assert_eq!(position, (0, input.len()));
     }
 
     #[test]
@@ -225,7 +305,7 @@ mod test_translate_position {
         let input = b"Hello\nWorld\n";
         let index = 2;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 0, column: 2 });
+        assert_eq!(position, (0, 2));
     }
 
     #[test]
@@ -233,7 +313,7 @@ mod test_translate_position {
         let input = b"Hello\nWorld\n";
         let index = 5;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 0, column: 5 });
+        assert_eq!(position, (0, 5));
     }
 
     #[test]
@@ -241,7 +321,7 @@ mod test_translate_position {
         let input = b"Hello\nWorld\n";
         let index = 6;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 1, column: 0 });
+        assert_eq!(position, (1, 0));
     }
 
     #[test]
@@ -249,7 +329,7 @@ mod test_translate_position {
         let input = b"Hello\nWorld\n";
         let index = 8;
         let position = translate_position(&input[..], index);
-        assert_eq!(position, SourcePosition { line: 1, column: 2 });
+        assert_eq!(position, (1, 2));
     }
 }
 
@@ -263,8 +343,6 @@ pub(crate) enum CustomError {
         key: Vec<Key>,
         actual: &'static str,
     },
-    InvalidHexEscape(u32),
-    UnparsedLine,
     OutOfRange,
 }
 
@@ -315,10 +393,6 @@ impl Display for CustomError {
                     path, actual
                 )
             }
-            CustomError::InvalidHexEscape(h) => {
-                writeln!(f, "Invalid hex escape code: {:x} ", h)
-            }
-            CustomError::UnparsedLine => writeln!(f, "Could not parse the line"),
             CustomError::OutOfRange => writeln!(f, "Value is out of range"),
         }
     }
diff --git a/crates/toml_edit/src/parser/inline_table.rs b/crates/toml_edit/src/parser/inline_table.rs
index 442e9832..dae8f1e9 100644
--- a/crates/toml_edit/src/parser/inline_table.rs
+++ b/crates/toml_edit/src/parser/inline_table.rs
@@ -1,30 +1,39 @@
-use combine::parser::byte::byte;
-use combine::stream::RangeStream;
-use combine::*;
-use indexmap::map::Entry;
+use nom8::bytes::one_of;
+use nom8::combinator::cut;
+use nom8::multi::separated_list0;
+use nom8::sequence::delimited;
 
 use crate::key::Key;
 use crate::parser::errors::CustomError;
 use crate::parser::key::key;
+use crate::parser::prelude::*;
 use crate::parser::trivia::ws;
 use crate::parser::value::value;
 use crate::table::TableKeyValue;
 use crate::{InlineTable, InternalString, Item, Value};
 
+use indexmap::map::Entry;
+
 // ;; Inline Table
 
 // inline-table = inline-table-open inline-table-keyvals inline-table-close
-parse!(inline_table() -> InlineTable, {
-    between(byte(INLINE_TABLE_OPEN), byte(INLINE_TABLE_CLOSE),
-            inline_table_keyvals().and_then(|(kv, p)| table_from_pairs(kv, p)))
-});
+pub(crate) fn inline_table(input: Input<'_>) -> IResult<Input<'_>, InlineTable, ParserError<'_>> {
+    delimited(
+        INLINE_TABLE_OPEN,
+        cut(inline_table_keyvals.map_res(|(kv, p)| table_from_pairs(kv, p))),
+        cut(INLINE_TABLE_CLOSE)
+            .context(Context::Expression("inline table"))
+            .context(Context::Expected(ParserValue::CharLiteral('}'))),
+    )
+    .parse(input)
+}
 
 fn table_from_pairs(
     v: Vec<(Vec<Key>, TableKeyValue)>,
     preamble: &str,
 ) -> Result<InlineTable, CustomError> {
     let mut root = InlineTable::new();
-    root.preamble = InternalString::from(preamble);
+    root.preamble = preamble.into();
     // Assuming almost all pairs will be directly in `root`
     root.items.reserve(v.len());
 
@@ -83,40 +92,43 @@ pub(crate) const KEYVAL_SEP: u8 = b'=';
 // ( key keyval-sep val inline-table-sep inline-table-keyvals-non-empty ) /
 // ( key keyval-sep val )
 
-parse!(inline_table_keyvals() -> (Vec<(Vec<Key>, TableKeyValue)>, &'a str), {
-    (
-        sep_by(keyval(), byte(INLINE_TABLE_SEP)),
-        ws(),
-    )
-});
+fn inline_table_keyvals(
+    input: Input<'_>,
+) -> IResult<Input<'_>, (Vec<(Vec<Key>, TableKeyValue)>, &str), ParserError<'_>> {
+    (separated_list0(INLINE_TABLE_SEP, keyval), ws).parse(input)
+}
 
-parse!(keyval() -> (Vec<Key>, TableKeyValue), {
+fn keyval(input: Input<'_>) -> IResult<Input<'_>, (Vec<Key>, TableKeyValue), ParserError<'_>> {
     (
-        key(),
-        byte(KEYVAL_SEP),
-        (ws(), value(), ws()),
-    ).map(|(key, _, v)| {
-        let mut path = key;
-        let key = path.pop().expect("grammar ensures at least 1");
+        key,
+        cut((
+            one_of(KEYVAL_SEP)
+                .context(Context::Expected(ParserValue::CharLiteral('.')))
+                .context(Context::Expected(ParserValue::CharLiteral('='))),
+            (ws, value, ws),
+        )),
+    )
+        .map(|(key, (_, v))| {
+            let mut path = key;
+            let key = path.pop().expect("grammar ensures at least 1");
 
-        let (pre, v, suf) = v;
-        let v = v.decorated(pre, suf);
-        (
-            path,
-            TableKeyValue {
-                key,
-                value: Item::Value(v),
-            }
-        )
-    })
-});
+            let (pre, v, suf) = v;
+            let v = v.decorated(pre, suf);
+            (
+                path,
+                TableKeyValue {
+                    key,
+                    value: Item::Value(v),
+                },
+            )
+        })
+        .parse(input)
+}
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use combine::stream::position::Stream;
-
     #[test]
     fn inline_tables() {
         let inputs = [
@@ -127,11 +139,12 @@ mod test {
             r#"{ hello.world = "a" }"#,
         ];
         for input in inputs {
-            parsed_value_eq!(input);
+            let parsed = inline_table.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
         let invalid_inputs = [r#"{a = 1e165"#, r#"{ hello = "world", a = 2, hello = 1}"#];
         for input in invalid_inputs {
-            let parsed = inline_table().easy_parse(Stream::new(input.as_bytes()));
+            let parsed = inline_table.parse(input.as_bytes()).finish();
             assert!(parsed.is_err());
         }
     }
diff --git a/crates/toml_edit/src/parser/key.rs b/crates/toml_edit/src/parser/key.rs
index ceb1873f..52c61569 100644
--- a/crates/toml_edit/src/parser/key.rs
+++ b/crates/toml_edit/src/parser/key.rs
@@ -1,9 +1,12 @@
-use combine::parser::byte::byte;
-use combine::parser::range::{recognize_with_value, take_while1};
-use combine::stream::RangeStream;
-use combine::*;
+use std::ops::RangeInclusive;
+
+use nom8::bytes::any;
+use nom8::bytes::take_while1;
+use nom8::combinator::peek;
+use nom8::multi::separated_list1;
 
 use crate::key::Key;
+use crate::parser::prelude::*;
 use crate::parser::strings::{basic_string, literal_string};
 use crate::parser::trivia::{from_utf8_unchecked, ws};
 use crate::repr::{Decor, Repr};
@@ -11,48 +14,58 @@ use crate::InternalString;
 
 // key = simple-key / dotted-key
 // dotted-key = simple-key 1*( dot-sep simple-key )
-parse!(key() -> Vec<Key>, {
-    sep_by1(
-        attempt((
-            ws(),
-            simple_key(),
-            ws(),
-        )).map(|(pre, (raw, key), suffix)| {
-            Key::new(key).with_repr_unchecked(Repr::new_unchecked(raw)).with_decor(Decor::new(pre, suffix))
+pub(crate) fn key(input: Input<'_>) -> IResult<Input<'_>, Vec<Key>, ParserError<'_>> {
+    separated_list1(
+        DOT_SEP,
+        (ws, simple_key, ws).map(|(pre, (raw, key), suffix)| {
+            Key::new(key)
+                .with_repr_unchecked(Repr::new_unchecked(raw))
+                .with_decor(Decor::new(pre, suffix))
         }),
-        byte(DOT_SEP)
-    ).expected("key")
-});
+    )
+    .context(Context::Expression("key"))
+    .parse(input)
+}
 
 // simple-key = quoted-key / unquoted-key
 // quoted-key = basic-string / literal-string
-parse!(simple_key() -> (&'a str, InternalString), {
-    recognize_with_value(
-        look_ahead(any()).then(|e| {
-            dispatch!(e;
-                crate::parser::strings::QUOTATION_MARK => basic_string().map(|s: String| s.into()),
-                crate::parser::strings::APOSTROPHE => literal_string().map(|s: &'a str| s.into()),
-                _ => unquoted_key().map(|s: &'a str| s.into()),
-            )
+pub(crate) fn simple_key(
+    input: Input<'_>,
+) -> IResult<Input<'_>, (&str, InternalString), ParserError<'_>> {
+    dispatch! {peek(any);
+        crate::parser::strings::QUOTATION_MARK => basic_string
+            .map(|s: std::borrow::Cow<'_, str>| s.as_ref().into()),
+        crate::parser::strings::APOSTROPHE => literal_string.map(|s: &str| s.into()),
+        _ => unquoted_key.map(|s: &str| s.into()),
+    }
+        .with_recognized()
+        .map(|(k, b)| {
+            let s = unsafe { from_utf8_unchecked(b, "If `quoted_key` or `unquoted_key` are valid, then their `recognize`d value is valid") };
+            (s, k)
         })
-    ).map(|(b, k)| {
-        let s = unsafe { from_utf8_unchecked(b, "If `quoted_key` or `unquoted_key` are valid, then their `recognize`d value is valid") };
-        (s, k)
-    })
-});
+        .parse(input)
+}
 
 // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
-parse!(unquoted_key() -> &'a str, {
-    take_while1(is_unquoted_char).map(|b| {
-        unsafe { from_utf8_unchecked(b, "`is_unquoted_char` filters out on-ASCII") }
-    })
-});
+fn unquoted_key(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    take_while1(UNQUOTED_CHAR)
+        .map(|b| unsafe { from_utf8_unchecked(b, "`is_unquoted_char` filters out on-ASCII") })
+        .parse(input)
+}
 
-#[inline]
 pub(crate) fn is_unquoted_char(c: u8) -> bool {
-    matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')
+    use nom8::input::FindToken;
+    UNQUOTED_CHAR.find_token(c)
 }
 
+const UNQUOTED_CHAR: (
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    u8,
+    u8,
+) = (b'A'..=b'Z', b'a'..=b'z', b'0'..=b'9', b'-', b'_');
+
 // dot-sep   = ws %x2E ws  ; . Period
 const DOT_SEP: u8 = b'.';
 
@@ -60,9 +73,6 @@ const DOT_SEP: u8 = b'.';
 mod test {
     use super::*;
 
-    use combine::stream::position::Stream;
-    use snapbox::assert_eq;
-
     #[test]
     fn keys() {
         let cases = [
@@ -72,11 +82,8 @@ mod test {
         ];
 
         for (input, expected) in cases {
-            let parsed = simple_key().easy_parse(Stream::new(input.as_bytes()));
-            assert!(parsed.is_ok());
-            let ((.., k), rest) = parsed.unwrap();
-            assert_eq(k.as_str(), expected);
-            assert_eq!(rest.input.len(), 0);
+            let parsed = simple_key.parse(input.as_bytes()).finish();
+            assert_eq!(parsed, Ok((input, expected.into())), "Parsing {input:?}");
         }
     }
 }
diff --git a/crates/toml_edit/src/parser/macros.rs b/crates/toml_edit/src/parser/macros.rs
index 8f27f577..591b2f7b 100644
--- a/crates/toml_edit/src/parser/macros.rs
+++ b/crates/toml_edit/src/parser/macros.rs
@@ -1,112 +1,13 @@
-#[doc(hidden)]
-#[macro_export]
-macro_rules! parse (
-    ($name:ident( $($arg: ident :  $arg_type: ty),* ) -> $ret:ty, $code:expr) => (
-        parser!{
-            pub(crate) fn $name['a, I]($($arg : $arg_type),*)(I) -> $ret
-                where
-                [I: RangeStream<
-                 Range = &'a [u8],
-                 Token = u8>,
-                 I::Error: ParseError<u8, &'a [u8], <I as StreamOnce>::Position>,
-                 <I::Error as ParseError<u8, &'a [u8], <I as StreamOnce>::Position>>::StreamError:
-                 From<std::num::ParseIntError> +
-                 From<std::num::ParseFloatError> +
-                 From<std::str::Utf8Error> +
-                 From<$crate::parser::errors::CustomError>
-                ]
-            {
-                $code
+macro_rules! dispatch {
+    ($match_parser: expr; $( $pat:pat $(if $pred:expr)? => $expr: expr ),+ $(,)? ) => {
+        move |i|
+        {
+            let (i, initial) = $match_parser.parse(i)?;
+            match initial {
+                $(
+                    $pat $(if $pred)? => $expr.parse(i),
+                )*
             }
         }
-    );
-);
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! toml_parser (
-    ($name:ident, $argh:ident, $closure:expr) => (
-        parser!{
-            fn $name['a, 'b, I]($argh: &'b RefCell<ParseState>)(I) -> ()
-            where
-                [I: RangeStream<
-                 Range = &'a [u8],
-                 Token = u8>,
-                 I::Error: ParseError<u8, &'a [u8], <I as StreamOnce>::Position>,
-                 <I::Error as ParseError<u8, &'a [u8], <I as StreamOnce>::Position>>::StreamError:
-                 From<std::num::ParseIntError> +
-                 From<std::num::ParseFloatError> +
-                 From<std::str::Utf8Error> +
-                 From<$crate::parser::errors::CustomError>
-                ]
-            {
-                $closure
-            }
-        }
-    );
-);
-
-#[cfg(test)]
-macro_rules! parsed_eq {
-    ($parsed:ident, $expected:expr) => {{
-        assert!($parsed.is_ok(), "{:?}", $parsed.err().unwrap());
-        let (v, rest) = $parsed.unwrap();
-        assert_eq!(v, $expected);
-        assert!(rest.input.is_empty());
-    }};
-}
-
-#[cfg(test)]
-macro_rules! parsed_float_eq {
-    ($input:ident, $expected:expr) => {{
-        let parsed = crate::parser::numbers::float().easy_parse(Stream::new($input.as_bytes()));
-        let (v, rest) = match parsed {
-            Ok(parsed) => parsed,
-            Err(err) => {
-                panic!("Unexpected error for {:?}: {:?}", $input, err);
-            }
-        };
-        if $expected.is_nan() {
-            assert!(v.is_nan());
-        } else if $expected.is_infinite() {
-            assert!(v.is_infinite());
-            assert_eq!($expected.is_sign_positive(), v.is_sign_positive());
-        } else {
-            dbg!($expected);
-            dbg!(v);
-            assert!(($expected - v).abs() < std::f64::EPSILON);
-        }
-        assert!(rest.input.is_empty());
-    }};
-}
-
-#[cfg(test)]
-macro_rules! parsed_value_eq {
-    ($input:expr) => {
-        use combine::EasyParser;
-        let parsed = crate::parser::value::value()
-            .easy_parse(combine::stream::position::Stream::new($input.as_bytes()));
-        let (v, rest) = match parsed {
-            Ok(parsed) => parsed,
-            Err(err) => {
-                panic!("Unexpected error for {:?}: {:?}", $input, err);
-            }
-        };
-        snapbox::assert_eq(v.to_string(), $input);
-        assert!(rest.input.is_empty());
-    };
-}
-
-#[cfg(test)]
-macro_rules! parsed_date_time_eq {
-    ($input:expr, $is:ident) => {{
-        use combine::EasyParser;
-        let parsed = crate::parser::value::value()
-            .easy_parse(combine::stream::position::Stream::new($input.as_bytes()));
-        assert!(parsed.is_ok());
-        let (v, rest) = parsed.unwrap();
-        snapbox::assert_eq(v.to_string(), $input);
-        assert!(rest.input.is_empty());
-        assert!(v.$is());
-    }};
+    }
 }
diff --git a/crates/toml_edit/src/parser/mod.rs b/crates/toml_edit/src/parser/mod.rs
index 03c9ed34..72f058ff 100644
--- a/crates/toml_edit/src/parser/mod.rs
+++ b/crates/toml_edit/src/parser/mod.rs
@@ -1,26 +1,64 @@
-#![allow(clippy::unneeded_field_pattern)]
-#![allow(clippy::toplevel_ref_arg)]
+#![allow(clippy::type_complexity)]
 
 #[macro_use]
-mod macros;
-mod array;
+pub(crate) mod macros;
+
+pub(crate) mod array;
 pub(crate) mod datetime;
-mod document;
-mod errors;
-mod inline_table;
-mod key;
+pub(crate) mod document;
+pub(crate) mod errors;
+pub(crate) mod inline_table;
+pub(crate) mod key;
 pub(crate) mod numbers;
-mod state;
+pub(crate) mod state;
 pub(crate) mod strings;
-mod table;
-mod trivia;
-mod value;
-
-pub(crate) use self::document::document;
-pub use self::errors::TomlError;
-pub(crate) use self::key::is_unquoted_char;
-pub(crate) use self::key::key as key_path;
-pub(crate) use self::key::simple_key;
-pub(crate) use self::value::value as value_parser;
-
-use self::state::ParseState;
+pub(crate) mod table;
+pub(crate) mod trivia;
+pub(crate) mod value;
+
+pub use errors::TomlError;
+
+mod prelude {
+    pub(crate) use super::errors::Context;
+    pub(crate) use super::errors::ParserError;
+    pub(crate) use super::errors::ParserValue;
+    pub(crate) use nom8::IResult;
+    pub(crate) use nom8::Parser as _;
+
+    #[cfg(test)]
+    pub(crate) use nom8::FinishIResult as _;
+
+    pub(crate) type Input<'b> = &'b [u8];
+
+    pub(crate) fn ok_error<I, O, E>(res: IResult<I, O, E>) -> Result<Option<(I, O)>, nom8::Err<E>> {
+        match res {
+            Ok(ok) => Ok(Some(ok)),
+            Err(nom8::Err::Error(_)) => Ok(None),
+            Err(err) => Err(err),
+        }
+    }
+
+    #[allow(dead_code)]
+    pub(crate) fn trace<I: std::fmt::Debug, O: std::fmt::Debug, E: std::fmt::Debug>(
+        context: impl std::fmt::Display,
+        mut parser: impl nom8::Parser<I, O, E>,
+    ) -> impl FnMut(I) -> IResult<I, O, E> {
+        static DEPTH: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
+        move |input: I| {
+            let depth = DEPTH.fetch_add(1, std::sync::atomic::Ordering::SeqCst) * 2;
+            eprintln!("{:depth$}--> {} {:?}", "", context, input);
+            match parser.parse(input) {
+                Ok((i, o)) => {
+                    DEPTH.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
+                    eprintln!("{:depth$}<-- {} {:?}", "", context, i);
+                    Ok((i, o))
+                }
+                Err(err) => {
+                    DEPTH.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
+                    eprintln!("{:depth$}<-- {} {:?}", "", context, err);
+                    Err(err)
+                }
+            }
+        }
+    }
+}
diff --git a/crates/toml_edit/src/parser/numbers.rs b/crates/toml_edit/src/parser/numbers.rs
index 58a2c4a3..3ac32a29 100644
--- a/crates/toml_edit/src/parser/numbers.rs
+++ b/crates/toml_edit/src/parser/numbers.rs
@@ -1,207 +1,270 @@
-use combine::parser::byte::{byte, bytes, digit, hex_digit, oct_digit};
-use combine::parser::range::{range, recognize};
-use combine::stream::RangeStream;
-use combine::*;
+use std::ops::RangeInclusive;
 
+use nom8::branch::alt;
+use nom8::bytes::any;
+use nom8::bytes::one_of;
+use nom8::bytes::tag;
+use nom8::combinator::cut;
+use nom8::combinator::opt;
+use nom8::combinator::peek;
+use nom8::multi::many0_count;
+use nom8::sequence::preceded;
+
+use crate::parser::prelude::*;
 use crate::parser::trivia::from_utf8_unchecked;
 
 // ;; Boolean
 
 // boolean = true / false
-pub(crate) const TRUE: &[u8] = b"true";
-pub(crate) const FALSE: &[u8] = b"false";
-parse!(boolean() -> bool, {
-    choice((
-        (byte(TRUE[0]), range(&TRUE[1..]),),
-        (byte(FALSE[0]), range(&FALSE[1..]),),
-    )).map(|p| p.0 == b't')
-});
-parse!(true_() -> bool, {
-    range(crate::parser::numbers::TRUE).map(|_| true)
-});
-parse!(false_() -> bool, {
-    range(crate::parser::numbers::FALSE).map(|_| false)
-});
+#[allow(dead_code)] // directly define in `fn value`
+pub(crate) fn boolean(input: Input<'_>) -> IResult<Input<'_>, bool, ParserError<'_>> {
+    alt((true_, false_)).parse(input)
+}
+
+pub(crate) fn true_(input: Input<'_>) -> IResult<Input<'_>, bool, ParserError<'_>> {
+    (peek(TRUE[0]), cut(TRUE)).value(true).parse(input)
+}
+const TRUE: &[u8] = b"true";
+
+pub(crate) fn false_(input: Input<'_>) -> IResult<Input<'_>, bool, ParserError<'_>> {
+    (peek(FALSE[0]), cut(FALSE)).value(false).parse(input)
+}
+const FALSE: &[u8] = b"false";
 
 // ;; Integer
 
 // integer = dec-int / hex-int / oct-int / bin-int
-parse!(integer() -> i64, {
-    choice!(
-        attempt(hex_int()),
-        attempt(oct_int()),
-        attempt(bin_int()),
-        dec_int()
-            .and_then(|s| s.replace('_', "").parse())
-            .message("While parsing an Integer")
-    )
-});
+pub(crate) fn integer(input: Input<'_>) -> IResult<Input<'_>, i64, ParserError<'_>> {
+    dispatch! {peek(opt((any, any)));
+        Some((b'0', b'x')) => hex_int.map_res(|s| i64::from_str_radix(&s.replace('_', ""), 16)),
+        Some((b'0', b'o')) => oct_int.map_res(|s| i64::from_str_radix(&s.replace('_', ""), 8)),
+        Some((b'0', b'b')) => bin_int.map_res(|s| i64::from_str_radix(&s.replace('_', ""), 2)),
+        _ => dec_int.map_res(|s| s.replace('_', "").parse()),
+    }
+    .parse(input)
+}
 
 // dec-int = [ minus / plus ] unsigned-dec-int
 // unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
-parse!(dec_int() -> &'a str, {
-    recognize((
-        optional(choice([byte(b'-'), byte(b'+')])),
-        choice((
-            byte(b'0'),
+pub(crate) fn dec_int(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (
+        opt(one_of((b'+', b'-'))),
+        alt((
             (
-                satisfy(|c| (b'1'..=b'9').contains(&c)),
-                skip_many((
-                    optional(byte(b'_')),
-                    skip_many1(digit()),
-                )),
-            ).map(|t| t.0),
+                one_of(DIGIT1_9),
+                many0_count(alt((
+                    digit.value(()),
+                    (
+                        one_of(b'_'),
+                        cut(digit).context(Context::Expected(ParserValue::Description("digit"))),
+                    )
+                        .value(()),
+                ))),
+            )
+                .value(()),
+            digit.value(()),
         )),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII") }
-    })
-});
+    )
+        .recognize()
+        .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII") })
+        .context(Context::Expression("integer"))
+        .parse(input)
+}
+const DIGIT1_9: RangeInclusive<u8> = b'1'..=b'9';
 
 // hex-prefix = %x30.78               ; 0x
 // hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
-parse!(hex_int() -> i64, {
-    bytes(b"0x").with(
-        recognize((
-            hex_digit(),
-            skip_many((
-                optional(byte(b'_')),
-                skip_many1(hex_digit()),
-            )),
-        ).map(|t| t.0)
-    )).and_then(|b: &[u8]| {
-        let s = unsafe { from_utf8_unchecked(b, "`hex_digit` and `_` filter out non-ASCII") };
-        i64::from_str_radix(&s.replace('_', ""), 16)
-    }).message("While parsing a hexadecimal Integer")
-});
+pub(crate) fn hex_int(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    preceded(
+        HEX_PREFIX,
+        cut((
+            hexdig,
+            many0_count(alt((
+                hexdig.value(()),
+                (
+                    one_of(b'_'),
+                    cut(hexdig).context(Context::Expected(ParserValue::Description("digit"))),
+                )
+                    .value(()),
+            ))),
+        ))
+        .recognize(),
+    )
+    .map(|b| unsafe { from_utf8_unchecked(b, "`hexdig` and `_` filter out non-ASCII") })
+    .context(Context::Expression("hexadecimal integer"))
+    .parse(input)
+}
+const HEX_PREFIX: &[u8] = b"0x";
 
 // oct-prefix = %x30.6F               ; 0o
 // oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
-parse!(oct_int() -> i64, {
-    bytes(b"0o").with(
-        recognize((
-            oct_digit(),
-            skip_many((
-                optional(byte(b'_')),
-                skip_many1(oct_digit()),
-            )),
-        ).map(|t| t.0)
-    )).and_then(|b: &[u8]| {
-        let s = unsafe { from_utf8_unchecked(b, "`oct_digit` and `_` filter out non-ASCII") };
-        i64::from_str_radix(&s.replace('_', ""), 8)
-    }).message("While parsing a octal Integer")
-});
+pub(crate) fn oct_int(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    preceded(
+        OCT_PREFIX,
+        cut((
+            one_of(DIGIT0_7),
+            many0_count(alt((
+                one_of(DIGIT0_7).value(()),
+                (
+                    one_of(b'_'),
+                    cut(one_of(DIGIT0_7))
+                        .context(Context::Expected(ParserValue::Description("digit"))),
+                )
+                    .value(()),
+            ))),
+        ))
+        .recognize(),
+    )
+    .map(|b| unsafe { from_utf8_unchecked(b, "`DIGIT0_7` and `_` filter out non-ASCII") })
+    .context(Context::Expression("octal integer"))
+    .parse(input)
+}
+const OCT_PREFIX: &[u8] = b"0o";
+const DIGIT0_7: RangeInclusive<u8> = b'0'..=b'7';
 
 // bin-prefix = %x30.62               ; 0b
 // bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
-parse!(bin_int() -> i64, {
-    bytes(b"0b").with(
-        recognize((
-            satisfy(|c: u8| c == b'0' || c == b'1'),
-            skip_many((
-                optional(byte(b'_')),
-                skip_many1(satisfy(|c: u8| c == b'0' || c == b'1')),
-            )),
-        ).map(|t| t.0)
-    )).and_then(|b: &[u8]| {
-        let s = unsafe { from_utf8_unchecked(b, "`is_digit` and `_` filter out non-ASCII") };
-        i64::from_str_radix(&s.replace('_', ""), 2)
-    }).message("While parsing a binary Integer")
-});
+pub(crate) fn bin_int(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    preceded(
+        BIN_PREFIX,
+        cut((
+            one_of(DIGIT0_1),
+            many0_count(alt((
+                one_of(DIGIT0_1).value(()),
+                (
+                    one_of(b'_'),
+                    cut(one_of(DIGIT0_1))
+                        .context(Context::Expected(ParserValue::Description("digit"))),
+                )
+                    .value(()),
+            ))),
+        ))
+        .recognize(),
+    )
+    .map(|b| unsafe { from_utf8_unchecked(b, "`DIGIT0_1` and `_` filter out non-ASCII") })
+    .context(Context::Expression("binary integer"))
+    .parse(input)
+}
+const BIN_PREFIX: &[u8] = b"0b";
+const DIGIT0_1: RangeInclusive<u8> = b'0'..=b'1';
 
 // ;; Float
 
 // float = float-int-part ( exp / frac [ exp ] )
 // float =/ special-float
 // float-int-part = dec-int
-parse!(float() -> f64, {
-    choice((
-        parse_float()
-            .and_then(|s| s.replace('_', "").parse()),
-        special_float(),
-    )).message("While parsing a Float")
-});
+pub(crate) fn float(input: Input<'_>) -> IResult<Input<'_>, f64, ParserError<'_>> {
+    alt((
+        float_.map_res(|s| s.replace('_', "").parse()),
+        special_float,
+    ))
+    .context(Context::Expression("floating-point number"))
+    .parse(input)
+}
 
-parse!(parse_float() -> &'a str, {
-    recognize((
-        attempt((dec_int(), look_ahead(one_of([b'e', b'E', b'.'])))),
-        choice((
-            exp(),
-            (
-                frac(),
-                optional(exp()),
-            ).map(|_| "")
-        )),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`dec_int`, `one_of`, `exp`, and `frac` filter out non-ASCII") }
-    })
-});
+pub(crate) fn float_(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (dec_int, alt((exp, (frac, opt(exp)).map(|_| ""))))
+        .recognize()
+        .map(|b: &[u8]| unsafe {
+            from_utf8_unchecked(
+                b,
+                "`dec_int`, `one_of`, `exp`, and `frac` filter out non-ASCII",
+            )
+        })
+        .parse(input)
+}
 
 // frac = decimal-point zero-prefixable-int
 // decimal-point = %x2E               ; .
-parse!(frac() -> &'a str, {
-    recognize((
-        byte(b'.'),
-        parse_zero_prefixable_int(),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`.` and `parse_zero_prefixable_int` filter out non-ASCII") }
-    })
-});
+pub(crate) fn frac(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (
+        b'.',
+        cut(zero_prefixable_int).context(Context::Expected(ParserValue::Description("digit"))),
+    )
+        .recognize()
+        .map(|b: &[u8]| unsafe {
+            from_utf8_unchecked(
+                b,
+                "`.` and `parse_zero_prefixable_int` filter out non-ASCII",
+            )
+        })
+        .parse(input)
+}
 
 // zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
-parse!(parse_zero_prefixable_int() -> &'a str, {
-    recognize((
-        skip_many1(digit()),
-        skip_many((
-            optional(byte(b'_')),
-            skip_many1(digit()),
-        )),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII") }
-    })
-});
+pub(crate) fn zero_prefixable_int(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (
+        digit,
+        many0_count(alt((
+            digit.value(()),
+            (
+                one_of(b'_'),
+                cut(digit).context(Context::Expected(ParserValue::Description("digit"))),
+            )
+                .value(()),
+        ))),
+    )
+        .recognize()
+        .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`digit` and `_` filter out non-ASCII") })
+        .parse(input)
+}
 
 // exp = "e" float-exp-part
 // float-exp-part = [ minus / plus ] zero-prefixable-int
-parse!(exp() -> &'a str, {
-    recognize((
-        one_of([b'e', b'E']),
-        optional(one_of([b'+', b'-'])),
-        parse_zero_prefixable_int(),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`one_of` and `parse_zero_prefixable_int` filter out non-ASCII") }
-    })
-});
+pub(crate) fn exp(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (
+        one_of((b'e', b'E')),
+        opt(one_of([b'+', b'-'])),
+        cut(zero_prefixable_int),
+    )
+        .recognize()
+        .map(|b: &[u8]| unsafe {
+            from_utf8_unchecked(
+                b,
+                "`one_of` and `parse_zero_prefixable_int` filter out non-ASCII",
+            )
+        })
+        .parse(input)
+}
 
 // special-float = [ minus / plus ] ( inf / nan )
-parse!(special_float() -> f64, {
-    attempt(optional(one_of([b'+', b'-'])).and(choice((inf(), nan()))).map(|(s, f)| {
-        match s {
+pub(crate) fn special_float(input: Input<'_>) -> IResult<Input<'_>, f64, ParserError<'_>> {
+    (opt(one_of((b'+', b'-'))), alt((inf, nan)))
+        .map(|(s, f)| match s {
             Some(b'+') | None => f,
             Some(b'-') => -f,
             _ => unreachable!("one_of should prevent this"),
-        }
-    }))
-});
-
+        })
+        .parse(input)
+}
 // inf = %x69.6e.66  ; inf
-pub(crate) const INF: &[u8] = b"inf";
-parse!(inf() -> f64, {
-    bytes(INF).map(|_| f64::INFINITY)
-});
-
+pub(crate) fn inf(input: Input<'_>) -> IResult<Input<'_>, f64, ParserError<'_>> {
+    tag(INF).value(f64::INFINITY).parse(input)
+}
+const INF: &[u8] = b"inf";
 // nan = %x6e.61.6e  ; nan
-pub(crate) const NAN: &[u8] = b"nan";
-parse!(nan() -> f64, {
-    bytes(NAN).map(|_| f64::NAN)
-});
+pub(crate) fn nan(input: Input<'_>) -> IResult<Input<'_>, f64, ParserError<'_>> {
+    tag(NAN).value(f64::NAN).parse(input)
+}
+const NAN: &[u8] = b"nan";
+
+// DIGIT = %x30-39 ; 0-9
+pub(crate) fn digit(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    one_of(DIGIT).parse(input)
+}
+const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
+
+// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+pub(crate) fn hexdig(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    one_of(HEXDIG).parse(input)
+}
+pub(crate) const HEXDIG: (RangeInclusive<u8>, RangeInclusive<u8>, RangeInclusive<u8>) =
+    (DIGIT, b'A'..=b'F', b'a'..=b'f');
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use crate::parser::*;
-    use combine::stream::position::Stream;
-
     #[test]
     fn integers() {
         let cases = [
@@ -219,15 +282,29 @@ mod test {
             (&std::i64::MAX.to_string()[..], std::i64::MAX),
         ];
         for &(input, expected) in &cases {
-            let parsed = numbers::integer().easy_parse(Stream::new(input.as_bytes()));
-            parsed_eq!(parsed, expected);
+            let parsed = integer.parse(input.as_bytes()).finish();
+            assert_eq!(parsed, Ok(expected), "Parsing {input:?}");
         }
 
         let overflow = "1000000000000000000000000000000000";
-        let parsed = numbers::integer().easy_parse(Stream::new(overflow.as_bytes()));
+        let parsed = integer.parse(overflow.as_bytes()).finish();
         assert!(parsed.is_err());
     }
 
+    #[track_caller]
+    fn assert_float_eq(actual: f64, expected: f64) {
+        if expected.is_nan() {
+            assert!(actual.is_nan());
+        } else if expected.is_infinite() {
+            assert!(actual.is_infinite());
+            assert_eq!(expected.is_sign_positive(), actual.is_sign_positive());
+        } else {
+            dbg!(expected);
+            dbg!(actual);
+            assert!((expected - actual).abs() < std::f64::EPSILON);
+        }
+    }
+
     #[test]
     fn floats() {
         let cases = [
@@ -250,7 +327,8 @@ mod test {
             // ("1e+400", std::f64::INFINITY),
         ];
         for &(input, expected) in &cases {
-            parsed_float_eq!(input, expected);
+            let parsed = float.parse(input.as_bytes()).finish().unwrap();
+            assert_float_eq(parsed, expected);
         }
     }
 }
diff --git a/crates/toml_edit/src/parser/strings.rs b/crates/toml_edit/src/parser/strings.rs
index 3b078976..8356e9e0 100644
--- a/crates/toml_edit/src/parser/strings.rs
+++ b/crates/toml_edit/src/parser/strings.rs
@@ -1,69 +1,99 @@
 use std::borrow::Cow;
 use std::char;
-
-use combine::error::Commit;
-use combine::parser::byte::{byte, bytes, hex_digit};
-use combine::parser::range::{range, recognize, take_while, take_while1};
-use combine::stream::RangeStream;
-use combine::*;
+use std::ops::RangeInclusive;
+
+use nom8::branch::alt;
+use nom8::bytes::any;
+use nom8::bytes::none_of;
+use nom8::bytes::one_of;
+use nom8::bytes::tag;
+use nom8::bytes::take_while;
+use nom8::bytes::take_while1;
+use nom8::bytes::take_while_m_n;
+use nom8::combinator::cut;
+use nom8::combinator::fail;
+use nom8::combinator::opt;
+use nom8::combinator::peek;
+use nom8::combinator::success;
+use nom8::multi::many0_count;
+use nom8::multi::many1_count;
+use nom8::sequence::delimited;
+use nom8::sequence::preceded;
+use nom8::sequence::terminated;
 
 use crate::parser::errors::CustomError;
-use crate::parser::trivia::{
-    from_utf8_unchecked, is_non_ascii, is_wschar, newline, ws, ws_newlines,
-};
+use crate::parser::numbers::HEXDIG;
+use crate::parser::prelude::*;
+use crate::parser::trivia::{from_utf8_unchecked, newline, ws, ws_newlines, NON_ASCII, WSCHAR};
 
 // ;; String
 
 // string = ml-basic-string / basic-string / ml-literal-string / literal-string
-parse!(string() -> String, {
-    choice((
-        ml_basic_string(),
-        basic_string(),
-        ml_literal_string(),
-        literal_string().map(|s: &'a str| s.into()),
+pub(crate) fn string(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    alt((
+        ml_basic_string,
+        basic_string,
+        ml_literal_string,
+        literal_string.map(Cow::Borrowed),
     ))
-});
+    .parse(input)
+}
 
 // ;; Basic String
 
 // basic-string = quotation-mark *basic-char quotation-mark
-parse!(basic_string() -> String, {
-    between(
-        byte(QUOTATION_MARK), byte(QUOTATION_MARK),
-        many(basic_chars())
-    )
-    .message("While parsing a Basic String")
-});
+pub(crate) fn basic_string(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    let (mut input, _) = one_of(QUOTATION_MARK).parse(input)?;
+
+    let mut c = Cow::Borrowed("");
+    if let Some((i, ci)) = ok_error(basic_chars.parse(input))? {
+        input = i;
+        c = ci;
+    }
+    while let Some((i, ci)) = ok_error(basic_chars.parse(input))? {
+        input = i;
+        c.to_mut().push_str(&ci);
+    }
+
+    let (input, _) = cut(one_of(QUOTATION_MARK))
+        .context(Context::Expression("basic string"))
+        .parse(input)?;
+
+    Ok((input, c))
+}
 
 // quotation-mark = %x22            ; "
 pub(crate) const QUOTATION_MARK: u8 = b'"';
 
 // basic-char = basic-unescaped / escaped
-parse!(basic_chars() -> Cow<'a, str>, {
-    choice((
+fn basic_chars(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    alt((
         // Deviate from the official grammar by batching the unescaped chars so we build a string a
         // chunk at a time, rather than a `char` at a time.
-        take_while1(is_basic_unescaped).and_then(std::str::from_utf8).map(Cow::Borrowed),
-        escaped().map(|c| Cow::Owned(String::from(c))),
+        take_while1(BASIC_UNESCAPED)
+            .map_res(std::str::from_utf8)
+            .map(Cow::Borrowed),
+        escaped.map(|c| Cow::Owned(String::from(c))),
     ))
-});
+    .parse(input)
+}
 
 // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
-#[inline]
-fn is_basic_unescaped(c: u8) -> bool {
-    is_wschar(c) | matches!(c, 0x21 | 0x23..=0x5B | 0x5D..=0x7E) | is_non_ascii(c)
-}
+pub(crate) const BASIC_UNESCAPED: (
+    (u8, u8),
+    u8,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+) = (WSCHAR, 0x21, 0x23..=0x5B, 0x5D..=0x7E, NON_ASCII);
 
 // escaped = escape escape-seq-char
-parse!(escaped() -> char, {
-    satisfy(|c| c == ESCAPE)
-        .then(|_| parser(move |input| {
-            escape_seq_char().parse_stream(input).into_result()
-        }))
-});
+fn escaped(input: Input<'_>) -> IResult<Input<'_>, char, ParserError<'_>> {
+    preceded(ESCAPE, escape_seq_char).parse(input)
+}
 
 // escape = %x5C                    ; \
-const ESCAPE: u8 = b'\\';
+pub(crate) const ESCAPE: u8 = b'\\';
 
 // escape-seq-char =  %x22         ; "    quotation mark  U+0022
 // escape-seq-char =/ %x5C         ; \    reverse solidus U+005C
@@ -74,243 +104,253 @@ const ESCAPE: u8 = b'\\';
 // escape-seq-char =/ %x74         ; t    tab             U+0009
 // escape-seq-char =/ %x75 4HEXDIG ; uXXXX                U+XXXX
 // escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX            U+XXXXXXXX
-parse!(escape_seq_char() -> char, {
-    satisfy(is_escape_seq_char)
-        .message("While parsing escape sequence")
-        .then(|c| {
-            parser(move |input| {
-                match c {
-                    b'b'  => Ok(('\u{8}', Commit::Peek(()))),
-                    b'f'  => Ok(('\u{c}', Commit::Peek(()))),
-                    b'n'  => Ok(('\n',    Commit::Peek(()))),
-                    b'r'  => Ok(('\r',    Commit::Peek(()))),
-                    b't'  => Ok(('\t',    Commit::Peek(()))),
-                    b'u'  => hexescape(4).parse_stream(input).into_result(),
-                    b'U'  => hexescape(8).parse_stream(input).into_result(),
-                    b'\\' => Ok(('\\',    Commit::Peek(()))),
-                    b'"'  => Ok(('"',     Commit::Peek(()))),
-                    _ => unreachable!("{:?} filtered out by is_escape_seq_char", c),
-                }
-            })
-        })
-});
-
-#[inline]
-fn is_escape_seq_char(c: u8) -> bool {
-    matches!(
-        c,
-        b'"' | b'\\' | b'b' | b'f' | b'n' | b'r' | b't' | b'u' | b'U'
-    )
+fn escape_seq_char(input: Input<'_>) -> IResult<Input<'_>, char, ParserError<'_>> {
+    dispatch! {any;
+        b'b' => success('\u{8}'),
+        b'f' => success('\u{c}'),
+        b'n' => success('\n'),
+        b'r' => success('\r'),
+        b't' => success('\t'),
+        b'u' => cut(hexescape::<4>).context(Context::Expression("unicode 4-digit hex code")),
+        b'U' => cut(hexescape::<8>).context(Context::Expression("unicode 8-digit hex code")),
+        b'\\' => success('\\'),
+        b'"' => success('"'),
+        _ => {
+            cut(fail::<_, char, _>)
+            .context(Context::Expression("escape sequence"))
+            .context(Context::Expected(ParserValue::CharLiteral('b')))
+            .context(Context::Expected(ParserValue::CharLiteral('f')))
+            .context(Context::Expected(ParserValue::CharLiteral('n')))
+            .context(Context::Expected(ParserValue::CharLiteral('r')))
+            .context(Context::Expected(ParserValue::CharLiteral('t')))
+            .context(Context::Expected(ParserValue::CharLiteral('u')))
+            .context(Context::Expected(ParserValue::CharLiteral('U')))
+            .context(Context::Expected(ParserValue::CharLiteral('\\')))
+            .context(Context::Expected(ParserValue::CharLiteral('"')))
+        }
+    }
+    .parse(input)
 }
 
-parse!(hexescape(n: usize) -> char, {
-    recognize(skip_count_min_max(
-        *n, *n,
-        hex_digit(),
-    ))
+pub(crate) fn hexescape<const N: usize>(
+    input: Input<'_>,
+) -> IResult<Input<'_>, char, ParserError<'_>> {
+    take_while_m_n(0, N, HEXDIG)
+        .verify(|b: &[u8]| b.len() == N)
         .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
-        .and_then(|s| u32::from_str_radix(s, 16))
-        .and_then(|h| char::from_u32(h).ok_or(CustomError::InvalidHexEscape(h)))
-});
+        .map_opt(|s| u32::from_str_radix(s, 16).ok())
+        .map_res(|h| char::from_u32(h).ok_or(CustomError::OutOfRange))
+        .parse(input)
+}
 
 // ;; Multiline Basic String
 
 // ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
 //                   ml-basic-string-delim
-parse!(ml_basic_string() -> String, {
-    (
-        range(ML_BASIC_STRING_DELIM),
-        (
-            optional(newline()),
-            ml_basic_body(),
-        ).map(|t| t.1),
-        // Deviate from grammar by pulling mll_quotes into here so we can handle the confusion with
-        // it and the closing delim
-        choice((
-            attempt((
-                bytes(b"\"\""), range(ML_BASIC_STRING_DELIM)
-            )).map(|_| Some("\"\"")),
-            attempt((
-                bytes(b"\""), range(ML_BASIC_STRING_DELIM)
-            )).map(|_| Some("\"")),
-            range(ML_BASIC_STRING_DELIM).map(|_| None),
-        )),
-    ).map(|(_, mut b, q)| {
-        if let Some(q) = q {
-            b.push_str(q);
-        }
-        b
-    }).message("While parsing a Multiline Basic String")
-});
+fn ml_basic_string(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    delimited(
+        ML_BASIC_STRING_DELIM,
+        preceded(opt(newline), cut(ml_basic_body)),
+        cut(ML_BASIC_STRING_DELIM),
+    )
+    .context(Context::Expression("multiline basic string"))
+    .parse(input)
+}
 
 // ml-basic-string-delim = 3quotation-mark
-const ML_BASIC_STRING_DELIM: &[u8] = b"\"\"\"";
+pub(crate) const ML_BASIC_STRING_DELIM: &[u8] = b"\"\"\"";
 
 // ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
-parse!(ml_basic_body() -> String, {
-    (
-        many(mlb_content()),
-        many(attempt((
-            mlb_quotes(),
-            many1(mlb_content()),
-        ).map(|(q, c): (&str, String)| {
-            let mut total = q.to_owned();
-            total.push_str(&c);
-            total
-        }))),
-        // Deviate: see `ml_basic_string`
-        //optional(mll_quotes()),
-    ).map(|(mut c, qc): (String, String)| {
-        c.push_str(&qc);
-        c
-    })
-});
+fn ml_basic_body(mut input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    let mut c = Cow::Borrowed("");
+    if let Some((i, ci)) = ok_error(mlb_content.parse(input))? {
+        input = i;
+        c = ci;
+    }
+    while let Some((i, ci)) = ok_error(mlb_content.parse(input))? {
+        input = i;
+        c.to_mut().push_str(&ci);
+    }
+
+    while let Some((i, qi)) = ok_error(mlb_quotes(none_of(b'\"').value(())).parse(input))? {
+        if let Some((i, ci)) = ok_error(mlb_content.parse(i))? {
+            input = i;
+            c.to_mut().push_str(qi);
+            c.to_mut().push_str(&ci);
+            while let Some((i, ci)) = ok_error(mlb_content.parse(input))? {
+                input = i;
+                c.to_mut().push_str(&ci);
+            }
+        } else {
+            break;
+        }
+    }
+
+    if let Some((i, qi)) = ok_error(mlb_quotes(tag(ML_BASIC_STRING_DELIM).value(())).parse(input))?
+    {
+        input = i;
+        c.to_mut().push_str(qi);
+    }
+
+    Ok((input, c))
+}
 
 // mlb-content = mlb-char / newline / mlb-escaped-nl
 // mlb-char = mlb-unescaped / escaped
-parse!(mlb_content() -> Cow<'a, str>, {
-    choice((
+fn mlb_content(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    alt((
         // Deviate from the official grammar by batching the unescaped chars so we build a string a
         // chunk at a time, rather than a `char` at a time.
-        take_while1(is_mlb_unescaped).and_then(std::str::from_utf8).map(Cow::Borrowed),
-        attempt(escaped().map(|c| Cow::Owned(String::from(c)))),
-        newline().map(|_| Cow::Borrowed("\n")),
-        mlb_escaped_nl().map(|_| Cow::Borrowed("")),
+        take_while1(MLB_UNESCAPED)
+            .map_res(std::str::from_utf8)
+            .map(Cow::Borrowed),
+        // Order changed fromg grammar so `escaped` can more easily `cut` on bad escape sequences
+        mlb_escaped_nl.map(|_| Cow::Borrowed("")),
+        escaped.map(|c| Cow::Owned(String::from(c))),
+        newline.map(|_| Cow::Borrowed("\n")),
     ))
-});
+    .parse(input)
+}
 
 // mlb-quotes = 1*2quotation-mark
-parse!(mlb_quotes() -> &'a str, {
-    choice((
-        attempt(bytes(b"\"\"")),
-        attempt(bytes(b"\"")),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`bytes` out npn-ASCII") }
-    })
-});
+fn mlb_quotes<'i>(
+    mut term: impl nom8::Parser<Input<'i>, (), ParserError<'i>>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, &str, ParserError<'i>> {
+    move |input| {
+        let res = terminated(b"\"\"", peek(term.by_ref()))
+            .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+            .parse(input);
+
+        match res {
+            Err(nom8::Err::Error(_)) => terminated(b"\"", peek(term.by_ref()))
+                .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+                .parse(input),
+            res => res,
+        }
+    }
+}
 
 // mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
-#[inline]
-fn is_mlb_unescaped(c: u8) -> bool {
-    is_wschar(c) | matches!(c, 0x21 | 0x23..=0x5B | 0x5D..=0x7E) | is_non_ascii(c)
-}
+pub(crate) const MLB_UNESCAPED: (
+    (u8, u8),
+    u8,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+) = (WSCHAR, 0x21, 0x23..=0x5B, 0x5D..=0x7E, NON_ASCII);
 
 // mlb-escaped-nl = escape ws newline *( wschar / newline
 // When the last non-whitespace character on a line is a \,
 // it will be trimmed along with all whitespace
 // (including newlines) up to the next non-whitespace
 // character or closing delimiter.
-parse!(mlb_escaped_nl() -> (), {
-    skip_many1(attempt((
-        byte(ESCAPE),
-        ws(),
-        ws_newlines(),
-    )))
-});
+fn mlb_escaped_nl(input: Input<'_>) -> IResult<Input<'_>, (), ParserError<'_>> {
+    many1_count((ESCAPE, ws, ws_newlines))
+        .value(())
+        .parse(input)
+}
 
 // ;; Literal String
 
 // literal-string = apostrophe *literal-char apostrophe
-parse!(literal_string() -> &'a str, {
-    between(
-        byte(APOSTROPHE), byte(APOSTROPHE),
-        take_while(is_literal_char)
-    ).and_then(std::str::from_utf8)
-        .message("While parsing a Literal String")
-});
+pub(crate) fn literal_string(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    delimited(APOSTROPHE, cut(take_while(LITERAL_CHAR)), cut(APOSTROPHE))
+        .map_res(std::str::from_utf8)
+        .context(Context::Expression("literal string"))
+        .parse(input)
+}
 
 // apostrophe = %x27 ; ' apostrophe
 pub(crate) const APOSTROPHE: u8 = b'\'';
 
 // literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
-#[inline]
-fn is_literal_char(c: u8) -> bool {
-    matches!(c, 0x09 | 0x20..=0x26 | 0x28..=0x7E) | is_non_ascii(c)
-}
+pub(crate) const LITERAL_CHAR: (
+    u8,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+) = (0x9, 0x20..=0x26, 0x28..=0x7E, NON_ASCII);
 
 // ;; Multiline Literal String
 
 // ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
 //                     ml-literal-string-delim
-parse!(ml_literal_string() -> String, {
-    (
-        range(ML_LITERAL_STRING_DELIM),
-        (
-            optional(newline()),
-            ml_literal_body(),
-        ).map(|t| t.1.replace("\r\n", "\n")),
-        // Deviate from grammar by pulling mll_quotes into here so we can handle the confusion with
-        // it and the closing delim
-        choice((
-            attempt((
-                bytes(b"''"), range(ML_LITERAL_STRING_DELIM)
-            )).map(|_| Some("''")),
-            attempt((
-                bytes(b"'"), range(ML_LITERAL_STRING_DELIM)
-            )).map(|_| Some("'")),
-            range(ML_LITERAL_STRING_DELIM).map(|_| None),
-        ))
-    ).map(|(_, mut b, q)| {
-        if let Some(q) = q {
-            b.push_str(q);
-        }
-        b
-    }).message("While parsing a Multiline Literal String")
-});
+fn ml_literal_string(input: Input<'_>) -> IResult<Input<'_>, Cow<'_, str>, ParserError<'_>> {
+    delimited(
+        (ML_LITERAL_STRING_DELIM, opt(newline)),
+        cut(ml_literal_body.map(|t| {
+            if t.contains("\r\n") {
+                Cow::Owned(t.replace("\r\n", "\n"))
+            } else {
+                Cow::Borrowed(t)
+            }
+        })),
+        cut(ML_LITERAL_STRING_DELIM),
+    )
+    .context(Context::Expression("multiline literal string"))
+    .parse(input)
+}
 
 // ml-literal-string-delim = 3apostrophe
-const ML_LITERAL_STRING_DELIM: &[u8] = b"'''";
+pub(crate) const ML_LITERAL_STRING_DELIM: &[u8] = b"'''";
 
 // ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
-parse!(ml_literal_body() -> &'a str, {
-    recognize((
-        skip_many(mll_content()),
-        skip_many(attempt((mll_quotes(), skip_many1(mll_content())))),
-        // Deviate: see ml_literal_string
-        //optional(mll_quotes()),
-    )).and_then(std::str::from_utf8)
-});
+fn ml_literal_body(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (
+        many0_count(mll_content),
+        many0_count((
+            mll_quotes(none_of(APOSTROPHE).value(())),
+            many1_count(mll_content),
+        )),
+        opt(mll_quotes(tag(ML_LITERAL_STRING_DELIM).value(()))),
+    )
+        .recognize()
+        .map_res(std::str::from_utf8)
+        .parse(input)
+}
 
 // mll-content = mll-char / newline
-parse!(mll_content() -> u8, {
-    choice((
-        satisfy(is_mll_char),
-        newline().map(|_| b'\n')
-    ))
-});
+fn mll_content(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    alt((one_of(MLL_CHAR), newline)).parse(input)
+}
 
 // mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
-#[inline]
-fn is_mll_char(c: u8) -> bool {
-    matches!(c, 0x09 | 0x20..=0x26 | 0x28..=0x7E) | is_non_ascii(c)
-}
+const MLL_CHAR: (
+    u8,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+    RangeInclusive<u8>,
+) = (0x9, 0x20..=0x26, 0x28..=0x7E, NON_ASCII);
 
 // mll-quotes = 1*2apostrophe
-parse!(mll_quotes() -> &'a str, {
-    choice((
-        attempt(bytes(b"''")),
-        attempt(bytes(b"'")),
-    )).map(|b: &[u8]| {
-        unsafe { from_utf8_unchecked(b, "`bytes` out npn-ASCII") }
-    })
-});
+fn mll_quotes<'i>(
+    mut term: impl nom8::Parser<Input<'i>, (), ParserError<'i>>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, &str, ParserError<'i>> {
+    move |input| {
+        let res = terminated(b"''", peek(term.by_ref()))
+            .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+            .parse(input);
+
+        match res {
+            Err(nom8::Err::Error(_)) => terminated(b"'", peek(term.by_ref()))
+                .map(|b| unsafe { from_utf8_unchecked(b, "`bytes` out non-ASCII") })
+                .parse(input),
+            res => res,
+        }
+    }
+}
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use crate::parser::*;
-    use combine::stream::position::Stream;
-
     #[test]
     fn basic_string() {
         let input =
             r#""I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF. \U0002070E""#;
-        let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-        parsed_eq!(
-            parsed,
-            "I\'m a string. \"You can quote me\". Name\tJosé\nLocation\tSF. \u{2070E}"
-        );
+        let expected = "I\'m a string. \"You can quote me\". Name\tJosé\nLocation\tSF. \u{2070E}";
+        let parsed = string.parse(input.as_bytes()).finish();
+        assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
     }
 
     #[test]
@@ -328,15 +368,14 @@ Violets are blue"#,
         ];
 
         for &(input, expected) in &cases {
-            dbg!(input);
-            let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-            parsed_eq!(parsed, expected);
+            let parsed = string.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
         }
 
         let invalid_cases = [r#""""  """#, r#""""  \""""#];
 
         for input in &invalid_cases {
-            let parsed = strings::ml_basic_string().easy_parse(Stream::new(input.as_bytes()));
+            let parsed = string.parse(input.as_bytes()).finish();
             assert!(parsed.is_err());
         }
     }
@@ -357,9 +396,9 @@ The quick brown \
        """"#,
         ];
         for input in &inputs {
-            dbg!(input);
-            let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-            parsed_eq!(parsed, "The quick brown fox jumps over the lazy dog.");
+            let expected = "The quick brown fox jumps over the lazy dog.";
+            let parsed = string.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
         }
         let empties = [
             r#""""\
@@ -369,9 +408,10 @@ The quick brown \
   \
 """"#,
         ];
-        for empty in &empties {
-            let parsed = strings::string().easy_parse(Stream::new(empty.as_bytes()));
-            parsed_eq!(parsed, "");
+        for input in &empties {
+            let expected = "";
+            let parsed = string.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
         }
     }
 
@@ -385,23 +425,32 @@ The quick brown \
         ];
 
         for input in &inputs {
-            let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-            parsed_eq!(parsed, &input[1..input.len() - 1]);
+            let expected = &input[1..input.len() - 1];
+            let parsed = string.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
         }
     }
 
     #[test]
     fn ml_literal_string() {
-        let input = r#"'''I [dw]on't need \d{2} apples'''"#;
-        let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-        parsed_eq!(parsed, &input[3..input.len() - 3]);
+        let inputs = [
+            r#"'''I [dw]on't need \d{2} apples'''"#,
+            r#"''''one_quote''''"#,
+        ];
+        for input in &inputs {
+            let expected = &input[3..input.len() - 3];
+            let parsed = string.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
+        }
+
         let input = r#"'''
 The first newline is
 trimmed in raw strings.
    All other whitespace
    is preserved.
 '''"#;
-        let parsed = strings::string().easy_parse(Stream::new(input.as_bytes()));
-        parsed_eq!(parsed, &input[4..input.len() - 3]);
+        let expected = &input[4..input.len() - 3];
+        let parsed = string.parse(input.as_bytes()).finish();
+        assert_eq!(parsed.as_deref(), Ok(expected), "Parsing {input:?}");
     }
 }
diff --git a/crates/toml_edit/src/parser/table.rs b/crates/toml_edit/src/parser/table.rs
index d4807453..89294704 100644
--- a/crates/toml_edit/src/parser/table.rs
+++ b/crates/toml_edit/src/parser/table.rs
@@ -1,13 +1,17 @@
 use std::cell::RefCell;
+#[allow(unused_imports)]
+use std::ops::DerefMut;
 
-use combine::parser::byte::byte;
-use combine::parser::range::range;
-use combine::stream::RangeStream;
-use combine::*;
+use nom8::bytes::take;
+use nom8::combinator::cut;
+use nom8::combinator::peek;
+use nom8::sequence::delimited;
 
+// https://github.com/rust-lang/rust/issues/41358
 use crate::parser::key::key;
+use crate::parser::prelude::*;
+use crate::parser::state::ParseState;
 use crate::parser::trivia::line_trailing;
-use crate::parser::ParseState;
 
 // std-table-open  = %x5B ws     ; [ Left square bracket
 pub(crate) const STD_TABLE_OPEN: u8 = b'[';
@@ -21,43 +25,63 @@ const ARRAY_TABLE_CLOSE: &[u8] = b"]]";
 // ;; Standard Table
 
 // std-table = std-table-open key *( table-key-sep key) std-table-close
-toml_parser!(std_table, parser, {
-    (
-        between(byte(STD_TABLE_OPEN), byte(STD_TABLE_CLOSE), key()),
-        line_trailing().and_then(std::str::from_utf8),
-    )
-        .and_then(|(h, t)| parser.borrow_mut().on_std_header(h, t))
-});
+pub(crate) fn std_table<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| {
+        (
+            delimited(
+                STD_TABLE_OPEN,
+                cut(key),
+                cut(STD_TABLE_CLOSE)
+                    .context(Context::Expected(ParserValue::CharLiteral('.')))
+                    .context(Context::Expected(ParserValue::StringLiteral("]"))),
+            ),
+            cut(line_trailing.map_res(std::str::from_utf8))
+                .context(Context::Expected(ParserValue::CharLiteral('\n')))
+                .context(Context::Expected(ParserValue::CharLiteral('#'))),
+        )
+            .map_res(|(h, t)| state.borrow_mut().deref_mut().on_std_header(h, t))
+            .parse(i)
+    }
+}
 
 // ;; Array Table
 
 // array-table = array-table-open key *( table-key-sep key) array-table-close
-toml_parser!(array_table, parser, {
-    (
-        between(range(ARRAY_TABLE_OPEN), range(ARRAY_TABLE_CLOSE), key()),
-        line_trailing().and_then(std::str::from_utf8),
-    )
-        .and_then(|(h, t)| parser.borrow_mut().on_array_header(h, t))
-});
+pub(crate) fn array_table<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| {
+        (
+            delimited(
+                ARRAY_TABLE_OPEN,
+                cut(key),
+                cut(ARRAY_TABLE_CLOSE)
+                    .context(Context::Expected(ParserValue::CharLiteral('.')))
+                    .context(Context::Expected(ParserValue::StringLiteral("]]"))),
+            ),
+            cut(line_trailing.map_res(std::str::from_utf8))
+                .context(Context::Expected(ParserValue::CharLiteral('\n')))
+                .context(Context::Expected(ParserValue::CharLiteral('#'))),
+        )
+            .map_res(|(h, t)| state.borrow_mut().deref_mut().on_array_header(h, t))
+            .parse(i)
+    }
+}
 
 // ;; Table
 
 // table = std-table / array-table
-parser! {
-    pub(crate) fn table['a, 'b, I](parser: &'b RefCell<ParseState>)(I) -> ()
-    where
-        [I: RangeStream<
-         Range = &'a [u8],
-         Token = u8>,
-         I::Error: ParseError<u8, &'a [u8], <I as StreamOnce>::Position>,
-         <I::Error as ParseError<u8, &'a [u8], <I as StreamOnce>::Position>>::StreamError:
-         From<std::num::ParseIntError> +
-         From<std::num::ParseFloatError> +
-         From<std::str::Utf8Error> +
-         From<crate::parser::errors::CustomError>
-    ]    {
-        array_table(parser)
-            .or(std_table(parser))
-            .message("While parsing a Table Header")
+pub(crate) fn table<'s, 'i>(
+    state: &'s RefCell<ParseState>,
+) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
+    move |i| {
+        dispatch!(peek::<_, &[u8],_,_>(take(2usize));
+            b"[[" => array_table(state),
+            _ => std_table(state),
+        )
+        .context(Context::Expression("table header"))
+        .parse(i)
     }
 }
diff --git a/crates/toml_edit/src/parser/trivia.rs b/crates/toml_edit/src/parser/trivia.rs
index 93b001db..aacb8b7a 100644
--- a/crates/toml_edit/src/parser/trivia.rs
+++ b/crates/toml_edit/src/parser/trivia.rs
@@ -1,7 +1,17 @@
-use combine::parser::byte::{byte, crlf, newline as lf};
-use combine::parser::range::{recognize, take_while, take_while1};
-use combine::stream::RangeStream;
-use combine::*;
+use std::ops::RangeInclusive;
+
+use nom8::branch::alt;
+use nom8::bytes::one_of;
+use nom8::bytes::take_while;
+use nom8::bytes::take_while1;
+use nom8::combinator::eof;
+use nom8::combinator::opt;
+use nom8::multi::many0_count;
+use nom8::multi::many1_count;
+use nom8::prelude::*;
+use nom8::sequence::terminated;
+
+use crate::parser::prelude::*;
 
 pub(crate) unsafe fn from_utf8_unchecked<'b>(
     bytes: &'b [u8],
@@ -17,120 +27,94 @@ pub(crate) unsafe fn from_utf8_unchecked<'b>(
 
 // wschar = ( %x20 /              ; Space
 //            %x09 )              ; Horizontal tab
-#[inline]
-pub(crate) fn is_wschar(c: u8) -> bool {
-    matches!(c, b' ' | b'\t')
-}
+pub(crate) const WSCHAR: (u8, u8) = (b' ', b'\t');
 
 // ws = *wschar
-parse!(ws() -> &'a str, {
-    take_while(is_wschar).map(|b| {
-        unsafe { from_utf8_unchecked(b, "`is_wschar` filters out on-ASCII") }
-    })
-});
+pub(crate) fn ws(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    take_while(WSCHAR)
+        .map(|b| unsafe { from_utf8_unchecked(b, "`is_wschar` filters out on-ASCII") })
+        .parse(input)
+}
 
 // non-ascii = %x80-D7FF / %xE000-10FFFF
-#[inline]
-pub(crate) fn is_non_ascii(c: u8) -> bool {
-    // - ASCII is 0xxxxxxx
-    // - First byte for UTF-8 is 11xxxxxx
-    // - Subsequent UTF-8 bytes are 10xxxxxx
-    matches!(c, 0x80..=0xff)
-}
+// - ASCII is 0xxxxxxx
+// - First byte for UTF-8 is 11xxxxxx
+// - Subsequent UTF-8 bytes are 10xxxxxx
+pub(crate) const NON_ASCII: RangeInclusive<u8> = 0x80..=0xff;
 
 // non-eol = %x09 / %x20-7E / non-ascii
-#[inline]
-fn is_non_eol(c: u8) -> bool {
-    matches!(c, 0x09 | 0x20..=0x7E) | is_non_ascii(c)
-}
+pub(crate) const NON_EOL: (u8, RangeInclusive<u8>, RangeInclusive<u8>) =
+    (0x09, 0x20..=0x7E, NON_ASCII);
 
 // comment-start-symbol = %x23 ; #
 pub(crate) const COMMENT_START_SYMBOL: u8 = b'#';
 
 // comment = comment-start-symbol *non-eol
-parse!(comment() -> &'a [u8], {
-    recognize((
-        byte(COMMENT_START_SYMBOL),
-        take_while(is_non_eol),
-    ))
-});
+pub(crate) fn comment(input: Input<'_>) -> IResult<Input<'_>, &[u8], ParserError<'_>> {
+    (COMMENT_START_SYMBOL, take_while(NON_EOL))
+        .recognize()
+        .parse(input)
+}
 
 // newline = ( %x0A /              ; LF
 //             %x0D.0A )           ; CRLF
+pub(crate) fn newline(input: Input<'_>) -> IResult<Input<'_>, u8, ParserError<'_>> {
+    alt((
+        one_of(LF).value(b'\n'),
+        (one_of(CR), one_of(LF)).value(b'\n'),
+    ))
+    .parse(input)
+}
 pub(crate) const LF: u8 = b'\n';
 pub(crate) const CR: u8 = b'\r';
-parse!(newline() -> char, {
-    choice((lf(), crlf()))
-        .map(|_| '\n')
-        .expected("newline")
-});
 
 // ws-newline       = *( wschar / newline )
-parse!(ws_newline() -> &'a str, {
-    recognize(
-        skip_many(choice((
-            newline().map(|_| &b"\n"[..]),
-            take_while1(is_wschar),
-        ))),
-    ).map(|b| {
-        unsafe { from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII") }
-    })
-});
+pub(crate) fn ws_newline(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    many0_count(alt((newline.value(&b"\n"[..]), take_while1(WSCHAR))))
+        .recognize()
+        .map(|b| unsafe {
+            from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII")
+        })
+        .parse(input)
+}
 
 // ws-newlines      = newline *( wschar / newline )
-parse!(ws_newlines() -> &'a str, {
-    recognize((
-        newline(),
-        ws_newline(),
-    )).map(|b| {
-        unsafe { from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII") }
-    })
-});
+pub(crate) fn ws_newlines(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    (newline, ws_newline)
+        .recognize()
+        .map(|b| unsafe {
+            from_utf8_unchecked(b, "`is_wschar` and `newline` filters out on-ASCII")
+        })
+        .parse(input)
+}
 
 // note: this rule is not present in the original grammar
 // ws-comment-newline = *( ws-newline-nonempty / comment )
-parse!(ws_comment_newline() -> &'a [u8], {
-    recognize(
-        skip_many(
-            choice((
-                skip_many1(
-                    choice((
-                        take_while1(is_wschar),
-                        newline().map(|_| &b"\n"[..]),
-                    ))
-                ),
-                comment().map(|_| ()),
-            ))
-        )
-    )
-});
+pub(crate) fn ws_comment_newline(input: Input<'_>) -> IResult<Input<'_>, &[u8], ParserError<'_>> {
+    many0_count(alt((
+        many1_count(alt((take_while1(WSCHAR), newline.value(&b"\n"[..])))).value(()),
+        comment.value(()),
+    )))
+    .recognize()
+    .parse(input)
+}
 
 // note: this rule is not present in the original grammar
 // line-ending = newline / eof
-parse!(line_ending() -> &'a str, {
-    choice((
-        newline().map(|_| "\n"),
-        eof().map(|_| "")
-    ))
-});
+pub(crate) fn line_ending(input: Input<'_>) -> IResult<Input<'_>, &str, ParserError<'_>> {
+    alt((newline.value("\n"), eof.value(""))).parse(input)
+}
 
 // note: this rule is not present in the original grammar
 // line-trailing = ws [comment] skip-line-ending
-parse!(line_trailing() -> &'a [u8], {
-    recognize((
-        ws(),
-        optional(comment()),
-    )).skip(line_ending())
-});
+pub(crate) fn line_trailing(input: Input<'_>) -> IResult<Input<'_>, &[u8], ParserError<'_>> {
+    terminated((ws, opt(comment)).recognize(), line_ending).parse(input)
+}
 
 #[cfg(test)]
 mod test {
     use super::*;
 
-    use crate::parser::*;
-    use combine::stream::position::Stream;
-    use snapbox::assert_eq;
-
     #[test]
     fn trivia() {
         let inputs = [
@@ -154,11 +138,10 @@ mod test {
    "#,
         ];
         for input in inputs {
-            let parsed = trivia::ws_comment_newline().easy_parse(Stream::new(input.as_bytes()));
-            assert!(parsed.is_ok());
-            let (t, rest) = parsed.unwrap();
-            assert!(rest.input.is_empty());
-            assert_eq(t, input.as_bytes());
+            let parsed = ws_comment_newline.parse(input.as_bytes()).finish();
+            assert!(parsed.is_ok(), "{:?}", parsed);
+            let parsed = parsed.unwrap();
+            assert_eq!(parsed, input.as_bytes());
         }
     }
 }
diff --git a/crates/toml_edit/src/parser/value.rs b/crates/toml_edit/src/parser/value.rs
index d55713dd..662b8311 100644
--- a/crates/toml_edit/src/parser/value.rs
+++ b/crates/toml_edit/src/parser/value.rs
@@ -1,11 +1,13 @@
-use combine::parser::range::recognize_with_value;
-use combine::stream::RangeStream;
-use combine::*;
+use nom8::branch::alt;
+use nom8::bytes::any;
+use nom8::combinator::fail;
+use nom8::combinator::peek;
 
 use crate::parser::array::array;
 use crate::parser::datetime::date_time;
 use crate::parser::inline_table::inline_table;
 use crate::parser::numbers::{float, integer};
+use crate::parser::prelude::*;
 use crate::parser::strings::string;
 use crate::parser::trivia::from_utf8_unchecked;
 use crate::repr::{Formatted, Repr};
@@ -13,50 +15,75 @@ use crate::value as v;
 use crate::Value;
 
 // val = string / boolean / array / inline-table / date-time / float / integer
-parse!(value() -> v::Value, {
-    recognize_with_value(look_ahead(any()).then(|e| {
-        dispatch!(e;
+pub(crate) fn value(input: Input<'_>) -> IResult<Input<'_>, v::Value, ParserError<'_>> {
+    dispatch!{peek(any);
             crate::parser::strings::QUOTATION_MARK |
-            crate::parser::strings::APOSTROPHE => string().map(|s| {
+            crate::parser::strings::APOSTROPHE => string.map(|s| {
                 v::Value::String(Formatted::new(
-                    s,
+                    s.into_owned()
                 ))
             }),
-            crate::parser::array::ARRAY_OPEN => array().map(v::Value::Array),
-            crate::parser::inline_table::INLINE_TABLE_OPEN => inline_table().map(v::Value::InlineTable),
+            crate::parser::array::ARRAY_OPEN => array.map(v::Value::Array),
+            crate::parser::inline_table::INLINE_TABLE_OPEN => inline_table.map(v::Value::InlineTable),
             // Date/number starts
-            b'+' | b'-' | b'0'..=b'9' |
-            // Report as if they were numbers because its most likely a typo
-            b'.' | b'_' => {
+            b'+' | b'-' | b'0'..=b'9' => {
                 // Uncommon enough not to be worth optimizing at this time
-                choice((
-                    date_time()
+                alt((
+                    date_time
                         .map(v::Value::from),
-                    float()
+                    float
                         .map(v::Value::from),
-                    integer()
+                    integer
                         .map(v::Value::from),
                 ))
             },
+            // Report as if they were numbers because its most likely a typo
+            b'_' => {
+                    integer
+                        .map(v::Value::from)
+                .context(Context::Expected(ParserValue::Description("leading digit")))
+            },
+            // Report as if they were numbers because its most likely a typo
+            b'.' =>  {
+                    float
+                        .map(v::Value::from)
+                .context(Context::Expected(ParserValue::Description("leading digit")))
+            },
             b't' => {
-                crate::parser::numbers::true_().map(v::Value::from).expected("quoted string")
+                crate::parser::numbers::true_.map(v::Value::from)
+                    .context(Context::Expression("string"))
+                    .context(Context::Expected(ParserValue::CharLiteral('"')))
+                    .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
             b'f' => {
-                crate::parser::numbers::false_().map(v::Value::from).expected("quoted string")
+                crate::parser::numbers::false_.map(v::Value::from)
+                    .context(Context::Expression("string"))
+                    .context(Context::Expected(ParserValue::CharLiteral('"')))
+                    .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
             b'i' => {
-                crate::parser::numbers::inf().map(v::Value::from).expected("quoted string")
+                crate::parser::numbers::inf.map(v::Value::from)
+                    .context(Context::Expression("string"))
+                    .context(Context::Expected(ParserValue::CharLiteral('"')))
+                    .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
             b'n' => {
-                crate::parser::numbers::nan().map(v::Value::from).expected("quoted string")
+                crate::parser::numbers::nan.map(v::Value::from)
+                    .context(Context::Expression("string"))
+                    .context(Context::Expected(ParserValue::CharLiteral('"')))
+                    .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
             _ => {
-                // Pick something random to fail, we'll override `expected` anyways
-                crate::parser::numbers::nan().map(v::Value::from).expected("quoted string")
+                fail
+                    .context(Context::Expression("string"))
+                    .context(Context::Expected(ParserValue::CharLiteral('"')))
+                    .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
-        )
-    })).and_then(|(raw, value)| apply_raw(value, raw))
-});
+    }
+        .with_recognized()
+        .map_res(|(value, raw)| apply_raw(value, raw))
+        .parse(input)
+}
 
 fn apply_raw(mut val: Value, raw: &[u8]) -> Result<Value, std::str::Utf8Error> {
     match val {
@@ -88,6 +115,8 @@ fn apply_raw(mut val: Value, raw: &[u8]) -> Result<Value, std::str::Utf8Error> {
 
 #[cfg(test)]
 mod test {
+    use super::*;
+
     #[test]
     fn values() {
         let inputs = [
@@ -108,7 +137,8 @@ trimmed in raw strings.
             r#"[ { x = 1, a = "2" }, {a = "a",b = "b",     c =    "c"} ]"#,
         ];
         for input in inputs {
-            parsed_value_eq!(input);
+            let parsed = value.parse(input.as_bytes()).finish();
+            assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
     }
 }
diff --git a/crates/toml_edit/src/value.rs b/crates/toml_edit/src/value.rs
index 48029914..12df12d4 100644
--- a/crates/toml_edit/src/value.rs
+++ b/crates/toml_edit/src/value.rs
@@ -1,7 +1,6 @@
 use std::iter::FromIterator;
 use std::str::FromStr;
 
-use combine::stream::position::Stream;
 use toml_datetime::*;
 
 use crate::key::Key;
@@ -212,19 +211,12 @@ impl FromStr for Value {
 
     /// Parses a value from a &str
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        use combine::stream::position::{IndexPositioner, Positioner};
-        use combine::EasyParser;
+        use nom8::prelude::*;
 
         let b = s.as_bytes();
-        let parsed = parser::value_parser().easy_parse(Stream::new(b));
+        let parsed = parser::value::value.parse(b).finish();
         match parsed {
-            Ok((_, ref rest)) if !rest.input.is_empty() => Err(Self::Err::from_unparsed(
-                (&rest.positioner
-                    as &dyn Positioner<usize, Position = usize, Checkpoint = IndexPositioner>)
-                    .position(),
-                b,
-            )),
-            Ok((mut value, _)) => {
+            Ok(mut value) => {
                 // Only take the repr and not decor, as its probably not intended
                 value.decor_mut().clear();
                 Ok(value)


Test patch:
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-leads.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-leads.stderr
index 1372096c..36fd8cc4 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-leads.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-leads.stderr
@@ -1,6 +1,5 @@
-TOML parse error at line 1, column 18
+TOML parse error at line 1, column 17
   |
 1 | no-leads = 1987-7-05T17:45:00Z
-  |                  ^
-expected 1 more elements
-While parsing a Date-Time
+  |                 ^
+Invalid date-time
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-secs.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-secs.stderr
index 9ec56c7b..c28861d4 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-secs.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-secs.stderr
@@ -2,6 +2,4 @@ TOML parse error at line 1, column 27
   |
 1 | no-secs = 1987-07-05T17:45Z
   |                           ^
-Unexpected `Z`
-Expected `:`
-While parsing a Date-Time
+Invalid date-time
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-t.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-t.stderr
index 62a77d25..c8abb9ba 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-t.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-no-t.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 18
   |
 1 | no-t = 1987-07-0517:45:00Z
   |                  ^
-Unexpected `1`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-with-milli.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-with-milli.stderr
index 13c18294..6604f0df 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-with-milli.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime-malformed-with-milli.stderr
@@ -1,6 +1,5 @@
-TOML parse error at line 1, column 23
+TOML parse error at line 1, column 22
   |
 1 | with-milli = 1987-07-5T17:45:00.12Z
-  |                       ^
-expected 1 more elements
-While parsing a Date-Time
+  |                      ^
+Invalid date-time
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-dotted-into-std.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-dotted-into-std.stderr
index 16e126ed..a20e3719 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-dotted-into-std.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-dotted-into-std.stderr
@@ -3,4 +3,3 @@ TOML parse error at line 5, column 1
 5 | apple.color = "red"
   | ^
 Duplicate key `color`
-
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-std-into-dotted.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-std-into-dotted.stderr
index 210e442e..43f84e4b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-std-into-dotted.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-std-into-dotted.stderr
@@ -3,4 +3,3 @@ TOML parse error at line 6, column 1
 6 | size = "small"
   | ^
 Duplicate key `size`
-
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-table.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-table.stderr
index a1c49025..5f2396a0 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-key-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-key-table.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 4, column 1
   |
 4 | [fruit.type]
   | ^
+Invalid table header
 Duplicate key `type` in table `fruit`
-
-While parsing a Table Header
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-cargo.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-cargo.stderr
index be31ca3e..0c4d4cdf 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-cargo.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-cargo.stderr
@@ -3,4 +3,3 @@ TOML parse error at line 7, column 1
 7 | categories = ["algorithms"]
   | ^
 Duplicate key `categories` in table `project`
-
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-dotted.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-dotted.stderr
index 4f51ae65..6f0fb425 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-dotted.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys-dotted.stderr
@@ -3,4 +3,3 @@ TOML parse error at line 3, column 1
 3 | ssl-version.min = 'tlsv1.2'
   | ^
 Dotted key `ssl-version` attempted to extend non-table type (string)
-
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys.stderr
index 586f016a..f247f969 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-keys.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-keys.stderr
@@ -3,4 +3,3 @@ TOML parse error at line 2, column 1
 2 | dupe = true
   | ^
 Duplicate key `dupe` in document root
-
diff --git a/crates/toml_edit/tests/fixtures/invalid/duplicate-tables.stderr b/crates/toml_edit/tests/fixtures/invalid/duplicate-tables.stderr
index 6ff97fa3..e951645e 100644
--- a/crates/toml_edit/tests/fixtures/invalid/duplicate-tables.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/duplicate-tables.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 2, column 1
   |
 2 | [a]
   | ^
+Invalid table header
 Duplicate key `a` in document root
-
-While parsing a Table Header
diff --git a/crates/toml_edit/tests/fixtures/invalid/empty-implicit-table.stderr b/crates/toml_edit/tests/fixtures/invalid/empty-implicit-table.stderr
index 95002644..69c6769b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/empty-implicit-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/empty-implicit-table.stderr
@@ -1,6 +1,6 @@
-TOML parse error at line 1, column 10
+TOML parse error at line 1, column 9
   |
 1 | [naughty..naughty]
-  |          ^
-Unexpected `.`
-While parsing a Table Header
+  |         ^
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/empty-table.stderr b/crates/toml_edit/tests/fixtures/invalid/empty-table.stderr
index 46e13837..1ff9d6d7 100644
--- a/crates/toml_edit/tests/fixtures/invalid/empty-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/empty-table.stderr
@@ -2,6 +2,4 @@ TOML parse error at line 1, column 2
   |
 1 | []
   |  ^
-Unexpected `]`
-Expected key
-While parsing a Table Header
+Invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-neg.stderr b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-neg.stderr
index ae05cd22..42f971ff 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-neg.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-neg.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 18
   |
 1 | leading-zero = -03.14
   |                  ^
-Unexpected `3`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-pos.stderr b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-pos.stderr
index 0539b608..0075d325 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-pos.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero-pos.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 18
   |
 1 | leading-zero = +03.14
   |                  ^
-Unexpected `3`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero.stderr b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero.stderr
index ba27e778..59b4a370 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-leading-zero.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-leading-zero.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 17
   |
 1 | leading-zero = 03.14
   |                 ^
-Unexpected `3`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-no-leading-zero.stderr b/crates/toml_edit/tests/fixtures/invalid/float-no-leading-zero.stderr
index ac781327..5d15c62d 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-no-leading-zero.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-no-leading-zero.stderr
@@ -2,14 +2,5 @@ TOML parse error at line 1, column 10
   |
 1 | answer = .12345
   |          ^
-Unexpected `.`
-Expected `-`, `+`, `inf`, `nan`, `0x`, `0o` or `0b`
-expected 4 more elements
-expected 2 more elements
-While parsing a Time
-While parsing a hexadecimal Integer
-While parsing a octal Integer
-While parsing a binary Integer
-While parsing an Integer
-While parsing a Date-Time
-While parsing a Float
+Invalid floating-point number
+Expected leading digit
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-no-trailing-digits.stderr b/crates/toml_edit/tests/fixtures/invalid/float-no-trailing-digits.stderr
index ddf64350..2cb07139 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-no-trailing-digits.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-no-trailing-digits.stderr
@@ -2,7 +2,5 @@ TOML parse error at line 1, column 12
   |
 1 | answer = 1.
   |            ^
-Unexpected `
-`
+Invalid floating-point number
 Expected digit
-While parsing a Float
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-underscore-after-point.stderr b/crates/toml_edit/tests/fixtures/invalid/float-underscore-after-point.stderr
index 852f5cf0..1e170938 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-underscore-after-point.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-underscore-after-point.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | bad = 1._2
   |         ^
-Unexpected `_`
+Invalid floating-point number
 Expected digit
-While parsing a Float
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-underscore-after.stderr b/crates/toml_edit/tests/fixtures/invalid/float-underscore-after.stderr
index 5522b626..36a87cb1 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-underscore-after.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-underscore-after.stderr
@@ -2,7 +2,5 @@ TOML parse error at line 1, column 11
   |
 1 | bad = 1.2_
   |           ^
-Unexpected `
-`
-Expected digit
-While parsing a Float
+Invalid floating-point number
+Expected digit, digit
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-underscore-before-point.stderr b/crates/toml_edit/tests/fixtures/invalid/float-underscore-before-point.stderr
index 7e53a5dc..bbc7aede 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-underscore-before-point.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-underscore-before-point.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | bad = 1_.2
   |         ^
-Unexpected `.`
+Invalid integer
 Expected digit
-While parsing an Integer
diff --git a/crates/toml_edit/tests/fixtures/invalid/float-underscore-before.stderr b/crates/toml_edit/tests/fixtures/invalid/float-underscore-before.stderr
index 1a903fa2..41607f5f 100644
--- a/crates/toml_edit/tests/fixtures/invalid/float-underscore-before.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/float-underscore-before.stderr
@@ -2,14 +2,5 @@ TOML parse error at line 1, column 7
   |
 1 | bad = _1.2
   |       ^
-Unexpected `_`
-Expected `-`, `+`, `inf`, `nan`, `0x`, `0o` or `0b`
-expected 4 more elements
-expected 2 more elements
-While parsing a Time
-While parsing a hexadecimal Integer
-While parsing a octal Integer
-While parsing a binary Integer
-While parsing an Integer
-While parsing a Date-Time
-While parsing a Float
+Invalid integer
+Expected leading digit
diff --git a/crates/toml_edit/tests/fixtures/invalid/inline-table-newline.stderr b/crates/toml_edit/tests/fixtures/invalid/inline-table-newline.stderr
index 54a52fbd..5967ff1d 100644
--- a/crates/toml_edit/tests/fixtures/invalid/inline-table-newline.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/inline-table-newline.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 7, column 11
   |
 7 | native = {
   |           ^
-Unexpected `
-`
-Expected key
+Invalid inline table
+Expected `}`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-binary-char.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-binary-char.stderr
index 5ced7591..ec273a8b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-binary-char.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-binary-char.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 11
   |
 1 | bad = 0b102
   |           ^
-Unexpected `2`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-hex-char.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-hex-char.stderr
index 05669dd1..35afb866 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-hex-char.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-hex-char.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 11
   |
 1 | bad = 0x12g
   |           ^
-Unexpected `g`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-octal-char.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-octal-char.stderr
index 97ff183d..f58d74b6 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-invalid-octal-char.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-invalid-octal-char.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 11
   |
 1 | bad = 0o758
   |           ^
-Unexpected `8`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-neg.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-neg.stderr
index a1106553..c0f4b181 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-neg.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-neg.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 18
   |
 1 | leading-zero = -012
   |                  ^
-Unexpected `1`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-pos.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-pos.stderr
index 475bc120..9e847996 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-pos.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero-pos.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 18
   |
 1 | leading-zero = +012
   |                  ^
-Unexpected `1`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero.stderr
index 8f5c4d4c..419996a8 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-leading-zero.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 17
   |
 1 | leading-zero = 012
   |                 ^
-Unexpected `1`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-after.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-after.stderr
index e731ba31..da11e07c 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-after.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-after.stderr
@@ -2,7 +2,5 @@ TOML parse error at line 1, column 11
   |
 1 | bad = 123_
   |           ^
-Unexpected `
-`
+Invalid integer
 Expected digit
-While parsing an Integer
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-before.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-before.stderr
index 24635fd8..756621a8 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-before.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-before.stderr
@@ -2,14 +2,5 @@ TOML parse error at line 1, column 7
   |
 1 | bad = _123
   |       ^
-Unexpected `_`
-Expected `-`, `+`, `inf`, `nan`, `0x`, `0o` or `0b`
-expected 4 more elements
-expected 2 more elements
-While parsing a Time
-While parsing a hexadecimal Integer
-While parsing a octal Integer
-While parsing a binary Integer
-While parsing an Integer
-While parsing a Date-Time
-While parsing a Float
+Invalid integer
+Expected leading digit
diff --git a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-double.stderr b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-double.stderr
index 3f17deea..757e661c 100644
--- a/crates/toml_edit/tests/fixtures/invalid/integer-underscore-double.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/integer-underscore-double.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | bad = 1__23
   |         ^
-Unexpected `_`
+Invalid integer
 Expected digit
-While parsing an Integer
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-after-array.stderr b/crates/toml_edit/tests/fixtures/invalid/key-after-array.stderr
index b08b31ba..f59405fd 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-after-array.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-after-array.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 14
   |
 1 | [[agencies]] owner = "S Cjelli"
   |              ^
-Unexpected `o`
-Expected newline or end of input
-While parsing a Table Header
+Invalid table header
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-after-table.stderr b/crates/toml_edit/tests/fixtures/invalid/key-after-table.stderr
index 3961a5ce..2fab5108 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-after-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-after-table.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | [error] this = "should not be here"
   |         ^
-Unexpected `t`
-Expected newline or end of input
-While parsing a Table Header
+Invalid table header
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-empty.stderr b/crates/toml_edit/tests/fixtures/invalid/key-empty.stderr
index 1d0241c7..a6ff759d 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-empty.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-empty.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 2
   |
 1 |  = 1
   |  ^
-Unexpected `=`
-Expected key
+Invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-hash.stderr b/crates/toml_edit/tests/fixtures/invalid/key-hash.stderr
index 2350d93c..4e086c25 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-hash.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-hash.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 2
   |
 1 | a# = 1
   |  ^
-Unexpected `#`
-Expected `.` or `=`
+Expected `.`, `=`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-newline.stderr b/crates/toml_edit/tests/fixtures/invalid/key-newline.stderr
index e9361179..114521ed 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-newline.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-newline.stderr
@@ -2,6 +2,4 @@ TOML parse error at line 1, column 2
   |
 1 | a
   |  ^
-Unexpected `
-`
-Expected `.` or `=`
+Expected `.`, `=`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-no-eol.stderr b/crates/toml_edit/tests/fixtures/invalid/key-no-eol.stderr
index de2d375e..cf811e1b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-no-eol.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-no-eol.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 7
   |
 1 | a = 1 b = 2
   |       ^
-Unexpected `b`
-Expected newline or end of input
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-open-bracket.stderr b/crates/toml_edit/tests/fixtures/invalid/key-open-bracket.stderr
index 6e23a74a..4179bf07 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-open-bracket.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-open-bracket.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 6
   |
 1 | [abc = 1
   |      ^
-Unexpected `=`
-Expected `.` or `]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-single-open-bracket.stderr b/crates/toml_edit/tests/fixtures/invalid/key-single-open-bracket.stderr
index 0851e4cb..c7d76e32 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-single-open-bracket.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-single-open-bracket.stderr
@@ -1,7 +1,5 @@
-TOML parse error at line 1, column 2
+TOML parse error at line 1, column 1
   |
 1 | [
-  |  ^
-Unexpected end of input
-Expected key
-While parsing a Table Header
+  | ^
+Invalid table header
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-space.stderr b/crates/toml_edit/tests/fixtures/invalid/key-space.stderr
index 3fd9d23a..867ae6c6 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-space.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-space.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 3
   |
 1 | a b = 1
   |   ^
-Unexpected `b`
-Expected `.` or `=`
+Expected `.`, `=`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-start-bracket.stderr b/crates/toml_edit/tests/fixtures/invalid/key-start-bracket.stderr
index d9590d32..4081804a 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-start-bracket.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-start-bracket.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 2, column 6
   |
 2 | [xyz = 5
   |      ^
-Unexpected `=`
-Expected `.` or `]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/key-two-equals.stderr b/crates/toml_edit/tests/fixtures/invalid/key-two-equals.stderr
index ad54644e..6cb510be 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key-two-equals.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key-two-equals.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 1, column 6
   |
 1 | key= = 1
   |      ^
-Unexpected `=`
-Expected quoted string
+Invalid string
+Expected `"`, `'`
diff --git a/crates/toml_edit/tests/fixtures/invalid/llbrace.stderr b/crates/toml_edit/tests/fixtures/invalid/llbrace.stderr
index e33fb629..3ca489df 100644
--- a/crates/toml_edit/tests/fixtures/invalid/llbrace.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/llbrace.stderr
@@ -2,7 +2,4 @@ TOML parse error at line 1, column 3
   |
 1 | [ [table]]
   |   ^
-Unexpected `[`
-Unexpected ` `
-Expected key
-While parsing a Table Header
+Invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/rrbrace.stderr b/crates/toml_edit/tests/fixtures/invalid/rrbrace.stderr
index 81ef3bb5..fa3f51b1 100644
--- a/crates/toml_edit/tests/fixtures/invalid/rrbrace.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/rrbrace.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 8
   |
 1 | [[table] ]
   |        ^
-Unexpected `]`
-Expected `.` or `]]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-bad-byte-escape.stderr b/crates/toml_edit/tests/fixtures/invalid/string-bad-byte-escape.stderr
index 7ec3bfa6..89851d08 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-bad-byte-escape.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-bad-byte-escape.stderr
@@ -1,7 +1,6 @@
-TOML parse error at line 1, column 13
+TOML parse error at line 1, column 14
   |
 1 | naughty = "\xAg"
-  |             ^
-Unexpected `x`
-While parsing escape sequence
-While parsing a Basic String
+  |              ^
+Invalid escape sequence
+Expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-bad-escape.stderr b/crates/toml_edit/tests/fixtures/invalid/string-bad-escape.stderr
index 64bfa6cd..8fe1e0cc 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-bad-escape.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-bad-escape.stderr
@@ -1,7 +1,6 @@
-TOML parse error at line 1, column 42
+TOML parse error at line 1, column 43
   |
 1 | invalid-escape = "This string has a bad \a escape character."
-  |                                          ^
-Unexpected `a`
-While parsing escape sequence
-While parsing a Basic String
+  |                                           ^
+Invalid escape sequence
+Expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-bad-surrogate.stderr b/crates/toml_edit/tests/fixtures/invalid/string-bad-surrogate.stderr
index 4e2c605f..613940d9 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-bad-surrogate.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-bad-surrogate.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 15
   |
 1 | notgood = "a\uDCFF"
   |               ^
-Invalid hex escape code: dcff 
-
-While parsing a Basic String
+Invalid unicode 4-digit hex code
+Value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-bad-uni-esc.stderr b/crates/toml_edit/tests/fixtures/invalid/string-bad-uni-esc.stderr
index 2192bd18..ae3f7382 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-bad-uni-esc.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-bad-uni-esc.stderr
@@ -1,6 +1,5 @@
-TOML parse error at line 1, column 14
+TOML parse error at line 1, column 13
   |
 1 | str = "val\ue"
-  |              ^
-expected 3 more elements
-While parsing a Basic String
+  |             ^
+Invalid unicode 4-digit hex code
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-byte-escapes.stderr b/crates/toml_edit/tests/fixtures/invalid/string-byte-escapes.stderr
index 52887059..f039b3a3 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-byte-escapes.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-byte-escapes.stderr
@@ -1,7 +1,6 @@
-TOML parse error at line 1, column 12
+TOML parse error at line 1, column 13
   |
 1 | answer = "\x33"
-  |            ^
-Unexpected `x`
-While parsing escape sequence
-While parsing a Basic String
+  |             ^
+Invalid escape sequence
+Expected `b`, `f`, `n`, `r`, `t`, `u`, `U`, `\`, `"`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-close.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-close.stderr
index fdeeb418..420fab61 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-close.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-close.stderr
@@ -2,7 +2,4 @@ TOML parse error at line 1, column 42
   |
 1 | no-ending-quote = "One time, at band camp
   |                                          ^
-Unexpected `
-`
-Expected `"`
-While parsing a Basic String
+Invalid basic string
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-constant-like.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-constant-like.stderr
index 75cc2b2b..62a1750b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-constant-like.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-constant-like.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 1, column 7
   |
 1 | value=trust
   |       ^
-Unexpected `t`
-Expected quoted string
+Invalid string
+Expected `"`, `'`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-array.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-array.stderr
index 75f73091..5ffa8eb0 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-array.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-array.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 1, column 10
   |
 1 | value = [ZZZ]
   |          ^
-Unexpected `Z`
-Expected newline or `#`
+Invalid array
+Expected `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-inline-table.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-inline-table.stderr
index add99d50..5c5117aa 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-inline-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-inline-table.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 1, column 17
   |
 1 | value = { key = ZZZ }
   |                 ^
-Unexpected `Z`
-Expected quoted string
+Invalid string
+Expected `"`, `'`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-table.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-table.stderr
index e16f11a2..9bc4cac9 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-in-table.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 1, column 7
   |
 1 | value=ZZZ
   |       ^
-Unexpected `Z`
-Expected quoted string
+Invalid string
+Expected `"`, `'`
diff --git a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-number-like.stderr b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-number-like.stderr
index 7499e608..96b1456f 100644
--- a/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-number-like.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/string-no-quotes-number-like.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 10
   |
 1 | value=123hello
   |          ^
-Unexpected `h`
-Expected `#`
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-array-implicit.stderr b/crates/toml_edit/tests/fixtures/invalid/table-array-implicit.stderr
index d771f5c3..ae0ae9b9 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-array-implicit.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-array-implicit.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 13, column 1
    |
 13 | [[albums]]
    | ^
+Invalid table header
 Duplicate key `albums` in document root
-
-While parsing a Table Header
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-bracket.stderr b/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-bracket.stderr
index a5dbf1de..0b5694c4 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-bracket.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-bracket.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | [[albums]
   |         ^
-Unexpected `]`
-Expected `.` or `]]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-empty.stderr b/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-empty.stderr
index 65253f80..b2732185 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-empty.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-array-malformed-empty.stderr
@@ -2,6 +2,4 @@ TOML parse error at line 1, column 3
   |
 1 | [[]]
   |   ^
-Unexpected `]`
-Expected key
-While parsing a Table Header
+Invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-empty.stderr b/crates/toml_edit/tests/fixtures/invalid/table-empty.stderr
index 46e13837..1ff9d6d7 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-empty.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-empty.stderr
@@ -2,6 +2,4 @@ TOML parse error at line 1, column 2
   |
 1 | []
   |  ^
-Unexpected `]`
-Expected key
-While parsing a Table Header
+Invalid key
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-close.stderr b/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-close.stderr
index c4a3172a..a40cd447 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-close.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-close.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 4
   |
 1 | [a]b]
   |    ^
-Unexpected `b`
-Expected `#`
-While parsing a Table Header
+Invalid table header
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-open.stderr b/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-open.stderr
index 68691354..8f78b998 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-open.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-nested-brackets-open.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 3
   |
 1 | [a[b]
   |   ^
-Unexpected `[`
-Expected `.` or `]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-whitespace.stderr b/crates/toml_edit/tests/fixtures/invalid/table-whitespace.stderr
index 01a9ee82..fc898d24 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-whitespace.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-whitespace.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 10
   |
 1 | [invalid key]
   |          ^
-Unexpected `k`
-Expected `.` or `]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/table-with-pound.stderr b/crates/toml_edit/tests/fixtures/invalid/table-with-pound.stderr
index 3550cc83..28574437 100644
--- a/crates/toml_edit/tests/fixtures/invalid/table-with-pound.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/table-with-pound.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 5
   |
 1 | [key#group]
   |     ^
-Unexpected `#`
-Expected `.` or `]`
-While parsing a Table Header
+Invalid table header
+Expected `.`, `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-after-array-entries.stderr b/crates/toml_edit/tests/fixtures/invalid/text-after-array-entries.stderr
index 62283a99..c262785a 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-after-array-entries.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-after-array-entries.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 2, column 46
   |
 2 |   "Is there life after an array separator?", No
   |                                              ^
-Unexpected `N`
+Invalid array
 Expected `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-after-integer.stderr b/crates/toml_edit/tests/fixtures/invalid/text-after-integer.stderr
index 66c84ee4..e6f20145 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-after-integer.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-after-integer.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 13
   |
 1 | answer = 42 the ultimate answer?
   |             ^
-Unexpected `t`
-Expected newline or end of input
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-after-string.stderr b/crates/toml_edit/tests/fixtures/invalid/text-after-string.stderr
index a757e60f..1ac8e02e 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-after-string.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-after-string.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 1, column 41
   |
 1 | string = "Is there life after strings?" No.
   |                                         ^
-Unexpected `N`
-Expected newline or end of input
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-after-table.stderr b/crates/toml_edit/tests/fixtures/invalid/text-after-table.stderr
index 051b37ef..cde352f9 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-after-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-after-table.stderr
@@ -2,6 +2,5 @@ TOML parse error at line 1, column 9
   |
 1 | [error] this shouldn't be here
   |         ^
-Unexpected `t`
-Expected newline or end of input
-While parsing a Table Header
+Invalid table header
+Expected newline, `#`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-before-array-separator.stderr b/crates/toml_edit/tests/fixtures/invalid/text-before-array-separator.stderr
index 277e603f..d9b2224b 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-before-array-separator.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-before-array-separator.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 2, column 46
   |
 2 |   "Is there life before an array separator?" No,
   |                                              ^
-Unexpected `N`
+Invalid array
 Expected `]`
diff --git a/crates/toml_edit/tests/fixtures/invalid/text-in-array.stderr b/crates/toml_edit/tests/fixtures/invalid/text-in-array.stderr
index eded832c..ecb8abc4 100644
--- a/crates/toml_edit/tests/fixtures/invalid/text-in-array.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/text-in-array.stderr
@@ -2,5 +2,5 @@ TOML parse error at line 3, column 3
   |
 3 |   I don't belong,
   |   ^
-Unexpected `I`
+Invalid array
 Expected `]`
diff --git a/crates/toml_edit/tests/test_parse.rs b/crates/toml_edit/tests/test_parse.rs
index 231d3feb..52332b54 100644
--- a/crates/toml_edit/tests/test_parse.rs
+++ b/crates/toml_edit/tests/test_parse.rs
@@ -21,27 +21,6 @@ macro_rules! test_key {
     }};
 }
 
-macro_rules! parse_error {
-    ($input:expr, $ty:ty, $err_msg:expr) => {{
-        let res = $input.parse::<$ty>();
-        assert!(res.is_err());
-        let err = res.unwrap_err();
-        assert!(
-            err.to_string().find($err_msg).is_some(),
-            "Error was: `{:?}`",
-            err.to_string()
-        );
-    }};
-}
-
-#[test]
-fn test_parse_error() {
-    parse_error!("'hello'bla", Value, "Could not parse the line");
-    parse_error!(r#"{a = 2"#, Value, "Expected `}`");
-
-    parse_error!("'\"", Key, "Expected `'`");
-}
-
 #[test]
 fn test_key_from_str() {
     test_key!("a", "a");