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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- 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
|