diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index 7694ed2324..7caf1e5f5b 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -284,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/default.py b/jac/jaclang/plugin/default.py index 5e454b3030..fbd05bc8a1 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -42,7 +42,7 @@ ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram -from jaclang.runtimelib.utils import traverse_graph +from jaclang.runtimelib.utils import collect_node_connections, traverse_graph import pluggy @@ -54,6 +54,47 @@ 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: @@ -129,6 +170,31 @@ def check_access_level(to: Anchor) -> AccessLevel: 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( @@ -763,7 +829,7 @@ def disconnect( and target.architype in right and Jac.check_write_access(target) ): - anchor.destroy() if anchor.persistent else Jac.detach(anchor) + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True if ( dir in [EdgeDir.IN, EdgeDir.ANY] @@ -771,7 +837,7 @@ def disconnect( and source.architype in right and Jac.check_write_access(source) ): - anchor.destroy() if anchor.persistent else Jac.detach(anchor) + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True return disconnect_occurred @@ -812,9 +878,13 @@ 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__ + eanch = edge.__jac__ = EdgeAnchor( + architype=edge, + source=source, + target=target, + is_undirected=is_undirected, + ) source.edges.append(eanch) target.edges.append(eanch) @@ -825,13 +895,44 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: else: raise ValueError(f"Invalid attribute: {fld}") if source.persistent or target.persistent: - eanch.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( diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index aff4a5fae3..7279d1dddd 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -5,6 +5,7 @@ import ast as ast3 import types from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union +from uuid import UUID from jaclang.plugin.spec import ( AccessLevel, @@ -30,6 +31,32 @@ 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.""" @@ -54,6 +81,11 @@ def check_access_level(to: Anchor) -> AccessLevel: 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, @@ -344,6 +376,20 @@ def 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 diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index 18c4b242bd..376fe051e0 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -15,6 +15,7 @@ TypeVar, Union, ) +from uuid import UUID from jaclang.compiler import absyntree as ast from jaclang.compiler.constant import EdgeDir @@ -45,6 +46,36 @@ 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: @@ -73,6 +104,12 @@ def check_access_level(to: Anchor) -> AccessLevel: 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( @@ -356,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( 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/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index 2aee28c343..6c1ad9a55c 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -10,8 +10,6 @@ from typing import Any, Callable, ClassVar, Optional, TypeVar from uuid import UUID, uuid4 -from jaclang.runtimelib.utils import collect_node_connections - logger = getLogger(__name__) TARCH = TypeVar("TARCH", bound="Architype") @@ -76,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__ @@ -303,41 +179,6 @@ class NodeAnchor(Anchor): architype: NodeArchitype edges: list[EdgeAnchor] - 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 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__() @@ -357,16 +198,6 @@ class EdgeAnchor(Anchor): target: NodeAnchor is_undirected: bool - 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): - Jac.detach(self) - jctx.mem.remove(self.id) - def __getstate__(self) -> dict[str, object]: """Serialize Node Anchor.""" state = super().__getstate__() @@ -424,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/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)); }