Skip to content

Implement default methods for io::Empty and io::Sink #137051

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Mar 20, 2025

Conversation

thaliaarchi
Copy link
Contributor

@thaliaarchi thaliaarchi commented Feb 14, 2025

Implements default methods of io::Read, io::BufRead, and io::Write for io::Empty and io::Sink. These implementations are equivalent to the defaults, except in doing less unnecessary work.

Read::read_to_string and BufRead::read_line both have a redundant call to str::from_utf8 which can't be inlined from core and Write::write_all_vectored has slicing logic which can't be simplified (See on Compiler Explorer). The rest are optimized to the minimal with -C opt-level=3, but this PR gives that benefit to unoptimized builds.

This includes an implementation of Write::write_fmt which just ignores the fmt::Arguments<'_>. This could be problematic whenever a user formatting impl is impure, but the docs do not guarantee that the args will be expanded.

Tracked in #136756.

r? @m-ou-se

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Feb 14, 2025
@thaliaarchi thaliaarchi force-pushed the io-optional-impls/empty branch from 854723e to bb69541 Compare February 14, 2025 23:37
@m-ou-se
Copy link
Member

m-ou-se commented Feb 19, 2025

Would it be a valid implementation of Write::write_fmt if the fmt::Arguments<'_> were just ignored? This would be problematic whenever a user formatting impl is impure. But is it actually guaranteed that the args will be expanded?

As far as I know, we technically don't guarantee that the write_fmt method uses the fmt::Arguments exactly once. Skipping it entirely when the output is ignored anyway seems entirely reasonable to me.

ping @rust-lang/libs-api - Please speak up if you don't think it's okay that write!(io::sink(), "{x}") just skips calling Display::fmt on x.

@m-ou-se m-ou-se added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 19, 2025
@thaliaarchi thaliaarchi force-pushed the io-optional-impls/empty branch from bb69541 to 8250dbe Compare February 19, 2025 17:54
@thaliaarchi
Copy link
Contributor Author

I've added nop Write::write_fmt.

@rustbot label -S-waiting-on-author +S-waiting-on-review

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Feb 19, 2025
@rust-log-analyzer

This comment has been minimized.

@thaliaarchi
Copy link
Contributor Author

thaliaarchi commented Feb 19, 2025

Test write-fmt-error expects a panic for Sink::write_fmt, given an Err-returning fmt impl:

let res = catch_unwind(|| write!(sink(), "{} {} {}", 1, ErrorDisplay, "bar"));
let err = res.expect_err("formatter error did not lead to panic").downcast::<&str>().unwrap();
assert!(
err.contains("formatting trait implementation returned an error"),
"unexpected panic: {}", err
);

Should I change the test or revert the write_fmt changes?

@m-ou-se
Copy link
Member

m-ou-se commented Feb 20, 2025

I don't think the purpose of that test is to test the behaviour of io::sink() specifically. It should be fine to change it to something that actually writes. (E.g. &mut Vec<u8>.)

@thaliaarchi thaliaarchi force-pushed the io-optional-impls/empty branch 2 times, most recently from 6e053fc to d846e72 Compare February 20, 2025 22:55
@thaliaarchi
Copy link
Contributor Author

thaliaarchi commented Feb 20, 2025

I've added tests for the new impls.

I also decided to keep Empty::is_read_vectored as the default false. I see it this way: Empty has nothing to read, so we don't need to read that nothing in larger batches; Sink can write large amounts cheaply, so we should batch those writes as large as possible.

In the standard library, only Chain<T, U> non-trivially uses is_read_vectored. It returns true when either of its inner Readers returns true. With Empty in a chain, if Empty::is_read_vectored() == true, it would force the other to use vectored reads:

rust/library/std/src/io/mod.rs

Lines 2729 to 2731 in f04bbc6

fn is_read_vectored(&self) -> bool {
self.first.is_read_vectored() || self.second.is_read_vectored()
}

This could be somewhat improved by making it conditional on whether the first has finished (but then constant propagation is probably worsened):

fn is_read_vectored(&self) -> bool {
    if !self.done_first { self.first.is_read_vectored() } else { self.second.is_read_vectored() }
}

In the standard library, only BufWriter<W> and LineWriterShim<'a, W> non-trivially use is_write_vectored. They select a writing strategy based on the inner writer's is_write_vectored. With Sink::is_write_vectored() == true (as currently), it does less work, because it batches more (nop) writes together. But, vectored or not, a buffered sink is admittedly dumb.

@thaliaarchi
Copy link
Contributor Author

@m-ou-se Hey, friendly 2-week ping. Would you mind taking a look at the changes since your last review?

@thaliaarchi thaliaarchi force-pushed the io-optional-impls/empty branch from d846e72 to 24978bd Compare March 10, 2025 08:37
Eliminate any redundant, unobservable logic from the their default
method implementations.

