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,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphQL schema transpiler - generate GraphQL schema from Yuho AST.
|
|
3
|
+
|
|
4
|
+
Converts Yuho statutes and types to GraphQL schema definitions
|
|
5
|
+
suitable for building legal APIs. Includes:
|
|
6
|
+
- Type definitions for statutes, elements, penalties
|
|
7
|
+
- Query types for statute lookup
|
|
8
|
+
- Enum types for currencies, element types, etc.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import List, Set
|
|
12
|
+
|
|
13
|
+
from yuho.ast import nodes
|
|
14
|
+
from yuho.ast.visitor import Visitor
|
|
15
|
+
from yuho.transpile.base import TranspileTarget, TranspilerBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GraphQLTranspiler(TranspilerBase, Visitor):
|
|
19
|
+
"""
|
|
20
|
+
Transpile Yuho AST to GraphQL schema definition language (SDL).
|
|
21
|
+
|
|
22
|
+
Generates a complete GraphQL schema with types, queries, and enums
|
|
23
|
+
for building legal statute APIs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, include_descriptions: bool = True):
|
|
27
|
+
"""
|
|
28
|
+
Initialize GraphQL transpiler.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
include_descriptions: Whether to include description comments
|
|
32
|
+
"""
|
|
33
|
+
self._output: List[str] = []
|
|
34
|
+
self._indent_level = 0
|
|
35
|
+
self.include_descriptions = include_descriptions
|
|
36
|
+
self._defined_types: Set[str] = set()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def target(self) -> TranspileTarget:
|
|
40
|
+
return TranspileTarget.GRAPHQL
|
|
41
|
+
|
|
42
|
+
def transpile(self, ast: nodes.ModuleNode) -> str:
|
|
43
|
+
"""Transpile AST to GraphQL schema."""
|
|
44
|
+
self._output = []
|
|
45
|
+
self._defined_types = set()
|
|
46
|
+
|
|
47
|
+
# Emit header comment
|
|
48
|
+
self._emit("# Auto-generated GraphQL schema from Yuho statutes")
|
|
49
|
+
self._emit("# Do not edit manually")
|
|
50
|
+
self._emit("")
|
|
51
|
+
|
|
52
|
+
# Emit built-in scalar types
|
|
53
|
+
self._emit_scalars()
|
|
54
|
+
|
|
55
|
+
# Emit enums
|
|
56
|
+
self._emit_enums()
|
|
57
|
+
|
|
58
|
+
# Emit custom struct types from AST
|
|
59
|
+
for struct in ast.type_defs:
|
|
60
|
+
self._visit_struct_def(struct)
|
|
61
|
+
|
|
62
|
+
# Emit core legal types
|
|
63
|
+
self._emit_core_types()
|
|
64
|
+
|
|
65
|
+
# Emit statute types
|
|
66
|
+
for statute in ast.statutes:
|
|
67
|
+
self._visit_statute(statute)
|
|
68
|
+
|
|
69
|
+
# Emit query root
|
|
70
|
+
self._emit_query_root(ast)
|
|
71
|
+
|
|
72
|
+
return "\n".join(self._output)
|
|
73
|
+
|
|
74
|
+
def _emit(self, text: str) -> None:
|
|
75
|
+
"""Add a line to output with current indentation."""
|
|
76
|
+
indent = " " * self._indent_level
|
|
77
|
+
self._output.append(f"{indent}{text}")
|
|
78
|
+
|
|
79
|
+
def _emit_blank(self) -> None:
|
|
80
|
+
"""Add a blank line."""
|
|
81
|
+
self._output.append("")
|
|
82
|
+
|
|
83
|
+
def _emit_description(self, text: str, multiline: bool = False) -> None:
|
|
84
|
+
"""Emit a GraphQL description string."""
|
|
85
|
+
if not self.include_descriptions:
|
|
86
|
+
return
|
|
87
|
+
if multiline or "\n" in text:
|
|
88
|
+
self._emit('"""')
|
|
89
|
+
for line in text.split("\n"):
|
|
90
|
+
self._emit(line)
|
|
91
|
+
self._emit('"""')
|
|
92
|
+
else:
|
|
93
|
+
self._emit(f'"{text}"')
|
|
94
|
+
|
|
95
|
+
# =========================================================================
|
|
96
|
+
# Scalar and Enum Types
|
|
97
|
+
# =========================================================================
|
|
98
|
+
|
|
99
|
+
def _emit_scalars(self) -> None:
|
|
100
|
+
"""Emit custom scalar type definitions."""
|
|
101
|
+
self._emit_description("Monetary amount with currency")
|
|
102
|
+
self._emit("scalar Money")
|
|
103
|
+
self._emit_blank()
|
|
104
|
+
|
|
105
|
+
self._emit_description("Duration of time (e.g., imprisonment term)")
|
|
106
|
+
self._emit("scalar Duration")
|
|
107
|
+
self._emit_blank()
|
|
108
|
+
|
|
109
|
+
self._emit_description("Percentage value")
|
|
110
|
+
self._emit("scalar Percent")
|
|
111
|
+
self._emit_blank()
|
|
112
|
+
|
|
113
|
+
self._emit_description("Date in ISO 8601 format")
|
|
114
|
+
self._emit("scalar Date")
|
|
115
|
+
self._emit_blank()
|
|
116
|
+
|
|
117
|
+
def _emit_enums(self) -> None:
|
|
118
|
+
"""Emit enum type definitions."""
|
|
119
|
+
# Currency enum
|
|
120
|
+
self._emit_description("Supported currency types")
|
|
121
|
+
self._emit("enum Currency {")
|
|
122
|
+
self._indent_level += 1
|
|
123
|
+
for currency in nodes.Currency:
|
|
124
|
+
self._emit(currency.name)
|
|
125
|
+
self._indent_level -= 1
|
|
126
|
+
self._emit("}")
|
|
127
|
+
self._emit_blank()
|
|
128
|
+
|
|
129
|
+
# Element type enum
|
|
130
|
+
self._emit_description("Types of legal elements")
|
|
131
|
+
self._emit("enum ElementType {")
|
|
132
|
+
self._indent_level += 1
|
|
133
|
+
self._emit("ACTUS_REUS")
|
|
134
|
+
self._emit("MENS_REA")
|
|
135
|
+
self._emit("CIRCUMSTANCE")
|
|
136
|
+
self._indent_level -= 1
|
|
137
|
+
self._emit("}")
|
|
138
|
+
self._emit_blank()
|
|
139
|
+
|
|
140
|
+
# =========================================================================
|
|
141
|
+
# Core Legal Types
|
|
142
|
+
# =========================================================================
|
|
143
|
+
|
|
144
|
+
def _emit_core_types(self) -> None:
|
|
145
|
+
"""Emit core legal type definitions."""
|
|
146
|
+
# Definition type
|
|
147
|
+
self._emit_description("Legal definition within a statute")
|
|
148
|
+
self._emit("type Definition {")
|
|
149
|
+
self._indent_level += 1
|
|
150
|
+
self._emit_description("Term being defined")
|
|
151
|
+
self._emit("term: String!")
|
|
152
|
+
self._emit_description("Definition text")
|
|
153
|
+
self._emit("definition: String!")
|
|
154
|
+
self._indent_level -= 1
|
|
155
|
+
self._emit("}")
|
|
156
|
+
self._emit_blank()
|
|
157
|
+
|
|
158
|
+
# Element type
|
|
159
|
+
self._emit_description("Element of an offense (actus reus or mens rea)")
|
|
160
|
+
self._emit("type Element {")
|
|
161
|
+
self._indent_level += 1
|
|
162
|
+
self._emit_description("Type of element")
|
|
163
|
+
self._emit("elementType: ElementType!")
|
|
164
|
+
self._emit_description("Element name/identifier")
|
|
165
|
+
self._emit("name: String!")
|
|
166
|
+
self._emit_description("Description of the element")
|
|
167
|
+
self._emit("description: String!")
|
|
168
|
+
self._indent_level -= 1
|
|
169
|
+
self._emit("}")
|
|
170
|
+
self._emit_blank()
|
|
171
|
+
|
|
172
|
+
# Penalty type
|
|
173
|
+
self._emit_description("Penalty specification for a statute")
|
|
174
|
+
self._emit("type Penalty {")
|
|
175
|
+
self._indent_level += 1
|
|
176
|
+
self._emit_description("Minimum imprisonment term")
|
|
177
|
+
self._emit("imprisonmentMin: Duration")
|
|
178
|
+
self._emit_description("Maximum imprisonment term")
|
|
179
|
+
self._emit("imprisonmentMax: Duration")
|
|
180
|
+
self._emit_description("Minimum fine amount")
|
|
181
|
+
self._emit("fineMin: Money")
|
|
182
|
+
self._emit_description("Maximum fine amount")
|
|
183
|
+
self._emit("fineMax: Money")
|
|
184
|
+
self._emit_description("Additional penalty information")
|
|
185
|
+
self._emit("supplementary: String")
|
|
186
|
+
self._indent_level -= 1
|
|
187
|
+
self._emit("}")
|
|
188
|
+
self._emit_blank()
|
|
189
|
+
|
|
190
|
+
# Illustration type
|
|
191
|
+
self._emit_description("Illustration example within a statute")
|
|
192
|
+
self._emit("type Illustration {")
|
|
193
|
+
self._indent_level += 1
|
|
194
|
+
self._emit_description("Label (e.g., '(a)', '(b)')")
|
|
195
|
+
self._emit("label: String")
|
|
196
|
+
self._emit_description("Illustration description")
|
|
197
|
+
self._emit("description: String!")
|
|
198
|
+
self._indent_level -= 1
|
|
199
|
+
self._emit("}")
|
|
200
|
+
self._emit_blank()
|
|
201
|
+
|
|
202
|
+
# Statute type
|
|
203
|
+
self._emit_description("Legal statute/provision")
|
|
204
|
+
self._emit("type Statute {")
|
|
205
|
+
self._indent_level += 1
|
|
206
|
+
self._emit_description("Section number (e.g., '299', '300')")
|
|
207
|
+
self._emit("sectionNumber: String!")
|
|
208
|
+
self._emit_description("Title of the statute")
|
|
209
|
+
self._emit("title: String")
|
|
210
|
+
self._emit_description("Legal definitions in this statute")
|
|
211
|
+
self._emit("definitions: [Definition!]!")
|
|
212
|
+
self._emit_description("Elements of the offense")
|
|
213
|
+
self._emit("elements: [Element!]!")
|
|
214
|
+
self._emit_description("Penalty specification")
|
|
215
|
+
self._emit("penalty: Penalty")
|
|
216
|
+
self._emit_description("Illustrative examples")
|
|
217
|
+
self._emit("illustrations: [Illustration!]!")
|
|
218
|
+
self._indent_level -= 1
|
|
219
|
+
self._emit("}")
|
|
220
|
+
self._emit_blank()
|
|
221
|
+
|
|
222
|
+
# =========================================================================
|
|
223
|
+
# Struct Definitions
|
|
224
|
+
# =========================================================================
|
|
225
|
+
|
|
226
|
+
def _visit_struct_def(self, node: nodes.StructDefNode) -> None:
|
|
227
|
+
"""Generate GraphQL type for struct definition."""
|
|
228
|
+
type_name = self._to_pascal_case(node.name)
|
|
229
|
+
if type_name in self._defined_types:
|
|
230
|
+
return
|
|
231
|
+
self._defined_types.add(type_name)
|
|
232
|
+
|
|
233
|
+
self._emit_description(f"Custom type: {node.name}")
|
|
234
|
+
self._emit(f"type {type_name} {{")
|
|
235
|
+
self._indent_level += 1
|
|
236
|
+
|
|
237
|
+
for field in node.fields:
|
|
238
|
+
field_name = self._to_camel_case(field.name)
|
|
239
|
+
field_type = self._type_to_graphql(field.type_annotation)
|
|
240
|
+
self._emit(f"{field_name}: {field_type}")
|
|
241
|
+
|
|
242
|
+
self._indent_level -= 1
|
|
243
|
+
self._emit("}")
|
|
244
|
+
self._emit_blank()
|
|
245
|
+
|
|
246
|
+
# =========================================================================
|
|
247
|
+
# Statute Processing (for query generation)
|
|
248
|
+
# =========================================================================
|
|
249
|
+
|
|
250
|
+
def _visit_statute(self, node: nodes.StatuteNode) -> None:
|
|
251
|
+
"""Process statute for query type generation."""
|
|
252
|
+
# Statutes are represented by the generic Statute type
|
|
253
|
+
# Individual statute data will be resolved at runtime
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
# =========================================================================
|
|
257
|
+
# Query Root
|
|
258
|
+
# =========================================================================
|
|
259
|
+
|
|
260
|
+
def _emit_query_root(self, ast: nodes.ModuleNode) -> None:
|
|
261
|
+
"""Emit the Query root type."""
|
|
262
|
+
self._emit_description("Root query type for statute API")
|
|
263
|
+
self._emit("type Query {")
|
|
264
|
+
self._indent_level += 1
|
|
265
|
+
|
|
266
|
+
# Query for single statute by section number
|
|
267
|
+
self._emit_description("Get a statute by section number")
|
|
268
|
+
self._emit("statute(sectionNumber: String!): Statute")
|
|
269
|
+
|
|
270
|
+
# Query for all statutes
|
|
271
|
+
self._emit_description("Get all statutes")
|
|
272
|
+
self._emit("statutes: [Statute!]!")
|
|
273
|
+
|
|
274
|
+
# Query for statutes by element type
|
|
275
|
+
self._emit_description("Find statutes containing a specific element type")
|
|
276
|
+
self._emit("statutesByElementType(elementType: ElementType!): [Statute!]!")
|
|
277
|
+
|
|
278
|
+
# Search statutes by text
|
|
279
|
+
self._emit_description("Search statutes by text in title or definitions")
|
|
280
|
+
self._emit("searchStatutes(query: String!): [Statute!]!")
|
|
281
|
+
|
|
282
|
+
# Get statute definitions
|
|
283
|
+
self._emit_description("Get all definitions across statutes")
|
|
284
|
+
self._emit("allDefinitions: [Definition!]!")
|
|
285
|
+
|
|
286
|
+
self._indent_level -= 1
|
|
287
|
+
self._emit("}")
|
|
288
|
+
self._emit_blank()
|
|
289
|
+
|
|
290
|
+
# Emit Mutation type for potential write operations
|
|
291
|
+
self._emit_description("Root mutation type for statute API")
|
|
292
|
+
self._emit("type Mutation {")
|
|
293
|
+
self._indent_level += 1
|
|
294
|
+
self._emit_description("Validate a statute definition (returns validation errors)")
|
|
295
|
+
self._emit("validateStatute(input: StatuteInput!): ValidationResult!")
|
|
296
|
+
self._indent_level -= 1
|
|
297
|
+
self._emit("}")
|
|
298
|
+
self._emit_blank()
|
|
299
|
+
|
|
300
|
+
# Emit input types
|
|
301
|
+
self._emit_input_types()
|
|
302
|
+
|
|
303
|
+
def _emit_input_types(self) -> None:
|
|
304
|
+
"""Emit GraphQL input types for mutations."""
|
|
305
|
+
# Statute input
|
|
306
|
+
self._emit_description("Input type for statute validation")
|
|
307
|
+
self._emit("input StatuteInput {")
|
|
308
|
+
self._indent_level += 1
|
|
309
|
+
self._emit("sectionNumber: String!")
|
|
310
|
+
self._emit("title: String")
|
|
311
|
+
self._emit("definitions: [DefinitionInput!]")
|
|
312
|
+
self._emit("elements: [ElementInput!]")
|
|
313
|
+
self._indent_level -= 1
|
|
314
|
+
self._emit("}")
|
|
315
|
+
self._emit_blank()
|
|
316
|
+
|
|
317
|
+
# Definition input
|
|
318
|
+
self._emit_description("Input type for definition")
|
|
319
|
+
self._emit("input DefinitionInput {")
|
|
320
|
+
self._indent_level += 1
|
|
321
|
+
self._emit("term: String!")
|
|
322
|
+
self._emit("definition: String!")
|
|
323
|
+
self._indent_level -= 1
|
|
324
|
+
self._emit("}")
|
|
325
|
+
self._emit_blank()
|
|
326
|
+
|
|
327
|
+
# Element input
|
|
328
|
+
self._emit_description("Input type for element")
|
|
329
|
+
self._emit("input ElementInput {")
|
|
330
|
+
self._indent_level += 1
|
|
331
|
+
self._emit("elementType: ElementType!")
|
|
332
|
+
self._emit("name: String!")
|
|
333
|
+
self._emit("description: String!")
|
|
334
|
+
self._indent_level -= 1
|
|
335
|
+
self._emit("}")
|
|
336
|
+
self._emit_blank()
|
|
337
|
+
|
|
338
|
+
# Validation result
|
|
339
|
+
self._emit_description("Result of statute validation")
|
|
340
|
+
self._emit("type ValidationResult {")
|
|
341
|
+
self._indent_level += 1
|
|
342
|
+
self._emit("valid: Boolean!")
|
|
343
|
+
self._emit("errors: [ValidationError!]!")
|
|
344
|
+
self._indent_level -= 1
|
|
345
|
+
self._emit("}")
|
|
346
|
+
self._emit_blank()
|
|
347
|
+
|
|
348
|
+
# Validation error
|
|
349
|
+
self._emit_description("Validation error details")
|
|
350
|
+
self._emit("type ValidationError {")
|
|
351
|
+
self._indent_level += 1
|
|
352
|
+
self._emit("field: String!")
|
|
353
|
+
self._emit("message: String!")
|
|
354
|
+
self._emit("code: String!")
|
|
355
|
+
self._indent_level -= 1
|
|
356
|
+
self._emit("}")
|
|
357
|
+
self._emit_blank()
|
|
358
|
+
|
|
359
|
+
# =========================================================================
|
|
360
|
+
# Type Conversion Helpers
|
|
361
|
+
# =========================================================================
|
|
362
|
+
|
|
363
|
+
def _type_to_graphql(self, node: nodes.TypeNode) -> str:
|
|
364
|
+
"""Convert Yuho type to GraphQL type."""
|
|
365
|
+
if isinstance(node, nodes.BuiltinType):
|
|
366
|
+
type_mapping = {
|
|
367
|
+
"int": "Int",
|
|
368
|
+
"float": "Float",
|
|
369
|
+
"bool": "Boolean",
|
|
370
|
+
"string": "String",
|
|
371
|
+
"money": "Money",
|
|
372
|
+
"percent": "Percent",
|
|
373
|
+
"date": "Date",
|
|
374
|
+
"duration": "Duration",
|
|
375
|
+
"void": "Void",
|
|
376
|
+
}
|
|
377
|
+
return type_mapping.get(node.name, "String")
|
|
378
|
+
|
|
379
|
+
elif isinstance(node, nodes.NamedType):
|
|
380
|
+
return self._to_pascal_case(node.name)
|
|
381
|
+
|
|
382
|
+
elif isinstance(node, nodes.OptionalType):
|
|
383
|
+
inner = self._type_to_graphql(node.inner)
|
|
384
|
+
# In GraphQL, types are nullable by default
|
|
385
|
+
# Remove ! if present since optional
|
|
386
|
+
return inner.rstrip("!")
|
|
387
|
+
|
|
388
|
+
elif isinstance(node, nodes.ArrayType):
|
|
389
|
+
elem = self._type_to_graphql(node.element_type)
|
|
390
|
+
return f"[{elem}!]!"
|
|
391
|
+
|
|
392
|
+
return "String"
|
|
393
|
+
|
|
394
|
+
def _to_pascal_case(self, name: str) -> str:
|
|
395
|
+
"""Convert name to PascalCase for GraphQL types."""
|
|
396
|
+
parts = name.replace("-", "_").split("_")
|
|
397
|
+
return "".join(p.capitalize() for p in parts)
|
|
398
|
+
|
|
399
|
+
def _to_camel_case(self, name: str) -> str:
|
|
400
|
+
"""Convert name to camelCase for GraphQL fields."""
|
|
401
|
+
parts = name.replace("-", "_").split("_")
|
|
402
|
+
if not parts:
|
|
403
|
+
return name
|
|
404
|
+
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON transpiler - serialize AST to JSON with type discriminators.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import date
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, Dict, 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 JSONTranspiler(TranspilerBase, Visitor):
|
|
16
|
+
"""
|
|
17
|
+
Transpile Yuho AST to JSON format.
|
|
18
|
+
|
|
19
|
+
Includes type discriminators ("_type" field) and source locations
|
|
20
|
+
for each node to enable round-trip parsing.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, include_locations: bool = True, indent: int = 2):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the JSON transpiler.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
include_locations: Whether to include source locations
|
|
29
|
+
indent: JSON indentation level (0 for compact)
|
|
30
|
+
"""
|
|
31
|
+
self.include_locations = include_locations
|
|
32
|
+
self.indent = indent
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def target(self) -> TranspileTarget:
|
|
36
|
+
return TranspileTarget.JSON
|
|
37
|
+
|
|
38
|
+
def transpile(self, ast: nodes.ModuleNode) -> str:
|
|
39
|
+
"""Transpile AST to JSON string."""
|
|
40
|
+
data = self._to_dict(ast)
|
|
41
|
+
return json.dumps(data, indent=self.indent if self.indent else None, ensure_ascii=False)
|
|
42
|
+
|
|
43
|
+
def _to_dict(self, node: nodes.ASTNode) -> Dict[str, Any]:
|
|
44
|
+
"""Convert an AST node to a dictionary."""
|
|
45
|
+
result: Dict[str, Any] = {"_type": type(node).__name__}
|
|
46
|
+
|
|
47
|
+
# Add source location if enabled
|
|
48
|
+
if self.include_locations and node.source_location:
|
|
49
|
+
loc = node.source_location
|
|
50
|
+
result["_loc"] = {
|
|
51
|
+
"file": loc.file,
|
|
52
|
+
"line": loc.line,
|
|
53
|
+
"col": loc.col,
|
|
54
|
+
"end_line": loc.end_line,
|
|
55
|
+
"end_col": loc.end_col,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Handle specific node types
|
|
59
|
+
if isinstance(node, nodes.IntLit):
|
|
60
|
+
result["value"] = node.value
|
|
61
|
+
elif isinstance(node, nodes.FloatLit):
|
|
62
|
+
result["value"] = node.value
|
|
63
|
+
elif isinstance(node, nodes.BoolLit):
|
|
64
|
+
result["value"] = node.value
|
|
65
|
+
elif isinstance(node, nodes.StringLit):
|
|
66
|
+
result["value"] = node.value
|
|
67
|
+
elif isinstance(node, nodes.MoneyNode):
|
|
68
|
+
result["currency"] = node.currency.name
|
|
69
|
+
result["amount"] = str(node.amount)
|
|
70
|
+
elif isinstance(node, nodes.PercentNode):
|
|
71
|
+
result["value"] = str(node.value)
|
|
72
|
+
elif isinstance(node, nodes.DateNode):
|
|
73
|
+
result["value"] = node.value.isoformat()
|
|
74
|
+
elif isinstance(node, nodes.DurationNode):
|
|
75
|
+
result["years"] = node.years
|
|
76
|
+
result["months"] = node.months
|
|
77
|
+
result["days"] = node.days
|
|
78
|
+
result["hours"] = node.hours
|
|
79
|
+
result["minutes"] = node.minutes
|
|
80
|
+
result["seconds"] = node.seconds
|
|
81
|
+
elif isinstance(node, nodes.IdentifierNode):
|
|
82
|
+
result["name"] = node.name
|
|
83
|
+
elif isinstance(node, nodes.FieldAccessNode):
|
|
84
|
+
result["base"] = self._to_dict(node.base)
|
|
85
|
+
result["field_name"] = node.field_name
|
|
86
|
+
elif isinstance(node, nodes.IndexAccessNode):
|
|
87
|
+
result["base"] = self._to_dict(node.base)
|
|
88
|
+
result["index"] = self._to_dict(node.index)
|
|
89
|
+
elif isinstance(node, nodes.FunctionCallNode):
|
|
90
|
+
result["callee"] = self._to_dict(node.callee)
|
|
91
|
+
result["args"] = [self._to_dict(a) for a in node.args]
|
|
92
|
+
elif isinstance(node, nodes.BinaryExprNode):
|
|
93
|
+
result["left"] = self._to_dict(node.left)
|
|
94
|
+
result["operator"] = node.operator
|
|
95
|
+
result["right"] = self._to_dict(node.right)
|
|
96
|
+
elif isinstance(node, nodes.UnaryExprNode):
|
|
97
|
+
result["operator"] = node.operator
|
|
98
|
+
result["operand"] = self._to_dict(node.operand)
|
|
99
|
+
elif isinstance(node, nodes.PassExprNode):
|
|
100
|
+
pass # Just the type is enough
|
|
101
|
+
elif isinstance(node, nodes.WildcardPattern):
|
|
102
|
+
pass
|
|
103
|
+
elif isinstance(node, nodes.LiteralPattern):
|
|
104
|
+
result["literal"] = self._to_dict(node.literal)
|
|
105
|
+
elif isinstance(node, nodes.BindingPattern):
|
|
106
|
+
result["name"] = node.name
|
|
107
|
+
elif isinstance(node, nodes.FieldPattern):
|
|
108
|
+
result["name"] = node.name
|
|
109
|
+
if node.pattern:
|
|
110
|
+
result["pattern"] = self._to_dict(node.pattern)
|
|
111
|
+
elif isinstance(node, nodes.StructPattern):
|
|
112
|
+
result["type_name"] = node.type_name
|
|
113
|
+
result["fields"] = [self._to_dict(f) for f in node.fields]
|
|
114
|
+
elif isinstance(node, nodes.MatchArm):
|
|
115
|
+
result["pattern"] = self._to_dict(node.pattern)
|
|
116
|
+
if node.guard:
|
|
117
|
+
result["guard"] = self._to_dict(node.guard)
|
|
118
|
+
result["body"] = self._to_dict(node.body)
|
|
119
|
+
elif isinstance(node, nodes.MatchExprNode):
|
|
120
|
+
if node.scrutinee:
|
|
121
|
+
result["scrutinee"] = self._to_dict(node.scrutinee)
|
|
122
|
+
result["arms"] = [self._to_dict(a) for a in node.arms]
|
|
123
|
+
result["ensure_exhaustiveness"] = node.ensure_exhaustiveness
|
|
124
|
+
elif isinstance(node, nodes.BuiltinType):
|
|
125
|
+
result["name"] = node.name
|
|
126
|
+
elif isinstance(node, nodes.NamedType):
|
|
127
|
+
result["name"] = node.name
|
|
128
|
+
elif isinstance(node, nodes.GenericType):
|
|
129
|
+
result["base"] = node.base
|
|
130
|
+
result["type_args"] = [self._to_dict(t) for t in node.type_args]
|
|
131
|
+
elif isinstance(node, nodes.OptionalType):
|
|
132
|
+
result["inner"] = self._to_dict(node.inner)
|
|
133
|
+
elif isinstance(node, nodes.ArrayType):
|
|
134
|
+
result["element_type"] = self._to_dict(node.element_type)
|
|
135
|
+
elif isinstance(node, nodes.FieldDef):
|
|
136
|
+
result["type"] = self._to_dict(node.type_annotation)
|
|
137
|
+
result["name"] = node.name
|
|
138
|
+
elif isinstance(node, nodes.StructDefNode):
|
|
139
|
+
result["name"] = node.name
|
|
140
|
+
result["fields"] = [self._to_dict(f) for f in node.fields]
|
|
141
|
+
if node.type_params:
|
|
142
|
+
result["type_params"] = list(node.type_params)
|
|
143
|
+
elif isinstance(node, nodes.FieldAssignment):
|
|
144
|
+
result["name"] = node.name
|
|
145
|
+
result["value"] = self._to_dict(node.value)
|
|
146
|
+
elif isinstance(node, nodes.StructLiteralNode):
|
|
147
|
+
if node.struct_name:
|
|
148
|
+
result["struct_name"] = node.struct_name
|
|
149
|
+
result["fields"] = [self._to_dict(f) for f in node.field_values]
|
|
150
|
+
elif isinstance(node, nodes.ParamDef):
|
|
151
|
+
result["type"] = self._to_dict(node.type_annotation)
|
|
152
|
+
result["name"] = node.name
|
|
153
|
+
elif isinstance(node, nodes.Block):
|
|
154
|
+
result["statements"] = [self._to_dict(s) for s in node.statements]
|
|
155
|
+
elif isinstance(node, nodes.FunctionDefNode):
|
|
156
|
+
result["name"] = node.name
|
|
157
|
+
result["params"] = [self._to_dict(p) for p in node.params]
|
|
158
|
+
if node.return_type:
|
|
159
|
+
result["return_type"] = self._to_dict(node.return_type)
|
|
160
|
+
result["body"] = self._to_dict(node.body)
|
|
161
|
+
elif isinstance(node, nodes.VariableDecl):
|
|
162
|
+
result["type"] = self._to_dict(node.type_annotation)
|
|
163
|
+
result["name"] = node.name
|
|
164
|
+
if node.value:
|
|
165
|
+
result["value"] = self._to_dict(node.value)
|
|
166
|
+
elif isinstance(node, nodes.AssignmentStmt):
|
|
167
|
+
result["target"] = self._to_dict(node.target)
|
|
168
|
+
result["value"] = self._to_dict(node.value)
|
|
169
|
+
elif isinstance(node, nodes.ReturnStmt):
|
|
170
|
+
if node.value:
|
|
171
|
+
result["value"] = self._to_dict(node.value)
|
|
172
|
+
elif isinstance(node, nodes.PassStmt):
|
|
173
|
+
pass
|
|
174
|
+
elif isinstance(node, nodes.ExpressionStmt):
|
|
175
|
+
result["expression"] = self._to_dict(node.expression)
|
|
176
|
+
elif isinstance(node, nodes.DefinitionEntry):
|
|
177
|
+
result["term"] = node.term
|
|
178
|
+
result["definition"] = self._to_dict(node.definition)
|
|
179
|
+
elif isinstance(node, nodes.ElementNode):
|
|
180
|
+
result["element_type"] = node.element_type
|
|
181
|
+
result["name"] = node.name
|
|
182
|
+
result["description"] = self._to_dict(node.description)
|
|
183
|
+
elif isinstance(node, nodes.PenaltyNode):
|
|
184
|
+
if node.imprisonment_min:
|
|
185
|
+
result["imprisonment_min"] = self._to_dict(node.imprisonment_min)
|
|
186
|
+
if node.imprisonment_max:
|
|
187
|
+
result["imprisonment_max"] = self._to_dict(node.imprisonment_max)
|
|
188
|
+
if node.fine_min:
|
|
189
|
+
result["fine_min"] = self._to_dict(node.fine_min)
|
|
190
|
+
if node.fine_max:
|
|
191
|
+
result["fine_max"] = self._to_dict(node.fine_max)
|
|
192
|
+
if node.supplementary:
|
|
193
|
+
result["supplementary"] = self._to_dict(node.supplementary)
|
|
194
|
+
elif isinstance(node, nodes.IllustrationNode):
|
|
195
|
+
if node.label:
|
|
196
|
+
result["label"] = node.label
|
|
197
|
+
result["description"] = self._to_dict(node.description)
|
|
198
|
+
elif isinstance(node, nodes.StatuteNode):
|
|
199
|
+
result["section_number"] = node.section_number
|
|
200
|
+
if node.title:
|
|
201
|
+
result["title"] = self._to_dict(node.title)
|
|
202
|
+
result["definitions"] = [self._to_dict(d) for d in node.definitions]
|
|
203
|
+
result["elements"] = [self._to_dict(e) for e in node.elements]
|
|
204
|
+
if node.penalty:
|
|
205
|
+
result["penalty"] = self._to_dict(node.penalty)
|
|
206
|
+
result["illustrations"] = [self._to_dict(i) for i in node.illustrations]
|
|
207
|
+
elif isinstance(node, nodes.ImportNode):
|
|
208
|
+
result["path"] = node.path
|
|
209
|
+
result["imported_names"] = list(node.imported_names)
|
|
210
|
+
elif isinstance(node, nodes.ModuleNode):
|
|
211
|
+
result["imports"] = [self._to_dict(i) for i in node.imports]
|
|
212
|
+
result["type_defs"] = [self._to_dict(t) for t in node.type_defs]
|
|
213
|
+
result["function_defs"] = [self._to_dict(f) for f in node.function_defs]
|
|
214
|
+
result["statutes"] = [self._to_dict(s) for s in node.statutes]
|
|
215
|
+
result["variables"] = [self._to_dict(v) for v in node.variables]
|
|
216
|
+
|
|
217
|
+
return result
|