├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── async │ └── main.rs └── factorial │ └── main.rs ├── src └── lib.rs └── tests ├── fail ├── await_no_tco.rs ├── await_no_tco.stderr ├── non_tco_op.rs └── non_tco_op.stderr ├── pass └── factorial.rs └── ui.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tco" 3 | version = "0.0.2" 4 | authors = ["Sam Sieber "] 5 | edition = "2018" 6 | keywords = ["recursion", "tco", "macro"] 7 | license = "MIT OR Apache-2.0" 8 | description = "A macro for transforming a tail-calling recursive function to eliminate recursion" 9 | readme = "README.md" 10 | repository = "https://github.com/samsieber/tco" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "1.0", features = ["full", "visit-mut"] } 17 | quote = "1.0" 18 | proc-macro-error = "1.0" 19 | 20 | [dev-dependencies] 21 | trybuild = "1.0" 22 | futures = "0.3" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TCO 2 | 3 | TCO is a tail-call optimization library. It's a proof-of-concept attribute macro you can slap onto item functions to optimize them if they are in tail-calling format. 4 | 5 | ## Limitations 6 | 7 | It's not very smart. 8 | 9 | It only works on free functions (eg. `fn foo(bar: Bar) -> u32` not in an impl block). 10 | 11 | It _can_ have problems with passing a non-copy argument. 12 | 13 | It's untested with references. 14 | 15 | It doesn't support mutual recursion. 16 | 17 | It supports only basic patterns in the function argument (no tuple destructuring). 18 | 19 | It can't turn a non-tail calling into a tail calling function. 20 | 21 | ## Help wanted 22 | 23 | This is just a proof-of-concept. I'd love help fleshing it out. 24 | 25 | ## Acknowledgements 26 | 27 | Thanks to user aloso for providing a better user experience by turning panics into compile errors and contributing a fix to prevent the optimization for happening to non tailcalling functions. 28 | 29 | ## Alternatives 30 | 31 | * [tramp](https://docs.rs/tramp/0.3.0/tramp/) 32 | * [async-recursion](https://docs.rs/async-recursion/0.3.1/async_recursion/) 33 | * [tailcall](https://docs.rs/tailcall/0.1.4/tailcall/) 34 | 35 | ## Examples 36 | 37 | Enough talk, examples! 38 | 39 | ### Sync Example: Factorial 40 | ```rust 41 | #[tco::rewrite] 42 | fn fac_with_acc(n: u128, acc: u128) -> u128 { 43 | if n > 1 { 44 | fac_with_acc(n - 1, acc * n) 45 | } else { 46 | acc 47 | } 48 | } 49 | ``` 50 | 51 | expands to 52 | 53 | ```rust 54 | fn fac_with_acc(n: u128, acc: u128) -> u128 { 55 | let mut n = n; 56 | let mut acc = acc; 57 | '__tco_loop: loop { 58 | return { 59 | if n > 1 { 60 | { 61 | let __tco_0 = (n - 1, acc * n); 62 | n = __tco_0.0; 63 | acc = __tco_0.1; 64 | continue '__tco_loop; 65 | } 66 | } else { 67 | acc 68 | } 69 | }; 70 | } 71 | } 72 | ``` 73 | 74 | ### Async Example: Factorial 75 | 76 | ```rust 77 | #[tco::rewrite] 78 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 79 | if n > 1 { 80 | fac_with_acc(n - 1, acc * n).await 81 | } else { 82 | acc 83 | } 84 | } 85 | 86 | ``` 87 | 88 | expands to 89 | 90 | ```rust 91 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 92 | let mut n = n; 93 | let mut acc = acc; 94 | '__tco_loop: loop { 95 | return { 96 | if n > 1 { 97 | { 98 | let __tco_0 = (n - 1, acc * n); 99 | n = __tco_0.0; 100 | acc = __tco_0.1; 101 | continue '__tco_loop; 102 | } 103 | } else { 104 | acc 105 | } 106 | }; 107 | } 108 | } 109 | ``` 110 | 111 | without the tco::rewrite attribute, you instead get the folliwing error: 112 | 113 | ``` 114 | error[E0733]: recursion in an `async fn` requires boxing 115 | --> $DIR/await_no_tco.rs:6:46 116 | | 117 | 6 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 118 | | ^^^^ recursive `async fn` 119 | | 120 | = note: a recursive `async fn` must be rewritten to return a boxed `dyn Future` 121 | ``` 122 | -------------------------------------------------------------------------------- /examples/async/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::future::Future; 4 | 5 | #[tco::rewrite] 6 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 7 | if n > 1 { 8 | fac_with_acc(n - 1, acc * n).await 9 | } else { 10 | acc 11 | } 12 | } 13 | 14 | pub fn main() { 15 | assert_eq!(futures::executor::block_on(fac_with_acc(5, 1)), 120); 16 | } 17 | -------------------------------------------------------------------------------- /examples/factorial/main.rs: -------------------------------------------------------------------------------- 1 | #[tco::rewrite] 2 | fn fac_with_acc(n: u128, acc: u128) -> u128 { 3 | if n > 1 { 4 | fac_with_acc(n - 1, acc * n) 5 | } else { 6 | acc 7 | } 8 | } 9 | 10 | fn main() { 11 | assert_eq!(fac_with_acc(5, 1), 120); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro_error::{abort, proc_macro_error}; 5 | use quote::{format_ident, quote, quote_spanned}; 6 | use syn::spanned::Spanned; 7 | use syn::visit_mut::{self, VisitMut}; 8 | use syn::{parse2, parse_quote}; 9 | use syn::{parse_macro_input, token::Comma, Block, Expr, ExprCall, FnArg, Ident, ItemFn, Pat}; 10 | 11 | struct TCO { 12 | ident: Ident, 13 | args: Vec, 14 | i: u32, 15 | } 16 | 17 | impl TCO { 18 | fn rewrite_return_to_tco_update(&mut self, node: &mut Expr) -> bool { 19 | let expr_call: &mut ExprCall = match node { 20 | Expr::Call(expr_call) => expr_call, 21 | Expr::Await(await_call) => { 22 | if self.rewrite_return_to_tco_update(&mut *await_call.base) { 23 | *node = *await_call.base.clone(); 24 | } 25 | return false; 26 | } 27 | _ => { 28 | visit_mut::visit_expr_mut(self, node); 29 | return false; 30 | } 31 | }; 32 | 33 | let mut replace_call = false; 34 | if let Expr::Path(ref mut fn_path) = *expr_call.func { 35 | if fn_path.attrs.len() == 0 36 | && fn_path.qself.is_none() 37 | && fn_path.path.leading_colon.is_none() 38 | && fn_path.path.segments.len() == 1 39 | { 40 | if fn_path.path.segments.first().unwrap().ident == self.ident { 41 | replace_call = true; 42 | } 43 | } 44 | } 45 | 46 | if replace_call { 47 | let tco_ident = format_ident!("__tco_{}", self.i, span = expr_call.span()); 48 | let span = expr_call.span(); 49 | let tup = &mut expr_call.args; 50 | if !tup.trailing_punct() { 51 | tup.push_punct(Comma::default()); 52 | } 53 | let updates = self.args.iter().enumerate().map(|(i, q)| { 54 | let i = syn::Index::from(i); 55 | quote!(#q = #tco_ident.#i;) 56 | }); 57 | let tokens = quote_spanned!(span=>({ 58 | let #tco_ident = (#tup); 59 | #(#updates)* 60 | continue '__tco_loop; 61 | })); 62 | *node = parse2(tokens).expect("This was in the right format!"); 63 | return true; 64 | } else { 65 | visit_mut::visit_expr_mut(self, node); 66 | return false; 67 | } 68 | } 69 | } 70 | 71 | impl VisitMut for TCO { 72 | fn visit_expr_mut(&mut self, node: &mut Expr) { 73 | self.rewrite_return_to_tco_update(node); 74 | } 75 | } 76 | 77 | #[proc_macro_attribute] 78 | #[proc_macro_error] 79 | pub fn rewrite(_attr: TokenStream, item: TokenStream) -> TokenStream { 80 | // Parse the input tokens into a syntax tree 81 | let mut input: ItemFn = parse_macro_input!(item as ItemFn); 82 | let fn_ident = input.sig.ident.clone(); 83 | 84 | let mut tco = TCO { 85 | ident: fn_ident, 86 | args: input 87 | .sig 88 | .inputs 89 | .iter() 90 | .map(|a| match a { 91 | FnArg::Typed(pat) => match &*pat.pat { 92 | Pat::Ident(ident_wrapper) => ident_wrapper.ident.clone(), 93 | span => abort!(span, "TCO only supports basic function args"), 94 | }, 95 | span => abort!(span, "TCO does not support self arg"), 96 | }) 97 | .collect(), 98 | i: 0, 99 | }; 100 | 101 | tco.visit_item_fn_mut(&mut input); 102 | { 103 | let old_body = input.block; 104 | let updates = tco.args.iter().map(|q| quote!(let mut #q = #q;)); 105 | let new_body: Block = parse_quote!( 106 | { 107 | #(#updates)* 108 | '__tco_loop: loop { 109 | #[allow(unused_parens)] 110 | #[deny(unreachable_code)] 111 | return #old_body; 112 | } 113 | } 114 | ); 115 | input.block = Box::new(new_body); 116 | } 117 | 118 | TokenStream::from(quote!(#input)) 119 | } 120 | -------------------------------------------------------------------------------- /tests/fail/await_no_tco.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::future::Future; 4 | 5 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 6 | if n > 1 { 7 | fac_with_acc(n - 1, acc * n).await 8 | } else { 9 | acc 10 | } 11 | } 12 | 13 | pub fn main(){ 14 | assert_eq!(futures::executor::block_on(fac_with_acc(5, 1)), 120); 15 | } -------------------------------------------------------------------------------- /tests/fail/await_no_tco.stderr: -------------------------------------------------------------------------------- 1 | error[E0733]: recursion in an `async fn` requires boxing 2 | --> $DIR/await_no_tco.rs:5:46 3 | | 4 | 5 | async fn fac_with_acc(n: u128, acc: u128) -> u128 { 5 | | ^^^^ recursive `async fn` 6 | | 7 | = note: a recursive `async fn` must be rewritten to return a boxed `dyn Future` 8 | -------------------------------------------------------------------------------- /tests/fail/non_tco_op.rs: -------------------------------------------------------------------------------- 1 | #[tco::rewrite] 2 | fn fac_with_acc(n: u128, acc: u128) -> u128 { 3 | if n > 1 { 4 | fac_with_acc(n - 1, acc * n) + 5 5 | } else { 6 | acc 7 | } 8 | } 9 | 10 | pub fn main(){ 11 | fac_with_acc(5,1); 12 | } -------------------------------------------------------------------------------- /tests/fail/non_tco_op.stderr: -------------------------------------------------------------------------------- 1 | error: unreachable expression 2 | --> $DIR/non_tco_op.rs:4:40 3 | | 4 | 4 | fac_with_acc(n - 1, acc * n) + 5 5 | | ---------------------------- ^ unreachable expression 6 | | | 7 | | any code following this expression is unreachable 8 | | 9 | note: the lint level is defined here 10 | --> $DIR/non_tco_op.rs:1:1 11 | | 12 | 1 | #[tco::rewrite] 13 | | ^^^^^^^^^^^^^^^ 14 | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) 15 | -------------------------------------------------------------------------------- /tests/pass/factorial.rs: -------------------------------------------------------------------------------- 1 | use tco; 2 | 3 | #[tco::rewrite] 4 | fn fac_with_acc(n: u128, acc: u128) -> u128 { 5 | if n > 1 { 6 | fac_with_acc(n - 1, acc * n) 7 | } else { 8 | 120 9 | } 10 | } 11 | 12 | fn main(){ 13 | assert_eq!(fac_with_acc(5,1),120); 14 | } -------------------------------------------------------------------------------- /tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/fail/*.rs"); 5 | t.pass("tests/pass/factorial.rs") 6 | } 7 | --------------------------------------------------------------------------------