py-scopes/scopes/core/core.py

177 lines
5.7 KiB
Python

# scopes.core.core
"""A concurrent cybernetic communication system"""
from __future__ import annotations # no check of type annotations at runtime
import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import dataclass, field
from typing import Any, Self
from scopes.core import actor
from scopes.core.message import Message
from scopes import util
from scopes.util import keywords as k, Symbol, Symbols
# keywords (domains, actions, classes, stages, data keys):
k('csys env _meta',
'connect create created data default query remove set value',
'c00 e00 s00 op rcvr',
'initial active retired',
'address pred succ')
type Mailbox = asyncio.Queue
type Categ = tuple[Symbol, Symbol]
type Proc = Callable[[Message, Scope], Awaitable[Scope | None]]
type Program = Callable[[Scope], Proc]
type ProgramSpec = dict[tuple[Symbol, Symbol], Proc]
type Syn = Callable[[Message], Awaitable[Symbol | None]]
type SynOp = Callable[[Message], Message | None]
type Handler = Callable[[Message, Scope],
Awaitable[Message | None | tuple[Message | None, Scope | None]]]
type Handlers = Handler | None | list[Handlers]
type Actions = dict[Symbol, Handlers]
def neuron(scope: Scope) -> Mailbox:
async def process(msg: Message) -> actor.Behavior | None:
nonlocal scope
scope = await scope.proc(msg, scope) or scope
return process
return actor.create(process)
def synapse(rcvr: Mailbox, op: SynOp = lambda x: x) -> Syn:
async def syn(msg: Message) -> Symbol | None:
msg_new = op(msg)
if msg_new is not None:
await rcvr.put(msg_new) # return actor.send(rcvr, msg_new)
return None
return syn
env_dummy: Mailbox = asyncio.Queue()
def notify() -> Handler:
async def handle(msg: Message, scope: Scope) -> None:
await scope.environ.put(msg)
return handle
def gen_proc(actions: Actions, default: Handlers = notify(),
prepare: Handlers = None, finalize: Handlers = None) -> Proc:
async def proc(msg: Message, scope: Scope) -> Scope:
acts = actions.get(msg.action, actions.get(k.default, default))
acts = util.flatten(prepare, acts, finalize, ignore_none=True)
for act in acts:
msg = await act(msg, scope)
if isinstance(msg, tuple):
msg, new_scope = msg
if new_scope is not None:
scope = new_scope
if msg is None:
break
return scope
return proc
def gen_program(spec: ProgramSpec, default: Proc = gen_proc({})) -> Program:
def program(scope: Scope) -> Proc:
key = (scope.categ[1], scope.stage)
proc = spec.get(key, spec.get((key[0], k.default)))
if proc is None:
proc = spec.get((k.default, key[1]), default)
return proc
return program
@dataclass
class Scope:
value: Any = 0
categ: Categ = (k.csys, k.c00)
stage: Symbol = k.initial
proc: Proc = field(init=False)
program: Program = gen_program({})
syns: list[Syn | None] = field(default_factory=list)
environ: Mailbox = env_dummy
def __post_init__(self):
self.set_proc()
def set_proc(self) -> Self:
self.proc = self.program(self)
return self
def create_scope(env: Mailbox, cat: Categ = (k.csys, k.c00), **kw) -> Scope:
return Scope(environ=env, categ=cat, **kw)
def clone_scope(scope: Scope, **kw) -> Scope:
value = kw.get('value', 0)
categ = kw.get('categ', scope.categ)
stage = kw.get('stage', k.initial)
return Scope(environ=scope.environ, value=value, categ=categ, stage=stage,
program=scope.program, syns=[])
async def create_zero(env: Mailbox) -> Mailbox:
scope = create_scope(env, program=gen_program({}, default=std_proc()))
zero = neuron(scope)
await notify_created(Message(k.csys), zero, scope)
return zero
# predefined action handlers and Action sets
def noop(no_forward: bool = False) -> Handler:
async def handle(msg: Message, scope: Scope) -> Message | None:
return None if no_forward else msg
return handle
def forward(no_syns_notify: bool = False) -> Handler:
do_notify = notify() if no_syns_notify else None
async def handle(msg: Message, scope: Scope) -> None:
count = 0
for idx, syn in enumerate(scope.syns):
res = await syn(msg) if syn is not None else None
if res == k.remove:
scope.syns[idx] = None
else:
count += 1
if count == 0 and do_notify:
do_notify(msg, scope)
return handle
def value_add() -> Handler:
async def handle(msg: Message, scope: Scope) -> tuple[Message, Scope]:
return msg, scope
return handle
def create() -> Handler:
async def handle(msg: Message, scope: Scope) -> None:
new = neuron(clone_scope(scope, **msg.data))
await notify_created(msg, new, scope)
conn = msg.data.get(k.connect)
return handle
def connect() -> Handler:
async def handle(msg: Message, scope: Scope) -> None:
return None
return handle
def basic_actions() -> Actions:
acts: Actions = {
k.value: value_add(),
k.create: create(),
k.connect: connect()
}
return acts
# predefined specialized procs
def std_proc(actions: Actions | None = None, finalize: Handler = forward(),
more: Actions = {}) -> Proc:
if actions is None:
actions = basic_actions()
actions.update(more)
return gen_proc(actions, finalize=finalize)
# helpers
async def notify_created(msg: Message, new: Mailbox, scope: Scope) -> None:
msg = Message(msg.domain, k.created, data=msg.data | {k.new: new})
await notify()(msg, scope)