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,8 @@
1
+ """
2
+ Yuho parser module - tree-sitter based parsing for .yh files
3
+ """
4
+
5
+ from yuho.parser.source_location import SourceLocation
6
+ from yuho.parser.wrapper import Parser, get_parser, clear_parser_cache
7
+
8
+ __all__ = ["Parser", "SourceLocation", "get_parser", "clear_parser_cache"]
@@ -0,0 +1,108 @@
1
+ """
2
+ Source location tracking for AST nodes and error reporting.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class SourceLocation:
11
+ """
12
+ Represents a span of source code with file, line, and column information.
13
+
14
+ All line and column numbers are 1-indexed for user-facing display.
15
+ Internally, tree-sitter uses 0-indexed positions which are converted
16
+ in the Parser wrapper.
17
+
18
+ Attributes:
19
+ file: Path to the source file (or "<string>" for inline parsing)
20
+ line: Starting line number (1-indexed)
21
+ col: Starting column number (1-indexed)
22
+ end_line: Ending line number (1-indexed)
23
+ end_col: Ending column number (1-indexed, exclusive)
24
+ offset: Byte offset from start of file
25
+ end_offset: Byte offset of end position
26
+ """
27
+
28
+ file: str
29
+ line: int
30
+ col: int
31
+ end_line: int
32
+ end_col: int
33
+ offset: Optional[int] = None
34
+ end_offset: Optional[int] = None
35
+
36
+ def __str__(self) -> str:
37
+ """Format as 'file:line:col' for error messages."""
38
+ if self.line == self.end_line:
39
+ return f"{self.file}:{self.line}:{self.col}"
40
+ return f"{self.file}:{self.line}:{self.col}-{self.end_line}:{self.end_col}"
41
+
42
+ def __repr__(self) -> str:
43
+ return (
44
+ f"SourceLocation(file={self.file!r}, "
45
+ f"line={self.line}, col={self.col}, "
46
+ f"end_line={self.end_line}, end_col={self.end_col})"
47
+ )
48
+
49
+ @classmethod
50
+ def from_tree_sitter_node(cls, node, file: str = "<string>") -> "SourceLocation":
51
+ """
52
+ Create a SourceLocation from a tree-sitter node.
53
+
54
+ Tree-sitter uses 0-indexed rows and columns, so we add 1 for
55
+ user-facing display.
56
+ """
57
+ start_point = node.start_point
58
+ end_point = node.end_point
59
+
60
+ return cls(
61
+ file=file,
62
+ line=start_point[0] + 1,
63
+ col=start_point[1] + 1,
64
+ end_line=end_point[0] + 1,
65
+ end_col=end_point[1] + 1,
66
+ offset=node.start_byte,
67
+ end_offset=node.end_byte,
68
+ )
69
+
70
+ @classmethod
71
+ def unknown(cls, file: str = "<unknown>") -> "SourceLocation":
72
+ """Create a placeholder location for generated or unknown nodes."""
73
+ return cls(file=file, line=0, col=0, end_line=0, end_col=0)
74
+
75
+ def contains(self, other: "SourceLocation") -> bool:
76
+ """Check if this location fully contains another location."""
77
+ if self.file != other.file:
78
+ return False
79
+
80
+ if self.offset is not None and other.offset is not None:
81
+ return (
82
+ self.offset <= other.offset
83
+ and self.end_offset >= other.end_offset
84
+ )
85
+
86
+ # Fallback to line/col comparison
87
+ start_before = (self.line < other.line) or (
88
+ self.line == other.line and self.col <= other.col
89
+ )
90
+ end_after = (self.end_line > other.end_line) or (
91
+ self.end_line == other.end_line and self.end_col >= other.end_col
92
+ )
93
+ return start_before and end_after
94
+
95
+ def merge(self, other: "SourceLocation") -> "SourceLocation":
96
+ """Create a new location spanning from this location to another."""
97
+ if self.file != other.file:
98
+ raise ValueError(f"Cannot merge locations from different files: {self.file} and {other.file}")
99
+
100
+ return SourceLocation(
101
+ file=self.file,
102
+ line=min(self.line, other.line),
103
+ col=self.col if self.line <= other.line else other.col,
104
+ end_line=max(self.end_line, other.end_line),
105
+ end_col=self.end_col if self.end_line >= other.end_line else other.end_col,
106
+ offset=min(self.offset, other.offset) if self.offset and other.offset else None,
107
+ end_offset=max(self.end_offset, other.end_offset) if self.end_offset and other.end_offset else None,
108
+ )
yuho/parser/wrapper.py ADDED
@@ -0,0 +1,311 @@
1
+ """
2
+ Parser wrapper class for tree-sitter based Yuho parsing.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Optional, List, Iterator
8
+ import os
9
+ import threading
10
+
11
+ from yuho.parser.source_location import SourceLocation
12
+
13
+
14
+ # Module-level parser cache for performance
15
+ _parser_cache: Optional["Parser"] = None
16
+ _parser_lock = threading.Lock()
17
+
18
+
19
+ @dataclass
20
+ class ParseError:
21
+ """
22
+ Represents a syntax error encountered during parsing.
23
+
24
+ Attributes:
25
+ message: Human-readable error description
26
+ location: Source location where the error occurred
27
+ node_type: The tree-sitter node type that has the error (if available)
28
+ """
29
+
30
+ message: str
31
+ location: SourceLocation
32
+ node_type: Optional[str] = None
33
+
34
+ def __str__(self) -> str:
35
+ return f"{self.location}: {self.message}"
36
+
37
+
38
+ @dataclass
39
+ class ParseResult:
40
+ """
41
+ Result of parsing a Yuho source file.
42
+
43
+ Attributes:
44
+ tree: The tree-sitter Tree object (or None if parsing failed completely)
45
+ errors: List of syntax errors found during parsing
46
+ source: The original source string
47
+ file: The file path (or "<string>" for inline parsing)
48
+ """
49
+
50
+ tree: object # tree_sitter.Tree
51
+ errors: List[ParseError]
52
+ source: str
53
+ file: str
54
+
55
+ @property
56
+ def is_valid(self) -> bool:
57
+ """Return True if the source parsed without errors."""
58
+ return len(self.errors) == 0
59
+
60
+ @property
61
+ def root_node(self):
62
+ """Return the root node of the parse tree."""
63
+ return self.tree.root_node if self.tree else None
64
+
65
+
66
+ class Parser:
67
+ """
68
+ Parser for Yuho source files using tree-sitter.
69
+
70
+ This class wraps the tree-sitter parser and provides a convenient
71
+ interface for parsing Yuho source code and extracting syntax errors.
72
+
73
+ Usage:
74
+ parser = Parser()
75
+ result = parser.parse('struct Foo { int x, }')
76
+ if result.is_valid:
77
+ # Process the AST
78
+ root = result.root_node
79
+ else:
80
+ for error in result.errors:
81
+ print(error)
82
+ """
83
+
84
+ def __init__(self):
85
+ """Initialize the parser with the Yuho language grammar."""
86
+ self._parser = None
87
+ self._language = None
88
+ self._initialized = False
89
+
90
+ def _ensure_initialized(self):
91
+ """Lazily initialize the tree-sitter parser."""
92
+ if self._initialized:
93
+ return
94
+
95
+ try:
96
+ from tree_sitter import Parser as TSParser
97
+ from tree_sitter_yuho import language
98
+
99
+ self._parser = TSParser()
100
+ self._language = language()
101
+ self._parser.set_language(self._language)
102
+ self._initialized = True
103
+ except ImportError as e:
104
+ raise ImportError(
105
+ "tree-sitter and tree-sitter-yuho must be installed. "
106
+ "Run: pip install tree-sitter tree-sitter-yuho"
107
+ ) from e
108
+
109
+ def parse(self, source: str, file: str = "<string>") -> ParseResult:
110
+ """
111
+ Parse a Yuho source string.
112
+
113
+ Args:
114
+ source: The Yuho source code to parse
115
+ file: Optional file path for error messages
116
+
117
+ Returns:
118
+ ParseResult containing the tree and any errors
119
+ """
120
+ self._ensure_initialized()
121
+
122
+ # Encode source as bytes for tree-sitter
123
+ source_bytes = source.encode("utf-8")
124
+ tree = self._parser.parse(source_bytes)
125
+
126
+ # Collect errors by walking the tree
127
+ errors = list(self._collect_errors(tree.root_node, source, file))
128
+
129
+ return ParseResult(
130
+ tree=tree,
131
+ errors=errors,
132
+ source=source,
133
+ file=file,
134
+ )
135
+
136
+ def parse_file(self, path: str | Path) -> ParseResult:
137
+ """
138
+ Parse a Yuho source file.
139
+
140
+ Args:
141
+ path: Path to the .yh file
142
+
143
+ Returns:
144
+ ParseResult containing the tree and any errors
145
+
146
+ Raises:
147
+ FileNotFoundError: If the file does not exist
148
+ UnicodeDecodeError: If the file is not valid UTF-8
149
+ """
150
+ path = Path(path)
151
+
152
+ if not path.exists():
153
+ raise FileNotFoundError(f"File not found: {path}")
154
+
155
+ source = path.read_text(encoding="utf-8")
156
+ return self.parse(source, file=str(path))
157
+
158
+ def _collect_errors(
159
+ self, node, source: str, file: str
160
+ ) -> Iterator[ParseError]:
161
+ """
162
+ Walk the parse tree and collect error nodes.
163
+
164
+ Tree-sitter marks nodes with errors using:
165
+ - is_error: True for ERROR nodes
166
+ - is_missing: True for MISSING nodes (expected but not found)
167
+ - has_error: True if this node or any descendant has errors
168
+ """
169
+ if node.is_error:
170
+ # This is an ERROR node - unexpected token(s)
171
+ location = SourceLocation.from_tree_sitter_node(node, file)
172
+ error_text = source[node.start_byte : node.end_byte]
173
+
174
+ if len(error_text) > 50:
175
+ error_text = error_text[:47] + "..."
176
+
177
+ yield ParseError(
178
+ message=f"Unexpected syntax: {error_text!r}",
179
+ location=location,
180
+ node_type="ERROR",
181
+ )
182
+
183
+ elif node.is_missing:
184
+ # This is a MISSING node - expected syntax not found
185
+ location = SourceLocation.from_tree_sitter_node(node, file)
186
+
187
+ # Generate helpful message based on what's missing
188
+ missing_type = node.type
189
+ message = self._missing_node_message(missing_type)
190
+
191
+ yield ParseError(
192
+ message=message,
193
+ location=location,
194
+ node_type=f"MISSING:{missing_type}",
195
+ )
196
+
197
+ elif node.has_error:
198
+ # Recurse into children to find the actual error nodes
199
+ for child in node.children:
200
+ yield from self._collect_errors(child, source, file)
201
+
202
+ def _missing_node_message(self, node_type: str) -> str:
203
+ """Generate a helpful error message for missing nodes."""
204
+ messages = {
205
+ ";": "Missing semicolon",
206
+ ",": "Missing comma",
207
+ "{": "Missing opening brace '{'",
208
+ "}": "Missing closing brace '}'",
209
+ "(": "Missing opening parenthesis '('",
210
+ ")": "Missing closing parenthesis ')'",
211
+ "[": "Missing opening bracket '['",
212
+ "]": "Missing closing bracket ']'",
213
+ ":=": "Missing assignment operator ':='",
214
+ ":": "Missing colon ':'",
215
+ "identifier": "Expected identifier",
216
+ "string_literal": "Expected string literal",
217
+ "integer_literal": "Expected integer",
218
+ "_type": "Expected type annotation",
219
+ "_expression": "Expected expression",
220
+ }
221
+ return messages.get(node_type, f"Missing {node_type}")
222
+
223
+ def walk_tree(self, tree) -> Iterator:
224
+ """
225
+ Generator that yields all nodes in the tree via depth-first traversal.
226
+
227
+ Usage:
228
+ for node in parser.walk_tree(result.tree):
229
+ print(node.type, node.text)
230
+ """
231
+ cursor = tree.walk()
232
+
233
+ visited_children = False
234
+ while True:
235
+ if not visited_children:
236
+ yield cursor.node
237
+ if cursor.goto_first_child():
238
+ continue
239
+
240
+ if cursor.goto_next_sibling():
241
+ visited_children = False
242
+ elif cursor.goto_parent():
243
+ visited_children = True
244
+ else:
245
+ break
246
+
247
+ def query(self, tree, query_string: str) -> List:
248
+ """
249
+ Run a tree-sitter query against the parse tree.
250
+
251
+ Args:
252
+ tree: The parsed tree (from ParseResult.tree)
253
+ query_string: A tree-sitter query in S-expression format
254
+
255
+ Returns:
256
+ List of (node, capture_name) tuples
257
+
258
+ Usage:
259
+ captures = parser.query(result.tree, '(struct_definition name: (identifier) @name)')
260
+ for node, name in captures:
261
+ print(f"Found struct: {node.text}")
262
+ """
263
+ self._ensure_initialized()
264
+
265
+ from tree_sitter import Query
266
+
267
+ query = self._language.query(query_string)
268
+ return query.captures(tree.root_node)
269
+
270
+
271
+ def get_parser() -> Parser:
272
+ """
273
+ Get a cached Parser instance for better performance.
274
+
275
+ This function returns a thread-safe singleton Parser instance.
276
+ Use this instead of creating new Parser() instances when parsing
277
+ multiple files to avoid repeated tree-sitter initialization overhead.
278
+
279
+ Returns:
280
+ A cached Parser instance
281
+
282
+ Usage:
283
+ from yuho.parser import get_parser
284
+ parser = get_parser()
285
+ result = parser.parse_file("statute.yh")
286
+ """
287
+ global _parser_cache
288
+
289
+ if _parser_cache is not None:
290
+ return _parser_cache
291
+
292
+ with _parser_lock:
293
+ # Double-check after acquiring lock
294
+ if _parser_cache is None:
295
+ _parser_cache = Parser()
296
+ _parser_cache._ensure_initialized()
297
+
298
+ return _parser_cache
299
+
300
+
301
+ def clear_parser_cache() -> None:
302
+ """
303
+ Clear the cached parser instance.
304
+
305
+ This is mainly useful for testing or when you need to
306
+ reinitialize the parser (e.g., after grammar changes).
307
+ """
308
+ global _parser_cache
309
+
310
+ with _parser_lock:
311
+ _parser_cache = None
@@ -0,0 +1,48 @@
1
+ """
2
+ Yuho testing module - pytest fixtures and utilities for testing Yuho statute implementations.
3
+
4
+ Usage in your tests:
5
+ from yuho.testing import yuho_parser, yuho_ast, parse_statute
6
+
7
+ Or import fixtures directly in conftest.py:
8
+ pytest_plugins = ["yuho.testing.fixtures"]
9
+
10
+ For coverage tracking:
11
+ from yuho.testing import CoverageTracker, analyze_test_coverage
12
+ """
13
+
14
+ from yuho.testing.fixtures import (
15
+ yuho_parser,
16
+ yuho_ast,
17
+ parse_statute,
18
+ parse_file,
19
+ statute_validator,
20
+ StatuteTestCase,
21
+ element_check,
22
+ penalty_check,
23
+ )
24
+ from yuho.testing.coverage import (
25
+ CoverageTracker,
26
+ CoverageReport,
27
+ StatuteCoverage,
28
+ ElementCoverage,
29
+ analyze_test_coverage,
30
+ )
31
+
32
+ __all__ = [
33
+ # Fixtures
34
+ "yuho_parser",
35
+ "yuho_ast",
36
+ "parse_statute",
37
+ "parse_file",
38
+ "statute_validator",
39
+ "StatuteTestCase",
40
+ "element_check",
41
+ "penalty_check",
42
+ # Coverage
43
+ "CoverageTracker",
44
+ "CoverageReport",
45
+ "StatuteCoverage",
46
+ "ElementCoverage",
47
+ "analyze_test_coverage",
48
+ ]