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,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)