jaclang 0.8.8__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 (114) hide show
  1. jaclang/cli/cli.py +194 -10
  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 -8
  6. jaclang/compiler/jac.lark +154 -62
  7. jaclang/compiler/larkparse/jac_parser.py +2 -2
  8. jaclang/compiler/parser.py +656 -149
  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/def_use_pass.py +1 -0
  28. jaclang/compiler/passes/main/pyast_gen_pass.py +413 -255
  29. jaclang/compiler/passes/main/pyast_load_pass.py +48 -11
  30. jaclang/compiler/passes/main/pyjac_ast_link_pass.py +2 -0
  31. jaclang/compiler/passes/main/sym_tab_build_pass.py +18 -1
  32. jaclang/compiler/passes/main/tests/fixtures/autoimpl.cl.jac +7 -0
  33. jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +3 -0
  34. jaclang/compiler/passes/main/tests/fixtures/checker_class_construct.jac +33 -0
  35. jaclang/compiler/passes/main/tests/fixtures/defuse_modpath.jac +7 -0
  36. jaclang/compiler/passes/main/tests/fixtures/member_access_type_resolve.jac +2 -1
  37. jaclang/compiler/passes/main/tests/test_checker_pass.py +31 -3
  38. jaclang/compiler/passes/main/tests/test_def_use_pass.py +12 -0
  39. jaclang/compiler/passes/main/tests/test_import_pass.py +23 -4
  40. jaclang/compiler/passes/main/tests/test_predynamo_pass.py +13 -14
  41. jaclang/compiler/passes/main/tests/test_pyast_gen_pass.py +25 -0
  42. jaclang/compiler/passes/main/type_checker_pass.py +7 -0
  43. jaclang/compiler/passes/tool/doc_ir_gen_pass.py +219 -20
  44. jaclang/compiler/passes/tool/fuse_comments_pass.py +1 -10
  45. jaclang/compiler/passes/tool/jac_formatter_pass.py +2 -2
  46. jaclang/compiler/passes/tool/tests/fixtures/import_fmt.jac +7 -1
  47. jaclang/compiler/passes/tool/tests/fixtures/tagbreak.jac +135 -29
  48. jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +4 -1
  49. jaclang/compiler/passes/transform.py +9 -1
  50. jaclang/compiler/passes/uni_pass.py +5 -7
  51. jaclang/compiler/program.py +27 -26
  52. jaclang/compiler/tests/test_client_codegen.py +113 -0
  53. jaclang/compiler/tests/test_importer.py +12 -10
  54. jaclang/compiler/tests/test_parser.py +249 -3
  55. jaclang/compiler/type_system/type_evaluator.jac +1078 -0
  56. jaclang/compiler/type_system/type_utils.py +1 -1
  57. jaclang/compiler/type_system/types.py +6 -0
  58. jaclang/compiler/unitree.py +438 -82
  59. jaclang/langserve/engine.jac +224 -288
  60. jaclang/langserve/sem_manager.jac +12 -8
  61. jaclang/langserve/server.jac +48 -48
  62. jaclang/langserve/tests/fixtures/greet.py +17 -0
  63. jaclang/langserve/tests/fixtures/md_path.jac +22 -0
  64. jaclang/langserve/tests/fixtures/user.jac +15 -0
  65. jaclang/langserve/tests/test_server.py +66 -371
  66. jaclang/lib.py +17 -0
  67. jaclang/runtimelib/archetype.py +25 -25
  68. jaclang/runtimelib/client_bundle.py +169 -0
  69. jaclang/runtimelib/client_runtime.jac +586 -0
  70. jaclang/runtimelib/constructs.py +4 -2
  71. jaclang/runtimelib/machine.py +308 -139
  72. jaclang/runtimelib/meta_importer.py +111 -22
  73. jaclang/runtimelib/mtp.py +15 -0
  74. jaclang/runtimelib/server.py +1089 -0
  75. jaclang/runtimelib/tests/fixtures/client_app.jac +18 -0
  76. jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
  77. jaclang/runtimelib/tests/fixtures/savable_object.jac +4 -5
  78. jaclang/runtimelib/tests/fixtures/serve_api.jac +75 -0
  79. jaclang/runtimelib/tests/test_client_bundle.py +55 -0
  80. jaclang/runtimelib/tests/test_client_render.py +63 -0
  81. jaclang/runtimelib/tests/test_serve.py +1069 -0
  82. jaclang/settings.py +0 -3
  83. jaclang/tests/fixtures/attr_pattern_case.jac +18 -0
  84. jaclang/tests/fixtures/funccall_genexpr.jac +7 -0
  85. jaclang/tests/fixtures/funccall_genexpr.py +5 -0
  86. jaclang/tests/fixtures/iife_functions.jac +142 -0
  87. jaclang/tests/fixtures/iife_functions_client.jac +143 -0
  88. jaclang/tests/fixtures/multistatement_lambda.jac +116 -0
  89. jaclang/tests/fixtures/multistatement_lambda_client.jac +113 -0
  90. jaclang/tests/fixtures/needs_import_dup.jac +6 -4
  91. jaclang/tests/fixtures/py2jac_empty.py +0 -0
  92. jaclang/tests/fixtures/py_run.py +7 -5
  93. jaclang/tests/fixtures/pyfunc_fstr.py +2 -2
  94. jaclang/tests/fixtures/simple_lambda_test.jac +12 -0
  95. jaclang/tests/test_cli.py +134 -18
  96. jaclang/tests/test_language.py +120 -32
  97. jaclang/tests/test_reference.py +20 -3
  98. jaclang/utils/NonGPT.py +375 -0
  99. jaclang/utils/helpers.py +64 -20
  100. jaclang/utils/lang_tools.py +31 -4
  101. jaclang/utils/tests/test_lang_tools.py +5 -16
  102. jaclang/utils/treeprinter.py +8 -3
  103. {jaclang-0.8.8.dist-info → jaclang-0.8.10.dist-info}/METADATA +3 -3
  104. {jaclang-0.8.8.dist-info → jaclang-0.8.10.dist-info}/RECORD +106 -71
  105. jaclang/compiler/passes/main/binder_pass.py +0 -594
  106. jaclang/compiler/passes/main/tests/fixtures/sym_binder.jac +0 -47
  107. jaclang/compiler/passes/main/tests/test_binder_pass.py +0 -111
  108. jaclang/compiler/type_system/type_evaluator.py +0 -844
  109. jaclang/langserve/tests/session.jac +0 -294
  110. jaclang/langserve/tests/test_dev_server.py +0 -80
  111. jaclang/runtimelib/importer.py +0 -351
  112. jaclang/tests/test_typecheck.py +0 -542
  113. {jaclang-0.8.8.dist-info → jaclang-0.8.10.dist-info}/WHEEL +0 -0
  114. {jaclang-0.8.8.dist-info → jaclang-0.8.10.dist-info}/entry_points.txt +0 -0
