diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fbee845..5165ed6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -60,6 +60,13 @@ jobs: cargo run --all-features --bin return-values-complex cargo run --all-features --bin return-values-non-copy cargo run --all-features --bin return-values-zero-args + cargo run --all-features --bin methods + cargo run --all-features --bin methods-multiple-args + cargo run --all-features --bin methods-return-values + cargo run --all-features --bin methods-mut-self + cargo run --all-features --bin methods-owned-self + cargo run --all-features --bin methods-complex + cargo run --all-features --bin methods-non-copy-arg clippy-rust: name: 4. Clippy lints on Rust crates diff --git a/splat-overload-test/src/bin/methods-complex.rs b/splat-overload-test/src/bin/methods-complex.rs new file mode 100644 index 0000000..fc885dd --- /dev/null +++ b/splat-overload-test/src/bin/methods-complex.rs @@ -0,0 +1,40 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Validator { + threshold: i32, +} + +overload! { + impl Validator { + fn check(&self) -> bool { self.threshold > 0 } + fn check(&self, x: i32) -> bool { + if x < 0 { + return false; + } + x >= self.threshold + } + fn check(&self, x: i32, y: i32) -> Vec { + vec![x >= self.threshold, y >= self.threshold] + } + } +} + +fn main() { + let v = Validator { threshold: 10 }; + + assert!(v.check()); + println!("zero-arg check: {}", v.check()); + + assert!(!v.check(-5i32), "the check must return false"); + assert!(v.check(15i32)); + println!("one-arg checks passed"); + + let results = v.check(5i32, 20i32); + assert_eq!(results, vec![false, true]); + println!("two-arg check: {:?}", results); +} diff --git a/splat-overload-test/src/bin/methods-multiple-args.rs b/splat-overload-test/src/bin/methods-multiple-args.rs new file mode 100644 index 0000000..f6b7972 --- /dev/null +++ b/splat-overload-test/src/bin/methods-multiple-args.rs @@ -0,0 +1,21 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Calculator; + +overload! { + impl Calculator { + fn compute(&self, x: i32, y: i32) { println!("sum: {}", x + y); } + fn compute(&self, x: f64, y: f64, z: f64) { println!("average: {}", (x + y + z) / 3.0); } + } +} + +fn main() { + let calc = Calculator; + calc.compute(10i32, 20i32); + calc.compute(1.0f64, 2.0f64, 3.0f64); +} diff --git a/splat-overload-test/src/bin/methods-mut-self.rs b/splat-overload-test/src/bin/methods-mut-self.rs new file mode 100644 index 0000000..2eeaa27 --- /dev/null +++ b/splat-overload-test/src/bin/methods-mut-self.rs @@ -0,0 +1,29 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Counter { + value: i32, +} + +overload! { + impl Counter { + fn add(&mut self, x: i32) { self.value += x; } + fn add(&mut self, x: i32, y: i32) { self.value += x + y; } + } +} + +fn main() { + let mut counter = Counter { value: 0 }; + + counter.add(5i32); + assert_eq!(counter.value, 5); + println!("after add(5): {}", counter.value); + + counter.add(3i32, 4i32); + assert_eq!(counter.value, 12); + println!("after add(3, 4): {}", counter.value); +} diff --git a/splat-overload-test/src/bin/methods-non-copy-arg.rs b/splat-overload-test/src/bin/methods-non-copy-arg.rs new file mode 100644 index 0000000..ad559fd --- /dev/null +++ b/splat-overload-test/src/bin/methods-non-copy-arg.rs @@ -0,0 +1,25 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Logger { + prefix: String, +} + +overload! { + impl Logger { + fn log(&self, message: String) { println!("{}: {}", self.prefix, message); } + fn log(&self, message: String, level: String) { println!("{} [{}]: {}", self.prefix, level, message); } + } +} + +fn main() { + let logger = Logger { + prefix: "APP".to_string(), + }; + logger.log("starting up".to_string()); + logger.log("something happened".to_string(), "WARN".to_string()); +} diff --git a/splat-overload-test/src/bin/methods-owned-self.rs b/splat-overload-test/src/bin/methods-owned-self.rs new file mode 100644 index 0000000..8a26b53 --- /dev/null +++ b/splat-overload-test/src/bin/methods-owned-self.rs @@ -0,0 +1,33 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Builder { + parts: Vec, +} + +overload! { + impl Builder { + fn build(self) -> String { self.parts.join("") } + fn build(self, sep: String) -> String { self.parts.join(&sep) } + } +} + +fn main() { + let b = Builder { + parts: vec!["a".to_string(), "b".to_string(), "c".to_string()], + }; + let result = b.build(); + assert_eq!(result, "abc"); + println!("joined: {}", result); + + let b2 = Builder { + parts: vec!["x".to_string(), "y".to_string(), "z".to_string()], + }; + let result2 = b2.build(", ".to_string()); + assert_eq!(result2, "x, y, z"); + println!("joined with sep: {}", result2); +} diff --git a/splat-overload-test/src/bin/methods-return-values.rs b/splat-overload-test/src/bin/methods-return-values.rs new file mode 100644 index 0000000..25c0bf5 --- /dev/null +++ b/splat-overload-test/src/bin/methods-return-values.rs @@ -0,0 +1,27 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; + +struct Calculator; + +overload! { + impl Calculator { + fn compute(&self, x: i32) -> i32 { x * 2 } + fn compute(&self, x: i32, y: i32) -> i32 { x + y } + } +} + +fn main() { + let calc = Calculator; + + let a = calc.compute(21i32); + assert_eq!(a, 42); + println!("single arg result: {}", a); + + let b = calc.compute(10i32, 32i32); + assert_eq!(b, 42); + println!("two arg result: {}", b); +} diff --git a/splat-overload-test/src/bin/methods.rs b/splat-overload-test/src/bin/methods.rs new file mode 100644 index 0000000..797600a --- /dev/null +++ b/splat-overload-test/src/bin/methods.rs @@ -0,0 +1,21 @@ +#![feature(splat)] +#![feature(tuple_trait)] +#![allow(incomplete_features)] +#![allow(unused_braces, clippy::disallowed_names)] + +use splat_overload::overload; + +struct Foo; + +overload! { + impl Foo { + fn method(&self, x: i32) { println!("i32: {}", x); } + fn method(&self, x: f64) { println!("f64: {}", x); } + } +} + +fn main() { + let foo = Foo; + foo.method(42i32); + foo.method(3.1f64); +} diff --git a/splat-overload/src/lib.rs b/splat-overload/src/lib.rs index 63db798..a0cbf0c 100644 --- a/splat-overload/src/lib.rs +++ b/splat-overload/src/lib.rs @@ -1,32 +1,56 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ - FnArg, ItemFn, Pat, Result, + FnArg, ItemFn, Pat, Result, Token, parse::{Parse, ParseStream}, parse_macro_input, }; -struct OverloadInput { - functions: Vec, +enum OverloadInput { + Functions(Vec), + Methods { + self_ty: syn::Ident, + functions: Vec, + }, } impl Parse for OverloadInput { fn parse(input: ParseStream) -> Result { - let mut functions = Vec::new(); - while !input.is_empty() { - functions.push(input.parse::()?); + if input.peek(Token![impl]) { + let item_impl: syn::ItemImpl = input.parse()?; + + let self_ty = match &*item_impl.self_ty { + syn::Type::Path(type_path) => type_path.path.segments.last().unwrap().ident.clone(), + _ => panic!("overload! impl block must use a plain type name"), + }; + + let functions = item_impl + .items + .into_iter() + .map(|item| match item { + syn::ImplItem::Fn(impl_fn) => syn::ItemFn { + attrs: impl_fn.attrs, + vis: impl_fn.vis, + sig: impl_fn.sig, + block: Box::new(impl_fn.block), + }, + _ => panic!("overload! impl block may only contain fn items"), + }) + .collect(); + + Ok(OverloadInput::Methods { self_ty, functions }) + } else { + let mut functions = Vec::new(); + while !input.is_empty() { + functions.push(input.parse::()?); + } + Ok(OverloadInput::Functions(functions)) } - Ok(OverloadInput { functions }) } } -#[proc_macro] -pub fn overload(input: TokenStream) -> TokenStream { - let OverloadInput { functions } = parse_macro_input!(input as OverloadInput); - - let fn_name = &functions[0].sig.ident; - - let trait_name = quote::format_ident!( +fn trait_name_for(fn_name: &syn::Ident) -> syn::Ident { + quote::format_ident!( "{}Args", fn_name .to_string() @@ -38,67 +62,198 @@ pub fn overload(input: TokenStream) -> TokenStream { c }) .collect::() - ); + ) +} + +fn collect_args( + func: &ItemFn, +) -> ( + Vec, + Vec, + Vec, +) { + let mut arg_types = Vec::new(); + let mut arg_names = Vec::new(); + let mut arg_indices = Vec::new(); + + let mut index = 0; + for arg in &func.sig.inputs { + if let FnArg::Typed(pat_type) = arg { + let ty = &pat_type.ty; + arg_types.push(quote! { #ty }); + + let arg_name = if let Pat::Ident(pat_ident) = &*pat_type.pat { + let ident = &pat_ident.ident; + quote! { #ident } + } else { + quote! { _arg } + }; + arg_names.push(arg_name); + + let idx = syn::Index::from(index); + arg_indices.push(quote! { self.#idx }); + index += 1; + } + } + + (arg_types, arg_names, arg_indices) +} + +fn output_ty_for(func: &ItemFn) -> proc_macro2::TokenStream { + match &func.sig.output { + syn::ReturnType::Default => quote! { () }, + syn::ReturnType::Type(_, ty) => quote! { #ty }, + } +} + +fn tuple_ty_for(arg_types: &[proc_macro2::TokenStream]) -> proc_macro2::TokenStream { + if arg_types.is_empty() { + quote! { () } + } else { + quote! { (#(#arg_types),*,) } + } +} + +fn generate_free_functions(functions: Vec) -> TokenStream { + let fn_name = &functions[0].sig.ident; + let trait_name = trait_name_for(fn_name); let mut impls = Vec::new(); for func in &functions { - // Collect All arguments and names - let mut arg_types = Vec::new(); - let mut arg_names = Vec::new(); - let mut arg_indices = Vec::new(); + let (arg_types, arg_names, arg_indices) = collect_args(func); + let output_ty = output_ty_for(func); + let tuple_ty = tuple_ty_for(&arg_types); let block = &func.block; - for (i, arg) in func.sig.inputs.iter().enumerate() { - if let FnArg::Typed(pat_type) = arg { - let ty = &pat_type.ty; - arg_types.push(quote! { #ty }); - - let arg_name = if let Pat::Ident(pat_ident) = &*pat_type.pat { - let ident = &pat_ident.ident; - quote! { #ident } - } else { - quote! { _arg } - }; - arg_names.push(arg_name); - - let index = syn::Index::from(i); - arg_indices.push(quote! { self.#index }); + + impls.push(quote! { + impl #trait_name for #tuple_ty { + type Output = #output_ty; + fn call(self) -> Self::Output { + #(let #arg_names = #arg_indices;)* + #block + } } + }); + } + + let generated = quote! { + trait #trait_name: std::marker::Tuple { + type Output; + fn call(self) -> Self::Output; } - let output_ty = match &func.sig.output { - syn::ReturnType::Default => quote! { () }, - syn::ReturnType::Type(_, ty) => quote! { #ty }, + #(#impls)* + + fn #fn_name(#[rustc_splat] args: T) -> T::Output { + args.call() + } + }; + + generated.into() +} + +fn generate_methods(self_ty: syn::Ident, functions: Vec) -> TokenStream { + let fn_name = &functions[0].sig.ident; + let trait_name = trait_name_for(fn_name); + + let (is_ref, is_mut) = { + let first_receiver = match functions[0].sig.inputs.first() { + Some(FnArg::Receiver(r)) => r, + _ => panic!("overload! methods must take self"), }; + for func in &functions { + match func.sig.inputs.first() { + Some(FnArg::Receiver(r)) => { + let same = r.reference.is_some() == first_receiver.reference.is_some() + && r.mutability.is_some() == first_receiver.mutability.is_some(); + if !same { + panic!( + "all overloads must use the same receiver kind (&self, &mut self, or self)" + ); + } + } + _ => panic!("overload! methods must take self"), + } + } + ( + first_receiver.reference.is_some(), + first_receiver.mutability.is_some(), + ) + }; - // Check if there are no arguments - let tuple_ty = if arg_types.is_empty() { - quote! { () } - } else { - quote! { (#(#arg_types),*,) } + let this_generic_ty = match (is_ref, is_mut) { + (true, true) => quote! { &mut R }, + (true, false) => quote! { &R }, + (false, _) => quote! { R }, + }; + + let this_concrete_ty = match (is_ref, is_mut) { + (true, true) => quote! { &mut #self_ty }, + (true, false) => quote! { &#self_ty }, + (false, _) => quote! { #self_ty }, + }; + + let mut impls = Vec::new(); + let mut hidden_methods = Vec::new(); + + for (i, func) in functions.iter().enumerate() { + let (arg_types, arg_names, arg_indices) = collect_args(func); + let output_ty = output_ty_for(func); + let tuple_ty = tuple_ty_for(&arg_types); + let block = &func.block; + let func_receiver = match func.sig.inputs.first() { + Some(FnArg::Receiver(r)) => quote! { #r }, + _ => panic!("overload! methods must take self"), }; + let hidden_name = quote::format_ident!("__{}_impl_{}", fn_name, i); + + hidden_methods.push(quote! { + fn #hidden_name(#func_receiver, #(#arg_names: #arg_types),*) -> #output_ty { + #block + } + }); + impls.push(quote! { - impl #trait_name for #tuple_ty { + impl #trait_name<#self_ty> for #tuple_ty { type Output = #output_ty; - fn call(self) -> Self::Output{ + fn call(self, this: #this_concrete_ty) -> Self::Output { #(let #arg_names = #arg_indices;)* - #block + this.#hidden_name(#(#arg_names),*) } } }); } + + let receiver = match functions[0].sig.inputs.first() { + Some(FnArg::Receiver(r)) => quote! { #r }, + _ => unreachable!(), + }; + let generated = quote! { - trait #trait_name: std::marker::Tuple { + trait #trait_name: std::marker::Tuple { type Output; - fn call(self) -> Self::Output; + fn call(self, this: #this_generic_ty) -> Self::Output; } #(#impls)* - fn #fn_name(#[rustc_splat] args: T) -> T::Output{ - args.call() + impl #self_ty { + #(#hidden_methods)* + + fn #fn_name>(#receiver, #[rustc_splat] args: T) -> T::Output { + args.call(self) + } } }; generated.into() } + +#[proc_macro] +pub fn overload(input: TokenStream) -> TokenStream { + match parse_macro_input!(input as OverloadInput) { + OverloadInput::Functions(functions) => generate_free_functions(functions), + OverloadInput::Methods { self_ty, functions } => generate_methods(self_ty, functions), + } +}