Skip to content

Commit eb43063

Browse files
committed
[hyperactor] support generic behaviors; implement Mesh resource behavior
Pull Request resolved: #1765 `hyperactor::behavior!` is expanded to support generic type parameters, allowing for parameterized behaviors. We use this new functionality to formally define what a mesh controller behavior is, by bundling related types into a trait, so that we can write `Controller<SomeMeshType>`. Mesh controllers should follow this behavior. The plan is then to expand this to a set of generic code, tools, and so on, that can be parameterized over different implementations of `Mesh`. ghstack-source-id: 322839080 Differential Revision: [D86420507](https://our.internmc.facebook.com/intern/diff/D86420507/) **NOTE FOR REVIEWERS**: This PR has internal Meta-specific changes or comments, please review them on [Phabricator](https://our.internmc.facebook.com/intern/diff/D86420507/)!
1 parent 5dbfdf8 commit eb43063

File tree

3 files changed

+194
-19
lines changed

3 files changed

+194
-19
lines changed

hyperactor/example/derive.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,7 @@ async fn main() -> Result<(), anyhow::Error> {
141141
// Spawn our actor, and get a handle for rank 0.
142142
let shopping_list_actor: hyperactor::ActorHandle<ShoppingListActor> =
143143
proc.spawn("shopping", ()).await?;
144-
// We attest this is safe because we know this is the id of an
145-
// actor we just spawned.
146-
let shopping_api: hyperactor::ActorRef<ShoppingApi> =
147-
hyperactor::ActorRef::attest(shopping_list_actor.actor_id().clone());
144+
let shopping_api: hyperactor::ActorRef<ShoppingApi> = shopping_list_actor.bind();
148145
// We join the system, so that we can send messages to actors.
149146
let (client, _) = proc.instance("client").unwrap();
150147

hyperactor_macros/src/lib.rs

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,16 +1606,22 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
16061606
/// Represents the full input to [`fn behavior`].
16071607
struct BehaviorInput {
16081608
behavior: Ident,
1609+
generics: syn::Generics,
16091610
handlers: Vec<HandlerSpec>,
16101611
}
16111612

16121613
impl syn::parse::Parse for BehaviorInput {
16131614
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
16141615
let behavior: Ident = input.parse()?;
1616+
let generics: syn::Generics = input.parse()?;
16151617
let _: Token![,] = input.parse()?;
16161618
let raw_handlers = input.parse_terminated(HandlerSpec::parse, Token![,])?;
16171619
let handlers = raw_handlers.into_iter().collect();
1618-
Ok(BehaviorInput { behavior, handlers })
1620+
Ok(BehaviorInput {
1621+
behavior,
1622+
generics,
1623+
handlers,
1624+
})
16191625
}
16201626
}
16211627

