From 69b970fe78f930fd282511944f78dae0c1f43fd6 Mon Sep 17 00:00:00 2001 From: Ashish Mahendra Date: Thu, 29 Aug 2024 14:15:35 +0000 Subject: [PATCH] Update: a working version --- jaclang/runtimelib/importer.py | 6 ++- jaclang/runtimelib/machine.py | 42 +++++++-------- jaclang/tests/fixtures/walker_reload/bar.jac | 34 ++++++++++++ jaclang/tests/fixtures/walker_reload/foo.jac | 56 ++++++++++++++++++++ jaclang/tests/test_language.py | 15 ++++++ 5 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 jaclang/tests/fixtures/walker_reload/bar.jac create mode 100644 jaclang/tests/fixtures/walker_reload/foo.jac diff --git a/jaclang/runtimelib/importer.py b/jaclang/runtimelib/importer.py index 8cb3dd9d4..439798a2c 100644 --- a/jaclang/runtimelib/importer.py +++ b/jaclang/runtimelib/importer.py @@ -6,6 +6,7 @@ import os import sys import types +from importlib import util from os import getcwd, path from typing import Optional, Union @@ -195,11 +196,11 @@ def run_import(self, spec: ImportPathSpec) -> ImportReturn: if spec.target.startswith("."): spec.target = spec.target.lstrip(".") full_target = path.normpath(path.join(spec.caller_dir, spec.target)) - imp_spec = importlib.util.spec_from_file_location( + imp_spec = util.spec_from_file_location( spec.target, full_target + ".py" ) if imp_spec and imp_spec.loader: - imported_module = importlib.util.module_from_spec(imp_spec) + imported_module = util.module_from_spec(imp_spec) sys.modules[imp_spec.name] = imported_module imp_spec.loader.exec_module(imported_module) else: @@ -339,6 +340,7 @@ def run_import( spec.full_target, caller_dir=spec.caller_dir, cachable=spec.cachable, + reload=reload if reload else False, ) try: diff --git a/jaclang/runtimelib/machine.py b/jaclang/runtimelib/machine.py index c39271fbe..2927b623f 100644 --- a/jaclang/runtimelib/machine.py +++ b/jaclang/runtimelib/machine.py @@ -5,7 +5,7 @@ import os import sys import types -from typing import Optional +from typing import Optional, Union from jaclang.compiler.absyntree import Module from jaclang.compiler.compile import compile_jac @@ -49,11 +49,12 @@ def get_bytecode( full_target: str, caller_dir: str, cachable: bool = True, + reload: bool = False, ) -> Optional[types.CodeType]: """Retrieve bytecode from the attached JacProgram.""" if self.jac_program: return self.jac_program.get_bytecode( - module_name, full_target, caller_dir, cachable + module_name, full_target, caller_dir, cachable, reload=reload ) return None @@ -99,7 +100,7 @@ def list_edges(self, module_name: str) -> list[str]: return nodes return [] - def update_module( + def update_walker( self, module_name: str, items: Optional[dict[str, Union[str, Optional[str]]]] ) -> tuple[types.ModuleType, ...]: """Reimport the module.""" @@ -107,12 +108,7 @@ def update_module( if module_name in self.loaded_modules: try: - # Unload the existing module - del self.loaded_modules[module_name] - if module_name in sys.modules: - del sys.modules[module_name] - - # Re-import the module + old_module = self.loaded_modules[module_name] importer = JacImporter(self) spec = ImportPathSpec( target=module_name, @@ -125,21 +121,24 @@ def update_module( items=items, ) import_result = importer.run_import(spec, reload=True) - # print(f"received items: {import_result.ret_items}") - # Load the updated module into JacMachine - self.load_module(module_name, import_result.ret_mod) - - # print(f"Module {module_name} successfully updated.") - # print(f"imported items: {import_result.ret_items}") - return ( - (import_result.ret_mod,) - if not items - else tuple(import_result.ret_items) - ) + ret_items = [] + if items: + for item_name in items: + if hasattr(old_module, item_name): + new_attr = getattr(import_result.ret_mod, item_name, None) + if new_attr: + ret_items.append(new_attr) + setattr( + old_module, + item_name, + new_attr, + ) + return (old_module,) if not items else tuple(ret_items) except Exception as e: logger.error(f"Failed to update module {module_name}: {e}") else: logger.warning(f"Module {module_name} not found in loaded modules.") + return () class JacProgram: @@ -158,6 +157,7 @@ def get_bytecode( full_target: str, caller_dir: str, cachable: bool = True, + reload: bool = False, ) -> Optional[types.CodeType]: """Get the bytecode for a specific module.""" if self.mod_bundle and isinstance(self.mod_bundle, Module): @@ -165,7 +165,7 @@ def get_bytecode( return marshal.loads(codeobj) if isinstance(codeobj, bytes) else None gen_dir = os.path.join(caller_dir, Con.JAC_GEN_DIR) pyc_file_path = os.path.join(gen_dir, module_name + ".jbc") - if cachable and os.path.exists(pyc_file_path): + if cachable and os.path.exists(pyc_file_path) and not reload: with open(pyc_file_path, "rb") as f: return marshal.load(f) diff --git a/jaclang/tests/fixtures/walker_reload/bar.jac b/jaclang/tests/fixtures/walker_reload/bar.jac new file mode 100644 index 000000000..34394779d --- /dev/null +++ b/jaclang/tests/fixtures/walker_reload/bar.jac @@ -0,0 +1,34 @@ +# Define a simple node type called `Item` +node Item { + has value: int = 0; +} + +# Define an edge type called `Link` +edge Link {} + +# Define the `bar` walker +walker bar_walk { + has count: int = 0; + + # Start walking from the root node or an Item node + can start with `root | Item entry { + here ++> Item(); + if self.count < 5 { + visit [-->]; + } else { + "Created 5 items." |> print; + disengage; + } + } + + # Walk over Item nodes and update their values + can walk with Item entry { + here.value = self.count; + f"Item value: {here.value}" |> print; + self.count += 1; + visit [-->] else { + "Finished walking over all items." |> print; + disengage; + } + } +} diff --git a/jaclang/tests/fixtures/walker_reload/foo.jac b/jaclang/tests/fixtures/walker_reload/foo.jac new file mode 100644 index 000000000..426b9cc59 --- /dev/null +++ b/jaclang/tests/fixtures/walker_reload/foo.jac @@ -0,0 +1,56 @@ +import:py from jaclang.plugin.feature {JacFeature as Jac} +import:jac from bar {bar_walk} +import:py from time {sleep} + + +can update_bar_walker { + new_behavior = ''' + # New behavior added during runtime + can end with `root exit { + "bar_walk has been updated with new behavior!" |> print; + disengage; + } +} + '''; + bar_file_path = '/home/ubuntu/jaclang/jaclang/tests/fixtures/walker_reload/bar.jac'; + with open(bar_file_path, 'r') as bar_file{ + original_content = bar_file.read(); + } + + with open(bar_file_path, 'r+') as bar_file { # Specify the correct path to bar.jac + content = bar_file.read(); + + # Replace the last occurrence of "}" with the new behavior + last_brace_index = content.rfind('}'); + if last_brace_index != -1{ + updated_content = content[:last_brace_index] + new_behavior; + bar_file.seek(0); + bar_file.write(updated_content); + bar_file.truncate(); + } + } + "Updated bar.jac with new behavior." |> print; + + (bar_walk_new,)=Jac.context().jac_machine.update_walker("bar", items={'bar_walk': None}); + "Running bar_walk after update..." |> print; + root spawn bar_walk_new(); + print(f"bar_walk: {bar_walk_new.__dict__}"); + with open(bar_file_path, 'w') as bar_file{ + bar_file.write(original_content); + } +} + +# Initialize the walker +can initial_run { + root spawn bar_walk(); + print(f"bar_walk: {bar_walk.__dict__}"); + +} + +# Define the entry point to run the test +with entry { + initial_run(); + + # Update the walker + update_bar_walker(); +} diff --git a/jaclang/tests/test_language.py b/jaclang/tests/test_language.py index ecc69196c..1bfdbbd52 100644 --- a/jaclang/tests/test_language.py +++ b/jaclang/tests/test_language.py @@ -977,3 +977,18 @@ def test_list_methods(self) -> None: ) self.assertIn("Item value: 0", stdout_value) self.assertIn("Created 5 items.", stdout_value) + + def test_walker_dynamic_update(self) -> None: + """Test dynamic update of a walker during runtime.""" + Jac.get_root().__jac__.edges.clear() + Jac.context().init_memory(base_path=self.fixture_abs_path("./")) + captured_output = io.StringIO() + sys.stdout = captured_output + + cli.run( + filename=self.fixture_abs_path("walker_reload/foo.jac"), + ) + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + expected_output = "bar_walk has been updated with new behavior!" + self.assertIn(expected_output, stdout_value.split("\n"))