floop 0.1.0

A more convenient and less error prone replacement for loop `{ select! { .. }}`
Documentation
use std::iter;

use unsynn::*;
use footgun_detector::footgun_detector;

use crate::parser::Ast;

mod footgun_detector;

fn hygenic_ident(str: &str) -> Ident {
    Ident::new(str, Span::mixed_site())
}

pub(crate) fn generate_code(ast: Ast) -> TokenStream {
    let mut tokens = TokenStream::new();
    let future_name = |i| hygenic_ident(&format!("__floop_future_{i}"));
    let pin_name = |i| hygenic_ident(&format!("__floop_pin_{i}"));
    let result_variant_name = |i| hygenic_ident(&format!("Variant{i}"));
    let result = hygenic_ident("__FloopResult");
    let output_name = |i| hygenic_ident(&format!("__floop_output{i}"));
    let unbiased_offset = hygenic_ident("__floop_unbiased_offset");
    let before = ast.before.as_ref().map(|arm| arm.block()).unwrap_or_default();
    let after = ast.after.as_ref().map(|arm| arm.block()).unwrap_or_default();
    let footgun_detector = footgun_detector();
    // this sadly isn't hygenic yet so more work arounds are used to prevent code from creating the struct.
    let dont_break = hygenic_ident("DontBreak");
    let make_dont_break = hygenic_ident("__floop_make_dont_break");

    // the loop stops once all arms broke,
    // if there are no arms then all arms broke is always true (vacuous truth),
    // `before` still runs once as it runs before the check if all arms broke,
    // the rest of the loop can be removed.
    if ast.arms.is_empty() {
        let block_name = hygenic_ident("__floop_block");
        let tick = LifetimeTick::new();

        return quote! {
            {
                async {
                    #tick #block_name: {
                        #before;
                        break #tick #block_name;
                        // ensure invalid `after` code still results in a compiler error.
                        #after;
                    };
                }
            }
        }
    }

    let tys = (0..ast.arms.len()).map(|i| hygenic_ident(&format!("T{i}")));
    let variants = tys.clone().enumerate().map(|(i, ty)| (result_variant_name(i), ParenthesisGroupContaining::new(ty), Punct::new(',', Spacing::Alone)));
    let tys = tys.zip(iter::repeat(Punct::new(',', Spacing::Alone)));
    tokens.extend(quote! {
        enum #result<#{tys}> {
            #{variants}
        }
    });

    let mut poll_fn_arms = TokenStream::new();
    let mut match_arms = TokenStream::new();
    let mut recreate_futures = TokenStream::new();
    let mut break_condition = "true".into_token_stream();
    let mut break_value = TokenStream::new();
    let mut detectors = TokenStream::new();

    tokens.extend(quote! {
        struct #dont_break;

        let #make_dont_break = || #dont_break;
        
        // ensure the async block never implements unpin.
        //i have no idea if this is actually necessary
        let __floop_marker = ::core::marker::PhantomPinned;
    });

    if !ast.biased {
        tokens.extend(quote! {
            let mut #unbiased_offset = 0;
        });
    }
    
    for (i, arm) in ast.arms.iter().enumerate() {
        let future = future_name(i);
        // this must be `Some` if `fut_name` is `Some` and `None` otherwise.
        let pin = pin_name(i);
        let result_variant = result_variant_name(i);
        let output = output_name(i);

        tokens.extend(quote! {
            let mut #future = ::core::option::Option::None;
            let mut #pin = ::core::option::Option::None;
            let mut #output = ::core::option::Option::None;
        });

        poll_fn_arms.extend(quote! {
            #i => {
                if let Some(ref mut #pin) = #pin {
                    let #pin = ::core::pin::Pin::as_mut(#pin);
                    // this consumes `pin`, preventing `fut_expr` from using it.
                    if let ::core::task::Poll::Ready(output) = ::core::future::Future::poll(#pin, ctx) {
                        return ::core::task::Poll::Ready(#result::#result_variant(output));
                    }
                }
            }
        });

        let fut_expr = &arm.pattern().expr();
        let tmp = hygenic_ident("__floop_fut_ref");
        let future_ref = hygenic_ident("__floop_future_ref");
        let condition = arm.pattern().condition().map(|condition| quote! {
            && #condition
        }).unwrap_or(TokenStream::new());
        recreate_futures.extend(quote! {
            // `fut_name` can't be used because it is borrowed by `pin_name`.
            if ::core::option::Option::is_none(&#pin) && ::core::option::Option::is_none(&#output) #condition {
                // this shouldn't be needed, but it shouldn't hurt.
                #pin = ::core::option::Option::None;
                // this may help the borrow checker undertsand that `fut_name` is `None`, i have no idea if it actually does anything.
                #future = ::core::option::Option::None;
                let #tmp = loop {
                    break {
                        #fut_expr
                    }
                };
                
                let #future_ref = ::core::option::Option::get_or_insert(&mut #future, {
                    #tmp
                });

                
                // SAFETY: `fut_name` will not be moved and it will be dropped before it's invalidated,
                // this starts a new `Pin` safety contract.
                // `future_ref` is `mixed_site` and created after any user code runs so it's impossible for user code to change it
                // (which would lead to undefined behaviour if it was possible).
                unsafe {
                    #pin = ::core::option::Option::Some(::core::pin::Pin::new_unchecked(#future_ref));
                }
            }
        });

        let block = &arm.block();
        let pattern = &arm.pattern().pattern();
        let output_storage = hygenic_ident("__floop_output_storage");
        let matched = hygenic_ident("__floop_matched");

        match_arms.extend(quote! {
            // TODO: add the ability to use `break` to break specific arms.
            // (and the entire loop should stop once all arms broke).
            #result::#result_variant(#matched) => {
                let #pattern = #matched;
                // this weird mess of loops catches `break` and breaks the specific arm instead of the entire loop.
                // FIXME: this allow likely also applies to `block`.
                #[allow(unreachable_code, clippy::never_loop, unused_variables)]
                '__floop_outer: loop {
                    let #output_storage = loop {
                        #block;

                        break '__floop_outer;
                    };

                    #output = ::core::option::Option::Some(#output_storage);
                    break;
                }

                // the future needs to be recreated so that it can be polled again.
                #pin = ::core::option::Option::None;
                // SAFETY: assignment runs `drop` before overwriting the value so this is safe,
                // the safety contract of `Pin` is now over as the value is dropped.
                #future = ::core::option::Option::None;
            }
        });

        break_condition.extend(quote! {
            && let Some(#output) = #output
        });
        // `(expr,)` is the syntax for single element tuples, which i didn't even know existed,
        // so trailing commas need to be removed.
        let comma = (i != 0).then_some(quote! { , });
        break_value.extend(quote! {
            #comma #output
        });

        if !arm.pattern().footgun_allowed() {
            let future = future_name(i);
            let expr = arm.pattern().expr();
            let detector = &footgun_detector.detector;
        
            detectors.extend(quote! {
                // the closure bypasses a bug in the compiler (const panics in async blocks sometimes compile) and
                // ensures that the detector doesn't actually do anything at runtime, as the closure is never called.
                 let _ = || {
                    if true #condition {
                        let mut #future = Some({#expr});
                        #future = #detector;
                    }
                };
            });
        }
    }

    // TODO: test this.
    let poll_fn = hygenic_ident("__floop_poll_fn");
    let arm_amount = ast.arms.len();
    let index = hygenic_ident("__floop_index");
    let unbias = (!ast.biased).then_some(quote! {
        // 0 arms result in a early return, so this can't panic.
        #[allow(clippy::modulo_one)]
        let #index = (#index + #unbiased_offset) % #arm_amount;
        #unbiased_offset += 1;
        #[allow(clippy::modulo_one)]
        {
            #unbiased_offset %= #arm_amount
        };
    });

    tokens.extend(quote! {
        let test: (#dont_break, _) = loop {
            struct #dont_break;
            
            #before;
            
            #recreate_futures
            
            let #poll_fn = |ctx: &mut ::core::task::Context| {
                for #index in 0..#arm_amount {
                    #unbias

                    match #index {
                        #poll_fn_arms
                        // all numbers between `0` and `arm_amount` are matched by a arm's branch.
                        _ => ::core::unreachable!(),
                    }
                }

                ::core::task::Poll::Pending
            };
            let #poll_fn = ::core::future::poll_fn(#poll_fn);

            match #poll_fn.await {
                #match_arms
            }

            if #break_condition {
                break (#make_dont_break(), (#break_value));
            }

            #after;
        };

        test.1
    });

    let footgun_declerations = footgun_detector.declerations;
    let output = hygenic_ident("__floop_output");

    // ensure the macro can be used as a expression.
    quote! {
        async {
            let #output = {
                #tokens
            };

            // the detectors need to be at the end for let conditions creating futures whose type is inferred based on a value produced inside a arm to work.
            {
                #footgun_declerations

                #detectors;
            }

            #output
        }
    }
}