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,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST visualization command for Yuho.
|
|
3
|
+
|
|
4
|
+
Displays statute AST structure as tree-like ASCII output in terminal.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Box-drawing characters for tree
|
|
14
|
+
TREE_CHARS = {
|
|
15
|
+
"pipe": "│",
|
|
16
|
+
"tee": "├",
|
|
17
|
+
"elbow": "└",
|
|
18
|
+
"dash": "─",
|
|
19
|
+
"space": " ",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Minimal fallback for terminals without unicode
|
|
23
|
+
TREE_CHARS_ASCII = {
|
|
24
|
+
"pipe": "|",
|
|
25
|
+
"tee": "+",
|
|
26
|
+
"elbow": "`",
|
|
27
|
+
"dash": "-",
|
|
28
|
+
"space": " ",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ASTVisualizer:
|
|
33
|
+
"""Generates tree visualization of Yuho AST."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, use_color: bool = True, use_unicode: bool = True):
|
|
36
|
+
self.use_color = use_color
|
|
37
|
+
self.chars = TREE_CHARS if use_unicode else TREE_CHARS_ASCII
|
|
38
|
+
self.lines: List[str] = []
|
|
39
|
+
|
|
40
|
+
def colorize(self, text: str, color: str, bold: bool = False) -> str:
|
|
41
|
+
"""Apply color if enabled."""
|
|
42
|
+
if not self.use_color:
|
|
43
|
+
return text
|
|
44
|
+
return click.style(text, fg=color, bold=bold)
|
|
45
|
+
|
|
46
|
+
def _get_node_display(self, node: Any) -> Tuple[str, str]:
|
|
47
|
+
"""
|
|
48
|
+
Get display text and color for a node.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
(display_text, color)
|
|
52
|
+
"""
|
|
53
|
+
node_type = type(node).__name__
|
|
54
|
+
|
|
55
|
+
# Map node types to colors and display format
|
|
56
|
+
type_info = {
|
|
57
|
+
"Statute": ("cyan", True),
|
|
58
|
+
"Section": ("blue", True),
|
|
59
|
+
"Definitions": ("yellow", False),
|
|
60
|
+
"Definition": ("yellow", False),
|
|
61
|
+
"Elements": ("green", False),
|
|
62
|
+
"Element": ("green", False),
|
|
63
|
+
"ActusReus": ("green", False),
|
|
64
|
+
"MensRea": ("green", False),
|
|
65
|
+
"Circumstance": ("green", False),
|
|
66
|
+
"Penalty": ("red", False),
|
|
67
|
+
"PenaltyClause": ("red", False),
|
|
68
|
+
"Imprisonment": ("red", False),
|
|
69
|
+
"Fine": ("red", False),
|
|
70
|
+
"Illustrations": ("magenta", False),
|
|
71
|
+
"Illustration": ("magenta", False),
|
|
72
|
+
"Import": ("white", False),
|
|
73
|
+
"TypeDecl": ("cyan", False),
|
|
74
|
+
"FuncDecl": ("cyan", False),
|
|
75
|
+
"Block": ("white", False),
|
|
76
|
+
"Program": ("cyan", True),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
color, bold = type_info.get(node_type, ("white", False))
|
|
80
|
+
|
|
81
|
+
# Build display text with node-specific info
|
|
82
|
+
display = node_type
|
|
83
|
+
|
|
84
|
+
if hasattr(node, "section_number"):
|
|
85
|
+
display = f"{node_type}[{node.section_number}]"
|
|
86
|
+
elif hasattr(node, "title") and node.title:
|
|
87
|
+
title = node.title[:30] + "..." if len(str(node.title)) > 30 else node.title
|
|
88
|
+
display = f"{node_type}: {title}"
|
|
89
|
+
elif hasattr(node, "name") and node.name:
|
|
90
|
+
display = f"{node_type}: {node.name}"
|
|
91
|
+
elif hasattr(node, "term") and node.term:
|
|
92
|
+
display = f"{node_type}: {node.term}"
|
|
93
|
+
elif hasattr(node, "label") and node.label:
|
|
94
|
+
display = f"{node_type}: {node.label}"
|
|
95
|
+
elif hasattr(node, "element_type"):
|
|
96
|
+
display = f"{node_type}({node.element_type})"
|
|
97
|
+
|
|
98
|
+
return display, color
|
|
99
|
+
|
|
100
|
+
def _get_children(self, node: Any) -> List[Tuple[str, Any]]:
|
|
101
|
+
"""
|
|
102
|
+
Get child nodes with their attribute names.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of (attribute_name, child_node) tuples
|
|
106
|
+
"""
|
|
107
|
+
children = []
|
|
108
|
+
|
|
109
|
+
# Known container attributes
|
|
110
|
+
container_attrs = [
|
|
111
|
+
"statutes", "sections", "statements", "items", "children",
|
|
112
|
+
"definitions", "elements", "illustrations", "penalties",
|
|
113
|
+
"clauses", "imports", "declarations", "blocks", "body",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
# Check for container attributes first
|
|
117
|
+
for attr in container_attrs:
|
|
118
|
+
if hasattr(node, attr):
|
|
119
|
+
items = getattr(node, attr)
|
|
120
|
+
if isinstance(items, (list, tuple)):
|
|
121
|
+
for i, item in enumerate(items):
|
|
122
|
+
if item is not None:
|
|
123
|
+
children.append((f"{attr}[{i}]", item))
|
|
124
|
+
|
|
125
|
+
# Check for single child attributes
|
|
126
|
+
single_attrs = [
|
|
127
|
+
"statute", "section", "penalty", "definition", "element",
|
|
128
|
+
"illustration", "actus_reus", "mens_rea", "circumstance",
|
|
129
|
+
"imprisonment", "fine", "supplementary", "condition",
|
|
130
|
+
"consequence", "left", "right", "value", "expression",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for attr in single_attrs:
|
|
134
|
+
if hasattr(node, attr):
|
|
135
|
+
child = getattr(node, attr)
|
|
136
|
+
if child is not None and not isinstance(child, (str, int, float, bool)):
|
|
137
|
+
children.append((attr, child))
|
|
138
|
+
|
|
139
|
+
return children
|
|
140
|
+
|
|
141
|
+
def _render_node(
|
|
142
|
+
self,
|
|
143
|
+
node: Any,
|
|
144
|
+
prefix: str = "",
|
|
145
|
+
is_last: bool = True,
|
|
146
|
+
show_attr: str = "",
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Recursively render a node and its children."""
|
|
149
|
+
# Determine connector
|
|
150
|
+
if prefix:
|
|
151
|
+
connector = self.chars["elbow"] if is_last else self.chars["tee"]
|
|
152
|
+
connector += self.chars["dash"] * 2 + " "
|
|
153
|
+
else:
|
|
154
|
+
connector = ""
|
|
155
|
+
|
|
156
|
+
# Get display text
|
|
157
|
+
display, color = self._get_node_display(node)
|
|
158
|
+
|
|
159
|
+
# Add attribute name if provided
|
|
160
|
+
if show_attr:
|
|
161
|
+
attr_text = self.colorize(f"{show_attr}: ", "white")
|
|
162
|
+
else:
|
|
163
|
+
attr_text = ""
|
|
164
|
+
|
|
165
|
+
# Build line
|
|
166
|
+
node_text = self.colorize(display, color, bold=(color == "cyan"))
|
|
167
|
+
self.lines.append(f"{prefix}{connector}{attr_text}{node_text}")
|
|
168
|
+
|
|
169
|
+
# Get children
|
|
170
|
+
children = self._get_children(node)
|
|
171
|
+
|
|
172
|
+
# Calculate new prefix for children
|
|
173
|
+
if prefix:
|
|
174
|
+
if is_last:
|
|
175
|
+
new_prefix = prefix + " "
|
|
176
|
+
else:
|
|
177
|
+
new_prefix = prefix + self.chars["pipe"] + " "
|
|
178
|
+
else:
|
|
179
|
+
new_prefix = ""
|
|
180
|
+
|
|
181
|
+
# Render children
|
|
182
|
+
for i, (attr, child) in enumerate(children):
|
|
183
|
+
is_last_child = (i == len(children) - 1)
|
|
184
|
+
self._render_node(child, new_prefix, is_last_child, attr)
|
|
185
|
+
|
|
186
|
+
def visualize(self, ast: Any) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Generate tree visualization of AST.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
ast: The AST root node
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
String containing the tree visualization
|
|
195
|
+
"""
|
|
196
|
+
self.lines = []
|
|
197
|
+
|
|
198
|
+
# Handle list of nodes
|
|
199
|
+
if isinstance(ast, (list, tuple)):
|
|
200
|
+
for i, node in enumerate(ast):
|
|
201
|
+
is_last = (i == len(ast) - 1)
|
|
202
|
+
self._render_node(node, "", is_last)
|
|
203
|
+
else:
|
|
204
|
+
self._render_node(ast, "", True)
|
|
205
|
+
|
|
206
|
+
return "\n".join(self.lines)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def format_ast_stats(ast: Any) -> Dict[str, int]:
|
|
210
|
+
"""
|
|
211
|
+
Collect statistics about the AST.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict mapping node type to count
|
|
215
|
+
"""
|
|
216
|
+
stats: Dict[str, int] = {}
|
|
217
|
+
|
|
218
|
+
def visit(node: Any) -> None:
|
|
219
|
+
if node is None:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
if isinstance(node, (list, tuple)):
|
|
223
|
+
for item in node:
|
|
224
|
+
visit(item)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Count this node type
|
|
228
|
+
node_type = type(node).__name__
|
|
229
|
+
stats[node_type] = stats.get(node_type, 0) + 1
|
|
230
|
+
|
|
231
|
+
# Visit all attributes that might contain children
|
|
232
|
+
for attr in dir(node):
|
|
233
|
+
if attr.startswith("_"):
|
|
234
|
+
continue
|
|
235
|
+
try:
|
|
236
|
+
value = getattr(node, attr)
|
|
237
|
+
if isinstance(value, (list, tuple)):
|
|
238
|
+
for item in value:
|
|
239
|
+
if hasattr(item, "__class__") and not isinstance(item, (str, int, float, bool)):
|
|
240
|
+
visit(item)
|
|
241
|
+
elif hasattr(value, "__class__") and not isinstance(value, (str, int, float, bool, type(None))):
|
|
242
|
+
if not callable(value):
|
|
243
|
+
visit(value)
|
|
244
|
+
except (AttributeError, TypeError):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
visit(ast)
|
|
248
|
+
return stats
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def run_ast_viz(
|
|
252
|
+
file: str,
|
|
253
|
+
output: Optional[str] = None,
|
|
254
|
+
stats: bool = False,
|
|
255
|
+
depth: int = 0,
|
|
256
|
+
no_unicode: bool = False,
|
|
257
|
+
verbose: bool = False,
|
|
258
|
+
color: bool = True,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Run AST visualization command.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
file: Input .yh file
|
|
265
|
+
output: Output file path (None = stdout)
|
|
266
|
+
stats: Show statistics
|
|
267
|
+
depth: Max depth (0 = unlimited)
|
|
268
|
+
no_unicode: Use ASCII-only characters
|
|
269
|
+
verbose: Verbose output
|
|
270
|
+
color: Use colors
|
|
271
|
+
"""
|
|
272
|
+
path = Path(file)
|
|
273
|
+
|
|
274
|
+
if not path.exists():
|
|
275
|
+
click.echo(f"Error: File not found: {file}", err=True)
|
|
276
|
+
raise SystemExit(1)
|
|
277
|
+
|
|
278
|
+
# Read source
|
|
279
|
+
source = path.read_text()
|
|
280
|
+
|
|
281
|
+
# Parse
|
|
282
|
+
try:
|
|
283
|
+
from yuho.parser.scanner import Scanner
|
|
284
|
+
from yuho.parser.parser import Parser
|
|
285
|
+
from yuho.ast.builder import ASTBuilder
|
|
286
|
+
|
|
287
|
+
scanner = Scanner(source)
|
|
288
|
+
tokens = scanner.scan_tokens()
|
|
289
|
+
|
|
290
|
+
parser = Parser(tokens)
|
|
291
|
+
tree = parser.parse()
|
|
292
|
+
|
|
293
|
+
builder = ASTBuilder()
|
|
294
|
+
ast = builder.build(tree)
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
click.echo(f"Parse error: {e}", err=True)
|
|
298
|
+
raise SystemExit(1)
|
|
299
|
+
|
|
300
|
+
# Visualize
|
|
301
|
+
visualizer = ASTVisualizer(use_color=color, use_unicode=not no_unicode)
|
|
302
|
+
tree_output = visualizer.visualize(ast)
|
|
303
|
+
|
|
304
|
+
# Build output
|
|
305
|
+
lines = []
|
|
306
|
+
|
|
307
|
+
# Header
|
|
308
|
+
if verbose:
|
|
309
|
+
lines.append(click.style(f"AST for: {path.name}", fg="cyan", bold=True))
|
|
310
|
+
lines.append(click.style("=" * 50, fg="cyan"))
|
|
311
|
+
lines.append("")
|
|
312
|
+
|
|
313
|
+
lines.append(tree_output)
|
|
314
|
+
|
|
315
|
+
# Stats
|
|
316
|
+
if stats:
|
|
317
|
+
lines.append("")
|
|
318
|
+
lines.append(click.style("Statistics:", fg="yellow", bold=True))
|
|
319
|
+
lines.append(click.style("-" * 30, fg="yellow"))
|
|
320
|
+
|
|
321
|
+
ast_stats = format_ast_stats(ast)
|
|
322
|
+
for node_type, count in sorted(ast_stats.items()):
|
|
323
|
+
lines.append(f" {node_type}: {count}")
|
|
324
|
+
lines.append(f" {'─' * 20}")
|
|
325
|
+
lines.append(f" Total nodes: {sum(ast_stats.values())}")
|
|
326
|
+
|
|
327
|
+
result = "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
# Output
|
|
330
|
+
if output:
|
|
331
|
+
Path(output).write_text(result)
|
|
332
|
+
click.echo(f"Written to {output}")
|
|
333
|
+
else:
|
|
334
|
+
click.echo(result)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check command - parse and validate Yuho files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from yuho.parser import Parser
|
|
13
|
+
from yuho.ast import ASTBuilder
|
|
14
|
+
from yuho.cli.error_formatter import format_errors, format_suggestion, Colors, colorize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Detailed explanations for common error patterns
|
|
18
|
+
ERROR_EXPLANATIONS = {
|
|
19
|
+
"Unexpected syntax": """
|
|
20
|
+
This error occurs when the parser encounters code it doesn't recognize.
|
|
21
|
+
|
|
22
|
+
Common causes:
|
|
23
|
+
- Missing or extra punctuation (commas, braces, colons)
|
|
24
|
+
- Typo in a keyword (e.g., 'strcut' instead of 'struct')
|
|
25
|
+
- Using syntax from another language that Yuho doesn't support
|
|
26
|
+
- Incomplete statement or expression
|
|
27
|
+
|
|
28
|
+
How to fix:
|
|
29
|
+
1. Check for typos in keywords: struct, fn, match, case, consequence, etc.
|
|
30
|
+
2. Ensure all braces {{ }} and parentheses ( ) are balanced
|
|
31
|
+
3. Verify that statements are complete (not cut off mid-expression)
|
|
32
|
+
4. Compare your code against examples in the documentation
|
|
33
|
+
""",
|
|
34
|
+
"Missing semicolon": """
|
|
35
|
+
Yuho doesn't require semicolons in most places, but they can be used
|
|
36
|
+
optionally at the end of statements.
|
|
37
|
+
|
|
38
|
+
This error usually means the parser expected a statement terminator
|
|
39
|
+
but found something else. Check that:
|
|
40
|
+
- The previous statement is complete
|
|
41
|
+
- You're not missing a closing brace or parenthesis
|
|
42
|
+
- Field assignments in structs are separated by commas
|
|
43
|
+
""",
|
|
44
|
+
"Missing closing brace": """
|
|
45
|
+
A closing brace '}}' is expected but was not found.
|
|
46
|
+
|
|
47
|
+
Common causes:
|
|
48
|
+
- Forgetting to close a struct, function, or match block
|
|
49
|
+
- Nested braces that don't match up
|
|
50
|
+
- An error earlier in the file causing the parser to get confused
|
|
51
|
+
|
|
52
|
+
How to fix:
|
|
53
|
+
1. Count your opening {{ and closing }} braces - they should match
|
|
54
|
+
2. Use editor features to highlight matching braces
|
|
55
|
+
3. Look for the last complete block before the error
|
|
56
|
+
""",
|
|
57
|
+
"Expected identifier": """
|
|
58
|
+
An identifier (name) was expected but something else was found.
|
|
59
|
+
|
|
60
|
+
In Yuho, identifiers:
|
|
61
|
+
- Start with a letter or underscore
|
|
62
|
+
- Can contain letters, numbers, and underscores
|
|
63
|
+
- Are case-sensitive
|
|
64
|
+
- Cannot be reserved keywords
|
|
65
|
+
|
|
66
|
+
Examples of valid identifiers: myVar, _private, CamelCase, snake_case
|
|
67
|
+
""",
|
|
68
|
+
"Expected type annotation": """
|
|
69
|
+
A type annotation was expected but not found.
|
|
70
|
+
|
|
71
|
+
In Yuho, variable declarations require explicit types:
|
|
72
|
+
|
|
73
|
+
Correct: int count := 0
|
|
74
|
+
Incorrect: count := 0
|
|
75
|
+
|
|
76
|
+
Built-in types: int, float, bool, string, money, percent, date, duration
|
|
77
|
+
User-defined types: Any struct name you've defined
|
|
78
|
+
""",
|
|
79
|
+
"MISSING": """
|
|
80
|
+
The parser expected to find something that wasn't there.
|
|
81
|
+
|
|
82
|
+
This usually indicates incomplete code. Check that:
|
|
83
|
+
- All statements and expressions are complete
|
|
84
|
+
- Required parts of syntax aren't missing
|
|
85
|
+
- The file didn't get truncated
|
|
86
|
+
""",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_error_explanation(error_message: str, node_type: Optional[str]) -> Optional[str]:
|
|
91
|
+
"""Get a detailed explanation for an error."""
|
|
92
|
+
# Check node type first
|
|
93
|
+
if node_type and node_type.startswith("MISSING:"):
|
|
94
|
+
missing_what = node_type.replace("MISSING:", "")
|
|
95
|
+
if missing_what in ERROR_EXPLANATIONS:
|
|
96
|
+
return ERROR_EXPLANATIONS[missing_what]
|
|
97
|
+
return ERROR_EXPLANATIONS.get("MISSING")
|
|
98
|
+
|
|
99
|
+
# Check error message patterns
|
|
100
|
+
for pattern, explanation in ERROR_EXPLANATIONS.items():
|
|
101
|
+
if pattern in error_message:
|
|
102
|
+
return explanation
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_check(
|
|
108
|
+
file: str,
|
|
109
|
+
json_output: bool = False,
|
|
110
|
+
verbose: bool = False,
|
|
111
|
+
explain_errors: bool = False,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Parse and validate a Yuho source file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file: Path to the .yh file
|
|
118
|
+
json_output: Output errors as JSON
|
|
119
|
+
verbose: Enable verbose output
|
|
120
|
+
explain_errors: Show detailed explanations for errors
|
|
121
|
+
"""
|
|
122
|
+
file_path = Path(file)
|
|
123
|
+
|
|
124
|
+
if verbose:
|
|
125
|
+
click.echo(f"Checking {file_path}...")
|
|
126
|
+
|
|
127
|
+
# Parse the file
|
|
128
|
+
parser = Parser()
|
|
129
|
+
try:
|
|
130
|
+
result = parser.parse_file(file_path)
|
|
131
|
+
except FileNotFoundError:
|
|
132
|
+
if json_output:
|
|
133
|
+
print(json.dumps({"valid": False, "errors": [{"message": f"File not found: {file}"}]}))
|
|
134
|
+
else:
|
|
135
|
+
click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
except UnicodeDecodeError as e:
|
|
138
|
+
if json_output:
|
|
139
|
+
print(json.dumps({"valid": False, "errors": [{"message": f"Invalid UTF-8: {e}"}]}))
|
|
140
|
+
else:
|
|
141
|
+
click.echo(colorize(f"error: Invalid UTF-8 encoding: {e}", Colors.RED), err=True)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Report parse errors
|
|
145
|
+
if result.errors:
|
|
146
|
+
if json_output:
|
|
147
|
+
errors_json = [
|
|
148
|
+
{
|
|
149
|
+
"message": err.message,
|
|
150
|
+
"location": {
|
|
151
|
+
"file": err.location.file,
|
|
152
|
+
"line": err.location.line,
|
|
153
|
+
"col": err.location.col,
|
|
154
|
+
"end_line": err.location.end_line,
|
|
155
|
+
"end_col": err.location.end_col,
|
|
156
|
+
},
|
|
157
|
+
"node_type": err.node_type,
|
|
158
|
+
"explanation": get_error_explanation(err.message, err.node_type) if explain_errors else None,
|
|
159
|
+
}
|
|
160
|
+
for err in result.errors
|
|
161
|
+
]
|
|
162
|
+
print(json.dumps({"valid": False, "errors": errors_json}, indent=2))
|
|
163
|
+
else:
|
|
164
|
+
error_output = format_errors(result.errors, result.source, str(file_path))
|
|
165
|
+
click.echo(error_output, err=True)
|
|
166
|
+
|
|
167
|
+
# Add suggestions
|
|
168
|
+
for err in result.errors:
|
|
169
|
+
suggestion = format_suggestion(err, result.source)
|
|
170
|
+
if suggestion:
|
|
171
|
+
click.echo(colorize(f" hint: {suggestion}", Colors.YELLOW), err=True)
|
|
172
|
+
|
|
173
|
+
# Add detailed explanation if requested
|
|
174
|
+
if explain_errors:
|
|
175
|
+
explanation = get_error_explanation(err.message, err.node_type)
|
|
176
|
+
if explanation:
|
|
177
|
+
click.echo(colorize("\n Explanation:", Colors.CYAN + Colors.BOLD), err=True)
|
|
178
|
+
for line in explanation.strip().split("\n"):
|
|
179
|
+
click.echo(colorize(f" {line}", Colors.DIM), err=True)
|
|
180
|
+
click.echo("", err=True)
|
|
181
|
+
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
# Build AST
|
|
185
|
+
try:
|
|
186
|
+
builder = ASTBuilder(result.source, str(file_path))
|
|
187
|
+
ast = builder.build(result.root_node)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if json_output:
|
|
190
|
+
print(json.dumps({"valid": False, "errors": [{"message": f"AST error: {e}"}]}))
|
|
191
|
+
else:
|
|
192
|
+
click.echo(colorize(f"error: Failed to build AST: {e}", Colors.RED), err=True)
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
# Success
|
|
196
|
+
if json_output:
|
|
197
|
+
summary = {
|
|
198
|
+
"valid": True,
|
|
199
|
+
"file": str(file_path),
|
|
200
|
+
"stats": {
|
|
201
|
+
"imports": len(ast.imports),
|
|
202
|
+
"structs": len(ast.type_defs),
|
|
203
|
+
"functions": len(ast.function_defs),
|
|
204
|
+
"statutes": len(ast.statutes),
|
|
205
|
+
"variables": len(ast.variables),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
print(json.dumps(summary, indent=2))
|
|
209
|
+
else:
|
|
210
|
+
click.echo(colorize(f"OK: {file_path}", Colors.CYAN + Colors.BOLD))
|
|
211
|
+
if verbose:
|
|
212
|
+
click.echo(f" {len(ast.imports)} imports")
|
|
213
|
+
click.echo(f" {len(ast.type_defs)} type definitions")
|
|
214
|
+
click.echo(f" {len(ast.function_defs)} functions")
|
|
215
|
+
click.echo(f" {len(ast.statutes)} statutes")
|
|
216
|
+
click.echo(f" {len(ast.variables)} variables")
|
|
217
|
+
|
|
218
|
+
sys.exit(0)
|