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,444 @@
1
+ """
2
+ REPL command - interactive statute experimentation.
3
+
4
+ Provides an interactive Read-Eval-Print Loop for:
5
+ - Parsing and validating Yuho code snippets
6
+ - Transpiling to various targets
7
+ - Exploring statute definitions
8
+ - Testing legal logic
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import readline
14
+ from pathlib import Path
15
+ from typing import Optional, List, Dict, Any
16
+
17
+ import click
18
+
19
+ from yuho.parser import Parser
20
+ from yuho.ast import ASTBuilder
21
+ from yuho.ast.nodes import ModuleNode
22
+ from yuho.transpile.base import TranspileTarget
23
+ from yuho.transpile.registry import TranspilerRegistry
24
+ from yuho.cli.error_formatter import format_errors, Colors, colorize
25
+
26
+
27
+ class YuhoREPL:
28
+ """
29
+ Interactive REPL for Yuho statute language.
30
+
31
+ Supports:
32
+ - Multi-line input with continuation
33
+ - Command history (via readline)
34
+ - Transpilation to any target
35
+ - AST inspection
36
+ - Statute exploration
37
+ """
38
+
39
+ PROMPT = "yuho> "
40
+ CONTINUATION_PROMPT = " ... "
41
+
42
+ COMMANDS = {
43
+ "help": "Show this help message",
44
+ "exit": "Exit the REPL (also: quit, Ctrl+D)",
45
+ "clear": "Clear the screen",
46
+ "history": "Show command history",
47
+ "load <file>": "Load and parse a .yh file",
48
+ "transpile <target>": "Transpile last valid input (json, english, mermaid, latex, graphql)",
49
+ "ast": "Show AST of last valid input",
50
+ "targets": "List available transpile targets",
51
+ "reset": "Clear session state",
52
+ }
53
+
54
+ def __init__(self, color: bool = True, verbose: bool = False):
55
+ """
56
+ Initialize the REPL.
57
+
58
+ Args:
59
+ color: Whether to use colored output
60
+ verbose: Enable verbose mode
61
+ """
62
+ self.color = color
63
+ self.verbose = verbose
64
+ self.parser = Parser()
65
+ self.registry = TranspilerRegistry.instance()
66
+
67
+ # Session state
68
+ self.history: List[str] = []
69
+ self.last_ast: Optional[ModuleNode] = None
70
+ self.last_source: str = ""
71
+ self.session_statutes: Dict[str, Any] = {}
72
+
73
+ # Setup readline history
74
+ self._setup_readline()
75
+
76
+ def _setup_readline(self) -> None:
77
+ """Configure readline for history and completion."""
78
+ # History file
79
+ history_file = Path.home() / ".yuho_history"
80
+ try:
81
+ readline.read_history_file(history_file)
82
+ except FileNotFoundError:
83
+ pass
84
+
85
+ # Save history on exit
86
+ import atexit
87
+ atexit.register(lambda: readline.write_history_file(history_file))
88
+
89
+ # Set history length
90
+ readline.set_history_length(1000)
91
+
92
+ def _colorize(self, text: str, color: str) -> str:
93
+ """Apply color if enabled."""
94
+ if not self.color:
95
+ return text
96
+ return colorize(text, color)
97
+
98
+ def _print_banner(self) -> None:
99
+ """Print welcome banner."""
100
+ banner = f"""
101
+ {self._colorize("Yuho REPL", Colors.CYAN)} - Interactive statute experimentation
102
+ Type {self._colorize("help", Colors.YELLOW)} for commands, {self._colorize("exit", Colors.YELLOW)} to quit
103
+ """
104
+ print(banner.strip())
105
+ print()
106
+
107
+ def _print_help(self) -> None:
108
+ """Print help message."""
109
+ print(f"\n{self._colorize('Commands:', Colors.CYAN)}")
110
+ for cmd, desc in self.COMMANDS.items():
111
+ print(f" {self._colorize(cmd, Colors.YELLOW):20s} {desc}")
112
+
113
+ print(f"\n{self._colorize('Yuho Syntax Examples:', Colors.CYAN)}")
114
+ examples = [
115
+ 'statute "299" "Culpable Homicide" { ... }',
116
+ 'struct Person { name: string, age: int }',
117
+ 'fn is_adult(age: int) -> bool { return age >= 18; }',
118
+ ]
119
+ for ex in examples:
120
+ print(f" {ex}")
121
+
122
+ print(f"\n{self._colorize('Multi-line Input:', Colors.CYAN)}")
123
+ print(" End a line with \\ to continue on the next line")
124
+ print(" Or use matching braces { } for blocks")
125
+ print()
126
+
127
+ def _print_targets(self) -> None:
128
+ """Print available transpile targets."""
129
+ print(f"\n{self._colorize('Available Targets:', Colors.CYAN)}")
130
+ for target in TranspileTarget:
131
+ name = target.name.lower()
132
+ ext = target.file_extension
133
+ print(f" {self._colorize(name, Colors.YELLOW):12s} -> {ext}")
134
+ print()
135
+
136
+ def _read_input(self) -> Optional[str]:
137
+ """
138
+ Read input, handling multi-line continuation.
139
+
140
+ Returns:
141
+ Complete input string, or None on EOF
142
+ """
143
+ lines: List[str] = []
144
+ prompt = self.PROMPT
145
+ brace_depth = 0
146
+
147
+ while True:
148
+ try:
149
+ line = input(self._colorize(prompt, Colors.GREEN))
150
+ except EOFError:
151
+ if lines:
152
+ print() # Newline after incomplete input
153
+ return None
154
+ except KeyboardInterrupt:
155
+ print(f"\n{self._colorize('(Use exit to quit)', Colors.YELLOW)}")
156
+ return ""
157
+
158
+ # Track brace depth for multi-line blocks
159
+ brace_depth += line.count("{") - line.count("}")
160
+
161
+ # Check for line continuation
162
+ if line.endswith("\\"):
163
+ lines.append(line[:-1])
164
+ prompt = self.CONTINUATION_PROMPT
165
+ continue
166
+
167
+ lines.append(line)
168
+
169
+ # Continue if braces are unbalanced
170
+ if brace_depth > 0:
171
+ prompt = self.CONTINUATION_PROMPT
172
+ continue
173
+
174
+ break
175
+
176
+ return "\n".join(lines)
177
+
178
+ def _handle_command(self, cmd: str) -> bool:
179
+ """
180
+ Handle a REPL command.
181
+
182
+ Args:
183
+ cmd: The command string (without leading colon)
184
+
185
+ Returns:
186
+ True if command was handled, False otherwise
187
+ """
188
+ parts = cmd.strip().split(maxsplit=1)
189
+ command = parts[0].lower()
190
+ arg = parts[1] if len(parts) > 1 else ""
191
+
192
+ if command in ("exit", "quit"):
193
+ print("Goodbye!")
194
+ return True # Signal exit
195
+
196
+ if command == "help":
197
+ self._print_help()
198
+ return False
199
+
200
+ if command == "clear":
201
+ os.system("clear" if os.name == "posix" else "cls")
202
+ return False
203
+
204
+ if command == "history":
205
+ if not self.history:
206
+ print(self._colorize("No history yet", Colors.YELLOW))
207
+ else:
208
+ print(f"\n{self._colorize('History:', Colors.CYAN)}")
209
+ for i, h in enumerate(self.history[-20:], 1):
210
+ preview = h[:60] + "..." if len(h) > 60 else h
211
+ print(f" {i:2d}: {preview}")
212
+ print()
213
+ return False
214
+
215
+ if command == "targets":
216
+ self._print_targets()
217
+ return False
218
+
219
+ if command == "reset":
220
+ self.last_ast = None
221
+ self.last_source = ""
222
+ self.session_statutes.clear()
223
+ print(self._colorize("Session state cleared", Colors.GREEN))
224
+ return False
225
+
226
+ if command == "ast":
227
+ if not self.last_ast:
228
+ print(self._colorize("No valid AST available. Parse some code first.", Colors.YELLOW))
229
+ else:
230
+ self._print_ast(self.last_ast)
231
+ return False
232
+
233
+ if command == "load":
234
+ if not arg:
235
+ print(self._colorize("Usage: load <file.yh>", Colors.RED))
236
+ else:
237
+ self._load_file(arg)
238
+ return False
239
+
240
+ if command == "transpile":
241
+ if not arg:
242
+ print(self._colorize("Usage: transpile <target>", Colors.RED))
243
+ self._print_targets()
244
+ else:
245
+ self._transpile(arg)
246
+ return False
247
+
248
+ # Unknown command - try parsing as code
249
+ return False
250
+
251
+ def _load_file(self, filepath: str) -> None:
252
+ """Load and parse a Yuho file."""
253
+ path = Path(filepath).expanduser()
254
+
255
+ if not path.exists():
256
+ print(self._colorize(f"File not found: {filepath}", Colors.RED))
257
+ return
258
+
259
+ try:
260
+ source = path.read_text(encoding="utf-8")
261
+ self._parse_and_validate(source, str(path))
262
+ except Exception as e:
263
+ print(self._colorize(f"Error loading file: {e}", Colors.RED))
264
+
265
+ def _parse_and_validate(self, source: str, filename: str = "<repl>") -> bool:
266
+ """
267
+ Parse and validate Yuho source code.
268
+
269
+ Args:
270
+ source: The source code
271
+ filename: Filename for error messages
272
+
273
+ Returns:
274
+ True if parsing succeeded
275
+ """
276
+ result = self.parser.parse_string(source, filename)
277
+
278
+ if result.errors:
279
+ print(self._colorize("Parse errors:", Colors.RED))
280
+ for err in result.errors[:5]: # Limit to 5 errors
281
+ loc = f"{err.location.line}:{err.location.col}" if err.location else "?"
282
+ print(f" [{loc}] {err.message}")
283
+ if len(result.errors) > 5:
284
+ print(f" ... and {len(result.errors) - 5} more errors")
285
+ return False
286
+
287
+ # Build AST
288
+ try:
289
+ builder = ASTBuilder()
290
+ ast = builder.build(result.tree)
291
+
292
+ self.last_ast = ast
293
+ self.last_source = source
294
+
295
+ # Track statutes in session
296
+ for statute in ast.statutes:
297
+ self.session_statutes[statute.section_number] = statute
298
+
299
+ # Success message
300
+ stats = []
301
+ if ast.statutes:
302
+ stats.append(f"{len(ast.statutes)} statute(s)")
303
+ if ast.type_defs:
304
+ stats.append(f"{len(ast.type_defs)} type(s)")
305
+ if ast.function_defs:
306
+ stats.append(f"{len(ast.function_defs)} function(s)")
307
+
308
+ if stats:
309
+ print(self._colorize(f"✓ Parsed: {', '.join(stats)}", Colors.GREEN))
310
+ else:
311
+ print(self._colorize("✓ Valid (empty module)", Colors.GREEN))
312
+
313
+ return True
314
+
315
+ except Exception as e:
316
+ print(self._colorize(f"AST build error: {e}", Colors.RED))
317
+ if self.verbose:
318
+ import traceback
319
+ traceback.print_exc()
320
+ return False
321
+
322
+ def _transpile(self, target_name: str) -> None:
323
+ """Transpile last valid AST to a target format."""
324
+ if not self.last_ast:
325
+ print(self._colorize("No valid AST available. Parse some code first.", Colors.YELLOW))
326
+ return
327
+
328
+ try:
329
+ target = TranspileTarget.from_string(target_name)
330
+ except ValueError:
331
+ print(self._colorize(f"Unknown target: {target_name}", Colors.RED))
332
+ self._print_targets()
333
+ return
334
+
335
+ try:
336
+ transpiler = self.registry.get(target)
337
+ output = transpiler.transpile(self.last_ast)
338
+
339
+ print(f"\n{self._colorize(f'=== {target.name} Output ===', Colors.CYAN)}")
340
+ print(output)
341
+ print()
342
+
343
+ except Exception as e:
344
+ print(self._colorize(f"Transpilation error: {e}", Colors.RED))
345
+ if self.verbose:
346
+ import traceback
347
+ traceback.print_exc()
348
+
349
+ def _print_ast(self, ast: ModuleNode) -> None:
350
+ """Print a simple representation of the AST."""
351
+ print(f"\n{self._colorize('=== AST ===', Colors.CYAN)}")
352
+
353
+ if ast.imports:
354
+ print(f"Imports: {len(ast.imports)}")
355
+ for imp in ast.imports:
356
+ print(f" - {imp.path}")
357
+
358
+ if ast.type_defs:
359
+ print(f"Types: {len(ast.type_defs)}")
360
+ for td in ast.type_defs:
361
+ fields = ", ".join(f.name for f in td.fields)
362
+ print(f" - struct {td.name} {{ {fields} }}")
363
+
364
+ if ast.function_defs:
365
+ print(f"Functions: {len(ast.function_defs)}")
366
+ for fn in ast.function_defs:
367
+ params = ", ".join(p.name for p in fn.params)
368
+ print(f" - fn {fn.name}({params})")
369
+
370
+ if ast.statutes:
371
+ print(f"Statutes: {len(ast.statutes)}")
372
+ for st in ast.statutes:
373
+ title = st.title.value if st.title else "(untitled)"
374
+ print(f" - Section {st.section_number}: {title}")
375
+ if st.elements:
376
+ print(f" Elements: {len(st.elements)}")
377
+ if st.penalty:
378
+ print(f" Has penalty")
379
+ if st.illustrations:
380
+ print(f" Illustrations: {len(st.illustrations)}")
381
+
382
+ print()
383
+
384
+ def run(self) -> int:
385
+ """
386
+ Run the REPL loop.
387
+
388
+ Returns:
389
+ Exit code (0 for success)
390
+ """
391
+ self._print_banner()
392
+
393
+ while True:
394
+ try:
395
+ source = self._read_input()
396
+
397
+ if source is None:
398
+ # EOF
399
+ print("\nGoodbye!")
400
+ break
401
+
402
+ if not source.strip():
403
+ continue
404
+
405
+ # Add to history
406
+ self.history.append(source)
407
+
408
+ # Check if it's a command
409
+ if source.strip() in self.COMMANDS or source.split()[0] in [
410
+ "help", "exit", "quit", "clear", "history", "load",
411
+ "transpile", "ast", "targets", "reset"
412
+ ]:
413
+ if self._handle_command(source):
414
+ break # Exit command
415
+ continue
416
+
417
+ # Try to parse as Yuho code
418
+ self._parse_and_validate(source)
419
+
420
+ except KeyboardInterrupt:
421
+ print(f"\n{self._colorize('(Use exit to quit)', Colors.YELLOW)}")
422
+ continue
423
+ except Exception as e:
424
+ print(self._colorize(f"Error: {e}", Colors.RED))
425
+ if self.verbose:
426
+ import traceback
427
+ traceback.print_exc()
428
+
429
+ return 0
430
+
431
+
432
+ def run_repl(color: bool = True, verbose: bool = False) -> int:
433
+ """
434
+ Entry point for the REPL command.
435
+
436
+ Args:
437
+ color: Whether to use colored output
438
+ verbose: Enable verbose mode
439
+
440
+ Returns:
441
+ Exit code
442
+ """
443
+ repl = YuhoREPL(color=color, verbose=verbose)
444
+ return repl.run()
@@ -0,0 +1,44 @@
1
+ """
2
+ Serve command - start MCP server.
3
+ """
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ import click
9
+
10
+ from yuho.cli.error_formatter import Colors, colorize
11
+
12
+
13
+ def run_serve(port: int = 8080, host: str = "127.0.0.1", stdio: bool = False, verbose: bool = False) -> None:
14
+ """
15
+ Start the MCP (Model Context Protocol) server.
16
+
17
+ Args:
18
+ port: Port to listen on
19
+ host: Host to bind to
20
+ stdio: Use stdio transport
21
+ verbose: Enable verbose output
22
+ """
23
+ try:
24
+ from yuho.mcp import create_server
25
+ except ImportError:
26
+ click.echo(colorize("error: MCP module not available. Install with: pip install yuho[mcp]", Colors.RED), err=True)
27
+ sys.exit(1)
28
+
29
+ server = create_server()
30
+
31
+ if stdio:
32
+ if verbose:
33
+ click.echo("Starting MCP server on stdio...")
34
+ server.run_stdio()
35
+ else:
36
+ if verbose:
37
+ click.echo(f"Starting MCP server on {host}:{port}...")
38
+ click.echo(colorize(f"Yuho MCP server listening on http://{host}:{port}", Colors.CYAN + Colors.BOLD))
39
+ click.echo(colorize("Press Ctrl+C to stop", Colors.DIM))
40
+
41
+ try:
42
+ server.run_http(host=host, port=port)
43
+ except KeyboardInterrupt:
44
+ click.echo("\nShutting down...")