@@ -1634,20 +1640,118 @@ impl syn::parse::Parse for BehaviorInput {
16341640
/// u64,
16351641
/// );
16361642
/// ```
1643+
///
1644+
/// This macro also supports generic behaviors:
1645+
/// ```
1646+
/// hyperactor::behavior!(
1647+
/// TestBehavior<T>,
1648+
/// Message<T> { castable = true },
1649+
/// u64,
1650+
/// );
1651+
/// ```
16371652
#[proc_macro]
16381653
pub fn behavior(input: TokenStream) -> TokenStream {
1639-
let BehaviorInput { behavior, handlers } = parse_macro_input!(input as BehaviorInput);
1654+
let BehaviorInput {
1655+
behavior,
1656+
generics,
1657+
handlers,
1658+
} = parse_macro_input!(input as BehaviorInput);
16401659
let tys = HandlerSpec::add_indexed(handlers);
16411660

1661+
// Add bounds to generics for Named, Serialize, Deserialize
1662+
let mut bounded_generics = generics.clone();
1663+
for param in bounded_generics.type_params_mut() {
1664+
param.bounds.push(syn::parse_quote!(hyperactor::Named));
1665+
param.bounds.push(syn::parse_quote!(serde::Serialize));
1666+
param.bounds.push(syn::parse_quote!(std::marker::Send));
1667+
param.bounds.push(syn::parse_quote!(std::marker::Sync));
1668+
param.bounds.push(syn::parse_quote!(std::fmt::Debug));
1669+
// Note: lifetime parameters are not *actually* hygienic.
1670+
// https://github.com/rust-lang/rust/issues/54727
1671+
let lifetime =
1672+
syn::Lifetime::new("'hyperactor_behavior_de", proc_macro2::Span::mixed_site());
1673+
param
1674+
.bounds
1675+
.push(syn::parse_quote!(for<#lifetime> serde::Deserialize<#lifetime>));
1676+
}
1677+
1678+
// Split the generics for use in different contexts
1679+
let (impl_generics, ty_generics, where_clause) = bounded_generics.split_for_impl();
1680+
1681+
// Create a combined generics for the Binds impl that includes both A and the behavior's generics
1682+
let mut binds_generics = bounded_generics.clone();
1683+
binds_generics.params.insert(
1684+
0,
1685+
syn::GenericParam::Type(syn::TypeParam {
1686+
attrs: vec![],
1687+
ident: Ident::new("A", proc_macro2::Span::call_site()),
1688+
colon_token: None,
1689+
bounds: Punctuated::new(),
1690+
eq_token: None,
1691+
default: None,
1692+
}),
1693+
);
1694+
let (binds_impl_generics, _, _) = binds_generics.split_for_impl();
1695+
1696+
// Determine typename and typehash implementation based on whether we have generics
1697+
let type_params: Vec<_> = bounded_generics.type_params().collect();
1698+
let has_generics = !type_params.is_empty();
1699+
1700+
let (typename_impl, typehash_impl) = if has_generics {
1701+
// Create format string with placeholders for each generic parameter
1702+
let placeholders = vec!["{}"; type_params.len()].join(", ");
1703+
let placeholders_format_string = format!("<{}>", placeholders);
1704+
let format_string = quote! { concat!(std::module_path!(), "::", stringify!(#behavior), #placeholders_format_string) };
1705+
1706+
let type_param_idents: Vec<_> = type_params.iter().map(|p| &p.ident).collect();
1707+
(
1708+
quote! {
1709+
hyperactor::data::intern_typename!(Self, #format_string, #(#type_param_idents),*)
1710+
},
1711+
quote! {
1712+
hyperactor::cityhasher::hash(Self::typename())
1713+
},
1714+
)
1715+
} else {
1716+
(
1717+
quote! {
1718+
concat!(std::module_path!(), "::", stringify!(#behavior))
1719+
},
1720+
quote! {
1721+
static TYPEHASH: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| {
1722+
hyperactor::cityhasher::hash(<#behavior as hyperactor::data::Named>::typename())
1723+
});
1724+
*TYPEHASH
1725+
},
1726+
)
1727+
};
1728+
1729+
let type_param_idents = generics.type_params().map(|p| &p.ident).collect::<Vec<_>>();
1730+
16421731
let expanded = quote! {
16431732
#[doc = "The generated behavior struct."]
1644-
#[derive(Debug, hyperactor::Named, serde::Serialize, serde::Deserialize)]
1645-
pub struct #behavior;
1646-
impl hyperactor::actor::Referable for #behavior {}
1733+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1734+
pub struct #behavior #impl_generics #where_clause {
1735+
_phantom: std::marker::PhantomData<(#(#type_param_idents),*)>
1736+
}
16471737

1648-
impl<A> hyperactor::actor::Binds<A> for #behavior
1738+
impl #impl_generics hyperactor::Named for #behavior #ty_generics #where_clause {
1739+
fn typename() -> &'static str {
1740+
#typename_impl
1741+
}
1742+
1743+
fn typehash() -> u64 {
1744+
#typehash_impl
1745+
}
1746+
}
1747+
1748+
impl #impl_generics hyperactor::actor::Referable for #behavior #ty_generics #where_clause {}
1749+
1750+
impl #binds_impl_generics hyperactor::actor::Binds<A> for #behavior #ty_generics
16491751
where
1650-
A: hyperactor::Actor #(+ hyperactor::Handler<#tys>)* {
1752+
A: hyperactor::Actor #(+ hyperactor::Handler<#tys>)*,
1753+
#where_clause
1754+
{
16511755
fn bind(ports: &hyperactor::proc::Ports<A>) {
16521756
#(
16531757
ports.bind::<#tys>();
@@ -1656,7 +1760,7 @@ pub fn behavior(input: TokenStream) -> TokenStream {
16561760
}
16571761

16581762
#(
1659-
impl hyperactor::actor::RemoteHandles<#tys> for #behavior {}
1763+
impl #impl_generics hyperactor::actor::RemoteHandles<#tys> for #behavior #ty_generics #where_clause {}
16601764
)*
16611765
};
16621766

hyperactor_mesh/src/resource/mesh.rs

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,84 @@ pub struct State<S> {
4444
state: S,
4545
}
4646

47-
// The behavior of a mesh controllšr.
48-
// hyperactor::behavior!(
49-
// Controller<Sp, St>,
50-
// CreateOrUpdate<Spec<Sp>>,
51-
// GetState<State<St>>,
52-
// Stop,
53-
// );
47+
/// A mesh trait bundles a set of types that together define a mesh resource.
48+
pub trait Mesh {
49+
/// The mesh-specific specification for this resource.
50+
type Spec: Named + Serialize + for<'de> Deserialize<'de> + Send + Sync + std::fmt::Debug;
51+
52+
/// The mesh-specific state for thsi resource.
53+
type State: Named + Serialize + for<'de> Deserialize<'de> + Send + Sync + std::fmt::Debug;
54+
}
55+
56+
// A behavior defining the interface for a mesh controller.
57+
hyperactor::behavior!(
58+
Controller<M: Mesh>,
59+
CreateOrUpdate<Spec<M::Spec>>,
60+
GetState<State<M::State>>,
61+
Stop,
62+
);
63+
64+
#[cfg(test)]
65+
mod test {
66+
use hyperactor::Actor;
67+
use hyperactor::ActorRef;
68+
use hyperactor::Context;
69+
use hyperactor::Handler;
70+
71+
use super::*;
72+
73+
// Consider upstreaming this into `hyperactor` -- lightweight handler definitions
74+
// can be quite useful.
75+
macro_rules! handler {
76+
(
77+
$actor:path,
78+
$(
79+
$name:ident: $msg:ty => $body:expr
80+
),* $(,)?
81+
) => {
82+
$(
83+
#[async_trait::async_trait]
84+
impl Handler<$msg> for $actor {
85+
async fn handle(
86+
&mut self,
87+
#[allow(unused_variables)]
88+
cx: & Context<Self>,
89+
$name: $msg
90+
) -> anyhow::Result<()> {
91+
$body
92+
}
93+
}
94+
)*
95+
};
96+
}
97+
98+
#[derive(Debug, Named, Serialize, Deserialize)]
99+
struct TestMesh;
100+
101+
impl Mesh for TestMesh {
102+
type Spec = ();
103+
type State = ();
104+
}
105+
106+
#[derive(Actor, Debug, Default, Named, Serialize, Deserialize)]
107+
struct TestMeshController;
108+
109+
// Ensure that TestMeshController conforms to the Controller behavior for TestMesh.
110+
handler! {
111+
TestMeshController,
112+
_message: CreateOrUpdate<Spec<()>> => unimplemented!(),
113+
_message: GetState<State<()>> => unimplemented!(),
114+
_message: Stop => unimplemented!(),
115+
}
116+
117+
#[test]
118+
fn test_controller_behavior() {
119+
use hyperactor::ActorHandle;
120+
121+
// This is a compile-time check that TestMeshController implements
122+
// the Controller<TestMesh> behavior correctly.
123+
fn _assert_bind(handle: ActorHandle<TestMeshController>) -> ActorRef<Controller<TestMesh>> {
124+
handle.bind()
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)