diff --git a/jaclang/runtimelib/importer.py b/jaclang/runtimelib/importer.py index 7a9ce27ed..45eefa5f0 100644 --- a/jaclang/runtimelib/importer.py +++ b/jaclang/runtimelib/importer.py @@ -7,6 +7,7 @@ import os import sys import types +from importlib import util from os import getcwd, path from typing import Optional, Union @@ -196,11 +197,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: @@ -340,8 +341,10 @@ def run_import( spec.full_target, caller_dir=spec.caller_dir, cachable=spec.cachable, + reload=reload if reload else False, ) try: + if not codeobj: raise ImportError(f"No bytecode found for {spec.full_target}") with sys_path_context(spec.caller_dir): diff --git a/jaclang/runtimelib/machine.py b/jaclang/runtimelib/machine.py index e1d90873d..545f8a2a5 100644 --- a/jaclang/runtimelib/machine.py +++ b/jaclang/runtimelib/machine.py @@ -6,7 +6,7 @@ import sys import types from contextvars import ContextVar -from typing import Optional +from typing import Optional, Union from jaclang.compiler.absyntree import Module from jaclang.compiler.compile import compile_jac @@ -55,11 +55,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 @@ -105,6 +106,46 @@ def list_edges(self, module_name: str) -> list[str]: return nodes return [] + def update_walker( + self, module_name: str, items: Optional[dict[str, Union[str, Optional[str]]]] + ) -> tuple[types.ModuleType, ...]: + """Reimport the module.""" + from .importer import JacImporter, ImportPathSpec + + if module_name in self.loaded_modules: + try: + old_module = self.loaded_modules[module_name] + importer = JacImporter(self) + spec = ImportPathSpec( + target=module_name, + base_path=self.base_path, + absorb=False, + cachable=True, + mdl_alias=None, + override_name=None, + lng="jac", + items=items, + ) + import_result = importer.run_import(spec, reload=True) + 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 () + @staticmethod def get(base_path: str = "") -> "JacMachine": """Get current jac machine.""" @@ -134,6 +175,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): @@ -141,7 +183,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/bar.jac b/jaclang/tests/fixtures/bar.jac index 73c5cb3b7..34394779d 100644 --- a/jaclang/tests/fixtures/bar.jac +++ b/jaclang/tests/fixtures/bar.jac @@ -31,4 +31,4 @@ walker bar_walk { disengage; } } -} \ No newline at end of file +} diff --git a/jaclang/tests/fixtures/foo.jac b/jaclang/tests/fixtures/foo.jac index 4b3d756cf..e692736ff 100644 --- a/jaclang/tests/fixtures/foo.jac +++ b/jaclang/tests/fixtures/foo.jac @@ -1,4 +1,3 @@ -import:py from jaclang.plugin.feature, JacFeature as Jac; import:py from jaclang.runtimelib.machine, JacMachine; import:jac from bar, bar_walk; # Test runner to initialize the walker diff --git a/jaclang/tests/fixtures/walker_update.jac b/jaclang/tests/fixtures/walker_update.jac new file mode 100644 index 000000000..011454303 --- /dev/null +++ b/jaclang/tests/fixtures/walker_update.jac @@ -0,0 +1,19 @@ +import:jac from bar { bar_walk } +import:py from jaclang.runtimelib.machine { JacMachine } +import:py os; + +can update_bar_walker { + "Updating bar.jac with new behavior." |> print; + (bar_walk_new, ) = JacMachine.get().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 entry { + update_bar_walker(); +} \ No newline at end of file diff --git a/jaclang/tests/test_language.py b/jaclang/tests/test_language.py index e286b91f7..7a312c38d 100644 --- a/jaclang/tests/test_language.py +++ b/jaclang/tests/test_language.py @@ -952,6 +952,64 @@ 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.""" + session = self.fixture_abs_path("bar_walk.session") + bar_file_path = self.fixture_abs_path("bar.jac") + update_file_path = self.fixture_abs_path("walker_update.jac") + captured_output = io.StringIO() + sys.stdout = captured_output + cli.enter( + filename=bar_file_path, + session=session, + entrypoint="bar_walk", + args=[], + ) + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + expected_output = "Created 5 items." + self.assertIn(expected_output, stdout_value.split("\n")) + # Define the new behavior to be added + new_behavior = """ + # New behavior added during runtime + can end with `root exit { + "bar_walk has been updated with new behavior!" |> print; + disengage; + } + } + """ + + # Backup the original file content + with open(bar_file_path, "r") as bar_file: + original_content = bar_file.read() + + # Update the bar.jac file with new behavior + with open(bar_file_path, "r+") as bar_file: + content = bar_file.read() + 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() + + captured_output = io.StringIO() + sys.stdout = captured_output + + try: + cli.run( + filename=update_file_path, + ) + 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")) + finally: + # Restore the original content of bar.jac + with open(bar_file_path, "w") as bar_file: + + bar_file.write(original_content) + def test_object_ref_interface(self) -> None: """Test class method output.""" captured_output = io.StringIO()