From a27d5540ec47ea4b234a1fa0581a5162004d6814 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Tue, 17 Sep 2024 00:47:07 +0800 Subject: [PATCH] [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)); }