Compare commits

...

4 commits

Author SHA1 Message Date
023a2a17fc Add more ergonomic constructor of ActorHandles
Signed-off-by: Marcel Müller <neikos@neikos.email>
2025-11-05 14:48:51 +01:00
d38b04396c Add Actor interface
This is the early idea. The error message is still not great if the
HandledMessages don't fit the Actor. (Aka forgot to impl Handle>

Signed-off-by: Marcel Müller <neikos@neikos.email>
2025-11-05 14:21:38 +01:00
bf3e65a6e2 Re-order internals
Signed-off-by: Marcel Müller <neikos@neikos.email>
2025-11-05 10:59:42 +01:00
db03733d3c Adapt internals
Signed-off-by: Marcel Müller <neikos@neikos.email>
2025-11-05 10:32:52 +01:00
6 changed files with 516 additions and 145 deletions

View file

@ -0,0 +1,147 @@
use std::any::Any;
use std::marker::PhantomData;
use std::pin::Pin;
use crate::InternalMessage;
use crate::IsContainedInBundle;
use crate::Message;
use crate::MessageBundle;
pub trait Actor: Any
where
Self: ActorHandler<Self::HandledMessages>,
{
type HandledMessages: MessageBundle;
}
pub trait IntoActorHandle<MB> {
fn into_actor_handle(self) -> ActorHandle<MB>;
}
impl<A: Actor> IntoActorHandle<A::HandledMessages> for A {
fn into_actor_handle(self) -> ActorHandle<A::HandledMessages> {
ActorHandle::new(self)
}
}
pub trait Handle<M: Message> {
fn handle(&mut self, message: M) -> impl Future<Output = anyhow::Result<M::Reply>>;
}
pub trait ActorHandler<MB>: Any {
fn handle_message(
&mut self,
message: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>>;
}
macro_rules! impl_actor_handle {
( $($ty:ident),* ) => {
impl<ACTOR, $($ty,)*> ActorHandler<($($ty,)*)> for ACTOR
where
ACTOR: Actor,
$( ACTOR: Handle<$ty>, )*
$( $ty: Message, )*
{
fn handle_message(
&mut self,
msg: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>> {
#[allow(unused_variables)]
async {
$(
let msg = match msg.into_inner::<$ty>() {
Ok(msg) => {
return <Self as Handle<$ty>>::handle(self, msg).await.map(InternalMessage::new);
}
Err(msg) => msg,
};
)*
Err(anyhow::anyhow!("Could not handle message"))
}
}
}
};
}
all_the_tuples!(impl_actor_handle);
pub struct ActorHandle<MB> {
actor: Box<dyn Any>,
#[allow(clippy::type_complexity)]
handle: for<'a> fn(
&'a mut dyn Any,
msg: InternalMessage,
) -> Pin<Box<dyn Future<Output = anyhow::Result<InternalMessage>> + 'a>>,
_pd: PhantomData<fn(MB)>,
}
impl<MB: MessageBundle + 'static> ActorHandle<MB> {
pub fn new<A: ActorHandler<MB>>(actor: A) -> ActorHandle<MB> {
ActorHandle {
actor: Box::new(actor),
handle: |actor, msg| {
let actor: &mut A = actor.downcast_mut().unwrap();
Box::pin(actor.handle_message(msg))
},
_pd: PhantomData,
}
}
pub async fn handle<M: Message + IsContainedInBundle<MB>>(
&mut self,
message: M,
) -> anyhow::Result<M::Reply> {
const {
let true = <M as IsContainedInBundle<MB>>::IS_CONTAINED else {
panic!("Message is not contained in MessageBundle",);
};
}
(self.handle)(self.actor.as_mut(), InternalMessage::new(message))
.await
.map(|msg| match msg.into_inner::<M::Reply>() {
Ok(t) => t,
Err(msg) => panic!("Could not process reply of type: {}", msg.type_name()),
})
}
}
#[cfg(test)]
mod tests {
use macro_rules_attribute::apply;
use smol_macros::test;
use super::*;
struct Foo;
impl Message for Foo {
type Reply = ();
}
struct FActor;
impl Actor for FActor {
type HandledMessages = (Foo,);
}
impl Handle<Foo> for FActor {
async fn handle(&mut self, _message: Foo) -> anyhow::Result<<Foo as Message>::Reply> {
Ok(())
}
}
#[apply(test!)]
async fn test_name() {
let mut actor = FActor.into_actor_handle();
actor.handle(Foo).await.unwrap();
}
}

View file

