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,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mermaid transpiler - decision tree flowchart generation.
|
|
3
|
+
|
|
4
|
+
Converts match expressions to Mermaid flowchart diagrams.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Dict, Set
|
|
8
|
+
|
|
9
|
+
from yuho.ast import nodes
|
|
10
|
+
from yuho.ast.visitor import Visitor
|
|
11
|
+
from yuho.transpile.base import TranspileTarget, TranspilerBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MermaidTranspiler(TranspilerBase, Visitor):
|
|
15
|
+
"""
|
|
16
|
+
Transpile Yuho AST to Mermaid flowchart diagrams.
|
|
17
|
+
|
|
18
|
+
Generates decision tree flowcharts from match expressions,
|
|
19
|
+
with diamonds for conditions and rectangles for outcomes.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, direction: str = "TD", use_subgraphs: bool = True):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the Mermaid transpiler.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
direction: Flowchart direction (TD=top-down, LR=left-right)
|
|
28
|
+
use_subgraphs: Whether to wrap nested match expressions in subgraphs
|
|
29
|
+
"""
|
|
30
|
+
self.direction = direction
|
|
31
|
+
self.use_subgraphs = use_subgraphs
|
|
32
|
+
self._output: List[str] = []
|
|
33
|
+
self._node_counter = 0
|
|
34
|
+
self._subgraph_counter = 0
|
|
35
|
+
self._node_ids: Dict[int, str] = {}
|
|
36
|
+
self._nesting_depth = 0 # Track nesting level for subgraph indentation
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def target(self) -> TranspileTarget:
|
|
40
|
+
return TranspileTarget.MERMAID
|
|
41
|
+
|
|
42
|
+
def transpile(self, ast: nodes.ModuleNode) -> str:
|
|
43
|
+
"""Transpile AST to Mermaid diagram."""
|
|
44
|
+
self._output = []
|
|
45
|
+
self._node_counter = 0
|
|
46
|
+
self._subgraph_counter = 0
|
|
47
|
+
self._nesting_depth = 0
|
|
48
|
+
|
|
49
|
+
# Header
|
|
50
|
+
self._emit(f"flowchart {self.direction}")
|
|
51
|
+
|
|
52
|
+
# Process each statute
|
|
53
|
+
for statute in ast.statutes:
|
|
54
|
+
self._transpile_statute(statute)
|
|
55
|
+
|
|
56
|
+
# Process standalone match expressions in functions
|
|
57
|
+
for func in ast.function_defs:
|
|
58
|
+
self._transpile_function(func)
|
|
59
|
+
|
|
60
|
+
return "\n".join(self._output)
|
|
61
|
+
|
|
62
|
+
def _emit(self, line: str) -> None:
|
|
63
|
+
"""Add a line to output."""
|
|
64
|
+
self._output.append(line)
|
|
65
|
+
|
|
66
|
+
def _new_node_id(self, prefix: str = "N") -> str:
|
|
67
|
+
"""Generate a new unique node ID."""
|
|
68
|
+
self._node_counter += 1
|
|
69
|
+
return f"{prefix}{self._node_counter}"
|
|
70
|
+
|
|
71
|
+
def _new_subgraph_id(self, name: str = "sub") -> str:
|
|
72
|
+
"""Generate a new unique subgraph ID."""
|
|
73
|
+
self._subgraph_counter += 1
|
|
74
|
+
return f"{name}_{self._subgraph_counter}"
|
|
75
|
+
|
|
76
|
+
def _indent(self) -> str:
|
|
77
|
+
"""Get current indentation string based on nesting depth."""
|
|
78
|
+
return " " * (self._nesting_depth + 1)
|
|
79
|
+
|
|
80
|
+
def _escape_text(self, text: str) -> str:
|
|
81
|
+
"""Escape text for Mermaid labels."""
|
|
82
|
+
# Escape quotes and special chars
|
|
83
|
+
text = text.replace('"', "'")
|
|
84
|
+
text = text.replace("<", "<")
|
|
85
|
+
text = text.replace(">", ">")
|
|
86
|
+
# Truncate long text
|
|
87
|
+
if len(text) > 50:
|
|
88
|
+
text = text[:47] + "..."
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
# =========================================================================
|
|
92
|
+
# Statute processing
|
|
93
|
+
# =========================================================================
|
|
94
|
+
|
|
95
|
+
def _transpile_statute(self, statute: nodes.StatuteNode) -> None:
|
|
96
|
+
"""Generate flowchart for a statute."""
|
|
97
|
+
title = statute.title.value if statute.title else statute.section_number
|
|
98
|
+
self._emit(f" %% Statute: {self._escape_text(title)}")
|
|
99
|
+
|
|
100
|
+
# Start node
|
|
101
|
+
start_id = self._new_node_id("START")
|
|
102
|
+
self._emit(f" {start_id}([Section {statute.section_number}])")
|
|
103
|
+
|
|
104
|
+
# Process elements that contain match expressions
|
|
105
|
+
prev_id = start_id
|
|
106
|
+
for elem in statute.elements:
|
|
107
|
+
if isinstance(elem.description, nodes.MatchExprNode):
|
|
108
|
+
elem_start = self._new_node_id("ELEM")
|
|
109
|
+
self._emit(f" {elem_start}[/{elem.name}/]")
|
|
110
|
+
self._emit(f" {prev_id} --> {elem_start}")
|
|
111
|
+
|
|
112
|
+
end_id = self._transpile_match_expr(elem.description, elem_start)
|
|
113
|
+
prev_id = end_id
|
|
114
|
+
else:
|
|
115
|
+
elem_id = self._new_node_id("ELEM")
|
|
116
|
+
label = self._escape_text(self._expr_to_label(elem.description))
|
|
117
|
+
self._emit(f" {elem_id}[{label}]")
|
|
118
|
+
self._emit(f" {prev_id} --> {elem_id}")
|
|
119
|
+
prev_id = elem_id
|
|
120
|
+
|
|
121
|
+
# Penalty outcome
|
|
122
|
+
if statute.penalty:
|
|
123
|
+
penalty_id = self._new_node_id("PENALTY")
|
|
124
|
+
penalty_text = self._penalty_to_label(statute.penalty)
|
|
125
|
+
self._emit(f" {penalty_id}[[\"{self._escape_text(penalty_text)}\"]]")
|
|
126
|
+
self._emit(f" {prev_id} --> {penalty_id}")
|
|
127
|
+
|
|
128
|
+
self._emit("")
|
|
129
|
+
|
|
130
|
+
# =========================================================================
|
|
131
|
+
# Function processing
|
|
132
|
+
# =========================================================================
|
|
133
|
+
|
|
134
|
+
def _transpile_function(self, func: nodes.FunctionDefNode) -> None:
|
|
135
|
+
"""Generate flowchart for function containing match expressions."""
|
|
136
|
+
# Find match expressions in function body
|
|
137
|
+
match_exprs = self._find_match_exprs(func.body)
|
|
138
|
+
if not match_exprs:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
self._emit(f" %% Function: {func.name}")
|
|
142
|
+
|
|
143
|
+
start_id = self._new_node_id("FN")
|
|
144
|
+
params = ", ".join(p.name for p in func.params)
|
|
145
|
+
self._emit(f" {start_id}([{func.name}({params})])")
|
|
146
|
+
|
|
147
|
+
prev_id = start_id
|
|
148
|
+
for match_expr in match_exprs:
|
|
149
|
+
end_id = self._transpile_match_expr(match_expr, prev_id)
|
|
150
|
+
prev_id = end_id
|
|
151
|
+
|
|
152
|
+
self._emit("")
|
|
153
|
+
|
|
154
|
+
def _find_match_exprs(self, node: nodes.ASTNode) -> List[nodes.MatchExprNode]:
|
|
155
|
+
"""Find all match expressions in a node tree."""
|
|
156
|
+
matches: List[nodes.MatchExprNode] = []
|
|
157
|
+
|
|
158
|
+
if isinstance(node, nodes.MatchExprNode):
|
|
159
|
+
matches.append(node)
|
|
160
|
+
|
|
161
|
+
for child in node.children():
|
|
162
|
+
matches.extend(self._find_match_exprs(child))
|
|
163
|
+
|
|
164
|
+
return matches
|
|
165
|
+
|
|
166
|
+
# =========================================================================
|
|
167
|
+
# Match expression to flowchart
|
|
168
|
+
# =========================================================================
|
|
169
|
+
|
|
170
|
+
def _transpile_match_expr(
|
|
171
|
+
self,
|
|
172
|
+
match: nodes.MatchExprNode,
|
|
173
|
+
start_id: str,
|
|
174
|
+
subgraph_name: Optional[str] = None,
|
|
175
|
+
) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Generate flowchart nodes for a match expression.
|
|
178
|
+
|
|
179
|
+
Returns the ID of the end/merge node.
|
|
180
|
+
"""
|
|
181
|
+
# Check if this contains nested match expressions
|
|
182
|
+
has_nested = any(
|
|
183
|
+
isinstance(arm.body, nodes.MatchExprNode)
|
|
184
|
+
for arm in match.arms
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Use subgraph for nested match at depth > 0 or if explicitly named
|
|
188
|
+
use_subgraph = (
|
|
189
|
+
self.use_subgraphs and
|
|
190
|
+
(self._nesting_depth > 0 or subgraph_name is not None)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
subgraph_id = None
|
|
194
|
+
if use_subgraph:
|
|
195
|
+
subgraph_id = self._new_subgraph_id("match")
|
|
196
|
+
label = subgraph_name or "nested decision"
|
|
197
|
+
self._emit(f"{self._indent()}subgraph {subgraph_id}[\"{self._escape_text(label)}\"]")
|
|
198
|
+
self._nesting_depth += 1
|
|
199
|
+
|
|
200
|
+
# Create decision node for scrutinee
|
|
201
|
+
if match.scrutinee:
|
|
202
|
+
scrutinee_label = self._expr_to_label(match.scrutinee)
|
|
203
|
+
decision_id = self._new_node_id("D")
|
|
204
|
+
self._emit(f"{self._indent()}{decision_id}{{{{{self._escape_text(scrutinee_label)}}}}}")
|
|
205
|
+
self._emit(f"{self._indent()}{start_id} --> {decision_id}")
|
|
206
|
+
else:
|
|
207
|
+
decision_id = start_id
|
|
208
|
+
|
|
209
|
+
# End node for merging
|
|
210
|
+
end_id = self._new_node_id("END")
|
|
211
|
+
|
|
212
|
+
# Process each arm
|
|
213
|
+
for i, arm in enumerate(match.arms):
|
|
214
|
+
arm_outcome_id = self._transpile_match_arm(arm, decision_id, i)
|
|
215
|
+
self._emit(f"{self._indent()}{arm_outcome_id} --> {end_id}")
|
|
216
|
+
|
|
217
|
+
# Create end node (circle for merge point)
|
|
218
|
+
self._emit(f"{self._indent()}{end_id}((*))")
|
|
219
|
+
|
|
220
|
+
# Close subgraph if opened
|
|
221
|
+
if use_subgraph:
|
|
222
|
+
self._nesting_depth -= 1
|
|
223
|
+
self._emit(f"{self._indent()}end")
|
|
224
|
+
|
|
225
|
+
return end_id
|
|
226
|
+
|
|
227
|
+
def _transpile_match_arm(self, arm: nodes.MatchArm, from_id: str, index: int) -> str:
|
|
228
|
+
"""
|
|
229
|
+
Generate nodes for a match arm.
|
|
230
|
+
|
|
231
|
+
Returns the ID of the outcome node.
|
|
232
|
+
"""
|
|
233
|
+
# Edge label from pattern
|
|
234
|
+
pattern_label = self._pattern_to_label(arm.pattern)
|
|
235
|
+
|
|
236
|
+
# If there's a guard, create intermediate decision node
|
|
237
|
+
if arm.guard:
|
|
238
|
+
guard_id = self._new_node_id("G")
|
|
239
|
+
guard_label = self._expr_to_label(arm.guard)
|
|
240
|
+
self._emit(f"{self._indent()}{guard_id}{{{{{self._escape_text(guard_label)}}}}}")
|
|
241
|
+
self._emit(f"{self._indent()}{from_id} -->|\"{self._escape_text(pattern_label)}\"| {guard_id}")
|
|
242
|
+
|
|
243
|
+
# Check if body is a nested match expression
|
|
244
|
+
if isinstance(arm.body, nodes.MatchExprNode):
|
|
245
|
+
# Generate subgraph for nested match
|
|
246
|
+
nested_label = f"when {pattern_label} (guarded)"
|
|
247
|
+
end_id = self._transpile_match_expr(arm.body, guard_id, nested_label)
|
|
248
|
+
return end_id
|
|
249
|
+
else:
|
|
250
|
+
# True path goes to outcome
|
|
251
|
+
outcome_id = self._new_node_id("O")
|
|
252
|
+
body_label = self._expr_to_label(arm.body)
|
|
253
|
+
self._emit(f"{self._indent()}{outcome_id}[\"{self._escape_text(body_label)}\"]")
|
|
254
|
+
self._emit(f"{self._indent()}{guard_id} -->|\"Yes\"| {outcome_id}")
|
|
255
|
+
return outcome_id
|
|
256
|
+
else:
|
|
257
|
+
# Check if body is a nested match expression
|
|
258
|
+
if isinstance(arm.body, nodes.MatchExprNode):
|
|
259
|
+
# Create a connector node for the nested match
|
|
260
|
+
connector_id = self._new_node_id("C")
|
|
261
|
+
self._emit(f"{self._indent()}{connector_id}((...))")
|
|
262
|
+
self._emit(f"{self._indent()}{from_id} -->|\"{self._escape_text(pattern_label)}\"| {connector_id}")
|
|
263
|
+
|
|
264
|
+
# Generate subgraph for nested match
|
|
265
|
+
nested_label = f"when {pattern_label}"
|
|
266
|
+
end_id = self._transpile_match_expr(arm.body, connector_id, nested_label)
|
|
267
|
+
return end_id
|
|
268
|
+
else:
|
|
269
|
+
# Direct path to outcome
|
|
270
|
+
outcome_id = self._new_node_id("O")
|
|
271
|
+
body_label = self._expr_to_label(arm.body)
|
|
272
|
+
self._emit(f"{self._indent()}{outcome_id}[\"{self._escape_text(body_label)}\"]")
|
|
273
|
+
self._emit(f"{self._indent()}{from_id} -->|\"{self._escape_text(pattern_label)}\"| {outcome_id}")
|
|
274
|
+
return outcome_id
|
|
275
|
+
|
|
276
|
+
# =========================================================================
|
|
277
|
+
# Label generation helpers
|
|
278
|
+
# =========================================================================
|
|
279
|
+
|
|
280
|
+
def _expr_to_label(self, node: nodes.ASTNode) -> str:
|
|
281
|
+
"""Convert expression to label text."""
|
|
282
|
+
if isinstance(node, nodes.IntLit):
|
|
283
|
+
return str(node.value)
|
|
284
|
+
elif isinstance(node, nodes.FloatLit):
|
|
285
|
+
return str(node.value)
|
|
286
|
+
elif isinstance(node, nodes.BoolLit):
|
|
287
|
+
return "TRUE" if node.value else "FALSE"
|
|
288
|
+
elif isinstance(node, nodes.StringLit):
|
|
289
|
+
return node.value
|
|
290
|
+
elif isinstance(node, nodes.MoneyNode):
|
|
291
|
+
return f"${node.amount}"
|
|
292
|
+
elif isinstance(node, nodes.PercentNode):
|
|
293
|
+
return f"{node.value}%"
|
|
294
|
+
elif isinstance(node, nodes.DateNode):
|
|
295
|
+
return node.value.isoformat()
|
|
296
|
+
elif isinstance(node, nodes.DurationNode):
|
|
297
|
+
return str(node)
|
|
298
|
+
elif isinstance(node, nodes.IdentifierNode):
|
|
299
|
+
return node.name
|
|
300
|
+
elif isinstance(node, nodes.FieldAccessNode):
|
|
301
|
+
base = self._expr_to_label(node.base)
|
|
302
|
+
return f"{base}.{node.field_name}"
|
|
303
|
+
elif isinstance(node, nodes.IndexAccessNode):
|
|
304
|
+
base = self._expr_to_label(node.base)
|
|
305
|
+
index = self._expr_to_label(node.index)
|
|
306
|
+
return f"{base}[{index}]"
|
|
307
|
+
elif isinstance(node, nodes.FunctionCallNode):
|
|
308
|
+
callee = self._expr_to_label(node.callee)
|
|
309
|
+
args = ", ".join(self._expr_to_label(a) for a in node.args)
|
|
310
|
+
return f"{callee}({args})"
|
|
311
|
+
elif isinstance(node, nodes.BinaryExprNode):
|
|
312
|
+
left = self._expr_to_label(node.left)
|
|
313
|
+
right = self._expr_to_label(node.right)
|
|
314
|
+
return f"{left} {node.operator} {right}"
|
|
315
|
+
elif isinstance(node, nodes.UnaryExprNode):
|
|
316
|
+
operand = self._expr_to_label(node.operand)
|
|
317
|
+
return f"{node.operator}{operand}"
|
|
318
|
+
elif isinstance(node, nodes.PassExprNode):
|
|
319
|
+
return "pass"
|
|
320
|
+
elif isinstance(node, nodes.MatchExprNode):
|
|
321
|
+
return "[nested decision]"
|
|
322
|
+
elif isinstance(node, nodes.StructLiteralNode):
|
|
323
|
+
if node.struct_name:
|
|
324
|
+
return f"new {node.struct_name}"
|
|
325
|
+
return "{...}"
|
|
326
|
+
else:
|
|
327
|
+
return "?"
|
|
328
|
+
|
|
329
|
+
def _pattern_to_label(self, pattern: nodes.PatternNode) -> str:
|
|
330
|
+
"""Convert pattern to edge label."""
|
|
331
|
+
if isinstance(pattern, nodes.WildcardPattern):
|
|
332
|
+
return "otherwise"
|
|
333
|
+
elif isinstance(pattern, nodes.LiteralPattern):
|
|
334
|
+
return self._expr_to_label(pattern.literal)
|
|
335
|
+
elif isinstance(pattern, nodes.BindingPattern):
|
|
336
|
+
return f"-> {pattern.name}"
|
|
337
|
+
elif isinstance(pattern, nodes.StructPattern):
|
|
338
|
+
fields = ", ".join(fp.name for fp in pattern.fields)
|
|
339
|
+
return f"{pattern.type_name}{{{fields}}}"
|
|
340
|
+
else:
|
|
341
|
+
return "?"
|
|
342
|
+
|
|
343
|
+
def _penalty_to_label(self, penalty: nodes.PenaltyNode) -> str:
|
|
344
|
+
"""Convert penalty to label text."""
|
|
345
|
+
parts: List[str] = []
|
|
346
|
+
|
|
347
|
+
if penalty.imprisonment_max:
|
|
348
|
+
duration = str(penalty.imprisonment_max)
|
|
349
|
+
parts.append(f"Imprisonment up to {duration}")
|
|
350
|
+
|
|
351
|
+
if penalty.fine_max:
|
|
352
|
+
parts.append(f"Fine up to ${penalty.fine_max.amount}")
|
|
353
|
+
|
|
354
|
+
if penalty.supplementary:
|
|
355
|
+
parts.append(penalty.supplementary.value)
|
|
356
|
+
|
|
357
|
+
return "; ".join(parts) if parts else "Penalty TBD"
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TranspilerRegistry singleton for managing transpiler instances.
|
|
3
|
+
|
|
4
|
+
Provides a centralized registry mapping TranspileTarget to Transpiler
|
|
5
|
+
instances, supporting both built-in and user-registered transpilers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Optional, Type, Callable
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from yuho.transpile.base import TranspileTarget, TranspilerBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TranspilerRegistry:
|
|
15
|
+
"""
|
|
16
|
+
Singleton registry mapping TranspileTarget to Transpiler instances.
|
|
17
|
+
|
|
18
|
+
Provides lazy instantiation of transpilers and supports registration
|
|
19
|
+
of custom transpilers.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
registry = TranspilerRegistry.instance()
|
|
23
|
+
transpiler = registry.get(TranspileTarget.JSON)
|
|
24
|
+
output = transpiler.transpile(ast)
|
|
25
|
+
|
|
26
|
+
# Register custom transpiler
|
|
27
|
+
registry.register(MyCustomTarget, MyCustomTranspiler)
|
|
28
|
+
|
|
29
|
+
Thread Safety:
|
|
30
|
+
The registry is thread-safe for both reading and registration.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_instance: Optional["TranspilerRegistry"] = None
|
|
34
|
+
_lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
def __new__(cls) -> "TranspilerRegistry":
|
|
37
|
+
"""Ensure only one instance exists (singleton pattern)."""
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
with cls._lock:
|
|
40
|
+
# Double-check locking
|
|
41
|
+
if cls._instance is None:
|
|
42
|
+
cls._instance = super().__new__(cls)
|
|
43
|
+
cls._instance._initialized = False
|
|
44
|
+
return cls._instance
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
"""Initialize the registry with built-in transpilers."""
|
|
48
|
+
if self._initialized:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
with self._lock:
|
|
52
|
+
if self._initialized:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Registry maps target -> transpiler class
|
|
56
|
+
self._registry: Dict[TranspileTarget, Type[TranspilerBase]] = {}
|
|
57
|
+
# Cache of instantiated transpilers
|
|
58
|
+
self._instances: Dict[TranspileTarget, TranspilerBase] = {}
|
|
59
|
+
# Factory functions for custom creation
|
|
60
|
+
self._factories: Dict[TranspileTarget, Callable[[], TranspilerBase]] = {}
|
|
61
|
+
|
|
62
|
+
# Register built-in transpilers lazily
|
|
63
|
+
self._register_builtins()
|
|
64
|
+
self._initialized = True
|
|
65
|
+
|
|
66
|
+
def _register_builtins(self) -> None:
|
|
67
|
+
"""Register all built-in transpilers."""
|
|
68
|
+
# Import lazily to avoid circular imports
|
|
69
|
+
from yuho.transpile.json_transpiler import JSONTranspiler
|
|
70
|
+
from yuho.transpile.jsonld_transpiler import JSONLDTranspiler
|
|
71
|
+
from yuho.transpile.english_transpiler import EnglishTranspiler
|
|
72
|
+
from yuho.transpile.latex_transpiler import LaTeXTranspiler
|
|
73
|
+
from yuho.transpile.mermaid_transpiler import MermaidTranspiler
|
|
74
|
+
from yuho.transpile.alloy_transpiler import AlloyTranspiler
|
|
75
|
+
from yuho.transpile.graphql_transpiler import GraphQLTranspiler
|
|
76
|
+
from yuho.transpile.blocks_transpiler import BlocksTranspiler
|
|
77
|
+
|
|
78
|
+
self._registry[TranspileTarget.JSON] = JSONTranspiler
|
|
79
|
+
self._registry[TranspileTarget.JSON_LD] = JSONLDTranspiler
|
|
80
|
+
self._registry[TranspileTarget.ENGLISH] = EnglishTranspiler
|
|
81
|
+
self._registry[TranspileTarget.LATEX] = LaTeXTranspiler
|
|
82
|
+
self._registry[TranspileTarget.MERMAID] = MermaidTranspiler
|
|
83
|
+
self._registry[TranspileTarget.ALLOY] = AlloyTranspiler
|
|
84
|
+
self._registry[TranspileTarget.GRAPHQL] = GraphQLTranspiler
|
|
85
|
+
self._registry[TranspileTarget.BLOCKS] = BlocksTranspiler
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def instance(cls) -> "TranspilerRegistry":
|
|
89
|
+
"""
|
|
90
|
+
Get the singleton instance of the registry.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The global TranspilerRegistry instance.
|
|
94
|
+
"""
|
|
95
|
+
return cls()
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def reset(cls) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Reset the singleton instance (primarily for testing).
|
|
101
|
+
|
|
102
|
+
Clears all registered transpilers and cached instances.
|
|
103
|
+
"""
|
|
104
|
+
with cls._lock:
|
|
105
|
+
if cls._instance is not None:
|
|
106
|
+
cls._instance._registry.clear()
|
|
107
|
+
cls._instance._instances.clear()
|
|
108
|
+
cls._instance._factories.clear()
|
|
109
|
+
cls._instance._initialized = False
|
|
110
|
+
cls._instance = None
|
|
111
|
+
|
|
112
|
+
def get(self, target: TranspileTarget) -> TranspilerBase:
|
|
113
|
+
"""
|
|
114
|
+
Get a transpiler instance for the given target.
|
|
115
|
+
|
|
116
|
+
Instances are cached for reuse. If no transpiler is registered
|
|
117
|
+
for the target, raises KeyError.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
target: The transpilation target.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A transpiler instance for the target.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
KeyError: If no transpiler is registered for the target.
|
|
127
|
+
"""
|
|
128
|
+
# Check cache first
|
|
129
|
+
if target in self._instances:
|
|
130
|
+
return self._instances[target]
|
|
131
|
+
|
|
132
|
+
with self._lock:
|
|
133
|
+
# Double-check after acquiring lock
|
|
134
|
+
if target in self._instances:
|
|
135
|
+
return self._instances[target]
|
|
136
|
+
|
|
137
|
+
# Check for factory function
|
|
138
|
+
if target in self._factories:
|
|
139
|
+
instance = self._factories[target]()
|
|
140
|
+
self._instances[target] = instance
|
|
141
|
+
return instance
|
|
142
|
+
|
|
143
|
+
# Check for registered class
|
|
144
|
+
if target in self._registry:
|
|
145
|
+
instance = self._registry[target]()
|
|
146
|
+
self._instances[target] = instance
|
|
147
|
+
return instance
|
|
148
|
+
|
|
149
|
+
raise KeyError(f"No transpiler registered for target: {target}")
|
|
150
|
+
|
|
151
|
+
def get_or_none(self, target: TranspileTarget) -> Optional[TranspilerBase]:
|
|
152
|
+
"""
|
|
153
|
+
Get a transpiler instance, returning None if not registered.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
target: The transpilation target.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A transpiler instance, or None if not registered.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
return self.get(target)
|
|
163
|
+
except KeyError:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def register(
|
|
167
|
+
self,
|
|
168
|
+
target: TranspileTarget,
|
|
169
|
+
transpiler_class: Type[TranspilerBase],
|
|
170
|
+
) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Register a transpiler class for a target.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
target: The transpilation target to register.
|
|
176
|
+
transpiler_class: The transpiler class to instantiate.
|
|
177
|
+
"""
|
|
178
|
+
with self._lock:
|
|
179
|
+
self._registry[target] = transpiler_class
|
|
180
|
+
# Clear cached instance to force re-creation
|
|
181
|
+
self._instances.pop(target, None)
|
|
182
|
+
|
|
183
|
+
def register_factory(
|
|
184
|
+
self,
|
|
185
|
+
target: TranspileTarget,
|
|
186
|
+
factory: Callable[[], TranspilerBase],
|
|
187
|
+
) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Register a factory function for creating a transpiler.
|
|
190
|
+
|
|
191
|
+
Useful when transpiler creation requires custom initialization.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
target: The transpilation target to register.
|
|
195
|
+
factory: A callable that returns a TranspilerBase instance.
|
|
196
|
+
"""
|
|
197
|
+
with self._lock:
|
|
198
|
+
self._factories[target] = factory
|
|
199
|
+
# Clear cached instance to force re-creation
|
|
200
|
+
self._instances.pop(target, None)
|
|
201
|
+
|
|
202
|
+
def register_instance(
|
|
203
|
+
self,
|
|
204
|
+
target: TranspileTarget,
|
|
205
|
+
instance: TranspilerBase,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Register a pre-created transpiler instance.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
target: The transpilation target to register.
|
|
212
|
+
instance: The transpiler instance to use.
|
|
213
|
+
"""
|
|
214
|
+
with self._lock:
|
|
215
|
+
self._instances[target] = instance
|
|
216
|
+
|
|
217
|
+
def unregister(self, target: TranspileTarget) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Remove a transpiler registration.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
target: The target to unregister.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if a registration was removed, False otherwise.
|
|
226
|
+
"""
|
|
227
|
+
with self._lock:
|
|
228
|
+
removed = False
|
|
229
|
+
if target in self._registry:
|
|
230
|
+
del self._registry[target]
|
|
231
|
+
removed = True
|
|
232
|
+
if target in self._factories:
|
|
233
|
+
del self._factories[target]
|
|
234
|
+
removed = True
|
|
235
|
+
if target in self._instances:
|
|
236
|
+
del self._instances[target]
|
|
237
|
+
removed = True
|
|
238
|
+
return removed
|
|
239
|
+
|
|
240
|
+
def is_registered(self, target: TranspileTarget) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Check if a transpiler is registered for the target.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
target: The target to check.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if a transpiler is registered.
|
|
249
|
+
"""
|
|
250
|
+
return (
|
|
251
|
+
target in self._registry
|
|
252
|
+
or target in self._factories
|
|
253
|
+
or target in self._instances
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def registered_targets(self) -> list[TranspileTarget]:
|
|
257
|
+
"""
|
|
258
|
+
Get all registered transpilation targets.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of registered TranspileTarget values.
|
|
262
|
+
"""
|
|
263
|
+
targets = set(self._registry.keys())
|
|
264
|
+
targets.update(self._factories.keys())
|
|
265
|
+
targets.update(self._instances.keys())
|
|
266
|
+
return list(targets)
|
|
267
|
+
|
|
268
|
+
def clear_cache(self) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Clear all cached transpiler instances.
|
|
271
|
+
|
|
272
|
+
Registered classes and factories are preserved.
|
|
273
|
+
"""
|
|
274
|
+
with self._lock:
|
|
275
|
+
self._instances.clear()
|
yuho/verify/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho verify module - formal verification with Alloy and Z3.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Alloy model generation from statute AST
|
|
6
|
+
- Alloy analyzer integration for bounded model checking
|
|
7
|
+
- Z3 constraint generation and satisfiability checking
|
|
8
|
+
- Z3 constraint generation from AST (parallel to Alloy)
|
|
9
|
+
- Combined Alloy+Z3 verification with cross-validation
|
|
10
|
+
- Counterexample parsing and diagnostic generation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from yuho.verify.alloy import (
|
|
14
|
+
AlloyGenerator,
|
|
15
|
+
AlloyAnalyzer,
|
|
16
|
+
AlloyCounterexample,
|
|
17
|
+
)
|
|
18
|
+
from yuho.verify.z3_solver import (
|
|
19
|
+
Z3Solver,
|
|
20
|
+
Z3Generator,
|
|
21
|
+
Z3Diagnostic,
|
|
22
|
+
Z3CounterexampleExtractor,
|
|
23
|
+
ConstraintGenerator,
|
|
24
|
+
SatisfiabilityResult,
|
|
25
|
+
)
|
|
26
|
+
from yuho.verify.combined import (
|
|
27
|
+
CombinedVerifier,
|
|
28
|
+
CombinedVerificationResult,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AlloyGenerator",
|
|
33
|
+
"AlloyAnalyzer",
|
|
34
|
+
"AlloyCounterexample",
|
|
35
|
+
"Z3Solver",
|
|
36
|
+
"Z3Generator",
|
|
37
|
+
"Z3Diagnostic",
|
|
38
|
+
"Z3CounterexampleExtractor",
|
|
39
|
+
"ConstraintGenerator",
|
|
40
|
+
"SatisfiabilityResult",
|
|
41
|
+
"CombinedVerifier",
|
|
42
|
+
"CombinedVerificationResult",
|
|
43
|
+
]
|