quantum-framework 0.9.0__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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,86 @@
1
+ """
2
+ Quantum API Server - API server for Quantum microservices
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from flask import Flask, jsonify, request
8
+ import json
9
+ from typing import Dict, Any
10
+
11
+ # Fix imports
12
+
13
+ from quantum.core.ast_nodes import ApplicationNode, QuantumRoute
14
+
15
+ class QuantumAPIServer:
16
+ """API server for Quantum microservices"""
17
+
18
+ def __init__(self, port: int = 8080):
19
+ self.port = port
20
+ self.app = Flask(__name__)
21
+ self.routes: Dict[str, Dict[str, Any]] = {}
22
+ self._setup_default_routes()
23
+
24
+ def _setup_default_routes(self):
25
+ """Configure default API routes"""
26
+ @self.app.route('/health', methods=['GET'])
27
+ def health_check():
28
+ return jsonify({
29
+ "status": "healthy",
30
+ "server": "Quantum API",
31
+ "routes": list(self.routes.keys())
32
+ })
33
+
34
+ def configure_from_ast(self, app_node: ApplicationNode):
35
+ """Configure API based on application AST"""
36
+ for route in app_node.routes:
37
+ self._add_route_from_ast(route)
38
+
39
+ def _add_route_from_ast(self, route: QuantumRoute):
40
+ """Add API route based on AST"""
41
+ path = route.path
42
+ method = route.method
43
+
44
+ # Get first q:return from route
45
+ content = "{}"
46
+ if route.returns:
47
+ content = route.returns[0].value or "{}"
48
+
49
+ self._add_api_route(path, method, content)
50
+
51
+ def _add_api_route(self, path: str, method: str, content: str):
52
+ """Add API route to Flask"""
53
+ self.routes[path] = {'method': method, 'content': content}
54
+
55
+ def api_handler():
56
+ return self._process_json_content(content)
57
+
58
+ # Unique name for function
59
+ api_handler.__name__ = f"api_{path.replace('/', '_').replace('-', '_')}"
60
+
61
+ # Add to Flask
62
+ self.app.add_url_rule(path, api_handler.__name__, api_handler, methods=[method])
63
+
64
+ def _process_json_content(self, content: str):
65
+ """Process content as JSON"""
66
+ try:
67
+ # Try to parse JSON
68
+ parsed = json.loads(content)
69
+ return jsonify(parsed)
70
+ except json.JSONDecodeError:
71
+ # If not valid JSON, return as text
72
+ return jsonify({"result": content, "type": "text"})
73
+
74
+ def start(self):
75
+ """Start API server"""
76
+ print(f"🛠️ Quantum API Server starting on port {self.port}")
77
+ print(f"🔗 API: http://localhost:{self.port}")
78
+ print(f"🩺 Health: http://localhost:{self.port}/health")
79
+ print("🛑 To stop: Ctrl+C")
80
+
81
+ try:
82
+ self.app.run(host='0.0.0.0', port=self.port, debug=False)
83
+ except KeyboardInterrupt:
84
+ print("\n👋 API server stopped by user")
85
+ except Exception as e:
86
+ print(f"❌ Server error: {e}")
@@ -0,0 +1,506 @@
1
+ """
2
+ AST Cache - Phase 2 Performance Optimization
3
+
4
+ Caches parsed AST structures for faster component loading.
5
+ Uses mtime-based invalidation to detect file changes.
6
+
7
+ Performance Impact:
8
+ - 10-50x speedup on subsequent file loads
9
+ - Automatic invalidation when files change
10
+ - LRU eviction with configurable size
11
+ - Thread-safe implementation
12
+ """
13
+
14
+ import os
15
+ import hashlib
16
+ import threading
17
+ import time
18
+ import pickle
19
+ from pathlib import Path
20
+ from typing import Any, Dict, Optional, Tuple, List
21
+ from dataclasses import dataclass, field
22
+ from functools import lru_cache
23
+ import weakref
24
+
25
+
26
+ @dataclass
27
+ class CacheEntry:
28
+ """A cached AST entry with metadata"""
29
+ ast: Any # ComponentNode
30
+ mtime: float # File modification time
31
+ size: int # File size in bytes
32
+ hash: str # Content hash for extra validation
33
+ created_at: float = field(default_factory=time.time)
34
+ access_count: int = 0
35
+ last_accessed: float = field(default_factory=time.time)
36
+
37
+ def touch(self):
38
+ """Update access statistics"""
39
+ self.access_count += 1
40
+ self.last_accessed = time.time()
41
+
42
+
43
+ @dataclass
44
+ class CacheStats:
45
+ """Statistics for cache performance monitoring"""
46
+ hits: int = 0
47
+ misses: int = 0
48
+ invalidations: int = 0
49
+ evictions: int = 0
50
+ total_parse_time_ms: float = 0.0
51
+ total_cache_time_ms: float = 0.0
52
+ entries_count: int = 0
53
+ memory_estimate_kb: float = 0.0
54
+
55
+ @property
56
+ def hit_rate(self) -> float:
57
+ total = self.hits + self.misses
58
+ return self.hits / total if total > 0 else 0.0
59
+
60
+ @property
61
+ def avg_parse_time_ms(self) -> float:
62
+ return self.total_parse_time_ms / self.misses if self.misses > 0 else 0.0
63
+
64
+ def to_dict(self) -> Dict[str, Any]:
65
+ return {
66
+ 'hits': self.hits,
67
+ 'misses': self.misses,
68
+ 'hit_rate': f"{self.hit_rate:.1%}",
69
+ 'invalidations': self.invalidations,
70
+ 'evictions': self.evictions,
71
+ 'entries_count': self.entries_count,
72
+ 'memory_estimate_kb': f"{self.memory_estimate_kb:.1f}",
73
+ 'avg_parse_time_ms': f"{self.avg_parse_time_ms:.2f}",
74
+ }
75
+
76
+
77
+ class ASTCache:
78
+ """
79
+ Thread-safe LRU cache for parsed AST structures.
80
+
81
+ Features:
82
+ - Caches parsed ComponentNode objects by file path
83
+ - Automatic invalidation based on file mtime and size
84
+ - Optional content hash validation for extra safety
85
+ - LRU eviction with configurable max entries
86
+ - Statistics tracking for monitoring
87
+ - Filesystem watch integration support
88
+
89
+ Usage:
90
+ cache = ASTCache(max_entries=100)
91
+
92
+ # Get cached AST or parse if needed
93
+ ast = cache.get_or_parse("app.q", parser)
94
+
95
+ # Manual invalidation
96
+ cache.invalidate("app.q")
97
+
98
+ # Check statistics
99
+ print(cache.stats.hit_rate)
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ max_entries: int = 100,
105
+ enable_hash_validation: bool = False,
106
+ enable_stats: bool = True,
107
+ cache_dir: Optional[Path] = None,
108
+ ):
109
+ """
110
+ Initialize the AST cache.
111
+
112
+ Args:
113
+ max_entries: Maximum number of AST entries to cache
114
+ enable_hash_validation: Whether to validate content hash (slower but safer)
115
+ enable_stats: Whether to track performance statistics
116
+ cache_dir: Optional directory for persistent cache (not implemented yet)
117
+ """
118
+ self._max_entries = max_entries
119
+ self._enable_hash = enable_hash_validation
120
+ self._enable_stats = enable_stats
121
+ self._cache_dir = cache_dir
122
+
123
+ self._cache: Dict[str, CacheEntry] = {}
124
+ self._stats = CacheStats()
125
+ self._lock = threading.RLock()
126
+
127
+ # For dependency tracking (imports)
128
+ self._dependencies: Dict[str, set] = {} # file -> set of files it imports
129
+
130
+ def _normalize_path(self, file_path: Any) -> str:
131
+ """Normalize a file path to a canonical string key"""
132
+ if isinstance(file_path, Path):
133
+ return str(file_path.resolve())
134
+ return str(Path(file_path).resolve())
135
+
136
+ def _compute_hash(self, content: str) -> str:
137
+ """Compute a hash of the file content"""
138
+ return hashlib.md5(content.encode('utf-8')).hexdigest()
139
+
140
+ def _get_file_info(self, file_path: str) -> Tuple[float, int]:
141
+ """Get file mtime and size"""
142
+ try:
143
+ stat = os.stat(file_path)
144
+ return stat.st_mtime, stat.st_size
145
+ except OSError:
146
+ return 0.0, 0
147
+
148
+ def _is_valid(self, entry: CacheEntry, file_path: str, content: Optional[str] = None) -> bool:
149
+ """Check if a cache entry is still valid"""
150
+ mtime, size = self._get_file_info(file_path)
151
+
152
+ # Check mtime and size
153
+ if entry.mtime != mtime or entry.size != size:
154
+ return False
155
+
156
+ # Optionally check content hash
157
+ if self._enable_hash and content is not None:
158
+ content_hash = self._compute_hash(content)
159
+ if entry.hash != content_hash:
160
+ return False
161
+
162
+ return True
163
+
164
+ def _evict_lru(self):
165
+ """Evict least recently used entries if over capacity"""
166
+ while len(self._cache) >= self._max_entries:
167
+ # Find LRU entry
168
+ lru_key = min(
169
+ self._cache.keys(),
170
+ key=lambda k: self._cache[k].last_accessed
171
+ )
172
+ del self._cache[lru_key]
173
+ if self._enable_stats:
174
+ self._stats.evictions += 1
175
+
176
+ def _estimate_memory(self, ast: Any) -> float:
177
+ """Estimate memory usage of an AST in KB"""
178
+ try:
179
+ # Rough estimate using pickle size
180
+ data = pickle.dumps(ast)
181
+ return len(data) / 1024
182
+ except:
183
+ return 10.0 # Default estimate
184
+
185
+ def get(self, file_path: Any) -> Optional[Any]:
186
+ """
187
+ Get a cached AST if available and valid.
188
+
189
+ Args:
190
+ file_path: Path to the .q file
191
+
192
+ Returns:
193
+ Cached ComponentNode or None if not cached/invalid
194
+ """
195
+ key = self._normalize_path(file_path)
196
+
197
+ with self._lock:
198
+ if key not in self._cache:
199
+ if self._enable_stats:
200
+ self._stats.misses += 1
201
+ return None
202
+
203
+ entry = self._cache[key]
204
+
205
+ # Validate entry
206
+ if not self._is_valid(entry, key):
207
+ del self._cache[key]
208
+ if self._enable_stats:
209
+ self._stats.misses += 1
210
+ self._stats.invalidations += 1
211
+ return None
212
+
213
+ # Cache hit
214
+ entry.touch()
215
+ if self._enable_stats:
216
+ self._stats.hits += 1
217
+
218
+ return entry.ast
219
+
220
+ def get_or_parse(
221
+ self,
222
+ file_path: Any,
223
+ parser: Any,
224
+ content: Optional[str] = None,
225
+ ) -> Any:
226
+ """
227
+ Get cached AST or parse the file if not cached.
228
+
229
+ Args:
230
+ file_path: Path to the .q file
231
+ parser: QuantumParser instance to use for parsing
232
+ content: Optional file content (if already read)
233
+
234
+ Returns:
235
+ ComponentNode (either cached or freshly parsed)
236
+ """
237
+ key = self._normalize_path(file_path)
238
+
239
+ with self._lock:
240
+ # Try cache first
241
+ cached = self.get(key)
242
+ if cached is not None:
243
+ return cached
244
+
245
+ # Need to parse
246
+ start_time = time.perf_counter()
247
+
248
+ # Read content if not provided
249
+ if content is None:
250
+ with open(key, 'r', encoding='utf-8') as f:
251
+ content = f.read()
252
+
253
+ # Parse
254
+ ast = parser.parse(content)
255
+
256
+ parse_time = (time.perf_counter() - start_time) * 1000
257
+
258
+ # Get file info
259
+ mtime, size = self._get_file_info(key)
260
+ content_hash = self._compute_hash(content) if self._enable_hash else ""
261
+
262
+ # Evict if needed
263
+ self._evict_lru()
264
+
265
+ # Cache the result
266
+ entry = CacheEntry(
267
+ ast=ast,
268
+ mtime=mtime,
269
+ size=size,
270
+ hash=content_hash,
271
+ )
272
+ self._cache[key] = entry
273
+
274
+ # Update stats
275
+ if self._enable_stats:
276
+ self._stats.total_parse_time_ms += parse_time
277
+ self._stats.entries_count = len(self._cache)
278
+ self._stats.memory_estimate_kb += self._estimate_memory(ast)
279
+
280
+ return ast
281
+
282
+ def put(self, file_path: Any, ast: Any, content: Optional[str] = None):
283
+ """
284
+ Manually put an AST into the cache.
285
+
286
+ Args:
287
+ file_path: Path to the .q file
288
+ ast: The parsed ComponentNode
289
+ content: Optional file content for hash computation
290
+ """
291
+ key = self._normalize_path(file_path)
292
+
293
+ with self._lock:
294
+ mtime, size = self._get_file_info(key)
295
+ content_hash = ""
296
+ if self._enable_hash and content is not None:
297
+ content_hash = self._compute_hash(content)
298
+
299
+ self._evict_lru()
300
+
301
+ entry = CacheEntry(
302
+ ast=ast,
303
+ mtime=mtime,
304
+ size=size,
305
+ hash=content_hash,
306
+ )
307
+ self._cache[key] = entry
308
+
309
+ if self._enable_stats:
310
+ self._stats.entries_count = len(self._cache)
311
+
312
+ def invalidate(self, file_path: Optional[Any] = None):
313
+ """
314
+ Invalidate cache entries.
315
+
316
+ Args:
317
+ file_path: Specific file to invalidate, or None for all
318
+ """
319
+ with self._lock:
320
+ if file_path is None:
321
+ count = len(self._cache)
322
+ self._cache.clear()
323
+ self._dependencies.clear()
324
+ if self._enable_stats:
325
+ self._stats.invalidations += count
326
+ self._stats.entries_count = 0
327
+ else:
328
+ key = self._normalize_path(file_path)
329
+ if key in self._cache:
330
+ del self._cache[key]
331
+ if self._enable_stats:
332
+ self._stats.invalidations += 1
333
+ self._stats.entries_count = len(self._cache)
334
+
335
+ # Also invalidate dependents
336
+ self._invalidate_dependents(key)
337
+
338
+ def _invalidate_dependents(self, file_path: str):
339
+ """Invalidate all files that depend on this file"""
340
+ dependents = []
341
+ for path, deps in self._dependencies.items():
342
+ if file_path in deps:
343
+ dependents.append(path)
344
+
345
+ for dep_path in dependents:
346
+ if dep_path in self._cache:
347
+ del self._cache[dep_path]
348
+ if self._enable_stats:
349
+ self._stats.invalidations += 1
350
+
351
+ def register_dependency(self, file_path: Any, depends_on: Any):
352
+ """
353
+ Register that file_path depends on depends_on.
354
+ Used for import tracking.
355
+
356
+ Args:
357
+ file_path: The file that has a dependency
358
+ depends_on: The file it depends on (imports)
359
+ """
360
+ key = self._normalize_path(file_path)
361
+ dep_key = self._normalize_path(depends_on)
362
+
363
+ with self._lock:
364
+ if key not in self._dependencies:
365
+ self._dependencies[key] = set()
366
+ self._dependencies[key].add(dep_key)
367
+
368
+ def preload(self, file_paths: List[Any], parser: Any):
369
+ """
370
+ Preload multiple files into the cache.
371
+
372
+ Args:
373
+ file_paths: List of paths to preload
374
+ parser: QuantumParser instance
375
+ """
376
+ for file_path in file_paths:
377
+ try:
378
+ self.get_or_parse(file_path, parser)
379
+ except Exception:
380
+ pass # Ignore errors during preload
381
+
382
+ @property
383
+ def stats(self) -> CacheStats:
384
+ """Get cache statistics"""
385
+ with self._lock:
386
+ self._stats.entries_count = len(self._cache)
387
+ return self._stats
388
+
389
+ def reset_stats(self):
390
+ """Reset statistics counters"""
391
+ with self._lock:
392
+ entries = self._stats.entries_count
393
+ memory = self._stats.memory_estimate_kb
394
+ self._stats = CacheStats()
395
+ self._stats.entries_count = entries
396
+ self._stats.memory_estimate_kb = memory
397
+
398
+ def clear(self):
399
+ """Clear the entire cache"""
400
+ with self._lock:
401
+ self._cache.clear()
402
+ self._dependencies.clear()
403
+ if self._enable_stats:
404
+ self._stats.entries_count = 0
405
+ self._stats.memory_estimate_kb = 0
406
+
407
+ def cache_info(self) -> Dict[str, Any]:
408
+ """Get detailed cache information"""
409
+ with self._lock:
410
+ entries = []
411
+ for path, entry in self._cache.items():
412
+ entries.append({
413
+ 'path': path,
414
+ 'mtime': entry.mtime,
415
+ 'size': entry.size,
416
+ 'access_count': entry.access_count,
417
+ 'age_seconds': time.time() - entry.created_at,
418
+ })
419
+
420
+ return {
421
+ 'max_entries': self._max_entries,
422
+ 'current_entries': len(self._cache),
423
+ 'hash_validation': self._enable_hash,
424
+ 'entries': entries,
425
+ }
426
+
427
+
428
+ # Global singleton instance
429
+ _global_cache: Optional[ASTCache] = None
430
+ _global_cache_lock = threading.Lock()
431
+
432
+
433
+ def get_ast_cache(max_entries: int = 100) -> ASTCache:
434
+ """
435
+ Get the global AST cache instance (singleton pattern).
436
+
437
+ Args:
438
+ max_entries: Maximum cache size (only used on first call)
439
+
440
+ Returns:
441
+ The global ASTCache instance
442
+ """
443
+ global _global_cache
444
+
445
+ if _global_cache is None:
446
+ with _global_cache_lock:
447
+ if _global_cache is None:
448
+ _global_cache = ASTCache(max_entries=max_entries)
449
+
450
+ return _global_cache
451
+
452
+
453
+ def invalidate_ast_cache(file_path: Optional[Any] = None):
454
+ """
455
+ Convenience function to invalidate the global AST cache.
456
+
457
+ Args:
458
+ file_path: Specific file to invalidate, or None for all
459
+ """
460
+ cache = get_ast_cache()
461
+ cache.invalidate(file_path)
462
+
463
+
464
+ # Integration with hot reload
465
+ class ASTCacheWatcher:
466
+ """
467
+ Watches for file changes and invalidates cache accordingly.
468
+ Integrates with the hot reload system.
469
+ """
470
+
471
+ def __init__(self, cache: Optional[ASTCache] = None):
472
+ self._cache = cache or get_ast_cache()
473
+ self._watching: Dict[str, float] = {} # path -> last known mtime
474
+
475
+ def check_and_invalidate(self, file_path: Any) -> bool:
476
+ """
477
+ Check if a file has changed and invalidate if so.
478
+
479
+ Returns:
480
+ True if the file was invalidated
481
+ """
482
+ key = str(Path(file_path).resolve())
483
+
484
+ try:
485
+ current_mtime = os.stat(key).st_mtime
486
+ except OSError:
487
+ return False
488
+
489
+ if key in self._watching:
490
+ if current_mtime != self._watching[key]:
491
+ self._cache.invalidate(key)
492
+ self._watching[key] = current_mtime
493
+ return True
494
+ else:
495
+ self._watching[key] = current_mtime
496
+
497
+ return False
498
+
499
+ def on_file_changed(self, file_path: Any):
500
+ """
501
+ Callback for file change events (from hot reload).
502
+
503
+ Args:
504
+ file_path: Path to the changed file
505
+ """
506
+ self._cache.invalidate(file_path)