jaclang/cli/cli.py CHANGED
@@ -22,9 +22,6 @@ from jaclang.settings import settings
22
22
  from jaclang.utils.helpers import debugger as db
23
23
  from jaclang.utils.lang_tools import AstTool
24
24
 
25
- Jac.create_cmd()
26
- Jac.setup()
27
-
28
25
 
29
26
  @cmd_registry.register
30
27
  def format(path: str, outfile: str = "", to_screen: bool = False) -> None:
@@ -62,7 +59,7 @@ def format(path: str, outfile: str = "", to_screen: bool = False) -> None:
62
59
  if path.endswith(".jac"):
63
60
  if not path_obj.exists():
64
61
  print(f"Error: File '{path}' does not exist.", file=sys.stderr)
65
- return
62
+ exit(1)
66
63
  formatted_code = JacProgram.jac_file_formatter(str(path_obj))
67
64
  write_formatted_code(formatted_code, str(path_obj))
68
65
  return
@@ -79,6 +76,7 @@ def format(path: str, outfile: str = "", to_screen: bool = False) -> None:
79
76
 
80
77
  # Case 3: Invalid path
81
78
  print(f"Error: '{path}' is not a .jac file or directory.", file=sys.stderr)
79
+ exit(1)
82
80
 
83
81
 
