yuho 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/ast/nodes.py ADDED
@@ -0,0 +1,907 @@
1
+ """
2
+ AST node definitions for Yuho v5.
3
+
4
+ All nodes are immutable dataclasses with source_location tracking
5
+ and an accept(visitor) method for the Visitor pattern.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from datetime import date
12
+ from decimal import Decimal
13
+ from enum import Enum, auto
14
+ from typing import TYPE_CHECKING, Optional, List, Dict, Tuple, Union
15
+
16
+ if TYPE_CHECKING:
17
+ from yuho.ast.visitor import Visitor
18
+ from yuho.parser.source_location import SourceLocation
19
+
20
+
21
+ # =============================================================================
22
+ # Currency Enum
23
+ # =============================================================================
24
+
25
+
26
+ class Currency(Enum):
27
+ """Supported currency types for MoneyNode."""
28
+
29
+ SGD = auto()
30
+ USD = auto()
31
+ EUR = auto()
32
+ GBP = auto()
33
+ JPY = auto()
34
+ CNY = auto()
35
+ INR = auto()
36
+ AUD = auto()
37
+ CAD = auto()
38
+ CHF = auto()
39
+
40
+ @classmethod
41
+ def from_symbol(cls, symbol: str) -> "Currency":
42
+ """Convert currency symbol to Currency enum."""
43
+ mapping = {
44
+ "$": cls.SGD, # Default $ to SGD for Singapore context
45
+ "£": cls.GBP,
46
+ "€": cls.EUR,
47
+ "¥": cls.JPY,
48
+ "₹": cls.INR,
49
+ "SGD": cls.SGD,
50
+ "USD": cls.USD,
51
+ "EUR": cls.EUR,
52
+ "GBP": cls.GBP,
53
+ "JPY": cls.JPY,
54
+ "CNY": cls.CNY,
55
+ "INR": cls.INR,
56
+ "AUD": cls.AUD,
57
+ "CAD": cls.CAD,
58
+ "CHF": cls.CHF,
59
+ }
60
+ return mapping.get(symbol, cls.USD)
61
+
62
+
63
+ # =============================================================================
64
+ # Base AST Node
65
+ # =============================================================================
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ASTNode(ABC):
70
+ """
71
+ Base class for all AST nodes.
72
+
73
+ All nodes are immutable (frozen dataclass) and carry source location
74
+ information for error reporting and IDE features.
75
+ """
76
+
77
+ source_location: Optional["SourceLocation"] = field(default=None, compare=False)
78
+
79
+ @abstractmethod
80
+ def accept(self, visitor: "Visitor"):
81
+ """Accept a visitor for double-dispatch traversal."""
82
+ pass
83
+
84
+ def children(self) -> List["ASTNode"]:
85
+ """Return child nodes for generic traversal. Override in subclasses."""
86
+ return []
87
+
88
+
89
+ # =============================================================================
90
+ # Type Nodes
91
+ # =============================================================================
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class TypeNode(ASTNode):
96
+ """Base class for type annotations."""
97
+
98
+ def accept(self, visitor: "Visitor"):
99
+ return visitor.visit_type(self)
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class BuiltinType(TypeNode):
104
+ """Built-in primitive type (int, float, bool, string, money, etc.)."""
105
+
106
+ name: str # "int", "float", "bool", "string", "money", "percent", "date", "duration", "void"
107
+
108
+ def accept(self, visitor: "Visitor"):
109
+ return visitor.visit_builtin_type(self)
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class NamedType(TypeNode):
114
+ """User-defined type reference (struct name)."""
115
+
116
+ name: str
117
+
118
+ def accept(self, visitor: "Visitor"):
119
+ return visitor.visit_named_type(self)
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class GenericType(TypeNode):
124
+ """Generic type application, e.g., List<int>."""
125
+
126
+ base: str
127
+ type_args: Tuple[TypeNode, ...]
128
+
129
+ def accept(self, visitor: "Visitor"):
130
+ return visitor.visit_generic_type(self)
131
+
132
+ def children(self) -> List[ASTNode]:
133
+ return list(self.type_args)
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class OptionalType(TypeNode):
138
+ """Optional type, e.g., int?."""
139
+
140
+ inner: TypeNode
141
+
142
+ def accept(self, visitor: "Visitor"):
143
+ return visitor.visit_optional_type(self)
144
+
145
+ def children(self) -> List[ASTNode]:
146
+ return [self.inner]
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class ArrayType(TypeNode):
151
+ """Array type, e.g., [int]."""
152
+
153
+ element_type: TypeNode
154
+
155
+ def accept(self, visitor: "Visitor"):
156
+ return visitor.visit_array_type(self)
157
+
158
+ def children(self) -> List[ASTNode]:
159
+ return [self.element_type]
160
+
161
+
162
+ # =============================================================================
163
+ # Literal Nodes
164
+ # =============================================================================
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class IntLit(ASTNode):
169
+ """Integer literal."""
170
+
171
+ value: int
172
+
173
+ def accept(self, visitor: "Visitor"):
174
+ return visitor.visit_int_lit(self)
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class FloatLit(ASTNode):
179
+ """Floating-point literal."""
180
+
181
+ value: float
182
+
183
+ def accept(self, visitor: "Visitor"):
184
+ return visitor.visit_float_lit(self)
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class BoolLit(ASTNode):
189
+ """Boolean literal (TRUE/FALSE)."""
190
+
191
+ value: bool
192
+
193
+ def accept(self, visitor: "Visitor"):
194
+ return visitor.visit_bool_lit(self)
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class StringLit(ASTNode):
199
+ """String literal with escape sequences processed."""
200
+
201
+ value: str
202
+
203
+ def accept(self, visitor: "Visitor"):
204
+ return visitor.visit_string_lit(self)
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class MoneyNode(ASTNode):
209
+ """
210
+ Money literal with currency and amount.
211
+
212
+ The amount is stored as Decimal for precise financial calculations.
213
+ """
214
+
215
+ currency: Currency
216
+ amount: Decimal
217
+
218
+ def __post_init__(self):
219
+ if not isinstance(self.amount, Decimal):
220
+ object.__setattr__(self, "amount", Decimal(str(self.amount)))
221
+
222
+ def accept(self, visitor: "Visitor"):
223
+ return visitor.visit_money(self)
224
+
225
+
226
+ @dataclass(frozen=True)
227
+ class PercentNode(ASTNode):
228
+ """
229
+ Percentage literal.
230
+
231
+ The value is stored as Decimal and validated to be in 0-100 range.
232
+ """
233
+
234
+ value: Decimal
235
+
236
+ def __post_init__(self):
237
+ if not isinstance(self.value, Decimal):
238
+ object.__setattr__(self, "value", Decimal(str(self.value)))
239
+ if not (0 <= self.value <= 100):
240
+ raise ValueError(f"Percent value must be 0-100, got {self.value}")
241
+
242
+ def accept(self, visitor: "Visitor"):
243
+ return visitor.visit_percent(self)
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class DateNode(ASTNode):
248
+ """
249
+ Date literal in ISO8601 format (YYYY-MM-DD).
250
+
251
+ Wraps datetime.date for date operations.
252
+ """
253
+
254
+ value: date
255
+
256
+ @classmethod
257
+ def from_iso8601(cls, date_str: str, source_location=None) -> "DateNode":
258
+ """Parse ISO8601 date string."""
259
+ return cls(value=date.fromisoformat(date_str), source_location=source_location)
260
+
261
+ def accept(self, visitor: "Visitor"):
262
+ return visitor.visit_date(self)
263
+
264
+
265
+ @dataclass(frozen=True)
266
+ class DurationNode(ASTNode):
267
+ """
268
+ Duration literal with years, months, days, hours, minutes, seconds.
269
+
270
+ Any component can be 0. The total duration is the sum of all components.
271
+ """
272
+
273
+ years: int = 0
274
+ months: int = 0
275
+ days: int = 0
276
+ hours: int = 0
277
+ minutes: int = 0
278
+ seconds: int = 0
279
+
280
+ def accept(self, visitor: "Visitor"):
281
+ return visitor.visit_duration(self)
282
+
283
+ def total_days(self) -> int:
284
+ """Approximate total days (assumes 30 days/month, 365 days/year)."""
285
+ return self.years * 365 + self.months * 30 + self.days
286
+
287
+ def __str__(self) -> str:
288
+ parts = []
289
+ if self.years:
290
+ parts.append(f"{self.years} year{'s' if self.years != 1 else ''}")
291
+ if self.months:
292
+ parts.append(f"{self.months} month{'s' if self.months != 1 else ''}")
293
+ if self.days:
294
+ parts.append(f"{self.days} day{'s' if self.days != 1 else ''}")
295
+ if self.hours:
296
+ parts.append(f"{self.hours} hour{'s' if self.hours != 1 else ''}")
297
+ if self.minutes:
298
+ parts.append(f"{self.minutes} minute{'s' if self.minutes != 1 else ''}")
299
+ if self.seconds:
300
+ parts.append(f"{self.seconds} second{'s' if self.seconds != 1 else ''}")
301
+ return ", ".join(parts) if parts else "0 days"
302
+
303
+
304
+ # =============================================================================
305
+ # Expression Nodes
306
+ # =============================================================================
307
+
308
+
309
+ @dataclass(frozen=True)
310
+ class IdentifierNode(ASTNode):
311
+ """Identifier reference."""
312
+
313
+ name: str
314
+
315
+ def accept(self, visitor: "Visitor"):
316
+ return visitor.visit_identifier(self)
317
+
318
+
319
+ @dataclass(frozen=True)
320
+ class FieldAccessNode(ASTNode):
321
+ """Field access expression: base.field."""
322
+
323
+ base: ASTNode # ExprNode
324
+ field_name: str
325
+
326
+ def accept(self, visitor: "Visitor"):
327
+ return visitor.visit_field_access(self)
328
+
329
+ def children(self) -> List[ASTNode]:
330
+ return [self.base]
331
+
332
+
333
+ @dataclass(frozen=True)
334
+ class IndexAccessNode(ASTNode):
335
+ """Index access expression: base[index]."""
336
+
337
+ base: ASTNode # ExprNode
338
+ index: ASTNode # ExprNode
339
+
340
+ def accept(self, visitor: "Visitor"):
341
+ return visitor.visit_index_access(self)
342
+
343
+ def children(self) -> List[ASTNode]:
344
+ return [self.base, self.index]
345
+
346
+
347
+ @dataclass(frozen=True)
348
+ class FunctionCallNode(ASTNode):
349
+ """Function call expression: callee(args...)."""
350
+
351
+ callee: Union[IdentifierNode, FieldAccessNode]
352
+ args: Tuple[ASTNode, ...]
353
+
354
+ def accept(self, visitor: "Visitor"):
355
+ return visitor.visit_function_call(self)
356
+
357
+ def children(self) -> List[ASTNode]:
358
+ return [self.callee] + list(self.args)
359
+
360
+
361
+ @dataclass(frozen=True)
362
+ class BinaryExprNode(ASTNode):
363
+ """Binary expression: left op right."""
364
+
365
+ left: ASTNode
366
+ operator: str # "+", "-", "*", "/", "==", "!=", "<", ">", "<=", ">=", "&&", "||"
367
+ right: ASTNode
368
+
369
+ def accept(self, visitor: "Visitor"):
370
+ return visitor.visit_binary_expr(self)
371
+
372
+ def children(self) -> List[ASTNode]:
373
+ return [self.left, self.right]
374
+
375
+
376
+ @dataclass(frozen=True)
377
+ class UnaryExprNode(ASTNode):
378
+ """Unary expression: op operand."""
379
+
380
+ operator: str # "!", "-"
381
+ operand: ASTNode
382
+
383
+ def accept(self, visitor: "Visitor"):
384
+ return visitor.visit_unary_expr(self)
385
+
386
+ def children(self) -> List[ASTNode]:
387
+ return [self.operand]
388
+
389
+
390
+ @dataclass(frozen=True)
391
+ class PassExprNode(ASTNode):
392
+ """Pass expression (null/none equivalent)."""
393
+
394
+ def accept(self, visitor: "Visitor"):
395
+ return visitor.visit_pass_expr(self)
396
+
397
+
398
+ # =============================================================================
399
+ # Pattern Nodes
400
+ # =============================================================================
401
+
402
+
403
+ @dataclass(frozen=True)
404
+ class PatternNode(ASTNode):
405
+ """Base class for match patterns."""
406
+
407
+ def accept(self, visitor: "Visitor"):
408
+ return visitor.visit_pattern(self)
409
+
410
+
411
+ @dataclass(frozen=True)
412
+ class WildcardPattern(PatternNode):
413
+ """Wildcard pattern (_) that matches anything."""
414
+
415
+ def accept(self, visitor: "Visitor"):
416
+ return visitor.visit_wildcard_pattern(self)
417
+
418
+
419
+ @dataclass(frozen=True)
420
+ class LiteralPattern(PatternNode):
421
+ """Literal pattern that matches a specific value."""
422
+
423
+ literal: ASTNode # IntLit, StringLit, BoolLit, etc.
424
+
425
+ def accept(self, visitor: "Visitor"):
426
+ return visitor.visit_literal_pattern(self)
427
+
428
+ def children(self) -> List[ASTNode]:
429
+ return [self.literal]
430
+
431
+
432
+ @dataclass(frozen=True)
433
+ class BindingPattern(PatternNode):
434
+ """Binding pattern that captures the matched value."""
435
+
436
+ name: str
437
+
438
+ def accept(self, visitor: "Visitor"):
439
+ return visitor.visit_binding_pattern(self)
440
+
441
+
442
+ @dataclass(frozen=True)
443
+ class FieldPattern(ASTNode):
444
+ """Field pattern within a struct pattern."""
445
+
446
+ name: str
447
+ pattern: Optional[PatternNode] = None # None means just match by name
448
+
449
+ def accept(self, visitor: "Visitor"):
450
+ return visitor.visit_field_pattern(self)
451
+
452
+ def children(self) -> List[ASTNode]:
453
+ return [self.pattern] if self.pattern else []
454
+
455
+
456
+ @dataclass(frozen=True)
457
+ class StructPattern(PatternNode):
458
+ """Struct pattern for destructuring."""
459
+
460
+ type_name: str
461
+ fields: Tuple[FieldPattern, ...]
462
+
463
+ def accept(self, visitor: "Visitor"):
464
+ return visitor.visit_struct_pattern(self)
465
+
466
+ def children(self) -> List[ASTNode]:
467
+ return list(self.fields)
468
+
469
+
470
+ # =============================================================================
471
+ # Match Expression
472
+ # =============================================================================
473
+
474
+
475
+ @dataclass(frozen=True)
476
+ class MatchArm(ASTNode):
477
+ """A single arm in a match expression."""
478
+
479
+ pattern: PatternNode
480
+ guard: Optional[ASTNode] = None # Optional guard expression
481
+ body: ASTNode = field(default_factory=PassExprNode) # ExprNode
482
+
483
+ def accept(self, visitor: "Visitor"):
484
+ return visitor.visit_match_arm(self)
485
+
486
+ def children(self) -> List[ASTNode]:
487
+ result = [self.pattern]
488
+ if self.guard:
489
+ result.append(self.guard)
490
+ result.append(self.body)
491
+ return result
492
+
493
+
494
+ @dataclass(frozen=True)
495
+ class MatchExprNode(ASTNode):
496
+ """
497
+ Match expression with scrutinee and arms.
498
+
499
+ The ensure_exhaustiveness flag indicates whether the type checker
500
+ should verify that all cases are covered.
501
+ """
502
+
503
+ scrutinee: Optional[ASTNode] # None for bare match blocks
504
+ arms: Tuple[MatchArm, ...]
505
+ ensure_exhaustiveness: bool = True
506
+
507
+ def accept(self, visitor: "Visitor"):
508
+ return visitor.visit_match_expr(self)
509
+
510
+ def children(self) -> List[ASTNode]:
511
+ result = []
512
+ if self.scrutinee:
513
+ result.append(self.scrutinee)
514
+ result.extend(self.arms)
515
+ return result
516
+
517
+
518
+ # =============================================================================
519
+ # Struct Definition and Literal
520
+ # =============================================================================
521
+
522
+
523
+ @dataclass(frozen=True)
524
+ class FieldDef(ASTNode):
525
+ """Field definition within a struct."""
526
+
527
+ type_annotation: TypeNode
528
+ name: str
529
+
530
+ def accept(self, visitor: "Visitor"):
531
+ return visitor.visit_field_def(self)
532
+
533
+ def children(self) -> List[ASTNode]:
534
+ return [self.type_annotation]
535
+
536
+
537
+ @dataclass(frozen=True)
538
+ class StructDefNode(ASTNode):
539
+ """Struct type definition."""
540
+
541
+ name: str
542
+ fields: Tuple[FieldDef, ...]
543
+ type_params: Tuple[str, ...] = () # Generic type parameters
544
+
545
+ def accept(self, visitor: "Visitor"):
546
+ return visitor.visit_struct_def(self)
547
+
548
+ def children(self) -> List[ASTNode]:
549
+ return list(self.fields)
550
+
551
+
552
+ @dataclass(frozen=True)
553
+ class FieldAssignment(ASTNode):
554
+ """Field assignment within a struct literal."""
555
+
556
+ name: str
557
+ value: ASTNode
558
+
559
+ def accept(self, visitor: "Visitor"):
560
+ return visitor.visit_field_assignment(self)
561
+
562
+ def children(self) -> List[ASTNode]:
563
+ return [self.value]
564
+
565
+
566
+ @dataclass(frozen=True)
567
+ class StructLiteralNode(ASTNode):
568
+ """Struct literal instantiation."""
569
+
570
+ struct_name: Optional[str] # None for anonymous struct literals
571
+ field_values: Tuple[FieldAssignment, ...]
572
+
573
+ def accept(self, visitor: "Visitor"):
574
+ return visitor.visit_struct_literal(self)
575
+
576
+ def children(self) -> List[ASTNode]:
577
+ return list(self.field_values)
578
+
579
+ def get_field(self, name: str) -> Optional[FieldAssignment]:
580
+ """Get a field assignment by name."""
581
+ for fa in self.field_values:
582
+ if fa.name == name:
583
+ return fa
584
+ return None
585
+
586
+
587
+ # =============================================================================
588
+ # Function Definition
589
+ # =============================================================================
590
+
591
+
592
+ @dataclass(frozen=True)
593
+ class ParamDef(ASTNode):
594
+ """Parameter definition in a function."""
595
+
596
+ type_annotation: TypeNode
597
+ name: str
598
+
599
+ def accept(self, visitor: "Visitor"):
600
+ return visitor.visit_param_def(self)
601
+
602
+ def children(self) -> List[ASTNode]:
603
+ return [self.type_annotation]
604
+
605
+
606
+ @dataclass(frozen=True)
607
+ class Block(ASTNode):
608
+ """Block of statements."""
609
+
610
+ statements: Tuple[ASTNode, ...]
611
+
612
+ def accept(self, visitor: "Visitor"):
613
+ return visitor.visit_block(self)
614
+
615
+ def children(self) -> List[ASTNode]:
616
+ return list(self.statements)
617
+
618
+
619
+ @dataclass(frozen=True)
620
+ class FunctionDefNode(ASTNode):
621
+ """Function definition."""
622
+
623
+ name: str
624
+ params: Tuple[ParamDef, ...]
625
+ return_type: Optional[TypeNode]
626
+ body: Block
627
+
628
+ def accept(self, visitor: "Visitor"):
629
+ return visitor.visit_function_def(self)
630
+
631
+ def children(self) -> List[ASTNode]:
632
+ result: List[ASTNode] = list(self.params)
633
+ if self.return_type:
634
+ result.append(self.return_type)
635
+ result.append(self.body)
636
+ return result
637
+
638
+
639
+ # =============================================================================
640
+ # Statements
641
+ # =============================================================================
642
+
643
+
644
+ @dataclass(frozen=True)
645
+ class VariableDecl(ASTNode):
646
+ """Variable declaration statement."""
647
+
648
+ type_annotation: TypeNode
649
+ name: str
650
+ value: Optional[ASTNode] = None
651
+
652
+ def accept(self, visitor: "Visitor"):
653
+ return visitor.visit_variable_decl(self)
654
+
655
+ def children(self) -> List[ASTNode]:
656
+ result = [self.type_annotation]
657
+ if self.value:
658
+ result.append(self.value)
659
+ return result
660
+
661
+
662
+ @dataclass(frozen=True)
663
+ class AssignmentStmt(ASTNode):
664
+ """Assignment statement."""
665
+
666
+ target: ASTNode # IdentifierNode, FieldAccessNode, or IndexAccessNode
667
+ value: ASTNode
668
+
669
+ def accept(self, visitor: "Visitor"):
670
+ return visitor.visit_assignment_stmt(self)
671
+
672
+ def children(self) -> List[ASTNode]:
673
+ return [self.target, self.value]
674
+
675
+
676
+ @dataclass(frozen=True)
677
+ class ReturnStmt(ASTNode):
678
+ """Return statement."""
679
+
680
+ value: Optional[ASTNode] = None
681
+
682
+ def accept(self, visitor: "Visitor"):
683
+ return visitor.visit_return_stmt(self)
684
+
685
+ def children(self) -> List[ASTNode]:
686
+ return [self.value] if self.value else []
687
+
688
+
689
+ @dataclass(frozen=True)
690
+ class PassStmt(ASTNode):
691
+ """Pass statement (no-op)."""
692
+
693
+ def accept(self, visitor: "Visitor"):
694
+ return visitor.visit_pass_stmt(self)
695
+
696
+
697
+ @dataclass(frozen=True)
698
+ class AssertStmt(ASTNode):
699
+ """Assert statement for test files."""
700
+
701
+ condition: ASTNode # Usually a BinaryExprNode with ==
702
+ message: Optional[StringLit] = None
703
+
704
+ def accept(self, visitor: "Visitor"):
705
+ return visitor.visit_assert_stmt(self)
706
+
707
+ def children(self) -> List[ASTNode]:
708
+ result = [self.condition]
709
+ if self.message:
710
+ result.append(self.message)
711
+ return result
712
+
713
+
714
+ @dataclass(frozen=True)
715
+ class ReferencingStmt(ASTNode):
716
+ """Referencing statement for test files to import statutes."""
717
+
718
+ path: str # e.g., "s300_murder/statute"
719
+
720
+ def accept(self, visitor: "Visitor"):
721
+ return visitor.visit_referencing_stmt(self)
722
+
723
+
724
+ @dataclass(frozen=True)
725
+ class ExpressionStmt(ASTNode):
726
+ """Expression statement."""
727
+
728
+ expression: ASTNode
729
+
730
+ def accept(self, visitor: "Visitor"):
731
+ return visitor.visit_expression_stmt(self)
732
+
733
+ def children(self) -> List[ASTNode]:
734
+ return [self.expression]
735
+
736
+
737
+ # =============================================================================
738
+ # Statute-specific Nodes
739
+ # =============================================================================
740
+
741
+
742
+ @dataclass(frozen=True)
743
+ class DefinitionEntry(ASTNode):
744
+ """Definition entry within a statute's definitions block."""
745
+
746
+ term: str
747
+ definition: StringLit
748
+
749
+ def accept(self, visitor: "Visitor"):
750
+ return visitor.visit_definition_entry(self)
751
+
752
+ def children(self) -> List[ASTNode]:
753
+ return [self.definition]
754
+
755
+
756
+ @dataclass(frozen=True)
757
+ class ElementNode(ASTNode):
758
+ """
759
+ Element of an offense (actus reus or mens rea).
760
+
761
+ element_type: "actus_reus", "mens_rea", or "circumstance"
762
+ """
763
+
764
+ element_type: str
765
+ name: str
766
+ description: ASTNode # Usually StringLit or match expression
767
+
768
+ def accept(self, visitor: "Visitor"):
769
+ return visitor.visit_element(self)
770
+
771
+ def children(self) -> List[ASTNode]:
772
+ return [self.description]
773
+
774
+
775
+ @dataclass(frozen=True)
776
+ class PenaltyNode(ASTNode):
777
+ """
778
+ Penalty specification for a statute.
779
+
780
+ Can specify imprisonment, fine, or both, with optional ranges.
781
+ """
782
+
783
+ imprisonment_min: Optional[DurationNode] = None
784
+ imprisonment_max: Optional[DurationNode] = None
785
+ fine_min: Optional[MoneyNode] = None
786
+ fine_max: Optional[MoneyNode] = None
787
+ supplementary: Optional[StringLit] = None
788
+
789
+ def accept(self, visitor: "Visitor"):
790
+ return visitor.visit_penalty(self)
791
+
792
+ def children(self) -> List[ASTNode]:
793
+ result = []
794
+ if self.imprisonment_min:
795
+ result.append(self.imprisonment_min)
796
+ if self.imprisonment_max:
797
+ result.append(self.imprisonment_max)
798
+ if self.fine_min:
799
+ result.append(self.fine_min)
800
+ if self.fine_max:
801
+ result.append(self.fine_max)
802
+ if self.supplementary:
803
+ result.append(self.supplementary)
804
+ return result
805
+
806
+
807
+ @dataclass(frozen=True)
808
+ class IllustrationNode(ASTNode):
809
+ """Illustration example within a statute."""
810
+
811
+ label: Optional[str]
812
+ description: StringLit
813
+
814
+ def accept(self, visitor: "Visitor"):
815
+ return visitor.visit_illustration(self)
816
+
817
+ def children(self) -> List[ASTNode]:
818
+ return [self.description]
819
+
820
+
821
+ @dataclass(frozen=True)
822
+ class StatuteNode(ASTNode):
823
+ """
824
+ Statute block representing a legal provision.
825
+
826
+ Contains section number, title, definitions, elements, penalties,
827
+ and illustrations.
828
+ """
829
+
830
+ section_number: str
831
+ title: Optional[StringLit]
832
+ definitions: Tuple[DefinitionEntry, ...]
833
+ elements: Tuple[ElementNode, ...]
834
+ penalty: Optional[PenaltyNode]
835
+ illustrations: Tuple[IllustrationNode, ...]
836
+
837
+ def accept(self, visitor: "Visitor"):
838
+ return visitor.visit_statute(self)
839
+
840
+ def children(self) -> List[ASTNode]:
841
+ result: List[ASTNode] = []
842
+ if self.title:
843
+ result.append(self.title)
844
+ result.extend(self.definitions)
845
+ result.extend(self.elements)
846
+ if self.penalty:
847
+ result.append(self.penalty)
848
+ result.extend(self.illustrations)
849
+ return result
850
+
851
+
852
+ # =============================================================================
853
+ # Import Node
854
+ # =============================================================================
855
+
856
+
857
+ @dataclass(frozen=True)
858
+ class ImportNode(ASTNode):
859
+ """
860
+ Import statement for referencing other .yh files.
861
+
862
+ imported_names: List of names to import, or ["*"] for wildcard
863
+ """
864
+
865
+ path: str
866
+ imported_names: Tuple[str, ...] # Empty tuple means import whole module
867
+
868
+ def accept(self, visitor: "Visitor"):
869
+ return visitor.visit_import(self)
870
+
871
+ @property
872
+ def is_wildcard(self) -> bool:
873
+ return "*" in self.imported_names
874
+
875
+
876
+ # =============================================================================
877
+ # Module Node (Root)
878
+ # =============================================================================
879
+
880
+
881
+ @dataclass(frozen=True)
882
+ class ModuleNode(ASTNode):
883
+ """
884
+ Root AST node representing a complete Yuho module (.yh file).
885
+ """
886
+
887
+ imports: Tuple[ImportNode, ...]
888
+ type_defs: Tuple[StructDefNode, ...]
889
+ function_defs: Tuple[FunctionDefNode, ...]
890
+ statutes: Tuple[StatuteNode, ...]
891
+ variables: Tuple[VariableDecl, ...]
892
+ references: Tuple["ReferencingStmt", ...] = ()
893
+ assertions: Tuple["AssertStmt", ...] = ()
894
+
895
+ def accept(self, visitor: "Visitor"):
896
+ return visitor.visit_module(self)
897
+
898
+ def children(self) -> List[ASTNode]:
899
+ result: List[ASTNode] = []
900
+ result.extend(self.imports)
901
+ result.extend(self.references)
902
+ result.extend(self.type_defs)
903
+ result.extend(self.function_defs)
904
+ result.extend(self.statutes)
905
+ result.extend(self.variables)
906
+ result.extend(self.assertions)
907
+ return result