yuho 5.0.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 (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/llm/utils.py ADDED
@@ -0,0 +1,470 @@
1
+ """
2
+ LLM response caching and rate limiting utilities.
3
+ """
4
+
5
+ from typing import Dict, Any, Optional, Callable
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timedelta
8
+ import hashlib
9
+ import json
10
+ import time
11
+ import threading
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class CacheEntry:
19
+ """A cached LLM response."""
20
+ response: str
21
+ prompt_hash: str
22
+ timestamp: datetime
23
+ hit_count: int = 0
24
+
25
+
26
+ class ResponseCache:
27
+ """
28
+ LRU cache for LLM responses using content-hash keys.
29
+
30
+ Caches responses based on a hash of the prompt, allowing
31
+ repeated queries to avoid redundant LLM calls.
32
+ """
33
+
34
+ def __init__(self, max_size: int = 1000, ttl_seconds: int = 3600):
35
+ """
36
+ Initialize the response cache.
37
+
38
+ Args:
39
+ max_size: Maximum number of entries to cache
40
+ ttl_seconds: Time-to-live for cache entries in seconds
41
+ """
42
+ self._cache: Dict[str, CacheEntry] = {}
43
+ self._max_size = max_size
44
+ self._ttl = timedelta(seconds=ttl_seconds)
45
+ self._lock = threading.Lock()
46
+ self._hits = 0
47
+ self._misses = 0
48
+
49
+ def _hash_prompt(self, prompt: str, **kwargs) -> str:
50
+ """Generate a content hash for a prompt and parameters."""
51
+ # Include relevant kwargs in hash
52
+ hash_content = json.dumps({
53
+ "prompt": prompt,
54
+ **{k: v for k, v in sorted(kwargs.items())},
55
+ }, sort_keys=True)
56
+ return hashlib.sha256(hash_content.encode()).hexdigest()[:16]
57
+
58
+ def get(self, prompt: str, **kwargs) -> Optional[str]:
59
+ """
60
+ Get a cached response for a prompt.
61
+
62
+ Args:
63
+ prompt: The prompt to look up
64
+ **kwargs: Additional parameters that affect the response
65
+
66
+ Returns:
67
+ Cached response or None if not found/expired
68
+ """
69
+ key = self._hash_prompt(prompt, **kwargs)
70
+
71
+ with self._lock:
72
+ if key not in self._cache:
73
+ self._misses += 1
74
+ return None
75
+
76
+ entry = self._cache[key]
77
+
78
+ # Check if expired
79
+ if datetime.now() - entry.timestamp > self._ttl:
80
+ del self._cache[key]
81
+ self._misses += 1
82
+ return None
83
+
84
+ # Update hit count and move to end (LRU)
85
+ entry.hit_count += 1
86
+ self._cache[key] = entry # Move to end
87
+ self._hits += 1
88
+
89
+ logger.debug(f"Cache hit for prompt hash {key}")
90
+ return entry.response
91
+
92
+ def put(self, prompt: str, response: str, **kwargs) -> None:
93
+ """
94
+ Store a response in the cache.
95
+
96
+ Args:
97
+ prompt: The prompt that produced the response
98
+ response: The LLM response to cache
99
+ **kwargs: Additional parameters that affect the response
100
+ """
101
+ key = self._hash_prompt(prompt, **kwargs)
102
+
103
+ with self._lock:
104
+ # Evict oldest entries if at capacity
105
+ while len(self._cache) >= self._max_size:
106
+ oldest_key = next(iter(self._cache))
107
+ del self._cache[oldest_key]
108
+ logger.debug(f"Evicted cache entry {oldest_key}")
109
+
110
+ self._cache[key] = CacheEntry(
111
+ response=response,
112
+ prompt_hash=key,
113
+ timestamp=datetime.now(),
114
+ )
115
+ logger.debug(f"Cached response for prompt hash {key}")
116
+
117
+ def invalidate(self, prompt: str, **kwargs) -> bool:
118
+ """
119
+ Remove a specific entry from the cache.
120
+
121
+ Returns:
122
+ True if an entry was removed, False otherwise
123
+ """
124
+ key = self._hash_prompt(prompt, **kwargs)
125
+
126
+ with self._lock:
127
+ if key in self._cache:
128
+ del self._cache[key]
129
+ return True
130
+ return False
131
+
132
+ def clear(self) -> int:
133
+ """
134
+ Clear all cache entries.
135
+
136
+ Returns:
137
+ Number of entries cleared
138
+ """
139
+ with self._lock:
140
+ count = len(self._cache)
141
+ self._cache.clear()
142
+ return count
143
+
144
+ @property
145
+ def stats(self) -> Dict[str, Any]:
146
+ """Get cache statistics."""
147
+ with self._lock:
148
+ total = self._hits + self._misses
149
+ hit_rate = self._hits / total if total > 0 else 0.0
150
+ return {
151
+ "size": len(self._cache),
152
+ "max_size": self._max_size,
153
+ "hits": self._hits,
154
+ "misses": self._misses,
155
+ "hit_rate": hit_rate,
156
+ }
157
+
158
+
159
+ @dataclass
160
+ class RateLimitState:
161
+ """State for tracking rate limit window."""
162
+ requests: int = 0
163
+ window_start: float = field(default_factory=time.time)
164
+ backoff_until: float = 0.0
165
+
166
+
167
+ class RateLimiter:
168
+ """
169
+ Rate limiter with exponential backoff for cloud API providers.
170
+
171
+ Tracks request counts within time windows and enforces
172
+ limits with configurable backoff on 429 responses.
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ requests_per_minute: int = 60,
178
+ requests_per_day: int = 10000,
179
+ base_backoff_seconds: float = 1.0,
180
+ max_backoff_seconds: float = 60.0,
181
+ backoff_multiplier: float = 2.0,
182
+ ):
183
+ """
184
+ Initialize the rate limiter.
185
+
186
+ Args:
187
+ requests_per_minute: Maximum requests per minute
188
+ requests_per_day: Maximum requests per day
189
+ base_backoff_seconds: Initial backoff duration
190
+ max_backoff_seconds: Maximum backoff duration
191
+ backoff_multiplier: Multiplier for exponential backoff
192
+ """
193
+ self.rpm_limit = requests_per_minute
194
+ self.rpd_limit = requests_per_day
195
+ self.base_backoff = base_backoff_seconds
196
+ self.max_backoff = max_backoff_seconds
197
+ self.backoff_multiplier = backoff_multiplier
198
+
199
+ self._minute_state = RateLimitState()
200
+ self._day_state = RateLimitState()
201
+ self._current_backoff = base_backoff_seconds
202
+ self._consecutive_failures = 0
203
+ self._lock = threading.Lock()
204
+
205
+ def _reset_window_if_needed(self, state: RateLimitState, window_seconds: float) -> None:
206
+ """Reset request count if window has elapsed."""
207
+ now = time.time()
208
+ if now - state.window_start >= window_seconds:
209
+ state.requests = 0
210
+ state.window_start = now
211
+
212
+ def acquire(self) -> float:
213
+ """
214
+ Acquire permission to make a request.
215
+
216
+ Returns:
217
+ Number of seconds to wait before making the request.
218
+ Returns 0.0 if request can proceed immediately.
219
+ """
220
+ with self._lock:
221
+ now = time.time()
222
+
223
+ # Check if in backoff period
224
+ if now < self._minute_state.backoff_until:
225
+ wait_time = self._minute_state.backoff_until - now
226
+ logger.warning(f"Rate limiter: backoff active, wait {wait_time:.2f}s")
227
+ return wait_time
228
+
229
+ # Reset windows if needed
230
+ self._reset_window_if_needed(self._minute_state, 60.0)
231
+ self._reset_window_if_needed(self._day_state, 86400.0)
232
+
233
+ # Check minute limit
234
+ if self._minute_state.requests >= self.rpm_limit:
235
+ wait_time = 60.0 - (now - self._minute_state.window_start)
236
+ logger.warning(f"Rate limit reached (RPM), wait {wait_time:.2f}s")
237
+ return max(0.0, wait_time)
238
+
239
+ # Check day limit
240
+ if self._day_state.requests >= self.rpd_limit:
241
+ wait_time = 86400.0 - (now - self._day_state.window_start)
242
+ logger.warning(f"Rate limit reached (RPD), wait {wait_time:.2f}s")
243
+ return max(0.0, wait_time)
244
+
245
+ # Increment counters
246
+ self._minute_state.requests += 1
247
+ self._day_state.requests += 1
248
+
249
+ return 0.0
250
+
251
+ def report_success(self) -> None:
252
+ """Report a successful request, resetting backoff."""
253
+ with self._lock:
254
+ self._consecutive_failures = 0
255
+ self._current_backoff = self.base_backoff
256
+
257
+ def report_rate_limited(self) -> float:
258
+ """
259
+ Report a rate limit response (429), triggering backoff.
260
+
261
+ Returns:
262
+ Recommended wait time before retry
263
+ """
264
+ with self._lock:
265
+ self._consecutive_failures += 1
266
+
267
+ # Calculate exponential backoff
268
+ backoff = min(
269
+ self.base_backoff * (self.backoff_multiplier ** self._consecutive_failures),
270
+ self.max_backoff,
271
+ )
272
+ self._current_backoff = backoff
273
+
274
+ # Set backoff period
275
+ now = time.time()
276
+ self._minute_state.backoff_until = now + backoff
277
+
278
+ logger.warning(
279
+ f"Rate limited: backoff {backoff:.2f}s "
280
+ f"(failure #{self._consecutive_failures})"
281
+ )
282
+ return backoff
283
+
284
+ @property
285
+ def stats(self) -> Dict[str, Any]:
286
+ """Get rate limiter statistics."""
287
+ with self._lock:
288
+ now = time.time()
289
+ return {
290
+ "requests_this_minute": self._minute_state.requests,
291
+ "requests_this_day": self._day_state.requests,
292
+ "rpm_limit": self.rpm_limit,
293
+ "rpd_limit": self.rpd_limit,
294
+ "current_backoff": self._current_backoff,
295
+ "consecutive_failures": self._consecutive_failures,
296
+ "in_backoff": now < self._minute_state.backoff_until,
297
+ }
298
+
299
+
300
+ class TokenCounter:
301
+ """
302
+ Token counting for context window management.
303
+
304
+ Provides approximate token counts using simple heuristics
305
+ or exact counts via tiktoken if available.
306
+ """
307
+
308
+ def __init__(self, model: str = "gpt-4"):
309
+ """
310
+ Initialize the token counter.
311
+
312
+ Args:
313
+ model: Model name for accurate tokenization (if tiktoken available)
314
+ """
315
+ self.model = model
316
+ self._encoding = None
317
+ self._use_tiktoken = False
318
+
319
+ # Try to load tiktoken for accurate counting
320
+ try:
321
+ import tiktoken
322
+ try:
323
+ self._encoding = tiktoken.encoding_for_model(model)
324
+ self._use_tiktoken = True
325
+ logger.debug(f"Using tiktoken for {model}")
326
+ except KeyError:
327
+ self._encoding = tiktoken.get_encoding("cl100k_base")
328
+ self._use_tiktoken = True
329
+ logger.debug("Using tiktoken with cl100k_base encoding")
330
+ except ImportError:
331
+ logger.debug("tiktoken not available, using approximate counting")
332
+
333
+ def count(self, text: str) -> int:
334
+ """
335
+ Count tokens in text.
336
+
337
+ Args:
338
+ text: Text to count tokens in
339
+
340
+ Returns:
341
+ Token count (exact with tiktoken, approximate otherwise)
342
+ """
343
+ if self._use_tiktoken and self._encoding:
344
+ return len(self._encoding.encode(text))
345
+
346
+ # Approximate: ~4 chars per token (conservative estimate)
347
+ return max(1, len(text) // 4)
348
+
349
+ def fits_context(self, text: str, context_window: int, reserved: int = 1000) -> bool:
350
+ """
351
+ Check if text fits within context window.
352
+
353
+ Args:
354
+ text: Text to check
355
+ context_window: Maximum context window size
356
+ reserved: Tokens reserved for response
357
+
358
+ Returns:
359
+ True if text fits, False otherwise
360
+ """
361
+ token_count = self.count(text)
362
+ available = context_window - reserved
363
+ return token_count <= available
364
+
365
+ def truncate_to_fit(self, text: str, max_tokens: int) -> str:
366
+ """
367
+ Truncate text to fit within token limit.
368
+
369
+ Args:
370
+ text: Text to truncate
371
+ max_tokens: Maximum allowed tokens
372
+
373
+ Returns:
374
+ Truncated text
375
+ """
376
+ if self._use_tiktoken and self._encoding:
377
+ tokens = self._encoding.encode(text)
378
+ if len(tokens) <= max_tokens:
379
+ return text
380
+ return self._encoding.decode(tokens[:max_tokens])
381
+
382
+ # Approximate: ~4 chars per token
383
+ max_chars = max_tokens * 4
384
+ if len(text) <= max_chars:
385
+ return text
386
+ return text[:max_chars] + "..."
387
+
388
+
389
+ class PromptCompressor:
390
+ """
391
+ Compress prompts for long statutes exceeding context window.
392
+
393
+ Strategies:
394
+ - Remove comments and whitespace
395
+ - Summarize sections
396
+ - Split into chunks
397
+ """
398
+
399
+ def __init__(self, token_counter: Optional[TokenCounter] = None):
400
+ """Initialize the prompt compressor."""
401
+ self.token_counter = token_counter or TokenCounter()
402
+
403
+ def compress(
404
+ self,
405
+ text: str,
406
+ max_tokens: int,
407
+ preserve_structure: bool = True,
408
+ ) -> str:
409
+ """
410
+ Compress text to fit within token limit.
411
+
412
+ Args:
413
+ text: Text to compress
414
+ max_tokens: Maximum allowed tokens
415
+ preserve_structure: Keep structural elements (headers, etc.)
416
+
417
+ Returns:
418
+ Compressed text
419
+ """
420
+ current_count = self.token_counter.count(text)
421
+ if current_count <= max_tokens:
422
+ return text
423
+
424
+ # Strategy 1: Remove excessive whitespace
425
+ compressed = self._remove_excessive_whitespace(text)
426
+ if self.token_counter.count(compressed) <= max_tokens:
427
+ return compressed
428
+
429
+ # Strategy 2: Remove comments
430
+ compressed = self._remove_comments(compressed)
431
+ if self.token_counter.count(compressed) <= max_tokens:
432
+ return compressed
433
+
434
+ # Strategy 3: Truncate with summary
435
+ if preserve_structure:
436
+ return self._truncate_with_summary(compressed, max_tokens)
437
+
438
+ # Final fallback: direct truncation
439
+ return self.token_counter.truncate_to_fit(compressed, max_tokens)
440
+
441
+ def _remove_excessive_whitespace(self, text: str) -> str:
442
+ """Remove excessive whitespace while preserving structure."""
443
+ import re
444
+ # Replace multiple blank lines with single
445
+ text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
446
+ # Remove trailing whitespace
447
+ text = '\n'.join(line.rstrip() for line in text.splitlines())
448
+ return text
449
+
450
+ def _remove_comments(self, text: str) -> str:
451
+ """Remove comment lines from Yuho code."""
452
+ lines = text.splitlines()
453
+ non_comment_lines = [
454
+ line for line in lines
455
+ if not line.strip().startswith('#') and not line.strip().startswith('//')
456
+ ]
457
+ return '\n'.join(non_comment_lines)
458
+
459
+ def _truncate_with_summary(self, text: str, max_tokens: int) -> str:
460
+ """Truncate text but include a summary notice."""
461
+ # Reserve some tokens for summary notice
462
+ summary_notice = "\n\n[... content truncated for context window ...]\n"
463
+ summary_tokens = self.token_counter.count(summary_notice)
464
+
465
+ available = max_tokens - summary_tokens
466
+ if available <= 0:
467
+ return summary_notice
468
+
469
+ truncated = self.token_counter.truncate_to_fit(text, available)
470
+ return truncated + summary_notice
yuho/lsp/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ Yuho LSP (Language Server Protocol) module.
3
+
4
+ Provides IDE features for .yh files:
5
+ - Syntax error diagnostics
6
+ - Code completion
7
+ - Hover information
8
+ - Go to definition
9
+ - Find references
10
+ """
11
+
12
+ from yuho.lsp.server import YuhoLanguageServer
13
+
14
+ __all__ = ["YuhoLanguageServer"]