WIP: switch to announcement, remove extract input mess
This commit is contained in:
48 files changed
+685
-1187
No files matched your search
+135
-172
@@ -1,17 +1,17 @@
|
||||
//! Procedural macros for dagx
|
||||
//!
|
||||
//! This crate provides the `#[task]` attribute macro that automatically implements
|
||||
//! the `Task` trait by deriving Input and Output types from the `run()` method signature.
|
||||
//! This crate provides the `#[task]` attribute macro that automatically generates
|
||||
//! an `execute()` method by deriving Input and Output types from the `run()` method signature.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType, Type};
|
||||
|
||||
/// Attribute macro to automatically implement the `Task` trait.
|
||||
/// Attribute macro to automatically generate the execution logic for a task.
|
||||
///
|
||||
/// Apply this to an `impl` block containing a `run()` method (sync or async). The macro:
|
||||
/// - Derives `Input` and `Output` types from the `run()` signature
|
||||
/// - Automatically implements the `Task` trait
|
||||
/// - Derives input types from the `run()` signature parameters
|
||||
/// - Generates an `execute()` method that handles listener extraction and announcement
|
||||
/// - Supports both sync and async run methods
|
||||
/// - Supports stateless (no self) and stateful (&self, &mut self) tasks
|
||||
/// - Handles various input patterns (no inputs, single input, multiple inputs)
|
||||
@@ -25,7 +25,7 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType
|
||||
/// Unit structs for pure computations. Use **no `self` parameter**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct Add;
|
||||
///
|
||||
@@ -42,7 +42,7 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType
|
||||
/// Tasks that read configuration or constant data. Use **`&self`**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct Multiplier {
|
||||
/// factor: i32,
|
||||
@@ -61,7 +61,7 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType
|
||||
/// Tasks that accumulate or modify state. Use **`&mut self`**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
@@ -81,7 +81,7 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType
|
||||
/// ## No Inputs (Source Tasks)
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct LoadData {
|
||||
/// value: i32,
|
||||
@@ -93,110 +93,51 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType
|
||||
/// self.value
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// ## Single Input
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct Double;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Double {
|
||||
/// async fn run(&mut self, input: &i32) -> i32 {
|
||||
/// async fn run(input: &i32) -> i32 {
|
||||
/// input * 2
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// ## Multiple Inputs (up to 8)
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
/// use dagx::task;
|
||||
///
|
||||
/// struct Combine;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Combine {
|
||||
/// async fn run(&mut self, a: &i32, b: &String, c: &bool) -> String {
|
||||
/// async fn run(a: &i32, b: &String, c: &bool) -> String {
|
||||
/// format!("{}: {} ({})", b, a, c)
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
/// - The impl block must contain exactly one `async fn run()` method
|
||||
/// - The `run()` method can be stateless (no self parameter) or stateful (`&mut self`)
|
||||
/// - All input parameters must be references (e.g., `&i32`, not `i32`)
|
||||
/// - The macro requires `Task` to be in scope: `use dagx::Task;`
|
||||
/// - For stateless tasks, the struct must implement `Default` (e.g., unit structs)
|
||||
///
|
||||
/// # Generated Code
|
||||
///
|
||||
/// The macro transforms your implementation into a full `Task` trait implementation.
|
||||
///
|
||||
/// For stateless tasks (no self parameter):
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Your code:
|
||||
/// #[task]
|
||||
/// impl Add {
|
||||
/// async fn run(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Add {
|
||||
/// type Input = (i32, i32);
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let (a, b) = input;
|
||||
/// Self::run_impl(&a, &b).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Add {
|
||||
/// #[inline]
|
||||
/// async fn run_impl(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
///
|
||||
/// For stateful tasks (with &mut self):
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Your code:
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Counter {
|
||||
/// type Input = i32;
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let inc = input;
|
||||
/// self.run_impl(&inc).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Counter {
|
||||
/// async fn run_impl(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// The macro transforms your implementation into an `execute()` method that:
|
||||
/// - Extracts inputs from announcement listeners
|
||||
/// - Calls your run() method
|
||||
/// - Announces the result to dependents
|
||||
/// - Returns the Arc-wrapped output
|
||||
#[proc_macro_attribute]
|
||||
pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let impl_block = parse_macro_input!(item as ItemImpl);
|
||||
@@ -220,7 +161,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
"impl block must contain a run() method\n\n\
|
||||
Expected signature: fn run(&mut self, ...) -> OutputType\n\
|
||||
or: async fn run(&mut self, ...) -> OutputType\n\
|
||||
The #[task] macro requires a run() method to implement the Task trait.",
|
||||
The #[task] macro requires a run() method to implement task execution.",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
@@ -287,31 +228,6 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
ReturnType::Type(_, ty) => ty.clone(),
|
||||
};
|
||||
|
||||
// Build Input type based on parameter count
|
||||
let input_type = match params.len() {
|
||||
0 => quote! { () },
|
||||
1 => {
|
||||
let (_name, ty) = ¶ms[0];
|
||||
quote! { #ty }
|
||||
}
|
||||
_ => {
|
||||
let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
|
||||
quote! { ( #(#types),* ) }
|
||||
}
|
||||
};
|
||||
|
||||
// Generate parameter destructuring for the wrapper run() method
|
||||
let (param_destructure, param_refs) = if params.is_empty() {
|
||||
(quote! { _ }, quote! {})
|
||||
} else if params.len() == 1 {
|
||||
let (name, _) = ¶ms[0];
|
||||
(quote! { #name }, quote! { &#name })
|
||||
} else {
|
||||
let names: Vec<_> = params.iter().map(|(name, _)| name).collect();
|
||||
let refs: Vec<_> = params.iter().map(|(name, _)| quote! { &#name }).collect();
|
||||
(quote! { ( #(#names),* ) }, quote! { #(#refs),* })
|
||||
};
|
||||
|
||||
// Clone the run method and rename it to run_impl
|
||||
let mut run_impl_method = run_method.clone();
|
||||
run_impl_method.sig.ident = syn::Ident::new("run_impl", run_method.sig.ident.span());
|
||||
@@ -323,81 +239,128 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
.iter()
|
||||
.any(|arg| matches!(arg, FnArg::Receiver(_)));
|
||||
|
||||
// Generate the Task trait implementation based on whether we have self and async/sync
|
||||
let expanded = if has_self_receiver {
|
||||
// Stateful task - consumes self but delegates to a method that borrows
|
||||
// Generate listener extraction code
|
||||
let listener_extractions: Vec<_> = params
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, ty))| {
|
||||
quote! {
|
||||
let #name = {
|
||||
if listeners.len() <= #i {
|
||||
return Err(format!(
|
||||
"Expected at least {} listener(s), got {}",
|
||||
#i + 1,
|
||||
listeners.len()
|
||||
));
|
||||
}
|
||||
let listener = listeners[#i]
|
||||
.downcast_ref::<announcement::Listener<#ty>>()
|
||||
.ok_or_else(|| {
|
||||
format!("Type mismatch: expected Listener<{}> at index {}",
|
||||
stringify!(#ty), #i)
|
||||
})?;
|
||||
listener.listen().await
|
||||
.map_err(|_| "Listener closed before receiving value")?
|
||||
};
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Generate parameter refs for the run call
|
||||
let param_refs: Vec<_> = params.iter().map(|(name, _)| quote! { &#name }).collect();
|
||||
|
||||
// Generate the execute method body based on whether we have self and async/sync
|
||||
let run_call = if has_self_receiver {
|
||||
if is_async {
|
||||
// Async with self
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(mut self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
self.run_impl(#param_refs).await
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
quote! { self.run_impl(#(#param_refs),*).await }
|
||||
} else {
|
||||
// Sync with self - wrap in async block
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(mut self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
self.run_impl(#param_refs)
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
quote! { self.run_impl(#(#param_refs),*) }
|
||||
}
|
||||
} else if is_async {
|
||||
quote! { Self::run_impl(#(#param_refs),*).await }
|
||||
} else {
|
||||
// Stateless task
|
||||
if is_async {
|
||||
// Async stateless
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
quote! { Self::run_impl(#(#param_refs),*) }
|
||||
};
|
||||
|
||||
async fn run(self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
Self::run_impl(#param_refs).await
|
||||
}
|
||||
}
|
||||
// Generate the self parameter for execute method
|
||||
let self_param = if has_self_receiver {
|
||||
quote! { mut self }
|
||||
} else {
|
||||
quote! { self }
|
||||
};
|
||||
|
||||
impl #struct_name {
|
||||
#[inline]
|
||||
#run_impl_method
|
||||
}
|
||||
// Generate the Input type based on parameter count
|
||||
let input_type = match params.len() {
|
||||
0 => quote! { () },
|
||||
1 => {
|
||||
let (_, ty) = ¶ms[0];
|
||||
quote! { #ty }
|
||||
}
|
||||
_ => {
|
||||
let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
|
||||
quote! { (#(#types,)*) }
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the complete execute method and TaskExecute trait implementation
|
||||
let expanded = quote! {
|
||||
impl #struct_name {
|
||||
#[doc(hidden)]
|
||||
pub async fn execute(
|
||||
#self_param,
|
||||
listeners: Vec<Box<dyn std::any::Any + Send>>,
|
||||
announcer: Box<dyn std::any::Any + Send>,
|
||||
) -> Result<std::sync::Arc<dyn std::any::Any + Send + Sync>, String> {
|
||||
// Extract inputs from listeners
|
||||
#(#listener_extractions)*
|
||||
|
||||
// Call the user's run method
|
||||
let output: #output_type = #run_call;
|
||||
|
||||
// Wrap in Arc for sharing
|
||||
let output_arc = std::sync::Arc::new(output);
|
||||
|
||||
// Announce the result
|
||||
let announcer_typed = announcer
|
||||
.downcast::<announcement::Announcer<#output_type>>()
|
||||
.map_err(|_| "Type mismatch: announcer type does not match task output type")?;
|
||||
let _ = announcer_typed.announce((*output_arc).clone());
|
||||
|
||||
// Return Arc-wrapped output
|
||||
Ok(output_arc as std::sync::Arc<dyn std::any::Any + Send + Sync>)
|
||||
}
|
||||
} else {
|
||||
// Sync stateless
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
Self::run_impl(#param_refs)
|
||||
}
|
||||
}
|
||||
#run_impl_method
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#[inline]
|
||||
#run_impl_method
|
||||
}
|
||||
// Implement TaskExecute trait for type erasure
|
||||
impl dagx::TaskExecute for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
fn execute(
|
||||
self,
|
||||
listeners: Vec<Box<dyn std::any::Any + Send>>,
|
||||
announcer: Box<dyn std::any::Any + Send>,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::sync::Arc<dyn std::any::Any + Send + Sync>, String>> + Send>> {
|
||||
Box::pin(async move {
|
||||
self.execute(listeners, announcer).await
|
||||
})
|
||||
}
|
||||
|
||||
fn create_announcement_erased() -> (Box<dyn std::any::Any + Send>, Box<dyn std::any::Any + Send>) {
|
||||
let (announcer, announcement) = announcement::Announcement::<#output_type>::new();
|
||||
(
|
||||
Box::new(announcer) as Box<dyn std::any::Any + Send>,
|
||||
Box::new(announcement) as Box<dyn std::any::Any + Send>,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_listener_erased(announcement: &dyn std::any::Any) -> Box<dyn std::any::Any + Send> {
|
||||
let announcement = announcement
|
||||
.downcast_ref::<announcement::Announcement<#output_type>>()
|
||||
.expect("Type mismatch: announcement type does not match task output type");
|
||||
Box::new(announcement.listener()) as Box<dyn std::any::Any + Send>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user