py-scopes/scopes/core/core.py

127 lines
3.8 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
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[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]
async def proc_dummy(msg: Message, scope: Scope) -> None:
await scope.environ.put(msg)
global_mailbox: Mailbox = asyncio.Queue()
def gen_program(spec: ProgramSpec, default: Proc = proc_dummy) -> 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] = field(default_factory=list)
environ: Mailbox = global_mailbox
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 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 create_zero(env: Mailbox) -> Mailbox:
scope = create_scope(env)
zero = neuron(scope)
return zero
def synapse(rcvr: Mailbox, op: SynOp = lambda x: x) -> Syn:
async def syn(msg: Message) -> None:
# op = update_op(msg)
msg_new = op(msg)
if msg_new is not None:
await rcvr.put(msg_new)
return syn
def gen_proc(actions: Actions, default: Handlers = None,
prepare: Handlers = None, finalize: Handlers = None) -> Proc:
async def proc(msg: Message, scope: Scope) -> Scope:
acts = actions.get(msg.head['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_std(actions: Actions) -> Proc:
return gen_proc(actions, finalize=forward())
def basic_actions() -> Actions:
acts: Actions = {
k.value: value_add(),
k.create: create()
}
return acts
# predefined action handlers
def forward() -> Handler:
async def do_forward(msg, scope) -> None:
for syn in scope.syns:
await syn(msg)
return do_forward
def value_add() -> Handler:
async def do_add(msg, scope) -> tuple[Message, Scope]:
return msg, scope
return do_add
def create() -> Handler:
async def do_create(msg, scope) -> None:
return None
return do_create