core.actor: Mailbox class (with status), send() function

This commit is contained in:
Helmut Merz 2026-10-27 09:40:05 +01:00
parent f0de1a3e7a
commit 87c2a68ef6
4 changed files with 45 additions and 42 deletions

View file

@ -5,10 +5,17 @@ from collections.abc import Awaitable, Callable
from typing import Any, Optional from typing import Any, Optional
from scopes.util import keywords as k, Symbol from scopes.util import keywords as k, Symbol
k('full quit')
# keywords:
k('active full ok quit retired suspended')
type Behavior = Callable[[Any], Awaitable[Behavior | None]] type Behavior = Callable[[Any], Awaitable[Behavior | None]]
type Mailbox = asyncio.Queue
class Mailbox:
def __init__(self, maxsize:int = 0, status: Symbol = k.active):
self.queue: asyncio.Queue = asyncio.Queue(maxsize)
self.status: Symbol = status
async def loop(q: asyncio.Queue, bhv: Behavior) -> None: async def loop(q: asyncio.Queue, bhv: Behavior) -> None:
while True: while True:
@ -18,18 +25,16 @@ async def loop(q: asyncio.Queue, bhv: Behavior) -> None:
bhv = await bhv(msg) or bhv bhv = await bhv(msg) or bhv
q.task_done() q.task_done()
def create(bhv: Behavior, maxsize: int = 0, def create(bhv: Behavior, maxsize: int = 0) -> Mailbox:
group: Optional[asyncio.TaskGroup] = None) -> asyncio.Queue: mb = Mailbox(maxsize)
q: asyncio.Queue = asyncio.Queue(maxsize) asyncio.create_task(loop(mb.queue, bhv))
if group is None: return mb
asyncio.create_task(loop(q, bhv))
else:
group.create_task(loop(q, bhv))
return q
async def try_send(rcvr: Mailbox, msg: Any) -> Symbol | None: async def send(mb: Mailbox, msg: Any, no_wait: bool = False) -> Symbol:
if rcvr.full(): if mb.status is k.retired:
return k.retired
if no_wait and mb.queue.full():
return k.full return k.full
await rcvr.put(msg) await mb.queue.put(msg)
return None return k.ok

View file

