effing_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{
4    braced, parenthesized,
5    parse::{Parse, ParseStream},
6    parse_macro_input, parse_quote,
7    punctuated::Punctuated,
8    Error, Expr, ExprBreak, ExprReturn, GenericParam, Generics, Ident, ItemFn, LifetimeDef, Member,
9    Pat, PatIdent, PatPath, PatTupleStruct, Path, PathArguments, PathSegment, ReturnType,
10    Signature, Token, Type, TypeParam, TypePath, Visibility,
11};
12
13fn quote_do(e: &Expr) -> Expr {
14    parse_quote! {
15        {
16            use ::core::ops::{Generator, GeneratorState};
17            use ::effing_mad::frunk::Coproduct;
18            let mut gen = #e;
19            let mut injection = Coproduct::inject(::effing_mad::injection::Begin);
20            loop {
21                // interesting hack to trick the borrow checker
22                // allows cloneable generators
23                let res = {
24                    // safety: same as in `handle_group`
25                    let pinned = unsafe { ::core::pin::Pin::new_unchecked(&mut gen) };
26                    pinned.resume(injection)
27                };
28                match res {
29                    GeneratorState::Yielded(effs) =>
30                        injection = (yield effs.embed()).subset().ok().unwrap(),
31                    GeneratorState::Complete(v) => break v,
32                }
33            }
34        }
35    }
36}
37
38struct Effectful {
39    effects: Punctuated<Type, Token![,]>,
40}
41
42impl Parse for Effectful {
43    fn parse(input: ParseStream) -> Result<Self, Error> {
44        let effects = Punctuated::parse_terminated(input)?;
45        Ok(Effectful { effects })
46    }
47}
48
49impl syn::visit_mut::VisitMut for Effectful {
50    fn visit_expr_mut(&mut self, e: &mut Expr) {
51        match e {
52            Expr::Field(ref mut ef) => {
53                self.visit_expr_mut(&mut ef.base);
54                let Member::Named(ref name) = ef.member else { return };
55                if name == "do_" {
56                    *e = quote_do(&ef.base);
57                }
58            },
59            Expr::Yield(ref y) => {
60                let Some(ref expr) = y.expr else { panic!("no expr?") };
61                *e = parse_quote! {
62                    {
63                        let effect = { #expr };
64                        let marker = ::effing_mad::macro_impl::mark(&effect);
65                        let injs = yield ::effing_mad::frunk::Coproduct::inject(effect);
66                        ::effing_mad::macro_impl::get_inj(injs, marker).unwrap()
67                    }
68                };
69            },
70            e => syn::visit_mut::visit_expr_mut(self, e),
71        }
72    }
73}
74
75/// Define an effectful function using `fn`-like syntax.
76///
77/// Effectful functions can suspend their execution, and when called immediately return an
78/// effectful computation. This is analogous to `async fn`s, which can also suspend their
79/// execution, and return a `Future` when called.
80///
81/// # Usage
82/// ```rust
83/// #[effectful(A, B)]
84/// /* optionally: */ #[effectful::cloneable]
85/// fn cool_function(arg: Foo) -> Bar {
86///     yield expr_a;
87///     let val = yield expr_b;
88///     epic_function(val).do_
89/// }
90/// ```
91/// This macro takes a list of types as its arguments. These types must implement `Effect` or
92/// `EffectGroup`. Expressions passed to `yield` must be of one of those effect types, or of an
93/// effect type that is in one of those groups.
94///
95/// The `yield expr` syntax runs the effect `expr`, and evaluates to the Injection of that effect
96/// type.
97///
98/// The `do_` operator is analogous to `.await`. It runs an effectful computation by yielding all
99/// of its effects to the caller of the current function. The callee's effects must be a subset of
100/// the current function's effects - in this example, a subset of `{A, B}`. The callee is usually
101/// another function defined using this macro.
102///
103/// It is possible to create effectful functions whose computations can be cloned. This requires
104/// marking the function as `#[effectful::cloneable]` after the `#[effectful(...)]` invocation,
105/// the function to have only `Clone` and `Unpin` locals, and the function to never hold a
106/// reference to a local across a yield point. In other words, the underlying generator must be
107/// `Clone` and `Unpin`.
108#[proc_macro_attribute]
109pub fn effectful(args: TokenStream, item: TokenStream) -> TokenStream {
110    let mut effects = parse_macro_input!(args as Effectful);
111    let effect_names = effects.effects.iter();
112    let yield_type = quote! {
113        <::effing_mad::frunk::Coprod!(#(#effect_names),*) as ::effing_mad::macro_impl::FlattenEffects>::Out
114    };
115    let ItemFn {
116        mut attrs,
117        vis,
118        sig,
119        mut block,
120    } = parse_macro_input!(item as ItemFn);
121    let Signature {
122        constness,
123        unsafety,
124        ident,
125        generics,
126        inputs,
127        output,
128        ..
129    } = sig;
130    let return_type = match output {
131        ReturnType::Default => quote!(()),
132        ReturnType::Type(_r_arrow, ref ty) => ty.to_token_stream(),
133    };
134    syn::visit_mut::visit_block_mut(&mut effects, &mut block);
135    let mut cloneable = false;
136    attrs.retain(|attr| {
137        if attr.path == parse_quote!(effectful::cloneable) {
138            cloneable = true;
139            false // remove it from the attrs list so no one gets confused
140        } else {
141            true
142        }
143    });
144    let clone_bound = cloneable.then_some(quote!( + ::core::clone::Clone + ::core::marker::Unpin));
145    quote! {
146        #(#attrs)*
147        #vis #constness #unsafety
148        fn #ident #generics(#inputs)
149        -> impl ::core::ops::Generator<
150            <#yield_type as ::effing_mad::injection::EffectList>::Injections,
151            Yield = #yield_type,
152            Return = #return_type
153        > #clone_bound {
154            move |_begin: <#yield_type as ::effing_mad::injection::EffectList>::Injections| {
155                #block
156            }
157        }
158    }
159    .into()
160}
161
162struct EffectArg {
163    name: Ident,
164    ty: Type,
165}
166
167impl Parse for EffectArg {
168    fn parse(input: ParseStream) -> syn::Result<Self> {
169        let name = input.parse()?;
170        let _: Token![:] = input.parse()?;
171        let ty: Type = input.parse()?;
172        Ok(EffectArg { name, ty })
173    }
174}
175
176struct Effect {
177    name: Ident,
178    args: Punctuated<EffectArg, Token![,]>,
179    ret: Type,
180}
181
182impl Parse for Effect {
183    fn parse(input: ParseStream) -> syn::Result<Self> {
184        <Token![fn]>::parse(input)?;
185        let name = input.parse()?;
186
187        let content;
188        parenthesized!(content in input);
189        let args = Punctuated::parse_terminated(&content)?;
190
191        <Token![->]>::parse(input)?;
192        let ret = input.parse()?;
193
194        Ok(Effect { name, args, ret })
195    }
196}
197
198struct Effects {
199    vis: Visibility,
200    group_name: Ident,
201    generics: Generics,
202    effects: Punctuated<Effect, Token![;]>,
203}
204
205impl Parse for Effects {
206    fn parse(input: ParseStream) -> syn::Result<Self> {
207        let vis = input.parse()?;
208        let group_name = input.parse()?;
209        let generics = input.parse()?;
210
211        let content;
212        braced!(content in input);
213        let effects = Punctuated::parse_terminated(&content)?;
214
215        Ok(Effects {
216            vis,
217            group_name,
218            generics,
219            effects,
220        })
221    }
222}
223
224/// Define a new group of effects.
225///
226/// # Usage
227/// ```rust
228/// effects! {
229///     State<T> {
230///         fn get() -> T;
231///         fn put(v: T) -> ();
232///     }
233/// }
234/// ```
235///
236/// Multiple new types are created: one for the group (`State` in this example) and one for each
237/// effect in the group. The group type has associated functions that look like the functions in the
238/// invocation.
239///
240/// This allows usage such as `let state = yield State::get()` and `yield State::put(val)`. The
241/// type after `->` defines the injection type of that effect, which is the type that such yield
242/// expressions will evaluate to.
243#[proc_macro]
244pub fn effects(input: TokenStream) -> TokenStream {
245    let Effects {
246        vis,
247        group_name,
248        generics,
249        effects,
250    } = parse_macro_input!(input as Effects);
251
252    let eff_name = effects.iter().map(|eff| &eff.name).collect::<Vec<_>>();
253    let phantom_datas = generics
254        .params
255        .iter()
256        .map(|param| match param {
257            GenericParam::Type(TypeParam { ident, .. }) => {
258                quote!(::core::marker::PhantomData::<#ident>)
259            },
260            GenericParam::Lifetime(LifetimeDef { lifetime, .. }) => {
261                quote!(::core::marker::PhantomData::<&#lifetime ()>)
262            },
263            GenericParam::Const(_) => todo!(),
264        })
265        .collect::<Vec<_>>();
266    let phantom_datas = quote!(#(#phantom_datas),*);
267    let maybe_phantom_data = generics
268        .lt_token
269        .map(|_| quote!(::core::marker::PhantomData::<#group_name #generics>));
270
271    let arg_name = effects
272        .iter()
273        .map(|eff| eff.args.iter().map(|arg| &arg.name).collect::<Vec<_>>())
274        .collect::<Vec<_>>();
275    let arg_ty = effects
276        .iter()
277        .map(|eff| eff.args.iter().map(|arg| &arg.ty).collect::<Vec<_>>())
278        .collect::<Vec<_>>();
279    let ret_ty = effects.iter().map(|eff| &eff.ret).collect::<Vec<_>>();
280
281    quote! {
282        #vis struct #group_name #generics (#phantom_datas);
283
284        impl #generics #group_name #generics {
285            #(
286            fn #eff_name(#(#arg_name: #arg_ty),*) -> #eff_name #generics {
287                #eff_name(#(#arg_name,)* #maybe_phantom_data)
288            }
289            )*
290        }
291
292        impl #generics ::effing_mad::EffectGroup for #group_name #generics {
293            type Effects = ::effing_mad::frunk::Coprod!(#(#eff_name #generics),*);
294        }
295
296        #(
297        #[allow(non_camel_case_types)]
298        #vis struct #eff_name #generics (#(#arg_ty,)* #maybe_phantom_data);
299
300        impl #generics ::effing_mad::Effect for #eff_name #generics {
301            type Injection = #ret_ty;
302        }
303        )*
304    }
305    .into()
306}
307
308struct HandlerArm {
309    pat: Pat,
310    body: Expr,
311}
312
313impl Parse for HandlerArm {
314    fn parse(input: ParseStream) -> syn::Result<Self> {
315        let pat = input.parse()?;
316        <Token![=>]>::parse(input)?;
317        let body = input.parse()?;
318        Ok(HandlerArm { pat, body })
319    }
320}
321
322struct Handler {
323    asyncness: Option<Token![async]>,
324    moveness: Option<Token![move]>,
325    group: TypePath,
326    arms: Punctuated<HandlerArm, Token![,]>,
327    is_shorthand: bool,
328}
329
330impl Parse for Handler {
331    fn parse(input: ParseStream) -> syn::Result<Self> {
332        let asyncness = input.parse()?;
333        let moveness = input.parse()?;
334        let ahead = input.fork();
335        if ahead.parse::<HandlerArm>().is_ok() {
336            // the docs say not to do this but idk how to do it otherwise
337            let single_arm: HandlerArm = input.parse().unwrap();
338            let path = match &single_arm.pat {
339                // struct name on its own gets parsed as ident
340                Pat::Ident(PatIdent { ident, .. }) => Path {
341                    leading_colon: None,
342                    segments: Punctuated::from_iter(std::iter::once(PathSegment {
343                        ident: ident.clone(),
344                        arguments: PathArguments::None,
345                    })),
346                },
347                Pat::Path(PatPath { path, .. }) | Pat::TupleStruct(PatTupleStruct { path, .. }) => {
348                    path.clone()
349                },
350                p => panic!("invalid pattern in handler: {p:?}"),
351            };
352            let group = TypePath { qself: None, path };
353
354            return Ok(Handler {
355                asyncness,
356                moveness,
357                group,
358                arms: Punctuated::from_iter(std::iter::once(single_arm)),
359                is_shorthand: true,
360            });
361        }
362        let group = input.parse()?;
363
364        let content;
365        braced!(content in input);
366        let arms = Punctuated::parse_terminated(&content)?;
367        Ok(Handler {
368            asyncness,
369            moveness,
370            group,
371            arms,
372            is_shorthand: false,
373        })
374    }
375}
376
377// can't figure out what the type is lol
378struct FixControlFlow<T: ToTokens> {
379    eff_ty: T,
380    is_shorthand: bool,
381}
382
383impl<T: ToTokens> syn::visit_mut::VisitMut for FixControlFlow<T> {
384    fn visit_expr_mut(&mut self, e: &mut Expr) {
385        let eff = &self.eff_ty;
386        match e {
387            Expr::Break(ExprBreak { expr, .. }) => {
388                let expr = expr
389                    .as_ref()
390                    .map(ToTokens::to_token_stream)
391                    .unwrap_or(quote!(()));
392                *e = parse_quote!(return ::core::ops::ControlFlow::Break(#expr));
393            },
394            Expr::Return(ExprReturn { expr, .. }) => {
395                let expr = expr
396                    .as_ref()
397                    .map(ToTokens::to_token_stream)
398                    .unwrap_or(quote!(()));
399                let inj = if self.is_shorthand {
400                    quote!(#expr)
401                } else {
402                    quote!(::effing_mad::injection::Tagged::<_, #eff>::new(#expr))
403                };
404                *e = parse_quote! {
405                    return ::core::ops::ControlFlow::Continue(
406                        ::effing_mad::frunk::Coproduct::inject(#inj)
407                    );
408                };
409            },
410            e => syn::visit_mut::visit_expr_mut(self, e),
411        }
412    }
413}
414
415/// Define a handler for an effect or group of effects.
416///
417/// # Usage
418/// Handling a group of effects at once:
419/// ```rust
420/// let mut state = 0i32;
421/// handle_group(
422///     g,
423///     handler! {
424///         State<i32> {
425///             get() => state,
426///             put(v) => state = v,
427///         }
428///     }
429/// )
430/// ```
431///
432/// Handling a single effect at once:
433/// ```
434/// handle(g, handler!(Cancel => break))
435/// ```
436///
437/// The value that a handler arm's expression evaluates to (for example `state` and `()` in the
438/// `State<i32>` example) is used as the injection for that effect. It is also possible to use the
439/// `break` keyword to cause the computation that is being handled to return. In this case, the
440/// type of the value passed to `break` must match the return type of a computation that the
441/// handler is used on. See `effing_mad::map` for a way to change the return value of a computation.
442///
443/// The handler can capture state from its environment, and/or be asynchronous. The keywords
444/// `async` and `move` can both optionally appear (in that order) at the very beginning of the
445/// macro input to control these behaviours, in a similar way to how they would affect a closure.
446/// These keywords apply to all arms of the handler. `handle_async` or `handle_group_async` must be
447/// used when applying an async handler.
448///
449/// Note that the `put` arm in this example mutably borrows the `state` variable, while the `get`
450/// arm also borrows it. This is the advantage of handling effects together. Internally, `handler!`
451/// expands to a single closure with a `match` expression in it, so the arms can all borrow the
452/// same content, even mutably.
453#[proc_macro]
454pub fn handler(input: TokenStream) -> TokenStream {
455    let Handler {
456        asyncness,
457        moveness,
458        group,
459        arms,
460        is_shorthand,
461    } = parse_macro_input!(input as Handler);
462
463    let generics = match group.path.segments.last().unwrap().arguments {
464        PathArguments::None => None,
465        PathArguments::AngleBracketed(ref v) => Some(v),
466        PathArguments::Parenthesized(_) => panic!("stop that"),
467    };
468
469    let mut matcher = quote! { match effs {} };
470    for arm in arms {
471        let HandlerArm { pat, mut body } = arm;
472        let eff_ty = match &pat {
473            // struct name on its own gets parsed as ident
474            Pat::Ident(ident) => quote!(#ident),
475            Pat::Path(path) => quote!(#path),
476            Pat::TupleStruct(PatTupleStruct { path, .. }) => quote!(#path),
477            p => panic!("invalid pattern in handler: {p:?}"),
478        };
479        let new_pat = if generics.is_some() {
480            match &pat {
481                Pat::Ident(ident) => quote!(#ident(::core::marker::PhantomData)),
482                Pat::Path(path) => quote!(#path(::core::marker::PhantomData)),
483                Pat::TupleStruct(p) => {
484                    let mut p = p.clone();
485                    p.pat.elems.push(parse_quote!(::core::marker::PhantomData));
486                    quote!(#p)
487                },
488                p => panic!("invalid pattern in handler: {p:?}"),
489            }
490        } else {
491            quote!(#pat)
492        };
493        if let Expr::Break(_) | Expr::Return(_) = body {
494            body = parse_quote!({ #body });
495        }
496        syn::visit_mut::visit_expr_mut(
497            &mut FixControlFlow {
498                eff_ty: &eff_ty,
499                is_shorthand,
500            },
501            &mut body,
502        );
503        if is_shorthand {
504            matcher = quote! {
505                {
506                    let #new_pat = effs;
507                    #body
508                }
509            };
510        } else {
511            matcher = quote! {
512                match effs.uninject() {
513                    Ok(#new_pat) => {
514                        let __effing_inj = #body;
515                        #[allow(unreachable_code)]
516                        ::effing_mad::frunk::Coproduct::inject(
517                            ::effing_mad::injection::Tagged::<_, #eff_ty #generics>::new(
518                                __effing_inj
519                            )
520                        )
521                    },
522                    Err(effs) => #matcher,
523                }
524            };
525        }
526    }
527    let effs_ty = if is_shorthand {
528        quote!(#group)
529    } else {
530        quote!(<#group as ::effing_mad::EffectGroup>::Effects)
531    };
532    quote! {
533        #moveness |effs: #effs_ty| #asyncness {
534            let __effing_inj = #matcher;
535            // if the handler unconditionally breaks then this line is unreachable, but we
536            // don't want to see a warning for it.
537            #[allow(unreachable_code)]
538            ::core::ops::ControlFlow::<_, _>::Continue(__effing_inj)
539        }
540    }
541    .into()
542}