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,406 @@
1
+ """
2
+ LaTeX transpiler - legal document generation.
3
+
4
+ Converts Yuho AST to LaTeX documents with professional legal document
5
+ formatting including:
6
+ - Article class with proper legal document preamble
7
+ - Section formatting with statute numbers
8
+ - Margin notes and cross-references
9
+ - Penalty tables with imprisonment/fine columns
10
+ - Illustration blocks with gray background and italic text
11
+ """
12
+
13
+ from typing import List, Optional
14
+ import subprocess
15
+ import os
16
+ import shutil
17
+
18
+ from yuho.ast import nodes
19
+ from yuho.ast.visitor import Visitor
20
+ from yuho.transpile.base import TranspileTarget, TranspilerBase
21
+ from yuho.transpile.latex_preamble import generate_preamble
22
+ from yuho.transpile.latex_utils import (
23
+ escape_latex,
24
+ operator_to_latex,
25
+ duration_to_latex,
26
+ money_to_latex,
27
+ type_to_latex,
28
+ expr_to_latex,
29
+ pattern_to_latex,
30
+ statement_to_latex,
31
+ )
32
+
33
+
34
+ class LaTeXTranspiler(TranspilerBase, Visitor):
35
+ """
36
+ Transpile Yuho AST to LaTeX documents.
37
+
38
+ Generates professional legal documents with proper typographic
39
+ conventions for statutes, penalties, and illustrations.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ document_title: str = "Legal Statutes",
45
+ author: str = "",
46
+ include_toc: bool = True,
47
+ use_margins: bool = True,
48
+ ):
49
+ """
50
+ Initialize LaTeX transpiler.
51
+
52
+ Args:
53
+ document_title: Title for the generated document
54
+ author: Document author
55
+ include_toc: Whether to include table of contents
56
+ use_margins: Whether to use margin notes for annotations
57
+ """
58
+ self._output: List[str] = []
59
+ self._indent_level = 0
60
+ self.document_title = document_title
61
+ self.author = author
62
+ self.include_toc = include_toc
63
+ self.use_margins = use_margins
64
+ self._section_refs: dict = {} # For cross-references
65
+
66
+ @property
67
+ def target(self) -> TranspileTarget:
68
+ return TranspileTarget.LATEX
69
+
70
+ def transpile(self, ast: nodes.ModuleNode) -> str:
71
+ """Transpile AST to LaTeX document."""
72
+ self._output = []
73
+ self._indent_level = 0
74
+ self._section_refs = {}
75
+
76
+ # Emit preamble from module
77
+ preamble_lines = generate_preamble(
78
+ document_title=self.document_title,
79
+ author=self.author,
80
+ use_margins=self.use_margins,
81
+ )
82
+ for line in preamble_lines:
83
+ self._emit(line)
84
+
85
+ # Begin document
86
+ self._emit(r"\begin{document}")
87
+ self._emit("")
88
+
89
+ # Title
90
+ self._emit(r"\maketitle")
91
+ self._emit("")
92
+
93
+ # Table of contents
94
+ if self.include_toc:
95
+ self._emit(r"\tableofcontents")
96
+ self._emit(r"\newpage")
97
+ self._emit("")
98
+
99
+ # Visit module content
100
+ self._visit_module(ast)
101
+
102
+ # End document
103
+ self._emit("")
104
+ self._emit(r"\end{document}")
105
+
106
+ return "\n".join(self._output)
107
+
108
+ def _emit(self, text: str) -> None:
109
+ """Add a line to output with current indentation."""
110
+ indent = " " * self._indent_level
111
+ self._output.append(f"{indent}{text}")
112
+
113
+ def _emit_blank(self) -> None:
114
+ """Add a blank line."""
115
+ self._output.append("")
116
+
117
+ # =========================================================================
118
+ # Module and imports
119
+ # =========================================================================
120
+
121
+ def _visit_module(self, node: nodes.ModuleNode) -> None:
122
+ """Generate LaTeX for entire module."""
123
+ # Imports (as references)
124
+ if node.imports:
125
+ self._emit(r"\section*{References}")
126
+ self._emit(r"\begin{itemize}")
127
+ for imp in node.imports:
128
+ self._visit_import(imp)
129
+ self._emit(r"\end{itemize}")
130
+ self._emit_blank()
131
+
132
+ # Type definitions
133
+ if node.type_defs:
134
+ self._emit(r"\section{Type Definitions}")
135
+ for struct in node.type_defs:
136
+ self._visit_struct_def(struct)
137
+ self._emit_blank()
138
+
139
+ # Functions
140
+ if node.function_defs:
141
+ self._emit(r"\section{Functions}")
142
+ for func in node.function_defs:
143
+ self._visit_function_def(func)
144
+ self._emit_blank()
145
+
146
+ # Statutes (main content)
147
+ if node.statutes:
148
+ self._emit(r"\section{Statutes}")
149
+ for statute in node.statutes:
150
+ self._visit_statute(statute)
151
+ self._emit_blank()
152
+
153
+ def _visit_import(self, node: nodes.ImportNode) -> None:
154
+ """Generate LaTeX for import."""
155
+ path = escape_latex(node.path)
156
+ if node.is_wildcard:
157
+ self._emit(rf" \item All definitions from \texttt{{{path}}}")
158
+ elif node.imported_names:
159
+ names = ", ".join(rf"\texttt{{{escape_latex(n)}}}" for n in node.imported_names)
160
+ self._emit(rf" \item {names} from \texttt{{{path}}}")
161
+ else:
162
+ self._emit(rf" \item \texttt{{{path}}}")
163
+
164
+ # =========================================================================
165
+ # Statute blocks
166
+ # =========================================================================
167
+
168
+ def _visit_statute(self, node: nodes.StatuteNode) -> None:
169
+ """Generate LaTeX for statute."""
170
+ section_num = escape_latex(node.section_number)
171
+ title = escape_latex(node.title.value if node.title else "Untitled")
172
+
173
+ # Store reference
174
+ self._section_refs[node.section_number] = title
175
+
176
+ # Statute header
177
+ self._emit(rf"\statute{{{section_num}}}{{{title}}}")
178
+ self._emit_blank()
179
+
180
+ # Margin note with section number
181
+ if self.use_margins:
182
+ self._emit(rf"\marginnote{{S. {section_num}}}")
183
+
184
+ # Definitions
185
+ if node.definitions:
186
+ self._emit(r"\paragraph{Definitions}")
187
+ self._emit(r"\begin{legaldefs}")
188
+ for defn in node.definitions:
189
+ term = escape_latex(defn.term)
190
+ definition = escape_latex(defn.definition.value)
191
+ self._emit(rf" \item[\textbf{{{term}}}] {definition}")
192
+ self._emit(r"\end{legaldefs}")
193
+ self._emit_blank()
194
+
195
+ # Elements
196
+ if node.elements:
197
+ self._emit(r"\paragraph{Elements of the Offence}")
198
+ self._emit(r"\begin{enumerate}")
199
+ for elem in node.elements:
200
+ self._visit_element(elem)
201
+ self._emit(r"\end{enumerate}")
202
+ self._emit_blank()
203
+
204
+ # Penalty
205
+ if node.penalty:
206
+ self._emit(r"\paragraph{Penalty}")
207
+ self._visit_penalty(node.penalty)
208
+ self._emit_blank()
209
+
210
+ # Illustrations
211
+ if node.illustrations:
212
+ self._emit(r"\paragraph{Illustrations}")
213
+ self._emit_blank()
214
+ for i, illus in enumerate(node.illustrations, 1):
215
+ self._visit_illustration(illus, i)
216
+
217
+ def _visit_element(self, node: nodes.ElementNode) -> None:
218
+ """Generate LaTeX for element."""
219
+ type_labels = {
220
+ "actus_reus": "Actus Reus",
221
+ "mens_rea": "Mens Rea",
222
+ "circumstance": "Circumstance",
223
+ }
224
+ label = type_labels.get(node.element_type, node.element_type.replace("_", " ").title())
225
+
226
+ # Margin annotation for element type
227
+ if self.use_margins:
228
+ margin = rf"\marginnote{{\scriptsize {label}}}"
229
+ else:
230
+ margin = ""
231
+
232
+ # Handle description
233
+ if isinstance(node.description, nodes.StringLit):
234
+ desc = escape_latex(node.description.value)
235
+ self._emit(rf" \item{margin} \element{{{label}}}{{{desc}}}")
236
+ elif isinstance(node.description, nodes.MatchExprNode):
237
+ name = escape_latex(node.name)
238
+ self._emit(rf" \item{margin} \element{{{label}}}{{{name}:}}")
239
+ self._emit(r" \begin{itemize}")
240
+ self._visit_match_expr_latex(node.description)
241
+ self._emit(r" \end{itemize}")
242
+ else:
243
+ desc = expr_to_latex(node.description)
244
+ self._emit(rf" \item{margin} \element{{{label}}}{{{desc}}}")
245
+
246
+ def _visit_penalty(self, node: nodes.PenaltyNode) -> None:
247
+ """Generate LaTeX penalty table."""
248
+ self._emit(r"\begin{center}")
249
+ self._emit(r"\begin{tabular}{@{}lll@{}}")
250
+ self._emit(r"\toprule")
251
+ self._emit(r"\textbf{Type} & \textbf{Minimum} & \textbf{Maximum} \\")
252
+ self._emit(r"\midrule")
253
+
254
+ # Imprisonment row
255
+ if node.imprisonment_max:
256
+ min_str = duration_to_latex(node.imprisonment_min) if node.imprisonment_min else "---"
257
+ max_str = duration_to_latex(node.imprisonment_max)
258
+ self._emit(rf"Imprisonment & {min_str} & {max_str} \\")
259
+
260
+ # Fine row
261
+ if node.fine_max:
262
+ min_str = money_to_latex(node.fine_min) if node.fine_min else "---"
263
+ max_str = money_to_latex(node.fine_max)
264
+ self._emit(rf"Fine & {min_str} & {max_str} \\")
265
+
266
+ self._emit(r"\bottomrule")
267
+ self._emit(r"\end{tabular}")
268
+ self._emit(r"\end{center}")
269
+
270
+ # Supplementary information
271
+ if node.supplementary:
272
+ self._emit_blank()
273
+ supp = escape_latex(node.supplementary.value)
274
+ self._emit(rf"\textit{{Additional: {supp}}}")
275
+
276
+ def _visit_illustration(self, node: nodes.IllustrationNode, index: int) -> None:
277
+ """Generate LaTeX for illustration with gray background and italic text."""
278
+ label = node.label or f"({chr(ord('a') + index - 1)})"
279
+ desc = escape_latex(node.description.value)
280
+
281
+ self._emit(r"\begin{illustrationbox}")
282
+ self._emit(rf"\textbf{{{label}}} {desc}")
283
+ self._emit(r"\end{illustrationbox}")
284
+ self._emit_blank()
285
+
286
+ # =========================================================================
287
+ # Match expressions
288
+ # =========================================================================
289
+
290
+ def _visit_match_expr_latex(self, node: nodes.MatchExprNode) -> None:
291
+ """Generate LaTeX for match expression."""
292
+ if node.scrutinee:
293
+ scrutinee = expr_to_latex(node.scrutinee)
294
+ self._emit(rf" \item[] \textit{{Based on {scrutinee}:}}")
295
+
296
+ for i, arm in enumerate(node.arms):
297
+ self._visit_match_arm_latex(arm, i, len(node.arms))
298
+
299
+ def _visit_match_arm_latex(self, node: nodes.MatchArm, index: int, total: int) -> None:
300
+ """Generate LaTeX for match arm."""
301
+ pattern = pattern_to_latex(node.pattern)
302
+ body = expr_to_latex(node.body)
303
+
304
+ # Guard
305
+ guard_str = ""
306
+ if node.guard:
307
+ guard = expr_to_latex(node.guard)
308
+ guard_str = f", provided that {guard}"
309
+
310
+ # Connector
311
+ if isinstance(node.pattern, nodes.WildcardPattern):
312
+ self._emit(rf" \item \textit{{Otherwise{guard_str}:}} {body}")
313
+ else:
314
+ self._emit(rf" \item If {pattern}{guard_str}: {body}")
315
+
316
+ # =========================================================================
317
+ # Struct definitions
318
+ # =========================================================================
319
+
320
+ def _visit_struct_def(self, node: nodes.StructDefNode) -> None:
321
+ """Generate LaTeX for struct definition."""
322
+ name = escape_latex(node.name)
323
+ self._emit(rf"\paragraph{{{name}}}")
324
+ self._emit(r"\begin{description}")
325
+ for field in node.fields:
326
+ field_name = escape_latex(field.name)
327
+ type_str = type_to_latex(field.type_annotation)
328
+ self._emit(rf" \item[\texttt{{{field_name}}}] {type_str}")
329
+ self._emit(r"\end{description}")
330
+
331
+ # =========================================================================
332
+ # Function definitions
333
+ # =========================================================================
334
+
335
+ def _visit_function_def(self, node: nodes.FunctionDefNode) -> None:
336
+ """Generate LaTeX for function definition."""
337
+ name = escape_latex(node.name)
338
+ params = ", ".join(
339
+ rf"\texttt{{{escape_latex(p.name)}}}: {type_to_latex(p.type_annotation)}"
340
+ for p in node.params
341
+ )
342
+ ret = f" $\\rightarrow$ {type_to_latex(node.return_type)}" if node.return_type else ""
343
+
344
+ self._emit(rf"\paragraph{{\texttt{{{name}}}({params}){ret}}}")
345
+ self._emit(r"\begin{quote}")
346
+ for stmt in node.body.statements:
347
+ stmt_latex = statement_to_latex(stmt)
348
+ self._emit(stmt_latex)
349
+ self._emit(r"\end{quote}")
350
+
351
+
352
+ def compile_to_pdf(
353
+ latex_file: str,
354
+ output_dir: Optional[str] = None,
355
+ compiler: str = "pdflatex",
356
+ ) -> Optional[str]:
357
+ """
358
+ Compile LaTeX file to PDF using pdflatex or xelatex.
359
+
360
+ Args:
361
+ latex_file: Path to the .tex file
362
+ output_dir: Output directory (defaults to same as input)
363
+ compiler: LaTeX compiler to use ('pdflatex' or 'xelatex')
364
+
365
+ Returns:
366
+ Path to generated PDF, or None if compilation failed
367
+ """
368
+ if not os.path.exists(latex_file):
369
+ raise FileNotFoundError(f"LaTeX file not found: {latex_file}")
370
+
371
+ # Find compiler
372
+ compiler_path = shutil.which(compiler)
373
+ if not compiler_path:
374
+ raise RuntimeError(f"Compiler not found: {compiler}")
375
+
376
+ # Prepare output directory
377
+ if output_dir is None:
378
+ output_dir = os.path.dirname(latex_file) or "."
379
+
380
+ # Run compiler (twice for cross-references)
381
+ cmd = [
382
+ compiler_path,
383
+ "-interaction=nonstopmode",
384
+ f"-output-directory={output_dir}",
385
+ latex_file,
386
+ ]
387
+
388
+ try:
389
+ # First pass
390
+ subprocess.run(cmd, check=True, capture_output=True, timeout=60)
391
+ # Second pass for cross-references
392
+ subprocess.run(cmd, check=True, capture_output=True, timeout=60)
393
+
394
+ # Determine output path
395
+ base_name = os.path.splitext(os.path.basename(latex_file))[0]
396
+ pdf_path = os.path.join(output_dir, f"{base_name}.pdf")
397
+
398
+ if os.path.exists(pdf_path):
399
+ return pdf_path
400
+ return None
401
+
402
+ except subprocess.CalledProcessError as e:
403
+ # Log error but don't crash
404
+ return None
405
+ except subprocess.TimeoutExpired:
406
+ return None
@@ -0,0 +1,206 @@
1
+ """
2
+ LaTeX utility functions for transpilation.
3
+
4
+ Contains helper functions for:
5
+ - Escaping special LaTeX characters
6
+ - Converting AST nodes to LaTeX strings
7
+ - Duration and money formatting
8
+ """
9
+
10
+ from typing import List, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from yuho.ast import nodes
14
+
15
+
16
+ def escape_latex(text: str) -> str:
17
+ """Escape special LaTeX characters."""
18
+ replacements = [
19
+ ("\\", r"\textbackslash{}"),
20
+ ("{", r"\{"),
21
+ ("}", r"\}"),
22
+ ("$", r"\$"),
23
+ ("%", r"\%"),
24
+ ("&", r"\&"),
25
+ ("#", r"\#"),
26
+ ("_", r"\_"),
27
+ ("^", r"\textasciicircum{}"),
28
+ ("~", r"\textasciitilde{}"),
29
+ ]
30
+ for old, new in replacements:
31
+ text = text.replace(old, new)
32
+ return text
33
+
34
+
35
+ def operator_to_latex(op: str) -> str:
36
+ """Convert operator to LaTeX symbol."""
37
+ operators = {
38
+ "+": "+",
39
+ "-": "--",
40
+ "*": r"$\times$",
41
+ "/": r"$\div$",
42
+ "%": r"\%",
43
+ "==": "=",
44
+ "!=": r"$\neq$",
45
+ "<": r"$<$",
46
+ ">": r"$>$",
47
+ "<=": r"$\leq$",
48
+ ">=": r"$\geq$",
49
+ "&&": r"\textbf{and}",
50
+ "||": r"\textbf{or}",
51
+ }
52
+ return operators.get(op, op)
53
+
54
+
55
+ def duration_to_latex(node: "nodes.DurationNode") -> str:
56
+ """Convert duration to LaTeX string."""
57
+ parts: List[str] = []
58
+ if node.years:
59
+ parts.append(f"{node.years} year{'s' if node.years != 1 else ''}")
60
+ if node.months:
61
+ parts.append(f"{node.months} month{'s' if node.months != 1 else ''}")
62
+ if node.days:
63
+ parts.append(f"{node.days} day{'s' if node.days != 1 else ''}")
64
+ if node.hours:
65
+ parts.append(f"{node.hours} hour{'s' if node.hours != 1 else ''}")
66
+ if node.minutes:
67
+ parts.append(f"{node.minutes} minute{'s' if node.minutes != 1 else ''}")
68
+ if node.seconds:
69
+ parts.append(f"{node.seconds} second{'s' if node.seconds != 1 else ''}")
70
+
71
+ if not parts:
72
+ return "---"
73
+ return ", ".join(parts)
74
+
75
+
76
+ def money_to_latex(node: "nodes.MoneyNode") -> str:
77
+ """Convert money to LaTeX string."""
78
+ from yuho.ast.nodes import Currency
79
+
80
+ currency_symbols = {
81
+ Currency.SGD: r"S\$",
82
+ Currency.USD: r"US\$",
83
+ Currency.EUR: r"\euro{}",
84
+ Currency.GBP: r"\pounds{}",
85
+ Currency.JPY: r"\textyen{}",
86
+ Currency.CNY: r"\textyen{}",
87
+ Currency.INR: r"\rupee{}",
88
+ Currency.AUD: r"A\$",
89
+ Currency.CAD: r"C\$",
90
+ Currency.CHF: "CHF~",
91
+ }
92
+ symbol = currency_symbols.get(node.currency, r"\$")
93
+ # Format with thousands separator
94
+ amount_str = f"{node.amount:,.2f}"
95
+ return f"{symbol}{amount_str}"
96
+
97
+
98
+ def type_to_latex(node: "nodes.TypeNode") -> str:
99
+ """Convert type to LaTeX string."""
100
+ from yuho.ast import nodes
101
+
102
+ if isinstance(node, nodes.BuiltinType):
103
+ type_names = {
104
+ "int": "integer",
105
+ "float": "decimal",
106
+ "bool": "boolean",
107
+ "string": "text",
108
+ "money": "monetary amount",
109
+ "percent": "percentage",
110
+ "date": "date",
111
+ "duration": "duration",
112
+ "void": "void",
113
+ }
114
+ return rf"\texttt{{{type_names.get(node.name, node.name)}}}"
115
+ elif isinstance(node, nodes.NamedType):
116
+ return rf"\texttt{{{escape_latex(node.name)}}}"
117
+ elif isinstance(node, nodes.OptionalType):
118
+ inner = type_to_latex(node.inner)
119
+ return f"{inner}?"
120
+ elif isinstance(node, nodes.ArrayType):
121
+ elem = type_to_latex(node.element_type)
122
+ return f"[{elem}]"
123
+ return r"\texttt{unknown}"
124
+
125
+
126
+ def expr_to_latex(node: "nodes.ASTNode") -> str:
127
+ """Convert expression to LaTeX string."""
128
+ from yuho.ast import nodes
129
+
130
+ if isinstance(node, nodes.IntLit):
131
+ return str(node.value)
132
+ elif isinstance(node, nodes.FloatLit):
133
+ return str(node.value)
134
+ elif isinstance(node, nodes.BoolLit):
135
+ return r"\texttt{TRUE}" if node.value else r"\texttt{FALSE}"
136
+ elif isinstance(node, nodes.StringLit):
137
+ return f"``{escape_latex(node.value)}''"
138
+ elif isinstance(node, nodes.MoneyNode):
139
+ return money_to_latex(node)
140
+ elif isinstance(node, nodes.PercentNode):
141
+ return f"{node.value}\\%"
142
+ elif isinstance(node, nodes.DateNode):
143
+ return node.value.strftime("%d %B %Y")
144
+ elif isinstance(node, nodes.DurationNode):
145
+ return duration_to_latex(node)
146
+ elif isinstance(node, nodes.IdentifierNode):
147
+ return rf"\textit{{{escape_latex(node.name)}}}"
148
+ elif isinstance(node, nodes.FieldAccessNode):
149
+ base = expr_to_latex(node.base)
150
+ field = escape_latex(node.field_name)
151
+ return f"{base}.{field}"
152
+ elif isinstance(node, nodes.BinaryExprNode):
153
+ left = expr_to_latex(node.left)
154
+ right = expr_to_latex(node.right)
155
+ op = operator_to_latex(node.operator)
156
+ return f"{left} {op} {right}"
157
+ elif isinstance(node, nodes.UnaryExprNode):
158
+ operand = expr_to_latex(node.operand)
159
+ if node.operator == "!":
160
+ return rf"\textit{{not}} {operand}"
161
+ return f"{node.operator}{operand}"
162
+ elif isinstance(node, nodes.PassExprNode):
163
+ return r"\textit{(none)}"
164
+ else:
165
+ return str(node)
166
+
167
+
168
+ def pattern_to_latex(node: "nodes.PatternNode") -> str:
169
+ """Convert pattern to LaTeX string."""
170
+ from yuho.ast import nodes
171
+
172
+ if isinstance(node, nodes.WildcardPattern):
173
+ return r"\textit{otherwise}"
174
+ elif isinstance(node, nodes.LiteralPattern):
175
+ return f"the value is {expr_to_latex(node.literal)}"
176
+ elif isinstance(node, nodes.BindingPattern):
177
+ name = escape_latex(node.name)
178
+ return rf"the value (call it ``{name}'')"
179
+ elif isinstance(node, nodes.StructPattern):
180
+ type_name = escape_latex(node.type_name)
181
+ fields = ", ".join(escape_latex(fp.name) for fp in node.fields)
182
+ return rf"it matches \texttt{{{type_name}}} with {{{fields}}}"
183
+ return r"\textit{(condition met)}"
184
+
185
+
186
+ def statement_to_latex(node: "nodes.ASTNode") -> str:
187
+ """Convert statement to LaTeX string."""
188
+ from yuho.ast import nodes
189
+
190
+ if isinstance(node, nodes.VariableDecl):
191
+ type_str = type_to_latex(node.type_annotation)
192
+ name = escape_latex(node.name)
193
+ if node.value:
194
+ value = expr_to_latex(node.value)
195
+ return rf"Let \texttt{{{name}}} be {type_str} = {value}."
196
+ return rf"Let \texttt{{{name}}} be {type_str}."
197
+ elif isinstance(node, nodes.ReturnStmt):
198
+ if node.value:
199
+ value = expr_to_latex(node.value)
200
+ return rf"Return {value}."
201
+ return "Return."
202
+ elif isinstance(node, nodes.AssignmentStmt):
203
+ target = expr_to_latex(node.target)
204
+ value = expr_to_latex(node.value)
205
+ return rf"Set {target} $\leftarrow$ {value}."
206
+ return str(node)