diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index dae2d2d6f..5eba825bc 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -766,10 +766,10 @@ class WalkerAnchor(BaseAnchor, _WalkerAnchor): # type: ignore[misc] """Walker Anchor.""" architype: "WalkerArchitype" - path: list[Anchor] = field(default_factory=list) - next: list[Anchor] = field(default_factory=list) + path: list[NodeAnchor] = field(default_factory=list) # type: ignore[assignment] + next: list[NodeAnchor] = field(default_factory=list) # type: ignore[assignment] returns: list[Any] = field(default_factory=list) - ignores: list[Anchor] = field(default_factory=list) + ignores: list[NodeAnchor] = field(default_factory=list) # type: ignore[assignment] disengaged: bool = False class Collection(BaseCollection["WalkerAnchor"]): diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 63918b2f9..484d593f8 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -26,6 +26,7 @@ from jaclang.plugin.default import JacFeatureImpl, hookimpl from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.architype import DSFunc +from jaclang.runtimelib.utils import all_issubclass from orjson import loads @@ -718,59 +719,85 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker.path = [] walker.next = [node] walker.returns = [] + current_node = node.architype + + # walker entry + for i in warch._jac_entry_funcs_: + if i.func and not i.trigger: + walker.returns.append(i.func(warch, current_node)) + if walker.disengaged: + return warch - if walker.next: - current_node = walker.next[-1].architype - for i in warch._jac_entry_funcs_: - if not i.trigger: - if i.func: - walker.returns.append(i.func(warch, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") while len(walker.next): if current_node := walker.next.pop(0).architype: + # walker entry with + for i in warch._jac_entry_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, NodeArchitype) + and isinstance(current_node, i.trigger) + ): + walker.returns.append(i.func(warch, current_node)) + if walker.disengaged: + return warch + + # node entry for i in current_node._jac_entry_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - walker.returns.append(i.func(current_node, warch)) - else: - raise ValueError(f"No function {i.name} to call.") + if i.func and not i.trigger: + walker.returns.append(i.func(current_node, warch)) 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 and i.trigger: - walker.returns.append(i.func(warch, current_node)) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") + + # node entry with + for i in current_node._jac_entry_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, WalkerArchitype) + and isinstance(warch, i.trigger) + ): + walker.returns.append(i.func(current_node, warch)) 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 and i.trigger: - walker.returns.append(i.func(warch, current_node)) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") + + # node exit with + for i in current_node._jac_exit_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, WalkerArchitype) + and isinstance(warch, i.trigger) + ): + walker.returns.append(i.func(current_node, warch)) if walker.disengaged: return warch + + # node exit for i in current_node._jac_exit_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - walker.returns.append(i.func(current_node, warch)) - else: - raise ValueError(f"No function {i.name} to call.") + if i.func and not i.trigger: + walker.returns.append(i.func(current_node, warch)) if walker.disengaged: return warch + + # walker exit with + for i in warch._jac_exit_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, NodeArchitype) + and isinstance(current_node, i.trigger) + ): + walker.returns.append(i.func(warch, current_node)) + if walker.disengaged: + return warch + # walker exit for i in warch._jac_exit_funcs_: - if not i.trigger: - if i.func: - walker.returns.append(i.func(warch, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") + if i.func and not i.trigger: + walker.returns.append(i.func(warch, current_node)) + if walker.disengaged: + return warch + walker.ignores = [] return warch diff --git a/jac-cloud/jac_cloud/tests/openapi_specs.json b/jac-cloud/jac_cloud/tests/openapi_specs.json index c0c50f61b..ac50d86a6 100644 --- a/jac-cloud/jac_cloud/tests/openapi_specs.json +++ b/jac-cloud/jac_cloud/tests/openapi_specs.json @@ -4540,6 +4540,78 @@ } } } + }, + "/walker/visit_sequence": { + "post": { + "tags": [ + "walker", + "walker" + ], + "summary": "/visit_sequence", + "operationId": "api_root_walker_visit_sequence_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextResponse_NoneType_" + } + } + } + } + } + } + }, + "/walker/visit_sequence/{node}": { + "post": { + "tags": [ + "walker", + "walker" + ], + "summary": "/visit_sequence/{node}", + "operationId": "api_entry_walker_visit_sequence__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextResponse_NoneType_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index dd8884222..88b96e590 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -505,6 +505,63 @@ walker different_return { can enter6 with `root entry -> list | dict { } + class __specs__ { + has auth: bool = False; + } +} + +walker Walker { + class __specs__ { + has private: bool = True; + } +} + +node Node { + has val: str; + + can entry1 with entry { + return f"{self.val}-2"; + } + + # with 'visit_sequence' (string class reference) is not yet supported + can entry3 with Walker entry { + return f"{self.val}-3"; + } + + # with 'visit_sequence' (string class reference) is not yet supported + can exit1 with Walker exit { + return f"{self.val}-4"; + } + + can exit2 with exit { + return f"{self.val}-5"; + } +} +walker visit_sequence:Walker: { + can entry1 with entry { + return "walker entry"; + } + + can entry2 with `root entry { + here ++> Node(val = "a"); + here ++> Node(val = "b"); + here ++> Node(val = "c"); + visit [-->]; + return "walker enter to root"; + } + + can entry3 with Node entry { + return f"{here.val}-1"; + } + + can exit1 with Node exit { + return f"{here.val}-6"; + } + + can exit2 with exit { + return "walker exit"; + } + class __specs__ { has auth: bool = False; } diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph.py b/jac-cloud/jac_cloud/tests/test_simple_graph.py index c4e0db3b6..a1ef197a8 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -490,7 +490,7 @@ def trigger_access_validation_test( self.post_api(f"visit_nested_node/{nested_node['id']}", expect_error=True), ) - async def nested_count_should_be(self, node: int, edge: int) -> None: + def nested_count_should_be(self, node: int, edge: int) -> None: """Test nested node count.""" self.assertEqual(node, self.q_node.count_documents({"name": "Nested"})) self.assertEqual( @@ -505,7 +505,7 @@ async def nested_count_should_be(self, node: int, edge: int) -> None: ), ) - async def trigger_custom_status_code(self) -> None: + def trigger_custom_status_code(self) -> None: """Test custom status code.""" for acceptable_code in [200, 201, 202, 203, 205, 206, 207, 208, 226]: res = self.post_api("custom_status_code", {"status": acceptable_code}) @@ -581,6 +581,38 @@ async def trigger_custom_status_code(self) -> None: Exception, self.post_api, "custom_status_code", {"status": invalid_code} ) + def trigger_visit_sequence(self) -> None: + """Test visit sequence.""" + res = post(f"{self.host}/walker/visit_sequence").json() + + self.assertEqual(200, res["status"]) + self.assertEqual( + [ + "walker entry", + "walker enter to root", + "a-1", + "a-2", + "a-3", + "a-4", + "a-5", + "a-6", + "b-1", + "b-2", + "b-3", + "b-4", + "b-5", + "b-6", + "c-1", + "c-2", + "c-3", + "c-4", + "c-5", + "c-6", + "walker exit", + ], + res["returns"], + ) + async def test_all_features(self) -> None: """Test Full Features.""" self.trigger_openapi_specs_test() @@ -597,51 +629,51 @@ async def test_all_features(self) -> None: # VIA DETACH # ################################################### - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) self.trigger_create_nested_node_test() - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) self.trigger_update_nested_node_test() self.trigger_detach_nested_node_test() - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) self.trigger_create_nested_node_test(manual=True) - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) self.trigger_update_nested_node_test(manual=True) self.trigger_detach_nested_node_test(manual=True) - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) ################################################### # VIA DESTROY # ################################################### self.trigger_create_nested_node_test() - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) self.trigger_delete_nested_node_test() - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) self.trigger_create_nested_node_test(manual=True) - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) self.trigger_delete_nested_node_test(manual=True) - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) self.trigger_create_nested_node_test() - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) self.trigger_delete_nested_edge_test() - await self.nested_count_should_be(node=0, edge=0) + self.nested_count_should_be(node=0, edge=0) self.trigger_create_nested_node_test(manual=True) - await self.nested_count_should_be(node=1, edge=1) + self.nested_count_should_be(node=1, edge=1) # only automatic cleanup remove nodes that doesn't have edges # manual save still needs to trigger the destroy for that node self.trigger_delete_nested_edge_test(manual=True) - await self.nested_count_should_be(node=1, edge=0) + self.nested_count_should_be(node=1, edge=0) self.trigger_access_validation_test(give_access_to_full_graph=False) self.trigger_access_validation_test(give_access_to_full_graph=True) @@ -657,4 +689,10 @@ async def test_all_features(self) -> None: # CUSTOM STATUS # ################################################### - await self.trigger_custom_status_code() + self.trigger_custom_status_code() + + ################################################### + # VISIT SEQUENCE # + ################################################### + + self.trigger_visit_sequence() diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 9fc1da55e..703f234a9 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -42,7 +42,11 @@ ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram -from jaclang.runtimelib.utils import collect_node_connections, traverse_graph +from jaclang.runtimelib.utils import ( + all_issubclass, + collect_node_connections, + traverse_graph, +) import pluggy @@ -377,58 +381,85 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker.path = [] walker.next = [node] - if walker.next: - current_node = walker.next[-1].architype - for i in warch._jac_entry_funcs_: - if not i.trigger: - if i.func: - i.func(warch, current_node) - else: - raise ValueError(f"No function {i.name} to call.") + current_node = node.architype + + # walker entry + for i in warch._jac_entry_funcs_: + if i.func and not i.trigger: + i.func(warch, current_node) + if walker.disengaged: + return warch + while len(walker.next): if current_node := walker.next.pop(0).architype: + # walker entry with + for i in warch._jac_entry_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, NodeArchitype) + and isinstance(current_node, i.trigger) + ): + i.func(warch, current_node) + if walker.disengaged: + return warch + + # node entry 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 i.func and not i.trigger: + i.func(current_node, warch) 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 and i.trigger: - i.func(warch, current_node) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") + + # node entry with + for i in current_node._jac_entry_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, WalkerArchitype) + and isinstance(warch, i.trigger) + ): + i.func(current_node, warch) 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 and i.trigger: - i.func(warch, current_node) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") + + # node exit with + for i in current_node._jac_exit_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, WalkerArchitype) + and isinstance(warch, i.trigger) + ): + i.func(current_node, warch) if walker.disengaged: return warch + + # node exit 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 i.func and not i.trigger: + i.func(current_node, warch) if walker.disengaged: return warch + + # walker exit with + for i in warch._jac_exit_funcs_: + if ( + i.func + and i.trigger + and all_issubclass(i.trigger, NodeArchitype) + and isinstance(current_node, i.trigger) + ): + i.func(warch, current_node) + if walker.disengaged: + return warch + # walker exit for i in warch._jac_exit_funcs_: - if not i.trigger: - if i.func: - i.func(warch, current_node) - else: - raise ValueError(f"No function {i.name} to call.") + if i.func and not i.trigger: + i.func(warch, current_node) + if walker.disengaged: + return warch + walker.ignores = [] return warch diff --git a/jac/jaclang/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index 6c1ad9a55..0a674afe1 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -219,9 +219,9 @@ class WalkerAnchor(Anchor): """Walker Anchor.""" architype: WalkerArchitype - path: list[Anchor] = field(default_factory=list) - next: list[Anchor] = field(default_factory=list) - ignores: list[Anchor] = field(default_factory=list) + path: list[NodeAnchor] = field(default_factory=list) + next: list[NodeAnchor] = field(default_factory=list) + ignores: list[NodeAnchor] = field(default_factory=list) disengaged: bool = False diff --git a/jac/jaclang/runtimelib/utils.py b/jac/jaclang/runtimelib/utils.py index 010016640..0e19d91e7 100644 --- a/jac/jaclang/runtimelib/utils.py +++ b/jac/jaclang/runtimelib/utils.py @@ -5,6 +5,7 @@ import ast as ast3 import sys from contextlib import contextmanager +from types import UnionType from typing import Callable, Iterator, TYPE_CHECKING import jaclang.compiler.absyntree as ast @@ -156,6 +157,21 @@ def extract_type(node: ast.AstNode) -> list[str]: return extracted_type +def all_issubclass( + classes: type | UnionType | tuple[type | UnionType, ...], target: type +) -> bool: + """Check if all classes is subclass of target type.""" + match classes: + case type(): + return issubclass(classes, target) + case UnionType(): + return all((all_issubclass(cls, target) for cls in classes.__args__)) + case tuple(): + return all((all_issubclass(cls, target) for cls in classes)) + case _: + return False + + def extract_params( body: ast.FuncCall, ) -> tuple[dict[str, ast.Expr], list[tuple[str, ast3.AST]], list[tuple[str, ast3.AST]]]: diff --git a/jac/jaclang/tests/fixtures/visit_sequence.jac b/jac/jaclang/tests/fixtures/visit_sequence.jac new file mode 100644 index 000000000..562285db3 --- /dev/null +++ b/jac/jaclang/tests/fixtures/visit_sequence.jac @@ -0,0 +1,53 @@ +walker SubWalker {} + + +node Node { + has val: str; + + can entry1 with entry { + print(f"{self.val}-2"); + } + + # with 'Walker' (string class reference) is not yet supported + can entry2 with SubWalker entry { + print(f"{self.val}-3"); + } + + # with 'Walker' (string class reference) is not yet supported + can exit1 with SubWalker exit { + print(f"{self.val}-4"); + } + + can exit2 with exit { + print(f"{self.val}-5"); + } +} +walker Walker:SubWalker: { + can entry1 with entry { + print("walker entry"); + } + + can entry2 with `root entry { + print("walker enter to root"); + visit [-->]; + } + + can entry3 with Node entry { + print(f"{here.val}-1"); + } + + can exit1 with Node exit { + print(f"{here.val}-6"); + } + + can exit2 with exit { + print("walker exit"); + } +} +with entry{ + root ++> Node(val = "a"); + root ++> Node(val = "b"); + root ++> Node(val = "c"); + + Walker() spawn root; +} \ No newline at end of file diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index e24da0b13..a3344790a 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -1156,3 +1156,18 @@ def test_visit_order(self) -> None: sys.stdout = sys.__stdout__ stdout_value = captured_output.getvalue() self.assertEqual("[MyNode(Name='End'), MyNode(Name='Middle')]\n", stdout_value) + + def test_visit_sequence(self) -> None: + """Test conn assign on edges.""" + captured_output = io.StringIO() + sys.stdout = captured_output + jac_import("visit_sequence", base_path=self.fixture_abs_path("./")) + sys.stdout = sys.__stdout__ + self.assertEqual( + "walker entry\nwalker enter to root\n" + "a-1\na-2\na-3\na-4\na-5\na-6\n" + "b-1\nb-2\nb-3\nb-4\nb-5\nb-6\n" + "c-1\nc-2\nc-3\nc-4\nc-5\nc-6\n" + "walker exit\n", + captured_output.getvalue(), + )