84
82
  def proc_file_sess(
@@ -104,6 +102,7 @@ def proc_file_sess(
104
102
  "Not a valid file!\nOnly supports `.jac`, `.jir`, and `.py`",
105
103
  file=sys.stderr,
106
104
  )
105
+ exit(1)
107
106
  mach = ExecutionContext(session=session, root=root)
108
107
  Jac.set_context(mach)
109
108
  return base, mod, mach
@@ -148,7 +147,11 @@ def run(
148
147
  lng=lng,
149
148
  )
150
149
  except Exception as e:
151
- print(f"Error running {filename}: {e}", file=sys.stderr)
150
+ from jaclang.utils.helpers import dump_traceback
151
+
152
+ print(dump_traceback(e), file=sys.stderr)
153
+ mach.close()
154
+ exit(1)
152
155
  elif filename.endswith(".jir"):
153
156
  try:
154
157
  with open(filename, "rb") as f:
@@ -160,7 +163,11 @@ def run(
160
163
  lng=lng,
161
164
  )
162
165
  except Exception as e:
163
- print(f"Error running {filename}: {e}", file=sys.stderr)
166
+ from jaclang.utils.helpers import dump_traceback
167
+
168
+ print(dump_traceback(e), file=sys.stderr)
169
+ mach.close()
170
+ exit(1)
164
171
 
165
172
  mach.close()
166
173
 
@@ -208,6 +215,8 @@ def get_object(filename: str, id: str, session: str = "", main: bool = True) ->
208
215
  data = obj.__jac__.__getstate__()
209
216
  else:
210
217
  print(f"Object with id {id} not found.", file=sys.stderr)
218
+ mach.close()
219
+ exit(1)
211
220
  mach.close()
212
221
  return data
213
222
 
@@ -238,6 +247,9 @@ def build(filename: str, typecheck: bool = False) -> None:
238
247
  for alrt in out.errors_had + out.warnings_had:
239
248
  print(alrt.pretty_print(), file=sys.stderr)
240
249
 
250
+ if errs > 0:
251
+ exit(1)
252
+
241
253
  with open(filename[:-4] + ".jir", "wb") as f:
242
254
  pickle.dump(out, f)
243
255
 
@@ -273,8 +285,11 @@ def bind(filename: str, typecheck: bool = False) -> None:
273
285
  divider = "=" * 40
274
286
  print(f"{divider}\n{header}\n{divider}\n{mods.sym_tab.sym_pp()}")
275
287
  print(f"Errors: {errs}, Warnings: {warnings}")
288
+ if errs > 0:
289
+ exit(1)
276
290
  else:
277
291
  print("Not a .jac/.py file.", file=sys.stderr)
292
+ exit(1)
278
293
 
279
294
 
280
295
  @cmd_registry.register
@@ -301,8 +316,11 @@ def check(filename: str, print_errs: bool = True) -> None:
301
316
  for e in prog.errors_had:
302
317
  print("Error:", e, file=sys.stderr)
303
318
  print(f"Errors: {errs}, Warnings: {warnings}")
319
+ if errs > 0:
320
+ exit(1)
304
321
  else:
305
322
  print("Not a .jac file.", file=sys.stderr)
323
+ exit(1)
306
324
 
307
325
 
308
326
  @cmd_registry.register
@@ -376,6 +394,8 @@ def enter(
376
394
  (loaded_mod,) = ret_module
377
395
  if not loaded_mod:
378
396
  print("Errors occurred while importing the module.", file=sys.stderr)
397
+ mach.close()
398
+ exit(1)
379
399
  else:
380
400
  archetype = getattr(loaded_mod, entrypoint)(*args)
381
401
 
@@ -466,6 +486,7 @@ def tool(tool: str, args: Optional[list] = None) -> None:
466
486
  raise e
467
487
  else:
468
488
  print(f"Ast tool {tool} not found.", file=sys.stderr)
489
+ exit(1)
469
490
 
470
491
 
471
492
  @cmd_registry.register
@@ -503,8 +524,10 @@ def debug(filename: str, main: bool = True, cache: bool = False) -> None:
503
524
  print("Done debugging.")
504
525
  else:
505
526
  print(f"Error while generating bytecode in {filename}.", file=sys.stderr)
527
+ exit(1)
506
528
  else:
507
529
  print("Not a .jac file.", file=sys.stderr)
530
+ exit(1)
508
531
 
509
532
 
510
533
  @cmd_registry.register
@@ -575,6 +598,7 @@ def dot(
575
598
  jac_machine.close()
576
599
  else:
577
600
  print("Not a .jac file.", file=sys.stderr)
601
+ exit(1)
578
602
 
579
603
 
580
604
  @cmd_registry.register
@@ -607,17 +631,20 @@ def py2jac(filename: str) -> None:
607
631
  print(formatted_code)
608
632
  else:
609
633
  print("Error converting Python code to Jac.", file=sys.stderr)
634
+ exit(1)
610
635
  else:
611
636
  print("Not a .py file.")
637
+ exit(1)
612
638
 
613
639
 
614
640
  @cmd_registry.register
615
641
  def jac2py(filename: str) -> None:
616
642
  """Convert a Jac file to Python code.
617
643
 
618
- Translates Jac source code to equivalent Python code. This is useful for
619
- understanding how Jac code is executed or for integrating Jac components
620
- with Python projects.
644
+ Translates Jac source code to equivalent Python code. The generated Python
645
+ uses direct imports from jaclang.lib, making the output clean and suitable
646
+ for use as a standalone library or for integrating Jac components with
647
+ Python projects.
621
648
 
622
649
  Args:
623
650
  filename: Path to the .jac file to convert
@@ -627,9 +654,166 @@ def jac2py(filename: str) -> None:
627
654
  """
628
655
  if filename.endswith(".jac"):
629
656
  code = JacProgram().compile(file_path=filename).gen.py
630
- print(code)
657
+ if code:
658
+ print(code)
659
+ else:
660
+ exit(1)
661
+ else:
662
+ print("Not a .jac file.", file=sys.stderr)
663
+ exit(1)
664
+
665
+
666
+ @cmd_registry.register
667
+ def js(filename: str) -> None:
668
+ """Convert a Jac file to JavaScript code.
669
+
670
+ Translates Jac source code to equivalent JavaScript/ECMAScript code using
671
+ the ESTree AST specification. This allows Jac programs to run in JavaScript
672
+ environments like Node.js or web browsers.
673
+
674
+ Args:
675
+ filename: Path to the .jac file to convert
676
+
677
+ Examples:
678
+ jac js myprogram.jac > myprogram.js
679
+ jac js myprogram.jac
680
+ """
681
+ if filename.endswith(".jac"):
682
+ try:
683
+ prog = JacProgram()
684
+ ir = prog.compile(file_path=filename)
685
+
686
+ if prog.errors_had:
687
+ for error in prog.errors_had:
688
+ print(f"Error: {error}", file=sys.stderr)
689
+ exit(1)
690
+ js_output = ir.gen.js or ""
691
+ if not js_output.strip():
692
+ print(
693
+ "ECMAScript code generation produced no output.",
694
+ file=sys.stderr,
695
+ )
696
+ exit(1)
697
+ print(js_output)
698
+ except Exception as e:
699
+ print(f"Error generating JavaScript: {e}", file=sys.stderr)
700
+ import traceback
701
+
702
+ traceback.print_exc()
703
+ exit(1)
631
704
  else:
632
705
  print("Not a .jac file.", file=sys.stderr)
706
+ exit(1)
707
+
708
+
709
+ # Register core commands first (before plugins load)
710
+ # These can be overridden by plugins with higher priority
711
+
712
+
713
+ @cmd_registry.register
714
+ def serve(
715
+ filename: str,
716
+ session: str = "",
717
+ port: int = 8000,
718
+ main: bool = True,
719
+ faux: bool = False,
720
+ ) -> None:
721
+ """Start a REST API server for the specified .jac file.
722
+
723
+ Executes the target module and turns all functions into authenticated REST API
724
+ endpoints. Function signatures are introspected to create the API interface.
725
+ Walkers are converted to REST APIs where their fields become the interface,
726
+ with an additional target_node field for spawning location.
727
+
728
+ Each user gets their own persistent root node that persists across runs.
729
+ Users must create an account and authenticate to access the API.
730
+
731
+ Args:
732
+ filename: Path to the .jac file to serve
733
+ session: Session identifier for persistent state (default: auto-generated)
734
+ port: Port to run the server on (default: 8000)
735
+ main: Treat the module as __main__ (default: True)
736
+ faux: Perform introspection and print endpoint docs without starting server (default: False)
737
+
738
+ Examples:
739
+ jac serve myprogram.jac
740
+ jac serve myprogram.jac --port 8080
741
+ jac serve myprogram.jac --session myapp.session
742
+ jac serve myprogram.jac --faux
743
+ """
744
+ from jaclang.runtimelib.server import JacAPIServer
745
+
746
+ # Process file and session
747
+ base, mod, mach = proc_file_sess(filename, session)
748
+ lng = filename.split(".")[-1]
749
+ Jac.set_base_path(base)
750
+
751
+ # Import the module
752
+ if filename.endswith((".jac", ".py")):
753
+ try:
754
+ Jac.jac_import(
755
+ target=mod,
756
+ base_path=base,
757
+ lng=lng,
758
+ )
759
+ except Exception as e:
760
+ print(f"Error loading {filename}: {e}", file=sys.stderr)
761
+ mach.close()
762
+ exit(1)
763
+ elif filename.endswith(".jir"):
764
+ try:
765
+ with open(filename, "rb") as f:
766
+ Jac.attach_program(pickle.load(f))
767
+ Jac.jac_import(
768
+ target=mod,
769
+ base_path=base,
770
+ lng=lng,
771
+ )
772
+ except Exception as e:
773
+ print(f"Error loading {filename}: {e}", file=sys.stderr)
774
+ mach.close()
775
+ exit(1)
776
+
777
+ # Create and start the API server
778
+ # Use session path for persistent storage across user sessions
779
+ session_path = session if session else os.path.join(base, f"{mod}.session")
780
+
781
+ server = JacAPIServer(
782
+ module_name=mod,
783
+ session_path=session_path,
784
+ port=port,
785
+ base_path=base,
786
+ )
787
+
788
+ # If faux mode, print endpoint documentation and exit
789
+ if faux:
790
+ try:
791
+ server.print_endpoint_docs()
792
+ mach.close()
793
+ return
794
+ except Exception as e:
795
+ print(f"Error generating endpoint documentation: {e}", file=sys.stderr)
796
+ mach.close()
797
+ exit(1)
798
+
799
+ # Don't close the context - keep the module loaded for the server
800
+ # mach.close()
801
+
802
+ try:
803
+ server.start()
804
+ except KeyboardInterrupt:
805
+ print("\nServer stopped.")
806
+ mach.close() # Close on shutdown
807
+ except Exception as e:
808
+ print(f"Server error: {e}", file=sys.stderr)
809
+ mach.close()
810
+ exit(1)
811
+
812
+
813
+ Jac.create_cmd()
814
+ Jac.setup()
815
+
816
+ cmd_registry.finalize()
633
817
 
634
818
 
635
819
  def start_cli() -> None:
jaclang/cli/cmdreg.py CHANGED
@@ -4,23 +4,47 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import inspect
7
+ import os
7
8
  import re
9
+ import sys
8
10
  from dataclasses import fields as dataclass_fields
11
+ from enum import IntEnum
9
12
  from typing import Callable, Dict, Optional
10
13
 
11
14
  from jaclang.settings import Settings as JacSettings
12
15
 
13
16
 
17
+ class CommandPriority(IntEnum):
18
+ """Priority levels for command registration.
19
+
20
+ Higher values take precedence when multiple commands with the same name are registered.
21
+ This allows plugins to override core commands in a controlled manner.
22
+ """
23
+
24
+ CORE = 100 # Core jaclang commands (lowest priority, can be overridden)
25
+ PLUGIN = 200 # Plugin-provided commands (override core)
26
+ USER = 300 # User-defined commands (highest priority, override everything)
27
+
28
+
14
29
  class Command:
15
30
  """Represents a command in the command line interface."""
16
31
 
17
32
  func: Callable
18
33
  sig: inspect.Signature
19
-
20
- def __init__(self, func: Callable) -> None:
34
+ priority: CommandPriority
35
+ source: str # Source plugin/module name
36
+
37
+ def __init__(
38
+ self,
39
+ func: Callable,
40
+ priority: CommandPriority = CommandPriority.CORE,
41
+ source: str = "core",
42
+ ) -> None:
21
43
  """Initialize a Command instance."""
22
44
  self.func = func
23
45
  self.sig = inspect.signature(func)
46
+ self.priority = priority
47
+ self.source = source
24
48
 
25
49
  def call(self, *args: list, **kwargs: dict) -> str:
26
50
  """Call the associated function with the specified arguments and keyword arguments."""
@@ -82,13 +106,17 @@ class CommandRegistry:
82
106
  """Registry for managing commands in the command line interface."""
83
107
 
84
108
  registry: dict[str, Command]
109
+ pending_commands: dict[str, list[Command]] # Commands waiting to be bound
85
110
  sub_parsers: argparse._SubParsersAction
86
111
  parser: argparse.ArgumentParser
87
112
  args: argparse.Namespace
113
+ _finalized: bool # Whether command registration has been finalized
88
114
 
89
115
  def __init__(self) -> None:
90
116
  """Initialize a CommandRegistry instance."""
91
117
  self.registry = {}
118
+ self.pending_commands = {}
119
+ self._finalized = False
92
120
  self.parser = argparse.ArgumentParser(
93
121
  prog="jac",
94
122
  description="Jac Programming Language CLI - A tool for working with Jac programs",
@@ -150,11 +178,55 @@ class CommandRegistry:
150
178
  help=f"str - Override setting '{name}'",
151
179
  )
152
180
 
153
- def register(self, func: Callable) -> Callable:
154
- """Register a command in the registry."""
155
- name = func.__name__
156
- cmd = Command(func)
157
- self.registry[name] = cmd
181
+ def register(
182
+ self,
183
+ func: Callable | None = None,
184
+ *,
185
+ priority: CommandPriority = CommandPriority.CORE,
186
+ source: str = "core",
187
+ ) -> Callable:
188
+ """Register a command in the registry.
189
+
190
+ This method supports both decorator syntax with and without arguments:
191
+ @cmd_registry.register
192
+ def my_cmd(): ...
193
+
194
+ @cmd_registry.register(priority=CommandPriority.PLUGIN, source="my-plugin")
195
+ def my_cmd(): ...
196
+
197
+ Args:
198
+ func: The command function to register
199
+ priority: Priority level for conflict resolution
200
+ source: Source plugin/module name for introspection
201
+
202
+ Returns:
203
+ The original function (for decorator usage)
204
+ """
205
+
206
+ def _register(f: Callable) -> Callable:
207
+ """Inner registration function."""
208
+ name = f.__name__
209
+ cmd = Command(f, priority=priority, source=source)
210
+
211
+ # Add to pending commands for priority resolution
212
+ if name not in self.pending_commands:
213
+ self.pending_commands[name] = []
214
+ self.pending_commands[name].append(cmd)
215
+
216
+ # If already finalized, bind immediately (late registration)
217
+ if self._finalized:
218
+ self._resolve_and_bind_command(name)
219
+
220
+ return f
221
+
222
+ # Support both @register and @register(...) syntax
223
+ if func is not None:
224
+ return _register(func)
225
+ return _register
226
+
227
+ def _bind_command_to_argparse(self, name: str, cmd: Command) -> None:
228
+ """Bind a command to argparse subparser."""
229
+ func = cmd.func
158
230
  # Extract the first paragraph from the docstring for brief description
159
231
  doc = func.__doc__ or ""
160
232
  brief_desc = doc.split("\n\n")[0].strip()
@@ -264,7 +336,51 @@ class CommandRegistry:
264
336
  else param.annotation
265
337
  ),
266
338
  )
267
- return func
339
+
340
+ def _resolve_and_bind_command(self, name: str) -> None:
341
+ """Resolve command conflicts by priority and bind to argparse."""
342
+ if name not in self.pending_commands:
343
+ return
344
+
345
+ commands = self.pending_commands[name]
346
+ if not commands:
347
+ return
348
+
349
+ # Sort by priority (highest first)
350
+ commands.sort(key=lambda c: c.priority, reverse=True)
351
+
352
+ # Winner is the highest priority command
353
+ winner = commands[0]
354
+
355
+ # Warn about conflicts if multiple commands with different priorities
356
+ # Only warn if JAC_CLI_VERBOSE environment variable is set
357
+ if len(commands) > 1 and os.getenv("JAC_CLI_VERBOSE"):
358
+ conflicts = [f"{c.source} (priority={c.priority})" for c in commands[1:]]
359
+ print(
360
+ f"Warning: Command '{name}' registered by multiple sources. "
361
+ f"Using {winner.source} (priority={winner.priority}). "
362
+ f"Overriding: {', '.join(conflicts)}",
363
+ file=sys.stderr,
364
+ )
365
+
366
+ # Register the winner
367
+ self.registry[name] = winner
368
+ self._bind_command_to_argparse(name, winner)
369
+
370
+ def finalize(self) -> None:
371
+ """Finalize command registration by resolving conflicts and binding to argparse.
372
+
373
+ This should be called after all plugins have had a chance to register commands.
374
+ """
375
+ if self._finalized:
376
+ return
377
+
378
+ # Resolve all pending commands
379
+ for name in list(self.pending_commands.keys()):
380
+ self._resolve_and_bind_command(name)
381
+
382
+ self._finalized = True
383
+ self.pending_commands.clear()
268
384
 
269
385
  def get(self, name: str) -> Optional[Command]:
270
386
  """Get the Command instance for a given command name."""
@@ -279,6 +395,26 @@ class CommandRegistry:
279
395
  all_commands[name] = (doc, args)
280
396
  return all_commands
281
397
 
398
+ def has_command(self, name: str) -> bool:
399
+ """Check if a command is already registered."""
400
+ return name in self.registry
401
+
402
+ def list_commands(self) -> dict[str, dict[str, CommandPriority | str]]:
403
+ """List all registered commands with metadata.
404
+
405
+ Returns:
406
+ Dictionary mapping command names to metadata including source and priority
407
+ """
408
+ return {
409
+ name: {
410
+ "source": cmd.source,
411
+ "priority": cmd.priority,
412
+ "priority_name": cmd.priority.name,
413
+ "doc": cmd.func.__doc__ or "No documentation",
414
+ }
415
+ for name, cmd in self.registry.items()
416
+ }
417
+
282
418
 
283
419
  cmd_registry = CommandRegistry()
284
420
 
@@ -36,7 +36,12 @@ except ModuleNotFoundError:
36
36
  generate_static_parser(force=True)
37
37
  from jaclang.compiler.larkparse import jac_parser as jac_lark
38
38
 
39
- jac_lark.logger.setLevel(logging.DEBUG)
39
+ if not hasattr(jac_lark, "Lark_StandAlone"):
40
+ generate_static_parser(force=True)
41
+ from jaclang.compiler.larkparse import jac_parser as jac_lark
42
+
43
+ with contextlib.suppress(AttributeError):
44
+ jac_lark.logger.setLevel(logging.DEBUG)
40
45
  contextlib.suppress(ModuleNotFoundError)
41
46
 
42
47
  TOKEN_MAP = {
@@ -3,12 +3,25 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast as ast3
6
- from typing import Optional, TYPE_CHECKING
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Optional, TYPE_CHECKING
7
8
 
8
9
  if TYPE_CHECKING:
10
+ from jaclang.compiler.passes.ecmascript.estree import Node as EsNode
9
11
  from jaclang.compiler.unitree import Source, Token
10
12
 
11
13
 
14
+ @dataclass
15
+ class ClientManifest:
16
+ """Client-side rendering manifest metadata."""
17
+
18
+ exports: list[str] = field(default_factory=list)
19
+ globals: list[str] = field(default_factory=list)
20
+ params: dict[str, list[str]] = field(default_factory=dict)
21
+ globals_values: dict[str, Any] = field(default_factory=dict)
22
+ has_client: bool = False
23
+
24
+
12
25
  class CodeGenTarget:
13
26
  """Code generation target."""
14
27
 
@@ -20,8 +33,10 @@ class CodeGenTarget:
20
33
  self.jac: str = ""
21
34
  self.doc_ir: doc.DocType = doc.Text("")
22
35
  self.js: str = ""
36
+ self.client_manifest: ClientManifest = ClientManifest()
23
37
  self.py_ast: list[ast3.AST] = []
24
38
  self.py_bytecode: Optional[bytes] = None
39
+ self.es_ast: Optional[EsNode] = None
25
40
 
26
41
 
27
42
  class CodeLocInfo:
@@ -197,7 +197,6 @@ class Tokens(str, Enum):
197
197
  BW_AND_EQ = "BW_AND_EQ"
198
198
  BW_OR_EQ = "BW_OR_EQ"
199
199
  BW_XOR_EQ = "BW_XOR_EQ"
200
- BW_NOT_EQ = "BW_NOT_EQ"
201
200
  LSHIFT_EQ = "LSHIFT_EQ"
202
201
  RSHIFT_EQ = "RSHIFT_EQ"
203
202
  WALRUS_EQ = "WALRUS_EQ"
@@ -248,6 +247,7 @@ class Tokens(str, Enum):
248
247
  KW_OVERRIDE = "KW_OVERRIDE"
249
248
  KW_MATCH = "KW_MATCH"
250
249
  KW_CASE = "KW_CASE"
250
+ KW_CLIENT = "KW_CLIENT"
251
251
  PLUS = "PLUS"
252
252
  MINUS = "MINUS"
253
253
  STAR_MUL = "STAR_MUL"
@@ -296,15 +296,40 @@ class Tokens(str, Enum):
296
296
  RETURN_HINT = "RETURN_HINT"
297
297
  NULL_OK = "NULL_OK"
298
298
  DECOR_OP = "DECOR_OP"
299
- FSTR_START = "FSTR_START"
300
- FSTR_END = "FSTR_END"
301
- FSTR_SQ_START = "FSTR_SQ_START"
302
- FSTR_SQ_END = "FSTR_SQ_END"
303
- FSTR_PIECE = "FSTR_PIECE"
304
- FSTR_SQ_PIECE = "FSTR_SQ_PIECE"
305
- FSTR_BESC = "FSTR_BESC"
299
+ JSX_TEXT = "JSX_TEXT"
300
+ JSX_OPEN_START = "JSX_OPEN_START"
301
+ JSX_SELF_CLOSE = "JSX_SELF_CLOSE"
302
+ JSX_TAG_END = "JSX_TAG_END"
303
+ JSX_CLOSE_START = "JSX_CLOSE_START"
304
+ JSX_FRAG_OPEN = "JSX_FRAG_OPEN"
305
+ JSX_FRAG_CLOSE = "JSX_FRAG_CLOSE"
306
+ JSX_NAME = "JSX_NAME"
306
307
  COMMENT = "COMMENT"
307
308
  WS = "WS"
309
+ F_DQ_START = "F_DQ_START"
310
+ F_SQ_START = "F_SQ_START"
311
+ F_TDQ_START = "F_TDQ_START"
312
+ F_TSQ_START = "F_TSQ_START"
313
+ RF_DQ_START = "RF_DQ_START"
314
+ RF_SQ_START = "RF_SQ_START"
315
+ RF_TDQ_START = "RF_TDQ_START"
316
+ RF_TSQ_START = "RF_TSQ_START"
317
+ F_DQ_END = "F_DQ_END"
318
+ F_SQ_END = "F_SQ_END"
319
+ F_TDQ_END = "F_TDQ_END"
320
+ F_TSQ_END = "F_TSQ_END"
321
+ F_TEXT_DQ = "F_TEXT_DQ"
322
+ F_TEXT_SQ = "F_TEXT_SQ"
323
+ F_TEXT_TDQ = "F_TEXT_TDQ"
324
+ F_TEXT_TSQ = "F_TEXT_TSQ"
325
+ RF_TEXT_DQ = "RF_TEXT_DQ"
326
+ RF_TEXT_SQ = "RF_TEXT_SQ"
327
+ RF_TEXT_TDQ = "RF_TEXT_TDQ"
328
+ RF_TEXT_TSQ = "RF_TEXT_TSQ"
329
+ D_LBRACE = "D_LBRACE"
330
+ D_RBRACE = "D_RBRACE"
331
+ CONV = "CONV"
332
+ F_FORMAT_TEXT = "F_FORMAT_TEXT"
308
333
 
309
334
  def __str__(self) -> str:
310
335
  return self.value