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,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Explain command - LLM-powered explanations of Yuho files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from yuho.parser import Parser
|
|
12
|
+
from yuho.ast import ASTBuilder
|
|
13
|
+
from yuho.transpile import EnglishTranspiler
|
|
14
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_explain(
|
|
18
|
+
file: str,
|
|
19
|
+
section: Optional[str] = None,
|
|
20
|
+
interactive: bool = False,
|
|
21
|
+
provider: Optional[str] = None,
|
|
22
|
+
model: Optional[str] = None,
|
|
23
|
+
verbose: bool = False,
|
|
24
|
+
stream: bool = True,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Generate natural language explanation of a Yuho file.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file: Path to the .yh file
|
|
31
|
+
section: Specific section to explain
|
|
32
|
+
interactive: Enable interactive REPL mode
|
|
33
|
+
provider: LLM provider (ollama, huggingface, openai, anthropic)
|
|
34
|
+
model: Model name
|
|
35
|
+
verbose: Enable verbose output
|
|
36
|
+
stream: Enable streaming output for real-time response
|
|
37
|
+
"""
|
|
38
|
+
file_path = Path(file)
|
|
39
|
+
|
|
40
|
+
# Parse and build AST
|
|
41
|
+
parser = Parser()
|
|
42
|
+
try:
|
|
43
|
+
result = parser.parse_file(file_path)
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
if result.errors:
|
|
49
|
+
click.echo(colorize(f"error: Parse errors in {file}", Colors.RED), err=True)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
builder = ASTBuilder(result.source, str(file_path))
|
|
53
|
+
ast = builder.build(result.root_node)
|
|
54
|
+
|
|
55
|
+
# Filter to specific section if requested
|
|
56
|
+
if section:
|
|
57
|
+
matching = [s for s in ast.statutes if section in s.section_number]
|
|
58
|
+
if not matching:
|
|
59
|
+
click.echo(colorize(f"error: Section {section} not found", Colors.RED), err=True)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
# Create filtered module
|
|
63
|
+
from yuho.ast.nodes import ModuleNode
|
|
64
|
+
ast = ModuleNode(
|
|
65
|
+
imports=ast.imports,
|
|
66
|
+
type_defs=ast.type_defs,
|
|
67
|
+
function_defs=ast.function_defs,
|
|
68
|
+
statutes=tuple(matching),
|
|
69
|
+
variables=ast.variables,
|
|
70
|
+
source_location=ast.source_location,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Generate base English explanation
|
|
74
|
+
english = EnglishTranspiler()
|
|
75
|
+
base_explanation = english.transpile(ast)
|
|
76
|
+
|
|
77
|
+
if interactive:
|
|
78
|
+
_run_interactive(base_explanation, ast, provider, model, stream)
|
|
79
|
+
else:
|
|
80
|
+
# Try to use LLM for enhanced explanation
|
|
81
|
+
try:
|
|
82
|
+
if stream:
|
|
83
|
+
_enhance_with_llm_stream(base_explanation, provider, model, verbose)
|
|
84
|
+
else:
|
|
85
|
+
enhanced = _enhance_with_llm(base_explanation, provider, model, verbose)
|
|
86
|
+
click.echo(enhanced)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
if verbose:
|
|
89
|
+
click.echo(colorize(f"LLM unavailable: {e}", Colors.YELLOW), err=True)
|
|
90
|
+
# Fall back to basic English transpilation
|
|
91
|
+
click.echo(base_explanation)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _enhance_with_llm(text: str, provider: Optional[str], model: Optional[str], verbose: bool) -> str:
|
|
95
|
+
"""Use LLM to enhance the explanation."""
|
|
96
|
+
try:
|
|
97
|
+
from yuho.llm import get_provider, LLMConfig
|
|
98
|
+
except ImportError:
|
|
99
|
+
return text # LLM module not available
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Build config
|
|
103
|
+
config = LLMConfig(
|
|
104
|
+
provider=provider or "ollama",
|
|
105
|
+
model_name=model or "llama3",
|
|
106
|
+
)
|
|
107
|
+
llm = get_provider(config)
|
|
108
|
+
|
|
109
|
+
prompt = f"""You are a legal expert explaining statute provisions to a general audience.
|
|
110
|
+
Given the following structured explanation of a legal statute, rewrite it in clear,
|
|
111
|
+
accessible language that a non-lawyer could understand. Keep the legal accuracy but
|
|
112
|
+
make it readable.
|
|
113
|
+
|
|
114
|
+
Structured explanation:
|
|
115
|
+
{text}
|
|
116
|
+
|
|
117
|
+
Clear explanation:"""
|
|
118
|
+
|
|
119
|
+
return llm.generate(prompt, max_tokens=2000)
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
if verbose:
|
|
123
|
+
click.echo(colorize(f"LLM error: {e}", Colors.YELLOW), err=True)
|
|
124
|
+
return text
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _enhance_with_llm_stream(text: str, provider: Optional[str], model: Optional[str], verbose: bool) -> None:
|
|
128
|
+
"""Use LLM to enhance the explanation with streaming output."""
|
|
129
|
+
try:
|
|
130
|
+
from yuho.llm import get_provider, LLMConfig
|
|
131
|
+
except ImportError:
|
|
132
|
+
click.echo(text) # LLM module not available
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Build config
|
|
137
|
+
config = LLMConfig(
|
|
138
|
+
provider=provider or "ollama",
|
|
139
|
+
model_name=model or "llama3",
|
|
140
|
+
)
|
|
141
|
+
llm = get_provider(config)
|
|
142
|
+
|
|
143
|
+
prompt = f"""You are a legal expert explaining statute provisions to a general audience.
|
|
144
|
+
Given the following structured explanation of a legal statute, rewrite it in clear,
|
|
145
|
+
accessible language that a non-lawyer could understand. Keep the legal accuracy but
|
|
146
|
+
make it readable.
|
|
147
|
+
|
|
148
|
+
Structured explanation:
|
|
149
|
+
{text}
|
|
150
|
+
|
|
151
|
+
Clear explanation:"""
|
|
152
|
+
|
|
153
|
+
# Check if provider supports streaming
|
|
154
|
+
if hasattr(llm, 'stream'):
|
|
155
|
+
# Stream tokens to stdout
|
|
156
|
+
for token in llm.stream(prompt, max_tokens=2000):
|
|
157
|
+
click.echo(token, nl=False)
|
|
158
|
+
sys.stdout.flush()
|
|
159
|
+
click.echo() # Final newline
|
|
160
|
+
else:
|
|
161
|
+
# Fall back to non-streaming
|
|
162
|
+
response = llm.generate(prompt, max_tokens=2000)
|
|
163
|
+
click.echo(response)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
if verbose:
|
|
167
|
+
click.echo(colorize(f"LLM streaming error: {e}", Colors.YELLOW), err=True)
|
|
168
|
+
# Fall back to basic explanation
|
|
169
|
+
click.echo(text)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _run_interactive(
|
|
173
|
+
base_explanation: str,
|
|
174
|
+
ast,
|
|
175
|
+
provider: Optional[str],
|
|
176
|
+
model: Optional[str],
|
|
177
|
+
stream: bool = True,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Run interactive REPL for follow-up questions."""
|
|
180
|
+
click.echo(colorize("Yuho Explain - Interactive Mode", Colors.CYAN + Colors.BOLD))
|
|
181
|
+
click.echo(colorize("Type 'quit' or 'exit' to leave. Type 'show' to see the statute.\n", Colors.DIM))
|
|
182
|
+
|
|
183
|
+
# Show initial explanation
|
|
184
|
+
click.echo(base_explanation)
|
|
185
|
+
click.echo()
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
from yuho.llm import get_provider, LLMConfig
|
|
189
|
+
config = LLMConfig(provider=provider or "ollama", model_name=model or "llama3")
|
|
190
|
+
llm = get_provider(config)
|
|
191
|
+
has_llm = True
|
|
192
|
+
can_stream = stream and hasattr(llm, 'stream')
|
|
193
|
+
except Exception:
|
|
194
|
+
has_llm = False
|
|
195
|
+
can_stream = False
|
|
196
|
+
click.echo(colorize("(LLM not available - limited to basic queries)", Colors.YELLOW))
|
|
197
|
+
|
|
198
|
+
context = base_explanation
|
|
199
|
+
|
|
200
|
+
while True:
|
|
201
|
+
try:
|
|
202
|
+
query = click.prompt(colorize("?", Colors.CYAN), default="", show_default=False)
|
|
203
|
+
except (EOFError, KeyboardInterrupt):
|
|
204
|
+
click.echo("\nGoodbye!")
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
query = query.strip()
|
|
208
|
+
if not query:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if query.lower() in ("quit", "exit", "q"):
|
|
212
|
+
click.echo("Goodbye!")
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if query.lower() == "show":
|
|
216
|
+
click.echo(base_explanation)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if has_llm:
|
|
220
|
+
prompt = f"""Context about the statute:
|
|
221
|
+
{context}
|
|
222
|
+
|
|
223
|
+
User question: {query}
|
|
224
|
+
|
|
225
|
+
Provide a helpful, accurate answer:"""
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if can_stream:
|
|
229
|
+
click.echo() # Start on new line
|
|
230
|
+
for token in llm.stream(prompt, max_tokens=1000):
|
|
231
|
+
click.echo(token, nl=False)
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
click.echo("\n") # End with newlines
|
|
234
|
+
else:
|
|
235
|
+
response = llm.generate(prompt, max_tokens=1000)
|
|
236
|
+
click.echo(f"\n{response}\n")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
click.echo(colorize(f"Error: {e}", Colors.RED))
|
|
239
|
+
else:
|
|
240
|
+
click.echo(colorize("Cannot answer - LLM not available", Colors.YELLOW))
|
yuho/cli/commands/fmt.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Format command - canonical formatting for Yuho files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from yuho.parser import Parser
|
|
12
|
+
from yuho.ast import ASTBuilder
|
|
13
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_fmt(file: str, in_place: bool = False, check: bool = False, verbose: bool = False) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Format a Yuho source file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
file: Path to the .yh file
|
|
22
|
+
in_place: Format file in place
|
|
23
|
+
check: Check if formatted (exit 1 if not)
|
|
24
|
+
verbose: Enable verbose output
|
|
25
|
+
"""
|
|
26
|
+
file_path = Path(file)
|
|
27
|
+
|
|
28
|
+
# Parse file
|
|
29
|
+
parser = Parser()
|
|
30
|
+
try:
|
|
31
|
+
result = parser.parse_file(file_path)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
if result.errors:
|
|
37
|
+
click.echo(colorize(f"error: Cannot format file with parse errors", Colors.RED), err=True)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
# Build AST
|
|
41
|
+
builder = ASTBuilder(result.source, str(file_path))
|
|
42
|
+
ast = builder.build(result.root_node)
|
|
43
|
+
|
|
44
|
+
# Format using canonical printer
|
|
45
|
+
formatted = _format_module(ast)
|
|
46
|
+
|
|
47
|
+
# Compare with original
|
|
48
|
+
original = result.source
|
|
49
|
+
is_formatted = original.strip() == formatted.strip()
|
|
50
|
+
|
|
51
|
+
if check:
|
|
52
|
+
if is_formatted:
|
|
53
|
+
if verbose:
|
|
54
|
+
click.echo(f"OK: {file_path} is formatted")
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
else:
|
|
57
|
+
click.echo(colorize(f"FAIL: {file_path} needs formatting", Colors.RED))
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
if in_place:
|
|
61
|
+
if is_formatted:
|
|
62
|
+
if verbose:
|
|
63
|
+
click.echo(f"No changes: {file_path}")
|
|
64
|
+
else:
|
|
65
|
+
file_path.write_text(formatted, encoding="utf-8")
|
|
66
|
+
click.echo(f"Formatted: {file_path}")
|
|
67
|
+
else:
|
|
68
|
+
print(formatted)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format_module(ast) -> str:
|
|
72
|
+
"""Format a module AST to canonical string representation."""
|
|
73
|
+
from yuho.ast import nodes
|
|
74
|
+
|
|
75
|
+
lines = []
|
|
76
|
+
|
|
77
|
+
# Imports
|
|
78
|
+
for imp in ast.imports:
|
|
79
|
+
if imp.is_wildcard:
|
|
80
|
+
lines.append(f'import * from "{imp.path}"')
|
|
81
|
+
elif imp.imported_names:
|
|
82
|
+
names = ", ".join(imp.imported_names)
|
|
83
|
+
lines.append(f'import {{ {names} }} from "{imp.path}"')
|
|
84
|
+
else:
|
|
85
|
+
lines.append(f'import "{imp.path}"')
|
|
86
|
+
|
|
87
|
+
if ast.imports:
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
# Struct definitions
|
|
91
|
+
for struct in ast.type_defs:
|
|
92
|
+
lines.append(f"struct {struct.name} {{")
|
|
93
|
+
for field in struct.fields:
|
|
94
|
+
type_str = _format_type(field.type_annotation)
|
|
95
|
+
lines.append(f" {type_str} {field.name},")
|
|
96
|
+
lines.append("}")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
# Function definitions
|
|
100
|
+
for func in ast.function_defs:
|
|
101
|
+
params = ", ".join(
|
|
102
|
+
f"{_format_type(p.type_annotation)} {p.name}"
|
|
103
|
+
for p in func.params
|
|
104
|
+
)
|
|
105
|
+
ret = f": {_format_type(func.return_type)}" if func.return_type else ""
|
|
106
|
+
lines.append(f"fn {func.name}({params}){ret} {{")
|
|
107
|
+
for stmt in func.body.statements:
|
|
108
|
+
lines.append(f" {_format_statement(stmt)}")
|
|
109
|
+
lines.append("}")
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
# Statutes
|
|
113
|
+
for statute in ast.statutes:
|
|
114
|
+
title = f' "{statute.title.value}"' if statute.title else ""
|
|
115
|
+
lines.append(f"statute {statute.section_number}{title} {{")
|
|
116
|
+
|
|
117
|
+
if statute.definitions:
|
|
118
|
+
lines.append(" definitions {")
|
|
119
|
+
for defn in statute.definitions:
|
|
120
|
+
lines.append(f' {defn.term} := "{defn.definition.value}";')
|
|
121
|
+
lines.append(" }")
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
if statute.elements:
|
|
125
|
+
lines.append(" elements {")
|
|
126
|
+
for elem in statute.elements:
|
|
127
|
+
desc = _format_expr(elem.description)
|
|
128
|
+
lines.append(f" {elem.element_type} {elem.name} := {desc};")
|
|
129
|
+
lines.append(" }")
|
|
130
|
+
lines.append("")
|
|
131
|
+
|
|
132
|
+
if statute.penalty:
|
|
133
|
+
lines.append(" penalty {")
|
|
134
|
+
if statute.penalty.imprisonment_max:
|
|
135
|
+
dur = _format_duration(statute.penalty.imprisonment_max)
|
|
136
|
+
if statute.penalty.imprisonment_min:
|
|
137
|
+
min_dur = _format_duration(statute.penalty.imprisonment_min)
|
|
138
|
+
lines.append(f" imprisonment := {min_dur} .. {dur};")
|
|
139
|
+
else:
|
|
140
|
+
lines.append(f" imprisonment := {dur};")
|
|
141
|
+
if statute.penalty.fine_max:
|
|
142
|
+
money = _format_money(statute.penalty.fine_max)
|
|
143
|
+
if statute.penalty.fine_min:
|
|
144
|
+
min_money = _format_money(statute.penalty.fine_min)
|
|
145
|
+
lines.append(f" fine := {min_money} .. {money};")
|
|
146
|
+
else:
|
|
147
|
+
lines.append(f" fine := {money};")
|
|
148
|
+
if statute.penalty.supplementary:
|
|
149
|
+
lines.append(f' supplementary := "{statute.penalty.supplementary.value}";')
|
|
150
|
+
lines.append(" }")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
for illus in statute.illustrations:
|
|
154
|
+
label = illus.label or ""
|
|
155
|
+
lines.append(f' illustration {label} {{')
|
|
156
|
+
lines.append(f' "{illus.description.value}"')
|
|
157
|
+
lines.append(" }")
|
|
158
|
+
lines.append("")
|
|
159
|
+
|
|
160
|
+
lines.append("}")
|
|
161
|
+
lines.append("")
|
|
162
|
+
|
|
163
|
+
# Variables
|
|
164
|
+
for var in ast.variables:
|
|
165
|
+
type_str = _format_type(var.type_annotation)
|
|
166
|
+
if var.value:
|
|
167
|
+
val = _format_expr(var.value)
|
|
168
|
+
lines.append(f"{type_str} {var.name} := {val};")
|
|
169
|
+
else:
|
|
170
|
+
lines.append(f"{type_str} {var.name};")
|
|
171
|
+
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _format_type(typ) -> str:
|
|
176
|
+
"""Format a type node."""
|
|
177
|
+
from yuho.ast import nodes
|
|
178
|
+
|
|
179
|
+
if isinstance(typ, nodes.BuiltinType):
|
|
180
|
+
return typ.name
|
|
181
|
+
elif isinstance(typ, nodes.NamedType):
|
|
182
|
+
return typ.name
|
|
183
|
+
elif isinstance(typ, nodes.OptionalType):
|
|
184
|
+
return f"{_format_type(typ.inner)}?"
|
|
185
|
+
elif isinstance(typ, nodes.ArrayType):
|
|
186
|
+
return f"[{_format_type(typ.element_type)}]"
|
|
187
|
+
elif isinstance(typ, nodes.GenericType):
|
|
188
|
+
args = ", ".join(_format_type(a) for a in typ.type_args)
|
|
189
|
+
return f"{typ.base}<{args}>"
|
|
190
|
+
return "?"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _format_expr(expr) -> str:
|
|
194
|
+
"""Format an expression node."""
|
|
195
|
+
from yuho.ast import nodes
|
|
196
|
+
|
|
197
|
+
if isinstance(expr, nodes.IntLit):
|
|
198
|
+
return str(expr.value)
|
|
199
|
+
elif isinstance(expr, nodes.FloatLit):
|
|
200
|
+
return str(expr.value)
|
|
201
|
+
elif isinstance(expr, nodes.BoolLit):
|
|
202
|
+
return "TRUE" if expr.value else "FALSE"
|
|
203
|
+
elif isinstance(expr, nodes.StringLit):
|
|
204
|
+
return f'"{expr.value}"'
|
|
205
|
+
elif isinstance(expr, nodes.MoneyNode):
|
|
206
|
+
return _format_money(expr)
|
|
207
|
+
elif isinstance(expr, nodes.PercentNode):
|
|
208
|
+
return f"{expr.value}%"
|
|
209
|
+
elif isinstance(expr, nodes.DateNode):
|
|
210
|
+
return expr.value.isoformat()
|
|
211
|
+
elif isinstance(expr, nodes.DurationNode):
|
|
212
|
+
return _format_duration(expr)
|
|
213
|
+
elif isinstance(expr, nodes.IdentifierNode):
|
|
214
|
+
return expr.name
|
|
215
|
+
elif isinstance(expr, nodes.FieldAccessNode):
|
|
216
|
+
return f"{_format_expr(expr.base)}.{expr.field_name}"
|
|
217
|
+
elif isinstance(expr, nodes.BinaryExprNode):
|
|
218
|
+
return f"{_format_expr(expr.left)} {expr.operator} {_format_expr(expr.right)}"
|
|
219
|
+
elif isinstance(expr, nodes.PassExprNode):
|
|
220
|
+
return "pass"
|
|
221
|
+
return "?"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _format_statement(stmt) -> str:
|
|
225
|
+
"""Format a statement."""
|
|
226
|
+
from yuho.ast import nodes
|
|
227
|
+
|
|
228
|
+
if isinstance(stmt, nodes.ReturnStmt):
|
|
229
|
+
if stmt.value:
|
|
230
|
+
return f"return {_format_expr(stmt.value)};"
|
|
231
|
+
return "return;"
|
|
232
|
+
elif isinstance(stmt, nodes.PassStmt):
|
|
233
|
+
return "pass;"
|
|
234
|
+
elif isinstance(stmt, nodes.ExpressionStmt):
|
|
235
|
+
return f"{_format_expr(stmt.expression)};"
|
|
236
|
+
return "?"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _format_duration(dur) -> str:
|
|
240
|
+
"""Format a duration."""
|
|
241
|
+
parts = []
|
|
242
|
+
if dur.years:
|
|
243
|
+
parts.append(f"{dur.years} year{'s' if dur.years != 1 else ''}")
|
|
244
|
+
if dur.months:
|
|
245
|
+
parts.append(f"{dur.months} month{'s' if dur.months != 1 else ''}")
|
|
246
|
+
if dur.days:
|
|
247
|
+
parts.append(f"{dur.days} day{'s' if dur.days != 1 else ''}")
|
|
248
|
+
return ", ".join(parts) if parts else "0 days"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _format_money(money) -> str:
|
|
252
|
+
"""Format a money value."""
|
|
253
|
+
return f"${money.amount}"
|