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
@@ -0,0 +1,377 @@
1
+ """
2
+ Live preview mode with file watching for Yuho.
3
+
4
+ Watches a .yh file, auto-transpiles to mermaid/english on changes,
5
+ and serves the result in a browser with live reload.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ import webbrowser
12
+ import hashlib
13
+ import threading
14
+ from pathlib import Path
15
+ from http.server import HTTPServer, SimpleHTTPRequestHandler
16
+ from typing import Optional, Dict, Any
17
+ import json
18
+
19
+ import click
20
+
21
+
22
+ class PreviewState:
23
+ """Shared state for preview server."""
24
+
25
+ def __init__(self):
26
+ self.content: str = ""
27
+ self.format: str = "english"
28
+ self.last_error: Optional[str] = None
29
+ self.file_path: str = ""
30
+ self.last_modified: float = 0
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return {
34
+ "content": self.content,
35
+ "format": self.format,
36
+ "error": self.last_error,
37
+ "file": self.file_path,
38
+ "modified": self.last_modified,
39
+ }
40
+
41
+
42
+ # Global state
43
+ _preview_state = PreviewState()
44
+
45
+
46
+ def transpile_file(path: Path, target: str) -> tuple[Optional[str], Optional[str]]:
47
+ """
48
+ Transpile a file to the target format.
49
+
50
+ Returns:
51
+ (content, error) - one will be None
52
+ """
53
+ try:
54
+ from yuho.parser.scanner import Scanner
55
+ from yuho.parser.parser import Parser
56
+ from yuho.ast.builder import ASTBuilder
57
+ from yuho.transpile.registry import TranspilerRegistry
58
+
59
+ source = path.read_text()
60
+
61
+ scanner = Scanner(source)
62
+ tokens = scanner.scan_tokens()
63
+
64
+ parser = Parser(tokens)
65
+ tree = parser.parse()
66
+
67
+ builder = ASTBuilder()
68
+ ast = builder.build(tree)
69
+
70
+ registry = TranspilerRegistry()
71
+ transpiler = registry.get(target)
72
+
73
+ if transpiler is None:
74
+ return None, f"Unknown target format: {target}"
75
+
76
+ result = transpiler.transpile(ast)
77
+ return result, None
78
+
79
+ except Exception as e:
80
+ return None, str(e)
81
+
82
+
83
+ def get_html_template(format_type: str) -> str:
84
+ """Get HTML template for preview."""
85
+
86
+ mermaid_script = ""
87
+ if format_type == "mermaid":
88
+ mermaid_script = """
89
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
90
+ <script>mermaid.initialize({startOnLoad: true, theme: 'dark'});</script>
91
+ """
92
+
93
+ return f"""<!DOCTYPE html>
94
+ <html lang="en">
95
+ <head>
96
+ <meta charset="UTF-8">
97
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
+ <title>Yuho Live Preview</title>
99
+ {mermaid_script}
100
+ <style>
101
+ :root {{
102
+ --bg-primary: #1a1a2e;
103
+ --bg-secondary: #16213e;
104
+ --text-primary: #eee;
105
+ --text-secondary: #aaa;
106
+ --accent: #4cc9f0;
107
+ --error: #ff6b6b;
108
+ --success: #4ecdc4;
109
+ }}
110
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
111
+ body {{
112
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
113
+ background: var(--bg-primary);
114
+ color: var(--text-primary);
115
+ line-height: 1.6;
116
+ min-height: 100vh;
117
+ }}
118
+ .header {{
119
+ background: var(--bg-secondary);
120
+ padding: 1rem 2rem;
121
+ border-bottom: 1px solid rgba(255,255,255,0.1);
122
+ display: flex;
123
+ justify-content: space-between;
124
+ align-items: center;
125
+ }}
126
+ .header h1 {{
127
+ font-size: 1.25rem;
128
+ color: var(--accent);
129
+ }}
130
+ .status {{
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.5rem;
134
+ font-size: 0.875rem;
135
+ color: var(--text-secondary);
136
+ }}
137
+ .status-dot {{
138
+ width: 8px;
139
+ height: 8px;
140
+ border-radius: 50%;
141
+ background: var(--success);
142
+ }}
143
+ .status-dot.error {{
144
+ background: var(--error);
145
+ }}
146
+ .container {{
147
+ max-width: 1200px;
148
+ margin: 0 auto;
149
+ padding: 2rem;
150
+ }}
151
+ .preview-content {{
152
+ background: var(--bg-secondary);
153
+ border-radius: 8px;
154
+ padding: 2rem;
155
+ white-space: pre-wrap;
156
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
157
+ font-size: 0.9rem;
158
+ overflow-x: auto;
159
+ }}
160
+ .preview-content.mermaid-container {{
161
+ background: #fff;
162
+ color: #333;
163
+ text-align: center;
164
+ }}
165
+ .error-message {{
166
+ background: rgba(255, 107, 107, 0.1);
167
+ border: 1px solid var(--error);
168
+ border-radius: 8px;
169
+ padding: 1rem;
170
+ color: var(--error);
171
+ margin-bottom: 1rem;
172
+ }}
173
+ .file-info {{
174
+ color: var(--text-secondary);
175
+ font-size: 0.875rem;
176
+ margin-bottom: 1rem;
177
+ }}
178
+ .mermaid {{
179
+ background: #fff;
180
+ }}
181
+ </style>
182
+ </head>
183
+ <body>
184
+ <div class="header">
185
+ <h1>📜 Yuho Live Preview</h1>
186
+ <div class="status">
187
+ <div class="status-dot" id="statusDot"></div>
188
+ <span id="statusText">Watching...</span>
189
+ </div>
190
+ </div>
191
+ <div class="container">
192
+ <div class="file-info" id="fileInfo"></div>
193
+ <div id="errorBox" class="error-message" style="display:none;"></div>
194
+ <div id="content" class="preview-content"></div>
195
+ </div>
196
+ <script>
197
+ let lastModified = 0;
198
+
199
+ async function fetchPreview() {{
200
+ try {{
201
+ const res = await fetch('/api/preview');
202
+ const data = await res.json();
203
+
204
+ const dot = document.getElementById('statusDot');
205
+ const status = document.getElementById('statusText');
206
+ const errorBox = document.getElementById('errorBox');
207
+ const content = document.getElementById('content');
208
+ const fileInfo = document.getElementById('fileInfo');
209
+
210
+ if (data.modified !== lastModified) {{
211
+ lastModified = data.modified;
212
+
213
+ fileInfo.textContent = 'File: ' + data.file + ' • Format: ' + data.format;
214
+
215
+ if (data.error) {{
216
+ dot.classList.add('error');
217
+ status.textContent = 'Error';
218
+ errorBox.textContent = data.error;
219
+ errorBox.style.display = 'block';
220
+ }} else {{
221
+ dot.classList.remove('error');
222
+ status.textContent = 'Updated ' + new Date().toLocaleTimeString();
223
+ errorBox.style.display = 'none';
224
+
225
+ if (data.format === 'mermaid') {{
226
+ content.className = 'preview-content mermaid-container';
227
+ content.innerHTML = '<pre class="mermaid">' + data.content + '</pre>';
228
+ if (window.mermaid) {{
229
+ mermaid.contentLoaded();
230
+ }}
231
+ }} else {{
232
+ content.className = 'preview-content';
233
+ content.textContent = data.content;
234
+ }}
235
+ }}
236
+ }}
237
+ }} catch (e) {{
238
+ console.error('Fetch error:', e);
239
+ }}
240
+ }}
241
+
242
+ // Poll for updates
243
+ setInterval(fetchPreview, 500);
244
+ fetchPreview();
245
+ </script>
246
+ </body>
247
+ </html>
248
+ """
249
+
250
+
251
+ class PreviewHandler(SimpleHTTPRequestHandler):
252
+ """HTTP handler for preview server."""
253
+
254
+ def log_message(self, format: str, *args) -> None:
255
+ """Suppress default logging."""
256
+ pass
257
+
258
+ def do_GET(self) -> None:
259
+ if self.path == "/" or self.path == "/index.html":
260
+ self.send_response(200)
261
+ self.send_header("Content-type", "text/html")
262
+ self.end_headers()
263
+ html = get_html_template(_preview_state.format)
264
+ self.wfile.write(html.encode())
265
+
266
+ elif self.path == "/api/preview":
267
+ self.send_response(200)
268
+ self.send_header("Content-type", "application/json")
269
+ self.send_header("Access-Control-Allow-Origin", "*")
270
+ self.end_headers()
271
+ self.wfile.write(json.dumps(_preview_state.to_dict()).encode())
272
+
273
+ else:
274
+ self.send_error(404)
275
+
276
+
277
+ def watch_file(path: Path, target: str, interval: float = 0.5) -> None:
278
+ """Watch file for changes and update preview state."""
279
+ global _preview_state
280
+
281
+ last_hash = ""
282
+
283
+ while True:
284
+ try:
285
+ if path.exists():
286
+ content = path.read_bytes()
287
+ current_hash = hashlib.md5(content).hexdigest()
288
+
289
+ if current_hash != last_hash:
290
+ last_hash = current_hash
291
+ _preview_state.last_modified = time.time()
292
+
293
+ result, error = transpile_file(path, target)
294
+
295
+ if error:
296
+ _preview_state.last_error = error
297
+ else:
298
+ _preview_state.content = result or ""
299
+ _preview_state.last_error = None
300
+
301
+ except Exception as e:
302
+ _preview_state.last_error = str(e)
303
+
304
+ time.sleep(interval)
305
+
306
+
307
+ def run_preview(
308
+ file: str,
309
+ target: str = "english",
310
+ port: int = 8000,
311
+ no_browser: bool = False,
312
+ verbose: bool = False,
313
+ color: bool = True,
314
+ ) -> None:
315
+ """
316
+ Run live preview server.
317
+
318
+ Args:
319
+ file: Input .yh file to watch
320
+ target: Transpile target (english, mermaid)
321
+ port: Server port
322
+ no_browser: Don't auto-open browser
323
+ verbose: Verbose output
324
+ color: Use colors
325
+ """
326
+ global _preview_state
327
+
328
+ path = Path(file)
329
+
330
+ if not path.exists():
331
+ click.echo(f"Error: File not found: {file}", err=True)
332
+ raise SystemExit(1)
333
+
334
+ # Validate target
335
+ valid_targets = ["english", "mermaid"]
336
+ if target not in valid_targets:
337
+ click.echo(f"Error: Preview supports: {', '.join(valid_targets)}", err=True)
338
+ raise SystemExit(1)
339
+
340
+ # Initialize state
341
+ _preview_state.file_path = str(path)
342
+ _preview_state.format = target
343
+
344
+ # Initial transpile
345
+ result, error = transpile_file(path, target)
346
+ if error:
347
+ _preview_state.last_error = error
348
+ else:
349
+ _preview_state.content = result or ""
350
+ _preview_state.last_modified = time.time()
351
+
352
+ # Start file watcher thread
353
+ watcher = threading.Thread(target=watch_file, args=(path, target), daemon=True)
354
+ watcher.start()
355
+
356
+ # Start server
357
+ server = HTTPServer(("127.0.0.1", port), PreviewHandler)
358
+
359
+ url = f"http://127.0.0.1:{port}"
360
+
361
+ click.echo(click.style("🔴 Yuho Live Preview", fg="cyan", bold=True))
362
+ click.echo(f" Watching: {click.style(str(path), fg='yellow')}")
363
+ click.echo(f" Format: {click.style(target, fg='green')}")
364
+ click.echo(f" Server: {click.style(url, fg='blue')}")
365
+ click.echo("")
366
+ click.echo(" Press Ctrl+C to stop")
367
+ click.echo("")
368
+
369
+ # Open browser
370
+ if not no_browser:
371
+ webbrowser.open(url)
372
+
373
+ try:
374
+ server.serve_forever()
375
+ except KeyboardInterrupt:
376
+ click.echo("\n\nStopping preview server...")
377
+ server.shutdown()