My first crate :D, floop: A more convenient and less error prone replacement for loop `{ select! { .. }}`

i was trying to write async code in another project and quickly got frustrated with loop select and the lack of better alternatives,
so i created floop, floop does not require futures to be cancel-safe, fused, or unpin
and supports no-std, no-alloc, and all runtimes (it only uses async, await, and the future traits/types from core).

when a arm finishes only that arm's future is reconstructed,
the other futures are left unchanged and not canceled.

accidentally using a reference to a future instead of a new future, which would be a footgun,
is detected at compile time using const autoref specialization.

if conditions, if let conditions, and break (breaks the specific arm, not the entire loop, the loop breaks once all arms broke) are also supported.

floop should also be rust-analyzer friendly (in most cases, according to my testing.), and does not use synn.

crate: crates.io: Rust Package Registry
docs: floop - Rust
repo: miroo/floop: A more convenient and less error prone replacement for loop `{ select! { .. }}` - Codeberg.org

the code, comments, docs, readme, etc are all written by me, a human, i do not use LLMs

example from the docs:

// NOTE: this just showcases the syntax, the code is mostly nonsense.
// the loop is generated by floop so you just write `floop! { ... }`,
// **not** `loop { floop! { ... }}`
floop! {
    // you need to specify whether `floop` should be `biased` or `unbiased`.
    unbiased
    foo in timer::every(Duration::from_millis(40)) => println!("tick"),
    after => at_the_end_of_loop(),
    // receive doesn't need to be cancel-safe, nothing does
    (bar, baz) in if should_receive_messages(), receive() => {}
}

@tguichaoua
i can't reply to Is tokio's `select!` cancel safe? anymore as its closed, but i just released a crate (floop), which doesn't entirely solve your issue, but could help, but it would require the functions to be combined (floop always loops, so it couldn't be used in foo)

// floop doesn't require cancel-safety, so `hoo` and `goo` are also allowed to be none-cancel-safe.
floop! {
    _ = sleep(Duration::from_secs(1)) => { /* ... */ }
    _ = goo() => { /* ... */ }
    _ = hoo() => { /* ... */ }
}

you can also have

floop! {
    _ = sleep(Duration::from_secs(1)) => { /* ... */ }
    // note that while floop doesn't cancel futures it doesn't prevent other futures from
    // cancelling futures.
    _ = any_future_you_want_eg_foo() => { /* ... *}
}

allowing parts of a floop invocation to be split into individual functions that can be reused by multiple different floops (which would allow a 1-to-1 translation of your example without the requirement for any future to be cancel-safe) sounds like a amazing feature idea in general, thanks for the inspiration, i hope i'll manage to implement it.
should i ping you again if/when i implement floop splitting?

turns out return in a async block breaks the block, not outer function,
i just published v2.0.0, which adds the option to support return at the cost of the future immediately being await (the behaviour of non-returning floop is unchanged).

Bizarre, I would have expected a compile error, but I guess it made sense in the context of the || async {} days

(just my theory, i'm not very well versed in the history of async)
i think the reason is the desugaring of async fns,

async fn foo() -> i32 {
    // imagine there's more code making it impossible to just replace the return with a tail expr.
    return 5;
}

should desugar into

fn foo() -> impl Future<Output = i32> {
    async {
        return 5;
    }
}

so return not breaking the closest async block would make async fn's special and impossible to desugar (unless you add a label to the block and replace return with break 'label_name, but that's way too complex and requires invasive changes to all code within the function, which is not the case for any desugering i'm aware of), which would be very frustrating when you need to turn a async fn into a -> impl Future fn.

in my opinion the main issue is the name async block, not the function, async blocks are closer to closures than blocks which is confusing (they can literally capture variables like closures, which i wouldn't expect from something with block in the name).

I guess the main take away is that there are always exactly 2 hard problems in computer science:

    1. naming things
    1. off by 1 errors
    1. concurrency

I mean if you compare it to rewriting a for loop:

for item in list { body(item) }

to (even ignoring unique name generation + fully qualified calls):

{
  let mut it = list.into_iter();
  loop {
    match it.next() {
      None => break, 
      Some(item) => body(item),
    }
  }
}

simply running a visit that replaces return with a labeled break up to declarations and lambdas seems pretty simple.

Simplifying manual rewriting is similar to the || async {} thing I mentioned, so it's possible but it doesn't quite seem right: if you do anything actually with that like wrapping it in a spawn() it instantly stops making sense.

My actual best guess now I've thought about it is it matches with the behavior of async blocks not starting until polled, which despite the name, makes them behave more like FnOnce() than a loop body.

Interestingly, the initial async-await RFC banned both break and return in async blocks and left them as unresolved questions, so there should be a ticket describing what the resolution was there, but I can't see it immediately from the tracking issues...

i found some of the discussions:

i went through every message after those containing return those with Ctrl-F and found nothing, my theory is that the question was never formally resolved.

Yeah, looks like it won by default because that's just what the implementor picked.

A bit unfortunate for macro cases like yours, but as someone points out you already have that with inner loops (eg retry until success).

One hack to at least catch "bad" returns (or breaks with a little more work, no luck on continues) is to expect a "private" type a caller can't provide, though since these are macros the best you can do is a fresh type that hygiene is hiding.

yep, that's what i ended up doing in the fix, due to hygiene issues (types can't be hidden) i do the following:
declare the type,
store a closure that constructs the type in a hygienic variable,
use the type once in a type annotation,
redeclare the type,
call user code,
use the closure to construct the type.

i never though of testing continue, there's a high chance that it just does nonsense no one would expect.
blocking continue isn't actually that difficult: Rust Playground.
break is already caught using worse hacks.

Neat! Yeah, I can never remember the mixed hygiene rules.

me neither,
to mitigate mistakes i try to put __floop in front of nearly everything that could be in scope from the perspective of user code, even if i expect it to be hygienic.

It was kinda resolved over some lang meetings, maybe. With a dash of "we gotta ship this", perhaps. (Finding the lang meetings may clarify more, but that's where I stopped digging.)

it's way too late but i finally found and fixed a bug that causes a infinite loop when using specific futures (the only example i know of are smol channels, none of my tests used channels) in a return floop,
i published 0.2.1 with the fix, it also fixed return floop with no arms not awaiting the resulting future and made all atomic reads/writes (floop uses a atomic bool for return) SeqCst as a precaution.

turns out i accidentally broke SemVer because return floop now correctly awaits the empty future it it creates instead of directly returning it.
i'm not sure what to do but i'm inclined to do nothing as 0.2.0 has 12 downloads at the moment making it unlikely that the breakage affects anyone (floop without any arms is useless so you wouldn't intentionally write it) and trying to fix it just has the possibility of causing more problems and costing more time.

i fixed rust-analyzer suggesting the wrong brackets using this solution.
the fix has been published as 0.2.2

i fixed continue, it just works as expected now.
it's published as 0.3.0 (breaking change because continue in the old version would rerun the arm it was called in, so updating may change the behaviour of your code and/or cause compiler errors).