@ -0,0 +1,87 @@
use std::any::Any;
use std::marker::PhantomData;
use std::pin::Pin;
use crate::InternalMessage;
use crate::IsContainedInBundle;
use crate::Message;
use crate::MessageBundle;
pub trait Address<MB> {
fn send<M: Message>(&mut self, message: M) -> impl Future<Output = anyhow::Result<M::Reply>>;
}
pub struct BoxedAddress<MB> {
addr: Box<dyn Any>,
#[allow(clippy::type_complexity)]
send: for<'a> fn(
&'a mut dyn Any,
InternalMessage,
) -> Pin<Box<dyn Future<Output = anyhow::Result<InternalMessage>> + 'a>>,
_pd: PhantomData<fn(MB)>,
}
impl<MB> BoxedAddress<MB> {
pub fn new<IMH>(addr: IMH) -> Self
where
IMH: InternalMessageHandler<HandledMessages = MB> + 'static,
{
BoxedAddress {
addr: Box::new(addr),
send: |addr, msg| {
let addr: &mut IMH = addr.downcast_mut().unwrap();
Box::pin(addr.handle_message(msg))
},
_pd: PhantomData,
}
}
}
impl<MB> InternalMessageHandler for BoxedAddress<MB>
where
MB: MessageBundle,
{
type HandledMessages = MB;
fn handle_message(
&mut self,
msg: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>> {
(self.send)(self.addr.as_mut(), msg)
}
}
pub trait InternalMessageHandler {
type HandledMessages: MessageBundle;
fn handle_message(
&mut self,
msg: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>>;
}
impl<IMH> Address<IMH::HandledMessages> for IMH
where
IMH: InternalMessageHandler,
{
fn send<M: Message>(&mut self, message: M) -> impl Future<Output = anyhow::Result<M::Reply>> {
const {
let true = <M as IsContainedInBundle<IMH::HandledMessages>>::IS_CONTAINED else {
panic!("Message is not contained in MessageBundle",);
};
}
let message = self.handle_message(InternalMessage::new(message));
async {
message.await.and_then(|msg| {
msg.into_inner::<M::Reply>().map_err(|e| {
anyhow::anyhow!(
"Expected a {}, but got a {}",
std::any::type_name::<M::Reply>(),
e.type_name()
)
})
})
}
}
}

View file

@ -1,114 +1,21 @@
#![feature(const_cmp)]
#![feature(const_trait_impl)]
use std::any::Any;
#[macro_use]
mod macros;
pub mod address;
pub mod message;
pub mod actor;
use std::any::TypeId;
pub use address::Address;
pub use address::BoxedAddress;
pub use address::InternalMessageHandler;
pub use anyhow;
pub trait Message: Send + Any {
type Reply: Send + Any;
}
pub trait MessageIdentifier {
const IDENT: BundleChain;
}
impl<M: Message> MessageIdentifier for M {
const IDENT: BundleChain = BundleChain::of::<M>();
}
pub struct InternalMessage {
value: Box<dyn std::any::Any>,
name: &'static str,
}
impl InternalMessage {
pub fn new<M: Any>(message: M) -> InternalMessage {
InternalMessage {
value: Box::new(message),
name: std::any::type_name::<M>(),
}
}
pub fn into_inner<M: Any>(self) -> Result<M, InternalMessage> {
self.value
.downcast()
.map(|v| *v)
.map_err(|value| InternalMessage {
value,
name: self.name,
})
}
pub fn as_ref<M: Any>(&self) -> Option<&M> {
self.value.downcast_ref()
}
pub fn type_name(&self) -> &'static str {
self.name
}
pub fn type_id(&self) -> TypeId {
self.value.as_ref().type_id()
}
}
pub trait Address<MB> {
fn send<M: Message>(&mut self, message: M) -> impl Future<Output = anyhow::Result<M::Reply>>;
}
pub trait InternalMessageHandler {
type HandledMessages: MessageBundle;
fn handle_message(
&mut self,
msg: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>>;
}
impl<IMH> Address<IMH::HandledMessages> for IMH
where
IMH: InternalMessageHandler,
{
fn send<M: Message>(&mut self, message: M) -> impl Future<Output = anyhow::Result<M::Reply>> {
const {
let true = <M as IsContainedInBundle<IMH::HandledMessages>>::IS_CONTAINED else {
panic!("Message is not contained in MessageBundle",);
};
}
let message = self.handle_message(InternalMessage::new(message));
async {
message.await.and_then(|msg| {
msg.into_inner::<M::Reply>().map_err(|e| {
anyhow::anyhow!(
"Expected a {}, but got a {}",
std::any::type_name::<M::Reply>(),
e.type_name()
)
})
})
}
}
}
pub trait MessageReceiver {}
pub trait MessageBundle {
const IDS: BundleChain;
}
impl MessageBundle for () {
const IDS: BundleChain = BundleChain {
next: None,
op: BundleOp::Remove(TypeId::of::<()>()),
};
}
impl<M: Message> MessageBundle for (M,) {
const IDS: BundleChain = BundleChain::of::<M>();
}
pub use message::InternalMessage;
pub use message::Message;
pub use message::MessageBundle;
#[derive(Debug, Clone, Copy)]
pub struct BundleChain {
@ -117,13 +24,21 @@ pub struct BundleChain {
}
#[derive(Debug, Clone, Copy)]
pub enum BundleOp {
enum BundleOp {
Add(TypeId),
Remove(TypeId),
Chain(&'static BundleChain),
None,
}
impl BundleChain {
pub const fn empty() -> BundleChain {
BundleChain {
op: BundleOp::None,
next: None,
}
}
pub const fn of<M: Message>() -> BundleChain {
BundleChain {
op: BundleOp::Add(TypeId::of::<M>()),
@ -135,6 +50,40 @@ impl BundleChain {
check_is_contained(self, id)
}
pub const fn is_subset_of(&self, ids: &BundleChain) -> bool {
let mut current = self;
loop {
match current.op {
BundleOp::Add(type_id) => {
if !ids.contains(type_id) {
return false;
}
}
BundleOp::Remove(..) => (),
BundleOp::Chain(bundle_chain) => {
let chain_result = bundle_chain.is_subset_of(ids);
let own_result = if let Some(bc) = self.next {
bc.is_subset_of(ids)
} else {
true
};
return chain_result && own_result;
}
BundleOp::None => (),
}
if let Some(next) = current.next {
current = next;
} else {
break;
};
}
true
}
pub const fn with<M: Message>(&'static self) -> BundleChain {
let to_add = TypeId::of::<M>();
BundleChain {
@ -159,24 +108,6 @@ impl BundleChain {
}
}
impl<A, B> MessageBundle for (A, B)
where
A: Message,
B: Message,
{
const IDS: BundleChain = BundleChain::of::<A>().with::<B>();
}
impl<A, B, C, D> MessageBundle for (A, B, C, D)
where
A: Message,
B: Message,
C: Message,
D: Message,
{
const IDS: BundleChain = BundleChain::of::<A>().with::<B>().with::<C>().with::<D>();
}
pub trait IsContainedInBundle<MB> {
const IS_CONTAINED: bool;
}
@ -186,7 +117,7 @@ where
M: Message,
MB: MessageBundle,
{
const IS_CONTAINED: bool = check_is_contained(&MB::IDS, TypeId::of::<M>());
const IS_CONTAINED: bool = check_is_contained(&MB::CHAIN, TypeId::of::<M>());
}
const fn check_is_contained(ids: &BundleChain, id: TypeId) -> bool {
@ -206,6 +137,7 @@ const fn check_is_contained(ids: &BundleChain, id: TypeId) -> bool {
return true;
}
}
BundleOp::None => (),
}
if let Some(next) = ids.next {
@ -269,6 +201,16 @@ mod tests {
));
}
#[test]
fn check_subset() {
const CHAIN: BundleChain = BundleChain::of::<Foo>().join(&BundleChain::of::<Bar>());
const SUB: BundleChain = BundleChain::of::<Foo>().with::<Bar>();
assert!(SUB.is_subset_of(&CHAIN));
assert!(!BundleChain::of::<Zap>().is_subset_of(&CHAIN));
}
#[apply(test!)]
async fn check_sending_messages() {
struct Sender;

View file

@ -0,0 +1,43 @@
#[rustfmt::skip]
macro_rules! all_the_tuples {
($name:ident) => {
$name!(M1);
$name!(M1, M2);
$name!(M1, M2, M3);
$name!(M1, M2, M3, M4);
$name!(M1, M2, M3, M4, M5);
$name!(M1, M2, M3, M4, M5, M6);
$name!(M1, M2, M3, M4, M5, M6, M7);
$name!(M1, M2, M3, M4, M5, M6, M7, M8);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15);
$name!(M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15, M16);
};
}
#[rustfmt::skip]
macro_rules! all_the_tuples_special_first {
($name:ident) => {
$name!([], M1);
$name!([M1], M2);
$name!([M1, M2], M3);
$name!([M1, M2, M3], M4);
$name!([M1, M2, M3, M4], M5);
$name!([M1, M2, M3, M4, M5], M6);
$name!([M1, M2, M3, M4, M5, M6], M7);
$name!([M1, M2, M3, M4, M5, M6, M7], M8);
$name!([M1, M2, M3, M4, M5, M6, M7, M8], M9);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9], M10);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10], M11);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11], M12);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12], M13);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13], M14);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14], M15);
$name!([M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15], M16);
};
}

