Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

Updating Walker #581

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions jaclang/runtimelib/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import sys
import types
from importlib import util
from os import getcwd, path
from typing import Optional, Union

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
48 changes: 45 additions & 3 deletions jaclang/runtimelib/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -134,14 +175,15 @@ 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):
codeobj = self.mod_bundle.mod_deps[full_target].gen.py_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)

Expand Down
2 changes: 1 addition & 1 deletion jaclang/tests/fixtures/bar.jac
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ walker bar_walk {
disengage;
}
}
}
}
1 change: 0 additions & 1 deletion jaclang/tests/fixtures/foo.jac
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 19 additions & 0 deletions jaclang/tests/fixtures/walker_update.jac
Original file line number Diff line number Diff line change
@@ -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();
}
58 changes: 58 additions & 0 deletions jaclang/tests/test_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading