jaclang 0.8.9__py3-none-any.whl → 0.8.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jaclang might be problematic. Click here for more details.

Files changed (103) hide show
  1. jaclang/cli/cli.py +147 -25
  2. jaclang/cli/cmdreg.py +144 -8
  3. jaclang/compiler/__init__.py +6 -1
  4. jaclang/compiler/codeinfo.py +16 -1
  5. jaclang/compiler/constant.py +33 -13
  6. jaclang/compiler/jac.lark +130 -31
  7. jaclang/compiler/larkparse/jac_parser.py +2 -2
  8. jaclang/compiler/parser.py +567 -176
  9. jaclang/compiler/passes/__init__.py +2 -1
  10. jaclang/compiler/passes/ast_gen/__init__.py +5 -0
  11. jaclang/compiler/passes/ast_gen/base_ast_gen_pass.py +54 -0
  12. jaclang/compiler/passes/ast_gen/jsx_processor.py +344 -0
  13. jaclang/compiler/passes/ecmascript/__init__.py +25 -0
  14. jaclang/compiler/passes/ecmascript/es_unparse.py +576 -0
  15. jaclang/compiler/passes/ecmascript/esast_gen_pass.py +2068 -0
  16. jaclang/compiler/passes/ecmascript/estree.py +972 -0
  17. jaclang/compiler/passes/ecmascript/tests/__init__.py +1 -0
  18. jaclang/compiler/passes/ecmascript/tests/fixtures/advanced_language_features.jac +170 -0
  19. jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.impl.jac +30 -0
  20. jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.jac +14 -0
  21. jaclang/compiler/passes/ecmascript/tests/fixtures/client_jsx.jac +89 -0
  22. jaclang/compiler/passes/ecmascript/tests/fixtures/core_language_features.jac +195 -0
  23. jaclang/compiler/passes/ecmascript/tests/test_esast_gen_pass.py +167 -0
  24. jaclang/compiler/passes/ecmascript/tests/test_js_generation.py +239 -0
  25. jaclang/compiler/passes/main/__init__.py +0 -3
  26. jaclang/compiler/passes/main/annex_pass.py +23 -1
  27. jaclang/compiler/passes/main/pyast_gen_pass.py +324 -234
  28. jaclang/compiler/passes/main/pyast_load_pass.py +46 -11
  29. jaclang/compiler/passes/main/pyjac_ast_link_pass.py +2 -0
  30. jaclang/compiler/passes/main/sym_tab_build_pass.py +18 -1
  31. jaclang/compiler/passes/main/tests/fixtures/autoimpl.cl.jac +7 -0
  32. jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +3 -0
  33. jaclang/compiler/passes/main/tests/fixtures/checker_class_construct.jac +33 -0
  34. jaclang/compiler/passes/main/tests/fixtures/defuse_modpath.jac +7 -0
  35. jaclang/compiler/passes/main/tests/fixtures/member_access_type_resolve.jac +2 -1
  36. jaclang/compiler/passes/main/tests/test_checker_pass.py +31 -2
  37. jaclang/compiler/passes/main/tests/test_def_use_pass.py +12 -0
  38. jaclang/compiler/passes/main/tests/test_import_pass.py +23 -4
  39. jaclang/compiler/passes/main/tests/test_pyast_gen_pass.py +25 -0
  40. jaclang/compiler/passes/main/type_checker_pass.py +7 -0
  41. jaclang/compiler/passes/tool/doc_ir_gen_pass.py +115 -0
  42. jaclang/compiler/passes/tool/fuse_comments_pass.py +1 -10
  43. jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +4 -1
  44. jaclang/compiler/passes/transform.py +9 -1
  45. jaclang/compiler/passes/uni_pass.py +5 -7
  46. jaclang/compiler/program.py +22 -25
  47. jaclang/compiler/tests/test_client_codegen.py +113 -0
  48. jaclang/compiler/tests/test_importer.py +12 -10
  49. jaclang/compiler/tests/test_parser.py +249 -3
  50. jaclang/compiler/type_system/type_evaluator.jac +169 -50
  51. jaclang/compiler/type_system/type_utils.py +1 -1
  52. jaclang/compiler/type_system/types.py +6 -0
  53. jaclang/compiler/unitree.py +430 -84
  54. jaclang/langserve/engine.jac +224 -288
  55. jaclang/langserve/sem_manager.jac +12 -8
  56. jaclang/langserve/server.jac +48 -48
  57. jaclang/langserve/tests/fixtures/greet.py +17 -0
  58. jaclang/langserve/tests/fixtures/md_path.jac +22 -0
  59. jaclang/langserve/tests/fixtures/user.jac +15 -0
  60. jaclang/langserve/tests/test_server.py +66 -371
  61. jaclang/lib.py +1 -1
  62. jaclang/runtimelib/client_bundle.py +169 -0
  63. jaclang/runtimelib/client_runtime.jac +586 -0
  64. jaclang/runtimelib/constructs.py +2 -0
  65. jaclang/runtimelib/machine.py +259 -100
  66. jaclang/runtimelib/meta_importer.py +111 -22
  67. jaclang/runtimelib/mtp.py +15 -0
  68. jaclang/runtimelib/server.py +1089 -0
  69. jaclang/runtimelib/tests/fixtures/client_app.jac +18 -0
  70. jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
  71. jaclang/runtimelib/tests/fixtures/savable_object.jac +4 -5
  72. jaclang/runtimelib/tests/fixtures/serve_api.jac +75 -0
  73. jaclang/runtimelib/tests/test_client_bundle.py +55 -0
  74. jaclang/runtimelib/tests/test_client_render.py +63 -0
  75. jaclang/runtimelib/tests/test_serve.py +1069 -0
  76. jaclang/settings.py +0 -2
  77. jaclang/tests/fixtures/iife_functions.jac +142 -0
  78. jaclang/tests/fixtures/iife_functions_client.jac +143 -0
  79. jaclang/tests/fixtures/multistatement_lambda.jac +116 -0
  80. jaclang/tests/fixtures/multistatement_lambda_client.jac +113 -0
  81. jaclang/tests/fixtures/needs_import_dup.jac +6 -4
  82. jaclang/tests/fixtures/py_run.py +7 -5
  83. jaclang/tests/fixtures/pyfunc_fstr.py +2 -2
  84. jaclang/tests/fixtures/simple_lambda_test.jac +12 -0
  85. jaclang/tests/test_cli.py +1 -1
  86. jaclang/tests/test_language.py +10 -39
  87. jaclang/tests/test_reference.py +17 -2
  88. jaclang/utils/NonGPT.py +375 -0
  89. jaclang/utils/helpers.py +44 -16
  90. jaclang/utils/lang_tools.py +31 -4
  91. jaclang/utils/tests/test_lang_tools.py +1 -1
  92. jaclang/utils/treeprinter.py +8 -3
  93. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/METADATA +3 -3
  94. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/RECORD +96 -66
  95. jaclang/compiler/passes/main/binder_pass.py +0 -594
  96. jaclang/compiler/passes/main/tests/fixtures/sym_binder.jac +0 -47
  97. jaclang/compiler/passes/main/tests/test_binder_pass.py +0 -111
  98. jaclang/langserve/tests/session.jac +0 -294
  99. jaclang/langserve/tests/test_dev_server.py +0 -80
  100. jaclang/runtimelib/importer.py +0 -351
  101. jaclang/tests/test_typecheck.py +0 -542
  102. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/WHEEL +0 -0
  103. {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/entry_points.txt +0 -0
@@ -5,13 +5,14 @@ from __future__ import annotations
5
5
  import ast as py_ast
6
6
  import marshal
7
7
  import types
8
+ from threading import Event
8
9
  from typing import Optional, TYPE_CHECKING
9
10
 
10
11
  import jaclang.compiler.unitree as uni
11
12
  from jaclang.compiler.parser import JacParser
13
+ from jaclang.compiler.passes.ecmascript import EsastGenPass
12
14
  from jaclang.compiler.passes.main import (
13
15
  Alert,
14
- BinderPass,
15
16
  CFGBuildPass,
16
17
  DeclImplMatchPass,
17
18
  DefUsePass,
@@ -34,14 +35,10 @@ from jaclang.compiler.passes.tool import (
34
35
  )
35
36
  from jaclang.runtimelib.utils import read_file_with_encoding
36
37
  from jaclang.settings import settings
37
- from jaclang.utils.log import logging
38
38
 
39
39
  if TYPE_CHECKING:
40
40
  from jaclang.compiler.type_system.type_evaluator import TypeEvaluator
41
41
 
42
-
43
- logger = logging.getLogger(__name__)
44
-
45
42
  ir_gen_sched = [
46
43
  SymTabBuildPass,
47
44
  DeclImplMatchPass,
@@ -53,6 +50,7 @@ type_check_sched: list = [
53
50
  TypeCheckPass,
54
51
  ]
55
52
  py_code_gen = [
53
+ EsastGenPass,
56
54
  PyastGenPass,
57
55
  PyJacAstLinkPass,
58
56
  PyBytecodeGenPass,
@@ -63,7 +61,10 @@ format_sched = [FuseCommentsPass, DocIRGenPass, JacFormatPass]
63
61
  class JacProgram:
64
62
  """JacProgram to handle the Jac program-related functionalities."""
65
63
 
66
- def __init__(self, main_mod: Optional[uni.ProgramModule] = None) -> None:
64
+ def __init__(
65
+ self,
66
+ main_mod: Optional[uni.ProgramModule] = None,
67
+ ) -> None:
67
68
  """Initialize the JacProgram object."""
68
69
  self.mod: uni.ProgramModule = main_mod if main_mod else uni.ProgramModule()
69
70
  self.py_raise_map: dict[str, str] = {}
@@ -87,7 +88,9 @@ class JacProgram:
87
88
  result = self.compile(file_path=full_target)
88
89
  return marshal.loads(result.gen.py_bytecode) if result.gen.py_bytecode else None
89
90
 
90
- def parse_str(self, source_str: str, file_path: str) -> uni.Module:
91
+ def parse_str(
92
+ self, source_str: str, file_path: str, cancel_token: Event | None = None
93
+ ) -> uni.Module:
91
94
  """Convert a Jac file to an AST."""
92
95
  had_error = False
93
96
  if file_path.endswith(".py") or file_path.endswith(".pyi"):
@@ -98,6 +101,7 @@ class JacProgram:
98
101
  orig_src=uni.Source(source_str, mod_path=file_path),
99
102
  ),
100
103
  prog=self,
104
+ cancel_token=cancel_token,
101
105
  )
102
106
  had_error = len(py_ast_ret.errors_had) > 0
103
107
  mod = py_ast_ret.ir_out
@@ -124,25 +128,23 @@ class JacProgram:
124
128
  # options in it.
125
129
  no_cgen: bool = False,
126
130
  type_check: bool = False,
131
+ cancel_token: Event | None = None,
127
132
  ) -> uni.Module:
128
133
  """Convert a Jac file to an AST."""
129
134
  keep_str = use_str or read_file_with_encoding(file_path)
130
- mod_targ = self.parse_str(keep_str, file_path)
131
- self.run_schedule(mod=mod_targ, passes=ir_gen_sched)
135
+ mod_targ = self.parse_str(keep_str, file_path, cancel_token=cancel_token)
136
+ self.run_schedule(mod=mod_targ, passes=ir_gen_sched, cancel_token=cancel_token)
132
137
  if type_check:
133
- self.run_schedule(mod=mod_targ, passes=type_check_sched)
138
+ self.run_schedule(
139
+ mod=mod_targ, passes=type_check_sched, cancel_token=cancel_token
140
+ )
134
141
  # If the module has syntax errors, we skip code generation.
135
142
  if (not mod_targ.has_syntax_errors) and (not no_cgen):
136
143
  if settings.predynamo_pass and PreDynamoPass not in py_code_gen:
137
144
  py_code_gen.insert(0, PreDynamoPass)
138
- self.run_schedule(mod=mod_targ, passes=py_code_gen)
139
- return mod_targ
140
-
141
- def bind(self, file_path: str, use_str: str | None = None) -> uni.Module:
142
- """Bind the Jac module."""
143
- keep_str = use_str or read_file_with_encoding(file_path)
144
- mod_targ = self.parse_str(keep_str, file_path)
145
- BinderPass(ir_in=mod_targ, prog=self)
145
+ self.run_schedule(
146
+ mod=mod_targ, passes=py_code_gen, cancel_token=cancel_token
147
+ )
146
148
  return mod_targ
147
149
 
148
150
  def build(
@@ -159,16 +161,11 @@ class JacProgram:
159
161
  self,
160
162
  mod: uni.Module,
161
163
  passes: list[type[Transform[uni.Module, uni.Module]]],
164
+ cancel_token: Event | None = None,
162
165
  ) -> None:
163
166
  """Run the passes on the module."""
164
- final_pass: Optional[type[Transform[uni.Module, uni.Module]]] = None
165
167
  for current_pass in passes:
166
- if current_pass == PyBytecodeGenPass:
167
- final_pass = current_pass
168
- break
169
- current_pass(ir_in=mod, prog=self) # type: ignore
170
- if final_pass:
171
- final_pass(mod, prog=self)
168
+ current_pass(ir_in=mod, prog=self, cancel_token=cancel_token) # type: ignore
172
169
 
173
170
  @staticmethod
174
171
  def jac_file_formatter(file_path: str) -> str:
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from jaclang.compiler.program import JacProgram
6
+
7
+
8
+ FIXTURE_DIR = Path(__file__).resolve().parent.parent / "passes" / "ecmascript" / "tests" / "fixtures"
9
+
10
+
11
+ def test_js_codegen_generates_js_and_manifest() -> None:
12
+ fixture = FIXTURE_DIR / "client_jsx.jac"
13
+ prog = JacProgram()
14
+ module = prog.compile(str(fixture))
15
+
16
+ assert module.gen.js.strip(), "Expected JavaScript output for client declarations"
17
+ assert "function component" in module.gen.js
18
+ assert "__jacJsx(" in module.gen.js
19
+
20
+ # Client Python code should be omitted in js_only mode
21
+ assert "def component" not in module.gen.py
22
+
23
+ # Metadata should be stored in module.gen.client_manifest
24
+ assert "__jac_client_manifest__" not in module.gen.py
25
+ manifest = module.gen.client_manifest
26
+ assert manifest, "Client manifest should be available in module.gen"
27
+ assert "component" in manifest.exports
28
+ assert "ButtonProps" in manifest.exports
29
+ assert "API_URL" in manifest.globals
30
+
31
+ # Module.gen.client_manifest should have the metadata
32
+ assert "component" in module.gen.client_manifest.exports
33
+ assert "ButtonProps" in module.gen.client_manifest.exports
34
+ assert "API_URL" in module.gen.client_manifest.globals
35
+ assert module.gen.client_manifest.params.get("component", []) == []
36
+ assert "ButtonProps" not in module.gen.client_manifest.params
37
+
38
+
39
+ def test_compilation_skips_python_stubs() -> None:
40
+ fixture = FIXTURE_DIR / "client_jsx.jac"
41
+ prog = JacProgram()
42
+ module = prog.compile(str(fixture))
43
+
44
+ assert module.gen.js.strip(), "Expected JavaScript output when emitting both"
45
+ assert "function component" in module.gen.js
46
+ assert "__jacJsx(" in module.gen.js
47
+
48
+ # Client Python definitions are intentionally omitted
49
+ assert "def component" not in module.gen.py
50
+ assert "__jac_client__" not in module.gen.py
51
+ assert "class ButtonProps" not in module.gen.py
52
+
53
+ # Manifest data should be in module.gen.client_manifest
54
+ assert "__jac_client_manifest__" not in module.gen.py
55
+ manifest = module.gen.client_manifest
56
+ assert manifest, "Client manifest should be available in module.gen"
57
+ assert "component" in manifest.exports
58
+ assert "ButtonProps" in manifest.exports
59
+ assert "API_URL" in manifest.globals
60
+
61
+ # Module.gen.client_manifest should have the metadata
62
+ assert "component" in module.gen.client_manifest.exports
63
+ assert "ButtonProps" in module.gen.client_manifest.exports
64
+ assert "API_URL" in module.gen.client_manifest.globals
65
+ assert module.gen.client_manifest.params.get("component", []) == []
66
+
67
+
68
+ def test_type_to_typeof_conversion() -> None:
69
+ """Test that type() calls are converted to typeof in JavaScript."""
70
+ from tempfile import NamedTemporaryFile
71
+
72
+ # Create a temporary test file
73
+ test_code = '''"""Test type() to typeof conversion."""
74
+
75
+ cl def check_types() {
76
+ let x = 42;
77
+ let y = "hello";
78
+ let z = True;
79
+
80
+ let t1 = type(x);
81
+ let t2 = type(y);
82
+ let t3 = type(z);
83
+ let t4 = type("world");
84
+
85
+ return t1;
86
+ }
87
+ '''
88
+
89
+ with NamedTemporaryFile(mode='w', suffix='.jac', delete=False) as f:
90
+ f.write(test_code)
91
+ f.flush()
92
+
93
+ prog = JacProgram()
94
+ module = prog.compile(f.name)
95
+
96
+ assert module.gen.js.strip(), "Expected JavaScript output for client code"
97
+
98
+ # Verify type() was converted to typeof
99
+ assert "typeof" in module.gen.js, "type() should be converted to typeof"
100
+ assert module.gen.js.count("typeof") == 4, "Should have 4 typeof expressions"
101
+
102
+ # Verify no type() calls remain
103
+ assert "type(" not in module.gen.js, "No type() calls should remain in JavaScript"
104
+
105
+ # Verify the typeof expressions are correctly formed
106
+ assert "typeof x" in module.gen.js
107
+ assert "typeof y" in module.gen.js
108
+ assert "typeof z" in module.gen.js
109
+ assert 'typeof "world"' in module.gen.js
110
+
111
+ # Clean up
112
+ import os
113
+ os.unlink(f.name)
@@ -17,6 +17,9 @@ class TestLoader(TestCase):
17
17
 
18
18
  def test_import_basic_python(self) -> None:
19
19
  """Test basic self loading."""
20
+ Jac.reset_machine()
21
+ sys.modules.pop("fixtures", None)
22
+ sys.modules.pop("fixtures.hello_world", None)
20
23
  Jac.set_base_path(self.fixture_abs_path(__file__))
21
24
  JacMachineInterface.attach_program(
22
25
  JacProgram(),
@@ -26,6 +29,9 @@ class TestLoader(TestCase):
26
29
 
27
30
  def test_modules_correct(self) -> None:
28
31
  """Test basic self loading."""
32
+ Jac.reset_machine()
33
+ sys.modules.pop("fixtures", None)
34
+ sys.modules.pop("fixtures.hello_world", None)
29
35
  Jac.set_base_path(self.fixture_abs_path(__file__))
30
36
  JacMachineInterface.attach_program(
31
37
  JacProgram(),
@@ -141,29 +147,25 @@ class TestLoader(TestCase):
141
147
  try:
142
148
  os.chdir(os.path.dirname(self.fixture_abs_path("jac_import_py_files.py")))
143
149
  Jac.set_base_path(self.fixture_abs_path("jac_import_py_files.py"))
144
- JacMachineInterface.attach_program(
145
- program:=JacProgram(),
146
- )
150
+ JacMachineInterface.attach_program(JacProgram())
147
151
  Jac.jac_import("jac_import_py_files", base_path=self.fixture_abs_path("jac_import_py_files.py"), lng="py")
148
152
  cli.run(self.fixture_abs_path("jac_import_py_files.py"))
149
153
  sys.stdout = sys.__stdout__
150
154
  stdout_value = captured_output.getvalue()
151
155
  self.assertIn("This is main test file for jac import of python files", stdout_value)
152
- self.assertIn("python_module <jaclang.compiler.unitree.Module object", str(program.mod.hub))
153
- self.assertIn("jac_module <jaclang.compiler.unitree.Module object", str(program.mod.hub))
156
+ self.assertIn("python_module <jaclang.compiler.unitree.Module object", str(Jac.program.mod.hub))
157
+ self.assertIn("jac_module <jaclang.compiler.unitree.Module object", str(Jac.program.mod.hub))
154
158
  os.environ["JAC_PYFILE_RAISE"] = "false"
155
159
  settings.load_env_vars()
156
160
  os.chdir(os.path.dirname(self.fixture_abs_path("jac_import_py_files.py")))
157
161
  Jac.set_base_path(self.fixture_abs_path("jac_import_py_files.py"))
158
- JacMachineInterface.attach_program(
159
- program:=JacProgram(),
160
- )
162
+ JacMachineInterface.attach_program(JacProgram())
161
163
  Jac.jac_import("jac_import_py_files", base_path=self.fixture_abs_path("jac_import_py_files.py"), lng="py")
162
164
  cli.run(self.fixture_abs_path("jac_import_py_files.py"))
163
165
  sys.stdout = sys.__stdout__
164
166
  stdout_value = captured_output.getvalue()
165
167
  self.assertIn("This is main test file for jac import of python files", stdout_value)
166
- self.assertNotIn("python_module <jaclang.compiler.unitree.Module object", str(program.mod.hub))
167
- self.assertNotIn("jac_module <jaclang.compiler.unitree.Module object", str(program.mod.hub))
168
+ self.assertNotIn("python_module <jaclang.compiler.unitree.Module object", str(Jac.program.mod.hub))
169
+ self.assertIn("jac_module <jaclang.compiler.unitree.Module object", str(Jac.program.mod.hub))
168
170
  finally:
169
171
  os.chdir(original_cwd)
@@ -4,6 +4,7 @@ import inspect
4
4
  import io
5
5
  import os
6
6
  import sys
7
+ from pathlib import Path
7
8
 
8
9
  from jaclang import JacMachineInterface as Jac
9
10
  from jaclang.compiler import jac_lark as jl
@@ -215,12 +216,12 @@ class TestLarkParser(TestCaseMicroSuite):
215
216
  }
216
217
  """,
217
218
  """
218
- Missing SEMI
219
+ Unexpected token 'bar'
220
+ with entry {
219
221
  foo = Foo(;
220
222
  func(foo bar)
223
+ ^^^
221
224
  foo.bar;
222
- ^^^
223
- }
224
225
  """
225
226
  ]
226
227
  for idx, alrt in enumerate(prog.errors_had):
@@ -229,4 +230,249 @@ class TestLarkParser(TestCaseMicroSuite):
229
230
  line = line.strip()
230
231
  self.assertIn(line, pretty)
231
232
 
233
+ def _load_combined_jsx_fixture(self) -> tuple[str, JacParser]:
234
+ """Parse the consolidated JSX fixture once for downstream assertions."""
235
+ fixture_path = (
236
+ Path(__file__)
237
+ .resolve()
238
+ .parent
239
+ .parent
240
+ / "passes"
241
+ / "ecmascript"
242
+ / "tests"
243
+ / "fixtures"
244
+ / "client_jsx.jac"
245
+ )
246
+ source_text = fixture_path.read_text(encoding="utf-8")
247
+ prse = JacParser(
248
+ root_ir=Source(source_text, mod_path=str(fixture_path)),
249
+ prog=JacProgram(),
250
+ )
251
+ self.assertFalse(
252
+ prse.errors_had,
253
+ f"Parser reported errors for JSX fixture: {[str(e) for e in prse.errors_had]}",
254
+ )
255
+ return source_text, prse
256
+
257
+ def test_jsx_comprehensive_fixture(self) -> None:
258
+ """Ensure the consolidated JSX fixture exercises varied grammar shapes."""
259
+ source_text, prse = self._load_combined_jsx_fixture()
260
+ tree_repr = prse.ir_out.pp()
261
+
262
+ expected_snippets = {
263
+ "self_closing": "<div />",
264
+ "attribute_binding": 'id={name}',
265
+ "namespaced_component": "<Form.Input.Text />",
266
+ "fragment": "<>",
267
+ "spread_attribute": "{...props}",
268
+ "expression_child": '{"Hello " + name + "!"}',
269
+ }
270
+ for label, snippet in expected_snippets.items():
271
+ with self.subTest(label=label):
272
+ self.assertIn(snippet, source_text)
273
+
274
+ ast_markers = {
275
+ "JsxElement": "JsxElement" in tree_repr,
276
+ "FragmentTokens": "Token - <>" in tree_repr and "Token - </>" in tree_repr,
277
+ "JsxSpreadAttribute": "JsxSpreadAttribute" in tree_repr,
278
+ }
279
+ for label, present in ast_markers.items():
280
+ with self.subTest(node=label):
281
+ self.assertTrue(present, f"{label} missing from AST pretty print")
282
+
283
+ def test_client_keyword_tagging(self) -> None:
284
+ """Test that cl keyword properly tags elements as client declarations.
285
+
286
+ Tests:
287
+ - Single statement with cl prefix
288
+ - Statement without cl prefix
289
+ - Block of statements with cl { }
290
+ - Empty cl blocks
291
+ - Multiple cl blocks at top level
292
+ - Various statement types (import, let, obj, test)
293
+ """
294
+ # Test 1: Mixed single and block client markers
295
+ source = """
296
+ cl let foo = 1;
297
+ let bar = 2;
298
+ cl {
299
+ let baz = 3;
300
+ test sample {}
301
+ }
302
+ """
303
+ module = JacProgram().parse_str(source, "test.jac")
304
+ body = module.body
305
+
306
+ self.assertEqual(
307
+ [type(stmt).__name__ for stmt in body],
308
+ ["GlobalVars", "GlobalVars", "GlobalVars", "Test"],
309
+ )
310
+ self.assertEqual(
311
+ [getattr(stmt, "is_client_decl", False) for stmt in body],
312
+ [True, False, True, True], # cl let, let, cl{let}, cl{test}
313
+ )
314
+
315
+ # Test 2: Block with different statement types
316
+ source = """
317
+ cl {
318
+ import foo;
319
+ let x = 1;
320
+ obj MyClass {}
321
+ test my_test {}
322
+ }
323
+ """
324
+ module = JacProgram().parse_str(source, "test.jac")
325
+ body = module.body
326
+
327
+ self.assertEqual(len(body), 4)
328
+ self.assertTrue(
329
+ all(
330
+ getattr(stmt, "is_client_decl", False)
331
+ for stmt in body
332
+ if hasattr(stmt, "is_client_decl")
333
+ )
334
+ )
335
+
336
+ # Test 3: Multiple cl blocks at top level
337
+ source = """
338
+ cl {
339
+ let a = 1;
340
+ }
341
+ let b = 2;
342
+ cl {
343
+ let c = 3;
344
+ }
345
+ """
346
+ module = JacProgram().parse_str(source, "test.jac")
347
+ body = module.body
348
+
349
+ self.assertEqual(len(body), 3)
350
+ self.assertEqual(
351
+ [getattr(stmt, "is_client_decl", False) for stmt in body],
352
+ [True, False, True], # cl{let a}, let b, cl{let c}
353
+ )
354
+
355
+ # Test 4: Empty client block
356
+ source = """
357
+ cl {}
358
+ let x = 1;
359
+ """
360
+ module = JacProgram().parse_str(source, "test.jac")
361
+ body = module.body
362
+
363
+ self.assertEqual(len(body), 1)
364
+ self.assertFalse(getattr(body[0], "is_client_decl", False))
365
+
366
+ # Test 5: Various statement types with single cl marker
367
+ source = """
368
+ cl import foo;
369
+ cl obj MyClass {}
370
+ cl test my_test {}
371
+ """
372
+ module = JacProgram().parse_str(source, "test.jac")
373
+ body = module.body
374
+
375
+ self.assertEqual(len(body), 3)
376
+ self.assertTrue(
377
+ all(
378
+ getattr(stmt, "is_client_decl", False)
379
+ for stmt in body
380
+ if hasattr(stmt, "is_client_decl")
381
+ )
382
+ )
383
+
384
+ def test_anonymous_ability_decl(self) -> None:
385
+ """Test that abilities can be declared without explicit names.
386
+
387
+ Tests:
388
+ - Anonymous ability with entry event
389
+ - Anonymous ability with exit event
390
+ - Named ability still works
391
+ - Autogenerated names are unique based on location
392
+ """
393
+ # Test 1: Anonymous ability with entry event
394
+ source = """
395
+ walker MyWalker {
396
+ can with entry {
397
+ print("hello");
398
+ }
399
+ }
400
+ """
401
+ prog = JacProgram()
402
+ module = prog.parse_str(source, "test.jac")
403
+ self.assertFalse(prog.errors_had)
404
+
405
+ # Find the walker and its ability
406
+ walker = module.body[0]
407
+ abilities = [stmt for stmt in walker.body if type(stmt).__name__ == "Ability"]
408
+ self.assertEqual(len(abilities), 1)
409
+
410
+ ability = abilities[0]
411
+ self.assertIsNone(ability.name_ref)
412
+ # Check that py_resolve_name generates a name
413
+ resolved_name = ability.py_resolve_name()
414
+ self.assertTrue(resolved_name.startswith("__ability_entry_"))
415
+ self.assertTrue(resolved_name.endswith("__"))
416
+
417
+ # Test 2: Anonymous ability with exit event
418
+ source = """
419
+ walker MyWalker {
420
+ can with exit {
421
+ print("goodbye");
422
+ }
423
+ }
424
+ """
425
+ prog = JacProgram()
426
+ module = prog.parse_str(source, "test.jac")
427
+ self.assertFalse(prog.errors_had)
428
+
429
+ walker = module.body[0]
430
+ abilities = [stmt for stmt in walker.body if type(stmt).__name__ == "Ability"]
431
+ ability = abilities[0]
432
+ resolved_name = ability.py_resolve_name()
433
+ self.assertTrue(resolved_name.startswith("__ability_exit_"))
434
+
435
+ # Test 3: Named ability still works
436
+ source = """
437
+ walker MyWalker {
438
+ can my_ability with entry {
439
+ print("named");
440
+ }
441
+ }
442
+ """
443
+ prog = JacProgram()
444
+ module = prog.parse_str(source, "test.jac")
445
+ self.assertFalse(prog.errors_had)
446
+
447
+ walker = module.body[0]
448
+ abilities = [stmt for stmt in walker.body if type(stmt).__name__ == "Ability"]
449
+ ability = abilities[0]
450
+ self.assertIsNotNone(ability.name_ref)
451
+ self.assertEqual(ability.py_resolve_name(), "my_ability")
452
+
453
+ # Test 4: Multiple anonymous abilities generate unique names
454
+ source = """
455
+ walker MyWalker {
456
+ can with entry {
457
+ print("first");
458
+ }
459
+ can with entry {
460
+ print("second");
461
+ }
462
+ }
463
+ """
464
+ prog = JacProgram()
465
+ module = prog.parse_str(source, "test.jac")
466
+ self.assertFalse(prog.errors_had)
467
+
468
+ walker = module.body[0]
469
+ abilities = [stmt for stmt in walker.body if type(stmt).__name__ == "Ability"]
470
+ self.assertEqual(len(abilities), 2)
471
+
472
+ name1 = abilities[0].py_resolve_name()
473
+ name2 = abilities[1].py_resolve_name()
474
+ # Names should be different due to different locations
475
+ self.assertNotEqual(name1, name2)
476
+
477
+
232
478
  TestLarkParser.self_attach_micro_tests()