From ba269935bdb254fda450cc045f88b7aac9fdbea0 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Thu, 29 Aug 2024 03:24:50 +0800 Subject: [PATCH] [ENHANCEMENT]: Implement Access Validation --- jaclang/cli/cli.py | 4 +- jaclang/plugin/default.py | 4 + .../tests/fixtures/other_root_access.jac | 82 +++++ jaclang/plugin/tests/test_jaseci.py | 323 +++++++++++++++++- jaclang/runtimelib/architype.py | 196 ++++++++++- jaclang/runtimelib/context.py | 53 ++- jaclang/runtimelib/memory.py | 31 +- 7 files changed, 666 insertions(+), 27 deletions(-) create mode 100644 jaclang/plugin/tests/fixtures/other_root_access.jac diff --git a/jaclang/cli/cli.py b/jaclang/cli/cli.py index e6003c713..e753501c2 100644 --- a/jaclang/cli/cli.py +++ b/jaclang/cli/cli.py @@ -250,7 +250,6 @@ def enter( base, mod = os.path.split(filename) base = base if base else "./" mod = mod[:-4] - if filename.endswith(".jac"): ret_module = jac_import( target=mod, @@ -275,13 +274,14 @@ def enter( jctx = ExecutionContext.create(session=session, root=root, entry=node) + if ret_module: (loaded_mod,) = ret_module if not loaded_mod: print("Errors occurred while importing the module.") else: architype = getattr(loaded_mod, entrypoint)(*args) - if isinstance(architype, WalkerArchitype): + if isinstance(architype, WalkerArchitype) and jctx.validate_access(): Jac.spawn_call(jctx.entry.architype, architype) jctx.close() diff --git a/jaclang/plugin/default.py b/jaclang/plugin/default.py index 37cb3190e..b2d7bf324 100644 --- a/jaclang/plugin/default.py +++ b/jaclang/plugin/default.py @@ -483,11 +483,14 @@ def disconnect( (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 target.architype in right + and source.has_write_access(target) ): anchor.destroy() if anchor.persistent else anchor.detach() disconnect_occurred = True @@ -495,6 +498,7 @@ def disconnect( dir in [EdgeDir.IN, EdgeDir.ANY] and node == target and source.architype in right + and target.has_write_access(source) ): anchor.destroy() if anchor.persistent else anchor.detach() disconnect_occurred = True diff --git a/jaclang/plugin/tests/fixtures/other_root_access.jac b/jaclang/plugin/tests/fixtures/other_root_access.jac new file mode 100644 index 000000000..df5192d9e --- /dev/null +++ b/jaclang/plugin/tests/fixtures/other_root_access.jac @@ -0,0 +1,82 @@ +import:py from jaclang.runtimelib.architype {Anchor} +import:py from uuid {UUID} + +node A { + has val: int; +} + +walker check_node { + can enter with `root entry { + visit [-->]; + } + + can enter2 with A entry { + print(here); + } +} + +walker update_node { + has val: int; + + can enter2 with A entry { + here.val = self.val; + } +} + +walker create_node { + has val: int; + + can enter with `root entry { + a = A(val=self.val); + here ++> a; + print(a.__jac__.id); + } +} + +walker create_other_root { + can enter with `root entry { + other_root = `root().__jac__; + other_root.save(); + print(other_root.id); + } +} + +walker allow_other_root_access { + has root_id: str, level: int | str = 1, via_all: bool = False; + + can enter_root with `root entry { + if self.via_all { + here.__jac__.unrestrict(self.level); + } else { + here.__jac__.allow_root(UUID(self.root_id), self.level); + } + } + + can enter_nested with A entry { + if self.via_all { + here.__jac__.unrestrict(self.level); + } else { + here.__jac__.allow_root(UUID(self.root_id), self.level); + } + } +} + +walker disallow_other_root_access { + has root_id: str, via_all: bool = False; + + can enter_root with `root entry { + if self.via_all { + here.__jac__.restrict(); + } else { + here.__jac__.disallow_root(UUID(self.root_id)); + } + } + + can enter_nested with A entry { + if self.via_all { + here.__jac__.restrict(); + } else { + here.__jac__.disallow_root(UUID(self.root_id)); + } + } +} \ No newline at end of file diff --git a/jaclang/plugin/tests/test_jaseci.py b/jaclang/plugin/tests/test_jaseci.py index eae59800e..f36274c59 100644 --- a/jaclang/plugin/tests/test_jaseci.py +++ b/jaclang/plugin/tests/test_jaseci.py @@ -265,6 +265,327 @@ def test_indirect_reference_node(self) -> None: ) self.assertEqual( self.capturedOutput.getvalue().strip(), - "[b(name='node b')]\n[GenericEdge]", + "[b(name='node b')]\n[GenericEdge()]", ) self._del_session(session) + + def trigger_access_validation_test( + self, give_access_to_full_graph: bool, via_all: bool = False + ) -> None: + """Test different access validation.""" + self._output2buffer() + + ############################################## + # ALLOW READ ACCESS # + ############################################## + + node_1 = "" if give_access_to_full_graph else self.nodes[0] + node_2 = "" if give_access_to_full_graph else self.nodes[1] + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="allow_other_root_access", + args=[self.roots[1], 1, via_all], + session=session, + root=self.roots[0], + node=node_1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="allow_other_root_access", + args=[self.roots[0], 1, via_all], + session=session, + root=self.roots[1], + node=node_2, + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="update_node", + args=[20], + session=session, + root=self.roots[0], + node=self.nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="update_node", + args=[10], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[0], + node=self.nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + archs = self.capturedOutput.getvalue().strip().split("\n") + self.assertTrue(len(archs) == 2) + + # --------- NO UPDATE SHOULD HAPPEN -------- # + + self.assertTrue(archs[0], "A(val=2)") + self.assertTrue(archs[1], "A(val=1)") + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="disallow_other_root_access", + args=[self.roots[1], via_all], + session=session, + root=self.roots[0], + node=node_1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="disallow_other_root_access", + args=[self.roots[0], via_all], + session=session, + root=self.roots[1], + node=node_2, + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[0], + node=self.nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + self.assertFalse(self.capturedOutput.getvalue().strip()) + + ############################################## + # ALLOW WRITE ACCESS # + ############################################## + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="allow_other_root_access", + args=[self.roots[1], "WRITE", via_all], + session=session, + root=self.roots[0], + node=node_1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="allow_other_root_access", + args=[self.roots[0], "WRITE", via_all], + session=session, + root=self.roots[1], + node=node_2, + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="update_node", + args=[20], + root=self.roots[0], + node=self.nodes[1], + session=session, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="update_node", + args=[10], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[0], + node=self.nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + archs = self.capturedOutput.getvalue().strip().split("\n") + self.assertTrue(len(archs) == 2) + + # --------- UPDATE SHOULD HAPPEN -------- # + + self.assertTrue(archs[0], "A(val=20)") + self.assertTrue(archs[1], "A(val=10)") + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="disallow_other_root_access", + args=[self.roots[1], via_all], + session=session, + root=self.roots[0], + node=node_1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="disallow_other_root_access", + args=[self.roots[0], via_all], + session=session, + root=self.roots[1], + node=node_2, + ) + + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[0], + node=self.nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=self.roots[1], + node=self.nodes[0], + ) + self.assertFalse(self.capturedOutput.getvalue().strip()) + + def test_other_root_access(self) -> None: + """Test filtering on node, then visit.""" + global session + session = self.fixture_abs_path("other_root_access.session") + + ############################################## + # CREATE ROOTS # + ############################################## + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="create_other_root", + args=[], + session=session, + ) + root1 = self.capturedOutput.getvalue().strip() + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="create_other_root", + args=[], + session=session, + ) + root2 = self.capturedOutput.getvalue().strip() + + ############################################## + # CREATE RESPECTIVE NODES # + ############################################## + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="create_node", + args=[1], + session=session, + root=root1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="create_node", + args=[2], + session=session, + root=root2, + ) + nodes = self.capturedOutput.getvalue().strip().split("\n") + self.assertTrue(len(nodes) == 2) + + ############################################## + # VISIT RESPECTIVE NODES # + ############################################## + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=root1, + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=root2, + ) + archs = self.capturedOutput.getvalue().strip().split("\n") + self.assertEqual(2, len(archs)) + self.assertTrue(archs[0], "A(val=1)") + self.assertTrue(archs[1], "A(val=2)") + + ############################################## + # SWAP TARGET NODE # + # NO ACCESS # + ############################################## + + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=root1, + node=nodes[1], + ) + cli.enter( + filename=self.fixture_abs_path("other_root_access.jac"), + entrypoint="check_node", + args=[], + session=session, + root=root2, + node=nodes[0], + ) + self.assertFalse(self.capturedOutput.getvalue().strip()) + + ############################################## + # TEST DIFFERENT ACCESS OPTIONS # + ############################################## + + self.roots = [root1, root2] + self.nodes = nodes + + self.trigger_access_validation_test(give_access_to_full_graph=False) + self.trigger_access_validation_test(give_access_to_full_graph=True) + + self.trigger_access_validation_test( + give_access_to_full_graph=False, via_all=True + ) + self.trigger_access_validation_test( + give_access_to_full_graph=True, via_all=True + ) + + self._del_session(session) diff --git a/jaclang/runtimelib/architype.py b/jaclang/runtimelib/architype.py index e6a1111dc..492cab95a 100644 --- a/jaclang/runtimelib/architype.py +++ b/jaclang/runtimelib/architype.py @@ -3,9 +3,10 @@ from __future__ import annotations from dataclasses import asdict, dataclass, field, fields, is_dataclass +from enum import IntEnum from pickle import dumps from types import UnionType -from typing import Any, Callable, Iterable, Optional, TypeVar +from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar from uuid import UUID, uuid4 from jaclang.compiler.constant import EdgeDir @@ -15,6 +16,52 @@ TANCH = TypeVar("TANCH", bound="Anchor") +class AccessLevel(IntEnum): + """Access level enum.""" + + UNSET = -1 + NO_ACCESS = 0 + READ = 1 + CONNECT = 2 + WRITE = 3 + + @staticmethod + def cast(val: int | str | AccessLevel) -> AccessLevel: + """Cast access level.""" + match val: + case int(): + return AccessLevel(val) + case str(): + return AccessLevel[val] + case _: + return val + + +@dataclass +class Access: + """Access Structure.""" + + whitelist: bool = True # whitelist or blacklist + anchors: dict[str, AccessLevel] = field(default_factory=dict) + + def check( + self, anchor: str + ) -> tuple[bool, AccessLevel]: # whitelist or blacklist, has_read_access, level + """Validate access.""" + if self.whitelist: + return self.whitelist, self.anchors.get(anchor, AccessLevel.NO_ACCESS) + else: + return self.whitelist, self.anchors.get(anchor, AccessLevel.WRITE) + + +@dataclass +class Permission: + """Anchor Access Handler.""" + + all: AccessLevel = AccessLevel.NO_ACCESS + roots: Access = field(default_factory=Access) + + @dataclass class AnchorReport: """Report Handler.""" @@ -29,14 +76,140 @@ class Anchor: architype: Architype id: UUID = field(default_factory=uuid4) + root: Optional[UUID] = None + access: Permission = field(default_factory=Permission) persistent: bool = False hash: int = 0 + current_access_level: AccessLevel = AccessLevel.WRITE + + ########################################################################## + # ACCESS CONTROL # + ########################################################################## + + def whitelist_roots(self, whitelist: bool = True) -> None: + """Toggle root whitelist/blacklist.""" + if whitelist != self.access.roots.whitelist: + self.access.roots.whitelist = whitelist + + 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 + if access.whitelist: + _root_id = str(root_id) + if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): + access.anchors[_root_id] = level + else: + self.disallow_root(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 + if access.whitelist: + access.anchors.pop(str(root_id), None) + else: + self.allow_root(root_id, level) + + 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.""" + return self.access_level(to) > AccessLevel.NO_ACCESS + + def has_connect_access(self, to: Anchor) -> bool: + """Write Access Validation.""" + return self.access_level(to) > AccessLevel.READ + + def has_write_access(self, to: Anchor) -> bool: + """Write Access Validation.""" + return self.access_level(to) > AccessLevel.CONNECT + + def access_level(self, to: Anchor) -> AccessLevel: + """Access validation.""" + if to.current_access_level <= AccessLevel.UNSET: + + from jaclang.plugin.feature import JacFeature as Jac + + jctx = Jac.get_context() + + jroot = jctx.root + to.current_access_level = AccessLevel.NO_ACCESS + + # 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: + to.current_access_level = AccessLevel.WRITE + return to.current_access_level + + # if target anchor have set access.all + if (to_access := to.access).all > AccessLevel.NO_ACCESS: + to.current_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.datasource.find_one(to.root), Anchor + ): + if to_root.access.all > to.current_access_level: + to.current_access_level = to_root.access.all + + whitelist, level = to_root.access.roots.check(str(jroot.id)) + if not whitelist: + if level < AccessLevel.READ: + to.current_access_level = AccessLevel.NO_ACCESS + return to.current_access_level + elif level < to.current_access_level: + level = to.current_access_level + elif ( + whitelist + and level > AccessLevel.NO_ACCESS + and to.current_access_level == AccessLevel.NO_ACCESS + ): + to.current_access_level = level + + # if target anchor have set allowed roots + # if current root is allowed to target anchor + whitelist, level = to_access.roots.check(str(jroot.id)) + if not whitelist: + if level < AccessLevel.READ: + to.current_access_level = AccessLevel.NO_ACCESS + return to.current_access_level + elif level < to.current_access_level: + level = to.current_access_level + elif ( + whitelist + and level > AccessLevel.NO_ACCESS + and to.current_access_level == AccessLevel.NO_ACCESS + ): + to.current_access_level = level + + return to.current_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 Jac.get_context().mem.set(self.id, self) def destroy(self) -> None: @@ -92,6 +265,8 @@ def __getstate__(self) -> dict[str, Any]: # NOTE: May be better type hinting return { "id": self.id, "architype": unlinked, + "root": self.root, + "access": self.access, "persistent": self.persistent, } else: @@ -100,6 +275,7 @@ def __getstate__(self) -> dict[str, Any]: # NOTE: May be better type hinting def __setstate__(self, state: dict[str, Any]) -> None: """Deserialize Anchor.""" self.__dict__.update(state) + self.current_access_level = AccessLevel.UNSET if self.is_populated() and self.architype: self.architype.__jac__ = self @@ -161,17 +337,21 @@ def get_edges( (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 source.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 target.has_read_access(source) ): ret_edges.append(anchor.architype) return ret_edges @@ -189,17 +369,21 @@ def edges_to_nodes( (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 source.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 target.has_read_access(source) ): ret_edges.append(source.architype) return ret_edges @@ -441,18 +625,20 @@ def __init__(self) -> None: self.__jac__ = WalkerAnchor(architype=self) +@dataclass(eq=False) class GenericEdge(EdgeArchitype): """Generic Root Node.""" - _jac_entry_funcs_ = [] - _jac_exit_funcs_ = [] + _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] # type: ignore[misc] + _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] # type: ignore[misc] +@dataclass(eq=False) class Root(NodeArchitype): """Generic Root Node.""" - _jac_entry_funcs_ = [] - _jac_exit_funcs_ = [] + _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] # type: ignore[misc] + _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] # type: ignore[misc] def __init__(self) -> None: """Create root node.""" diff --git a/jaclang/runtimelib/context.py b/jaclang/runtimelib/context.py index e24d58c1c..65e5cade8 100644 --- a/jaclang/runtimelib/context.py +++ b/jaclang/runtimelib/context.py @@ -7,16 +7,16 @@ from typing import Any, Callable, Optional, cast from uuid import UUID -from .architype import NodeAnchor, Root +from .architype import AccessLevel, NodeAnchor, Root from .memory import Memory, ShelfStorage EXECUTION_CONTEXT = ContextVar[Optional["ExecutionContext"]]("ExecutionContext") -SUPER_ROOT_UUID = "00000000-0000-0000-0000-000000000000" +SUPER_ROOT_UUID = UUID("00000000-0000-0000-0000-000000000000") SUPER_ROOT_ARCHITYPE = object.__new__(Root) SUPER_ROOT_ANCHOR = NodeAnchor( - id=UUID(SUPER_ROOT_UUID), architype=SUPER_ROOT_ARCHITYPE, persistent=False, edges=[] + id=SUPER_ROOT_UUID, architype=SUPER_ROOT_ARCHITYPE, persistent=False, edges=[] ) SUPER_ROOT_ARCHITYPE.__jac__ = SUPER_ROOT_ANCHOR @@ -28,7 +28,7 @@ class ExecutionContext: reports: list[Any] system_root: NodeAnchor root: NodeAnchor - entry: NodeAnchor + __entry__: NodeAnchor | str | None def generate_system_root(self) -> NodeAnchor: """Generate default system root.""" @@ -40,17 +40,35 @@ def generate_system_root(self) -> NodeAnchor: self.mem.set(anchor.id, anchor) return anchor + @property + def entry(self) -> NodeAnchor: + """Get entry lazy load.""" + match self.__entry__: + case NodeAnchor(): + pass + case str(): + self.__entry__ = self.init_anchor(self.__entry__, self.root) + case _: + self.__entry__ = self.root + return self.__entry__ + def init_anchor( self, anchor_id: str | None, - default: NodeAnchor | Callable[[], NodeAnchor], + default: NodeAnchor, ) -> NodeAnchor: """Load initial anchors.""" - if anchor_id and isinstance( - anchor := self.mem.find_by_id(UUID(anchor_id)), NodeAnchor - ): - return anchor - return default() if callable(default) else default + if anchor_id: + if isinstance( + anchor := self.mem.find_by_id(UUID(anchor_id)), NodeAnchor + ): + return anchor + raise ValueError(f"Invalid anchor id {anchor_id} !") + return default if callable(default) elsedefault + + def validate_access(self) -> bool: + """Validate access.""" + return self.root.has_read_access(self.entry) def close(self) -> None: """Close current ExecutionContext.""" @@ -68,9 +86,20 @@ def create( ctx = ExecutionContext() ctx.mem = ShelfStorage(session) ctx.reports = [] - ctx.system_root = ctx.init_anchor(SUPER_ROOT_UUID, ctx.generate_system_root) + + if not isinstance( + system_root := ctx.datasource.find_by_id(SUPER_ROOT_UUID), NodeAnchor + ): + system_root = Root().__jac__ + system_root.id = SUPER_ROOT_UUID + ctx.datasource.set(system_root.id, system_root) + + ctx.system_root = system_root + ctx.root = ctx.init_anchor(root, ctx.system_root) - ctx.entry = ctx.init_anchor(entry, ctx.root) + ctx.root.current_access_level = AccessLevel.WRITE + + ctx.__entry__ = entry if auto_close and (old_ctx := EXECUTION_CONTEXT.get(None)): old_ctx.close() diff --git a/jaclang/runtimelib/memory.py b/jaclang/runtimelib/memory.py index 321e51a98..6a9330bfc 100644 --- a/jaclang/runtimelib/memory.py +++ b/jaclang/runtimelib/memory.py @@ -8,7 +8,7 @@ from typing import Callable, Generator, Generic, Iterable, TypeVar from uuid import UUID -from .architype import Anchor, NodeAnchor, Root, TANCH +from .architype import AccessLevel, Anchor, NodeAnchor, Root, TANCH ID = TypeVar("ID") @@ -85,16 +85,33 @@ def close(self) -> None: self.__mem__.pop(id, None) for d in self.__mem__.values(): - if d.persistent and d.hash != (new_hash := hash(dumps(d))): - if ( + if d.persistent and d.hash != hash(dumps(d)): + _id = str(d.id) + if p_d := self.__shelf__.get(_id): + if ( + isinstance(p_d, NodeAnchor) + and isinstance(d, NodeAnchor) + and p_d.edges != d.edges + and d.current_access_level >= AccessLevel.CONNECT + ): + if not d.edges: + self.__shelf__.pop(_id, None) + continue + p_d.edges = d.edges + + if d.current_access_level >= AccessLevel.WRITE: + if hash(dumps(p_d.access)) != hash(dumps(d.access)): + p_d.access = d.access + if hash(dumps(d.architype)) != hash(dumps(d.architype)): + p_d.architype = d.architype + + self.__shelf__[_id] = p_d + elif not ( isinstance(d, NodeAnchor) and not isinstance(d.architype, Root) and not d.edges ): - self.__shelf__.pop(str(d.id), None) - else: - d.hash = new_hash - self.__shelf__[str(d.id)] = d + self.__shelf__[_id] = d self.__shelf__.close() super().close()