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,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
English transpiler - controlled natural language generation.
|
|
3
|
+
|
|
4
|
+
Converts Yuho AST to readable English text using templates
|
|
5
|
+
for legal documents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from yuho.ast import nodes
|
|
11
|
+
from yuho.ast.visitor import Visitor
|
|
12
|
+
from yuho.transpile.base import TranspileTarget, TranspilerBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnglishTranspiler(TranspilerBase, Visitor):
|
|
16
|
+
"""
|
|
17
|
+
Transpile Yuho AST to controlled natural language (English).
|
|
18
|
+
|
|
19
|
+
Uses templates for legal text generation with proper clause
|
|
20
|
+
connectors and penalty formatting.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._output: List[str] = []
|
|
25
|
+
self._indent_level = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def target(self) -> TranspileTarget:
|
|
29
|
+
return TranspileTarget.ENGLISH
|
|
30
|
+
|
|
31
|
+
def transpile(self, ast: nodes.ModuleNode) -> str:
|
|
32
|
+
"""Transpile AST to English text."""
|
|
33
|
+
self._output = []
|
|
34
|
+
self._indent_level = 0
|
|
35
|
+
self._visit_module(ast)
|
|
36
|
+
return "\n".join(self._output)
|
|
37
|
+
|
|
38
|
+
def _emit(self, text: str) -> None:
|
|
39
|
+
"""Add a line to output with current indentation."""
|
|
40
|
+
indent = " " * self._indent_level
|
|
41
|
+
self._output.append(f"{indent}{text}")
|
|
42
|
+
|
|
43
|
+
def _emit_blank(self) -> None:
|
|
44
|
+
"""Add a blank line."""
|
|
45
|
+
self._output.append("")
|
|
46
|
+
|
|
47
|
+
# =========================================================================
|
|
48
|
+
# Module and imports
|
|
49
|
+
# =========================================================================
|
|
50
|
+
|
|
51
|
+
def _visit_module(self, node: nodes.ModuleNode) -> None:
|
|
52
|
+
"""Generate English for entire module."""
|
|
53
|
+
# Imports
|
|
54
|
+
if node.imports:
|
|
55
|
+
for imp in node.imports:
|
|
56
|
+
self._visit_import(imp)
|
|
57
|
+
self._emit_blank()
|
|
58
|
+
|
|
59
|
+
# Type definitions
|
|
60
|
+
if node.type_defs:
|
|
61
|
+
self._emit("TYPE DEFINITIONS")
|
|
62
|
+
self._emit("=" * 50)
|
|
63
|
+
self._emit_blank()
|
|
64
|
+
for struct in node.type_defs:
|
|
65
|
+
self._visit_struct_def(struct)
|
|
66
|
+
self._emit_blank()
|
|
67
|
+
|
|
68
|
+
# Functions
|
|
69
|
+
if node.function_defs:
|
|
70
|
+
self._emit("FUNCTIONS")
|
|
71
|
+
self._emit("=" * 50)
|
|
72
|
+
self._emit_blank()
|
|
73
|
+
for func in node.function_defs:
|
|
74
|
+
self._visit_function_def(func)
|
|
75
|
+
self._emit_blank()
|
|
76
|
+
|
|
77
|
+
# Statutes (main content)
|
|
78
|
+
for statute in node.statutes:
|
|
79
|
+
self._visit_statute(statute)
|
|
80
|
+
self._emit_blank()
|
|
81
|
+
|
|
82
|
+
# Variables
|
|
83
|
+
if node.variables:
|
|
84
|
+
self._emit("DECLARATIONS")
|
|
85
|
+
self._emit("=" * 50)
|
|
86
|
+
self._emit_blank()
|
|
87
|
+
for var in node.variables:
|
|
88
|
+
self._visit_variable_decl(var)
|
|
89
|
+
|
|
90
|
+
def _visit_import(self, node: nodes.ImportNode) -> None:
|
|
91
|
+
"""Generate English for import."""
|
|
92
|
+
if node.is_wildcard:
|
|
93
|
+
self._emit(f"Reference: All definitions from \"{node.path}\"")
|
|
94
|
+
elif node.imported_names:
|
|
95
|
+
names = ", ".join(node.imported_names)
|
|
96
|
+
self._emit(f"Reference: {names} from \"{node.path}\"")
|
|
97
|
+
else:
|
|
98
|
+
self._emit(f"Reference: \"{node.path}\"")
|
|
99
|
+
|
|
100
|
+
# =========================================================================
|
|
101
|
+
# Statute blocks
|
|
102
|
+
# =========================================================================
|
|
103
|
+
|
|
104
|
+
def _visit_statute(self, node: nodes.StatuteNode) -> None:
|
|
105
|
+
"""Generate English for statute."""
|
|
106
|
+
# Header
|
|
107
|
+
title = node.title.value if node.title else "Untitled"
|
|
108
|
+
self._emit(f"SECTION {node.section_number}: {title}")
|
|
109
|
+
self._emit("=" * 60)
|
|
110
|
+
self._emit_blank()
|
|
111
|
+
|
|
112
|
+
# Definitions
|
|
113
|
+
if node.definitions:
|
|
114
|
+
self._emit("Definitions:")
|
|
115
|
+
self._indent_level += 1
|
|
116
|
+
for defn in node.definitions:
|
|
117
|
+
self._emit(f'"{defn.term}" means {defn.definition.value}')
|
|
118
|
+
self._indent_level -= 1
|
|
119
|
+
self._emit_blank()
|
|
120
|
+
|
|
121
|
+
# Elements
|
|
122
|
+
if node.elements:
|
|
123
|
+
self._emit("Elements of the offence:")
|
|
124
|
+
self._indent_level += 1
|
|
125
|
+
for elem in node.elements:
|
|
126
|
+
self._visit_element(elem)
|
|
127
|
+
self._indent_level -= 1
|
|
128
|
+
self._emit_blank()
|
|
129
|
+
|
|
130
|
+
# Penalty
|
|
131
|
+
if node.penalty:
|
|
132
|
+
self._visit_penalty(node.penalty)
|
|
133
|
+
self._emit_blank()
|
|
134
|
+
|
|
135
|
+
# Illustrations
|
|
136
|
+
if node.illustrations:
|
|
137
|
+
self._emit("Illustrations:")
|
|
138
|
+
self._emit_blank()
|
|
139
|
+
for i, illus in enumerate(node.illustrations, 1):
|
|
140
|
+
label = illus.label or f"({chr(ord('a') + i - 1)})"
|
|
141
|
+
self._emit(f"{label} {illus.description.value}")
|
|
142
|
+
self._emit_blank()
|
|
143
|
+
|
|
144
|
+
def _visit_element(self, node: nodes.ElementNode) -> None:
|
|
145
|
+
"""Generate English for element."""
|
|
146
|
+
type_labels = {
|
|
147
|
+
"actus_reus": "Physical element (actus reus)",
|
|
148
|
+
"mens_rea": "Mental element (mens rea)",
|
|
149
|
+
"circumstance": "Circumstance",
|
|
150
|
+
}
|
|
151
|
+
label = type_labels.get(node.element_type, node.element_type)
|
|
152
|
+
|
|
153
|
+
# Handle description
|
|
154
|
+
if isinstance(node.description, nodes.StringLit):
|
|
155
|
+
self._emit(f"{label}: {node.description.value}")
|
|
156
|
+
elif isinstance(node.description, nodes.MatchExprNode):
|
|
157
|
+
self._emit(f"{label}: {node.name}")
|
|
158
|
+
self._indent_level += 1
|
|
159
|
+
self._visit_match_expr_english(node.description)
|
|
160
|
+
self._indent_level -= 1
|
|
161
|
+
else:
|
|
162
|
+
desc = self._expr_to_english(node.description)
|
|
163
|
+
self._emit(f"{label}: {desc}")
|
|
164
|
+
|
|
165
|
+
def _visit_penalty(self, node: nodes.PenaltyNode) -> None:
|
|
166
|
+
"""Generate English for penalty."""
|
|
167
|
+
self._emit("Penalty:")
|
|
168
|
+
self._indent_level += 1
|
|
169
|
+
|
|
170
|
+
parts: List[str] = []
|
|
171
|
+
|
|
172
|
+
# Imprisonment
|
|
173
|
+
if node.imprisonment_max:
|
|
174
|
+
if node.imprisonment_min:
|
|
175
|
+
min_str = self._duration_to_english(node.imprisonment_min)
|
|
176
|
+
max_str = self._duration_to_english(node.imprisonment_max)
|
|
177
|
+
parts.append(f"imprisonment for a term of not less than {min_str} and not more than {max_str}")
|
|
178
|
+
else:
|
|
179
|
+
max_str = self._duration_to_english(node.imprisonment_max)
|
|
180
|
+
parts.append(f"imprisonment for a term which may extend to {max_str}")
|
|
181
|
+
|
|
182
|
+
# Fine
|
|
183
|
+
if node.fine_max:
|
|
184
|
+
if node.fine_min:
|
|
185
|
+
min_str = self._money_to_english(node.fine_min)
|
|
186
|
+
max_str = self._money_to_english(node.fine_max)
|
|
187
|
+
parts.append(f"a fine of not less than {min_str} and not more than {max_str}")
|
|
188
|
+
else:
|
|
189
|
+
max_str = self._money_to_english(node.fine_max)
|
|
190
|
+
parts.append(f"a fine which may extend to {max_str}")
|
|
191
|
+
|
|
192
|
+
# Combine parts
|
|
193
|
+
if len(parts) == 2:
|
|
194
|
+
self._emit(f"Shall be punished with {parts[0]}, or with {parts[1]}, or with both.")
|
|
195
|
+
elif len(parts) == 1:
|
|
196
|
+
self._emit(f"Shall be punished with {parts[0]}.")
|
|
197
|
+
else:
|
|
198
|
+
self._emit("Penalty to be determined.")
|
|
199
|
+
|
|
200
|
+
# Supplementary
|
|
201
|
+
if node.supplementary:
|
|
202
|
+
self._emit_blank()
|
|
203
|
+
self._emit(f"Additionally: {node.supplementary.value}")
|
|
204
|
+
|
|
205
|
+
self._indent_level -= 1
|
|
206
|
+
|
|
207
|
+
# =========================================================================
|
|
208
|
+
# Match expressions
|
|
209
|
+
# =========================================================================
|
|
210
|
+
|
|
211
|
+
def _visit_match_expr_english(self, node: nodes.MatchExprNode) -> None:
|
|
212
|
+
"""Generate English for match expression."""
|
|
213
|
+
if node.scrutinee:
|
|
214
|
+
scrutinee = self._expr_to_english(node.scrutinee)
|
|
215
|
+
self._emit(f"Based on {scrutinee}:")
|
|
216
|
+
|
|
217
|
+
for i, arm in enumerate(node.arms):
|
|
218
|
+
self._visit_match_arm_english(arm, i, len(node.arms))
|
|
219
|
+
|
|
220
|
+
def _visit_match_arm_english(self, node: nodes.MatchArm, index: int, total: int) -> None:
|
|
221
|
+
"""Generate English for match arm."""
|
|
222
|
+
# Pattern
|
|
223
|
+
pattern = self._pattern_to_english(node.pattern)
|
|
224
|
+
|
|
225
|
+
# Guard
|
|
226
|
+
guard_str = ""
|
|
227
|
+
if node.guard:
|
|
228
|
+
guard = self._expr_to_english(node.guard)
|
|
229
|
+
guard_str = f", provided that {guard}"
|
|
230
|
+
|
|
231
|
+
# Body
|
|
232
|
+
body = self._expr_to_english(node.body)
|
|
233
|
+
|
|
234
|
+
# Connectors based on position
|
|
235
|
+
if index == 0:
|
|
236
|
+
connector = "If"
|
|
237
|
+
elif isinstance(node.pattern, nodes.WildcardPattern):
|
|
238
|
+
connector = "Otherwise"
|
|
239
|
+
pattern = ""
|
|
240
|
+
else:
|
|
241
|
+
connector = "If"
|
|
242
|
+
|
|
243
|
+
if pattern:
|
|
244
|
+
self._emit(f"{connector} {pattern}{guard_str}: {body}")
|
|
245
|
+
else:
|
|
246
|
+
self._emit(f"{connector}{guard_str}: {body}")
|
|
247
|
+
|
|
248
|
+
# =========================================================================
|
|
249
|
+
# Struct definitions
|
|
250
|
+
# =========================================================================
|
|
251
|
+
|
|
252
|
+
def _visit_struct_def(self, node: nodes.StructDefNode) -> None:
|
|
253
|
+
"""Generate English for struct definition."""
|
|
254
|
+
self._emit(f"Type \"{node.name}\" consists of:")
|
|
255
|
+
self._indent_level += 1
|
|
256
|
+
for field in node.fields:
|
|
257
|
+
type_str = self._type_to_english(field.type_annotation)
|
|
258
|
+
self._emit(f"- {field.name}: {type_str}")
|
|
259
|
+
self._indent_level -= 1
|
|
260
|
+
|
|
261
|
+
# =========================================================================
|
|
262
|
+
# Function definitions
|
|
263
|
+
# =========================================================================
|
|
264
|
+
|
|
265
|
+
def _visit_function_def(self, node: nodes.FunctionDefNode) -> None:
|
|
266
|
+
"""Generate English for function definition."""
|
|
267
|
+
params_str = ", ".join(
|
|
268
|
+
f"{p.name} ({self._type_to_english(p.type_annotation)})"
|
|
269
|
+
for p in node.params
|
|
270
|
+
)
|
|
271
|
+
ret_str = f" returning {self._type_to_english(node.return_type)}" if node.return_type else ""
|
|
272
|
+
|
|
273
|
+
self._emit(f"Function \"{node.name}\"({params_str}){ret_str}:")
|
|
274
|
+
self._indent_level += 1
|
|
275
|
+
for stmt in node.body.statements:
|
|
276
|
+
self._visit_statement(stmt)
|
|
277
|
+
self._indent_level -= 1
|
|
278
|
+
|
|
279
|
+
# =========================================================================
|
|
280
|
+
# Statements
|
|
281
|
+
# =========================================================================
|
|
282
|
+
|
|
283
|
+
def _visit_variable_decl(self, node: nodes.VariableDecl) -> None:
|
|
284
|
+
"""Generate English for variable declaration."""
|
|
285
|
+
type_str = self._type_to_english(node.type_annotation)
|
|
286
|
+
if node.value:
|
|
287
|
+
value_str = self._expr_to_english(node.value)
|
|
288
|
+
self._emit(f"Let {node.name} be a {type_str} with value {value_str}.")
|
|
289
|
+
else:
|
|
290
|
+
self._emit(f"Let {node.name} be a {type_str}.")
|
|
291
|
+
|
|
292
|
+
def _visit_statement(self, node: nodes.ASTNode) -> None:
|
|
293
|
+
"""Generate English for statement."""
|
|
294
|
+
if isinstance(node, nodes.VariableDecl):
|
|
295
|
+
self._visit_variable_decl(node)
|
|
296
|
+
elif isinstance(node, nodes.AssignmentStmt):
|
|
297
|
+
target = self._expr_to_english(node.target)
|
|
298
|
+
value = self._expr_to_english(node.value)
|
|
299
|
+
self._emit(f"Set {target} to {value}.")
|
|
300
|
+
elif isinstance(node, nodes.ReturnStmt):
|
|
301
|
+
if node.value:
|
|
302
|
+
value = self._expr_to_english(node.value)
|
|
303
|
+
self._emit(f"Return {value}.")
|
|
304
|
+
else:
|
|
305
|
+
self._emit("Return.")
|
|
306
|
+
elif isinstance(node, nodes.PassStmt):
|
|
307
|
+
self._emit("(No action)")
|
|
308
|
+
elif isinstance(node, nodes.ExpressionStmt):
|
|
309
|
+
expr = self._expr_to_english(node.expression)
|
|
310
|
+
self._emit(f"{expr}")
|
|
311
|
+
|
|
312
|
+
# =========================================================================
|
|
313
|
+
# Helper methods for English generation
|
|
314
|
+
# =========================================================================
|
|
315
|
+
|
|
316
|
+
def _expr_to_english(self, node: nodes.ASTNode) -> str:
|
|
317
|
+
"""Convert expression to English."""
|
|
318
|
+
if isinstance(node, nodes.IntLit):
|
|
319
|
+
return str(node.value)
|
|
320
|
+
elif isinstance(node, nodes.FloatLit):
|
|
321
|
+
return str(node.value)
|
|
322
|
+
elif isinstance(node, nodes.BoolLit):
|
|
323
|
+
return "true" if node.value else "false"
|
|
324
|
+
elif isinstance(node, nodes.StringLit):
|
|
325
|
+
return f'"{node.value}"'
|
|
326
|
+
elif isinstance(node, nodes.MoneyNode):
|
|
327
|
+
return self._money_to_english(node)
|
|
328
|
+
elif isinstance(node, nodes.PercentNode):
|
|
329
|
+
return f"{node.value}%"
|
|
330
|
+
elif isinstance(node, nodes.DateNode):
|
|
331
|
+
return node.value.strftime("%d %B %Y")
|
|
332
|
+
elif isinstance(node, nodes.DurationNode):
|
|
333
|
+
return self._duration_to_english(node)
|
|
334
|
+
elif isinstance(node, nodes.IdentifierNode):
|
|
335
|
+
return node.name
|
|
336
|
+
elif isinstance(node, nodes.FieldAccessNode):
|
|
337
|
+
base = self._expr_to_english(node.base)
|
|
338
|
+
return f"{base}'s {node.field_name}"
|
|
339
|
+
elif isinstance(node, nodes.IndexAccessNode):
|
|
340
|
+
base = self._expr_to_english(node.base)
|
|
341
|
+
index = self._expr_to_english(node.index)
|
|
342
|
+
return f"{base}[{index}]"
|
|
343
|
+
elif isinstance(node, nodes.FunctionCallNode):
|
|
344
|
+
callee = self._expr_to_english(node.callee)
|
|
345
|
+
args = ", ".join(self._expr_to_english(a) for a in node.args)
|
|
346
|
+
return f"{callee}({args})"
|
|
347
|
+
elif isinstance(node, nodes.BinaryExprNode):
|
|
348
|
+
left = self._expr_to_english(node.left)
|
|
349
|
+
right = self._expr_to_english(node.right)
|
|
350
|
+
op = self._operator_to_english(node.operator)
|
|
351
|
+
return f"{left} {op} {right}"
|
|
352
|
+
elif isinstance(node, nodes.UnaryExprNode):
|
|
353
|
+
operand = self._expr_to_english(node.operand)
|
|
354
|
+
if node.operator == "!":
|
|
355
|
+
return f"not {operand}"
|
|
356
|
+
return f"{node.operator}{operand}"
|
|
357
|
+
elif isinstance(node, nodes.PassExprNode):
|
|
358
|
+
return "nothing"
|
|
359
|
+
elif isinstance(node, nodes.MatchExprNode):
|
|
360
|
+
return "(see decision tree below)"
|
|
361
|
+
elif isinstance(node, nodes.StructLiteralNode):
|
|
362
|
+
if node.struct_name:
|
|
363
|
+
return f"a {node.struct_name}"
|
|
364
|
+
return "a record"
|
|
365
|
+
else:
|
|
366
|
+
return str(node)
|
|
367
|
+
|
|
368
|
+
def _pattern_to_english(self, node: nodes.PatternNode) -> str:
|
|
369
|
+
"""Convert pattern to English."""
|
|
370
|
+
if isinstance(node, nodes.WildcardPattern):
|
|
371
|
+
return "in any other case"
|
|
372
|
+
elif isinstance(node, nodes.LiteralPattern):
|
|
373
|
+
return f"the value is {self._expr_to_english(node.literal)}"
|
|
374
|
+
elif isinstance(node, nodes.BindingPattern):
|
|
375
|
+
return f"the value (let us call it {node.name})"
|
|
376
|
+
elif isinstance(node, nodes.StructPattern):
|
|
377
|
+
fields = ", ".join(fp.name for fp in node.fields)
|
|
378
|
+
return f"it matches {node.type_name} with {fields}"
|
|
379
|
+
else:
|
|
380
|
+
return "the condition is met"
|
|
381
|
+
|
|
382
|
+
def _type_to_english(self, node: nodes.TypeNode) -> str:
|
|
383
|
+
"""Convert type to English."""
|
|
384
|
+
if isinstance(node, nodes.BuiltinType):
|
|
385
|
+
type_names = {
|
|
386
|
+
"int": "integer",
|
|
387
|
+
"float": "decimal number",
|
|
388
|
+
"bool": "boolean",
|
|
389
|
+
"string": "text",
|
|
390
|
+
"money": "monetary amount",
|
|
391
|
+
"percent": "percentage",
|
|
392
|
+
"date": "date",
|
|
393
|
+
"duration": "time period",
|
|
394
|
+
"void": "nothing",
|
|
395
|
+
}
|
|
396
|
+
return type_names.get(node.name, node.name)
|
|
397
|
+
elif isinstance(node, nodes.NamedType):
|
|
398
|
+
return node.name
|
|
399
|
+
elif isinstance(node, nodes.OptionalType):
|
|
400
|
+
inner = self._type_to_english(node.inner)
|
|
401
|
+
return f"optional {inner}"
|
|
402
|
+
elif isinstance(node, nodes.ArrayType):
|
|
403
|
+
elem = self._type_to_english(node.element_type)
|
|
404
|
+
return f"list of {elem}"
|
|
405
|
+
elif isinstance(node, nodes.GenericType):
|
|
406
|
+
args = ", ".join(self._type_to_english(a) for a in node.type_args)
|
|
407
|
+
return f"{node.base} of {args}"
|
|
408
|
+
else:
|
|
409
|
+
return "value"
|
|
410
|
+
|
|
411
|
+
def _operator_to_english(self, op: str) -> str:
|
|
412
|
+
"""Convert operator to English."""
|
|
413
|
+
operators = {
|
|
414
|
+
"+": "plus",
|
|
415
|
+
"-": "minus",
|
|
416
|
+
"*": "times",
|
|
417
|
+
"/": "divided by",
|
|
418
|
+
"%": "modulo",
|
|
419
|
+
"==": "equals",
|
|
420
|
+
"!=": "does not equal",
|
|
421
|
+
"<": "is less than",
|
|
422
|
+
">": "is greater than",
|
|
423
|
+
"<=": "is at most",
|
|
424
|
+
">=": "is at least",
|
|
425
|
+
"&&": "and",
|
|
426
|
+
"||": "or",
|
|
427
|
+
}
|
|
428
|
+
return operators.get(op, op)
|
|
429
|
+
|
|
430
|
+
def _duration_to_english(self, node: nodes.DurationNode) -> str:
|
|
431
|
+
"""Convert duration to English."""
|
|
432
|
+
parts: List[str] = []
|
|
433
|
+
if node.years:
|
|
434
|
+
parts.append(f"{node.years} year{'s' if node.years != 1 else ''}")
|
|
435
|
+
if node.months:
|
|
436
|
+
parts.append(f"{node.months} month{'s' if node.months != 1 else ''}")
|
|
437
|
+
if node.days:
|
|
438
|
+
parts.append(f"{node.days} day{'s' if node.days != 1 else ''}")
|
|
439
|
+
if node.hours:
|
|
440
|
+
parts.append(f"{node.hours} hour{'s' if node.hours != 1 else ''}")
|
|
441
|
+
if node.minutes:
|
|
442
|
+
parts.append(f"{node.minutes} minute{'s' if node.minutes != 1 else ''}")
|
|
443
|
+
if node.seconds:
|
|
444
|
+
parts.append(f"{node.seconds} second{'s' if node.seconds != 1 else ''}")
|
|
445
|
+
|
|
446
|
+
if not parts:
|
|
447
|
+
return "no time"
|
|
448
|
+
elif len(parts) == 1:
|
|
449
|
+
return parts[0]
|
|
450
|
+
else:
|
|
451
|
+
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
|
452
|
+
|
|
453
|
+
def _money_to_english(self, node: nodes.MoneyNode) -> str:
|
|
454
|
+
"""Convert money to English."""
|
|
455
|
+
currency_symbols = {
|
|
456
|
+
nodes.Currency.SGD: "S$",
|
|
457
|
+
nodes.Currency.USD: "US$",
|
|
458
|
+
nodes.Currency.EUR: "€",
|
|
459
|
+
nodes.Currency.GBP: "£",
|
|
460
|
+
nodes.Currency.JPY: "¥",
|
|
461
|
+
nodes.Currency.CNY: "¥",
|
|
462
|
+
nodes.Currency.INR: "₹",
|
|
463
|
+
nodes.Currency.AUD: "A$",
|
|
464
|
+
nodes.Currency.CAD: "C$",
|
|
465
|
+
nodes.Currency.CHF: "CHF ",
|
|
466
|
+
}
|
|
467
|
+
symbol = currency_symbols.get(node.currency, "$")
|
|
468
|
+
# Format with thousands separator
|
|
469
|
+
amount_str = f"{node.amount:,.2f}"
|
|
470
|
+
return f"{symbol}{amount_str}"
|