View file

@ -0,0 +1,66 @@
use std::any::Any;
use std::any::TypeId;
use crate::BundleChain;
pub trait Message: Send + Any {
type Reply: Send + Any;
}
pub struct InternalMessage {
value: Box<dyn std::any::Any>,
name: &'static str,
}
impl InternalMessage {
pub fn new<M: Any>(message: M) -> InternalMessage {
InternalMessage {
value: Box::new(message),
name: std::any::type_name::<M>(),
}
}
pub fn into_inner<M: Any>(self) -> Result<M, InternalMessage> {
self.value
.downcast()
.map(|v| *v)
.map_err(|value| InternalMessage {
value,
name: self.name,
})
}
pub fn as_ref<M: Any>(&self) -> Option<&M> {
self.value.downcast_ref()
}
pub fn type_name(&self) -> &'static str {
self.name
}
pub fn type_id(&self) -> TypeId {
self.value.as_ref().type_id()
}
}
pub trait MessageBundle {
const CHAIN: BundleChain;
}
impl MessageBundle for () {
const CHAIN: BundleChain = BundleChain::empty();
}
macro_rules! impl_message_bundles {
( [ $($ty:ident),* ] , $last:ident ) => {
impl<$($ty,)* $last> MessageBundle for ($($ty,)* $last,)
where
$( $ty: Message, )*
$last: Message,
{
const CHAIN: BundleChain = <($($ty,)*) as MessageBundle>::CHAIN.with::<$last>();
}
};
}
all_the_tuples_special_first!(impl_message_bundles);

