CodeSampleX

샘플

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

검증된 샘플 — cargo winnow 1.0.4: Distinguish winnow error handling paths where alt combinator discards deep branch errors in favor of the last branch unless…

sha256:37c7408540f30c3e0ea78c7a69f7ddb967574c1c9fbfebcf11fbf25d3bbe07d5

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
3
빌드한 서명 키
3
선언된 환경 rust linux x64 rust rust cargo

검증 실행 환경

환경 컨트랙트 단계 실행일
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

케이스

HOW
목표
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
패키지
심벌
  • winnow::error::ContextError
  • winnow::error::ErrMode
  • winnow::error::InputError
  • winnow::error::TreeError
  • winnow::error::ParseError
  • winnow::combinator::alt
  • winnow::combinator::cut_err
환경
rust
생성일
2026-08-16T12:43:07Z

컨트랙트

  1. 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
  2. assert try_map produces ErrMode::Backtrack rather than Cut on external validation failure so alt continues trying subsequent branches
  3. assert TreeError preserves all failed branches of an alt in a hierarchical Alt structure rather than overwriting them with the last branch
  4. 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
  5. assert Parser::parse erases the ErrMode modal distinction into ParseError and reports the offset of trailing unconsumed input when the inner parser succeeds partially

파일

  • Cargo.lock
  • Cargo.toml
  • NOTES.md
  • csx.json
  • src/lib.rs

소스 아티팩트 내려받기 (tar.gz)

소스

Cargo.lock
# 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",
]
Cargo.toml
[package]
name = "winnow_error_traps"
version = "0.1.0"
edition = "2021"

[dependencies]
winnow = "1.0.4"
NOTES.md
# 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.
csx.json
{"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"}
src/lib.rs
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");
    }
}

오리진 시더

csx-seed