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,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alloy transpiler - formal verification model generation.
|
|
3
|
+
|
|
4
|
+
Converts Yuho AST to Alloy specifications for bounded model checking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Set, Dict, Optional
|
|
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 AlloyTranspiler(TranspilerBase, Visitor):
|
|
15
|
+
"""
|
|
16
|
+
Transpile Yuho AST to Alloy specification language.
|
|
17
|
+
|
|
18
|
+
Generates:
|
|
19
|
+
- sig declarations from struct definitions
|
|
20
|
+
- fact constraints from statute elements
|
|
21
|
+
- pred functions from function definitions
|
|
22
|
+
- assert statements for cross-statute consistency
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._output: List[str] = []
|
|
27
|
+
self._indent = 0
|
|
28
|
+
self._known_sigs: Set[str] = set()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def target(self) -> TranspileTarget:
|
|
32
|
+
return TranspileTarget.ALLOY
|
|
33
|
+
|
|
34
|
+
def transpile(self, ast: nodes.ModuleNode) -> str:
|
|
35
|
+
"""Transpile AST to Alloy specification."""
|
|
36
|
+
self._output = []
|
|
37
|
+
self._indent = 0
|
|
38
|
+
self._known_sigs = set()
|
|
39
|
+
|
|
40
|
+
# Module header
|
|
41
|
+
self._emit("-- Alloy specification generated from Yuho")
|
|
42
|
+
self._emit("-- Formal verification model for legal statutes")
|
|
43
|
+
self._emit("")
|
|
44
|
+
|
|
45
|
+
# Built-in types
|
|
46
|
+
self._emit_builtins()
|
|
47
|
+
|
|
48
|
+
# Struct definitions -> sig declarations
|
|
49
|
+
for struct in ast.type_defs:
|
|
50
|
+
self._transpile_struct(struct)
|
|
51
|
+
self._emit("")
|
|
52
|
+
|
|
53
|
+
# Function definitions -> pred declarations
|
|
54
|
+
for func in ast.function_defs:
|
|
55
|
+
self._transpile_function(func)
|
|
56
|
+
self._emit("")
|
|
57
|
+
|
|
58
|
+
# Statutes -> facts and assertions
|
|
59
|
+
for statute in ast.statutes:
|
|
60
|
+
self._transpile_statute(statute)
|
|
61
|
+
self._emit("")
|
|
62
|
+
|
|
63
|
+
# Cross-statute consistency assertions
|
|
64
|
+
if len(ast.statutes) > 1:
|
|
65
|
+
self._emit_consistency_assertions(ast.statutes)
|
|
66
|
+
|
|
67
|
+
# Run commands
|
|
68
|
+
self._emit_run_commands(ast)
|
|
69
|
+
|
|
70
|
+
return "\n".join(self._output)
|
|
71
|
+
|
|
72
|
+
def _emit(self, line: str = "") -> None:
|
|
73
|
+
"""Add a line to output with indentation."""
|
|
74
|
+
if line:
|
|
75
|
+
indent = " " * self._indent
|
|
76
|
+
self._output.append(f"{indent}{line}")
|
|
77
|
+
else:
|
|
78
|
+
self._output.append("")
|
|
79
|
+
|
|
80
|
+
def _emit_builtins(self) -> None:
|
|
81
|
+
"""Emit built-in type signatures."""
|
|
82
|
+
self._emit("-- Built-in types")
|
|
83
|
+
self._emit("sig Bool {}")
|
|
84
|
+
self._emit("one sig True, False extends Bool {}")
|
|
85
|
+
self._emit("")
|
|
86
|
+
self._emit("sig Money {")
|
|
87
|
+
self._indent += 1
|
|
88
|
+
self._emit("amount: Int")
|
|
89
|
+
self._indent -= 1
|
|
90
|
+
self._emit("}")
|
|
91
|
+
self._emit("")
|
|
92
|
+
self._emit("sig Duration {")
|
|
93
|
+
self._indent += 1
|
|
94
|
+
self._emit("years: Int,")
|
|
95
|
+
self._emit("months: Int,")
|
|
96
|
+
self._emit("days: Int")
|
|
97
|
+
self._indent -= 1
|
|
98
|
+
self._emit("}")
|
|
99
|
+
self._emit("")
|
|
100
|
+
self._emit("sig Percent {")
|
|
101
|
+
self._indent += 1
|
|
102
|
+
self._emit("value: Int")
|
|
103
|
+
self._indent -= 1
|
|
104
|
+
self._emit("}")
|
|
105
|
+
self._emit("")
|
|
106
|
+
self._emit("-- Percent must be 0-100")
|
|
107
|
+
self._emit("fact PercentRange {")
|
|
108
|
+
self._indent += 1
|
|
109
|
+
self._emit("all p: Percent | p.value >= 0 and p.value <= 100")
|
|
110
|
+
self._indent -= 1
|
|
111
|
+
self._emit("}")
|
|
112
|
+
self._emit("")
|
|
113
|
+
|
|
114
|
+
# Track built-in sigs
|
|
115
|
+
self._known_sigs.update(["Bool", "True", "False", "Money", "Duration", "Percent"])
|
|
116
|
+
|
|
117
|
+
# =========================================================================
|
|
118
|
+
# Struct -> sig
|
|
119
|
+
# =========================================================================
|
|
120
|
+
|
|
121
|
+
def _transpile_struct(self, struct: nodes.StructDefNode) -> None:
|
|
122
|
+
"""Generate Alloy sig from struct definition."""
|
|
123
|
+
self._emit(f"-- Type: {struct.name}")
|
|
124
|
+
self._emit(f"sig {struct.name} {{")
|
|
125
|
+
self._indent += 1
|
|
126
|
+
|
|
127
|
+
for i, field in enumerate(struct.fields):
|
|
128
|
+
field_type = self._type_to_alloy(field.type_annotation)
|
|
129
|
+
comma = "," if i < len(struct.fields) - 1 else ""
|
|
130
|
+
self._emit(f"{field.name}: {field_type}{comma}")
|
|
131
|
+
|
|
132
|
+
self._indent -= 1
|
|
133
|
+
self._emit("}")
|
|
134
|
+
|
|
135
|
+
self._known_sigs.add(struct.name)
|
|
136
|
+
|
|
137
|
+
def _type_to_alloy(self, typ: nodes.TypeNode) -> str:
|
|
138
|
+
"""Convert Yuho type to Alloy type."""
|
|
139
|
+
if isinstance(typ, nodes.BuiltinType):
|
|
140
|
+
type_map = {
|
|
141
|
+
"int": "Int",
|
|
142
|
+
"float": "Int", # Alloy doesn't have floats
|
|
143
|
+
"bool": "Bool",
|
|
144
|
+
"string": "String", # May need to be modeled
|
|
145
|
+
"money": "Money",
|
|
146
|
+
"percent": "Percent",
|
|
147
|
+
"date": "Int", # Model as day count
|
|
148
|
+
"duration": "Duration",
|
|
149
|
+
"void": "none",
|
|
150
|
+
}
|
|
151
|
+
return type_map.get(typ.name, "univ")
|
|
152
|
+
elif isinstance(typ, nodes.NamedType):
|
|
153
|
+
return typ.name
|
|
154
|
+
elif isinstance(typ, nodes.OptionalType):
|
|
155
|
+
inner = self._type_to_alloy(typ.inner)
|
|
156
|
+
return f"lone {inner}"
|
|
157
|
+
elif isinstance(typ, nodes.ArrayType):
|
|
158
|
+
elem = self._type_to_alloy(typ.element_type)
|
|
159
|
+
return f"set {elem}"
|
|
160
|
+
elif isinstance(typ, nodes.GenericType):
|
|
161
|
+
# Simplify generics
|
|
162
|
+
return typ.base
|
|
163
|
+
else:
|
|
164
|
+
return "univ"
|
|
165
|
+
|
|
166
|
+
# =========================================================================
|
|
167
|
+
# Function -> pred
|
|
168
|
+
# =========================================================================
|
|
169
|
+
|
|
170
|
+
def _transpile_function(self, func: nodes.FunctionDefNode) -> None:
|
|
171
|
+
"""Generate Alloy pred from function definition."""
|
|
172
|
+
self._emit(f"-- Function: {func.name}")
|
|
173
|
+
|
|
174
|
+
# Build parameter list
|
|
175
|
+
params = []
|
|
176
|
+
for param in func.params:
|
|
177
|
+
param_type = self._type_to_alloy(param.type_annotation)
|
|
178
|
+
params.append(f"{param.name}: {param_type}")
|
|
179
|
+
|
|
180
|
+
param_str = ", ".join(params) if params else ""
|
|
181
|
+
|
|
182
|
+
self._emit(f"pred {func.name}[{param_str}] {{")
|
|
183
|
+
self._indent += 1
|
|
184
|
+
|
|
185
|
+
# Transpile body
|
|
186
|
+
for stmt in func.body.statements:
|
|
187
|
+
self._transpile_statement(stmt)
|
|
188
|
+
|
|
189
|
+
# If empty, add trivial constraint
|
|
190
|
+
if not func.body.statements:
|
|
191
|
+
self._emit("some univ")
|
|
192
|
+
|
|
193
|
+
self._indent -= 1
|
|
194
|
+
self._emit("}")
|
|
195
|
+
|
|
196
|
+
def _transpile_statement(self, stmt: nodes.ASTNode) -> None:
|
|
197
|
+
"""Transpile statement to Alloy constraint."""
|
|
198
|
+
if isinstance(stmt, nodes.ReturnStmt):
|
|
199
|
+
if stmt.value:
|
|
200
|
+
expr = self._expr_to_alloy(stmt.value)
|
|
201
|
+
self._emit(f"-- return: {expr}")
|
|
202
|
+
elif isinstance(stmt, nodes.ExpressionStmt):
|
|
203
|
+
expr = self._expr_to_alloy(stmt.expression)
|
|
204
|
+
if isinstance(stmt.expression, nodes.MatchExprNode):
|
|
205
|
+
self._transpile_match_as_constraint(stmt.expression)
|
|
206
|
+
else:
|
|
207
|
+
self._emit(expr)
|
|
208
|
+
elif isinstance(stmt, nodes.VariableDecl):
|
|
209
|
+
typ = self._type_to_alloy(stmt.type_annotation)
|
|
210
|
+
if stmt.value:
|
|
211
|
+
val = self._expr_to_alloy(stmt.value)
|
|
212
|
+
self._emit(f"some {stmt.name}: {typ} | {stmt.name} = {val}")
|
|
213
|
+
else:
|
|
214
|
+
self._emit(f"some {stmt.name}: {typ}")
|
|
215
|
+
|
|
216
|
+
# =========================================================================
|
|
217
|
+
# Statute -> facts
|
|
218
|
+
# =========================================================================
|
|
219
|
+
|
|
220
|
+
def _transpile_statute(self, statute: nodes.StatuteNode) -> None:
|
|
221
|
+
"""Generate Alloy facts from statute."""
|
|
222
|
+
title = statute.title.value if statute.title else statute.section_number
|
|
223
|
+
safe_name = self._safe_name(statute.section_number)
|
|
224
|
+
|
|
225
|
+
self._emit(f"-- Statute: Section {statute.section_number} - {title}")
|
|
226
|
+
|
|
227
|
+
# Create sig for offense
|
|
228
|
+
self._emit(f"sig {safe_name}Offense {{")
|
|
229
|
+
self._indent += 1
|
|
230
|
+
|
|
231
|
+
# Fields for each element
|
|
232
|
+
for elem in statute.elements:
|
|
233
|
+
elem_name = self._safe_name(elem.name)
|
|
234
|
+
self._emit(f"{elem_name}: Bool,")
|
|
235
|
+
|
|
236
|
+
# Add penalty fields if present
|
|
237
|
+
if statute.penalty:
|
|
238
|
+
if statute.penalty.imprisonment_max:
|
|
239
|
+
self._emit("imprisonmentApplies: Bool,")
|
|
240
|
+
if statute.penalty.fine_max:
|
|
241
|
+
self._emit("fineApplies: Bool,")
|
|
242
|
+
|
|
243
|
+
self._emit("guilty: Bool")
|
|
244
|
+
self._indent -= 1
|
|
245
|
+
self._emit("}")
|
|
246
|
+
self._emit("")
|
|
247
|
+
|
|
248
|
+
self._known_sigs.add(f"{safe_name}Offense")
|
|
249
|
+
|
|
250
|
+
# Facts for element requirements
|
|
251
|
+
self._emit(f"fact {safe_name}ElementRequirements {{")
|
|
252
|
+
self._indent += 1
|
|
253
|
+
|
|
254
|
+
# All elements must be true for guilt (conjunction)
|
|
255
|
+
actus_reus_elems = [self._safe_name(e.name) for e in statute.elements if e.element_type == "actus_reus"]
|
|
256
|
+
mens_rea_elems = [self._safe_name(e.name) for e in statute.elements if e.element_type == "mens_rea"]
|
|
257
|
+
|
|
258
|
+
self._emit(f"all o: {safe_name}Offense |")
|
|
259
|
+
self._indent += 1
|
|
260
|
+
|
|
261
|
+
conditions = []
|
|
262
|
+
for elem in statute.elements:
|
|
263
|
+
elem_name = self._safe_name(elem.name)
|
|
264
|
+
conditions.append(f"o.{elem_name} = True")
|
|
265
|
+
|
|
266
|
+
if conditions:
|
|
267
|
+
condition_str = " and\n ".join(conditions)
|
|
268
|
+
self._emit(f"o.guilty = True iff ({condition_str})")
|
|
269
|
+
else:
|
|
270
|
+
self._emit("o.guilty = True")
|
|
271
|
+
|
|
272
|
+
self._indent -= 2
|
|
273
|
+
self._emit("}")
|
|
274
|
+
self._emit("")
|
|
275
|
+
|
|
276
|
+
# Generate match expression constraints
|
|
277
|
+
for elem in statute.elements:
|
|
278
|
+
if isinstance(elem.description, nodes.MatchExprNode):
|
|
279
|
+
self._emit(f"-- Element: {elem.name}")
|
|
280
|
+
self._emit(f"fact {safe_name}_{self._safe_name(elem.name)} {{")
|
|
281
|
+
self._indent += 1
|
|
282
|
+
self._transpile_match_as_constraint(elem.description)
|
|
283
|
+
self._indent -= 1
|
|
284
|
+
self._emit("}")
|
|
285
|
+
self._emit("")
|
|
286
|
+
|
|
287
|
+
def _transpile_match_as_constraint(self, match: nodes.MatchExprNode) -> None:
|
|
288
|
+
"""Convert match expression to Alloy disjunction constraint."""
|
|
289
|
+
if not match.arms:
|
|
290
|
+
self._emit("some univ")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Each arm becomes a disjunct
|
|
294
|
+
arm_constraints = []
|
|
295
|
+
for arm in match.arms:
|
|
296
|
+
pattern_constraint = self._pattern_to_constraint(arm.pattern)
|
|
297
|
+
guard_constraint = self._expr_to_alloy(arm.guard) if arm.guard else "some univ"
|
|
298
|
+
body_constraint = self._expr_to_alloy(arm.body)
|
|
299
|
+
|
|
300
|
+
if isinstance(arm.pattern, nodes.WildcardPattern):
|
|
301
|
+
# Wildcard is the default case
|
|
302
|
+
arm_constraints.append(f"({body_constraint})")
|
|
303
|
+
else:
|
|
304
|
+
arm_constraints.append(f"({pattern_constraint} and {guard_constraint} implies {body_constraint})")
|
|
305
|
+
|
|
306
|
+
self._emit(" or\n ".join(arm_constraints))
|
|
307
|
+
|
|
308
|
+
def _pattern_to_constraint(self, pattern: nodes.PatternNode) -> str:
|
|
309
|
+
"""Convert pattern to Alloy constraint."""
|
|
310
|
+
if isinstance(pattern, nodes.WildcardPattern):
|
|
311
|
+
return "some univ"
|
|
312
|
+
elif isinstance(pattern, nodes.LiteralPattern):
|
|
313
|
+
return self._expr_to_alloy(pattern.literal)
|
|
314
|
+
elif isinstance(pattern, nodes.BindingPattern):
|
|
315
|
+
return f"some {pattern.name}"
|
|
316
|
+
elif isinstance(pattern, nodes.StructPattern):
|
|
317
|
+
fields = " and ".join(f"some _.{fp.name}" for fp in pattern.fields)
|
|
318
|
+
return f"some _: {pattern.type_name} | {fields}"
|
|
319
|
+
else:
|
|
320
|
+
return "some univ"
|
|
321
|
+
|
|
322
|
+
def _expr_to_alloy(self, expr: nodes.ASTNode) -> str:
|
|
323
|
+
"""Convert expression to Alloy."""
|
|
324
|
+
if isinstance(expr, nodes.IntLit):
|
|
325
|
+
return str(expr.value)
|
|
326
|
+
elif isinstance(expr, nodes.FloatLit):
|
|
327
|
+
return str(int(expr.value)) # Truncate to int
|
|
328
|
+
elif isinstance(expr, nodes.BoolLit):
|
|
329
|
+
return "True" if expr.value else "False"
|
|
330
|
+
elif isinstance(expr, nodes.StringLit):
|
|
331
|
+
# Strings need to be modeled; use placeholder
|
|
332
|
+
return f'"{expr.value}"'
|
|
333
|
+
elif isinstance(expr, nodes.IdentifierNode):
|
|
334
|
+
return expr.name
|
|
335
|
+
elif isinstance(expr, nodes.FieldAccessNode):
|
|
336
|
+
base = self._expr_to_alloy(expr.base)
|
|
337
|
+
return f"{base}.{expr.field_name}"
|
|
338
|
+
elif isinstance(expr, nodes.BinaryExprNode):
|
|
339
|
+
left = self._expr_to_alloy(expr.left)
|
|
340
|
+
right = self._expr_to_alloy(expr.right)
|
|
341
|
+
op = self._op_to_alloy(expr.operator)
|
|
342
|
+
return f"({left} {op} {right})"
|
|
343
|
+
elif isinstance(expr, nodes.UnaryExprNode):
|
|
344
|
+
operand = self._expr_to_alloy(expr.operand)
|
|
345
|
+
if expr.operator == "!":
|
|
346
|
+
return f"not {operand}"
|
|
347
|
+
return f"({expr.operator}{operand})"
|
|
348
|
+
elif isinstance(expr, nodes.PassExprNode):
|
|
349
|
+
return "none"
|
|
350
|
+
elif isinstance(expr, nodes.FunctionCallNode):
|
|
351
|
+
callee = self._expr_to_alloy(expr.callee)
|
|
352
|
+
args = ", ".join(self._expr_to_alloy(a) for a in expr.args)
|
|
353
|
+
return f"{callee}[{args}]"
|
|
354
|
+
else:
|
|
355
|
+
return "some univ"
|
|
356
|
+
|
|
357
|
+
def _op_to_alloy(self, op: str) -> str:
|
|
358
|
+
"""Convert operator to Alloy operator."""
|
|
359
|
+
op_map = {
|
|
360
|
+
"+": "add",
|
|
361
|
+
"-": "sub",
|
|
362
|
+
"*": "mul",
|
|
363
|
+
"/": "div",
|
|
364
|
+
"==": "=",
|
|
365
|
+
"!=": "!=",
|
|
366
|
+
"<": "<",
|
|
367
|
+
">": ">",
|
|
368
|
+
"<=": "<=",
|
|
369
|
+
">=": ">=",
|
|
370
|
+
"&&": "and",
|
|
371
|
+
"||": "or",
|
|
372
|
+
}
|
|
373
|
+
return op_map.get(op, op)
|
|
374
|
+
|
|
375
|
+
# =========================================================================
|
|
376
|
+
# Assertions
|
|
377
|
+
# =========================================================================
|
|
378
|
+
|
|
379
|
+
def _emit_consistency_assertions(self, statutes: tuple) -> None:
|
|
380
|
+
"""Emit cross-statute consistency assertions."""
|
|
381
|
+
self._emit("-- Cross-statute consistency assertions")
|
|
382
|
+
self._emit("")
|
|
383
|
+
|
|
384
|
+
# Assert: no contradictory elements
|
|
385
|
+
self._emit("assert NoContradictoryElements {")
|
|
386
|
+
self._indent += 1
|
|
387
|
+
self._emit("-- No offense can have contradictory required elements")
|
|
388
|
+
self._emit("-- (This is a placeholder; specific contradictions should be modeled)")
|
|
389
|
+
self._emit("some univ")
|
|
390
|
+
self._indent -= 1
|
|
391
|
+
self._emit("}")
|
|
392
|
+
self._emit("")
|
|
393
|
+
|
|
394
|
+
# Assert: penalty ordering (more serious offenses have higher penalties)
|
|
395
|
+
if len(statutes) >= 2:
|
|
396
|
+
self._emit("assert PenaltyOrdering {")
|
|
397
|
+
self._indent += 1
|
|
398
|
+
self._emit("-- More serious offenses should have higher or equal penalties")
|
|
399
|
+
self._emit("-- (Specific ordering relationships should be modeled)")
|
|
400
|
+
self._emit("some univ")
|
|
401
|
+
self._indent -= 1
|
|
402
|
+
self._emit("}")
|
|
403
|
+
self._emit("")
|
|
404
|
+
|
|
405
|
+
def _emit_run_commands(self, ast: nodes.ModuleNode) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Emit run and check commands for bounded model checking.
|
|
408
|
+
|
|
409
|
+
Generates:
|
|
410
|
+
- run commands to find satisfying instances
|
|
411
|
+
- check commands to verify assertions
|
|
412
|
+
- parameterized scope bounds
|
|
413
|
+
"""
|
|
414
|
+
self._emit("-- =========================================================================")
|
|
415
|
+
self._emit("-- Verification commands for bounded model checking")
|
|
416
|
+
self._emit("-- =========================================================================")
|
|
417
|
+
self._emit("")
|
|
418
|
+
|
|
419
|
+
# Configuration comment
|
|
420
|
+
self._emit("-- Scope configuration (adjust bounds as needed)")
|
|
421
|
+
self._emit("-- Default scope is 5 atoms per sig, Int scope is 4 bits (-8 to 7)")
|
|
422
|
+
self._emit("")
|
|
423
|
+
|
|
424
|
+
# Run commands for finding satisfying offense instances
|
|
425
|
+
self._emit("-- Run commands: find satisfying instances")
|
|
426
|
+
for statute in ast.statutes:
|
|
427
|
+
safe_name = self._safe_name(statute.section_number)
|
|
428
|
+
offense_sig = f"{safe_name}Offense"
|
|
429
|
+
|
|
430
|
+
# Basic run: find any instance
|
|
431
|
+
self._emit(f"run show{safe_name}Instance {{")
|
|
432
|
+
self._indent += 1
|
|
433
|
+
self._emit(f"some o: {offense_sig} | o.guilty = True")
|
|
434
|
+
self._indent -= 1
|
|
435
|
+
self._emit("} for 3 but 4 Int")
|
|
436
|
+
self._emit("")
|
|
437
|
+
|
|
438
|
+
# Run with all elements satisfied
|
|
439
|
+
self._emit(f"run show{safe_name}GuiltyScenario {{")
|
|
440
|
+
self._indent += 1
|
|
441
|
+
self._emit(f"some o: {offense_sig} |")
|
|
442
|
+
self._indent += 1
|
|
443
|
+
conditions = [f"o.{self._safe_name(e.name)} = True" for e in statute.elements]
|
|
444
|
+
if conditions:
|
|
445
|
+
self._emit(" and ".join(conditions))
|
|
446
|
+
else:
|
|
447
|
+
self._emit("o.guilty = True")
|
|
448
|
+
self._indent -= 1
|
|
449
|
+
self._indent -= 1
|
|
450
|
+
self._emit("} for 5 but 4 Int")
|
|
451
|
+
self._emit("")
|
|
452
|
+
|
|
453
|
+
# Run to find innocent scenario (not guilty)
|
|
454
|
+
self._emit(f"run show{safe_name}InnocentScenario {{")
|
|
455
|
+
self._indent += 1
|
|
456
|
+
self._emit(f"some o: {offense_sig} | o.guilty = False")
|
|
457
|
+
self._indent -= 1
|
|
458
|
+
self._emit("} for 3 but 4 Int")
|
|
459
|
+
self._emit("")
|
|
460
|
+
|
|
461
|
+
# Check commands for assertions
|
|
462
|
+
self._emit("-- Check commands: verify assertions with counterexample search")
|
|
463
|
+
self._emit("")
|
|
464
|
+
|
|
465
|
+
# Check element consistency
|
|
466
|
+
for statute in ast.statutes:
|
|
467
|
+
safe_name = self._safe_name(statute.section_number)
|
|
468
|
+
offense_sig = f"{safe_name}Offense"
|
|
469
|
+
|
|
470
|
+
# Check that guilty implies all elements true
|
|
471
|
+
self._emit(f"assert {safe_name}GuiltyImpliesElements {{")
|
|
472
|
+
self._indent += 1
|
|
473
|
+
self._emit(f"all o: {offense_sig} | o.guilty = True implies (")
|
|
474
|
+
self._indent += 1
|
|
475
|
+
conditions = [f"o.{self._safe_name(e.name)} = True" for e in statute.elements]
|
|
476
|
+
if conditions:
|
|
477
|
+
self._emit(" and ".join(conditions))
|
|
478
|
+
else:
|
|
479
|
+
self._emit("True = True") # Trivially true
|
|
480
|
+
self._indent -= 1
|
|
481
|
+
self._emit(")")
|
|
482
|
+
self._indent -= 1
|
|
483
|
+
self._emit("}")
|
|
484
|
+
self._emit(f"check {safe_name}GuiltyImpliesElements for 5 but 4 Int")
|
|
485
|
+
self._emit("")
|
|
486
|
+
|
|
487
|
+
# Check that all elements true implies guilty
|
|
488
|
+
if statute.elements:
|
|
489
|
+
self._emit(f"assert {safe_name}ElementsImplyGuilty {{")
|
|
490
|
+
self._indent += 1
|
|
491
|
+
self._emit(f"all o: {offense_sig} | (")
|
|
492
|
+
self._indent += 1
|
|
493
|
+
self._emit(" and ".join(conditions))
|
|
494
|
+
self._indent -= 1
|
|
495
|
+
self._emit(f") implies o.guilty = True")
|
|
496
|
+
self._indent -= 1
|
|
497
|
+
self._emit("}")
|
|
498
|
+
self._emit(f"check {safe_name}ElementsImplyGuilty for 5 but 4 Int")
|
|
499
|
+
self._emit("")
|
|
500
|
+
|
|
501
|
+
# Global consistency assertions
|
|
502
|
+
self._emit("check NoContradictoryElements for 5 but 4 Int")
|
|
503
|
+
if len(ast.statutes) >= 2:
|
|
504
|
+
self._emit("check PenaltyOrdering for 5 but 4 Int")
|
|
505
|
+
self._emit("")
|
|
506
|
+
|
|
507
|
+
# Negative checks (looking for counterexamples to impossibilities)
|
|
508
|
+
self._emit("-- Negative checks: these should find NO counterexamples")
|
|
509
|
+
for statute in ast.statutes:
|
|
510
|
+
safe_name = self._safe_name(statute.section_number)
|
|
511
|
+
offense_sig = f"{safe_name}Offense"
|
|
512
|
+
|
|
513
|
+
# Check: it's impossible to be guilty with no elements satisfied
|
|
514
|
+
if statute.elements:
|
|
515
|
+
self._emit(f"assert {safe_name}NoElementsNoGuilt {{")
|
|
516
|
+
self._indent += 1
|
|
517
|
+
no_conditions = [f"o.{self._safe_name(e.name)} = False" for e in statute.elements]
|
|
518
|
+
self._emit(f"all o: {offense_sig} | (")
|
|
519
|
+
self._indent += 1
|
|
520
|
+
self._emit(" and ".join(no_conditions))
|
|
521
|
+
self._indent -= 1
|
|
522
|
+
self._emit(") implies o.guilty = False")
|
|
523
|
+
self._indent -= 1
|
|
524
|
+
self._emit("}")
|
|
525
|
+
self._emit(f"check {safe_name}NoElementsNoGuilt for 5 but 4 Int")
|
|
526
|
+
self._emit("")
|
|
527
|
+
|
|
528
|
+
# Exhaustive exploration hint
|
|
529
|
+
self._emit("-- =========================================================================")
|
|
530
|
+
self._emit("-- To run in Alloy Analyzer:")
|
|
531
|
+
self._emit("-- 1. Open this file in Alloy Analyzer")
|
|
532
|
+
self._emit("-- 2. Execute 'run' commands to find satisfying instances")
|
|
533
|
+
self._emit("-- 3. Execute 'check' commands to verify assertions")
|
|
534
|
+
self._emit("-- 4. Green checkmark = assertion holds within scope")
|
|
535
|
+
self._emit("-- 5. Red X = counterexample found (click to view)")
|
|
536
|
+
self._emit("-- =========================================================================")
|
|
537
|
+
|
|
538
|
+
def _safe_name(self, name: str) -> str:
|
|
539
|
+
"""Convert name to safe Alloy identifier."""
|
|
540
|
+
# Remove special chars, capitalize
|
|
541
|
+
safe = "".join(c if c.isalnum() else "_" for c in name)
|
|
542
|
+
# Ensure starts with letter
|
|
543
|
+
if safe and safe[0].isdigit():
|
|
544
|
+
safe = "S" + safe
|
|
545
|
+
return safe
|
|
546
|
+
|
yuho/transpile/base.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transpiler base class and target enum.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
from yuho.ast.nodes import ModuleNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TranspileTarget(Enum):
|
|
12
|
+
"""Supported transpilation targets."""
|
|
13
|
+
|
|
14
|
+
JSON = auto()
|
|
15
|
+
JSON_LD = auto()
|
|
16
|
+
ENGLISH = auto()
|
|
17
|
+
LATEX = auto()
|
|
18
|
+
MERMAID = auto()
|
|
19
|
+
ALLOY = auto()
|
|
20
|
+
GRAPHQL = auto()
|
|
21
|
+
BLOCKS = auto()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_string(cls, name: str) -> "TranspileTarget":
|
|
25
|
+
"""Convert string to TranspileTarget."""
|
|
26
|
+
mapping = {
|
|
27
|
+
"json": cls.JSON,
|
|
28
|
+
"jsonld": cls.JSON_LD,
|
|
29
|
+
"json-ld": cls.JSON_LD,
|
|
30
|
+
"english": cls.ENGLISH,
|
|
31
|
+
"en": cls.ENGLISH,
|
|
32
|
+
"latex": cls.LATEX,
|
|
33
|
+
"tex": cls.LATEX,
|
|
34
|
+
"mermaid": cls.MERMAID,
|
|
35
|
+
"mmd": cls.MERMAID,
|
|
36
|
+
"alloy": cls.ALLOY,
|
|
37
|
+
"graphql": cls.GRAPHQL,
|
|
38
|
+
"gql": cls.GRAPHQL,
|
|
39
|
+
"blocks": cls.BLOCKS,
|
|
40
|
+
"block": cls.BLOCKS,
|
|
41
|
+
}
|
|
42
|
+
target = mapping.get(name.lower())
|
|
43
|
+
if not target:
|
|
44
|
+
raise ValueError(f"Unknown transpile target: {name}")
|
|
45
|
+
return target
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def file_extension(self) -> str:
|
|
49
|
+
"""Return the file extension for this target."""
|
|
50
|
+
extensions = {
|
|
51
|
+
TranspileTarget.JSON: ".json",
|
|
52
|
+
TranspileTarget.JSON_LD: ".jsonld",
|
|
53
|
+
TranspileTarget.ENGLISH: ".txt",
|
|
54
|
+
TranspileTarget.LATEX: ".tex",
|
|
55
|
+
TranspileTarget.MERMAID: ".mmd",
|
|
56
|
+
TranspileTarget.ALLOY: ".als",
|
|
57
|
+
TranspileTarget.GRAPHQL: ".graphql",
|
|
58
|
+
TranspileTarget.BLOCKS: ".blocks",
|
|
59
|
+
}
|
|
60
|
+
return extensions.get(self, ".txt")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TranspilerBase(ABC):
|
|
64
|
+
"""
|
|
65
|
+
Abstract base class for transpilers.
|
|
66
|
+
|
|
67
|
+
Subclasses must implement the transpile() method to convert
|
|
68
|
+
a Yuho AST (ModuleNode) to the target format.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def target(self) -> TranspileTarget:
|
|
74
|
+
"""Return the transpilation target."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def transpile(self, ast: ModuleNode) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Transpile a Yuho AST to the target format.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
ast: The root ModuleNode of the AST
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The transpiled output as a string
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def transpile_to_file(self, ast: ModuleNode, path: str) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Transpile and write to a file.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
ast: The root ModuleNode of the AST
|
|
96
|
+
path: Output file path
|
|
97
|
+
"""
|
|
98
|
+
output = self.transpile(ast)
|
|
99
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
100
|
+
f.write(output)
|