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