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,193 @@
1
+ """
2
+ Rich error formatting for CLI output.
3
+
4
+ Provides source line display with carets pointing to error locations,
5
+ and Levenshtein-based suggestions for typos.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import List, Optional, Tuple
11
+
12
+ from yuho.parser.source_location import SourceLocation
13
+ from yuho.parser.wrapper import ParseError
14
+
15
+
16
+ # Global color control - can be set by CLI
17
+ # None = auto-detect, True = force on, False = force off
18
+ COLOR_ENABLED: Optional[bool] = None
19
+
20
+
21
+ # ANSI color codes
22
+ class Colors:
23
+ RED = "\033[91m"
24
+ YELLOW = "\033[93m"
25
+ BLUE = "\033[94m"
26
+ CYAN = "\033[96m"
27
+ GREEN = "\033[92m"
28
+ BOLD = "\033[1m"
29
+ DIM = "\033[2m"
30
+ RESET = "\033[0m"
31
+
32
+
33
+ def supports_color() -> bool:
34
+ """
35
+ Check if terminal supports color.
36
+
37
+ Respects the global COLOR_ENABLED flag if set,
38
+ otherwise auto-detects based on terminal capabilities.
39
+ """
40
+ global COLOR_ENABLED
41
+
42
+ # If explicitly set, use that value
43
+ if COLOR_ENABLED is not None:
44
+ return COLOR_ENABLED
45
+
46
+ # Auto-detect
47
+ if not hasattr(sys.stdout, "isatty"):
48
+ return False
49
+ if not sys.stdout.isatty():
50
+ return False
51
+ return True
52
+
53
+
54
+ def colorize(text: str, color: str) -> str:
55
+ """Apply color if supported."""
56
+ if supports_color():
57
+ return f"{color}{text}{Colors.RESET}"
58
+ return text
59
+
60
+
61
+
62
+ def format_error(error: ParseError, source: str) -> str:
63
+ """
64
+ Format a parse error with source context.
65
+
66
+ Shows the error location with the source line and a caret
67
+ pointing to the exact position.
68
+ """
69
+ lines = source.splitlines()
70
+ loc = error.location
71
+
72
+ output: List[str] = []
73
+
74
+ # Error header
75
+ header = f"{loc}: {error.message}"
76
+ output.append(colorize(f"error: {header}", Colors.RED + Colors.BOLD))
77
+
78
+ # Source context (line before, error line, line after)
79
+ if 1 <= loc.line <= len(lines):
80
+ # Line before
81
+ if loc.line > 1:
82
+ line_before = lines[loc.line - 2]
83
+ output.append(f" {colorize(str(loc.line - 1).rjust(4), Colors.DIM)} | {line_before}")
84
+
85
+ # Error line
86
+ error_line = lines[loc.line - 1]
87
+ output.append(f" {colorize(str(loc.line).rjust(4), Colors.CYAN)} | {error_line}")
88
+
89
+ # Caret line
90
+ caret_padding = " " * (loc.col - 1 + 7) # 7 = " XXXX | "
91
+ if loc.end_col > loc.col:
92
+ caret = "^" * (loc.end_col - loc.col)
93
+ else:
94
+ caret = "^"
95
+ output.append(colorize(f"{caret_padding}{caret}", Colors.RED))
96
+
97
+ # Line after
98
+ if loc.line < len(lines):
99
+ line_after = lines[loc.line]
100
+ output.append(f" {colorize(str(loc.line + 1).rjust(4), Colors.DIM)} | {line_after}")
101
+
102
+ return "\n".join(output)
103
+
104
+
105
+ def format_errors(errors: List[ParseError], source: str, file: str) -> str:
106
+ """Format multiple errors."""
107
+ if not errors:
108
+ return ""
109
+
110
+ output: List[str] = []
111
+ output.append(colorize(f"Found {len(errors)} error(s) in {file}:", Colors.BOLD))
112
+ output.append("")
113
+
114
+ for error in errors:
115
+ output.append(format_error(error, source))
116
+ output.append("")
117
+
118
+ return "\n".join(output)
119
+
120
+
121
+ # =============================================================================
122
+ # Levenshtein distance for typo suggestions
123
+ # =============================================================================
124
+
125
+
126
+ def levenshtein_distance(s1: str, s2: str) -> int:
127
+ """Calculate the Levenshtein (edit) distance between two strings."""
128
+ if len(s1) < len(s2):
129
+ return levenshtein_distance(s2, s1)
130
+
131
+ if len(s2) == 0:
132
+ return len(s1)
133
+
134
+ previous_row = range(len(s2) + 1)
135
+ for i, c1 in enumerate(s1):
136
+ current_row = [i + 1]
137
+ for j, c2 in enumerate(s2):
138
+ insertions = previous_row[j + 1] + 1
139
+ deletions = current_row[j] + 1
140
+ substitutions = previous_row[j] + (c1 != c2)
141
+ current_row.append(min(insertions, deletions, substitutions))
142
+ previous_row = current_row
143
+
144
+ return previous_row[-1]
145
+
146
+
147
+ def find_similar(word: str, candidates: List[str], max_distance: int = 2) -> List[str]:
148
+ """Find candidates similar to word within max_distance."""
149
+ similar = []
150
+ for candidate in candidates:
151
+ dist = levenshtein_distance(word.lower(), candidate.lower())
152
+ if dist <= max_distance:
153
+ similar.append((dist, candidate))
154
+
155
+ # Sort by distance
156
+ similar.sort(key=lambda x: x[0])
157
+ return [s[1] for s in similar]
158
+
159
+
160
+ # Known keywords and types for suggestions
161
+ YUHO_KEYWORDS = [
162
+ "struct", "fn", "match", "case", "consequence", "pass", "return",
163
+ "statute", "definitions", "elements", "penalty", "illustration",
164
+ "import", "from", "actus_reus", "mens_rea", "circumstance",
165
+ "imprisonment", "fine", "supplementary", "TRUE", "FALSE",
166
+ ]
167
+
168
+ YUHO_TYPES = [
169
+ "int", "float", "bool", "string", "money", "percent", "date", "duration", "void",
170
+ ]
171
+
172
+
173
+ def suggest_keyword(typo: str) -> Optional[str]:
174
+ """Suggest a correct keyword for a typo."""
175
+ candidates = YUHO_KEYWORDS + YUHO_TYPES
176
+ similar = find_similar(typo, candidates, max_distance=2)
177
+ return similar[0] if similar else None
178
+
179
+
180
+ def format_suggestion(error: ParseError, source: str) -> Optional[str]:
181
+ """Generate a suggestion for fixing an error."""
182
+ # Try to extract the problematic token from the error
183
+ if "Unexpected" in error.message:
184
+ # Extract the token text
185
+ import re
186
+ match = re.search(r"Unexpected syntax: ['\"]?([^'\"]+)['\"]?", error.message)
187
+ if match:
188
+ token = match.group(1).strip()
189
+ suggestion = suggest_keyword(token)
190
+ if suggestion:
191
+ return f"Did you mean '{suggestion}'?"
192
+
193
+ return None