feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
//! # Arithmetic
|
||||
//!
|
||||
//! This parses arithmetic expressions and directly evaluates them.
|
||||
//!
|
||||
//! See also [C-style Expressions][crate::_topic::language#c-style-expressions],
|
||||
//! for a more complex example.
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/arithmetic/parser.rs")]
|
||||
//! ```
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
//! # Custom Errors
|
||||
//!
|
||||
//! A lot can be accomplished with the built-in error tools, like:
|
||||
//! - [`ContextError`]
|
||||
//! - [`Parser::context`]
|
||||
//! - [`cut_err`]
|
||||
//!
|
||||
//! *(see [tutorial][chapter_7])*
|
||||
//!
|
||||
//! Most other needs can likely be met by using a custom context type with [`ContextError`] instead
|
||||
//! of [`StrContext`].
|
||||
//! This will require implementing a custom renderer.
|
||||
//!
|
||||
//! ## `ParserError` Trait
|
||||
//!
|
||||
//! When needed, you can also create your own type that implements [`ParserError`].
|
||||
//!
|
||||
//! Optional traits include:
|
||||
//! - [`AddContext`]
|
||||
//! - [`FromExternalError`]
|
||||
//! - [`ErrorConvert`]
|
||||
//!
|
||||
//! There are multiple strategies for implementing support for [`AddContext`] and [`FromExternalError`]:
|
||||
//! - Make your error type generic over the context or external error
|
||||
//! - Require a trait for the context or external error and `Box` it
|
||||
//! - Make the context an enum like [`StrContext`]
|
||||
//! - Implement the trait multiple times, one for each concrete context or external error type,
|
||||
//! allowing custom behavior per type
|
||||
//!
|
||||
//! Example:
|
||||
//!```rust
|
||||
#![doc = include_str!("../../examples/custom_error.rs")]
|
||||
//!```
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::combinator::cut_err;
|
||||
use crate::error::ContextError;
|
||||
use crate::error::ErrorConvert;
|
||||
use crate::error::StrContext;
|
||||
use crate::Parser;
|
||||
use crate::_tutorial::chapter_7;
|
||||
use crate::error::AddContext;
|
||||
use crate::error::FromExternalError;
|
||||
use crate::error::ParserError;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
//! # Implementing `FromStr`
|
||||
//!
|
||||
//! The [`FromStr` trait][std::str::FromStr] provides
|
||||
//! a common interface to parse from a string.
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/css/parser.rs")]
|
||||
//! ```
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
//! # HTTP
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/http/parser.rs")]
|
||||
//! ```
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
//! # INI
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/ini/parser.rs")]
|
||||
//! ```
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
//! # json
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../examples/json/parser_dispatch.rs")]
|
||||
//! ```
|
||||
Vendored
+341
@@ -0,0 +1,341 @@
|
||||
//! # Elements of Programming Languages
|
||||
//!
|
||||
//! These are short recipes for accomplishing common tasks.
|
||||
//!
|
||||
//! * [Whitespace](#whitespace)
|
||||
//! + [Wrapper combinators that eat whitespace before and after a parser](#wrapper-combinators-that-eat-whitespace-before-and-after-a-parser)
|
||||
//! * [Comments](#comments)
|
||||
//! + [`// C++/EOL-style comments`](#-ceol-style-comments)
|
||||
//! + [`/* C-style comments */`](#-c-style-comments-)
|
||||
//! * [Identifiers](#identifiers)
|
||||
//! + [`Rust-Style Identifiers`](#rust-style-identifiers)
|
||||
//! * [Literal Values](#literal-values)
|
||||
//! + [Escaped Strings](#escaped-strings)
|
||||
//! + [Integers](#integers)
|
||||
//! - [Hexadecimal](#hexadecimal)
|
||||
//! - [Octal](#octal)
|
||||
//! - [Binary](#binary)
|
||||
//! - [Decimal](#decimal)
|
||||
//! + [Floating Point Numbers](#floating-point-numbers)
|
||||
//! * [C-style Expressions](#c-style-expressions)
|
||||
//!
|
||||
//! ## Whitespace
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! ### Wrapper combinators that eat whitespace before and after a parser
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! error::ParserError,
|
||||
//! combinator::delimited,
|
||||
//! ascii::multispace0,
|
||||
//! };
|
||||
//!
|
||||
//! /// A combinator that takes a parser `inner` and produces a parser that also consumes both leading and
|
||||
//! /// trailing whitespace, returning the output of `inner`.
|
||||
//! fn ws<'a, F, O, E: ParserError<&'a str>>(inner: F) -> impl Parser<&'a str, O, E>
|
||||
//! where
|
||||
//! F: Parser<&'a str, O, E>,
|
||||
//! {
|
||||
//! delimited(
|
||||
//! multispace0,
|
||||
//! inner,
|
||||
//! multispace0
|
||||
//! )
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! To eat only trailing whitespace, replace `delimited(...)` with `terminated(&inner, multispace0)`.
|
||||
//! Likewise, the eat only leading whitespace, replace `delimited(...)` with `preceded(multispace0,
|
||||
//! &inner)`. You can use your own parser instead of `multispace0` if you want to skip a different set
|
||||
//! of lexemes.
|
||||
//!
|
||||
//! ## Comments
|
||||
//!
|
||||
//! ### `// C++/EOL-style comments`
|
||||
//!
|
||||
//! This version uses `%` to start a comment, does not consume the newline character, and returns an
|
||||
//! output of `()`.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! error::ParserError,
|
||||
//! token::take_till,
|
||||
//! };
|
||||
//!
|
||||
//! pub fn peol_comment<'a, E: ParserError<&'a str>>(i: &mut &'a str) -> ModalResult<(), E>
|
||||
//! {
|
||||
//! ('%', take_till(1.., ['\n', '\r']))
|
||||
//! .void() // Output is thrown away.
|
||||
//! .parse_next(i)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### `/* C-style comments */`
|
||||
//!
|
||||
//! Inline comments surrounded with sentinel literals `(*` and `*)`. This version returns an output of `()`
|
||||
//! and does not handle nested comments.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! error::ParserError,
|
||||
//! token::take_until,
|
||||
//! };
|
||||
//!
|
||||
//! pub fn pinline_comment<'a, E: ParserError<&'a str>>(i: &mut &'a str) -> ModalResult<(), E> {
|
||||
//! (
|
||||
//! "(*",
|
||||
//! take_until(0.., "*)"),
|
||||
//! "*)"
|
||||
//! )
|
||||
//! .void() // Output is thrown away.
|
||||
//! .parse_next(i)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Identifiers
|
||||
//!
|
||||
//! ### `Rust-Style Identifiers`
|
||||
//!
|
||||
//! Parsing identifiers that may start with a letter (or underscore) and may contain underscores,
|
||||
//! letters and numbers may be parsed like this:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! stream::AsChar,
|
||||
//! token::take_while,
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! pub fn identifier<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! (
|
||||
//! one_of(|c: char| c.is_alpha() || c == '_'),
|
||||
//! take_while(0.., |c: char| c.is_alphanum() || c == '_')
|
||||
//! )
|
||||
//! .take()
|
||||
//! .parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Let's say we apply this to the identifier `hello_world123abc`. The first element of the tuple
|
||||
//! would uses [`one_of`][crate::token::one_of] which would take `h`. The tuple ensures that
|
||||
//! `ello_world123abc` will be piped to the next [`take_while`][crate::token::take_while] parser,
|
||||
//! which takes every remaining character. However, the tuple returns a tuple of the results
|
||||
//! of its sub-parsers. The [`take`][crate::Parser::take] parser produces a `&str` of the
|
||||
//! input text that was parsed, which in this case is the entire `&str` `hello_world123abc`.
|
||||
//!
|
||||
//! ## Literal Values
|
||||
//!
|
||||
//! ### Escaped Strings
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/string/parser.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! See also [`take_escaped`] and [`escaped`].
|
||||
//!
|
||||
//! ### Integers
|
||||
//!
|
||||
//! The following recipes all return string slices rather than integer values. How to obtain an
|
||||
//! integer value instead is demonstrated for hexadecimal integers. The others are similar.
|
||||
//!
|
||||
//! The parsers allow the grouping character `_`, which allows one to group the digits by byte, for
|
||||
//! example: `0xA4_3F_11_28`. If you prefer to exclude the `_` character, the lambda to convert from a
|
||||
//! string slice to an integer value is slightly simpler. You can also strip the `_` from the string
|
||||
//! slice that is returned, which is demonstrated in the second hexadecimal number parser.
|
||||
//!
|
||||
//! #### Hexadecimal
|
||||
//!
|
||||
//! The parser outputs the string slice of the digits without the leading `0x`/`0X`.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::alt,
|
||||
//! combinator::repeat,
|
||||
//! combinator::{preceded, terminated},
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn hexadecimal<'s>(input: &mut &'s str) -> ModalResult<&'s str> { // <'a, E: ParserError<&'a str>>
|
||||
//! preceded(
|
||||
//! alt(("0x", "0X")),
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of(('0'..='9', 'a'..='f', 'A'..='F')), repeat(0.., '_').map(|()| ()))
|
||||
//! ).map(|()| ()).take()
|
||||
//! ).parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you want it to return the integer value instead, use map:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::alt,
|
||||
//! combinator::repeat,
|
||||
//! combinator::{preceded, terminated},
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn hexadecimal_value(input: &mut &str) -> ModalResult<i64> {
|
||||
//! preceded(
|
||||
//! alt(("0x", "0X")),
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of(('0'..='9', 'a'..='f', 'A'..='F')), repeat(0.., '_').map(|()| ()))
|
||||
//! ).map(|()| ()).take()
|
||||
//! ).try_map(
|
||||
//! |out: &str| i64::from_str_radix(&str::replace(&out, "_", ""), 16)
|
||||
//! ).parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! See also [`hex_uint`]
|
||||
//!
|
||||
//! #### Octal
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::alt,
|
||||
//! combinator::repeat,
|
||||
//! combinator::{preceded, terminated},
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn octal<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! preceded(
|
||||
//! alt(("0o", "0O")),
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of('0'..='7'), repeat(0.., '_').map(|()| ()))
|
||||
//! ).map(|()| ()).take()
|
||||
//! ).parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Binary
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::alt,
|
||||
//! combinator::repeat,
|
||||
//! combinator::{preceded, terminated},
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn binary<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! preceded(
|
||||
//! alt(("0b", "0B")),
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of('0'..='1'), repeat(0.., '_').map(|()| ()))
|
||||
//! ).map(|()| ()).take()
|
||||
//! ).parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Decimal
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::repeat,
|
||||
//! combinator::terminated,
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn decimal<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of('0'..='9'), repeat(0.., '_').map(|()| ()))
|
||||
//! ).map(|()| ())
|
||||
//! .take()
|
||||
//! .parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! See also [`dec_uint`] and [`dec_int`]
|
||||
//!
|
||||
//! ### Floating Point Numbers
|
||||
//!
|
||||
//! The following is adapted from [the Python parser by Valentin Lorentz](https://github.com/ProgVal/rust-python-parser/blob/master/src/numbers.rs).
|
||||
//!
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! use winnow::{
|
||||
//! combinator::alt,
|
||||
//! combinator::repeat,
|
||||
//! combinator::opt,
|
||||
//! combinator::{preceded, terminated},
|
||||
//! token::one_of,
|
||||
//! };
|
||||
//!
|
||||
//! fn float<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! alt((
|
||||
//! // Case one: .42
|
||||
//! (
|
||||
//! '.',
|
||||
//! decimal,
|
||||
//! opt((
|
||||
//! one_of(['e', 'E']),
|
||||
//! opt(one_of(['+', '-'])),
|
||||
//! decimal
|
||||
//! ))
|
||||
//! ).take()
|
||||
//! , // Case two: 42e42 and 42.42e42
|
||||
//! (
|
||||
//! decimal,
|
||||
//! opt(preceded(
|
||||
//! '.',
|
||||
//! decimal,
|
||||
//! )),
|
||||
//! one_of(['e', 'E']),
|
||||
//! opt(one_of(['+', '-'])),
|
||||
//! decimal
|
||||
//! ).take()
|
||||
//! , // Case three: 42. and 42.42
|
||||
//! (
|
||||
//! decimal,
|
||||
//! '.',
|
||||
//! opt(decimal)
|
||||
//! ).take()
|
||||
//! )).parse_next(input)
|
||||
//! }
|
||||
//!
|
||||
//! fn decimal<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
|
||||
//! repeat(1..,
|
||||
//! terminated(one_of('0'..='9'), repeat(0.., '_').map(|()| ()))
|
||||
//! ).
|
||||
//! map(|()| ())
|
||||
//! .take()
|
||||
//! .parse_next(input)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! See also [`float`]
|
||||
//!
|
||||
//! ## C-style Expressions
|
||||
//!
|
||||
//! An example using the [`expression()`] parser to build an abstract syntax tree
|
||||
//! for C-style expressions.
|
||||
//!
|
||||
//! The operator precedence level is based on the [C language][c-precedence].
|
||||
//!
|
||||
//! [c-precedence]: https://en.cppreference.com/w/c/language/operator_precedence.html
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/c_expression/parser.rs")]
|
||||
//! ```
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::ascii::dec_int;
|
||||
use crate::ascii::dec_uint;
|
||||
use crate::ascii::escaped;
|
||||
use crate::ascii::float;
|
||||
use crate::ascii::hex_uint;
|
||||
use crate::ascii::take_escaped;
|
||||
use crate::combinator::expression;
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
//! # Lexing and Parsing
|
||||
//!
|
||||
//! ## Parse to AST
|
||||
//!
|
||||
//! The simplest way to write a parser is to parse directly to the AST.
|
||||
//!
|
||||
//! Example:
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/arithmetic/parser_ast.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ## Lexing
|
||||
//!
|
||||
//! However, there are times when you may want to separate lexing from parsing.
|
||||
//! Winnow provides [`TokenSlice`] to simplify this.
|
||||
//!
|
||||
//! Example:
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/arithmetic/parser_lexer.rs")]
|
||||
//! ```
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::stream::TokenSlice;
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
//! # Special Topics
|
||||
//!
|
||||
//! These are short recipes for accomplishing common tasks.
|
||||
//!
|
||||
//! - [Why `winnow`?][why]
|
||||
//! - [For `nom` users][nom]
|
||||
//! - Formats:
|
||||
//! - [Elements of Programming Languages][language]
|
||||
//! - [Arithmetic][arithmetic]
|
||||
//! - [s-expression][s_expression]
|
||||
//! - [json]
|
||||
//! - [INI][ini]
|
||||
//! - [HTTP][http]
|
||||
//! - Special Topics:
|
||||
//! - [Implementing `FromStr`][fromstr]
|
||||
//! - [Performance][performance]
|
||||
//! - [Parsing Partial Input][partial]
|
||||
//! - [Lexing and Parsing][lexing]
|
||||
//! - [Custom stream or token][stream]
|
||||
//! - [Custom errors][error]
|
||||
//! - [Debugging][crate::_tutorial::chapter_8]
|
||||
//!
|
||||
//! See also parsers written with `winnow`:
|
||||
//!
|
||||
//! - [`toml_edit`](https://crates.io/crates/toml_edit)
|
||||
//! - [`hcl-edit`](https://crates.io/crates/hcl-edit)
|
||||
#![allow(clippy::std_instead_of_core)]
|
||||
#![allow(clippy::test_attr_in_doctest)]
|
||||
|
||||
pub mod arithmetic;
|
||||
pub mod error;
|
||||
pub mod fromstr;
|
||||
pub mod http;
|
||||
pub mod ini;
|
||||
pub mod json;
|
||||
pub mod language;
|
||||
pub mod lexing;
|
||||
pub mod nom;
|
||||
pub mod partial;
|
||||
pub mod performance;
|
||||
pub mod s_expression;
|
||||
pub mod stream;
|
||||
pub mod why;
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
//! # For `nom` users
|
||||
//!
|
||||
//! ## Migrating from `nom`
|
||||
//!
|
||||
//! For comparisons with `nom`, see
|
||||
//! - [Why `winnow`][super::why]
|
||||
//! - [parse-rosetta-rs](https://github.com/rosetta-rs/parse-rosetta-rs/)
|
||||
//!
|
||||
//! What approach you take depends on the size and complexity of your parser.
|
||||
//! For small, simple parsers, its likely easiest to directly port from `nom`.
|
||||
//! When trying to look for the equivalent of a `nom` combinator, search in the docs for the name
|
||||
//! of the `nom` combinator. It is expected that, where names diverge, a doc alias exists.
|
||||
//! See also the [List of combinators][crate::combinator].
|
||||
//!
|
||||
//! ### Complex migrations
|
||||
//!
|
||||
//! For larger parsers, it is likely best to take smaller steps
|
||||
//! - Easier to debug when something goes wrong
|
||||
//! - Deprecation messages will help assist through the process
|
||||
//!
|
||||
//! The workflow goes something like:
|
||||
//! 1. Run `cargo rm nom && cargo add winnow@0.3`
|
||||
//! 1. Ensure everything compiles and tests pass, ignoring deprecation messages (see [migration
|
||||
//! notes](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md#nom-migration-guide))
|
||||
//! 1. Commit
|
||||
//! 1. Switch any `impl FnMut(I) -> IResult<I, O, E>` to `impl Parser<I, O, E>`
|
||||
//! 1. Resolve deprecation messages
|
||||
//! 1. Commit
|
||||
//! 1. Run `cargo add winnow@0.4`
|
||||
//! 1. Ensure everything compiles and tests pass, ignoring deprecation messages (see [changelog](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md#040---2023-03-18) for more details)
|
||||
//! 1. Commit
|
||||
//! 1. Resolve deprecation messages
|
||||
//! 1. Commit
|
||||
//! 1. Run `cargo add winnow@0.5`
|
||||
//! 1. Ensure everything compiles and tests pass, ignoring deprecation messages
|
||||
//! (see [migration notes](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md#050---2023-07-13))
|
||||
//! 1. Commit
|
||||
//! 1. Resolve deprecation messages
|
||||
//! 1. Commit
|
||||
//!
|
||||
//! ### Examples
|
||||
//!
|
||||
//! For example migrations, see
|
||||
//! - [git-config-env](https://github.com/gitext-rs/git-config-env/pull/11) (nom to winnow 0.3)
|
||||
//! - [git-conventional](https://github.com/crate-ci/git-conventional/pull/37) (nom to winnow 0.3,
|
||||
//! adds explicit tracing for easier debugging)
|
||||
//! - [typos](https://github.com/crate-ci/typos/pull/664) (nom to winnow 0.3)
|
||||
//! - [cargo-smart-release](https://github.com/Byron/gitoxide/pull/948) (gradual migration from nom
|
||||
//! to winnow 0.5)
|
||||
//! - [gix-config](https://github.com/Byron/gitoxide/pull/951) (gradual migration from nom
|
||||
//! to winnow 0.5)
|
||||
//! - [gix-protocol](https://github.com/Byron/gitoxide/pull/1009) (gradual migration from nom
|
||||
//! to winnow 0.5)
|
||||
//! - [gitoxide](https://github.com/Byron/gitoxide/pull/956) (gradual migration from nom
|
||||
//! to winnow 0.5)
|
||||
//!
|
||||
//! ## Differences
|
||||
//!
|
||||
//! These are key differences to help Nom users adapt to writing parsers with Winnow.
|
||||
//!
|
||||
//! ### Renamed APIs
|
||||
//!
|
||||
//! Names have changed for consistency or clarity.
|
||||
//!
|
||||
//! To find a parser you are looking for,
|
||||
//! - Search the docs for the `nom` parser
|
||||
//! - See the [List of combinators][crate::combinator]
|
||||
//!
|
||||
//! ### GATs
|
||||
//!
|
||||
//! `nom` v8 back-propagates how you will use a parser to parser functions using a language feature
|
||||
//! called GATs.
|
||||
//! Winnow has made the conscious choice not to use this feature, finding alternative ways of
|
||||
//! getting most of the benefits.
|
||||
//!
|
||||
//! Benefits of GATs:
|
||||
//! - Performance as the compiler is able to instantiate copies of a parser that are
|
||||
//! better tailored to how it will be used, like discarding unused allocations for output or
|
||||
//! errors.
|
||||
//!
|
||||
//! Benefits of not using GATs:
|
||||
//! - Predictable performance:
|
||||
//! With GATs, seemingly innocuous changes like choosing to hand write a parser using idiomatic function parsers
|
||||
//! (`fn(&mut I) -> Result<O>`) can cause surprising slow downs because these functions sever the back-propagation from GATs.
|
||||
//! The causes of these slowdowns could be hard to identify by inspection or profiling.
|
||||
//! - No "eek out X% perf improvement" pressure to contort a parser
|
||||
//! that is more easily written imperatively
|
||||
//! to be written declaratively
|
||||
//! so it can preserve the back-propagation from GATs.
|
||||
//! - Built-in parsers serve are can serve as examples to users of idiomatic function parsers
|
||||
//! (`fn(&mut I) -> Result<O>`).
|
||||
//! With GATs, built-in parsers tend to be complex implementations of traits.
|
||||
//! - Faster build times and smaller binary size as parsers only need to be generated for one mode,
|
||||
//! not upto 8
|
||||
//!
|
||||
//! #### Partial/streaming parsers
|
||||
//!
|
||||
//! `nom` v8 back-propagates whether `Parser::parse_complete` was used to select `complete`
|
||||
//! parsers.
|
||||
//! Previously, users had ensure consistently using a parser from the `streaming` or `complete` module.
|
||||
//!
|
||||
//! Instead, you tag the input type (`I`) by wrapping it in [`Partial<I>`] and parsers will adjust
|
||||
//! their behavior accordingly.
|
||||
//! See [partial] special topic.
|
||||
//!
|
||||
//! #### Eliding Output
|
||||
//!
|
||||
//! `nom` v8 back-propagates whether an Output will be used and skips its creation.
|
||||
//! For example, `value(Null, many0(_))` will avoid creating and pushing to a `Vec`.
|
||||
//! Previously, users had to select `count_many0` over `many0` to avoid creating a `Vec`.
|
||||
//!
|
||||
//! Instead, `repeat` returns an `impl Accumulate<T>` which could be a `Vec`, a `usize` for `count`
|
||||
//! variants, or `()` to do no extra work.
|
||||
//!
|
||||
//! #### Eliding Backtracked Errors
|
||||
//!
|
||||
//! Under the hood, [`alt`] is an `if-not-error-else` ladder, see [`_tutorial::chapter_3`].
|
||||
//! nom v8 back-propagates whether the error will be discarded and avoids any expensive work done
|
||||
//! for rich error messages.
|
||||
//!
|
||||
//! Instead, [`ContextError`] and other changes have made it so errors have very little overhead.
|
||||
//! [`dispatch!`] can also be used in some situations to avoid `alt`s overhead.
|
||||
//!
|
||||
//! ### Parsers return [`Stream::Slice`], rather than [`Stream`]
|
||||
//!
|
||||
//! In `nom`, parsers like [`take_while`] parse a [`Stream`] and return a [`Stream`].
|
||||
//! When wrapping the input, like with [`Stateful`],
|
||||
//! you have to unwrap the input to integrate it in your application,
|
||||
//! and it requires [`Stream`] to be `Clone`
|
||||
//! (which requires `RefCell` for mutable external state and can be expensive).
|
||||
//!
|
||||
//! Instead, [`Stream::Slice`] was added to track the intended type for parsers to return.
|
||||
//! If you want to then parse the slice, you then need to take it and turn it back into a
|
||||
//! [`Stream`].
|
||||
//!
|
||||
//! ### `&mut I`
|
||||
//!
|
||||
//! `winnow` switched from pure-function parser (`Fn(I) -> (I, O)` to `Fn(&mut I) -> O`).
|
||||
//! On error, `i` is left pointing at where the error happened.
|
||||
//!
|
||||
//! Benefits of `Fn(&mut I) -> O`:
|
||||
//! - Cleaner code: Removes need to pass `i` everywhere and makes changes to `i` more explicit
|
||||
//! - Correctness: No forgetting to chain `i` through a parser
|
||||
//! - Flexibility: `I` does not need to be `Copy` or even `Clone`. For example, [`Stateful`] can use `&mut S` instead of `RefCell<S>`.
|
||||
//! - Performance: `Result::Ok` is smaller without `i`, reducing the risk that the output will be
|
||||
//! returned on the stack, rather than the much faster CPU registers.
|
||||
//! `Result::Err` can also be smaller because the error type does not need to carry `i` to point
|
||||
//! to the error.
|
||||
//! See also [#72](https://github.com/winnow-rs/winnow/issues/72).
|
||||
//!
|
||||
//! Benefits of `Fn(I) -> (I, O)`:
|
||||
//! - Pure functions can be easier to reason about
|
||||
//! - Less boilerplate in some situations (see below)
|
||||
//! - Less syntactic noise in some situations (see below)
|
||||
//!
|
||||
//! When returning a slice from the input, you have to add a lifetime:
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! fn foo<'i>(i: &mut &'i str) -> ModalResult<&'i str> {
|
||||
//! # Ok("")
|
||||
//! // ...
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! When writing a closure, you need to annotate the type
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! # use winnow::combinator::alt;
|
||||
//! # use winnow::error::ContextError;
|
||||
//! # let mut input = "";
|
||||
//! # fn foo<'i>() -> impl ModalParser<&'i str, &'i str, ContextError> {
|
||||
//! alt((
|
||||
//! |i: &mut _| {
|
||||
//! # Ok("")
|
||||
//! // ...
|
||||
//! },
|
||||
//! |i: &mut _| {
|
||||
//! # Ok("")
|
||||
//! // ...
|
||||
//! },
|
||||
//! ))
|
||||
//! # }
|
||||
//! ```
|
||||
//! *(at least the full type isn't needed)*
|
||||
//!
|
||||
//! To save and restore from intermediate states, [`Stream::checkpoint`] and [`Stream::reset`] can help:
|
||||
//! ```rust
|
||||
//! use winnow::prelude::*;
|
||||
//! # let mut i = "";
|
||||
//! # let i = &mut i;
|
||||
//!
|
||||
//! let start = i.checkpoint();
|
||||
//! // ...
|
||||
//! i.reset(&start);
|
||||
//! ```
|
||||
//!
|
||||
//! When the Output of a parser is a slice, you have to add a lifetime:
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! fn foo<'i>(i: &mut &'i str) -> ModalResult<&'i str> {
|
||||
//! // ...
|
||||
//! # winnow::token::rest.parse_next(i)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! When writing a closure, you need to annotate the type:
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! # use winnow::combinator::trace;
|
||||
//! fn foo(i: &mut &str) -> ModalResult<usize> {
|
||||
//! trace("foo", |i: &mut _| {
|
||||
//! // ...
|
||||
//! # Ok(0)
|
||||
//! }).parse_next(i)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Optional [`ErrMode`]
|
||||
//!
|
||||
//! Called `Err` in `nom`, [`ErrMode`] is responsible for
|
||||
//! - Deciding whether to backtrack and try another branch in cases like `alt` or report back to
|
||||
//! the error back to users
|
||||
//! - Tracking incomplete input on partial parsing
|
||||
//!
|
||||
//! As this isn't needed in every parser, it was made optional. [`ModalResult`] is a convenience
|
||||
//! type for using [`ErrMode`].
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::_topic::partial;
|
||||
use crate::_tutorial;
|
||||
use crate::combinator::alt;
|
||||
use crate::combinator::dispatch;
|
||||
use crate::error::ContextError;
|
||||
use crate::error::ErrMode;
|
||||
use crate::error::ModalResult;
|
||||
use crate::stream::Accumulate;
|
||||
use crate::stream::Partial;
|
||||
use crate::stream::Stateful;
|
||||
use crate::stream::Stream;
|
||||
use crate::token::take_while;
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
//! # Parsing Partial Input
|
||||
//!
|
||||
//! Typically, the input being parsed is all in-memory, or is complete. Some data sources are too
|
||||
//! large to fit into memory, only allowing parsing an incomplete or [`Partial`] subset of the
|
||||
//! data, requiring incrementally parsing.
|
||||
//!
|
||||
//! By wrapping a stream, like `&[u8]`, with [`Partial`], parsers will report when the data is
|
||||
//! [`Incomplete`] and more input is [`Needed`], allowing the caller to stream-in additional data
|
||||
//! to be parsed. The data is then parsed a chunk at a time.
|
||||
//!
|
||||
//! Chunks are typically defined by either:
|
||||
//! - A header reporting the number of bytes, like with [`length_and_then`]
|
||||
//! - [`Partial`] can explicitly be changed to being complete once the specified bytes are
|
||||
//! acquired via [`StreamIsPartial::complete`].
|
||||
//! - A delimiter, like with [ndjson](https://github.com/ndjson/ndjson-spec/)
|
||||
//! - You can parse up-to the delimiter or do a `take_until(0.., delim).and_then(parser)`
|
||||
//!
|
||||
//! If the chunks are not homogeneous, a state machine will be needed to track what the expected
|
||||
//! parser is for the next chunk.
|
||||
//!
|
||||
//! Caveats:
|
||||
//! - `winnow` takes the approach of re-parsing from scratch. Chunks should be relatively small to
|
||||
//! prevent the re-parsing overhead from dominating.
|
||||
//! - Parsers like [`repeat`] do not know when an `eof` is from insufficient data or the end of the
|
||||
//! stream, causing them to always report [`Incomplete`].
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! `main.rs`:
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../examples/ndjson/main.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! `parser.rs`:
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../examples/ndjson/parser.rs")]
|
||||
//! ```
|
||||
|
||||
#![allow(unused_imports)] // Used for intra-doc links
|
||||
|
||||
use crate::binary::length_and_then;
|
||||
use crate::combinator::repeat;
|
||||
use crate::error::ErrMode::Incomplete;
|
||||
use crate::error::Needed;
|
||||
use crate::stream::Partial;
|
||||
use crate::stream::StreamIsPartial;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
//! # Performance
|
||||
//!
|
||||
//! ## Runtime Performance
|
||||
//!
|
||||
//! See also the general Rust [Performance Book](https://nnethercote.github.io/perf-book/)
|
||||
//!
|
||||
//! Tips
|
||||
//! - Try `cargo add winnow -F simd`. For some it offers significant performance improvements
|
||||
//! - When enough cases of an [`alt`] have unique prefixes, prefer [`dispatch`]
|
||||
//! - When parsing text, try to parse as bytes (`u8`) rather than `char`s ([`BStr`] can make
|
||||
//! debugging easier)
|
||||
//! - Find simplified subsets of the grammar to parse, falling back to the full grammar when it
|
||||
//! doesn't work. For example, when parsing json strings, parse them without support for escapes,
|
||||
//! falling back to escape support if it fails.
|
||||
//! - Watch for large return types. A surprising place these can show up is when chaining parsers
|
||||
//! with a tuple.
|
||||
//!
|
||||
//! ## Build-time Performance
|
||||
//!
|
||||
//! Returning complex types as `impl Trait` can negatively impact build times. This can hit in
|
||||
//! surprising cases like:
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! fn foo<I, O, E>() -> impl Parser<I, O, E>
|
||||
//! # where
|
||||
//! # I: winnow::stream::Stream<Token=O>,
|
||||
//! # I: winnow::stream::StreamIsPartial,
|
||||
//! # E: winnow::error::ParserError<I>,
|
||||
//! {
|
||||
//! // ...some chained combinators...
|
||||
//! # winnow::token::any
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Instead, wrap the combinators in a closure to simplify the type:
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! fn foo<I, O, E>() -> impl Parser<I, O, E>
|
||||
//! # where
|
||||
//! # I: winnow::stream::Stream<Token=O>,
|
||||
//! # I: winnow::stream::StreamIsPartial,
|
||||
//! # E: winnow::error::ParserError<I>,
|
||||
//! {
|
||||
//! move |input: &mut I| {
|
||||
//! // ...some chained combinators...
|
||||
//! # winnow::token::any
|
||||
//! .parse_next(input)
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::combinator::alt;
|
||||
use crate::combinator::dispatch;
|
||||
use crate::stream::BStr;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
//! # s-expression
|
||||
//!
|
||||
//! ```rust
|
||||
#![doc = include_str!("../../examples/s_expression/parser.rs")]
|
||||
//! ```
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
//! # Custom [`Stream`]
|
||||
//!
|
||||
//! `winnow` is batteries included with support for
|
||||
//! - Basic inputs like `&str`, newtypes with
|
||||
//! - Improved debug output like [`Bytes`]
|
||||
//! - [`Stateful`] for passing state through your parser, like tracking recursion
|
||||
//! depth
|
||||
//! - [`LocatingSlice`] for looking up the absolute position of a token
|
||||
//!
|
||||
//! ## Implementing a custom token
|
||||
//!
|
||||
//! The first level of customization is parsing [`&[MyItem]`][Stream#impl-Stream-for-%26%5BT%5D]
|
||||
//! or [`TokenSlice<MyItem>`].
|
||||
//!
|
||||
//! The basic traits you may want for a custom token type are:
|
||||
//!
|
||||
//! | trait | usage |
|
||||
//! |---|---|
|
||||
//! | [`AsChar`] |Transforms common types to a char for basic token parsing|
|
||||
//! | [`ContainsToken`] |Look for the token in the given set|
|
||||
//!
|
||||
//! See also [`TokenSlice<MyItem>`], [lexing].
|
||||
//!
|
||||
//! ## Implementing a custom stream
|
||||
//!
|
||||
//! Let's assume we have an input type we'll call `MyStream`.
|
||||
//! `MyStream` is a sequence of `MyItem` tokens.
|
||||
//!
|
||||
//! The goal is to define parsers with this signature: `&mut MyStream -> ModalResult<Output>`.
|
||||
//! ```rust
|
||||
//! # use winnow::prelude::*;
|
||||
//! # type MyStream<'i> = &'i str;
|
||||
//! # type Output<'i> = &'i str;
|
||||
//! fn parser<'s>(i: &mut MyStream<'s>) -> ModalResult<Output<'s>> {
|
||||
//! "test".parse_next(i)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Like above, you'll need to implement the related token traits for `MyItem`.
|
||||
//!
|
||||
//! The traits you may want to implement for `MyStream` include:
|
||||
//!
|
||||
//! | trait | usage |
|
||||
//! |---|---|
|
||||
//! | [`Stream`] |Core trait for driving parsing|
|
||||
//! | [`StreamIsPartial`] | Marks the input as being the complete buffer or a partial buffer for streaming input |
|
||||
//! | [`AsBytes`] |Casts the input type to a byte slice|
|
||||
//! | [`AsBStr`] |Casts the input type to a slice of ASCII / UTF-8-like bytes|
|
||||
//! | [`Compare`] |Character comparison operations|
|
||||
//! | [`FindSlice`] |Look for a substring in self|
|
||||
//! | [`Location`] |Calculate location within initial input|
|
||||
//! | [`Offset`] |Calculate the offset between slices|
|
||||
//!
|
||||
//! And for `&[MyItem]` (slices returned by [`Stream`]):
|
||||
//!
|
||||
//! | trait | usage |
|
||||
//! |---|---|
|
||||
//! | [`SliceLen`] |Calculate the input length|
|
||||
//! | [`ParseSlice`] |Used to integrate `&str`'s `parse()` method|
|
||||
|
||||
#![allow(unused_imports)] // Here for intra-dock links
|
||||
use super::lexing;
|
||||
use crate::stream::*;
|
||||
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
//! # Why `winnow`?
|
||||
//!
|
||||
//! To answer this question, it will be useful to contrast this with other approaches to parsing.
|
||||
//!
|
||||
//! <div class="warning">
|
||||
//!
|
||||
//! **Note:** This will focus on principles and priorities. For a deeper and wider
|
||||
//! comparison with other Rust parser libraries, see
|
||||
//! [parse-rosetta-rs](https://github.com/rosetta-rs/parse-rosetta-rs).
|
||||
//!
|
||||
//! </div>
|
||||
//!
|
||||
//! ## Hand-written parsers
|
||||
//!
|
||||
//! Typically, a hand-written parser gives you the flexibility to get
|
||||
//! - Fast parse performance
|
||||
//! - Fast compile-time
|
||||
//! - Small binary sizes
|
||||
//! - High quality error message
|
||||
//! - Fewer dependencies to audit
|
||||
//!
|
||||
//! However, this comes at the cost of doing it all yourself, including
|
||||
//! - Optimizing for each of the above characteristics you care about
|
||||
//! - Ensuring the safety of any `unsafe` code (buffer overflows being a common bug with parsers)
|
||||
//! - Being aware of, familiar with, and correctly implement the relevant algorithms.
|
||||
//! matklad, who has written two rust compile frontends, commented
|
||||
//! ["I’ve implemented a production-grade Pratt parser once, but I no longer immediately understand that code :-)"](https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html)
|
||||
//!
|
||||
//! This approach works well if:
|
||||
//! - Your format is small and is unlikely to change
|
||||
//! - Your format is large but you have people who can focus solely on parsing, like with large
|
||||
//! programming languages
|
||||
//!
|
||||
//! ## `winnow`
|
||||
//!
|
||||
//! Unlike traditional programming language parsers that use
|
||||
//! [lex](https://en.wikipedia.org/wiki/Lex_(software)) or
|
||||
//! [yacc](https://en.wikipedia.org/wiki/Yacc), you can think of `winnow` as a general version of
|
||||
//! the helpers you would create along the way to writing a hand-written parser.
|
||||
//!
|
||||
//! `winnow` includes support for:
|
||||
//! - Zero-copy parsing
|
||||
//! - [Parse traces][trace] for easier debugging
|
||||
//! - [Streaming parsing][Partial] for network communication or large file
|
||||
//! - [Stateful] parsers
|
||||
//!
|
||||
//! For binary formats, `winnow` includes:
|
||||
//! - [A hexadecimal view][crate::Bytes] in [trace]
|
||||
//! - [TLV](https://en.wikipedia.org/wiki/Type-length-value) (e.g. [`length_take`])
|
||||
//! - Some common parsers to help get started, like numbers
|
||||
//!
|
||||
//! For text formats, `winnow` includes:
|
||||
//! - [Tracking of spans][crate::LocatingSlice]
|
||||
//! - [A textual view when parsing as bytes][crate::BStr] in [trace]
|
||||
//! - Ability to evaluate directly, parse to an AST, or lex and parse the format
|
||||
//!
|
||||
//! This works well for:
|
||||
//! - Prototyping for what will be a hand-written parser
|
||||
//! - When you want to minimize the work to evolve your format
|
||||
//! - When you don't have contributors focused solely on parsing and your grammar is large enough
|
||||
//! to be unwieldy to hand write.
|
||||
//!
|
||||
//! ## `nom`
|
||||
//!
|
||||
//! `winnow` is a fork of the venerable [`nom`](https://crates.io/crates/nom). The difference
|
||||
//! between them is largely in priorities. `nom` prioritizes:
|
||||
//! - Lower churn for existing users while `winnow` is trying to find ways to make things better
|
||||
//! for the parsers yet to be written.
|
||||
//! - Having a small core, relying on external crates like
|
||||
//! [`nom-locate`](https://crates.io/crates/nom_locate) and
|
||||
//! [`nom-supreme`](https://crates.io/crates/nom-supreme), encouraging flexibility among users
|
||||
//! and to not block users on new features being merged while `winnow` aims to include all the
|
||||
//! fundamentals for parsing to ensure the experience is cohesive and high quality.
|
||||
//!
|
||||
//! For more details, see the [design differences][super::nom#api-differences].
|
||||
//!
|
||||
//! See also our [nom migration guide][super::nom#migrating-from-nom].
|
||||
//!
|
||||
//! ## `chumsky`
|
||||
//!
|
||||
//! [`chumsky`](https://crates.io/crates/chumsky) is an up and coming parser-combinator library
|
||||
//! that includes advanced features like error recovery.
|
||||
//!
|
||||
//! Probably the biggest diverging philosophy is `chumsky`s stance:
|
||||
//!
|
||||
//! > "If you need to implement either `Parser` or `Strategy` by hand, that's a problem that needs fixing".
|
||||
//!
|
||||
//! This is under "batteries included" but it also ties into the feeling that `chumsky` acts more like
|
||||
//! a framework. Instead of composing together helpers, you are expected to do everything through
|
||||
//! their system to the point that it is non-trivial to implement their `Parser` trait and are
|
||||
//! encouraged to use the
|
||||
//! [`custom`](https://docs.rs/chumsky/0.9.0/chumsky/primitive/fn.custom.html) helper. This
|
||||
//! requires re-framing everything to fit within their model and makes the code harder to understand
|
||||
//! and debug as you are working with abstract operations that will eventually be applied
|
||||
//! rather than directly with the parsers.
|
||||
//!
|
||||
//! In contrast, `winnow` is an introspectable toolbox that can easily be customized at any level.
|
||||
//! Probably the biggest thing that `winnow` loses out on is optimizations from ["parse modes" via
|
||||
//! GATs](https://github.com/zesterer/chumsky/pull/82) which allows downstream parsers to tell
|
||||
//! upstream parsers when information will be discarded, allowing bypassing expensive operations,
|
||||
//! like allocations. This requires a lot more complex interaction with parsers that isn't as
|
||||
//! trivial to do with bare functions which would lose out on any of that side-band information.
|
||||
//! Instead, we work around this with things like the [`Accumulate`] trait.
|
||||
|
||||
#![allow(unused_imports)]
|
||||
use crate::binary::length_take;
|
||||
use crate::combinator::trace;
|
||||
use crate::stream::Accumulate;
|
||||
use crate::stream::Partial;
|
||||
use crate::stream::Stateful;
|
||||
Reference in new issue
Block a user