From 83106702cb9a0f1e6907af3c8df85894a137efac Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Tue, 10 Sep 2024 01:24:37 +0800 Subject: [PATCH 1/8] [ABSTRACTION]: Initial --- jac/jaclang/runtimelib/implementation.py | 162 +++++++++++++++++++++++ jac/jaclang/runtimelib/interface.py | 147 ++++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 jac/jaclang/runtimelib/implementation.py create mode 100644 jac/jaclang/runtimelib/interface.py diff --git a/jac/jaclang/runtimelib/implementation.py b/jac/jaclang/runtimelib/implementation.py new file mode 100644 index 0000000000..22ccdf944b --- /dev/null +++ b/jac/jaclang/runtimelib/implementation.py @@ -0,0 +1,162 @@ +"""Jaclang Runtimelib Implementation.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from logging import getLogger +from typing import ClassVar, TypeAlias +from uuid import UUID, uuid4 + +from .interface import ( + EdgeAnchor as _EdgeAnchor, + EdgeArchitype as _EdgeArchitype, + JID as _JID, + NodeAnchor as _NodeAnchor, + NodeArchitype as _NodeArchitype, + Permission, + WalkerAnchor as _WalkerAnchor, + WalkerArchitype as _WalkerArchitype, + _ANCHOR, +) + +ANCHOR_TYPES: TypeAlias = "NodeAnchor" | "EdgeAnchor" | "WalkerAnchor" +logger = getLogger(__name__) + + +@dataclass(kw_only=True) +class JID(_JID[UUID, _ANCHOR]): + """Jaclang Default JID.""" + + id: UUID = field(default_factory=uuid4) + name: str = "" + + def __repr__(self) -> str: + """Override string representation.""" + return f"{self.type.__class__.__name__[:1].lower()}:{self.name}:{self.id}" + + def __str__(self) -> str: + """Override string parsing.""" + return f"{self.type.__class__.__name__[:1].lower()}:{self.name}:{self.id}" + + +@dataclass(kw_only=True) +class NodeAnchor(_NodeAnchor["NodeAnchor"]): + """NodeAnchor Interface.""" + + jid: JID[NodeAnchor] = field(default_factory=lambda: JID(type=NodeAnchor)) + architype: "NodeArchitype" + root: JID["NodeAnchor"] | None = None + access: Permission = field(default_factory=Permission) + persistent: bool = False + hash: int = 0 + + edge_ids: set[JID[EdgeAnchor]] = field(default_factory=set) + + def __serialize__(self) -> NodeAnchor: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: NodeAnchor) -> NodeAnchor: + """Override deserialization.""" + return data + + +@dataclass(kw_only=True) +class EdgeAnchor(_EdgeAnchor["EdgeAnchor"]): + """NodeAnchor Interface.""" + + jid: JID[EdgeAnchor] = field(default_factory=lambda: JID(type=EdgeAnchor)) + architype: "EdgeArchitype" + root: JID["NodeAnchor"] | None = None + access: Permission = field(default_factory=Permission) + persistent: bool = False + hash: int = 0 + + source_id: JID[NodeAnchor] + target_id: JID[NodeAnchor] + + def __serialize__(self) -> EdgeAnchor: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: EdgeAnchor) -> EdgeAnchor: + """Override deserialization.""" + return data + + +@dataclass(kw_only=True) +class WalkerAnchor(_WalkerAnchor["WalkerAnchor"]): + """NodeAnchor Interface.""" + + jid: JID[WalkerAnchor] = field(default_factory=lambda: JID(type=WalkerAnchor)) + architype: "WalkerArchitype" + root: JID["NodeAnchor"] | None = None + access: Permission = field(default_factory=Permission) + persistent: bool = False + hash: int = 0 + + def __serialize__(self) -> WalkerAnchor: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: WalkerAnchor) -> WalkerAnchor: + """Override deserialization.""" + return data + + +class NodeArchitype(_NodeArchitype["NodeArchitype"]): + """NodeArchitype Interface.""" + + __jac__: ClassVar[NodeAnchor] + + def __serialize__(self) -> NodeArchitype: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: NodeArchitype) -> NodeArchitype: + """Override deserialization.""" + return data + + +class EdgeArchitype(_EdgeArchitype["EdgeArchitype"]): + """EdgeArchitype Interface.""" + + __jac__: ClassVar[EdgeAnchor] + + def __serialize__(self) -> EdgeArchitype: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: EdgeArchitype) -> EdgeArchitype: + """Override deserialization.""" + return data + + +class WalkerArchitype(_WalkerArchitype["WalkerArchitype"]): + """Walker Architype Interface.""" + + __jac__: ClassVar[WalkerAnchor] + + def __serialize__(self) -> WalkerArchitype: + """Override serialization.""" + return self + + @classmethod + def __deserialize__(cls, data: WalkerArchitype) -> WalkerArchitype: + """Override deserialization.""" + return data + + +@dataclass(kw_only=True) +class Root(NodeArchitype): + """Default Root Architype.""" + + +@dataclass(kw_only=True) +class GenericEdge(EdgeArchitype): + """Default Edge Architype.""" diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py new file mode 100644 index 0000000000..0b23f09ab6 --- /dev/null +++ b/jac/jaclang/runtimelib/interface.py @@ -0,0 +1,147 @@ +"""Jaclang Runtimelib interfaces.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import IntEnum +from typing import ClassVar, Generic, Iterable, Type, TypeVar + + +_ID = TypeVar("_ID") +_ANCHOR = TypeVar("_ANCHOR", bound="Anchor") + +_SERIALIZE = TypeVar("_SERIALIZE") +_DESERIALIZE = TypeVar("_DESERIALIZE") + + +@dataclass(kw_only=True) +class JID(Generic[_ID, _ANCHOR], ABC): + """Jaclang ID Interface.""" + + id: _ID + type: Type[_ANCHOR] + name: str + + +class AccessLevel(IntEnum): + """Access level enum.""" + + NO_ACCESS = -1 + READ = 0 + CONNECT = 1 + WRITE = 2 + + @staticmethod + def cast(val: int | str | "AccessLevel") -> "AccessLevel": + """Cast access level.""" + match val: + case int(): + return AccessLevel(val) + case str(): + return AccessLevel[val] + case _: + return val + + +@dataclass +class Access: + """Access Structure.""" + + anchors: dict[str, AccessLevel] = field(default_factory=dict) + + def check(self, anchor: str) -> AccessLevel: + """Validate access.""" + return self.anchors.get(anchor, AccessLevel.NO_ACCESS) + + +@dataclass +class Permission: + """Anchor Access Handler.""" + + all: AccessLevel = AccessLevel.NO_ACCESS + roots: Access = field(default_factory=Access) + + +@dataclass(kw_only=True) +class Anchor(Generic[_SERIALIZE], ABC): + """Anchor Interface.""" + + jid: JID + architype: "Architype" + root: JID | None + access: Permission + + @abstractmethod + def __serialize__(self) -> _SERIALIZE: + """Override string representation.""" + + @classmethod + @abstractmethod + def __deserialize__(cls: Type[_DESERIALIZE], data: _SERIALIZE) -> _DESERIALIZE: + """Override string parsing.""" + + +@dataclass(kw_only=True) +class NodeAnchor(Anchor[_SERIALIZE]): + """NodeAnchor Interface.""" + + architype: "NodeArchitype" + edge_ids: Iterable[JID] + + +@dataclass(kw_only=True) +class EdgeAnchor(Anchor[_SERIALIZE]): + """EdgeAnchor Interface.""" + + architype: "EdgeArchitype" + source_id: JID + target_id: JID + + +@dataclass(kw_only=True) +class WalkerAnchor(Anchor[_SERIALIZE]): + """WalkerAnchor Interface.""" + + architype: "WalkerArchitype" + + +class Architype(Generic[_SERIALIZE], ABC): + """Architype Interface.""" + + __jac__: ClassVar[Anchor] + + @abstractmethod + def __serialize__(self) -> _SERIALIZE: + """Override string representation.""" + + @classmethod + @abstractmethod + def __deserialize__(cls: Type[_DESERIALIZE], data: _SERIALIZE) -> _DESERIALIZE: + """Override string parsing.""" + + +class NodeArchitype(Architype[_SERIALIZE]): + """NodeArchitype Interface.""" + + __jac__: ClassVar[NodeAnchor] + + +class EdgeArchitype(Architype[_SERIALIZE]): + """EdgeArchitype Interface.""" + + __jac__: ClassVar[EdgeAnchor] + + +class WalkerArchitype(Architype[_SERIALIZE]): + """Walker Architype Interface.""" + + __jac__: ClassVar[WalkerAnchor] + + +@dataclass(kw_only=True) +class Root(NodeArchitype[_SERIALIZE]): + """Default Root Architype.""" + + +@dataclass(kw_only=True) +class GenericEdge(EdgeArchitype[_SERIALIZE]): + """Default Edge Architype.""" From a27d5540ec47ea4b234a1fa0581a5162004d6814 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Tue, 17 Sep 2024 00:47:07 +0800 Subject: [PATCH 2/8] [REFACTOR]: Migrate all process to pluggy --- jac/jaclang/__init__.py | 15 +- jac/jaclang/cli/cli.py | 5 +- jac/jaclang/plugin/builtin.py | 6 +- jac/jaclang/plugin/default.py | 568 ++++++++++++++---- jac/jaclang/plugin/feature.py | 339 ++++++++--- jac/jaclang/plugin/spec.py | 259 ++++++-- .../tests/fixtures/other_root_access.jac | 18 +- jac/jaclang/plugin/tests/test_features.py | 4 +- jac/jaclang/runtimelib/architype.py | 353 +---------- jac/jaclang/runtimelib/constructs.py | 2 + jac/jaclang/runtimelib/context.py | 4 - jac/jaclang/runtimelib/memory.py | 6 +- jac/jaclang/tests/fixtures/edge_node_walk.jac | 2 +- jac/jaclang/tests/fixtures/edges_walk.jac | 2 +- .../tests/fixtures/gendot_bubble_sort.jac | 2 +- 15 files changed, 945 insertions(+), 640 deletions(-) diff --git a/jac/jaclang/__init__.py b/jac/jaclang/__init__.py index 9afb8c8aa9..a27927956d 100644 --- a/jac/jaclang/__init__.py +++ b/jac/jaclang/__init__.py @@ -1,17 +1,18 @@ """The Jac Programming Language.""" -from jaclang.plugin.default import ( # noqa: E402 +from jaclang.plugin.default import ( JacBuiltin, JacCmdDefaults, - JacFeatureDefaults, + JacFeatureImpl, ) -from jaclang.plugin.feature import JacFeature, pm # noqa: E402 +from jaclang.plugin.feature import JacFeature, hookmanager jac_import = JacFeature.jac_import -pm.register(JacFeatureDefaults) -pm.register(JacBuiltin) -pm.register(JacCmdDefaults) -pm.load_setuptools_entrypoints("jac") + +hookmanager.register(JacFeatureImpl) +hookmanager.register(JacBuiltin) +hookmanager.register(JacCmdDefaults) +hookmanager.load_setuptools_entrypoints("jac") __all__ = ["jac_import"] diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index e6e3949cb8..7caf1e5f5b 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -28,6 +28,7 @@ Cmd.create_cmd() +Jac.setup() @cmd_registry.register @@ -283,7 +284,9 @@ def enter( jctx.set_entry_node(node) - if isinstance(architype, WalkerArchitype) and jctx.validate_access(): + if isinstance(architype, WalkerArchitype) and Jac.check_read_access( + jctx.entry_node + ): Jac.spawn_call(jctx.entry_node.architype, architype) jctx.close() diff --git a/jac/jaclang/plugin/builtin.py b/jac/jaclang/plugin/builtin.py index 9288d26ae7..4509937eff 100644 --- a/jac/jaclang/plugin/builtin.py +++ b/jac/jaclang/plugin/builtin.py @@ -19,9 +19,9 @@ def dotgen( dot_file: Optional[str] = None, ) -> str: """Print the dot graph.""" - from jaclang.plugin.feature import pm + from jaclang.plugin.feature import JacBuiltin as JacB, JacFeature as Jac - root = pm.hook.get_root() + root = Jac.get_root() node = node if node is not None else root depth = depth if depth is not None else -1 traverse = traverse if traverse is not None else False @@ -29,7 +29,7 @@ def dotgen( edge_limit = edge_limit if edge_limit is not None else 512 node_limit = node_limit if node_limit is not None else 512 - return pm.hook.dotgen( + return JacB.dotgen( edge_type=edge_type, node=node, depth=depth, diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 22db8b68a8..cbcdc71c08 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -11,59 +11,434 @@ from collections import OrderedDict from dataclasses import field from functools import wraps +from logging import getLogger from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union from uuid import UUID -import jaclang.compiler.absyntree as ast -from jaclang.compiler.constant import EdgeDir, colors -from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass +from jaclang.compiler.constant import colors from jaclang.compiler.semtable import SemInfo, SemRegistry, SemScope -from jaclang.runtimelib.constructs import ( +from jaclang.plugin.feature import ( + AccessLevel, + Anchor, Architype, DSFunc, EdgeAnchor, EdgeArchitype, + EdgeDir, ExecutionContext, - GenericEdge, - JacTestCheck, + JacFeature as Jac, NodeAnchor, NodeArchitype, + P, + PyastGenPass, Root, - WalkerAnchor, + T, WalkerArchitype, + ast, +) +from jaclang.runtimelib.constructs import ( + GenericEdge, + JacTestCheck, ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram -from jaclang.runtimelib.utils import traverse_graph -from jaclang.plugin.feature import JacFeature as Jac # noqa: I100 -from jaclang.plugin.spec import P, T +from jaclang.runtimelib.utils import collect_node_connections, traverse_graph import pluggy hookimpl = pluggy.HookimplMarker("jac") +logger = getLogger(__name__) + + +class JacAccessValidationImpl: + """Jac Access Validation Implementations.""" + + @staticmethod + @hookimpl + def allow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow all access from target root graph to current Architype.""" + level = AccessLevel.cast(level) + access = anchor.access.roots + + _root_id = str(root_id) + if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): + access.anchors[_root_id] = level + + @staticmethod + @hookimpl + def disallow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Disallow all access from target root graph to current Architype.""" + level = AccessLevel.cast(level) + access = anchor.access.roots + + access.anchors.pop(str(root_id), None) + + @staticmethod + @hookimpl + def unrestrict( + anchor: Anchor, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow everyone to access current Architype.""" + level = AccessLevel.cast(level) + if level != anchor.access.all: + anchor.access.all = level + + @staticmethod + @hookimpl + def restrict(anchor: Anchor) -> None: + """Disallow others to access current Architype.""" + if anchor.access.all > AccessLevel.NO_ACCESS: + anchor.access.all = AccessLevel.NO_ACCESS + + @staticmethod + @hookimpl + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.NO_ACCESS): + logger.info( + f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.READ): + logger.info( + f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.CONNECT): + logger.info( + f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + if not to.persistent: + return AccessLevel.WRITE + + jctx = Jac.get_context() + + jroot = jctx.root + + # if current root is system_root + # if current root id is equal to target anchor's root id + # if current root is the target anchor + if jroot == jctx.system_root or jroot.id == to.root or jroot == to: + return AccessLevel.WRITE + + access_level = AccessLevel.NO_ACCESS + + # if target anchor have set access.all + if (to_access := to.access).all > AccessLevel.NO_ACCESS: + access_level = to_access.all + + # if target anchor's root have set allowed roots + # if current root is allowed to the whole graph of target anchor's root + if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor): + if to_root.access.all > access_level: + access_level = to_root.access.all + + level = to_root.access.roots.check(str(jroot.id)) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + # if target anchor have set allowed roots + # if current root is allowed to target anchor + level = to_access.roots.check(str(jroot.id)) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + return access_level + + +class JacNodeImpl: + """Jac Node Operations.""" + + @staticmethod + @hookimpl + def node_dot(node: NodeArchitype, dot_file: Optional[str] = None) -> str: + """Generate Dot file for visualizing nodes and edges.""" + visited_nodes: set[NodeAnchor] = set() + connections: set[tuple[NodeArchitype, NodeArchitype, str]] = set() + unique_node_id_dict = {} + + collect_node_connections(node.__jac__, visited_nodes, connections) + dot_content = 'digraph {\nnode [style="filled", shape="ellipse", fillcolor="invis", fontcolor="black"];\n' + for idx, i in enumerate([nodes_.architype for nodes_ in visited_nodes]): + unique_node_id_dict[i] = (i.__class__.__name__, str(idx)) + dot_content += f'{idx} [label="{i}"];\n' + dot_content += 'edge [color="gray", style="solid"];\n' + + for pair in list(set(connections)): + dot_content += ( + f"{unique_node_id_dict[pair[0]][1]} -> {unique_node_id_dict[pair[1]][1]}" + f' [label="{pair[2]}"];\n' + ) + if dot_file: + with open(dot_file, "w") as f: + f.write(dot_content + "}") + return dot_content + "}" + + @staticmethod + @hookimpl + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + ret_edges: list[EdgeArchitype] = [] + for anchor in node.edges: + if ( + (source := anchor.source) + and (target := anchor.target) + and (not filter_func or filter_func([anchor.architype])) + and source.architype + and target.architype + ): + if ( + dir in [EdgeDir.OUT, EdgeDir.ANY] + and node == source + and (not target_obj or target.architype in target_obj) + and Jac.check_read_access(target) + ): + ret_edges.append(anchor.architype) + if ( + dir in [EdgeDir.IN, EdgeDir.ANY] + and node == target + and (not target_obj or source.architype in target_obj) + and Jac.check_read_access(source) + ): + ret_edges.append(anchor.architype) + return ret_edges + + @staticmethod + @hookimpl + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + ret_edges: list[NodeArchitype] = [] + for anchor in node.edges: + if ( + (source := anchor.source) + and (target := anchor.target) + and (not filter_func or filter_func([anchor.architype])) + and source.architype + and target.architype + ): + if ( + dir in [EdgeDir.OUT, EdgeDir.ANY] + and node == source + and (not target_obj or target.architype in target_obj) + and Jac.check_read_access(target) + ): + ret_edges.append(target.architype) + if ( + dir in [EdgeDir.IN, EdgeDir.ANY] + and node == target + and (not target_obj or source.architype in target_obj) + and Jac.check_read_access(source) + ): + ret_edges.append(source.architype) + return ret_edges + + @staticmethod + @hookimpl + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + for idx, ed in enumerate(node.edges): + if ed.id == edge.id: + node.edges.pop(idx) + break + + +class JacEdgeImpl: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + Jac.remove_edge(node=edge.source, edge=edge) + Jac.remove_edge(node=edge.target, edge=edge) + + +class JacWalkerImpl: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's visit stmt feature.""" + if isinstance(walker, WalkerArchitype): + """Walker visits node.""" + wanch = walker.__jac__ + before_len = len(wanch.next) + for anchor in ( + (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] + ): + if anchor not in wanch.ignores: + if isinstance(anchor, NodeAnchor): + wanch.next.append(anchor) + elif isinstance(anchor, EdgeAnchor): + if target := anchor.target: + wanch.next.append(target) + else: + raise ValueError("Edge has no target.") + return len(wanch.next) > before_len + else: + raise TypeError("Invalid walker object") -__all__ = [ - "EdgeAnchor", - "GenericEdge", - "hookimpl", - "JacTestCheck", - "NodeAnchor", - "WalkerAnchor", - "NodeArchitype", - "EdgeArchitype", - "Root", - "WalkerArchitype", - "Architype", - "DSFunc", - "T", -] - - -class JacFeatureDefaults: + @staticmethod + @hookimpl + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's ignore stmt feature.""" + if isinstance(walker, WalkerArchitype): + wanch = walker.__jac__ + before_len = len(wanch.ignores) + for anchor in ( + (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] + ): + if anchor not in wanch.ignores: + if isinstance(anchor, NodeAnchor): + wanch.ignores.append(anchor) + elif isinstance(anchor, EdgeAnchor): + if target := anchor.target: + wanch.ignores.append(target) + else: + raise ValueError("Edge has no target.") + return len(wanch.ignores) > before_len + else: + raise TypeError("Invalid walker object") + + @staticmethod + @hookimpl + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + if isinstance(op1, WalkerArchitype): + warch = op1 + walker = op1.__jac__ + if isinstance(op2, NodeArchitype): + node = op2.__jac__ + elif isinstance(op2, EdgeArchitype): + node = op2.__jac__.source + else: + raise TypeError("Invalid target object") + elif isinstance(op2, WalkerArchitype): + warch = op2 + walker = op2.__jac__ + if isinstance(op1, NodeArchitype): + node = op1.__jac__ + elif isinstance(op1, EdgeArchitype): + node = op1.__jac__.source + else: + raise TypeError("Invalid target object") + else: + raise TypeError("Invalid walker object") + + walker.path = [] + walker.next = [node] + while len(walker.next): + if current_node := walker.next.pop(0).architype: + for i in current_node._jac_entry_funcs_: + if not i.trigger or isinstance(warch, i.trigger): + if i.func: + i.func(current_node, warch) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_entry_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + i.func(warch, current_node) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_exit_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + i.func(warch, current_node) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in current_node._jac_exit_funcs_: + if not i.trigger or isinstance(warch, i.trigger): + if i.func: + i.func(current_node, warch) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + walker.ignores = [] + return warch + + @staticmethod + @hookimpl + def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 + """Jac's disengage stmt feature.""" + walker.__jac__.disengaged = True + return True + + +class JacFeatureImpl(JacAccessValidationImpl, JacNodeImpl, JacEdgeImpl, JacWalkerImpl): """Jac Feature.""" - pm = pluggy.PluginManager("jac") + @staticmethod + @hookimpl + def setup() -> None: + """Set Class References.""" + ######################################################################################## + # REFERENCE FOR PLUGIN # + ######################################################################################## + # Jac.EdgeDir = EdgeDir + # Jac.DSFunc = DSFunc + # Jac.RootType = Root + # Jac.Obj = Architype + # Jac.Node = NodeArchitype + # Jac.Edge = EdgeArchitype + # Jac.Walker = WalkerArchitype @staticmethod @hookimpl @@ -74,6 +449,7 @@ def get_context() -> ExecutionContext: @staticmethod @hookimpl def get_object(id: str) -> Architype | None: + """Get object by id.""" if id == "root": return Jac.get_context().root.architype elif obj := Jac.get_context().mem.find_by_id(UUID(id)): @@ -84,6 +460,7 @@ def get_object(id: str) -> Architype | None: @staticmethod @hookimpl def object_ref(obj: Architype) -> str: + """Get object's id.""" return obj.__jac__.id.hex @staticmethod @@ -361,69 +738,11 @@ def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" return field(default_factory=lambda: gen_func()) - @staticmethod - @hookimpl - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - if isinstance(op1, WalkerArchitype): - return op1.__jac__.spawn_call(op2.__jac__) - elif isinstance(op2, WalkerArchitype): - return op2.__jac__.spawn_call(op1.__jac__) - else: - raise TypeError("Invalid walker object") - @staticmethod @hookimpl def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" - @staticmethod - @hookimpl - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's ignore stmt feature.""" - if isinstance(walker, WalkerArchitype): - return walker.__jac__.ignore_node( - (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] - ) - else: - raise TypeError("Invalid walker object") - - @staticmethod - @hookimpl - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's visit stmt feature.""" - if isinstance(walker, WalkerArchitype): - return walker.__jac__.visit_node( - (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] - ) - else: - raise TypeError("Invalid walker object") - - @staticmethod - @hookimpl - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - walker.__jac__.disengage_now() - return True - @staticmethod @hookimpl def edge_ref( @@ -444,16 +763,16 @@ def edge_ref( if edges_only: connected_edges: list[EdgeArchitype] = [] for node in node_obj: - connected_edges += node.__jac__.get_edges( - dir, filter_func, target_obj=targ_obj_set + connected_edges += Jac.get_edges( + node.__jac__, dir, filter_func, target_obj=targ_obj_set ) return list(set(connected_edges)) else: connected_nodes: list[NodeArchitype] = [] for node in node_obj: connected_nodes.extend( - node.__jac__.edges_to_nodes( - dir, filter_func, target_obj=targ_obj_set + Jac.edges_to_nodes( + node.__jac__, dir, filter_func, target_obj=targ_obj_set ) ) return list(set(connected_nodes)) @@ -474,14 +793,12 @@ def connect( right = [right] if isinstance(right, NodeArchitype) else right edges = [] - root = Jac.get_root().__jac__ - for i in left: _left = i.__jac__ - if root.has_connect_access(_left): + if Jac.check_connect_access(_left): for j in right: _right = j.__jac__ - if root.has_connect_access(_right): + if Jac.check_connect_access(_right): edges.append(edge_spec(_left, _right)) return right if not edges_only else edges @@ -498,8 +815,6 @@ def disconnect( left = [left] if isinstance(left, NodeArchitype) else left right = [right] if isinstance(right, NodeArchitype) else right - root = Jac.get_root().__jac__ - for i in left: node = i.__jac__ for anchor in set(node.edges): @@ -514,17 +829,17 @@ def disconnect( dir in [EdgeDir.OUT, EdgeDir.ANY] and node == source and target.architype in right - and root.has_write_access(target) + and Jac.check_write_access(target) ): - anchor.destroy() if anchor.persistent else anchor.detach() + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True if ( dir in [EdgeDir.IN, EdgeDir.ANY] and node == target and source.architype in right - and root.has_write_access(source) + and Jac.check_write_access(source) ): - anchor.destroy() if anchor.persistent else anchor.detach() + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True return disconnect_occurred @@ -565,7 +880,16 @@ def build_edge( def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: edge = conn_type() if isinstance(conn_type, type) else conn_type - edge.__attach__(source, target, is_undirected) + + eanch = edge.__jac__ = EdgeAnchor( + architype=edge, + source=source, + target=target, + is_undirected=is_undirected, + ) + source.edges.append(eanch) + target.edges.append(eanch) + if conn_assign: for fld, val in zip(conn_assign[0], conn_assign[1]): if hasattr(edge, fld): @@ -573,13 +897,44 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: else: raise ValueError(f"Invalid attribute: {fld}") if source.persistent or target.persistent: - edge.__jac__.save() - target.save() - source.save() + Jac.save(eanch) + Jac.save(target) + Jac.save(source) return edge return builder + @staticmethod + @hookimpl + def save(obj: Architype | Anchor) -> None: + """Destroy object.""" + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + jctx = Jac.get_context() + + anchor.persistent = True + anchor.root = jctx.root.id + + jctx.mem.set(anchor.id, anchor) + + @staticmethod + @hookimpl + def destroy(obj: Architype | Anchor) -> None: + """Destroy object.""" + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + if Jac.check_write_access(anchor): + match anchor: + case NodeAnchor(): + for edge in anchor.edges: + Jac.destroy(edge) + case EdgeAnchor(): + Jac.detach(anchor) + case _: + pass + + Jac.get_context().mem.remove(anchor.id) + @staticmethod @hookimpl def get_semstr_type( @@ -645,6 +1000,7 @@ def obj_scope(file_loc: str, attr: str) -> str: @staticmethod @hookimpl def get_sem_type(file_loc: str, attr: str) -> tuple[str | None, str | None]: + """Jac's get_semstr_type implementation.""" with open( os.path.join( os.path.dirname(file_loc), @@ -854,7 +1210,7 @@ def dotgen( node: NodeArchitype, depth: int, traverse: bool, - edge_type: list[str], + edge_type: Optional[list[str]], bfs: bool, edge_limit: int, node_limit: int, diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index fd9fe49e19..c043196ccb 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -4,57 +4,213 @@ import ast as ast3 import types -from typing import Any, Callable, Mapping, Optional, Sequence, Type, TypeAlias, Union +from typing import ( + Any, + Callable, + ClassVar, + Mapping, + Optional, + Sequence, + Type, + TypeAlias, + Union, +) +from uuid import UUID -import jaclang.compiler.absyntree as ast -from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass -from jaclang.plugin.spec import JacBuiltin, JacCmdSpec, JacFeatureSpec, P, T -from jaclang.runtimelib.constructs import ( +from jaclang.plugin.spec import ( + AccessLevel, + Anchor, Architype, + DSFunc, + EdgeAnchor, EdgeArchitype, + EdgeDir, + ExecutionContext, NodeAnchor, NodeArchitype, + P, + PyastGenPass, Root, + T, WalkerArchitype, + ast, + hookmanager, ) -from jaclang.runtimelib.context import ExecutionContext -import pluggy -pm = pluggy.PluginManager("jac") -pm.add_hookspecs(JacFeatureSpec) -pm.add_hookspecs(JacCmdSpec) -pm.add_hookspecs(JacBuiltin) +class JacAccessValidation: + """Jac Access Validation Specs.""" + @staticmethod + def allow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow all access from target root graph to current Architype.""" + hookmanager.hook.allow_root(anchor=anchor, root_id=root_id, level=level) + + @staticmethod + def disallow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Disallow all access from target root graph to current Architype.""" + hookmanager.hook.disallow_root(anchor=anchor, root_id=root_id, level=level) + + @staticmethod + def unrestrict( + anchor: Anchor, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow everyone to access current Architype.""" + hookmanager.hook.unrestrict(anchor=anchor, level=level) + + @staticmethod + def restrict(anchor: Anchor) -> None: + """Disallow others to access current Architype.""" + hookmanager.hook.restrict(anchor=anchor) + + @staticmethod + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + return hookmanager.hook.check_read_access(to=to) + + @staticmethod + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + return hookmanager.hook.check_connect_access(to=to) + + @staticmethod + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + return hookmanager.hook.check_write_access(to=to) + + @staticmethod + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + return hookmanager.hook.check_access_level(to=to) + + +class JacNode: + """Jac Node Operations.""" + + @staticmethod + def node_dot(node: NodeArchitype, dot_file: Optional[str] = None) -> str: + """Generate Dot file for visualizing nodes and edges.""" + return hookmanager.hook.node_dot(node=node, dot_file=dot_file) + + @staticmethod + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + return hookmanager.hook.get_edges( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj + ) + + @staticmethod + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + return hookmanager.hook.edges_to_nodes( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj + ) + + @staticmethod + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + return hookmanager.hook.remove_edge(node=node, edge=edge) -class JacFeature: - """Jac Feature.""" - from jaclang.compiler.constant import EdgeDir as EdgeDirType - from jaclang.runtimelib.constructs import DSFunc as DSFuncType +class JacEdge: + """Jac Edge Operations.""" + + @staticmethod + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + return hookmanager.hook.detach(edge=edge) - EdgeDir: TypeAlias = EdgeDirType - DSFunc: TypeAlias = DSFuncType - RootType: TypeAlias = Root - Obj: TypeAlias = Architype - Node: TypeAlias = NodeArchitype - Edge: TypeAlias = EdgeArchitype - Walker: TypeAlias = WalkerArchitype + +class JacWalker: + """Jac Edge Operations.""" + + @staticmethod + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's visit stmt feature.""" + return hookmanager.hook.visit_node(walker=walker, expr=expr) + + @staticmethod + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's ignore stmt feature.""" + return hookmanager.hook.ignore(walker=walker, expr=expr) + + @staticmethod + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Jac's spawn operator feature.""" + return hookmanager.hook.spawn_call(op1=op1, op2=op2) + + @staticmethod + def disengage(walker: WalkerArchitype) -> bool: + """Jac's disengage stmt feature.""" + return hookmanager.hook.disengage(walker=walker) + + +class JacClassReferences: + """Default Classes References.""" + + EdgeDir: ClassVar[TypeAlias] = EdgeDir + DSFunc: ClassVar[TypeAlias] = DSFunc + RootType: ClassVar[TypeAlias] = Root + Obj: ClassVar[TypeAlias] = Architype + Node: ClassVar[TypeAlias] = NodeArchitype + Edge: ClassVar[TypeAlias] = EdgeArchitype + Walker: ClassVar[TypeAlias] = WalkerArchitype + + +class JacFeature(JacClassReferences, JacAccessValidation, JacNode, JacEdge, JacWalker): + """Jac Feature.""" + + @staticmethod + def setup() -> None: + """Set Class References.""" + hookmanager.hook.setup() @staticmethod def get_context() -> ExecutionContext: """Get current execution context.""" - return pm.hook.get_context() + return hookmanager.hook.get_context() @staticmethod def get_object(id: str) -> Architype | None: """Get object given id.""" - return pm.hook.get_object(id=id) + return hookmanager.hook.get_object(id=id) @staticmethod def object_ref(obj: Architype) -> str: """Get object reference id.""" - return pm.hook.object_ref(obj=obj) + return hookmanager.hook.object_ref(obj=obj) @staticmethod def make_architype( @@ -64,7 +220,7 @@ def make_architype( on_exit: list[DSFunc], ) -> Type[Architype]: """Create a obj architype.""" - return pm.hook.make_architype( + return hookmanager.hook.make_architype( cls=cls, on_entry=on_entry, on_exit=on_exit, arch_base=arch_base ) @@ -73,35 +229,35 @@ def make_obj( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a obj architype.""" - return pm.hook.make_obj(on_entry=on_entry, on_exit=on_exit) + return hookmanager.hook.make_obj(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_node( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a node architype.""" - return pm.hook.make_node(on_entry=on_entry, on_exit=on_exit) + return hookmanager.hook.make_node(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_edge( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a edge architype.""" - return pm.hook.make_edge(on_entry=on_entry, on_exit=on_exit) + return hookmanager.hook.make_edge(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_walker( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a walker architype.""" - return pm.hook.make_walker(on_entry=on_entry, on_exit=on_exit) + return hookmanager.hook.make_walker(on_entry=on_entry, on_exit=on_exit) @staticmethod def impl_patch_filename( file_loc: str, ) -> Callable[[Callable[P, T]], Callable[P, T]]: """Update impl file location.""" - return pm.hook.impl_patch_filename(file_loc=file_loc) + return hookmanager.hook.impl_patch_filename(file_loc=file_loc) @staticmethod def jac_import( @@ -116,7 +272,7 @@ def jac_import( reload_module: Optional[bool] = False, ) -> tuple[types.ModuleType, ...]: """Core Import Process.""" - return pm.hook.jac_import( + return hookmanager.hook.jac_import( target=target, base_path=base_path, absorb=absorb, @@ -131,7 +287,7 @@ def jac_import( @staticmethod def create_test(test_fun: Callable) -> Callable: """Create a test.""" - return pm.hook.create_test(test_fun=test_fun) + return hookmanager.hook.create_test(test_fun=test_fun) @staticmethod def run_test( @@ -143,7 +299,7 @@ def run_test( verbose: bool = False, ) -> int: """Run the test suite in the specified .jac file.""" - return pm.hook.run_test( + return hookmanager.hook.run_test( filepath=filepath, filter=filter, xit=xit, @@ -155,55 +311,17 @@ def run_test( @staticmethod def elvis(op1: Optional[T], op2: T) -> T: """Jac's elvis operator feature.""" - return pm.hook.elvis(op1=op1, op2=op2) + return hookmanager.hook.elvis(op1=op1, op2=op2) @staticmethod def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" - return pm.hook.has_instance_default(gen_func=gen_func) - - @staticmethod - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - return pm.hook.spawn_call(op1=op1, op2=op2) + return hookmanager.hook.has_instance_default(gen_func=gen_func) @staticmethod def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" - return pm.hook.report(expr=expr) - - @staticmethod - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's ignore stmt feature.""" - return pm.hook.ignore(walker=walker, expr=expr) - - @staticmethod - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's visit stmt feature.""" - return pm.hook.visit_node(walker=walker, expr=expr) - - @staticmethod - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - return pm.hook.disengage(walker=walker) + return hookmanager.hook.report(expr=expr) @staticmethod def edge_ref( @@ -214,7 +332,7 @@ def edge_ref( edges_only: bool = False, ) -> list[NodeArchitype] | list[EdgeArchitype]: """Jac's apply_dir stmt feature.""" - return pm.hook.edge_ref( + return hookmanager.hook.edge_ref( node_obj=node_obj, target_obj=target_obj, dir=dir, @@ -233,7 +351,7 @@ def connect( Note: connect needs to call assign compr with tuple in op """ - return pm.hook.connect( + return hookmanager.hook.connect( left=left, right=right, edge_spec=edge_spec, edges_only=edges_only ) @@ -245,7 +363,7 @@ def disconnect( filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], ) -> bool: """Jac's disconnect operator feature.""" - return pm.hook.disconnect( + return hookmanager.hook.disconnect( left=left, right=right, dir=dir, @@ -257,17 +375,17 @@ def assign_compr( target: list[T], attr_val: tuple[tuple[str], tuple[Any]] ) -> list[T]: """Jac's assign comprehension feature.""" - return pm.hook.assign_compr(target=target, attr_val=attr_val) + return hookmanager.hook.assign_compr(target=target, attr_val=attr_val) @staticmethod def get_root() -> Root: """Jac's root getter.""" - return pm.hook.get_root() + return hookmanager.hook.get_root() @staticmethod def get_root_type() -> Type[Root]: """Jac's root type getter.""" - return pm.hook.get_root_type() + return hookmanager.hook.get_root_type() @staticmethod def build_edge( @@ -276,28 +394,42 @@ def build_edge( conn_assign: Optional[tuple[tuple, tuple]], ) -> Callable[[NodeAnchor, NodeAnchor], EdgeArchitype]: """Jac's root getter.""" - return pm.hook.build_edge( + return hookmanager.hook.build_edge( is_undirected=is_undirected, conn_type=conn_type, conn_assign=conn_assign ) + @staticmethod + def save( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + hookmanager.hook.save(obj=obj) + + @staticmethod + def destroy( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + hookmanager.hook.destroy(obj=obj) + @staticmethod def get_semstr_type( file_loc: str, scope: str, attr: str, return_semstr: bool ) -> Optional[str]: """Jac's get_semstr_type feature.""" - return pm.hook.get_semstr_type( + return hookmanager.hook.get_semstr_type( file_loc=file_loc, scope=scope, attr=attr, return_semstr=return_semstr ) @staticmethod def obj_scope(file_loc: str, attr: str) -> str: """Jac's get_semstr_type feature.""" - return pm.hook.obj_scope(file_loc=file_loc, attr=attr) + return hookmanager.hook.obj_scope(file_loc=file_loc, attr=attr) @staticmethod def get_sem_type(file_loc: str, attr: str) -> tuple[str | None, str | None]: """Jac's get_semstr_type feature.""" - return pm.hook.get_sem_type(file_loc=file_loc, attr=attr) + return hookmanager.hook.get_sem_type(file_loc=file_loc, attr=attr) @staticmethod def with_llm( @@ -314,7 +446,7 @@ def with_llm( _locals: Mapping, ) -> Any: # noqa: ANN401 """Jac's with_llm feature.""" - return pm.hook.with_llm( + return hookmanager.hook.with_llm( file_loc=file_loc, model=model, model_params=model_params, @@ -331,7 +463,7 @@ def with_llm( @staticmethod def gen_llm_body(_pass: PyastGenPass, node: ast.Ability) -> list[ast3.AST]: """Generate the by LLM body.""" - return pm.hook.gen_llm_body(_pass=_pass, node=node) + return hookmanager.hook.gen_llm_body(_pass=_pass, node=node) @staticmethod def by_llm_call( @@ -346,7 +478,7 @@ def by_llm_call( exclude_info: list[tuple[str, ast3.AST]], ) -> ast3.Call: """Return the LLM Call, e.g. _Jac.with_llm().""" - return pm.hook.by_llm_call( + return hookmanager.hook.by_llm_call( _pass=_pass, model=model, model_params=model_params, @@ -361,7 +493,34 @@ def by_llm_call( @staticmethod def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: """Get the by LLM call args.""" - return pm.hook.get_by_llm_call_args(_pass=_pass, node=node) + return hookmanager.hook.get_by_llm_call_args(_pass=_pass, node=node) + + +class JacBuiltin: + """Jac Builtins.""" + + @staticmethod + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Generate Dot file for visualizing nodes and edges.""" + return hookmanager.hook.dotgen( + node=node, + depth=depth, + traverse=traverse, + edge_type=edge_type, + bfs=bfs, + edge_limit=edge_limit, + node_limit=node_limit, + dot_file=dot_file, + ) class JacCmd: @@ -370,4 +529,4 @@ class JacCmd: @staticmethod def create_cmd() -> None: """Create Jac CLI cmds.""" - return pm.hook.create_cmd() + return hookmanager.hook.create_cmd() diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index e5fccc63dc..376fe051e0 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -11,37 +11,199 @@ Optional, ParamSpec, Sequence, - TYPE_CHECKING, Type, TypeVar, Union, ) +from uuid import UUID -import jaclang.compiler.absyntree as ast +from jaclang.compiler import absyntree as ast +from jaclang.compiler.constant import EdgeDir from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass - -if TYPE_CHECKING: - from jaclang.plugin.default import ( - Architype, - EdgeDir, - WalkerArchitype, - Root, - DSFunc, - ) - from jaclang.runtimelib.constructs import EdgeArchitype, NodeAnchor, NodeArchitype - from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.constructs import ( + AccessLevel, + Anchor, + Architype, + DSFunc, + EdgeAnchor, + EdgeArchitype, + NodeAnchor, + NodeArchitype, + Root, + WalkerArchitype, +) +from jaclang.runtimelib.context import ExecutionContext import pluggy hookspec = pluggy.HookspecMarker("jac") +hookmanager = pluggy.PluginManager("jac") T = TypeVar("T") P = ParamSpec("P") -class JacFeatureSpec: +class JacAccessValidationSpec: + """Jac Access Validation Specs.""" + + @staticmethod + @hookspec(firstresult=True) + def allow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow all access from target root graph to current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def disallow_root( + anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Disallow all access from target root graph to current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def unrestrict( + anchor: Anchor, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow everyone to access current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def restrict(anchor: Anchor) -> None: + """Disallow others to access current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + raise NotImplementedError + + +class JacNodeSpec: + """Jac Node Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def node_dot(node: NodeArchitype, dot_file: Optional[str] = None) -> str: + """Generate Dot file for visualizing nodes and edges.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + raise NotImplementedError + + +class JacEdgeSpec: + """Jac Edge Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + raise NotImplementedError + + +class JacWalkerSpec: + """Jac Edge Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's visit stmt feature.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's ignore stmt feature.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def disengage(walker: WalkerArchitype) -> bool: + """Jac's disengage stmt feature.""" + raise NotImplementedError + + +class JacFeatureSpec(JacAccessValidationSpec, JacNodeSpec, JacEdgeSpec, JacWalkerSpec): """Jac Feature.""" + @staticmethod + @hookspec(firstresult=True) + def setup() -> None: + """Set Class References.""" + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def get_context() -> ExecutionContext: @@ -51,13 +213,13 @@ def get_context() -> ExecutionContext: @staticmethod @hookspec(firstresult=True) def get_object(id: str) -> Architype | None: - """Get object given id..""" + """Get object by id.""" raise NotImplementedError @staticmethod @hookspec(firstresult=True) def object_ref(obj: Architype) -> str: - """Get object given id..""" + """Get object's id.""" raise NotImplementedError @staticmethod @@ -158,54 +320,12 @@ def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" raise NotImplementedError - @staticmethod - @hookspec(firstresult=True) - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - raise NotImplementedError - @staticmethod @hookspec(firstresult=True) def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" raise NotImplementedError - @staticmethod - @hookspec(firstresult=True) - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's ignore stmt feature.""" - raise NotImplementedError - - @staticmethod - @hookspec(firstresult=True) - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's visit stmt feature.""" - raise NotImplementedError - - @staticmethod - @hookspec(firstresult=True) - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - raise NotImplementedError - @staticmethod @hookspec(firstresult=True) def edge_ref( @@ -273,6 +393,22 @@ def build_edge( """Jac's root getter.""" raise NotImplementedError + @staticmethod + @hookspec(firstresult=True) + def save( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def destroy( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def get_semstr_type( @@ -348,7 +484,7 @@ def dotgen( node: NodeArchitype, depth: int, traverse: bool, - edge_type: list[str], + edge_type: Optional[list[str]], bfs: bool, edge_limit: int, node_limit: int, @@ -366,3 +502,8 @@ class JacCmdSpec: def create_cmd() -> None: """Create Jac CLI cmds.""" raise NotImplementedError + + +hookmanager.add_hookspecs(JacFeatureSpec) +hookmanager.add_hookspecs(JacCmdSpec) +hookmanager.add_hookspecs(JacBuiltin) diff --git a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac index df5192d9ec..905d05ad4d 100644 --- a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac +++ b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac @@ -36,7 +36,7 @@ walker create_node { walker create_other_root { can enter with `root entry { other_root = `root().__jac__; - other_root.save(); + Jac.save(other_root); print(other_root.id); } } @@ -46,17 +46,17 @@ walker allow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here.__jac__, self.level); } else { - here.__jac__.allow_root(UUID(self.root_id), self.level); + Jac.allow_root(here.__jac__, UUID(self.root_id), self.level); } } can enter_nested with A entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here.__jac__, self.level); } else { - here.__jac__.allow_root(UUID(self.root_id), self.level); + Jac.allow_root(here.__jac__, UUID(self.root_id), self.level); } } } @@ -66,17 +66,17 @@ walker disallow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here.__jac__); } else { - here.__jac__.disallow_root(UUID(self.root_id)); + Jac.disallow_root(here.__jac__, UUID(self.root_id)); } } can enter_nested with A entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here.__jac__); } else { - here.__jac__.disallow_root(UUID(self.root_id)); + Jac.disallow_root(here.__jac__, UUID(self.root_id)); } } } \ No newline at end of file diff --git a/jac/jaclang/plugin/tests/test_features.py b/jac/jaclang/plugin/tests/test_features.py index 22a3f99faa..6c3e8e0afa 100644 --- a/jac/jaclang/plugin/tests/test_features.py +++ b/jac/jaclang/plugin/tests/test_features.py @@ -3,7 +3,7 @@ import inspect from typing import List, Type -from jaclang.plugin.default import JacFeatureDefaults +from jaclang.plugin.default import JacFeatureImpl from jaclang.plugin.feature import JacFeature from jaclang.plugin.spec import JacFeatureSpec from jaclang.utils.test import TestCase @@ -46,7 +46,7 @@ def test_feature_funcs_synced(self) -> None: """Test if JacFeature, JacFeatureDefaults, and JacFeatureSpec have synced methods.""" # Get methods of each class jac_feature_methods = self.get_methods(JacFeature) - jac_feature_defaults_methods = self.get_methods(JacFeatureDefaults) + jac_feature_defaults_methods = self.get_methods(JacFeatureImpl) jac_feature_spec_methods = self.get_methods(JacFeatureSpec) # Check if all methods are the same in all classes diff --git a/jac/jaclang/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index d8f469d774..6c1ad9a55c 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -7,12 +7,9 @@ from logging import getLogger from pickle import dumps from types import UnionType -from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar +from typing import Any, Callable, ClassVar, Optional, TypeVar from uuid import UUID, uuid4 -from jaclang.compiler.constant import EdgeDir -from jaclang.runtimelib.utils import collect_node_connections - logger = getLogger(__name__) TARCH = TypeVar("TARCH", bound="Architype") @@ -77,128 +74,6 @@ class Anchor: persistent: bool = False hash: int = 0 - ########################################################################## - # ACCESS CONTROL: TODO: Make Base Type # - ########################################################################## - - def allow_root( - self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Allow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - _root_id = str(root_id) - if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): - access.anchors[_root_id] = level - - def disallow_root( - self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Disallow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - access.anchors.pop(str(root_id), None) - - def unrestrict(self, level: AccessLevel | int | str = AccessLevel.READ) -> None: - """Allow everyone to access current Architype.""" - level = AccessLevel.cast(level) - if level != self.access.all: - self.access.all = level - - def restrict(self) -> None: - """Disallow others to access current Architype.""" - if self.access.all > AccessLevel.NO_ACCESS: - self.access.all = AccessLevel.NO_ACCESS - - def has_read_access(self, to: Anchor) -> bool: - """Read Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.NO_ACCESS): - logger.info( - f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def has_connect_access(self, to: Anchor) -> bool: - """Write Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.READ): - logger.info( - f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def has_write_access(self, to: Anchor) -> bool: - """Write Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.CONNECT): - logger.info( - f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def access_level(self, to: Anchor) -> AccessLevel: - """Access validation.""" - if not to.persistent: - return AccessLevel.WRITE - - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - jroot = jctx.root - - # if current root is system_root - # if current root id is equal to target anchor's root id - # if current root is the target anchor - if jroot == jctx.system_root or jroot.id == to.root or jroot == to: - return AccessLevel.WRITE - - access_level = AccessLevel.NO_ACCESS - - # if target anchor have set access.all - if (to_access := to.access).all > AccessLevel.NO_ACCESS: - access_level = to_access.all - - # if target anchor's root have set allowed roots - # if current root is allowed to the whole graph of target anchor's root - if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor): - if to_root.access.all > access_level: - access_level = to_root.access.all - - level = to_root.access.roots.check(str(jroot.id)) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - # if target anchor have set allowed roots - # if current root is allowed to target anchor - level = to_access.roots.check(str(jroot.id)) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - return access_level - - # ---------------------------------------------------------------------- # - - def save(self) -> None: - """Save Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - self.persistent = True - self.root = jctx.root.id - - jctx.mem.set(self.id, self) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - jctx.mem.remove(self.id) - def is_populated(self) -> bool: """Check if state.""" return "architype" in self.__dict__ @@ -304,122 +179,6 @@ class NodeAnchor(Anchor): architype: NodeArchitype edges: list[EdgeAnchor] - def get_edges( - self, - dir: EdgeDir, - filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], - target_obj: Optional[list[NodeArchitype]], - ) -> list[EdgeArchitype]: - """Get edges connected to this node.""" - from jaclang.plugin.feature import JacFeature as Jac - - root = Jac.get_root().__jac__ - ret_edges: list[EdgeArchitype] = [] - for anchor in self.edges: - if ( - (source := anchor.source) - and (target := anchor.target) - and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype - ): - if ( - dir in [EdgeDir.OUT, EdgeDir.ANY] - and self == source - and (not target_obj or target.architype in target_obj) - and root.has_read_access(target) - ): - ret_edges.append(anchor.architype) - if ( - dir in [EdgeDir.IN, EdgeDir.ANY] - and self == target - and (not target_obj or source.architype in target_obj) - and root.has_read_access(source) - ): - ret_edges.append(anchor.architype) - return ret_edges - - def edges_to_nodes( - self, - dir: EdgeDir, - filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], - target_obj: Optional[list[NodeArchitype]], - ) -> list[NodeArchitype]: - """Get set of nodes connected to this node.""" - from jaclang.plugin.feature import JacFeature as Jac - - root = Jac.get_root().__jac__ - ret_edges: list[NodeArchitype] = [] - for anchor in self.edges: - if ( - (source := anchor.source) - and (target := anchor.target) - and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype - ): - if ( - dir in [EdgeDir.OUT, EdgeDir.ANY] - and self == source - and (not target_obj or target.architype in target_obj) - and root.has_read_access(target) - ): - ret_edges.append(target.architype) - if ( - dir in [EdgeDir.IN, EdgeDir.ANY] - and self == target - and (not target_obj or source.architype in target_obj) - and root.has_read_access(source) - ): - ret_edges.append(source.architype) - return ret_edges - - def remove_edge(self, edge: EdgeAnchor) -> None: - """Remove reference without checking sync status.""" - for idx, ed in enumerate(self.edges): - if ed.id == edge.id: - self.edges.pop(idx) - break - - def gen_dot(self, dot_file: Optional[str] = None) -> str: - """Generate Dot file for visualizing nodes and edges.""" - visited_nodes: set[NodeAnchor] = set() - connections: set[tuple[NodeArchitype, NodeArchitype, str]] = set() - unique_node_id_dict = {} - - collect_node_connections(self, visited_nodes, connections) - dot_content = 'digraph {\nnode [style="filled", shape="ellipse", fillcolor="invis", fontcolor="black"];\n' - for idx, i in enumerate([nodes_.architype for nodes_ in visited_nodes]): - unique_node_id_dict[i] = (i.__class__.__name__, str(idx)) - dot_content += f'{idx} [label="{i}"];\n' - dot_content += 'edge [color="gray", style="solid"];\n' - - for pair in list(set(connections)): - dot_content += ( - f"{unique_node_id_dict[pair[0]][1]} -> {unique_node_id_dict[pair[1]][1]}" - f' [label="{pair[2]}"];\n' - ) - if dot_file: - with open(dot_file, "w") as f: - f.write(dot_content + "}") - return dot_content + "}" - - def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype: - """Invoke data spatial call.""" - return walk.spawn_call(self) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - for edge in self.edges: - edge.destroy() - - jctx.mem.remove(self.id) - def __getstate__(self) -> dict[str, object]: """Serialize Node Anchor.""" state = super().__getstate__() @@ -439,30 +198,6 @@ class EdgeAnchor(Anchor): target: NodeAnchor is_undirected: bool - def __post_init__(self) -> None: - """Populate edge to source and target.""" - self.source.edges.append(self) - self.target.edges.append(self) - - def detach(self) -> None: - """Detach edge from nodes.""" - self.source.remove_edge(self) - self.target.remove_edge(self) - - def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype: - """Invoke data spatial call.""" - return walk.spawn_call(self.target) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - self.detach() - jctx.mem.remove(self.id) - def __getstate__(self) -> dict[str, object]: """Serialize Node Anchor.""" state = super().__getstate__() @@ -489,81 +224,6 @@ class WalkerAnchor(Anchor): ignores: list[Anchor] = field(default_factory=list) disengaged: bool = False - def visit_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool: - """Walker visits node.""" - before_len = len(self.next) - for anchor in anchors: - if anchor not in self.ignores: - if isinstance(anchor, NodeAnchor): - self.next.append(anchor) - elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: - self.next.append(target) - else: - raise ValueError("Edge has no target.") - return len(self.next) > before_len - - def ignore_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool: - """Walker ignores node.""" - before_len = len(self.ignores) - for anchor in anchors: - if anchor not in self.ignores: - if isinstance(anchor, NodeAnchor): - self.ignores.append(anchor) - elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: - self.ignores.append(target) - else: - raise ValueError("Edge has no target.") - return len(self.ignores) > before_len - - def disengage_now(self) -> None: - """Disengage walker from traversal.""" - self.disengaged = True - - def spawn_call(self, node: Anchor) -> WalkerArchitype: - """Invoke data spatial call.""" - if walker := self.architype: - self.path = [] - self.next = [node] - while len(self.next): - if current_node := self.next.pop(0).architype: - for i in current_node._jac_entry_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - i.func(current_node, walker) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_entry_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - i.func(walker, current_node) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_exit_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - i.func(walker, current_node) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in current_node._jac_exit_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - i.func(current_node, walker) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - self.ignores = [] - return walker - raise Exception(f"Invalid Reference {self.id}") - class Architype: """Architype Protocol.""" @@ -595,17 +255,6 @@ class EdgeArchitype(Architype): __jac__: EdgeAnchor - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - self.__jac__ = EdgeAnchor( - architype=self, source=source, target=target, is_undirected=is_undirected - ) - class WalkerArchitype(Architype): """Walker Architype Protocol.""" diff --git a/jac/jaclang/runtimelib/constructs.py b/jac/jaclang/runtimelib/constructs.py index 655bd4bc00..b39b0abf0d 100644 --- a/jac/jaclang/runtimelib/constructs.py +++ b/jac/jaclang/runtimelib/constructs.py @@ -4,6 +4,7 @@ from .architype import ( + AccessLevel, Anchor, Architype, DSFunc, @@ -21,6 +22,7 @@ from .test import JacTestCheck, JacTestResult, JacTextTestRunner __all__ = [ + "AccessLevel", "Anchor", "NodeAnchor", "EdgeAnchor", diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index 750ab83484..b300cb430a 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -42,10 +42,6 @@ def init_anchor( raise ValueError(f"Invalid anchor id {anchor_id} !") return default - def validate_access(self) -> bool: - """Validate access.""" - return self.root.has_read_access(self.entry_node) - def set_entry_node(self, entry_node: str | None) -> None: """Override entry.""" self.entry_node = self.init_anchor(entry_node, self.root) diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 2874053181..051165e5ee 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -82,8 +82,6 @@ def close(self) -> None: if isinstance(self.__shelf__, Shelf): from jaclang.plugin.feature import JacFeature as Jac - root = Jac.get_root().__jac__ - for anchor in self.__gc__: self.__shelf__.pop(str(anchor.id), None) self.__mem__.pop(anchor.id, None) @@ -96,14 +94,14 @@ def close(self) -> None: isinstance(p_d, NodeAnchor) and isinstance(d, NodeAnchor) and p_d.edges != d.edges - and root.has_connect_access(d) + and Jac.check_connect_access(d) ): if not d.edges: self.__shelf__.pop(_id, None) continue p_d.edges = d.edges - if root.has_write_access(d): + if Jac.check_write_access(d): if hash(dumps(p_d.access)) != hash(dumps(d.access)): p_d.access = d.access if hash(dumps(d.architype)) != hash(dumps(d.architype)): diff --git a/jac/jaclang/tests/fixtures/edge_node_walk.jac b/jac/jaclang/tests/fixtures/edge_node_walk.jac index c4ba4b0b61..7901930743 100644 --- a/jac/jaclang/tests/fixtures/edge_node_walk.jac +++ b/jac/jaclang/tests/fixtures/edge_node_walk.jac @@ -37,7 +37,7 @@ edge Edge_c { with entry { print(root spawn creator()); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); print([root-:Edge_a:->]); print([root-:Edge_c:->]); print([root-:Edge_a:->-:Edge_b:->]); diff --git a/jac/jaclang/tests/fixtures/edges_walk.jac b/jac/jaclang/tests/fixtures/edges_walk.jac index b5b242f0ca..aaef4dd15a 100644 --- a/jac/jaclang/tests/fixtures/edges_walk.jac +++ b/jac/jaclang/tests/fixtures/edges_walk.jac @@ -30,7 +30,7 @@ edge Edge_c{ with entry{ print(root spawn creator()); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); print([root -:Edge_a:->]); print([root -:Edge_c:->]); print([root -:Edge_a:-> -:Edge_b:->]); diff --git a/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac b/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac index 30c8003bbb..8348fc1fee 100644 --- a/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac +++ b/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac @@ -73,5 +73,5 @@ with entry { root spawn walker1(); root spawn walker2(); root spawn walker3(); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); } From e70b6f6bcf1a75a319cfeb8d2b5ed01a8b731f8c Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Tue, 24 Sep 2024 22:00:49 +0800 Subject: [PATCH 3/8] temporary --- jac/jaclang/cli/cli.py | 5 +- jac/jaclang/plugin/builtin.py | 7 ++- jac/jaclang/plugin/feature.py | 82 ++++++++++++++++------------- jac/jaclang/plugin/spec.py | 80 +++++++++++++++------------- jac/jaclang/runtimelib/context.py | 2 +- jac/jaclang/runtimelib/interface.py | 16 +++++- 6 files changed, 110 insertions(+), 82 deletions(-) diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index 7caf1e5f5b..294ca36d7c 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -18,7 +18,6 @@ from jaclang.compiler.passes.main.schedules import py_code_gen_typed from jaclang.compiler.passes.tool.schedules import format_pass from jaclang.plugin.builtin import dotgen -from jaclang.plugin.feature import JacCmd as Cmd from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.constructs import WalkerArchitype from jaclang.runtimelib.context import ExecutionContext @@ -27,7 +26,7 @@ from jaclang.utils.lang_tools import AstTool -Cmd.create_cmd() +Jac.create_cmd() Jac.setup() @@ -161,7 +160,7 @@ def get_object( data = {} obj = Jac.get_object(id) if obj: - data = obj.__jac__.__getstate__() + data = obj.__jac__.__serialize__() else: print(f"Object with id {id} not found.") diff --git a/jac/jaclang/plugin/builtin.py b/jac/jaclang/plugin/builtin.py index 4509937eff..be4f49cabe 100644 --- a/jac/jaclang/plugin/builtin.py +++ b/jac/jaclang/plugin/builtin.py @@ -4,8 +4,7 @@ from typing import Optional -from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.constructs import Architype, NodeArchitype +from jaclang.plugin.feature import Architype, JacFeature as Jac, NodeArchitype def dotgen( @@ -19,7 +18,7 @@ def dotgen( dot_file: Optional[str] = None, ) -> str: """Print the dot graph.""" - from jaclang.plugin.feature import JacBuiltin as JacB, JacFeature as Jac + from jaclang.plugin.feature import JacFeature as Jac root = Jac.get_root() node = node if node is not None else root @@ -29,7 +28,7 @@ def dotgen( edge_limit = edge_limit if edge_limit is not None else 512 node_limit = node_limit if node_limit is not None else 512 - return JacB.dotgen( + return Jac.dotgen( edge_type=edge_type, node=node, depth=depth, diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index c043196ccb..9f469f0301 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -177,6 +177,42 @@ def disengage(walker: WalkerArchitype) -> bool: return hookmanager.hook.disengage(walker=walker) +class JacBuiltin: + """Jac Builtins.""" + + @staticmethod + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Generate Dot file for visualizing nodes and edges.""" + return hookmanager.hook.dotgen( + node=node, + depth=depth, + traverse=traverse, + edge_type=edge_type, + bfs=bfs, + edge_limit=edge_limit, + node_limit=node_limit, + dot_file=dot_file, + ) + + +class JacCmd: + """Jac CLI command.""" + + @staticmethod + def create_cmd() -> None: + """Create Jac CLI cmds.""" + return hookmanager.hook.create_cmd() + + class JacClassReferences: """Default Classes References.""" @@ -189,7 +225,15 @@ class JacClassReferences: Walker: ClassVar[TypeAlias] = WalkerArchitype -class JacFeature(JacClassReferences, JacAccessValidation, JacNode, JacEdge, JacWalker): +class JacFeature( + JacClassReferences, + JacAccessValidation, + JacNode, + JacEdge, + JacWalker, + JacBuiltin, + JacCmd, +): """Jac Feature.""" @staticmethod @@ -494,39 +538,3 @@ def by_llm_call( def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: """Get the by LLM call args.""" return hookmanager.hook.get_by_llm_call_args(_pass=_pass, node=node) - - -class JacBuiltin: - """Jac Builtins.""" - - @staticmethod - def dotgen( - node: NodeArchitype, - depth: int, - traverse: bool, - edge_type: Optional[list[str]], - bfs: bool, - edge_limit: int, - node_limit: int, - dot_file: Optional[str], - ) -> str: - """Generate Dot file for visualizing nodes and edges.""" - return hookmanager.hook.dotgen( - node=node, - depth=depth, - traverse=traverse, - edge_type=edge_type, - bfs=bfs, - edge_limit=edge_limit, - node_limit=node_limit, - dot_file=dot_file, - ) - - -class JacCmd: - """Jac CLI command.""" - - @staticmethod - def create_cmd() -> None: - """Create Jac CLI cmds.""" - return hookmanager.hook.create_cmd() diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index 376fe051e0..60e8435930 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -12,6 +12,7 @@ ParamSpec, Sequence, Type, + TypeAlias, TypeVar, Union, ) @@ -20,19 +21,18 @@ from jaclang.compiler import absyntree as ast from jaclang.compiler.constant import EdgeDir from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass -from jaclang.runtimelib.constructs import ( +from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.interface import ( AccessLevel, - Anchor, - Architype, DSFunc, EdgeAnchor, EdgeArchitype, NodeAnchor, NodeArchitype, Root, + WalkerAnchor, WalkerArchitype, ) -from jaclang.runtimelib.context import ExecutionContext import pluggy @@ -42,6 +42,9 @@ T = TypeVar("T") P = ParamSpec("P") +Anchor: TypeAlias = NodeAnchor | EdgeAnchor | WalkerAnchor +Architype: TypeAlias = NodeArchitype | EdgeArchitype | WalkerArchitype + class JacAccessValidationSpec: """Jac Access Validation Specs.""" @@ -195,7 +198,43 @@ def disengage(walker: WalkerArchitype) -> bool: raise NotImplementedError -class JacFeatureSpec(JacAccessValidationSpec, JacNodeSpec, JacEdgeSpec, JacWalkerSpec): +class JacBuiltin: + """Jac Builtins.""" + + @staticmethod + @hookspec(firstresult=True) + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Print the dot graph.""" + raise NotImplementedError + + +class JacCmdSpec: + """Jac CLI command.""" + + @staticmethod + @hookspec + def create_cmd() -> None: + """Create Jac CLI cmds.""" + raise NotImplementedError + + +class JacFeatureSpec( + JacAccessValidationSpec, + JacNodeSpec, + JacEdgeSpec, + JacWalkerSpec, + JacBuiltin, + JacCmdSpec, +): """Jac Feature.""" @staticmethod @@ -475,35 +514,4 @@ def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: raise NotImplementedError -class JacBuiltin: - """Jac Builtins.""" - - @staticmethod - @hookspec(firstresult=True) - def dotgen( - node: NodeArchitype, - depth: int, - traverse: bool, - edge_type: Optional[list[str]], - bfs: bool, - edge_limit: int, - node_limit: int, - dot_file: Optional[str], - ) -> str: - """Print the dot graph.""" - raise NotImplementedError - - -class JacCmdSpec: - """Jac CLI command.""" - - @staticmethod - @hookspec - def create_cmd() -> None: - """Create Jac CLI cmds.""" - raise NotImplementedError - - hookmanager.add_hookspecs(JacFeatureSpec) -hookmanager.add_hookspecs(JacCmdSpec) -hookmanager.add_hookspecs(JacBuiltin) diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index b300cb430a..d2badd6c2a 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Optional, cast from uuid import UUID -from .architype import NodeAnchor, Root +from .interface import NodeAnchor, Root from .memory import Memory, ShelfStorage diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py index 0b23f09ab6..8ca75da385 100644 --- a/jac/jaclang/runtimelib/interface.py +++ b/jac/jaclang/runtimelib/interface.py @@ -3,7 +3,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import IntEnum -from typing import ClassVar, Generic, Iterable, Type, TypeVar +from types import UnionType +from typing import Any, Callable, ClassVar, Generic, Iterable, Type, TypeVar _ID = TypeVar("_ID") @@ -145,3 +146,16 @@ class Root(NodeArchitype[_SERIALIZE]): @dataclass(kw_only=True) class GenericEdge(EdgeArchitype[_SERIALIZE]): """Default Edge Architype.""" + + +@dataclass(eq=False) +class DSFunc: + """Data Spatial Function.""" + + name: str + trigger: type | UnionType | tuple[type | UnionType, ...] | None + func: Callable[[Any, Any], Any] | None = None + + def resolve(self, cls: type) -> None: + """Resolve the function.""" + self.func = getattr(cls, self.name) From b5d29d502f3fdd3210d1d11b3d26d4a01592dfe2 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Wed, 25 Sep 2024 17:55:13 +0800 Subject: [PATCH 4/8] temporary --- jac/jaclang/cli/cli.py | 2 +- jac/jaclang/runtimelib/context.py | 15 +-- jac/jaclang/runtimelib/implementation.py | 3 +- jac/jaclang/runtimelib/interface.py | 127 ++++++++++++++++++++++- jac/jaclang/runtimelib/memory.py | 115 ++++++-------------- 5 files changed, 166 insertions(+), 96 deletions(-) diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index 294ca36d7c..3634449d9a 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -19,8 +19,8 @@ from jaclang.compiler.passes.tool.schedules import format_pass from jaclang.plugin.builtin import dotgen from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.constructs import WalkerArchitype from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.implementation import WalkerArchitype from jaclang.runtimelib.machine import JacMachine, JacProgram from jaclang.utils.helpers import debugger as db from jaclang.utils.lang_tools import AstTool diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index d2badd6c2a..5df5becf4e 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -4,11 +4,12 @@ import unittest from contextvars import ContextVar -from typing import Any, Callable, Optional, cast +from typing import Callable, Optional, cast from uuid import UUID -from .interface import NodeAnchor, Root -from .memory import Memory, ShelfStorage +from .implementation import NodeAnchor, Root +from .interface import ExecutionContext as BaseExecutionContext +from .memory import ShelfStorage EXECUTION_CONTEXT = ContextVar[Optional["ExecutionContext"]]("ExecutionContext") @@ -21,15 +22,9 @@ SUPER_ROOT_ARCHITYPE.__jac__ = SUPER_ROOT_ANCHOR -class ExecutionContext: +class ExecutionContext(BaseExecutionContext): """Execution Context.""" - mem: Memory - reports: list[Any] - system_root: NodeAnchor - root: NodeAnchor - entry_node: NodeAnchor - def init_anchor( self, anchor_id: str | None, diff --git a/jac/jaclang/runtimelib/implementation.py b/jac/jaclang/runtimelib/implementation.py index 22ccdf944b..c5705f5f77 100644 --- a/jac/jaclang/runtimelib/implementation.py +++ b/jac/jaclang/runtimelib/implementation.py @@ -19,7 +19,8 @@ _ANCHOR, ) -ANCHOR_TYPES: TypeAlias = "NodeAnchor" | "EdgeAnchor" | "WalkerAnchor" +Anchor: TypeAlias = "NodeAnchor" | "EdgeAnchor" | "WalkerAnchor" +Architype: TypeAlias = "NodeArchitype" | "EdgeArchitype" | "WalkerArchitype" logger = getLogger(__name__) diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py index 8ca75da385..89c46c31a5 100644 --- a/jac/jaclang/runtimelib/interface.py +++ b/jac/jaclang/runtimelib/interface.py @@ -1,10 +1,12 @@ """Jaclang Runtimelib interfaces.""" +from __future__ import annotations + from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import IntEnum from types import UnionType -from typing import Any, Callable, ClassVar, Generic, Iterable, Type, TypeVar +from typing import Any, Callable, ClassVar, Generator, Generic, Iterable, Type, TypeVar _ID = TypeVar("_ID") @@ -14,6 +16,11 @@ _DESERIALIZE = TypeVar("_DESERIALIZE") +######################################################################################### +# ID / ACCESS # +######################################################################################### + + @dataclass(kw_only=True) class JID(Generic[_ID, _ANCHOR], ABC): """Jaclang ID Interface.""" @@ -62,6 +69,11 @@ class Permission: roots: Access = field(default_factory=Access) +######################################################################################### +# ANCHORS # +######################################################################################### + + @dataclass(kw_only=True) class Anchor(Generic[_SERIALIZE], ABC): """Anchor Interface.""" @@ -105,6 +117,11 @@ class WalkerAnchor(Anchor[_SERIALIZE]): architype: "WalkerArchitype" +######################################################################################### +# ARCHITYPES # +######################################################################################### + + class Architype(Generic[_SERIALIZE], ABC): """Architype Interface.""" @@ -159,3 +176,111 @@ class DSFunc: def resolve(self, cls: type) -> None: """Resolve the function.""" self.func = getattr(cls, self.name) + + +######################################################################################### +# MEMORY INTERFACES # +######################################################################################### + + +@dataclass +class Memory(Generic[_ID, _ANCHOR]): + """Generic Memory Handler.""" + + __mem__: dict[_ID, _ANCHOR] = field(default_factory=dict) + __gc__: set[_ID] = field(default_factory=set) + + def close(self) -> None: + """Close memory handler.""" + self.__mem__.clear() + self.__gc__.clear() + + def find( + self, + ids: _ID | Iterable[_ID], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> Generator[_ANCHOR, None, None]: + """Find anchors from memory by ids with filter.""" + if not isinstance(ids, Iterable): + ids = [ids] + + return ( + anchor + for id in ids + if (anchor := self.__mem__.get(id)) and (not filter or filter(anchor)) + ) + + def find_one( + self, + ids: _ID | Iterable[_ID], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> _ANCHOR | None: + """Find one anchor from memory by ids with filter.""" + return next(self.find(ids, filter), None) + + def find_by_id(self, id: _ID) -> _ANCHOR | None: + """Find one by id.""" + return self.__mem__.get(id) + + def set(self, id: _ID, data: _ANCHOR) -> None: + """Save anchor to memory.""" + self.__mem__[id] = data + + def remove(self, ids: _ID | Iterable[_ID]) -> None: + """Remove anchor/s from memory.""" + if not isinstance(ids, Iterable): + ids = [ids] + + for id in ids: + if self.__mem__.pop(id, None): + self.__gc__.add(id) + + +######################################################################################### +# CONTEXT # +######################################################################################### + + +class ExecutionContext(ABC): + """Execution Context.""" + + mem: Memory + reports: list[Any] + system_root: NodeAnchor + root: NodeAnchor + entry_node: NodeAnchor + + @abstractmethod + def init_anchor( + self, + anchor_id: str | None, + default: NodeAnchor, + ) -> NodeAnchor: + """Load initial anchors.""" + + def set_entry_node(self, entry_node: str | None) -> None: + """Override entry.""" + self.entry_node = self.init_anchor(entry_node, self.root) + + @abstractmethod + def close(self) -> None: + """Close current ExecutionContext.""" + + @staticmethod + @abstractmethod + def create( + session: str | None = None, + root: str | None = None, + auto_close: bool = True, + ) -> ExecutionContext: + """Create ExecutionContext.""" + + @staticmethod + @abstractmethod + def get() -> ExecutionContext: + """Get current ExecutionContext.""" + + @staticmethod + @abstractmethod + def get_root() -> Root: + """Get current root.""" diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 051165e5ee..dc0730cb76 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -2,72 +2,19 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from pickle import dumps from shelve import Shelf, open -from typing import Callable, Generator, Generic, Iterable, TypeVar -from uuid import UUID +from typing import Callable, Generator, Iterable, TypeVar -from .architype import Anchor, NodeAnchor, Root, TANCH +from .implementation import Anchor, JID, NodeAnchor, Root +from .interface import Memory ID = TypeVar("ID") @dataclass -class Memory(Generic[ID, TANCH]): - """Generic Memory Handler.""" - - __mem__: dict[ID, TANCH] = field(default_factory=dict) - __gc__: set[TANCH] = field(default_factory=set) - - def close(self) -> None: - """Close memory handler.""" - self.__mem__.clear() - self.__gc__.clear() - - def find( - self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> Generator[TANCH, None, None]: - """Find anchors from memory by ids with filter.""" - if not isinstance(ids, Iterable): - ids = [ids] - - return ( - anchor - for id in ids - if (anchor := self.__mem__.get(id)) and (not filter or filter(anchor)) - ) - - def find_one( - self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> TANCH | None: - """Find one anchor from memory by ids with filter.""" - return next(self.find(ids, filter), None) - - def find_by_id(self, id: ID) -> TANCH | None: - """Find one by id.""" - return self.__mem__.get(id) - - def set(self, id: ID, data: TANCH) -> None: - """Save anchor to memory.""" - self.__mem__[id] = data - - def remove(self, ids: ID | Iterable[ID]) -> None: - """Remove anchor/s from memory.""" - if not isinstance(ids, Iterable): - ids = [ids] - - for id in ids: - if anchor := self.__mem__.pop(id, None): - self.__gc__.add(anchor) - - -@dataclass -class ShelfStorage(Memory[UUID, Anchor]): +class ShelfStorage(Memory[JID[Anchor], Anchor]): """Shelf Handler.""" __shelf__: Shelf[Anchor] | None = None @@ -82,45 +29,47 @@ def close(self) -> None: if isinstance(self.__shelf__, Shelf): from jaclang.plugin.feature import JacFeature as Jac - for anchor in self.__gc__: - self.__shelf__.pop(str(anchor.id), None) - self.__mem__.pop(anchor.id, None) + for jid in self.__gc__: + self.__shelf__.pop(str(jid), None) + self.__mem__.pop(jid, None) - for d in self.__mem__.values(): - if d.persistent and d.hash != hash(dumps(d)): - _id = str(d.id) - if p_d := self.__shelf__.get(_id): + for jid, anchor in self.__mem__.items(): + if anchor.persistent and anchor.hash != hash(dumps(anchor)): + _jid = str(jid) + if p_d := self.__shelf__.get(_jid): if ( isinstance(p_d, NodeAnchor) - and isinstance(d, NodeAnchor) - and p_d.edges != d.edges - and Jac.check_connect_access(d) + and isinstance(anchor, NodeAnchor) + and p_d.edge_ids != anchor.edge_ids + and Jac.check_connect_access(anchor) ): - if not d.edges: - self.__shelf__.pop(_id, None) + if not anchor.edge_ids: + self.__shelf__.pop(_jid, None) continue - p_d.edges = d.edges + p_d.edge_ids = anchor.edge_ids - if Jac.check_write_access(d): - if hash(dumps(p_d.access)) != hash(dumps(d.access)): - p_d.access = d.access - if hash(dumps(d.architype)) != hash(dumps(d.architype)): - p_d.architype = d.architype + if Jac.check_write_access(anchor): + if hash(dumps(p_d.access)) != hash(dumps(anchor.access)): + p_d.access = anchor.access + if hash(dumps(anchor.architype)) != hash( + dumps(anchor.architype) + ): + p_d.architype = anchor.architype - self.__shelf__[_id] = p_d + self.__shelf__[_jid] = p_d elif not ( - isinstance(d, NodeAnchor) - and not isinstance(d.architype, Root) - and not d.edges + isinstance(anchor, NodeAnchor) + and not isinstance(anchor.architype, Root) + and not anchor.edge_ids ): - self.__shelf__[_id] = d + self.__shelf__[_jid] = anchor self.__shelf__.close() super().close() def find( self, - ids: UUID | Iterable[UUID], + ids: JID[Anchor] | Iterable[JID[Anchor]], filter: Callable[[Anchor], Anchor] | None = None, ) -> Generator[Anchor, None, None]: """Find anchors from datasource by ids with filter.""" @@ -142,7 +91,7 @@ def find( else: yield from super().find(ids, filter) - def find_by_id(self, id: UUID) -> Anchor | None: + def find_by_id(self, id: JID[Anchor]) -> Anchor | None: """Find one by id.""" data = super().find_by_id(id) From 5fdf10bab7f709351972ae07625fe940f8869769 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Wed, 25 Sep 2024 19:07:21 +0800 Subject: [PATCH 5/8] temporary --- jac/jaclang/plugin/default.py | 49 ++++++++++++++----------- jac/jaclang/plugin/feature.py | 18 ++++++---- jac/jaclang/plugin/spec.py | 10 +++--- jac/jaclang/runtimelib/utils.py | 64 +++++++++++++++++++-------------- 4 files changed, 81 insertions(+), 60 deletions(-) diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index cbcdc71c08..39cc2b0d93 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -13,35 +13,36 @@ from functools import wraps from logging import getLogger from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union -from uuid import UUID from jaclang.compiler.constant import colors from jaclang.compiler.semtable import SemInfo, SemRegistry, SemScope from jaclang.plugin.feature import ( AccessLevel, - Anchor, - Architype, DSFunc, - EdgeAnchor, - EdgeArchitype, EdgeDir, - ExecutionContext, JacFeature as Jac, - NodeAnchor, - NodeArchitype, P, PyastGenPass, - Root, T, - WalkerArchitype, ast, ) -from jaclang.runtimelib.constructs import ( +from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.implementation import ( + Anchor, + Architype, + EdgeAnchor, + EdgeArchitype, GenericEdge, - JacTestCheck, + JID, + NodeAnchor, + NodeArchitype, + Root, + WalkerArchitype, ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram +from jaclang.runtimelib.memory import ShelfStorage +from jaclang.runtimelib.test import JacTestCheck from jaclang.runtimelib.utils import collect_node_connections, traverse_graph @@ -57,26 +58,30 @@ class JacAccessValidationImpl: @staticmethod @hookimpl def allow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, + root_jid: JID, + level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Allow all access from target root graph to current Architype.""" level = AccessLevel.cast(level) access = anchor.access.roots - _root_id = str(root_id) - if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): - access.anchors[_root_id] = level + _root_jid = str(root_jid) + if level != access.anchors.get(_root_jid, AccessLevel.NO_ACCESS): + access.anchors[_root_jid] = level @staticmethod @hookimpl def disallow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, + root_jid: JID, + level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Disallow all access from target root graph to current Architype.""" level = AccessLevel.cast(level) access = anchor.access.roots - access.anchors.pop(str(root_id), None) + access.anchors.pop(str(root_jid), None) @staticmethod @hookimpl @@ -448,11 +453,13 @@ def get_context() -> ExecutionContext: @staticmethod @hookimpl - def get_object(id: str) -> Architype | None: + def get_object(jid: JID) -> Architype | None: """Get object by id.""" + jctx = Jac.get_context() + mem: ShelfStorage = jctx.mem if id == "root": - return Jac.get_context().root.architype - elif obj := Jac.get_context().mem.find_by_id(UUID(id)): + return jctx.root.architype + elif obj := mem.find_by_id(jid): return obj.architype return None diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index 9f469f0301..df53d578bc 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -15,7 +15,6 @@ TypeAlias, Union, ) -from uuid import UUID from jaclang.plugin.spec import ( AccessLevel, @@ -26,6 +25,7 @@ EdgeArchitype, EdgeDir, ExecutionContext, + JID, NodeAnchor, NodeArchitype, P, @@ -43,17 +43,21 @@ class JacAccessValidation: @staticmethod def allow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, + root_jid: JID, + level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Allow all access from target root graph to current Architype.""" - hookmanager.hook.allow_root(anchor=anchor, root_id=root_id, level=level) + hookmanager.hook.allow_root(anchor=anchor, root_jid=root_jid, level=level) @staticmethod def disallow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, + root_jid: JID, + level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Disallow all access from target root graph to current Architype.""" - hookmanager.hook.disallow_root(anchor=anchor, root_id=root_id, level=level) + hookmanager.hook.disallow_root(anchor=anchor, root_jid=root_jid, level=level) @staticmethod def unrestrict( @@ -247,9 +251,9 @@ def get_context() -> ExecutionContext: return hookmanager.hook.get_context() @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(jid: JID) -> Architype | None: """Get object given id.""" - return hookmanager.hook.get_object(id=id) + return hookmanager.hook.get_object(jid=jid) @staticmethod def object_ref(obj: Architype) -> str: diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index 60e8435930..bf54bd06e8 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -16,17 +16,17 @@ TypeVar, Union, ) -from uuid import UUID from jaclang.compiler import absyntree as ast from jaclang.compiler.constant import EdgeDir from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass -from jaclang.runtimelib.context import ExecutionContext from jaclang.runtimelib.interface import ( AccessLevel, DSFunc, EdgeAnchor, EdgeArchitype, + ExecutionContext, + JID, NodeAnchor, NodeArchitype, Root, @@ -52,7 +52,7 @@ class JacAccessValidationSpec: @staticmethod @hookspec(firstresult=True) def allow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, root_jid: JID, level: AccessLevel | int | str = AccessLevel.READ ) -> None: """Allow all access from target root graph to current Architype.""" raise NotImplementedError @@ -60,7 +60,7 @@ def allow_root( @staticmethod @hookspec(firstresult=True) def disallow_root( - anchor: Anchor, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ + anchor: Anchor, root_jid: JID, level: AccessLevel | int | str = AccessLevel.READ ) -> None: """Disallow all access from target root graph to current Architype.""" raise NotImplementedError @@ -251,7 +251,7 @@ def get_context() -> ExecutionContext: @staticmethod @hookspec(firstresult=True) - def get_object(id: str) -> Architype | None: + def get_object(jid: JID) -> Architype | None: """Get object by id.""" raise NotImplementedError diff --git a/jac/jaclang/runtimelib/utils.py b/jac/jaclang/runtimelib/utils.py index 010016640a..af9e8a8e8c 100644 --- a/jac/jaclang/runtimelib/utils.py +++ b/jac/jaclang/runtimelib/utils.py @@ -5,13 +5,12 @@ import ast as ast3 import sys from contextlib import contextmanager -from typing import Callable, Iterator, TYPE_CHECKING +from typing import Callable, Iterator import jaclang.compiler.absyntree as ast from jaclang.compiler.semtable import SemScope - -if TYPE_CHECKING: - from jaclang.runtimelib.constructs import NodeAnchor, NodeArchitype +from jaclang.plugin.feature import JacFeature as Jac +from jaclang.runtimelib.interface import EdgeArchitype, NodeAnchor, NodeArchitype @contextmanager @@ -35,18 +34,21 @@ def collect_node_connections( """Nodes and edges representing the graph are collected in visited_nodes and connections.""" if current_node not in visited_nodes: visited_nodes.add(current_node) - edges = current_node.edges - for edge_ in edges: - target = edge_.target - if target: - connections.add( - ( - current_node.architype, - target.architype, - edge_.__class__.__name__, - ) + for edge_id in current_node.edge_ids: + if not isinstance( + edge := Jac.get_object(edge_id), EdgeArchitype + ) or not isinstance( + target := Jac.get_object(edge.__jac__.target_id), NodeArchitype + ): + continue + connections.add( + ( + current_node.architype, + target, + edge.__class__.__name__, ) - collect_node_connections(target, visited_nodes, connections) + ) + collect_node_connections(target.__jac__, visited_nodes, connections) def traverse_graph( @@ -65,20 +67,28 @@ def traverse_graph( edge_limit: int, ) -> None: """Traverse the graph using Breadth-First Search (BFS) or Depth-First Search (DFS).""" - for edge in node.__jac__.edges: - is_self_loop = id(edge.source) == id(edge.target) - is_in_edge = edge.target == node.__jac__ - if (traverse and is_in_edge) or edge.architype.__class__.__name__ in edge_type: + nanch = node.__jac__ + for edge_id in nanch.edge_ids: + if not isinstance(edge := Jac.get_object(edge_id), EdgeArchitype): + continue + + eanch = edge.__jac__ + if eanch.source_id == eanch.target_id: + continue # lets skip self loop for a while, need to handle it later [ old is_self_loop ] + + if not isinstance( + source := Jac.get_object(eanch.source_id), NodeArchitype + ) or not isinstance(target := Jac.get_object(eanch.target_id), NodeArchitype): + continue + + is_in_edge = target == nanch + + if (traverse and is_in_edge) or edge.__class__.__name__ in edge_type: continue - if is_self_loop: - continue # lets skip self loop for a while, need to handle it later - elif (other_nda := edge.target if not is_in_edge else edge.source) and ( - other_nd := other_nda.architype - ): + + if other_nd := target if not is_in_edge else source: new_con = ( - (node, other_nd, edge.architype) - if not is_in_edge - else (other_nd, node, edge.architype) + (node, other_nd, edge) if not is_in_edge else (other_nd, node, edge) ) if node in node_depths and node_depths[node] is not None: if other_nd in node_depths: From 8f67d6c87d7709d000d543a0ffbc2df0b2060bf0 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Thu, 26 Sep 2024 00:08:23 +0800 Subject: [PATCH 6/8] temporary --- jac/jaclang/plugin/default.py | 12 +++--- jac/jaclang/runtimelib/context.py | 18 ++++---- jac/jaclang/runtimelib/implementation.py | 53 +++++++++++++++++++++--- jac/jaclang/runtimelib/interface.py | 28 ++++++------- 4 files changed, 78 insertions(+), 33 deletions(-) diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 39cc2b0d93..d2f3a46f0f 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -106,7 +106,7 @@ def check_read_access(to: Anchor) -> bool: """Read Access Validation.""" if not (access_level := Jac.check_access_level(to) > AccessLevel.NO_ACCESS): logger.info( - f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]" + f"Current root doesn't have read access to {to.__class__.__name__}[{to.jid}]" ) return access_level @@ -116,7 +116,7 @@ def check_connect_access(to: Anchor) -> bool: """Write Access Validation.""" if not (access_level := Jac.check_access_level(to) > AccessLevel.READ): logger.info( - f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]" + f"Current root doesn't have connect access to {to.__class__.__name__}[{to.jid}]" ) return access_level @@ -126,7 +126,7 @@ def check_write_access(to: Anchor) -> bool: """Write Access Validation.""" if not (access_level := Jac.check_access_level(to) > AccessLevel.CONNECT): logger.info( - f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]" + f"Current root doesn't have write access to {to.__class__.__name__}[{to.jid}]" ) return access_level @@ -138,13 +138,13 @@ def check_access_level(to: Anchor) -> AccessLevel: return AccessLevel.WRITE jctx = Jac.get_context() - + jmem: ShelfStorage = jctx.mem jroot = jctx.root # if current root is system_root # if current root id is equal to target anchor's root id # if current root is the target anchor - if jroot == jctx.system_root or jroot.id == to.root or jroot == to: + if jroot == jctx.system_root or jroot.jid == to.root or jroot == to: return AccessLevel.WRITE access_level = AccessLevel.NO_ACCESS @@ -155,7 +155,7 @@ def check_access_level(to: Anchor) -> AccessLevel: # if target anchor's root have set allowed roots # if current root is allowed to the whole graph of target anchor's root - if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor): + if to.root and isinstance(to_root := jmem.find_one(to.root), Anchor): if to_root.access.all > access_level: access_level = to_root.access.all diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index 5df5becf4e..ec9953a501 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -7,24 +7,28 @@ from typing import Callable, Optional, cast from uuid import UUID -from .implementation import NodeAnchor, Root +from .implementation import JID, NodeAnchor, Root from .interface import ExecutionContext as BaseExecutionContext from .memory import ShelfStorage EXECUTION_CONTEXT = ContextVar[Optional["ExecutionContext"]]("ExecutionContext") -SUPER_ROOT_UUID = UUID("00000000-0000-0000-0000-000000000000") +SUPER_ROOT_JID = JID[NodeAnchor]( + id=UUID("00000000-0000-0000-0000-000000000000"), type=NodeAnchor +) SUPER_ROOT_ARCHITYPE = object.__new__(Root) SUPER_ROOT_ANCHOR = NodeAnchor( - id=SUPER_ROOT_UUID, architype=SUPER_ROOT_ARCHITYPE, persistent=False, edges=[] + jid=SUPER_ROOT_JID, architype=SUPER_ROOT_ARCHITYPE, persistent=False, edge_ids=set() ) SUPER_ROOT_ARCHITYPE.__jac__ = SUPER_ROOT_ANCHOR -class ExecutionContext(BaseExecutionContext): +class ExecutionContext(BaseExecutionContext[NodeAnchor]): """Execution Context.""" + mem: ShelfStorage + def init_anchor( self, anchor_id: str | None, @@ -32,7 +36,7 @@ def init_anchor( ) -> NodeAnchor: """Load initial anchors.""" if anchor_id: - if isinstance(anchor := self.mem.find_by_id(UUID(anchor_id)), NodeAnchor): + if isinstance(anchor := self.mem.find_by_id(JID(anchor_id)), NodeAnchor): return anchor raise ValueError(f"Invalid anchor id {anchor_id} !") return default @@ -58,10 +62,10 @@ def create( ctx.reports = [] if not isinstance( - system_root := ctx.mem.find_by_id(SUPER_ROOT_UUID), NodeAnchor + system_root := ctx.mem.find_by_id(SUPER_ROOT_JID), NodeAnchor ): system_root = Root().__jac__ - system_root.id = SUPER_ROOT_UUID + system_root.id = SUPER_ROOT_JID ctx.mem.set(system_root.id, system_root) ctx.system_root = system_root diff --git a/jac/jaclang/runtimelib/implementation.py b/jac/jaclang/runtimelib/implementation.py index c5705f5f77..0df204d51b 100644 --- a/jac/jaclang/runtimelib/implementation.py +++ b/jac/jaclang/runtimelib/implementation.py @@ -4,7 +4,8 @@ from dataclasses import dataclass, field from logging import getLogger -from typing import ClassVar, TypeAlias +from re import IGNORECASE, compile +from typing import Type, TypeAlias from uuid import UUID, uuid4 from .interface import ( @@ -19,6 +20,13 @@ _ANCHOR, ) + +JID_REGEX = compile( + r"^(n|e|w):([^:]*):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", + IGNORECASE, +) + + Anchor: TypeAlias = "NodeAnchor" | "EdgeAnchor" | "WalkerAnchor" Architype: TypeAlias = "NodeArchitype" | "EdgeArchitype" | "WalkerArchitype" logger = getLogger(__name__) @@ -28,8 +36,41 @@ class JID(_JID[UUID, _ANCHOR]): """Jaclang Default JID.""" - id: UUID = field(default_factory=uuid4) - name: str = "" + id: UUID + name: str + + def __init__( + self: JID[Anchor], + id: str | UUID | None = None, + type: Type[Anchor] | None = None, + name: str = "", + ) -> None: + """Override JID initializer.""" + match id: + case str(): + if matched := JID_REGEX.search(id): + self.id = UUID(matched.group(3)) + self.name = matched.group(2) + match matched.group(1).lower(): + case "n": + self.type = NodeAnchor + case "e": + self.type = EdgeAnchor + case _: + self.type = WalkerAnchor + return + raise ValueError("Not a valid JID format!") + case UUID(): + self.id = id + case None: + self.id = uuid4() + case _: + raise ValueError("Not a valid id for JID!") + + if type is None: + raise ValueError("Type is required from non string JID!") + self.type = type + self.name = name def __repr__(self) -> str: """Override string representation.""" @@ -111,7 +152,7 @@ def __deserialize__(cls, data: WalkerAnchor) -> WalkerAnchor: class NodeArchitype(_NodeArchitype["NodeArchitype"]): """NodeArchitype Interface.""" - __jac__: ClassVar[NodeAnchor] + __jac__: NodeAnchor def __serialize__(self) -> NodeArchitype: """Override serialization.""" @@ -126,7 +167,7 @@ def __deserialize__(cls, data: NodeArchitype) -> NodeArchitype: class EdgeArchitype(_EdgeArchitype["EdgeArchitype"]): """EdgeArchitype Interface.""" - __jac__: ClassVar[EdgeAnchor] + __jac__: EdgeAnchor def __serialize__(self) -> EdgeArchitype: """Override serialization.""" @@ -141,7 +182,7 @@ def __deserialize__(cls, data: EdgeArchitype) -> EdgeArchitype: class WalkerArchitype(_WalkerArchitype["WalkerArchitype"]): """Walker Architype Interface.""" - __jac__: ClassVar[WalkerAnchor] + __jac__: WalkerAnchor def __serialize__(self) -> WalkerArchitype: """Override serialization.""" diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py index 89c46c31a5..eaad1de59c 100644 --- a/jac/jaclang/runtimelib/interface.py +++ b/jac/jaclang/runtimelib/interface.py @@ -6,16 +6,16 @@ from dataclasses import dataclass, field from enum import IntEnum from types import UnionType -from typing import Any, Callable, ClassVar, Generator, Generic, Iterable, Type, TypeVar +from typing import Any, Callable, Generator, Generic, Iterable, Type, TypeVar _ID = TypeVar("_ID") _ANCHOR = TypeVar("_ANCHOR", bound="Anchor") +_NODE_ANCHOR = TypeVar("_NODE_ANCHOR", bound="NodeAnchor") _SERIALIZE = TypeVar("_SERIALIZE") _DESERIALIZE = TypeVar("_DESERIALIZE") - ######################################################################################### # ID / ACCESS # ######################################################################################### @@ -125,7 +125,7 @@ class WalkerAnchor(Anchor[_SERIALIZE]): class Architype(Generic[_SERIALIZE], ABC): """Architype Interface.""" - __jac__: ClassVar[Anchor] + __jac__: Anchor @abstractmethod def __serialize__(self) -> _SERIALIZE: @@ -140,19 +140,19 @@ def __deserialize__(cls: Type[_DESERIALIZE], data: _SERIALIZE) -> _DESERIALIZE: class NodeArchitype(Architype[_SERIALIZE]): """NodeArchitype Interface.""" - __jac__: ClassVar[NodeAnchor] + __jac__: NodeAnchor class EdgeArchitype(Architype[_SERIALIZE]): """EdgeArchitype Interface.""" - __jac__: ClassVar[EdgeAnchor] + __jac__: EdgeAnchor class WalkerArchitype(Architype[_SERIALIZE]): """Walker Architype Interface.""" - __jac__: ClassVar[WalkerAnchor] + __jac__: WalkerAnchor @dataclass(kw_only=True) @@ -241,24 +241,24 @@ def remove(self, ids: _ID | Iterable[_ID]) -> None: ######################################################################################### -class ExecutionContext(ABC): +class ExecutionContext(Generic[_NODE_ANCHOR], ABC): """Execution Context.""" mem: Memory reports: list[Any] - system_root: NodeAnchor - root: NodeAnchor - entry_node: NodeAnchor + system_root: _NODE_ANCHOR + root: _NODE_ANCHOR + entry_node: _NODE_ANCHOR @abstractmethod def init_anchor( self, - anchor_id: str | None, - default: NodeAnchor, - ) -> NodeAnchor: + anchor_jid: JID | None, + default: _NODE_ANCHOR, + ) -> _NODE_ANCHOR: """Load initial anchors.""" - def set_entry_node(self, entry_node: str | None) -> None: + def set_entry_node(self, entry_node: JID | None) -> None: """Override entry.""" self.entry_node = self.init_anchor(entry_node, self.root) From 4de6f806efbe0ecb69533bacd2c12b8e9a984895 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Thu, 26 Sep 2024 18:18:45 +0800 Subject: [PATCH 7/8] temporary --- jac/jaclang/runtimelib/context.py | 4 +- jac/jaclang/runtimelib/implementation.py | 16 +++--- jac/jaclang/runtimelib/interface.py | 69 ++++++++++++++---------- jac/jaclang/runtimelib/memory.py | 22 +++++--- 4 files changed, 66 insertions(+), 45 deletions(-) diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index ec9953a501..1f60d03e09 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -14,9 +14,7 @@ EXECUTION_CONTEXT = ContextVar[Optional["ExecutionContext"]]("ExecutionContext") -SUPER_ROOT_JID = JID[NodeAnchor]( - id=UUID("00000000-0000-0000-0000-000000000000"), type=NodeAnchor -) +SUPER_ROOT_JID = JID(id=UUID("00000000-0000-0000-0000-000000000000"), type=NodeAnchor) SUPER_ROOT_ARCHITYPE = object.__new__(Root) SUPER_ROOT_ANCHOR = NodeAnchor( jid=SUPER_ROOT_JID, architype=SUPER_ROOT_ARCHITYPE, persistent=False, edge_ids=set() diff --git a/jac/jaclang/runtimelib/implementation.py b/jac/jaclang/runtimelib/implementation.py index 0df204d51b..9daeaae023 100644 --- a/jac/jaclang/runtimelib/implementation.py +++ b/jac/jaclang/runtimelib/implementation.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from logging import getLogger from re import IGNORECASE, compile -from typing import Type, TypeAlias +from typing import Type, TypeAlias, TypeVar from uuid import UUID, uuid4 from .interface import ( @@ -17,7 +17,6 @@ Permission, WalkerAnchor as _WalkerAnchor, WalkerArchitype as _WalkerArchitype, - _ANCHOR, ) @@ -27,22 +26,24 @@ ) +_ANCHOR = TypeVar("_ANCHOR", "NodeAnchor", "EdgeAnchor", "WalkerAnchor", covariant=True) Anchor: TypeAlias = "NodeAnchor" | "EdgeAnchor" | "WalkerAnchor" Architype: TypeAlias = "NodeArchitype" | "EdgeArchitype" | "WalkerArchitype" logger = getLogger(__name__) @dataclass(kw_only=True) -class JID(_JID[UUID, _ANCHOR]): +class JID(_JID[UUID, Anchor]): """Jaclang Default JID.""" id: UUID + type: Type[Anchor] name: str def __init__( - self: JID[Anchor], + self, id: str | UUID | None = None, - type: Type[Anchor] | None = None, + type: Type[_ANCHOR] | None = None, name: str = "", ) -> None: """Override JID initializer.""" @@ -85,7 +86,7 @@ def __str__(self) -> str: class NodeAnchor(_NodeAnchor["NodeAnchor"]): """NodeAnchor Interface.""" - jid: JID[NodeAnchor] = field(default_factory=lambda: JID(type=NodeAnchor)) + jid: JID["NodeAnchor"] = field(default_factory=lambda: JID(type=NodeAnchor)) architype: "NodeArchitype" root: JID["NodeAnchor"] | None = None access: Permission = field(default_factory=Permission) @@ -104,6 +105,9 @@ def __deserialize__(cls, data: NodeAnchor) -> NodeAnchor: return data +aa = JID(id=UUID(), type=NodeAnchor) + + @dataclass(kw_only=True) class EdgeAnchor(_EdgeAnchor["EdgeAnchor"]): """NodeAnchor Interface.""" diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py index eaad1de59c..565cee2d5c 100644 --- a/jac/jaclang/runtimelib/interface.py +++ b/jac/jaclang/runtimelib/interface.py @@ -6,11 +6,19 @@ from dataclasses import dataclass, field from enum import IntEnum from types import UnionType -from typing import Any, Callable, Generator, Generic, Iterable, Type, TypeVar +from typing import ( + Any, + Callable, + Generator, + Generic, + Iterable, + Type, + TypeVar, +) _ID = TypeVar("_ID") -_ANCHOR = TypeVar("_ANCHOR", bound="Anchor") +_ANCHOR = TypeVar("_ANCHOR", "NodeAnchor", "EdgeAnchor", "WalkerAnchor", covariant=True) _NODE_ANCHOR = TypeVar("_NODE_ANCHOR", bound="NodeAnchor") _SERIALIZE = TypeVar("_SERIALIZE") @@ -75,12 +83,12 @@ class Permission: @dataclass(kw_only=True) -class Anchor(Generic[_SERIALIZE], ABC): +class Anchor(Generic[_ID, _SERIALIZE], ABC): """Anchor Interface.""" - jid: JID - architype: "Architype" - root: JID | None + jid: JID[_ID, NodeAnchor] | JID[_ID, EdgeAnchor] | JID[_ID, WalkerAnchor] + architype: "NodeArchitype" | "EdgeArchitype" | "WalkerArchitype" + root: JID[_ID, NodeAnchor] | None access: Permission @abstractmethod @@ -94,26 +102,29 @@ def __deserialize__(cls: Type[_DESERIALIZE], data: _SERIALIZE) -> _DESERIALIZE: @dataclass(kw_only=True) -class NodeAnchor(Anchor[_SERIALIZE]): +class NodeAnchor(Anchor[_ID, _SERIALIZE]): """NodeAnchor Interface.""" + jid: JID[_ID, NodeAnchor] architype: "NodeArchitype" - edge_ids: Iterable[JID] + edge_ids: Iterable[JID[_ID, EdgeAnchor]] @dataclass(kw_only=True) -class EdgeAnchor(Anchor[_SERIALIZE]): +class EdgeAnchor(Anchor[_ID, _SERIALIZE]): """EdgeAnchor Interface.""" + jid: JID[_ID, EdgeAnchor] architype: "EdgeArchitype" - source_id: JID - target_id: JID + source_id: JID[_ID, NodeAnchor] + target_id: JID[_ID, NodeAnchor] @dataclass(kw_only=True) -class WalkerAnchor(Anchor[_SERIALIZE]): +class WalkerAnchor(Anchor[_ID, _SERIALIZE]): """WalkerAnchor Interface.""" + jid: JID[_ID, WalkerAnchor] architype: "WalkerArchitype" @@ -125,7 +136,7 @@ class WalkerAnchor(Anchor[_SERIALIZE]): class Architype(Generic[_SERIALIZE], ABC): """Architype Interface.""" - __jac__: Anchor + __jac__: NodeAnchor | EdgeAnchor | WalkerAnchor @abstractmethod def __serialize__(self) -> _SERIALIZE: @@ -187,8 +198,8 @@ def resolve(self, cls: type) -> None: class Memory(Generic[_ID, _ANCHOR]): """Generic Memory Handler.""" - __mem__: dict[_ID, _ANCHOR] = field(default_factory=dict) - __gc__: set[_ID] = field(default_factory=set) + __mem__: dict[JID[_ID, _ANCHOR], _ANCHOR] = field(default_factory=dict) + __gc__: set[JID[_ID, _ANCHOR]] = field(default_factory=set) def close(self) -> None: """Close memory handler.""" @@ -197,36 +208,38 @@ def close(self) -> None: def find( self, - ids: _ID | Iterable[_ID], + ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]], filter: Callable[[_ANCHOR], _ANCHOR] | None = None, ) -> Generator[_ANCHOR, None, None]: """Find anchors from memory by ids with filter.""" if not isinstance(ids, Iterable): ids = [ids] - return ( - anchor - for id in ids - if (anchor := self.__mem__.get(id)) and (not filter or filter(anchor)) - ) + for id in ids: + if ( + (anchor := self.__mem__.get(id)) + and isinstance(anchor, id.type) + and (not filter or filter(anchor)) + ): + yield anchor def find_one( self, - ids: _ID | Iterable[_ID], + ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]], filter: Callable[[_ANCHOR], _ANCHOR] | None = None, ) -> _ANCHOR | None: """Find one anchor from memory by ids with filter.""" return next(self.find(ids, filter), None) - def find_by_id(self, id: _ID) -> _ANCHOR | None: + def find_by_id(self, id: JID[_ID, _ANCHOR]) -> _ANCHOR | None: """Find one by id.""" return self.__mem__.get(id) - def set(self, id: _ID, data: _ANCHOR) -> None: + def set(self, data: _ANCHOR) -> None: """Save anchor to memory.""" - self.__mem__[id] = data + self.__mem__[data.jid] = data - def remove(self, ids: _ID | Iterable[_ID]) -> None: + def remove(self, ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]]) -> None: """Remove anchor/s from memory.""" if not isinstance(ids, Iterable): ids = [ids] @@ -253,12 +266,12 @@ class ExecutionContext(Generic[_NODE_ANCHOR], ABC): @abstractmethod def init_anchor( self, - anchor_jid: JID | None, + anchor_jid: str | None, default: _NODE_ANCHOR, ) -> _NODE_ANCHOR: """Load initial anchors.""" - def set_entry_node(self, entry_node: JID | None) -> None: + def set_entry_node(self, entry_node: str | None) -> None: """Override entry.""" self.entry_node = self.init_anchor(entry_node, self.root) diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index dc0730cb76..e29d28cdce 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -7,14 +7,20 @@ from shelve import Shelf, open from typing import Callable, Generator, Iterable, TypeVar -from .implementation import Anchor, JID, NodeAnchor, Root +from .implementation import ( + Anchor, + EdgeAnchor, + JID, + NodeAnchor, + Root, + WalkerAnchor, + _ANCHOR, +) from .interface import Memory -ID = TypeVar("ID") - @dataclass -class ShelfStorage(Memory[JID[Anchor], Anchor]): +class ShelfStorage(Memory[JID, Anchor]): """Shelf Handler.""" __shelf__: Shelf[Anchor] | None = None @@ -69,9 +75,9 @@ def close(self) -> None: def find( self, - ids: JID[Anchor] | Iterable[JID[Anchor]], - filter: Callable[[Anchor], Anchor] | None = None, - ) -> Generator[Anchor, None, None]: + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> Generator[_ANCHOR, None, None]: """Find anchors from datasource by ids with filter.""" if not isinstance(ids, Iterable): ids = [ids] @@ -91,7 +97,7 @@ def find( else: yield from super().find(ids, filter) - def find_by_id(self, id: JID[Anchor]) -> Anchor | None: + def find_by_id(self, id: JID[_ANCHOR]) -> _ANCHOR | None: """Find one by id.""" data = super().find_by_id(id) From e55d7e99119abcc65eb70c8a3520ada832182cba Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Thu, 26 Sep 2024 19:12:26 +0800 Subject: [PATCH 8/8] temporary --- jac/jaclang/runtimelib/implementation.py | 17 ++++------ jac/jaclang/runtimelib/interface.py | 43 ++++++++++++------------ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/jac/jaclang/runtimelib/implementation.py b/jac/jaclang/runtimelib/implementation.py index 9daeaae023..e0f87d1d9d 100644 --- a/jac/jaclang/runtimelib/implementation.py +++ b/jac/jaclang/runtimelib/implementation.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from logging import getLogger from re import IGNORECASE, compile -from typing import Type, TypeAlias, TypeVar +from typing import Generic, Type, TypeAlias, TypeVar from uuid import UUID, uuid4 from .interface import ( @@ -33,12 +33,11 @@ @dataclass(kw_only=True) -class JID(_JID[UUID, Anchor]): +class JID(Generic[_ANCHOR], _JID): """Jaclang Default JID.""" id: UUID - type: Type[Anchor] - name: str + type: Type[_ANCHOR] def __init__( self, @@ -52,13 +51,14 @@ def __init__( if matched := JID_REGEX.search(id): self.id = UUID(matched.group(3)) self.name = matched.group(2) + # currently no way to base hinting on string regex! match matched.group(1).lower(): case "n": - self.type = NodeAnchor + self.type = NodeAnchor # type: ignore [assignment] case "e": - self.type = EdgeAnchor + self.type = EdgeAnchor # type: ignore [assignment] case _: - self.type = WalkerAnchor + self.type = WalkerAnchor # type: ignore [assignment] return raise ValueError("Not a valid JID format!") case UUID(): @@ -105,9 +105,6 @@ def __deserialize__(cls, data: NodeAnchor) -> NodeAnchor: return data -aa = JID(id=UUID(), type=NodeAnchor) - - @dataclass(kw_only=True) class EdgeAnchor(_EdgeAnchor["EdgeAnchor"]): """NodeAnchor Interface.""" diff --git a/jac/jaclang/runtimelib/interface.py b/jac/jaclang/runtimelib/interface.py index 565cee2d5c..94be3ee6d8 100644 --- a/jac/jaclang/runtimelib/interface.py +++ b/jac/jaclang/runtimelib/interface.py @@ -17,7 +17,6 @@ ) -_ID = TypeVar("_ID") _ANCHOR = TypeVar("_ANCHOR", "NodeAnchor", "EdgeAnchor", "WalkerAnchor", covariant=True) _NODE_ANCHOR = TypeVar("_NODE_ANCHOR", bound="NodeAnchor") @@ -30,10 +29,10 @@ @dataclass(kw_only=True) -class JID(Generic[_ID, _ANCHOR], ABC): +class JID(Generic[_ANCHOR], ABC): """Jaclang ID Interface.""" - id: _ID + id: Any type: Type[_ANCHOR] name: str @@ -83,12 +82,12 @@ class Permission: @dataclass(kw_only=True) -class Anchor(Generic[_ID, _SERIALIZE], ABC): +class Anchor(Generic[_SERIALIZE], ABC): """Anchor Interface.""" - jid: JID[_ID, NodeAnchor] | JID[_ID, EdgeAnchor] | JID[_ID, WalkerAnchor] + jid: JID[NodeAnchor] | JID[EdgeAnchor] | JID[WalkerAnchor] architype: "NodeArchitype" | "EdgeArchitype" | "WalkerArchitype" - root: JID[_ID, NodeAnchor] | None + root: JID[NodeAnchor] | None access: Permission @abstractmethod @@ -102,29 +101,29 @@ def __deserialize__(cls: Type[_DESERIALIZE], data: _SERIALIZE) -> _DESERIALIZE: @dataclass(kw_only=True) -class NodeAnchor(Anchor[_ID, _SERIALIZE]): +class NodeAnchor(Anchor[_SERIALIZE]): """NodeAnchor Interface.""" - jid: JID[_ID, NodeAnchor] + jid: JID[NodeAnchor] architype: "NodeArchitype" - edge_ids: Iterable[JID[_ID, EdgeAnchor]] + edge_ids: Iterable[JID[EdgeAnchor]] @dataclass(kw_only=True) -class EdgeAnchor(Anchor[_ID, _SERIALIZE]): +class EdgeAnchor(Anchor[_SERIALIZE]): """EdgeAnchor Interface.""" - jid: JID[_ID, EdgeAnchor] + jid: JID[EdgeAnchor] architype: "EdgeArchitype" - source_id: JID[_ID, NodeAnchor] - target_id: JID[_ID, NodeAnchor] + source_id: JID[NodeAnchor] + target_id: JID[NodeAnchor] @dataclass(kw_only=True) -class WalkerAnchor(Anchor[_ID, _SERIALIZE]): +class WalkerAnchor(Anchor[_SERIALIZE]): """WalkerAnchor Interface.""" - jid: JID[_ID, WalkerAnchor] + jid: JID[WalkerAnchor] architype: "WalkerArchitype" @@ -195,11 +194,11 @@ def resolve(self, cls: type) -> None: @dataclass -class Memory(Generic[_ID, _ANCHOR]): +class Memory(Generic[_ANCHOR]): """Generic Memory Handler.""" - __mem__: dict[JID[_ID, _ANCHOR], _ANCHOR] = field(default_factory=dict) - __gc__: set[JID[_ID, _ANCHOR]] = field(default_factory=set) + __mem__: dict[JID[_ANCHOR], _ANCHOR] = field(default_factory=dict) + __gc__: set[JID[_ANCHOR]] = field(default_factory=set) def close(self) -> None: """Close memory handler.""" @@ -208,7 +207,7 @@ def close(self) -> None: def find( self, - ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]], + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], filter: Callable[[_ANCHOR], _ANCHOR] | None = None, ) -> Generator[_ANCHOR, None, None]: """Find anchors from memory by ids with filter.""" @@ -225,13 +224,13 @@ def find( def find_one( self, - ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]], + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], filter: Callable[[_ANCHOR], _ANCHOR] | None = None, ) -> _ANCHOR | None: """Find one anchor from memory by ids with filter.""" return next(self.find(ids, filter), None) - def find_by_id(self, id: JID[_ID, _ANCHOR]) -> _ANCHOR | None: + def find_by_id(self, id: JID[_ANCHOR]) -> _ANCHOR | None: """Find one by id.""" return self.__mem__.get(id) @@ -239,7 +238,7 @@ def set(self, data: _ANCHOR) -> None: """Save anchor to memory.""" self.__mem__[data.jid] = data - def remove(self, ids: JID[_ID, _ANCHOR] | Iterable[JID[_ID, _ANCHOR]]) -> None: + def remove(self, ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]]) -> None: """Remove anchor/s from memory.""" if not isinstance(ids, Iterable): ids = [ids]