@ -15,12 +15,11 @@ from scopes.util import keywords as k, Symbol, Symbols
# keywords (domains, actions, classes, stages, data keys): # keywords (domains, actions, classes, stages, data keys):
k('csys env _meta', k('csys env _meta',
'connect create created data default query remove set value', 'connect create created data default query set value',
'c00 e00 s00 op rcvr', 'c00 e00 s00 op rcvr',
'initial active retired', 'initial active retired',
'address pred succ') 'address pred succ')
type Mailbox = asyncio.Queue
type Categ = tuple[Symbol, Symbol] type Categ = tuple[Symbol, Symbol]
type Proc = Callable[[Message, Scope], Awaitable[Scope | None]] type Proc = Callable[[Message, Scope], Awaitable[Scope | None]]
type Program = Callable[[Scope], Proc] type Program = Callable[[Scope], Proc]
@ -33,26 +32,26 @@ type Handler = Callable[[Message, Scope],
type Handlers = Handler | None | list[Handlers] type Handlers = Handler | None | list[Handlers]
type Actions = dict[Symbol, Handlers] type Actions = dict[Symbol, Handlers]
def neuron(scope: Scope) -> Mailbox: def neuron(scope: Scope) -> actor.Mailbox:
async def process(msg: Message) -> actor.Behavior | None: async def process(msg: Message) -> actor.Behavior | None:
nonlocal scope nonlocal scope
scope = await scope.proc(msg, scope) or scope scope = await scope.proc(msg, scope) or scope
return process return process
return actor.create(process) return actor.create(process)
def synapse(rcvr: Mailbox, op: SynOp = lambda x: x) -> Syn: def synapse(rcvr: actor.Mailbox, op: SynOp = lambda x: x) -> Syn:
async def syn(msg: Message) -> Symbol | None: async def syn(msg: Message) -> Symbol | None:
msg_new = op(msg) msg_new = op(msg)
if msg_new is not None: if msg_new is not None:
await rcvr.put(msg_new) # return actor.send(rcvr, msg_new) return await actor.send(rcvr, msg_new)
return None return None
return syn return syn
env_dummy: Mailbox = asyncio.Queue() env_dummy: actor.Mailbox = actor.Mailbox()
def notify() -> Handler: def notify() -> Handler:
async def handle(msg: Message, scope: Scope) -> None: async def handle(msg: Message, scope: Scope) -> None:
await scope.environ.put(msg) await actor.send(scope.environ, msg)
return handle return handle
def gen_proc(actions: Actions, default: Handlers = notify(), def gen_proc(actions: Actions, default: Handlers = notify(),
@ -89,7 +88,7 @@ class Scope:
proc: Proc = field(init=False) proc: Proc = field(init=False)
program: Program = gen_program({}) program: Program = gen_program({})
syns: list[Syn | None] = field(default_factory=list) syns: list[Syn | None] = field(default_factory=list)
environ: Mailbox = env_dummy environ: actor.Mailbox = env_dummy
def __post_init__(self): def __post_init__(self):
self.set_proc() self.set_proc()
@ -98,7 +97,7 @@ class Scope:
self.proc = self.program(self) self.proc = self.program(self)
return self return self
def create_scope(env: Mailbox, cat: Categ = (k.csys, k.c00), **kw) -> Scope: def create_scope(env: actor.Mailbox, cat: Categ = (k.csys, k.c00), **kw) -> Scope:
return Scope(environ=env, categ=cat, **kw) return Scope(environ=env, categ=cat, **kw)
def clone_scope(scope: Scope, **kw) -> Scope: def clone_scope(scope: Scope, **kw) -> Scope:
@ -108,7 +107,7 @@ def clone_scope(scope: Scope, **kw) -> Scope:
return Scope(environ=scope.environ, value=value, categ=categ, stage=stage, return Scope(environ=scope.environ, value=value, categ=categ, stage=stage,
program=scope.program, syns=[]) program=scope.program, syns=[])
async def create_zero(env: Mailbox) -> Mailbox: async def create_zero(env: actor.Mailbox) -> actor.Mailbox:
scope = create_scope(env, program=gen_program({}, default=std_proc())) scope = create_scope(env, program=gen_program({}, default=std_proc()))
zero = neuron(scope) zero = neuron(scope)
await notify_created(Message(k.csys), zero, scope) await notify_created(Message(k.csys), zero, scope)
@ -127,10 +126,9 @@ def forward(no_syns_notify: bool = False) -> Handler:
count = 0 count = 0
for idx, syn in enumerate(scope.syns): for idx, syn in enumerate(scope.syns):
res = await syn(msg) if syn is not None else None res = await syn(msg) if syn is not None else None
if res == k.remove: match res:
scope.syns[idx] = None case k.retired: scope.syns[idx] = None
else: case k.ok: count += 1
count += 1
if count == 0 and do_notify: if count == 0 and do_notify:
do_notify(msg, scope) do_notify(msg, scope)
return handle return handle
@ -171,7 +169,7 @@ def std_proc(actions: Actions | None = None, finalize: Handler = forward(),
# helpers # helpers
async def notify_created(msg: Message, new: Mailbox, scope: Scope) -> None: async def notify_created(msg: Message, new: actor.Mailbox, scope: Scope) -> None:
msg = Message(msg.domain, k.created, data=msg.data | {k.new: new}) msg = Message(msg.domain, k.created, data=msg.data | {k.new: new})
await notify()(msg, scope) await notify()(msg, scope)

View file

@ -7,7 +7,7 @@ from scopes.core import actor
from scopes.core.message import Message from scopes.core.message import Message
from scopes.util import keywords as k from scopes.util import keywords as k
def create_env(meta_env: core.Mailbox) -> core.Mailbox: def create_env(meta_env: actor.Mailbox) -> actor.Mailbox:
scope = core.create_scope(meta_env, (k.env, k.c00), scope = core.create_scope(meta_env, (k.env, k.c00),
program=core.gen_program({}, default=env_proc())) program=core.gen_program({}, default=env_proc()))
return core.neuron(scope) return core.neuron(scope)

View file

@ -37,15 +37,15 @@ def handle_alt(coll):
@pytest.mark.anyio @pytest.mark.anyio
async def test_actor(collector): async def test_actor(collector):
async with asyncio.TaskGroup() as group: mb1 = actor.create(handle_basic(collector))
q1 = actor.create(handle_basic(collector), group) mb2 = actor.create(handle_basic(collector))
q2 = actor.create(handle_basic(collector), group) msgs = [Message(k.test, k.hello), k.show, k.switch, k.show, "actor 1: Goodbye"]
msgs = [Message(k.test, k.hello), k.show, k.switch, k.show, "actor 1: Goodbye"] await asyncio.gather(*[actor.send(mb1, m) for m in msgs])
await asyncio.gather(*[q1.put(m) for m in msgs]) await actor.send(mb2, "actor 2")
await q2.put("actor 2") await actor.send(mb2, k.show)
await q2.put(k.show) await actor.send(mb2, k.quit)
await q1.put(k.quit) await actor.send(mb2, k.quit)
await q2.put(k.quit) await asyncio.sleep(0.01)
#print(collector) #print(collector)
assert len(collector) == 6 assert len(collector) == 6
assert collector[-1] == 'actor 1: Goodbye' assert collector[-1] == 'actor 1: Goodbye'
@ -69,9 +69,9 @@ async def meta_env(collector):
async def test_core(meta_env, collector): async def test_core(meta_env, collector):
env = environ.create_env(meta_env) env = environ.create_env(meta_env)
zero = await core.create_zero(env) zero = await core.create_zero(env)
await env.put(Message(k.env, k.hello)) await actor.send(env, Message(k.env, k.hello))
await zero.put(Message(k.csys, k.hello)) await actor.send(zero, Message(k.csys, k.hello))
await zero.put(Message(k.csys, k.create, await actor.send(zero, Message(k.csys, k.create,
data={k.connect: k.pred, k.address: (1, 1)})) data={k.connect: k.pred, k.address: (1, 1)}))
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
assert len(collector) == 4 assert len(collector) == 4