View file

@ -1,6 +1,7 @@
use std::marker::PhantomData;
use futures::FutureExt;
use tytix_core::BoxedAddress;
use tytix_core::InternalMessage;
use tytix_core::InternalMessageHandler;
use tytix_core::IsContainedInBundle;
@ -18,17 +19,29 @@ pub trait AddressExt<MB> {
fn inspect<F, M, U>(self, f: F) -> Inspect<Self, F, U, M>
where
F: Fn(&M) -> U,
M: Message + IsContainedInBundle<MB>,
Inspect<Self, F, U, M>: InternalMessageHandler,
Self: Sized;
fn join<Other>(self, o: Other) -> Joined<Self, Other>
where
Joined<Self, Other>: InternalMessageHandler,
Self: Sized;
fn simplify<SMB>(self) -> SimpleAddress<SMB, Self>
where
SimpleAddress<SMB, Self>: InternalMessageHandler,
Self: Sized;
fn boxed(self) -> BoxedAddress<MB>
where
BoxedAddress<MB>: InternalMessageHandler;
}
impl<MB: MessageBundle, A: InternalMessageHandler<HandledMessages = MB>> AddressExt<MB> for A {
impl<MB, A> AddressExt<MB> for A
where
MB: MessageBundle,
A: InternalMessageHandler<HandledMessages = MB> + 'static,
{
fn map<M, F, U, RF, RU>(self, f: F, r: RF) -> MappedMessage<Self, M, F, U, RF, RU>
where
MappedMessage<Self, M, F, U, RF, RU>: InternalMessageHandler,
@ -44,16 +57,9 @@ impl<MB: MessageBundle, A: InternalMessageHandler<HandledMessages = MB>> Address
fn inspect<F, M, U>(self, f: F) -> Inspect<Self, F, U, M>
where
F: Fn(&M) -> U,
Inspect<Self, F, U, M>: InternalMessageHandler,
Self: Sized,
M: Message + IsContainedInBundle<MB>,
{
const {
let true = <M as IsContainedInBundle<MB>>::IS_CONTAINED else {
panic!("Message is not contained in MessageBundle",);
};
}
Inspect {
address: self,
func: f,
@ -71,6 +77,47 @@ impl<MB: MessageBundle, A: InternalMessageHandler<HandledMessages = MB>> Address
right: o,
}
}
fn simplify<SMB>(self) -> SimpleAddress<SMB, Self>
where
SimpleAddress<SMB, Self>: InternalMessageHandler,
Self: Sized,
{
SimpleAddress {
inner: self,
_pd: PhantomData,
}
}
fn boxed(self) -> BoxedAddress<MB> {
BoxedAddress::new(self)
}
}
pub struct SimpleAddress<MB, A> {
inner: A,
_pd: PhantomData<fn(MB)>,
}
impl<SMB, MB, A> InternalMessageHandler for SimpleAddress<SMB, A>
where
SMB: MessageBundle,
MB: MessageBundle,
A: InternalMessageHandler<HandledMessages = MB>,
{
type HandledMessages = SMB;
fn handle_message(
&mut self,
msg: InternalMessage,
) -> impl Future<Output = anyhow::Result<InternalMessage>> {
const {
let true = SMB::CHAIN.is_subset_of(&MB::CHAIN) else {
panic!("Message is not contained in MessageBundle",);
};
}
self.inner.handle_message(msg)
}
}
pub struct Joined<L, R> {
@ -87,7 +134,8 @@ where
L: MessageBundle,
R: MessageBundle,
{
const IDS: tytix_core::BundleChain = <L as MessageBundle>::IDS.join(&<R as MessageBundle>::IDS);
const CHAIN: tytix_core::BundleChain =
<L as MessageBundle>::CHAIN.join(&<R as MessageBundle>::CHAIN);
}
impl<L, R> InternalMessageHandler for Joined<L, R>
@ -100,7 +148,7 @@ where
async fn handle_message(&mut self, msg: InternalMessage) -> anyhow::Result<InternalMessage> {
let message_id = msg.type_id();
if L::HandledMessages::IDS.contains(message_id) {
if L::HandledMessages::CHAIN.contains(message_id) {
self.left.handle_message(msg).await
} else {
self.right.handle_message(msg).await
@ -148,7 +196,7 @@ where
M: Message,
R: Message + IsContainedInBundle<MB>,
{
const IDS: tytix_core::BundleChain = MB::IDS.without::<R>().with::<M>();
const CHAIN: tytix_core::BundleChain = MB::CHAIN.without::<R>().with::<M>();
}
impl<A, M, R, F, U, RF, RU> InternalMessageHandler for MappedMessage<A, M, F, U, RF, RU>
@ -219,9 +267,9 @@ mod tests {
type Reply = usize;
}
struct SimpleAddress;
struct FooBarAddress;
impl InternalMessageHandler for SimpleAddress {
impl InternalMessageHandler for FooBarAddress {
type HandledMessages = (Foo, Bar);
async fn handle_message(
@ -253,7 +301,7 @@ mod tests {
async fn check_mapping() {
static MSG: OnceLock<bool> = OnceLock::new();
let mut sa = SimpleAddress.map(
let mut sa = FooBarAddress.map(
|_b: Bar| {
let _ = MSG.set(true);
async { Foo }
@ -270,7 +318,7 @@ mod tests {
async fn check_inspect() {
static MSG: OnceLock<bool> = OnceLock::new();
let mut sa = SimpleAddress.inspect(|_b: &Bar| {
let mut sa = FooBarAddress.inspect(|_b: &Bar| {
let _ = MSG.set(true);
async {}
});
@ -285,11 +333,11 @@ mod tests {
}
#[apply(test!)]
async fn check_join() {
async fn check_simplify() {
static MSG_SA: OnceLock<bool> = OnceLock::new();
static MSG_ZAP: OnceLock<bool> = OnceLock::new();
let sa = SimpleAddress.inspect(|_b: &Bar| {
let sa = FooBarAddress.inspect(|_b: &Bar| {
MSG_SA.set(true).unwrap();
async {}
});
@ -308,4 +356,42 @@ mod tests {
MSG_ZAP.get().expect("The message was NOT inspected!");
}
#[apply(test!)]
async fn check_join() {
static MSG_SA: OnceLock<bool> = OnceLock::new();
let sa = FooBarAddress.inspect(|_b: &Bar| {
MSG_SA.set(true).unwrap();
async {}
});
let zap = ZapAddress.join(ZapAddress).join(ZapAddress);
let joined = sa.join(zap);
let mut simple = joined.simplify::<(Foo, Bar, Zap)>();
simple.send(Bar).await.unwrap();
MSG_SA.get().expect("The message was not :CC inspected!");
}
#[apply(test!)]
async fn check_boxed() {
static MSG_SA: OnceLock<bool> = OnceLock::new();
let sa = FooBarAddress.inspect(|_b: &Bar| {
MSG_SA.set(true).unwrap();
async {}
});
let zap = ZapAddress.join(ZapAddress).join(ZapAddress);
let mut boxed = zap.join(sa).simplify::<(Bar, Zap, Foo)>().boxed();
boxed.send(Bar).await.unwrap();
MSG_SA.get().expect("The message was not :CC inspected!");
}
}