Exemple
winnow 1.0.4: Distinguish winnow error handling paths where alt combinator discards deep branch errors in favor of the last branch unless cut_err is used, try_map failures yield recoverable Backtrack rather than fatal Cut, and ContextError formatting displays only the innermost label while InputError drops context annotations entirely
Échantillon vérifié pour cargo winnow 1.0.4: Distinguish winnow error handling paths where alt combinator discards deep branch errors in favor of the last…
sha256:37c7408540f30c3e0ea78c7a69f7ddb967574c1c9fbfebcf11fbf25d3bbe07d5
Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré.
Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes.
MIT-0
Preuves d'exécution
L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.
- Base de preuve
- Contrat signé réussi
- Reçus de vérification
- 3
- Clés de signature qui l’ont compilé
- 3
Environnement déclaré
rust linux x64 rust rust cargo
Environnements des exécutions de vérification
| Environnement | Contrat | Étapes | Exécution |
|---|---|---|---|
| rust 1 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1 |
2026-08-16 |
| rust 1 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1 |
2026-08-18 |
| rust 1 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1rust:1-alpine@sha256:a10e64dd139b… |
2026-09-08 |
Cas
HOW- Objectif
- Distinguish winnow error handling paths where alt combinator discards deep branch errors in favor of the last branch unless cut_err is used, try_map failures yield recoverable Backtrack rather than fatal Cut, and ContextError formatting displays only the innermost label while InputError drops context annotations entirely
- Paquets
- Symboles
-
- winnow::error::ContextError
- winnow::error::ErrMode
- winnow::error::InputError
- winnow::error::TreeError
- winnow::error::ParseError
- winnow::combinator::alt
- winnow::combinator::cut_err
- Environnement
- rust
- Créé
- 2026-08-16T12:43:07Z
Contrat
- assert alt with ContextError discards a partial match in an earlier branch and returns the error and offset of the last evaluated branch, while cut_err prevents backtrack and preserves the deep failure site
- assert try_map produces ErrMode::Backtrack rather than Cut on external validation failure so alt continues trying subsequent branches
- assert TreeError preserves all failed branches of an alt in a hierarchical Alt structure rather than overwriting them with the last branch
- assert InputError ignores AddContext labels and records the remaining input slice, whereas ContextError formats the innermost StrContext::Label and all Expected tokens but renders empty if unannotated
- assert Parser::parse erases the ErrMode modal distinction into ParseError and reports the offset of trailing unconsumed input when the inner parser succeeds partially
Fichiers
- Cargo.lock
- Cargo.toml
- NOTES.md
- csx.json
- src/lib.rs
Code source
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "winnow_error_traps"
version = "0.1.0"
dependencies = [
"winnow",
]
[package]
name = "winnow_error_traps"
version = "0.1.0"
edition = "2021"
[dependencies]
winnow = "1.0.4"
# winnow Error Handling and Error Path Traps
## Search Results
A search via `search_known_solution` returned existing samples:
- `sha256:b350c4d0906c65d969286b7014ce1d602e577b7c34f46b9fbb965f77f6481f6f` (1.0 migration arity caps, `Result`/`ModalResult`, `dec_uint` leading zero behavior, Pratt parser)
- `sha256:2bf2e3a631dbfd7719fde5509c11e3d126e1ac2480515e9bea445ea1c731d5aa` (`dec_uint`/`dec_int` digit consumption vs overflow)
- `sha256:2e7c98b44f1a021b539ac43ba7a898357bab96ad6119271910b73c8fe6e3ecf8` (`StreamIsPartial` and `ErrMode::Incomplete` vs `ErrMode::Backtrack`)
- `sha256:7ed98b9f45a560a4f35d8108ac104e684bcbc1c9e3bba5a8dad3f2766b48039a` (`Parser::parse` EOF consumption vs `Parser::parse_next`)
This sample explores a different, fundamental error handling trap: how `winnow` combinators and error types (`ContextError`, `ErrMode`, `InputError`, `TreeError`, `ParseError`) report and propagate errors when input is invalid.
## Failure Mode
Assuming `alt` reports the deepest error location fails silently with a green build at compile time, but produces completely misleading runtime diagnostics (reporting offset 0 of the final branch instead of offset 6 of the matched branch) unless `cut_err` is used or full error trees are collected with `TreeError`.
## Behavior Disproved
1. **`alt` Error Discarding**: `ContextError` implements `ParserError::or` by simply returning the right-hand error (`other`). When an input matches the prefix of an earlier alternative and fails deeper in that branch with `ErrMode::Backtrack`, `alt` rewinds and tests subsequent alternatives. The final error returned is exclusively from the last alternative evaluated (often at index 0), discarding the deep parse failure. `cut_err` is required to upgrade `ErrMode::Backtrack` to `ErrMode::Cut` to commit to the branch and preserve the true failure site.
2. **`try_map` Validation Fallthrough**: Semantic validation failures inside `try_map` (e.g. integer range overflow via `str::parse`) construct `ErrMode::Backtrack`, not `ErrMode::Cut`. In an `alt` chain, semantic errors cause fallback to other branches rather than terminating immediately.
3. **`TreeError` Branch Preservation**: Unlike `ContextError` which overwrites earlier errors, `TreeError` aggregates all alternative failures into `TreeError::Alt`.
4. **`InputError` vs `ContextError`**: `InputError` silently ignores all `.context(...)` annotations because `AddContext` is a no-op, whereas `ContextError` accumulates context but its `Display` implementation only outputs the innermost `StrContext::Label` (while collecting all `StrContext::Expected` values).
5. **`Parser::parse` Modal Erasure**: `Parser::parse` demands EOF and unrolls `ErrMode<E>` into `ParseError<I, E>`, discarding the modal distinction between `Cut` and `Backtrack`. When an inner parser partially succeeds, `ParseError::offset` points to the start of unconsumed trailing data.
{"case":{"believed":"When an alt combinator fails on invalid input after partially matching an alternative, ContextError preserves the error details and byte offset from whichever branch parsed deepest.","caseId":"case:sha256:7aac3d5471009ccc1943bae7df1c36a86524156af640c68ae512a49ce6957d6b","contract":["assert alt with ContextError discards a partial match in an earlier branch and returns the error and offset of the last evaluated branch, while cut_err prevents backtrack and preserves the deep failure site","assert try_map produces ErrMode::Backtrack rather than Cut on external validation failure so alt continues trying subsequent branches","assert TreeError preserves all failed branches of an alt in a hierarchical Alt structure rather than overwriting them with the last branch","assert InputError ignores AddContext labels and records the remaining input slice, whereas ContextError formats the innermost StrContext::Label and all Expected tokens but renders empty if unannotated","assert Parser::parse erases the ErrMode modal distinction into ParseError and reports the offset of trailing unconsumed input when the inner parser succeeds partially"],"goal":"Distinguish winnow error handling paths where alt combinator discards deep branch errors in favor of the last branch unless cut_err is used, try_map failures yield recoverable Backtrack rather than fatal Cut, and ContextError formatting displays only the innermost label while InputError drops context annotations entirely","kind":"HOW","packages":["pkg:cargo/winnow@1.0.4"],"schemaVersion":1,"symbols":["winnow::error::ContextError","winnow::error::ErrMode","winnow::error::InputError","winnow::error::TreeError","winnow::error::ParseError","winnow::combinator::alt","winnow::combinator::cut_err"]},"contractCommand":["cargo","test","--offline"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/winnow@1.0.4"],"schemaVersion":1,"symbols":["winnow::error::ContextError","winnow::error::ErrMode","winnow::error::InputError","winnow::error::TreeError","winnow::error::ParseError","winnow::combinator::alt","winnow::combinator::cut_err"],"verifierAdapter":"cargo@1"}
use winnow::ascii::digit1;
use winnow::combinator::{cut_err, preceded};
use winnow::error::{InputError, ModalResult, StrContext};
use winnow::prelude::*;
/// Parser for the "alpha:" branch using standard backtracking.
pub fn parse_alpha_backtrack<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
preceded("alpha:", digit1)
.context(StrContext::Label("alpha branch"))
.parse_next(input)
}
/// Parser for the "alpha:" branch with cut_err to commit after seeing the prefix.
pub fn parse_alpha_cut<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
preceded("alpha:", cut_err(digit1))
.context(StrContext::Label("alpha branch"))
.parse_next(input)
}
/// Parser for the "beta:" branch.
pub fn parse_beta<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
preceded("beta:", digit1)
.context(StrContext::Label("beta branch"))
.parse_next(input)
}
/// Parser for the "gamma:" branch (last branch in alt).
pub fn parse_gamma<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
preceded("gamma:", digit1)
.context(StrContext::Label("gamma branch"))
.parse_next(input)
}
/// Parser for port using try_map which fails on u8 overflow.
pub fn parse_port_try_map<'i>(input: &mut &'i str) -> ModalResult<u8> {
preceded("port:", digit1.try_map(|s: &str| s.parse::<u8>())).parse_next(input)
}
/// Fallback port parser in alt.
pub fn parse_port_fallback<'i>(input: &mut &'i str) -> ModalResult<u8> {
preceded("port:raw-", digit1.parse_to::<u8>()).parse_next(input)
}
/// Parser with InputError to demonstrate context dropping.
pub fn parse_input_error<'i>(input: &mut &'i str) -> ModalResult<&'i str, InputError<&'i str>> {
digit1
.context(StrContext::Label("digits"))
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
use winnow::combinator::alt;
use winnow::error::{
ContextError, ErrMode, ParseError, StrContextValue, TreeError,
};
use winnow::token::literal;
#[test]
fn test_alt_context_error_discards_deep_failure_without_cut() {
// In alt((alpha, beta, gamma)), on "alpha:XYZ":
// 1. alpha matches "alpha:" (6 bytes), but fails on digit1 at index 6.
// 2. Because it produces ErrMode::Backtrack, alt tries beta at index 0 (fails immediately).
// 3. alt tries gamma at index 0 (fails immediately).
// 4. ContextError's ParserError::or overwrites previous errors with the right-hand operand,
// so the final error reports offset 0 and label "gamma branch", losing the fact that
// alpha parsed 6 characters into the stream.
let mut alt_backtrack = alt((parse_alpha_backtrack, parse_beta, parse_gamma));
let err: ParseError<&str, ContextError> = alt_backtrack.parse("alpha:XYZ").unwrap_err();
assert_eq!(err.offset(), 0, "alt without cut_err reports offset of the last branch");
let ctx = err.into_inner();
let labels: Vec<_> = ctx.context().collect();
assert_eq!(
labels,
vec![&StrContext::Label("gamma branch")],
"alt returns the last branch's context rather than the deepest match"
);
// With cut_err: once "alpha:" is matched, the digit1 failure is upgraded to ErrMode::Cut.
// alt sees Cut, aborts subsequent branches, and pins the error at offset 6 with "alpha branch".
let mut alt_cut = alt((parse_alpha_cut, parse_beta, parse_gamma));
let cut_err_res: ParseError<&str, ContextError> = alt_cut.parse("alpha:XYZ").unwrap_err();
assert_eq!(cut_err_res.offset(), 6, "cut_err preserves the true failure site at offset 6");
let cut_ctx = cut_err_res.into_inner();
let cut_labels: Vec<_> = cut_ctx.context().collect();
assert_eq!(
cut_labels,
vec![&StrContext::Label("alpha branch")],
"cut_err preserves the matching branch's context"
);
}
#[test]
fn test_try_map_returns_backtrack_allowing_alt_fallthrough() {
// try_map semantic validation failure (e.g. u8 overflow on 999) produces ErrMode::Backtrack,
// not ErrMode::Cut. In an alt combinator, this means semantic failures are treated as
// syntactic mismatches and fall through to subsequent branches.
let mut alt_ports = alt((parse_port_try_map, parse_port_fallback));
// On "port:999", parse_port_try_map parses "999" then u8::from_str fails with ParseIntError.
// It returns ErrMode::Backtrack(cause: ParseIntError).
// alt then rewinds and tries parse_port_fallback, which also fails on "port:999" vs "port:raw-".
let mut input = "port:999";
let err = alt_ports.parse_next(&mut input).unwrap_err();
assert!(
matches!(err, ErrMode::Backtrack(_)),
"try_map failures produce Backtrack so alt continues branch exploration"
);
}
#[test]
fn test_tree_error_retains_all_alt_branches() {
// Unlike ContextError which drops earlier branch errors, TreeError aggregates
// each branch's failure into a TreeError::Alt variant.
let mut tree_parser = alt((
preceded("alpha:", digit1::<&str, ErrMode<TreeError<&str>>>)
.context(StrContext::Label("alpha branch")),
preceded("beta:", digit1::<&str, ErrMode<TreeError<&str>>>)
.context(StrContext::Label("beta branch")),
));
let mut input = "alpha:XYZ";
let tree_err = tree_parser.parse_next(&mut input).unwrap_err();
match tree_err {
ErrMode::Backtrack(TreeError::Stack { base, .. }) => match *base {
TreeError::Alt(branches) => {
assert_eq!(branches.len(), 2, "TreeError retains all alternative branches");
}
other => panic!("expected TreeError::Alt base, got {:?}", other),
},
other => panic!("expected TreeError::Stack, got {:?}", other),
}
}
#[test]
fn test_input_error_drops_context_while_context_error_formats_selectively() {
// 1. InputError records only the unparsed input slice at error position;
// AddContext is a no-op so `.context(...)` annotations are silently discarded.
let mut input = "XYZ";
let err = parse_input_error.parse_next(&mut input).unwrap_err();
match err {
ErrMode::Backtrack(InputError { input: rem }) => {
assert_eq!(rem, "XYZ");
assert_eq!(
format!("{}", InputError::at(rem)),
"failed to parse starting at: XYZ"
);
}
_ => panic!("unexpected ErrMode"),
}
// 2. ContextError formatting:
// - Display only prints the FIRST (innermost) StrContext::Label pushed.
// - Multiple StrContext::Expected values are collected into a comma-separated list.
// - CharLiteral renders in backticks (e.g. '`' around characters).
// - Unannotated ContextError formats to empty string.
let mut ctx = ContextError::new();
ctx.push(StrContext::Label("inner_rule"));
ctx.push(StrContext::Label("outer_rule"));
ctx.push(StrContext::Expected(StrContextValue::Description("valid identifier")));
ctx.push(StrContext::Expected(StrContextValue::CharLiteral(';')));
assert_eq!(
format!("{ctx}"),
"invalid inner_rule\nexpected valid identifier, `;`"
);
assert_eq!(
format!("{}", ContextError::new()),
"",
"empty ContextError produces empty Display output"
);
}
#[test]
fn test_parse_erases_modal_err_mode_and_reports_trailing_offset() {
// Parser::parse wraps `(self, eof)`.
// If the inner parser succeeds on a prefix but trailing input remains,
// Parser::parse returns ParseError where:
// - offset is at the end of the consumed prefix (the unconsumed remainder start),
// - ErrMode (Cut vs Backtrack) is erased into the inner error type E.
let mut prefix_parser = literal::<&str, &str, ErrMode<ContextError>>("START");
let err = prefix_parser.parse("START_EXTRA_GARBAGE").unwrap_err();
assert_eq!(err.offset(), 5, "offset points to the start of unconsumed trailing data");
assert_eq!(err.char_span(), 5..6, "char_span covers the first unconsumed char");
assert_eq!(err.input(), &"START_EXTRA_GARBAGE", "input() retains the initial stream");
}
}