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
@@ -0,0 +1,1089 @@
1
+ """REST API Server for Jac Programs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import html
7
+ import inspect
8
+ import json
9
+ import os
10
+ import secrets
11
+ from contextlib import suppress
12
+ from dataclasses import dataclass, field
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ from typing import Any, Callable, Literal, TypeAlias, get_type_hints
15
+ from urllib.parse import parse_qs, urlparse
16
+
17
+ from jaclang.runtimelib.client_bundle import ClientBundleBuilder, ClientBundleError
18
+ from jaclang.runtimelib.constructs import (
19
+ Archetype,
20
+ NodeArchetype,
21
+ Root,
22
+ WalkerArchetype,
23
+ )
24
+ from jaclang.runtimelib.machine import ExecutionContext, JacMachine as Jac
25
+
26
+ # Type Aliases
27
+ JsonValue: TypeAlias = (
28
+ None | str | int | float | bool | list["JsonValue"] | dict[str, "JsonValue"]
29
+ )
30
+ StatusCode: TypeAlias = Literal[200, 201, 400, 401, 404, 503]
31
+
32
+
33
+ # Response Models
34
+ @dataclass(frozen=True, slots=True)
35
+ class Response:
36
+ """Base response container."""
37
+
38
+ status: StatusCode
39
+ body: JsonValue
40
+ content_type: str = "application/json"
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class UserData:
45
+ """User authentication data."""
46
+
47
+ username: str
48
+ token: str
49
+ root_id: str
50
+
51
+
52
+ # Core Serializer
53
+ class JacSerializer:
54
+ """Type-safe serializer for Jac objects."""
55
+
56
+ @staticmethod
57
+ def serialize(obj: object) -> JsonValue:
58
+ """Serialize objects to JSON-compatible format."""
59
+ if obj is None or isinstance(obj, (str, int, float, bool)):
60
+ return obj
61
+
62
+ if isinstance(obj, (list, tuple)):
63
+ return [JacSerializer.serialize(item) for item in obj]
64
+
65
+ if isinstance(obj, dict):
66
+ return {key: JacSerializer.serialize(value) for key, value in obj.items()}
67
+
68
+ if isinstance(obj, Archetype):
69
+ return JacSerializer._serialize_archetype(obj)
70
+
71
+ if hasattr(obj, "__dict__"):
72
+ with suppress(Exception):
73
+ return JacSerializer.serialize(
74
+ {k: v for k, v in obj.__dict__.items() if not k.startswith("_")}
75
+ )
76
+
77
+ return str(obj) if hasattr(obj, "__str__") else f"<{type(obj).__name__}>"
78
+
79
+ @staticmethod
80
+ def _serialize_archetype(arch: Archetype) -> dict[str, JsonValue]:
81
+ """Serialize Archetype instances."""
82
+ result: dict[str, JsonValue] = {
83
+ "_jac_type": type(arch).__name__,
84
+ "_jac_id": arch.__jac__.id.hex,
85
+ "_jac_archetype": (
86
+ "node"
87
+ if isinstance(arch, NodeArchetype)
88
+ else "walker" if isinstance(arch, WalkerArchetype) else "archetype"
89
+ ),
90
+ }
91
+
92
+ for attr_name in dir(arch):
93
+ if not attr_name.startswith("_") and attr_name != "__jac__":
94
+ with suppress(Exception):
95
+ attr_value = getattr(arch, attr_name)
96
+ if not callable(attr_value):
97
+ result[attr_name] = JacSerializer.serialize(attr_value)
98
+
99
+ return result
100
+
101
+
102
+ # User Management
103
+ @dataclass(slots=True)
104
+ class UserManager:
105
+ """Manage users and their persistent roots."""
106
+
107
+ session_path: str
108
+ _users: dict[str, dict[str, str]] = field(default_factory=dict, init=False)
109
+ _tokens: dict[str, str] = field(default_factory=dict, init=False)
110
+ _db_path: str = field(init=False)
111
+
112
+ def __post_init__(self) -> None:
113
+ """Initialize user database."""
114
+ self._db_path = f"{self.session_path}.users.json"
115
+ self._load_db()
116
+
117
+ def _load_db(self) -> None:
118
+ """Load user data from persistent storage."""
119
+ try:
120
+ with open(self._db_path, encoding="utf-8") as fh:
121
+ data = json.load(fh)
122
+ self._users = data.get("__jac_users__", {})
123
+ self._tokens = data.get("__jac_tokens__", {})
124
+ except Exception:
125
+ self._users, self._tokens = {}, {}
126
+
127
+ def _persist(self) -> None:
128
+ """Save user data to persistent storage."""
129
+ with open(self._db_path, "w", encoding="utf-8") as fh:
130
+ json.dump(
131
+ {"__jac_users__": self._users, "__jac_tokens__": self._tokens}, fh
132
+ )
133
+
134
+ def create_user(self, username: str, password: str) -> dict[str, str]:
135
+ """Create a new user with their own root node. Returns dict with user data or error."""
136
+ if username in self._users:
137
+ return {"error": "User already exists"}
138
+
139
+ ctx = ExecutionContext(session=self.session_path)
140
+ Jac.set_context(ctx)
141
+
142
+ try:
143
+ user_root = Root()
144
+ root_anchor = user_root.__jac__
145
+ Jac.save(root_anchor)
146
+ Jac.commit(root_anchor)
147
+ root_id = root_anchor.id.hex
148
+ finally:
149
+ ctx.mem.close()
150
+ Jac.set_context(ExecutionContext())
151
+
152
+ token = secrets.token_urlsafe(32)
153
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
154
+
155
+ self._users[username] = {
156
+ "password_hash": password_hash,
157
+ "token": token,
158
+ "root_id": root_id,
159
+ }
160
+ self._tokens[token] = username
161
+ self._persist()
162
+
163
+ return {"username": username, "token": token, "root_id": root_id}
164
+
165
+ def authenticate(self, username: str, password: str) -> dict[str, str] | None:
166
+ """Authenticate a user and return their data."""
167
+ if username not in self._users:
168
+ return None
169
+
170
+ user = self._users[username]
171
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
172
+
173
+ if user["password_hash"] == password_hash:
174
+ return {
175
+ "username": username,
176
+ "token": user["token"],
177
+ "root_id": user["root_id"],
178
+ }
179
+ return None
180
+
181
+ def validate_token(self, token: str) -> str | None:
182
+ """Validate token and return username."""
183
+ return self._tokens.get(token)
184
+
185
+ def get_root_id(self, username: str) -> str | None:
186
+ """Get user's root node ID."""
187
+ return self._users[username]["root_id"] if username in self._users else None
188
+
189
+ def close(self) -> None:
190
+ """Close and persist user data."""
191
+ self._persist()
192
+
193
+
194
+ # Execution Context Manager
195
+ class ExecutionManager:
196
+ """Manages execution contexts for user operations."""
197
+
198
+ def __init__(self, session_path: str, user_manager: UserManager) -> None:
199
+ """Initialize execution manager."""
200
+ self.session_path = session_path
201
+ self.user_manager = user_manager
202
+
203
+ def execute_function(
204
+ self, func: Callable[..., Any], args: dict[str, Any], username: str
205
+ ) -> dict[str, JsonValue]:
206
+ """Execute a function in user's context."""
207
+ root_id = self.user_manager.get_root_id(username)
208
+ if not root_id:
209
+ return {"error": "User not found"}
210
+
211
+ ctx = ExecutionContext(session=self.session_path, root=root_id)
212
+ Jac.set_context(ctx)
213
+
214
+ try:
215
+ result = func(**args)
216
+ Jac.commit()
217
+ return {
218
+ "result": JacSerializer.serialize(result),
219
+ "reports": JacSerializer.serialize(ctx.reports),
220
+ }
221
+ except Exception as e:
222
+ return {"error": str(e)}
223
+ finally:
224
+ ctx.mem.close()
225
+ Jac.set_context(ExecutionContext())
226
+
227
+ def spawn_walker(
228
+ self, walker_cls: type[WalkerArchetype], fields: dict[str, Any], username: str
229
+ ) -> dict[str, JsonValue]:
230
+ """Spawn a walker in user's context."""
231
+ root_id = self.user_manager.get_root_id(username)
232
+ if not root_id:
233
+ return {"error": "User not found"}
234
+
235
+ target_node_id = fields.pop("_jac_spawn_node", None)
236
+ ctx = ExecutionContext(session=self.session_path, root=root_id)
237
+ Jac.set_context(ctx)
238
+
239
+ try:
240
+ walker = walker_cls(**fields)
241
+
242
+ if target_node_id:
243
+ target_node = Jac.get_object(target_node_id)
244
+ if not isinstance(target_node, NodeArchetype):
245
+ return {"error": f"Invalid target node: {target_node_id}"}
246
+ else:
247
+ target_node = ctx.get_root()
248
+
249
+ Jac.spawn(walker, target_node)
250
+ Jac.commit()
251
+
252
+ return {
253
+ "result": JacSerializer.serialize(walker),
254
+ "reports": JacSerializer.serialize(ctx.reports),
255
+ }
256
+ except Exception as e:
257
+ import traceback
258
+
259
+ return {"error": str(e), "traceback": traceback.format_exc()}
260
+ finally:
261
+ ctx.mem.close()
262
+ Jac.set_context(ExecutionContext())
263
+
264
+
265
+ # Module Introspector
266
+ @dataclass(slots=True)
267
+ class ModuleIntrospector:
268
+ """Introspects and caches module metadata."""
269
+
270
+ module_name: str
271
+ base_path: str | None
272
+ _module: Any = field(default=None, init=False)
273
+ _functions: dict[str, Callable[..., Any]] = field(default_factory=dict, init=False)
274
+ _walkers: dict[str, type[WalkerArchetype]] = field(default_factory=dict, init=False)
275
+ _client_functions: dict[str, Callable[..., Any]] = field(
276
+ default_factory=dict, init=False
277
+ )
278
+ _client_manifest: dict[str, Any] = field(default_factory=dict, init=False)
279
+ _bundle: Any = field(default=None, init=False)
280
+ _bundle_error: str | None = field(default=None, init=False)
281
+ _bundle_builder: ClientBundleBuilder = field(
282
+ default_factory=ClientBundleBuilder, init=False
283
+ )
284
+
285
+ def load(self, force_reload: bool = False) -> None:
286
+ """Load module and refresh caches."""
287
+ needs_import = force_reload or self.module_name not in Jac.loaded_modules
288
+
289
+ if needs_import and self.base_path:
290
+ Jac.jac_import(
291
+ target=self.module_name,
292
+ base_path=os.path.abspath(self.base_path),
293
+ lng="jac",
294
+ reload_module=force_reload,
295
+ )
296
+
297
+ module = Jac.loaded_modules.get(self.module_name)
298
+ if not module or self._module is module and not needs_import:
299
+ return
300
+
301
+ self._module = module
302
+ self._load_manifest()
303
+ self._functions = self._collect_functions()
304
+ self._walkers = self._collect_walkers()
305
+ self._bundle = None
306
+ self._bundle_error = None
307
+
308
+ def _load_manifest(self) -> None:
309
+ """Load client manifest from module."""
310
+ if not self._module:
311
+ return
312
+
313
+ mod_path = getattr(self._module, "__file__", None)
314
+ if mod_path:
315
+ mod = Jac.program.mod.hub.get(mod_path)
316
+ if mod and mod.gen.client_manifest:
317
+ manifest = mod.gen.client_manifest
318
+ self._client_manifest = {
319
+ "exports": manifest.exports,
320
+ "globals": manifest.globals,
321
+ "params": manifest.params,
322
+ "globals_values": manifest.globals_values,
323
+ "has_client": manifest.has_client,
324
+ }
325
+ return
326
+ self._client_manifest = {}
327
+
328
+ def _collect_functions(self) -> dict[str, Callable[..., Any]]:
329
+ """Collect callable functions from module."""
330
+ if not self._module:
331
+ return {}
332
+
333
+ functions: dict[str, Callable[..., Any]] = {}
334
+ export_names = set(self._client_manifest.get("exports", []))
335
+
336
+ for name, obj in inspect.getmembers(self._module):
337
+ if (
338
+ inspect.isfunction(obj)
339
+ and not name.startswith("_")
340
+ and obj.__module__ == self._module.__name__
341
+ ):
342
+ functions[name] = obj
343
+ if name in export_names:
344
+ self._client_functions[name] = obj
345
+
346
+ # Ensure all manifest exports are included
347
+ for name in export_names:
348
+ if name not in self._client_functions and hasattr(self._module, name):
349
+ attr = getattr(self._module, name)
350
+ if callable(attr):
351
+ self._client_functions[name] = attr
352
+
353
+ return functions
354
+
355
+ def _collect_walkers(self) -> dict[str, type[WalkerArchetype]]:
356
+ """Collect walker classes from module."""
357
+ if not self._module:
358
+ return {}
359
+
360
+ walkers: dict[str, type[WalkerArchetype]] = {}
361
+ for name, obj in inspect.getmembers(self._module):
362
+ if (
363
+ isinstance(obj, type)
364
+ and issubclass(obj, WalkerArchetype)
365
+ and obj is not WalkerArchetype
366
+ and obj.__module__ == self._module.__name__
367
+ ):
368
+ walkers[name] = obj
369
+ return walkers
370
+
371
+ def get_functions(self) -> dict[str, Callable[..., Any]]:
372
+ """Get all functions."""
373
+ if not self._functions:
374
+ self.load()
375
+ return dict(self._functions)
376
+
377
+ def get_walkers(self) -> dict[str, type[WalkerArchetype]]:
378
+ """Get all walkers."""
379
+ if not self._walkers:
380
+ self.load()
381
+ return dict(self._walkers)
382
+
383
+ def get_client_functions(self) -> dict[str, Callable[..., Any]]:
384
+ """Get client-exportable functions."""
385
+ if not self._client_functions:
386
+ self.load()
387
+ return dict(self._client_functions)
388
+
389
+ def introspect_callable(self, func: Callable[..., Any]) -> dict[str, Any]:
390
+ """Get callable signature information."""
391
+ try:
392
+ sig = inspect.signature(func)
393
+ type_hints = get_type_hints(func)
394
+ except Exception:
395
+ return {"parameters": {}, "return_type": "Any"}
396
+
397
+ params = {
398
+ name: {
399
+ "type": str(type_hints.get(name, Any)),
400
+ "required": param.default == inspect.Parameter.empty,
401
+ "default": (
402
+ None
403
+ if param.default == inspect.Parameter.empty
404
+ else str(param.default)
405
+ ),
406
+ }
407
+ for name, param in sig.parameters.items()
408
+ }
409
+
410
+ return {"parameters": params, "return_type": str(type_hints.get("return", Any))}
411
+
412
+ def introspect_walker(self, walker_cls: type[WalkerArchetype]) -> dict[str, Any]:
413
+ """Get walker field information."""
414
+ try:
415
+ sig = inspect.signature(walker_cls.__init__)
416
+ type_hints = get_type_hints(walker_cls.__init__)
417
+ except Exception:
418
+ return {"fields": {}}
419
+
420
+ fields = {
421
+ name: {
422
+ "type": str(type_hints.get(name, Any)),
423
+ "required": param.default == inspect.Parameter.empty,
424
+ "default": (
425
+ None
426
+ if param.default == inspect.Parameter.empty
427
+ else str(param.default)
428
+ ),
429
+ }
430
+ for name, param in sig.parameters.items()
431
+ if name not in ("self", "args", "kwargs")
432
+ }
433
+
434
+ fields["_jac_spawn_node"] = {
435
+ "type": "str (node ID, optional)",
436
+ "required": False,
437
+ "default": "root",
438
+ }
439
+
440
+ return {"fields": fields}
441
+
442
+ def ensure_bundle(self) -> str:
443
+ """Ensure client bundle is available and return hash."""
444
+ if not self._module:
445
+ raise RuntimeError("Module not loaded")
446
+
447
+ if self._bundle:
448
+ return self._bundle.hash
449
+
450
+ try:
451
+ self._bundle = self._bundle_builder.build(self._module)
452
+ self._bundle_error = None
453
+ return self._bundle.hash
454
+ except ClientBundleError as exc:
455
+ self._bundle = None
456
+ self._bundle_error = str(exc)
457
+ raise RuntimeError(self._bundle_error) from exc
458
+
459
+ def render_page(
460
+ self, function_name: str, args: dict[str, Any], username: str
461
+ ) -> dict[str, Any]:
462
+ """Render HTML page for client function."""
463
+ self.load()
464
+
465
+ available_exports = set(self._client_manifest.get("exports", [])) or set(
466
+ self.get_client_functions().keys()
467
+ )
468
+ if function_name not in available_exports:
469
+ raise ValueError(f"Client function '{function_name}' not found")
470
+
471
+ bundle_hash = self.ensure_bundle()
472
+ arg_order = list(self._client_manifest.get("params", {}).get(function_name, []))
473
+
474
+ globals_payload = {
475
+ name: JacSerializer.serialize(value)
476
+ for name, value in self._collect_client_globals().items()
477
+ }
478
+
479
+ initial_state = {
480
+ "module": self._module.__name__ if self._module else self.module_name,
481
+ "function": function_name,
482
+ "args": {
483
+ key: JacSerializer.serialize(value) for key, value in args.items()
484
+ },
485
+ "globals": globals_payload,
486
+ "argOrder": arg_order,
487
+ }
488
+
489
+ safe_initial_json = json.dumps(initial_state).replace("</", "<\\/")
490
+
491
+ page = (
492
+ "<!DOCTYPE html>"
493
+ '<html lang="en">'
494
+ "<head>"
495
+ '<meta charset="utf-8"/>'
496
+ f"<title>{html.escape(function_name)}</title>"
497
+ "</head>"
498
+ "<body>"
499
+ '<div id="__jac_root"></div>'
500
+ f'<script id="__jac_init__" type="application/json">{safe_initial_json}</script>'
501
+ f'<script src="/static/client.js?hash={bundle_hash}" defer></script>'
502
+ "</body>"
503
+ "</html>"
504
+ )
505
+
506
+ return {
507
+ "html": page,
508
+ "bundle_hash": bundle_hash,
509
+ "bundle_code": self._bundle.code,
510
+ }
511
+
512
+ def _collect_client_globals(self) -> dict[str, Any]:
513
+ """Collect client-exposed global values."""
514
+ if not self._module:
515
+ return {}
516
+
517
+ result: dict[str, Any] = {}
518
+ names = self._client_manifest.get("globals", [])
519
+ values_map = self._client_manifest.get("globals_values", {})
520
+
521
+ for name in names:
522
+ if name in values_map:
523
+ result[name] = values_map[name]
524
+ elif hasattr(self._module, name):
525
+ result[name] = getattr(self._module, name)
526
+ else:
527
+ result[name] = None
528
+
529
+ return result
530
+
531
+
532
+ # HTTP Response Builder
533
+ class ResponseBuilder:
534
+ """Build and send HTTP responses."""
535
+
536
+ @staticmethod
537
+ def send_json(
538
+ handler: BaseHTTPRequestHandler, status: StatusCode, data: dict[str, JsonValue]
539
+ ) -> None:
540
+ """Send JSON response with CORS headers."""
541
+ handler.send_response(status)
542
+ handler.send_header("Content-Type", "application/json")
543
+ ResponseBuilder._add_cors_headers(handler)
544
+ handler.end_headers()
545
+ handler.wfile.write(json.dumps(data).encode())
546
+
547
+ @staticmethod
548
+ def send_html(
549
+ handler: BaseHTTPRequestHandler, status: StatusCode, body: str
550
+ ) -> None:
551
+ """Send HTML response with CORS headers."""
552
+ payload = body.encode("utf-8")
553
+ handler.send_response(status)
554
+ handler.send_header("Content-Type", "text/html; charset=utf-8")
555
+ handler.send_header("Content-Length", str(len(payload)))
556
+ ResponseBuilder._add_cors_headers(handler)
557
+ handler.end_headers()
558
+ handler.wfile.write(payload)
559
+
560
+ @staticmethod
561
+ def send_javascript(handler: BaseHTTPRequestHandler, code: str) -> None:
562
+ """Send JavaScript response."""
563
+ payload = code.encode("utf-8")
564
+ handler.send_response(200)
565
+ handler.send_header("Content-Type", "application/javascript; charset=utf-8")
566
+ handler.send_header("Content-Length", str(len(payload)))
567
+ handler.send_header("Cache-Control", "no-cache")
568
+ ResponseBuilder._add_cors_headers(handler)
569
+ handler.end_headers()
570
+ handler.wfile.write(payload)
571
+
572
+ @staticmethod
573
+ def _add_cors_headers(handler: BaseHTTPRequestHandler) -> None:
574
+ """Add CORS headers to response."""
575
+ handler.send_header("Access-Control-Allow-Origin", "*")
576
+ handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
577
+ handler.send_header(
578
+ "Access-Control-Allow-Headers", "Content-Type, Authorization"
579
+ )
580
+
581
+
582
+ # Route Handlers
583
+ class RouteHandler:
584
+ """Base route handler."""
585
+
586
+ def __init__(
587
+ self,
588
+ introspector: ModuleIntrospector,
589
+ execution_manager: ExecutionManager,
590
+ user_manager: UserManager,
591
+ ) -> None:
592
+ """Initialize route handler."""
593
+ self.introspector = introspector
594
+ self.execution_manager = execution_manager
595
+ self.user_manager = user_manager
596
+
597
+
598
+ class AuthHandler(RouteHandler):
599
+ """Handle authentication routes."""
600
+
601
+ def create_user(self, username: str, password: str) -> Response:
602
+ """Create new user."""
603
+ if not username or not password:
604
+ return Response(400, {"error": "Username and password required"})
605
+
606
+ result = self.user_manager.create_user(username, password)
607
+ if "error" in result:
608
+ return Response(400, dict[str, JsonValue](result))
609
+
610
+ return Response(201, dict[str, JsonValue](result))
611
+
612
+ def login(self, username: str, password: str) -> Response:
613
+ """Authenticate user."""
614
+ if not username or not password:
615
+ return Response(400, {"error": "Username and password required"})
616
+
617
+ result = self.user_manager.authenticate(username, password)
618
+ if not result:
619
+ return Response(401, {"error": "Invalid credentials"})
620
+
621
+ return Response(200, dict[str, JsonValue](result))
622
+
623
+
624
+ class IntrospectionHandler(RouteHandler):
625
+ """Handle introspection routes."""
626
+
627
+ def list_functions(self) -> Response:
628
+ """List all functions."""
629
+ self.introspector.load()
630
+ return Response(
631
+ 200, {"functions": list(self.introspector.get_functions().keys())}
632
+ )
633
+
634
+ def list_walkers(self) -> Response:
635
+ """List all walkers."""
636
+ self.introspector.load()
637
+ return Response(200, {"walkers": list(self.introspector.get_walkers().keys())})
638
+
639
+ def get_function_info(self, name: str) -> Response:
640
+ """Get function signature."""
641
+ self.introspector.load()
642
+ functions = self.introspector.get_functions()
643
+
644
+ if name not in functions:
645
+ return Response(404, {"error": f"Function '{name}' not found"})
646
+
647
+ signature = self.introspector.introspect_callable(functions[name])
648
+ return Response(200, {"name": name, "signature": signature})
649
+
650
+ def get_walker_info(self, name: str) -> Response:
651
+ """Get walker info."""
652
+ self.introspector.load()
653
+ walkers = self.introspector.get_walkers()
654
+
655
+ if name not in walkers:
656
+ return Response(404, {"error": f"Walker '{name}' not found"})
657
+
658
+ info = self.introspector.introspect_walker(walkers[name])
659
+ return Response(200, {"name": name, "info": info})
660
+
661
+
662
+ class ExecutionHandler(RouteHandler):
663
+ """Handle execution routes."""
664
+
665
+ def call_function(self, name: str, args: dict[str, Any], username: str) -> Response:
666
+ """Call a function."""
667
+ self.introspector.load()
668
+ functions = self.introspector.get_functions()
669
+
670
+ if name not in functions:
671
+ return Response(404, {"error": f"Function '{name}' not found"})
672
+
673
+ result = self.execution_manager.execute_function(
674
+ functions[name], args, username
675
+ )
676
+ return Response(200, result)
677
+
678
+ def spawn_walker(
679
+ self, name: str, fields: dict[str, Any], username: str
680
+ ) -> Response:
681
+ """Spawn a walker."""
682
+ self.introspector.load()
683
+ walkers = self.introspector.get_walkers()
684
+
685
+ if name not in walkers:
686
+ return Response(404, {"error": f"Walker '{name}' not found"})
687
+
688
+ result = self.execution_manager.spawn_walker(walkers[name], fields, username)
689
+ return Response(200, result)
690
+
691
+
692
+ # Main Server
693
+ class JacAPIServer:
694
+ """REST API Server for Jac programs."""
695
+
696
+ def __init__(
697
+ self,
698
+ module_name: str,
699
+ session_path: str,
700
+ port: int = 8000,
701
+ base_path: str | None = None,
702
+ ) -> None:
703
+ """Initialize the API server."""
704
+ self.module_name = module_name
705
+ self.session_path = session_path
706
+ self.port = port
707
+ self.base_path = base_path
708
+
709
+ # Core components
710
+ self.user_manager = UserManager(session_path)
711
+ self.introspector = ModuleIntrospector(module_name, base_path)
712
+ self.execution_manager = ExecutionManager(session_path, self.user_manager)
713
+
714
+ # Route handlers
715
+ self.auth_handler = AuthHandler(
716
+ self.introspector, self.execution_manager, self.user_manager
717
+ )
718
+ self.introspection_handler = IntrospectionHandler(
719
+ self.introspector, self.execution_manager, self.user_manager
720
+ )
721
+ self.execution_handler = ExecutionHandler(
722
+ self.introspector, self.execution_manager, self.user_manager
723
+ )
724
+
725
+ def create_handler(self) -> type[BaseHTTPRequestHandler]:
726
+ """Create HTTP request handler."""
727
+ server = self
728
+
729
+ class JacRequestHandler(BaseHTTPRequestHandler):
730
+ """Handle HTTP requests."""
731
+
732
+ def _get_auth_token(self) -> str | None:
733
+ """Extract auth token from Authorization header."""
734
+ auth_header = self.headers.get("Authorization")
735
+ return (
736
+ auth_header[7:]
737
+ if auth_header and auth_header.startswith("Bearer ")
738
+ else None
739
+ )
740
+
741
+ def _authenticate(self) -> str | None:
742
+ """Authenticate request and return username."""
743
+ token = self._get_auth_token()
744
+ return server.user_manager.validate_token(token) if token else None
745
+
746
+ def _read_json(self) -> dict[str, Any]:
747
+ """Read and parse JSON from request body."""
748
+ content_length = int(self.headers.get("Content-Length", 0))
749
+ body = (
750
+ self.rfile.read(content_length).decode()
751
+ if content_length > 0
752
+ else "{}"
753
+ )
754
+ return json.loads(body)
755
+
756
+ def _send_response(self, response: Response) -> None:
757
+ """Send response to client."""
758
+ ResponseBuilder.send_json(self, response.status, response.body) # type: ignore[arg-type]
759
+
760
+ def do_OPTIONS(self) -> None: # noqa: N802
761
+ """Handle OPTIONS requests (CORS preflight)."""
762
+ self.send_response(200)
763
+ ResponseBuilder._add_cors_headers(self)
764
+ self.end_headers()
765
+
766
+ def do_GET(self) -> None: # noqa: N802
767
+ """Handle GET requests."""
768
+ parsed_path = urlparse(self.path)
769
+ path = parsed_path.path
770
+
771
+ # Static assets
772
+ if path == "/static/client.js":
773
+ try:
774
+ server.introspector.load()
775
+ server.introspector.ensure_bundle()
776
+ ResponseBuilder.send_javascript(
777
+ self, server.introspector._bundle.code
778
+ )
779
+ except RuntimeError as exc:
780
+ ResponseBuilder.send_json(self, 503, {"error": str(exc)})
781
+ return
782
+
783
+ # Root endpoint
784
+ if path == "/":
785
+ ResponseBuilder.send_json(
786
+ self,
787
+ 200,
788
+ {
789
+ "message": "Jac API Server",
790
+ "endpoints": {
791
+ "POST /user/create": "Create a new user",
792
+ "POST /user/login": "Authenticate and get token",
793
+ "GET /functions": "List all available functions",
794
+ "GET /walkers": "List all available walkers",
795
+ "GET /function/<name>": "Get function signature",
796
+ "GET /walker/<name>": "Get walker fields",
797
+ "POST /function/<name>": "Call a function",
798
+ "POST /walker/<name>": "Spawn a walker",
799
+ "GET /page/<name>": "Render HTML page for client function",
800
+ },
801
+ },
802
+ )
803
+ return
804
+
805
+ # Client page rendering (public or authenticated)
806
+ if path.startswith("/page/"):
807
+ func_name = path.split("/")[-1]
808
+ query_params = parse_qs(parsed_path.query, keep_blank_values=True)
809
+ args = {
810
+ key: values[0] if len(values) == 1 else values
811
+ for key, values in query_params.items()
812
+ if key != "mode"
813
+ }
814
+
815
+ username = self._authenticate()
816
+ if not username:
817
+ username = "__guest__"
818
+ if username not in server.user_manager._users:
819
+ server.user_manager.create_user(username, "__no_password__")
820
+
821
+ try:
822
+ render_payload = server.introspector.render_page(
823
+ func_name, args, username
824
+ )
825
+ ResponseBuilder.send_html(self, 200, render_payload["html"])
826
+ except ValueError as exc:
827
+ ResponseBuilder.send_json(self, 404, {"error": str(exc)})
828
+ except RuntimeError as exc:
829
+ ResponseBuilder.send_json(self, 503, {"error": str(exc)})
830
+ return
831
+
832
+ # Protected endpoints
833
+ username = self._authenticate()
834
+ if not username:
835
+ ResponseBuilder.send_json(self, 401, {"error": "Unauthorized"})
836
+ return
837
+
838
+ # Route to introspection handlers
839
+ if path == "/functions":
840
+ self._send_response(server.introspection_handler.list_functions())
841
+ elif path == "/walkers":
842
+ self._send_response(server.introspection_handler.list_walkers())
843
+ elif path.startswith("/function/"):
844
+ name = path.split("/")[-1]
845
+ self._send_response(
846
+ server.introspection_handler.get_function_info(name)
847
+ )
848
+ elif path.startswith("/walker/"):
849
+ name = path.split("/")[-1]
850
+ self._send_response(
851
+ server.introspection_handler.get_walker_info(name)
852
+ )
853
+ else:
854
+ ResponseBuilder.send_json(self, 404, {"error": "Not found"})
855
+
856
+ def do_POST(self) -> None: # noqa: N802
857
+ """Handle POST requests."""
858
+ parsed_path = urlparse(self.path)
859
+ path = parsed_path.path
860
+
861
+ try:
862
+ data = self._read_json()
863
+ except json.JSONDecodeError:
864
+ ResponseBuilder.send_json(self, 400, {"error": "Invalid JSON"})
865
+ return
866
+
867
+ # Public auth endpoints
868
+ if path == "/user/create":
869
+ response = server.auth_handler.create_user(
870
+ data.get("username", ""), data.get("password", "")
871
+ )
872
+ self._send_response(response)
873
+ return
874
+
875
+ if path == "/user/login":
876
+ response = server.auth_handler.login(
877
+ data.get("username", ""), data.get("password", "")
878
+ )
879
+ self._send_response(response)
880
+ return
881
+
882
+ # Protected endpoints
883
+ username = self._authenticate()
884
+ if not username:
885
+ ResponseBuilder.send_json(self, 401, {"error": "Unauthorized"})
886
+ return
887
+
888
+ # Route to execution handlers
889
+ if path.startswith("/function/"):
890
+ name = path.split("/")[-1]
891
+ response = server.execution_handler.call_function(
892
+ name, data.get("args", {}), username
893
+ )
894
+ self._send_response(response)
895
+ elif path.startswith("/walker/"):
896
+ name = path.split("/")[-1]
897
+ response = server.execution_handler.spawn_walker(
898
+ name, data.get("fields", {}), username
899
+ )
900
+ self._send_response(response)
901
+ else:
902
+ ResponseBuilder.send_json(self, 404, {"error": "Not found"})
903
+
904
+ def log_message(self, format: str, *args: object) -> None:
905
+ """Log HTTP requests."""
906
+ print(f"{self.address_string()} - {format % args}")
907
+
908
+ return JacRequestHandler
909
+
910
+ def load_module(self, force_reload: bool = False) -> None:
911
+ """Load the target module (backward compatibility)."""
912
+ self.introspector.load(force_reload)
913
+
914
+ @property
915
+ def module(self) -> object:
916
+ """Get loaded module (backward compatibility)."""
917
+ return self.introspector._module
918
+
919
+ def get_functions(self) -> dict[str, Callable[..., Any]]:
920
+ """Get all functions (backward compatibility)."""
921
+ return self.introspector.get_functions()
922
+
923
+ def get_walkers(self) -> dict[str, type[WalkerArchetype]]:
924
+ """Get all walkers (backward compatibility)."""
925
+ return self.introspector.get_walkers()
926
+
927
+ def introspect_callable(self, func: Callable[..., Any]) -> dict[str, Any]:
928
+ """Introspect callable (backward compatibility)."""
929
+ return self.introspector.introspect_callable(func)
930
+
931
+ def introspect_walker(self, walker_cls: type[WalkerArchetype]) -> dict[str, Any]:
932
+ """Introspect walker (backward compatibility)."""
933
+ return self.introspector.introspect_walker(walker_cls)
934
+
935
+ def render_client_page(
936
+ self, function_name: str, args: dict[str, Any], username: str
937
+ ) -> dict[str, Any]:
938
+ """Render client page (backward compatibility)."""
939
+ return self.introspector.render_page(function_name, args, username)
940
+
941
+ def get_client_bundle_code(self) -> str:
942
+ """Get client bundle code (backward compatibility)."""
943
+ self.introspector.load()
944
+ self.introspector.ensure_bundle()
945
+ return self.introspector._bundle.code
946
+
947
+ def print_endpoint_docs(self) -> None:
948
+ """Print comprehensive documentation for all endpoints that would be generated."""
949
+ print_endpoint_docs(self)
950
+
951
+ def start(self) -> None:
952
+ """Start the HTTP server."""
953
+ self.introspector.load()
954
+ handler_class = self.create_handler()
955
+
956
+ with HTTPServer(("0.0.0.0", self.port), handler_class) as httpd:
957
+ print(f"Jac API Server running on http://0.0.0.0:{self.port}")
958
+ print(f"Module: {self.module_name}")
959
+ print(f"Session: {self.session_path}")
960
+ print("\nAvailable endpoints:")
961
+ print(" POST /user/create - Create a new user")
962
+ print(" POST /user/login - Login and get auth token")
963
+ print(" GET /functions - List all functions")
964
+ print(" GET /walkers - List all walkers")
965
+ print(" GET /function/<name> - Get function signature")
966
+ print(" GET /walker/<name> - Get walker info")
967
+ print(" POST /function/<name> - Call a function")
968
+ print(" POST /walker/<name> - Spawn a walker")
969
+ print("\nPress Ctrl+C to stop the server")
970
+
971
+ try:
972
+ httpd.serve_forever()
973
+ except KeyboardInterrupt:
974
+ print("\nShutting down server...")
975
+
976
+
977
+ def print_endpoint_docs(server: JacAPIServer) -> None:
978
+ """Print comprehensive documentation for all endpoints that would be generated."""
979
+ server.introspector.load()
980
+ functions = server.introspector.get_functions()
981
+ walkers = server.introspector.get_walkers()
982
+ client_exports = server.introspector._client_manifest.get("exports", [])
983
+
984
+ def section(title: str, auth: str = "") -> None:
985
+ """Print section header."""
986
+ print(f"\n{title}{f' ({auth})' if auth else ''}\n{'-' * 80}")
987
+
988
+ def endpoint(method: str, path: str, desc: str, details: str = "") -> None:
989
+ """Print endpoint with description."""
990
+ print(f"\n{method:6} {path}")
991
+ print(f" {desc}")
992
+ if details:
993
+ print(f" {details}")
994
+
995
+ def format_param(name: str, info: dict[str, Any]) -> str:
996
+ """Format parameter info."""
997
+ req = "required" if info["required"] else "optional"
998
+ default = f", default: {info['default']}" if info.get("default") else ""
999
+ return f'{name}: {info["type"]} ({req}{default})'
1000
+
1001
+ # Header
1002
+ print("\n" + "=" * 80)
1003
+ print(f"JAC API SERVER - {server.module_name}")
1004
+ print("=" * 80)
1005
+
1006
+ # Auth endpoints
1007
+ section("AUTHENTICATION")
1008
+ endpoint(
1009
+ "POST",
1010
+ "/user/create",
1011
+ "Create new user account",
1012
+ 'Body: { "username": str, "password": str }',
1013
+ )
1014
+ endpoint(
1015
+ "POST",
1016
+ "/user/login",
1017
+ "Authenticate and get token",
1018
+ 'Body: { "username": str, "password": str }',
1019
+ )
1020
+
1021
+ # Introspection
1022
+ section("INTROSPECTION", "Authenticated")
1023
+ endpoint("GET", "/functions", "List all functions → string[]")
1024
+ endpoint("GET", "/walkers", "List all walkers → string[]")
1025
+
1026
+ # Functions
1027
+ if functions:
1028
+ section("FUNCTIONS", "Authenticated")
1029
+ for name, func in functions.items():
1030
+ sig = server.introspector.introspect_callable(func)
1031
+ params = [format_param(n, i) for n, i in sig["parameters"].items()]
1032
+ params_str = ", ".join(params) if params else "none"
1033
+
1034
+ endpoint("GET", f"/function/{name}", "Get signature")
1035
+ endpoint(
1036
+ "POST",
1037
+ f"/function/{name}",
1038
+ f"Call function → {sig['return_type']}",
1039
+ f"Args: {{ {params_str} }}" if params else "No arguments",
1040
+ )
1041
+
1042
+ # Walkers
1043
+ if walkers:
1044
+ section("WALKERS", "Authenticated")
1045
+ for name, walker_cls in walkers.items():
1046
+ info = server.introspector.introspect_walker(walker_cls)
1047
+ fields = [format_param(n, i) for n, i in info["fields"].items()]
1048
+ fields_str = ", ".join(fields[:3])
1049
+ if len(fields) > 3:
1050
+ fields_str += f", ... (+{len(fields) - 3} more)"
1051
+
1052
+ endpoint("GET", f"/walker/{name}", "Get walker fields")
1053
+ endpoint(
1054
+ "POST",
1055
+ f"/walker/{name}",
1056
+ "Spawn walker",
1057
+ f"Fields: {{ {fields_str} }}" if fields else "No fields",
1058
+ )
1059
+
1060
+ # Client pages
1061
+ section("CLIENT PAGES", "Public")
1062
+ if client_exports:
1063
+ funcs_list = ", ".join(sorted(client_exports)[:8])
1064
+ if len(client_exports) > 8:
1065
+ funcs_list += f" (+{len(client_exports) - 8} more)"
1066
+ print(f"\nAvailable ({len(client_exports)}): {funcs_list}")
1067
+ endpoint(
1068
+ "GET",
1069
+ "/page/<name>",
1070
+ "Render HTML for client function",
1071
+ "Example: /page/App?arg1=value1",
1072
+ )
1073
+ else:
1074
+ print("\nNo client functions. Define with 'cl def' for browser-side rendering.")
1075
+
1076
+ # Static
1077
+ section("STATIC")
1078
+ endpoint("GET", "/", "API information and endpoint list")
1079
+ endpoint("GET", "/static/client.js", "Client JavaScript bundle")
1080
+
1081
+ # Summary
1082
+ total = 2 + 2 + len(functions) * 2 + len(walkers) * 2 + 2
1083
+ print("\n" + "=" * 80)
1084
+ print(
1085
+ f"TOTAL: {len(functions)} functions · {len(walkers)} walkers · "
1086
+ f"{len(client_exports)} client functions · {total} endpoints"
1087
+ )
1088
+ print("=" * 80)
1089
+ print("\nAuth: Bearer token (Authorization: Bearer <token>)\n")