From 7473f350d14039355b31dd49b91c745c85f4a50a Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Mon, 2 Dec 2024 14:25:28 +0800 Subject: [PATCH] [FEATURE]: JID (JacLang) --- jac/jaclang/cli/cli.py | 8 +- jac/jaclang/plugin/builtin.py | 4 +- jac/jaclang/plugin/default.py | 112 ++++--- jac/jaclang/plugin/feature.py | 15 +- jac/jaclang/plugin/spec.py | 10 +- .../tests/fixtures/other_root_access.jac | 17 +- jac/jaclang/plugin/tests/test_jaseci.py | 22 +- jac/jaclang/runtimelib/architype.py | 296 +++++++++--------- jac/jaclang/runtimelib/constructs.py | 6 + jac/jaclang/runtimelib/context.py | 10 +- jac/jaclang/runtimelib/memory.py | 96 +++--- jac/jaclang/runtimelib/utils.py | 21 +- jac/jaclang/tests/fixtures/edge_ops.jac | 2 +- jac/jaclang/tests/test_language.py | 2 +- 14 files changed, 328 insertions(+), 293 deletions(-) diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index a16c288443..af28f7135c 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -21,7 +21,7 @@ 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.constructs import Anchor, WalkerArchitype from jaclang.runtimelib.context import ExecutionContext from jaclang.runtimelib.machine import JacMachine, JacProgram from jaclang.utils.helpers import debugger as db @@ -129,7 +129,7 @@ def run( @cmd_registry.register def get_object( filename: str, id: str, session: str = "", main: bool = True, cache: bool = True -) -> dict: +) -> Anchor | None: """Get the object with the specified id.""" if session == "": session = ( @@ -169,10 +169,10 @@ def get_object( JacMachine.detach() raise ValueError("Not a valid file!\nOnly supports `.jac` and `.jir`") - data = {} + data = None obj = Jac.get_object(id) if obj: - data = obj.__jac__.__getstate__() + data = obj.__jac__ else: print(f"Object with id {id} not found.", file=sys.stderr) diff --git a/jac/jaclang/plugin/builtin.py b/jac/jaclang/plugin/builtin.py index aaeb3fdffe..af4a516d65 100644 --- a/jac/jaclang/plugin/builtin.py +++ b/jac/jaclang/plugin/builtin.py @@ -5,7 +5,7 @@ from typing import Optional from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.constructs import Architype, NodeArchitype +from jaclang.runtimelib.constructs import Architype, JID, NodeArchitype def dotgen( @@ -41,6 +41,6 @@ def dotgen( ) -def jid(obj: Architype) -> str: +def jid(obj: Architype) -> JID: """Get the id of the object.""" return Jac.object_ref(obj) diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 76fa0df859..3a4569331b 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -12,7 +12,6 @@ from functools import wraps from logging import getLogger from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union, cast -from uuid import UUID from jaclang.compiler.constant import colors from jaclang.compiler.semtable import SemInfo, SemRegistry, SemScope @@ -25,6 +24,7 @@ EdgeArchitype, EdgeDir, ExecutionContext, + JID, JacFeature as Jac, NodeAnchor, NodeArchitype, @@ -37,6 +37,7 @@ ) from jaclang.runtimelib.constructs import ( GenericEdge, + JacLangJID, JacTestCheck, ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter @@ -55,11 +56,16 @@ class JacCallableImplementation: """Callable Implementations.""" @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(id: str | JID) -> Architype | None: """Get object by id.""" + jctx = Jac.get_context() if id == "root": - return Jac.get_context().root.architype - elif obj := Jac.get_context().mem.find_by_id(UUID(id)): + return jctx.root.architype + + if isinstance(id, str): + id = JacLangJID(id) + + if obj := id.anchor: return obj.architype return None @@ -78,26 +84,25 @@ def elevate_root() -> None: @staticmethod @hookimpl def allow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Allow all access from target root graph to current Architype.""" level = AccessLevel.cast(level) access = architype.__jac__.access.roots - _root_id = str(root_id) - if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): - access.anchors[_root_id] = level + if level != access.anchors.get(root_id, AccessLevel.NO_ACCESS): + access.anchors[root_id] = level @staticmethod @hookimpl def disallow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Disallow all access from target root graph to current Architype.""" level = AccessLevel.cast(level) access = architype.__jac__.access.roots - access.anchors.pop(str(root_id), None) + access.anchors.pop(root_id, None) @staticmethod @hookimpl @@ -160,7 +165,7 @@ def check_access_level(to: Anchor) -> AccessLevel: # 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 @@ -171,17 +176,17 @@ 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 (to_root := 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)) + level = to_root.access.roots.check(jroot.jid) 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)) + level = to_access.roots.check(jroot.jid) if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: access_level = level @@ -226,13 +231,12 @@ def get_edges( ) -> list[EdgeArchitype]: """Get edges connected to this node.""" ret_edges: list[EdgeArchitype] = [] - for anchor in node.edges: + for jid in node.edges: if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -260,13 +264,12 @@ def edges_to_nodes( ) -> list[NodeArchitype]: """Get set of nodes connected to this node.""" ret_edges: list[NodeArchitype] = [] - for anchor in node.edges: + for jid in node.edges: if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -289,7 +292,7 @@ def edges_to_nodes( 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: + if ed == edge.jid: node.edges.pop(idx) break @@ -301,8 +304,10 @@ class JacEdgeImpl: @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) + if source := edge.source.anchor: + Jac.remove_edge(node=source, edge=edge) + if target := edge.target.anchor: + Jac.remove_edge(node=target, edge=edge) class JacWalkerImpl: @@ -332,7 +337,7 @@ def visit_node( if isinstance(anchor, NodeAnchor): wanch.next.append(anchor) elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: + if target := anchor.target.anchor: wanch.next.append(target) else: raise ValueError("Edge has no target.") @@ -363,7 +368,7 @@ def ignore( if isinstance(anchor, NodeAnchor): wanch.ignores.append(anchor) elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: + if target := anchor.target.anchor: wanch.ignores.append(target) else: raise ValueError("Edge has no target.") @@ -380,8 +385,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op1.__jac__ if isinstance(op2, NodeArchitype): node = op2.__jac__ - elif isinstance(op2, EdgeArchitype): - node = op2.__jac__.target + elif isinstance(op2, EdgeArchitype) and ( + target := op2.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") elif isinstance(op2, WalkerArchitype): @@ -389,8 +396,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op2.__jac__ if isinstance(op1, NodeArchitype): node = op1.__jac__ - elif isinstance(op1, EdgeArchitype): - node = op1.__jac__.target + elif isinstance(op1, EdgeArchitype) and ( + target := op1.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") else: @@ -602,10 +611,10 @@ def reset_graph(root: Optional[Root] = None) -> int: if isinstance(anchors := mem.__shelf__, Shelf) else mem.__mem__.values() ): - if anchor == ranchor or anchor.root != ranchor.id: + if anchor == ranchor or anchor.root != ranchor.jid: continue - if loaded_anchor := mem.find_by_id(anchor.id): + if loaded_anchor := mem.find_by_id(anchor.jid): deleted_count += 1 Jac.destroy(loaded_anchor) @@ -613,15 +622,15 @@ def reset_graph(root: Optional[Root] = None) -> int: @staticmethod @hookimpl - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object by id func.""" return JacCallableImplementation.get_object @staticmethod @hookimpl - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object's id.""" - return obj.__jac__.id.hex + return obj.__jac__.jid @staticmethod @hookimpl @@ -988,13 +997,12 @@ def disconnect( for i in left: node = i.__jac__ - for anchor in set(node.edges): + for jid in set(node.edges): if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -1012,7 +1020,6 @@ def disconnect( ): Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True - return disconnect_occurred @staticmethod @@ -1054,12 +1061,12 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: eanch = edge.__jac__ = EdgeAnchor( architype=edge, - source=source, - target=target, + source=source.jid, + target=target.jid, is_undirected=is_undirected, ) - source.edges.append(eanch) - target.edges.append(eanch) + source.edges.append(eanch.jid) + target.edges.append(eanch.jid) if conn_assign: for fld, val in zip(conn_assign[0], conn_assign[1]): @@ -1084,9 +1091,9 @@ def save(obj: Architype | Anchor) -> None: jctx = Jac.get_context() anchor.persistent = True - anchor.root = jctx.root.id + anchor.root = jctx.root.jid - jctx.mem.set(anchor.id, anchor) + jctx.mem.set(anchor.jid, anchor) @staticmethod @hookimpl @@ -1098,13 +1105,14 @@ def destroy(obj: Architype | Anchor) -> None: match anchor: case NodeAnchor(): for edge in anchor.edges: - Jac.destroy(edge) + if eanch := edge.anchor: + Jac.destroy(eanch) case EdgeAnchor(): Jac.detach(anchor) case _: pass - Jac.get_context().mem.remove(anchor.id) + Jac.get_context().mem.remove(anchor.jid) @staticmethod @hookimpl diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index 808b254ae9..2dc6fe9cd9 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, @@ -36,6 +36,7 @@ ast, plugin_manager, ) +from jaclang.runtimelib.constructs import ObjectArchitype class JacAccessValidation: @@ -49,7 +50,7 @@ def elevate_root() -> None: @staticmethod def allow_root( architype: Architype, - root_id: UUID, + root_id: JID, level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Allow all access from target root graph to current Architype.""" @@ -60,7 +61,7 @@ def allow_root( @staticmethod def disallow_root( architype: Architype, - root_id: UUID, + root_id: JID, level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Disallow all access from target root graph to current Architype.""" @@ -196,7 +197,7 @@ class JacClassReferences: EdgeDir: ClassVar[TypeAlias] = EdgeDir DSFunc: ClassVar[TypeAlias] = DSFunc RootType: ClassVar[TypeAlias] = Root - Obj: ClassVar[TypeAlias] = Architype + Obj: ClassVar[TypeAlias] = ObjectArchitype Node: ClassVar[TypeAlias] = NodeArchitype Edge: ClassVar[TypeAlias] = EdgeArchitype Walker: ClassVar[TypeAlias] = WalkerArchitype @@ -265,17 +266,17 @@ def reset_graph(root: Optional[Root] = None) -> int: return plugin_manager.hook.reset_graph(root=root) @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(id: str | JID) -> Architype | None: """Get object given id.""" return plugin_manager.hook.get_object_func()(id=id) @staticmethod - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object given id.""" return plugin_manager.hook.get_object_func() @staticmethod - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object reference id.""" return plugin_manager.hook.object_ref(obj=obj) diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index bc1bf01259..84bf979aca 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -15,7 +15,6 @@ TypeVar, Union, ) -from uuid import UUID from jaclang.compiler import absyntree as ast from jaclang.compiler.constant import EdgeDir @@ -27,6 +26,7 @@ DSFunc, EdgeAnchor, EdgeArchitype, + JID, NodeAnchor, NodeArchitype, Root, @@ -55,7 +55,7 @@ def elevate_root() -> None: @staticmethod @hookspec(firstresult=True) def allow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Allow all access from target root graph to current Architype.""" raise NotImplementedError @@ -63,7 +63,7 @@ def allow_root( @staticmethod @hookspec(firstresult=True) def disallow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Disallow all access from target root graph to current Architype.""" raise NotImplementedError @@ -258,13 +258,13 @@ def reset_graph(root: Optional[Root]) -> int: @staticmethod @hookspec(firstresult=True) - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object by id func.""" raise NotImplementedError @staticmethod @hookspec(firstresult=True) - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object's id.""" raise NotImplementedError diff --git a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac index 9b971f6622..be46386d97 100644 --- a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac +++ b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac @@ -1,5 +1,4 @@ -import:py from jaclang.runtimelib.architype {Anchor} -import:py from uuid {UUID} +import:py from jaclang.runtimelib.architype {JacLangJID} node A { has val: int; @@ -48,15 +47,15 @@ walker create_node { can enter with `root entry { a = A(val=self.val); here ++> a; - print(a.__jac__.id); + print(jid(a)); } } walker create_other_root { can enter with `root entry { - other_root = `root().__jac__; + other_root = `root(); Jac.save(other_root); - print(other_root.id); + print(jid(other_root)); } } @@ -67,7 +66,7 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - Jac.allow_root(here, UUID(self.root_id), self.level); + Jac.allow_root(here, JacLangJID(self.root_id), self.level); } } @@ -75,7 +74,7 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - Jac.allow_root(here, UUID(self.root_id), self.level); + Jac.allow_root(here, JacLangJID(self.root_id), self.level); } } } @@ -87,7 +86,7 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - Jac.disallow_root(here, UUID(self.root_id)); + Jac.disallow_root(here, JacLangJID(self.root_id)); } } @@ -95,7 +94,7 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - Jac.disallow_root(here, UUID(self.root_id)); + Jac.disallow_root(here, JacLangJID(self.root_id)); } } } \ No newline at end of file diff --git a/jac/jaclang/plugin/tests/test_jaseci.py b/jac/jaclang/plugin/tests/test_jaseci.py index a66a3c2472..ba9d3ea430 100644 --- a/jac/jaclang/plugin/tests/test_jaseci.py +++ b/jac/jaclang/plugin/tests/test_jaseci.py @@ -78,7 +78,7 @@ def test_entrypoint_root(self) -> None: session=session, entrypoint="traverse", args=[], - node=str(obj["id"]), + node=str(obj.jid), ) output = self.capturedOutput.getvalue().strip() self.assertEqual(output, "node a\nnode b") @@ -100,12 +100,12 @@ def test_entrypoint_non_root(self) -> None: ) edge_obj = cli.get_object( filename=self.fixture_abs_path("simple_persistent.jac"), - id=obj["edges"][0].id.hex, + id=str(obj.edges[0]), session=session, ) a_obj = cli.get_object( filename=self.fixture_abs_path("simple_persistent.jac"), - id=edge_obj["target"].id.hex, + id=str(edge_obj.target), session=session, ) self._output2buffer() @@ -113,7 +113,7 @@ def test_entrypoint_non_root(self) -> None: filename=self.fixture_abs_path("simple_persistent.jac"), session=session, entrypoint="traverse", - node=str(a_obj["id"]), + node=str(a_obj.jid), args=[], ) output = self.capturedOutput.getvalue().strip() @@ -132,28 +132,26 @@ def test_get_edge(self) -> None: session=session, id="root", ) - self.assertEqual(len(obj["edges"]), 2) + self.assertEqual(len(obj.edges), 2) edge_objs = [ cli.get_object( filename=self.fixture_abs_path("simple_node_connection.jac"), session=session, - id=e.id.hex, + id=str(e), ) - for e in obj["edges"] + for e in obj.edges ] - node_ids = [obj["target"].id.hex for obj in edge_objs] + node_ids = [str(obj.target) for obj in edge_objs] node_objs = [ cli.get_object( filename=self.fixture_abs_path("simple_node_connection.jac"), session=session, - id=str(n_id), + id=n_id, ) for n_id in node_ids ] self.assertEqual(len(node_objs), 2) - self.assertEqual( - {obj["architype"].tag for obj in node_objs}, {"first", "second"} - ) + self.assertEqual({obj.architype.tag for obj in node_objs}, {"first", "second"}) self._del_session(session) def test_filter_on_edge_get_edge(self) -> None: diff --git a/jac/jaclang/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index 382bd41945..222403f92e 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -3,18 +3,109 @@ from __future__ import annotations import inspect -from dataclasses import asdict, dataclass, field, fields, is_dataclass +from dataclasses import asdict, dataclass, field, is_dataclass from enum import IntEnum +from functools import cached_property from logging import getLogger -from pickle import dumps +from re import IGNORECASE, compile from types import UnionType -from typing import Any, Callable, ClassVar, Optional, TypeVar +from typing import Any, Callable, ClassVar, Generic, Optional, Type, TypeVar from uuid import UUID, uuid4 logger = getLogger(__name__) -TARCH = TypeVar("TARCH", bound="Architype") -TANCH = TypeVar("TANCH", bound="Anchor") + +JID_REGEX = compile( + r"^(n|e|w|o):([^:]*):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", + IGNORECASE, +) +_ANCHOR = TypeVar("_ANCHOR", bound="Anchor") + + +@dataclass(kw_only=True) +class JID(Generic[_ANCHOR]): + """Jaclang ID Interface.""" + + id: Any + type: Type[_ANCHOR] + name: str + + @cached_property + def __cached_repr__(self) -> str: + """Cached string representation.""" + return f"{self.type.__name__[:1].lower()}:{self.name}:{self.id}" + + @cached_property + def anchor(self) -> _ANCHOR | None: + """Get architype.""" + from jaclang.plugin.feature import JacFeature + + return JacFeature.get_context().mem.find_by_id(self) + + def __repr__(self) -> str: + """Override string representation.""" + return self.__cached_repr__ + + def __str__(self) -> str: + """Override string parsing.""" + return self.__cached_repr__ + + def __hash__(self) -> int: + """Return default hasher.""" + return hash(self.__cached_repr__) + + +@dataclass(kw_only=True) +class JacLangJID(JID[_ANCHOR]): + """Jaclang ID Implementation.""" + + def __init__( + self, + 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) + # currently no way to base hinting on string regex! + match matched.group(1).lower(): + case "n": + self.type = NodeAnchor # type: ignore [assignment] + case "e": + self.type = EdgeAnchor # type: ignore [assignment] + case "w": + self.type = WalkerAnchor # type: ignore [assignment] + case _: + self.type = ObjectAnchor # type: ignore [assignment] + 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 __getstate__(self) -> str: + """Override getstate.""" + return self.__cached_repr__ + + def __setstate__(self, state: str) -> None: + """Override setstate.""" + self.__init__(state) # type: ignore[misc] + + def __hash__(self) -> int: + """Return default hasher.""" + return hash(self.__cached_repr__) class AccessLevel(IntEnum): @@ -41,11 +132,11 @@ def cast(val: int | str | AccessLevel) -> AccessLevel: class Access: """Access Structure.""" - anchors: dict[str, AccessLevel] = field(default_factory=dict) + anchors: dict[JID, AccessLevel] = field(default_factory=dict) - def check(self, anchor: str) -> AccessLevel: + def check(self, id: JID) -> AccessLevel: """Validate access.""" - return self.anchors.get(anchor, AccessLevel.NO_ACCESS) + return self.anchors.get(id, AccessLevel.NO_ACCESS) @dataclass @@ -60,100 +151,41 @@ class Permission: class AnchorReport: """Report Handler.""" - id: str + id: JID context: dict[str, Any] -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class Anchor: """Object Anchor.""" architype: Architype - id: UUID = field(default_factory=uuid4) - root: Optional[UUID] = None + id: Any = field(default_factory=uuid4) + root: Optional[JID[NodeAnchor]] = None access: Permission = field(default_factory=Permission) persistent: bool = False hash: int = 0 - def is_populated(self) -> bool: - """Check if state.""" - return "architype" in self.__dict__ - - def make_stub(self: TANCH) -> TANCH: - """Return unsynced copy of anchor.""" - if self.is_populated(): - unloaded = object.__new__(self.__class__) - unloaded.id = self.id - return unloaded - return self - - def populate(self) -> None: - """Retrieve the Architype from db and return.""" - from jaclang.plugin.feature import JacFeature as Jac - - jsrc = Jac.get_context().mem - - if anchor := jsrc.find_by_id(self.id): - self.__dict__.update(anchor.__dict__) - - def __getattr__(self, name: str) -> object: - """Trigger load if detects unloaded state.""" - if not self.is_populated(): - self.populate() - - if not self.is_populated(): - raise ValueError( - f"{self.__class__.__name__} [{self.id}] is not a valid reference!" - ) - - return getattr(self, name) - - raise AttributeError( - f"'{self.__class__.__name__}' object has not attribute '{name}'" + @cached_property + def jid(self: _ANCHOR) -> JID[_ANCHOR]: + """Get JID representation.""" + jid = JacLangJID[_ANCHOR]( + self.id, + self.__class__, + ( + "" + if isinstance(self.architype, (GenericEdge, Root)) + else self.architype.__class__.__name__ + ), ) + jid.anchor = self - def __getstate__(self) -> dict[str, Any]: # NOTE: May be better type hinting - """Serialize Anchor.""" - if self.is_populated(): - unlinked = object.__new__(self.architype.__class__) - unlinked.__dict__.update(self.architype.__dict__) - unlinked.__dict__.pop("__jac__", None) - - return { - "id": self.id, - "architype": unlinked, - "root": self.root, - "access": self.access, - "persistent": self.persistent, - } - else: - return {"id": self.id} - - def __setstate__(self, state: dict[str, Any]) -> None: - """Deserialize Anchor.""" - self.__dict__.update(state) - - if self.is_populated() and self.architype: - self.architype.__jac__ = self - self.hash = hash(dumps(self)) - - def __repr__(self) -> str: - """Override representation.""" - if self.is_populated(): - attrs = "" - for f in fields(self): - if f.name in self.__dict__: - attrs += f"{f.name}={self.__dict__[f.name]}, " - attrs = attrs[:-2] - else: - attrs = f"id={self.id}" - - return f"{self.__class__.__name__}({attrs})" + return jid def report(self) -> AnchorReport: """Report Anchor.""" return AnchorReport( - id=self.id.hex, + id=self.jid, context=( asdict(self.architype) if is_dataclass(self.architype) and not isinstance(self.architype, type) @@ -163,59 +195,34 @@ def report(self) -> AnchorReport: def __hash__(self) -> int: """Override hash for anchor.""" - return hash(self.id) - - def __eq__(self, other: object) -> bool: - """Override equal implementation.""" - if isinstance(other, Anchor): - return self.__class__ is other.__class__ and self.id == other.id + return hash(self.jid) + def __eq__(self, value: object) -> bool: + """Override __eq__.""" + if isinstance(value, Anchor): + return value.jid == self.jid return False -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class NodeAnchor(Anchor): """Node Anchor.""" architype: NodeArchitype - edges: list[EdgeAnchor] - - def __getstate__(self) -> dict[str, object]: - """Serialize Node Anchor.""" - state = super().__getstate__() - - if self.is_populated(): - state["edges"] = [edge.make_stub() for edge in self.edges] + edges: list[JID["EdgeAnchor"]] - return state - -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class EdgeAnchor(Anchor): """Edge Anchor.""" architype: EdgeArchitype - source: NodeAnchor - target: NodeAnchor + source: JID["NodeAnchor"] + target: JID["NodeAnchor"] is_undirected: bool - def __getstate__(self) -> dict[str, object]: - """Serialize Node Anchor.""" - state = super().__getstate__() - - if self.is_populated(): - state.update( - { - "source": self.source.make_stub(), - "target": self.target.make_stub(), - "is_undirected": self.is_undirected, - } - ) - - return state - -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class WalkerAnchor(Anchor): """Walker Anchor.""" @@ -226,9 +233,9 @@ class WalkerAnchor(Anchor): disengaged: bool = False -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class ObjectAnchor(Anchor): - """Edge Anchor.""" + """Object Anchor.""" architype: ObjectArchitype @@ -239,23 +246,23 @@ class Architype: _jac_entry_funcs_: ClassVar[list[DSFunc]] _jac_exit_funcs_: ClassVar[list[DSFunc]] - def __init__(self) -> None: - """Create default architype.""" - self.__jac__ = Anchor(architype=self) - def __repr__(self) -> str: """Override repr for architype.""" return f"{self.__class__.__name__}" + @cached_property + def __jac__(self) -> Anchor: + """Build anchor reference.""" + return Anchor(architype=self) + class NodeArchitype(Architype): """Node Architype Protocol.""" - __jac__: NodeAnchor - - def __init__(self) -> None: - """Create node architype.""" - self.__jac__ = NodeAnchor(architype=self, edges=[]) + @cached_property + def __jac__(self) -> NodeAnchor: + """Build anchor reference.""" + return NodeAnchor(architype=self, edges=[]) class EdgeArchitype(Architype): @@ -267,21 +274,19 @@ class EdgeArchitype(Architype): class WalkerArchitype(Architype): """Walker Architype Protocol.""" - __jac__: WalkerAnchor - - def __init__(self) -> None: - """Create walker architype.""" - self.__jac__ = WalkerAnchor(architype=self) + @cached_property + def __jac__(self) -> WalkerAnchor: + """Build anchor reference.""" + return WalkerAnchor(architype=self) class ObjectArchitype(Architype): """Walker Architype Protocol.""" - __jac__: ObjectAnchor - - def __init__(self) -> None: - """Create walker architype.""" - self.__jac__ = ObjectAnchor(architype=self) + @cached_property + def __jac__(self) -> ObjectAnchor: + """Build anchor reference.""" + return ObjectAnchor(architype=self) @dataclass(eq=False) @@ -299,9 +304,10 @@ class Root(NodeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __init__(self) -> None: - """Create root node.""" - self.__jac__ = NodeAnchor(architype=self, persistent=True, edges=[]) + @cached_property + def __jac__(self) -> NodeAnchor: + """Build anchor reference.""" + return NodeAnchor(architype=self, persistent=True, edges=[]) @dataclass(eq=False) diff --git a/jac/jaclang/runtimelib/constructs.py b/jac/jaclang/runtimelib/constructs.py index b39b0abf0d..555af339f0 100644 --- a/jac/jaclang/runtimelib/constructs.py +++ b/jac/jaclang/runtimelib/constructs.py @@ -11,8 +11,11 @@ EdgeAnchor, EdgeArchitype, GenericEdge, + JID, + JacLangJID, NodeAnchor, NodeArchitype, + ObjectArchitype, Root, WalkerAnchor, WalkerArchitype, @@ -31,7 +34,10 @@ "NodeArchitype", "EdgeArchitype", "WalkerArchitype", + "ObjectArchitype", "GenericEdge", + "JID", + "JacLangJID", "Root", "DSFunc", "Memory", diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index aa07b6193a..a65e4285f2 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -8,7 +8,7 @@ from typing import Any, Callable, Optional, cast from uuid import UUID -from .architype import NodeAnchor, Root +from .architype import JacLangJID, NodeAnchor, Root from .memory import Memory, ShelfStorage @@ -39,7 +39,9 @@ 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(JacLangJID(anchor_id)), NodeAnchor + ): return anchor raise ValueError(f"Invalid anchor id {anchor_id} !") return default @@ -65,11 +67,11 @@ 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_ANCHOR.jid), NodeAnchor ): system_root = Root().__jac__ system_root.id = SUPER_ROOT_UUID - ctx.mem.set(system_root.id, system_root) + ctx.mem.set(system_root.jid, system_root) ctx.system_root = system_root diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 07fb2bea31..72b198fab5 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -5,35 +5,32 @@ from dataclasses import dataclass, field 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 -from .architype import Anchor, NodeAnchor, Root, TANCH - -ID = TypeVar("ID") +from .architype import Anchor, JID, NodeAnchor, Root, _ANCHOR @dataclass -class Memory(Generic[ID, TANCH]): +class Memory: """Generic Memory Handler.""" - __mem__: dict[ID, TANCH] = field(default_factory=dict) - __gc__: set[TANCH] = field(default_factory=set) + __mem__: dict[JID, Anchor] = field(default_factory=dict) + __gc__: set[JID] = field(default_factory=set) def close(self) -> None: """Close memory handler.""" self.__mem__.clear() self.__gc__.clear() - def is_cached(self, id: ID) -> bool: + def is_cached(self, id: JID) -> bool: """Check if id if already cached.""" return id in self.__mem__ def find( self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> Generator[TANCH, None, None]: + ids: JID[_ANCHOR] | Iterable[JID[_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] @@ -41,37 +38,43 @@ def find( return ( anchor for id in ids - if (anchor := self.__mem__.get(id)) and (not filter or filter(anchor)) + if (anchor := self.__mem__.get(id)) + and isinstance(anchor, id.type) + and (not filter or filter(anchor)) ) def find_one( self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> TANCH | None: + 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: ID) -> TANCH | None: + def find_by_id(self, id: JID[_ANCHOR]) -> _ANCHOR | None: """Find one by id.""" - return self.__mem__.get(id) + return ( + anchor + if (anchor := self.__mem__.get(id)) and isinstance(anchor, id.type) + else None + ) - def set(self, id: ID, data: TANCH) -> None: + def set(self, id: JID[_ANCHOR], data: _ANCHOR) -> None: """Save anchor to memory.""" self.__mem__[id] = data - def remove(self, ids: ID | Iterable[ID]) -> None: + def remove(self, ids: JID | Iterable[JID]) -> 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) + self.__mem__.pop(id, None) + self.__gc__.add(id) @dataclass -class ShelfStorage(Memory[UUID, Anchor]): +class ShelfStorage(Memory): """Shelf Handler.""" __shelf__: Shelf[Anchor] | None = None @@ -84,9 +87,9 @@ def __init__(self, session: str | None = None) -> None: def close(self) -> None: """Close memory handler.""" if isinstance(self.__shelf__, Shelf): - 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) keys = set(self.__mem__.keys()) @@ -99,7 +102,7 @@ def close(self) -> None: self.__shelf__.close() super().close() - def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: + def sync_mem_to_db(self, keys: Iterable[JID]) -> None: """Manually sync memory to db.""" from jaclang.plugin.feature import JacFeature as Jac @@ -110,7 +113,7 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: and d.persistent and d.hash != hash(dumps(d)) ): - _id = str(d.id) + _id = str(d.jid) if p_d := self.__shelf__.get(_id): if ( isinstance(p_d, NodeAnchor) @@ -123,11 +126,15 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: continue p_d.edges = d.edges - if Jac.check_write_access(d): - if hash(dumps(p_d.access)) != hash(dumps(d.access)): - p_d.access = d.access - if hash(dumps(p_d.architype)) != hash(dumps(d.architype)): - p_d.architype = d.architype + if hash(dumps(p_d.architype)) != hash( + dumps(d.architype) + ) and Jac.check_write_access(d): + p_d.architype = d.architype + + if hash(dumps(p_d.access)) != hash( + dumps(d.access) + ) and Jac.check_write_access(d): + p_d.access = d.access self.__shelf__[_id] = p_d elif not ( @@ -139,9 +146,9 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: def find( self, - ids: UUID | Iterable[UUID], - 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] @@ -155,21 +162,28 @@ def find( and id not in self.__gc__ and (_anchor := self.__shelf__.get(str(id))) ): - self.__mem__[id] = anchor = _anchor - if anchor and (not filter or filter(anchor)): + anchor = self.__mem__[id] = _anchor + anchor.architype.__jac__ = anchor + if ( + anchor + and isinstance(anchor, id.type) + and (not filter or filter(anchor)) + ): yield anchor 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) if ( not data and isinstance(self.__shelf__, Shelf) - and (data := self.__shelf__.get(str(id))) + and (_data := self.__shelf__.get(str(id))) + and isinstance(_data, id.type) ): - self.__mem__[id] = data + data = self.__mem__[id] = _data + data.architype.__jac__ = data return data diff --git a/jac/jaclang/runtimelib/utils.py b/jac/jaclang/runtimelib/utils.py index 83e35ab2cf..fb907b4e05 100644 --- a/jac/jaclang/runtimelib/utils.py +++ b/jac/jaclang/runtimelib/utils.py @@ -37,14 +37,13 @@ def collect_node_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: + for jid in edges: + if (edge := jid.anchor) and (target := edge.target.anchor): connections.add( ( current_node.architype, target.architype, - edge_.__class__.__name__, + edge.__class__.__name__, ) ) collect_node_connections(target, visited_nodes, connections) @@ -66,16 +65,18 @@ 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__ + for jid in node.__jac__.edges: + if not (edge := jid.anchor): + continue + is_self_loop = edge.source == edge.target + is_in_edge = edge.target == node.__jac__.jid if (traverse and is_in_edge) or edge.architype.__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 - ): + elif ( + other_nda := edge.target.anchor if not is_in_edge else edge.source.anchor + ) and (other_nd := other_nda.architype): new_con = ( (node, other_nd, edge.architype) if not is_in_edge diff --git a/jac/jaclang/tests/fixtures/edge_ops.jac b/jac/jaclang/tests/fixtures/edge_ops.jac index 9ace322c16..e024717464 100644 --- a/jac/jaclang/tests/fixtures/edge_ops.jac +++ b/jac/jaclang/tests/fixtures/edge_ops.jac @@ -22,7 +22,7 @@ edge MyEdge { for j=0 to j<3 by j+=1 { end +:MyEdge:val=random.randint(1, 15), val2=random.randint(1, 5):+> node_a(value=j + 10); } - print([(arch.val, arch.val2) for i in end.__jac__.edges if isinstance(arch := i.architype, MyEdge)]); + print([(arch.val, arch.val2) for i in end.__jac__.edges if (anch := i.anchor) and isinstance(arch := anch.architype, MyEdge)]); } } for i=0 to i<3 by i+=1 { diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index 92f3394cc1..a403c9d3be 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -1177,7 +1177,7 @@ def test_object_ref_interface(self) -> None: cli.run(self.fixture_abs_path("objref.jac")) sys.stdout = sys.__stdout__ stdout_value = captured_output.getvalue().split("\n") - self.assertEqual(len(stdout_value[0]), 32) + self.assertEqual(len(stdout_value[0]), 45) self.assertEqual("MyNode(value=0)", stdout_value[1]) self.assertEqual("valid: True", stdout_value[2])