The observable changes are that `Write::write_fmt` for both types now
ignores the formatting arguments, so a user fmt impl which has side
effects is not invoked, and `Write::write_all_vectored` for both types
does not advance the borrowed buffers. Neither behavior is guaranteed by
the docs and the latter is documented as unspecified.

`Empty` is not marked as vectored, so that `Chain<Empty, _>` and
`Chain<_, Empty>` are not forced to be vectored.
@m-ou-se
Copy link
Member

m-ou-se commented Mar 18, 2025

@bors r+

@bors
Copy link
Collaborator

bors commented Mar 18, 2025

📌 Commit 523b9d9 has been approved by m-ou-se

It is now in the queue for this repository.

@bors
Copy link
Collaborator

bors commented Mar 18, 2025

🌲 The tree is currently closed for pull requests below priority 100. This pull request will be tested once the tree is reopened.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 18, 2025
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 19, 2025
…iaskrgr

Rollup of 7 pull requests

Successful merges:

 - rust-lang#135394 (`MaybeUninit` inherent slice methods part 2)
 - rust-lang#137051 (Implement default methods for `io::Empty` and `io::Sink`)
 - rust-lang#138001 (mir_build: consider privacy when checking for irrefutable patterns)
 - rust-lang#138540 (core/slice: Mark some `split_off` variants unstably const)
 - rust-lang#138589 (If a label is placed on the block of a loop instead of the header, suggest moving it to the header.)
 - rust-lang#138594 (Fix next solver handling of shallow trait impl check)
 - rust-lang#138613 (Remove E0773 "A builtin-macro was defined more than once.")

Failed merges:

 - rust-lang#138602 (Slim `rustc_parse_format` dependencies down)

r? `@ghost`
`@rustbot` modify labels: rollup
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 19, 2025
…iaskrgr

Rollup of 7 pull requests

Successful merges:

 - rust-lang#135394 (`MaybeUninit` inherent slice methods part 2)
 - rust-lang#137051 (Implement default methods for `io::Empty` and `io::Sink`)
 - rust-lang#138001 (mir_build: consider privacy when checking for irrefutable patterns)
 - rust-lang#138540 (core/slice: Mark some `split_off` variants unstably const)
 - rust-lang#138589 (If a label is placed on the block of a loop instead of the header, suggest moving it to the header.)
 - rust-lang#138594 (Fix next solver handling of shallow trait impl check)
 - rust-lang#138613 (Remove E0773 "A builtin-macro was defined more than once.")

Failed merges:

 - rust-lang#138602 (Slim `rustc_parse_format` dependencies down)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit ce76292 into rust-lang:master Mar 20, 2025
6 checks passed
@rustbot rustbot added this to the 1.87.0 milestone Mar 20, 2025
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Mar 20, 2025
Rollup merge of rust-lang#137051 - thaliaarchi:io-optional-impls/empty, r=m-ou-se

Implement default methods for `io::Empty` and `io::Sink`

Implements default methods of `io::Read`, `io::BufRead`, and `io::Write` for `io::Empty` and `io::Sink`. These implementations are equivalent to the defaults, except in doing less unnecessary work.

`Read::read_to_string` and `BufRead::read_line` both have a redundant call to `str::from_utf8` which can't be inlined from `core` and `Write::write_all_vectored` has slicing logic which can't be simplified (See on [Compiler Explorer](https://rust.godbolt.org/z/KK6xcrWr4)). The rest are optimized to the minimal with `-C opt-level=3`, but this PR gives that benefit to unoptimized builds.

This includes an implementation of `Write::write_fmt` which just ignores the `fmt::Arguments<'_>`. This could be problematic whenever a user formatting impl is impure, but the docs do not guarantee that the args will be expanded.

Tracked in rust-lang#136756.

r? `@m-ou-se`
@thaliaarchi thaliaarchi deleted the io-optional-impls/empty branch March 20, 2025 00:55
github-actions bot pushed a commit to model-checking/verify-rust-std that referenced this pull request Apr 2, 2025
…y, r=m-ou-se

Implement default methods for `io::Empty` and `io::Sink`

Implements default methods of `io::Read`, `io::BufRead`, and `io::Write` for `io::Empty` and `io::Sink`. These implementations are equivalent to the defaults, except in doing less unnecessary work.

`Read::read_to_string` and `BufRead::read_line` both have a redundant call to `str::from_utf8` which can't be inlined from `core` and `Write::write_all_vectored` has slicing logic which can't be simplified (See on [Compiler Explorer](https://rust.godbolt.org/z/KK6xcrWr4)). The rest are optimized to the minimal with `-C opt-level=3`, but this PR gives that benefit to unoptimized builds.

This includes an implementation of `Write::write_fmt` which just ignores the `fmt::Arguments<'_>`. This could be problematic whenever a user formatting impl is impure, but the docs do not guarantee that the args will be expanded.

Tracked in rust-lang#136756.

r? `@m-ou-se`
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants