dalang 0.1.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.
- dalang/__init__.py +6 -0
- dalang/__main__.py +3 -0
- dalang/ast_nodes.py +565 -0
- dalang/check.py +223 -0
- dalang/checker.py +956 -0
- dalang/cli.py +150 -0
- dalang/codegen.py +669 -0
- dalang/errors.py +48 -0
- dalang/lexer.py +198 -0
- dalang/parser.py +1019 -0
- dalang/run.py +78 -0
- dalang/runtime.py +554 -0
- dalang/schema.py +109 -0
- dalang/tokens.py +100 -0
- dalang-0.1.0.dist-info/METADATA +128 -0
- dalang-0.1.0.dist-info/RECORD +20 -0
- dalang-0.1.0.dist-info/WHEEL +5 -0
- dalang-0.1.0.dist-info/entry_points.txt +2 -0
- dalang-0.1.0.dist-info/licenses/LICENSE +202 -0
- dalang-0.1.0.dist-info/top_level.txt +1 -0
dalang/__init__.py
ADDED
dalang/__main__.py
ADDED
dalang/ast_nodes.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""Node AST Dalang (Tata Bahasa v0.1).
|
|
2
|
+
|
|
3
|
+
Setiap node menyimpan posisi awal (line, col) di sumber, untuk pesan error dan source map.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Node:
|
|
14
|
+
line: int = field(default=0, compare=False, repr=False, kw_only=True)
|
|
15
|
+
col: int = field(default=0, compare=False, repr=False, kw_only=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =====================================================================
|
|
19
|
+
# Ekspresi
|
|
20
|
+
# =====================================================================
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Number(Node):
|
|
24
|
+
value: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class String(Node):
|
|
29
|
+
value: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Bool(Node):
|
|
34
|
+
value: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Missing(Node):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Name(Node):
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ListLit(Node):
|
|
49
|
+
items: list["Expr"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Member(Node):
|
|
54
|
+
obj: "Expr"
|
|
55
|
+
name: str
|
|
56
|
+
quoted: bool = False # sales."Nilai Penjualan"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Arg(Node):
|
|
61
|
+
name: Optional[str] # argumen bernama: nama: nilai
|
|
62
|
+
value: "Expr"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Call(Node):
|
|
67
|
+
func: "Expr"
|
|
68
|
+
args: list[Arg]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Unary(Node):
|
|
73
|
+
op: str # "-" atau "NOT"
|
|
74
|
+
operand: "Expr"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Binary(Node):
|
|
79
|
+
op: str # + - * / ^ & MOD AND OR = <> < > <= >= IN
|
|
80
|
+
left: "Expr"
|
|
81
|
+
right: "Expr"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class IsMissing(Node):
|
|
86
|
+
operand: "Expr"
|
|
87
|
+
negated: bool = False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---- pengubah pendek (aturan A2) ----
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class Previous(Node):
|
|
94
|
+
expr: "Expr"
|
|
95
|
+
unit: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class LastN(Node):
|
|
100
|
+
expr: "Expr"
|
|
101
|
+
count: str
|
|
102
|
+
unit: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class ToDate(Node):
|
|
107
|
+
expr: "Expr"
|
|
108
|
+
unit: str
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class IgnoreFilter(Node):
|
|
113
|
+
expr: "Expr"
|
|
114
|
+
target: "Expr"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class ShareOf(Node):
|
|
119
|
+
expr: "Expr"
|
|
120
|
+
target: "Expr"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class ForEachRow(Node):
|
|
125
|
+
expr: "Expr"
|
|
126
|
+
table: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---- klausa panjang / pipeline (aturan A3) ----
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class Coord(Node):
|
|
133
|
+
target: "Expr" # name_ref atau satuan waktu (Name)
|
|
134
|
+
value: "Expr"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class Where(Node):
|
|
139
|
+
expr: Optional["Expr"] # None bila klausa milik perintah
|
|
140
|
+
cond: "Expr"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class At(Node):
|
|
145
|
+
expr: Optional["Expr"]
|
|
146
|
+
coords: list[Coord]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class Select(Node):
|
|
151
|
+
expr: Optional["Expr"]
|
|
152
|
+
columns: list[str]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class SortBy(Node):
|
|
157
|
+
expr: Optional["Expr"]
|
|
158
|
+
key: "Expr"
|
|
159
|
+
descending: bool = False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---- bentuk khusus ----
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class Option(Node):
|
|
166
|
+
name: str
|
|
167
|
+
value: "Expr"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class Load(Node):
|
|
172
|
+
format: str
|
|
173
|
+
path: str
|
|
174
|
+
options: list[Option]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Top(Node):
|
|
179
|
+
count: str
|
|
180
|
+
table: str
|
|
181
|
+
key: "Expr"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Rank(Node):
|
|
186
|
+
table: str
|
|
187
|
+
key: "Expr"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class Join(Node):
|
|
192
|
+
left: str
|
|
193
|
+
right: str
|
|
194
|
+
on: "Expr"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class Forecast(Node):
|
|
199
|
+
measure: str
|
|
200
|
+
by_unit: str
|
|
201
|
+
count: str
|
|
202
|
+
unit: str
|
|
203
|
+
engine: Optional[str]
|
|
204
|
+
options: list[Option]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class Regress(Node):
|
|
209
|
+
target: "Expr"
|
|
210
|
+
predictors: list["Expr"]
|
|
211
|
+
engine: Optional[str]
|
|
212
|
+
options: list[Option]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
Expr = Union[
|
|
216
|
+
Number, String, Bool, Missing, Name, ListLit, Member, Call, Unary, Binary, IsMissing,
|
|
217
|
+
Previous, LastN, ToDate, IgnoreFilter, ShareOf, ForEachRow, Where, At, Select, SortBy,
|
|
218
|
+
Load, Top, Rank, Join, Forecast, Regress,
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# =====================================================================
|
|
223
|
+
# Pernyataan
|
|
224
|
+
# =====================================================================
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class Target(Node):
|
|
228
|
+
name: str
|
|
229
|
+
column: Optional[str] = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class Use(Node):
|
|
234
|
+
language: Optional[str] # "PYTHON", "R", atau None untuk modul .dal
|
|
235
|
+
path: str
|
|
236
|
+
alias: str
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class Parameter(Node):
|
|
241
|
+
name: str
|
|
242
|
+
type: Optional[str]
|
|
243
|
+
default: Expr
|
|
244
|
+
options: list[Option]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass
|
|
248
|
+
class Let(Node):
|
|
249
|
+
target: Target
|
|
250
|
+
type: Optional[str]
|
|
251
|
+
value: Expr
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class Set(Node):
|
|
256
|
+
target: Target
|
|
257
|
+
value: Expr
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass
|
|
261
|
+
class IfBranch(Node):
|
|
262
|
+
cond: Expr
|
|
263
|
+
body: list["Stmt"]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass
|
|
267
|
+
class If(Node):
|
|
268
|
+
branches: list[IfBranch]
|
|
269
|
+
else_body: Optional[list["Stmt"]]
|
|
270
|
+
inline: bool = False
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class For(Node):
|
|
275
|
+
var: str
|
|
276
|
+
start: Expr
|
|
277
|
+
end: Expr
|
|
278
|
+
step: Optional[Expr]
|
|
279
|
+
body: list["Stmt"]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class ForEach(Node):
|
|
284
|
+
var: str
|
|
285
|
+
iterable: Expr
|
|
286
|
+
body: list["Stmt"]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
class While(Node):
|
|
291
|
+
cond: Expr
|
|
292
|
+
body: list["Stmt"]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataclass
|
|
296
|
+
class Exit(Node):
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class Continue(Node):
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass
|
|
306
|
+
class Param(Node):
|
|
307
|
+
name: str
|
|
308
|
+
type: Optional[str]
|
|
309
|
+
default: Optional[Expr]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class Function(Node):
|
|
314
|
+
name: str
|
|
315
|
+
params: list[Param]
|
|
316
|
+
body: list["Stmt"]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class Return(Node):
|
|
321
|
+
value: Expr
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class Try(Node):
|
|
326
|
+
body: list["Stmt"]
|
|
327
|
+
error_name: str
|
|
328
|
+
handler: list["Stmt"]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass
|
|
332
|
+
class Fail(Node):
|
|
333
|
+
message: Expr
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass
|
|
337
|
+
class Tables(Node):
|
|
338
|
+
names: list[str]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass
|
|
342
|
+
class Relate(Node):
|
|
343
|
+
many: Expr
|
|
344
|
+
one: Expr
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass
|
|
348
|
+
class Calendar(Node):
|
|
349
|
+
column: Expr
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass
|
|
353
|
+
class Measure(Node):
|
|
354
|
+
name: str
|
|
355
|
+
expr: Expr
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass
|
|
359
|
+
class Model(Node):
|
|
360
|
+
name: str
|
|
361
|
+
items: list[Union[Tables, Relate, Calendar, Measure]]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@dataclass
|
|
365
|
+
class Test(Node):
|
|
366
|
+
name: str
|
|
367
|
+
expects: list[Expr]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@dataclass
|
|
371
|
+
class Show(Node):
|
|
372
|
+
values: list[Expr]
|
|
373
|
+
by: list[Expr]
|
|
374
|
+
clauses: list[Union[Where, At, Select, SortBy]]
|
|
375
|
+
options: list[Option]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dataclass
|
|
379
|
+
class Plot(Node):
|
|
380
|
+
chart: Optional[str]
|
|
381
|
+
values: list[Expr]
|
|
382
|
+
by: list[Expr]
|
|
383
|
+
clauses: list[Union[Where, At, Select, SortBy]]
|
|
384
|
+
options: list[Option]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass
|
|
388
|
+
class Save(Node):
|
|
389
|
+
value: Expr
|
|
390
|
+
format: str
|
|
391
|
+
path: str
|
|
392
|
+
options: list[Option]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@dataclass
|
|
396
|
+
class Explain(Node):
|
|
397
|
+
expr: Expr
|
|
398
|
+
coords: list[Coord]
|
|
399
|
+
options: list[Option]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass
|
|
403
|
+
class Export(Node):
|
|
404
|
+
values: list[Expr]
|
|
405
|
+
by: list[Expr]
|
|
406
|
+
engine: str
|
|
407
|
+
options: list[Option]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass
|
|
411
|
+
class Ask(Node):
|
|
412
|
+
prompt: str
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@dataclass
|
|
416
|
+
class Append(Node):
|
|
417
|
+
value: Expr
|
|
418
|
+
target: str
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@dataclass
|
|
422
|
+
class Rename(Node):
|
|
423
|
+
column: Expr
|
|
424
|
+
new_name: str
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@dataclass
|
|
428
|
+
class Remove(Node):
|
|
429
|
+
column: Expr
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@dataclass
|
|
433
|
+
class Fill(Node):
|
|
434
|
+
column: Expr
|
|
435
|
+
value: Expr
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
class ExprStmt(Node):
|
|
440
|
+
expr: Expr
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
Stmt = Union[
|
|
444
|
+
Use, Parameter, Let, Set, If, For, ForEach, While, Exit, Continue, Function, Return,
|
|
445
|
+
Try, Fail, Model, Test, Show, Plot, Save, Explain, Export, Ask, Append, Rename, Remove,
|
|
446
|
+
Fill, ExprStmt,
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@dataclass
|
|
451
|
+
class Program(Node):
|
|
452
|
+
body: list[Stmt]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# =====================================================================
|
|
456
|
+
# Penulisan ulang ekspresi dengan kurung lengkap (untuk tes dan `dal parse`)
|
|
457
|
+
# =====================================================================
|
|
458
|
+
|
|
459
|
+
def fmt(e: object) -> str:
|
|
460
|
+
"""Tulis ekspresi dengan kurung lengkap, sehingga cara baca parser terlihat."""
|
|
461
|
+
if isinstance(e, Number):
|
|
462
|
+
return e.value
|
|
463
|
+
if isinstance(e, String):
|
|
464
|
+
return '"' + e.value.replace('"', '""') + '"'
|
|
465
|
+
if isinstance(e, Bool):
|
|
466
|
+
return "TRUE" if e.value else "FALSE"
|
|
467
|
+
if isinstance(e, Missing):
|
|
468
|
+
return "MISSING"
|
|
469
|
+
if isinstance(e, Name):
|
|
470
|
+
return e.name
|
|
471
|
+
if isinstance(e, ListLit):
|
|
472
|
+
return "[" + ", ".join(fmt(i) for i in e.items) + "]"
|
|
473
|
+
if isinstance(e, Member):
|
|
474
|
+
name = f'"{e.name}"' if e.quoted else e.name
|
|
475
|
+
return f"{fmt(e.obj)}.{name}"
|
|
476
|
+
if isinstance(e, Call):
|
|
477
|
+
args = ", ".join((f"{a.name}: " if a.name else "") + fmt(a.value) for a in e.args)
|
|
478
|
+
return f"{fmt(e.func)}({args})"
|
|
479
|
+
if isinstance(e, Unary):
|
|
480
|
+
sep = " " if e.op == "NOT" else ""
|
|
481
|
+
return f"({e.op}{sep}{fmt(e.operand)})"
|
|
482
|
+
if isinstance(e, Binary):
|
|
483
|
+
return f"({fmt(e.left)} {e.op} {fmt(e.right)})"
|
|
484
|
+
if isinstance(e, IsMissing):
|
|
485
|
+
return f"({fmt(e.operand)} IS {'NOT ' if e.negated else ''}MISSING)"
|
|
486
|
+
if isinstance(e, Previous):
|
|
487
|
+
return f"({fmt(e.expr)} PREVIOUS {e.unit})"
|
|
488
|
+
if isinstance(e, LastN):
|
|
489
|
+
return f"({fmt(e.expr)} LAST {e.count} {e.unit})"
|
|
490
|
+
if isinstance(e, ToDate):
|
|
491
|
+
return f"({fmt(e.expr)} {e.unit} TO DATE)"
|
|
492
|
+
if isinstance(e, IgnoreFilter):
|
|
493
|
+
return f"({fmt(e.expr)} IGNORE FILTER {fmt(e.target)})"
|
|
494
|
+
if isinstance(e, ShareOf):
|
|
495
|
+
return f"({fmt(e.expr)} AS SHARE OF {fmt(e.target)})"
|
|
496
|
+
if isinstance(e, ForEachRow):
|
|
497
|
+
return f"({fmt(e.expr)} FOR EACH ROW OF {e.table})"
|
|
498
|
+
if isinstance(e, Where):
|
|
499
|
+
return f"({fmt(e.expr)} WHERE {fmt(e.cond)})" if e.expr else f"WHERE {fmt(e.cond)}"
|
|
500
|
+
if isinstance(e, At):
|
|
501
|
+
coords = ", ".join(f"{fmt(c.target)} = {fmt(c.value)}" for c in e.coords)
|
|
502
|
+
return f"({fmt(e.expr)} AT {coords})" if e.expr else f"AT {coords}"
|
|
503
|
+
if isinstance(e, Select):
|
|
504
|
+
cols = ", ".join(e.columns)
|
|
505
|
+
return f"({fmt(e.expr)} SELECT {cols})" if e.expr else f"SELECT {cols}"
|
|
506
|
+
if isinstance(e, SortBy):
|
|
507
|
+
tail = f"SORT BY {fmt(e.key)}" + (" DESCENDING" if e.descending else "")
|
|
508
|
+
return f"({fmt(e.expr)} {tail})" if e.expr else tail
|
|
509
|
+
if isinstance(e, Load):
|
|
510
|
+
return f'LOAD {e.format} "{e.path}"' + _fmt_opts(e.options)
|
|
511
|
+
if isinstance(e, Top):
|
|
512
|
+
return f"(TOP {e.count} {e.table} BY {fmt(e.key)})"
|
|
513
|
+
if isinstance(e, Rank):
|
|
514
|
+
return f"(RANK {e.table} BY {fmt(e.key)})"
|
|
515
|
+
if isinstance(e, Join):
|
|
516
|
+
return f"(JOIN {e.left}, {e.right} ON {fmt(e.on)})"
|
|
517
|
+
if isinstance(e, Forecast):
|
|
518
|
+
engine = f" USING {e.engine}" if e.engine else ""
|
|
519
|
+
return (f"(FORECAST {e.measure} BY {e.by_unit} NEXT {e.count} {e.unit}{engine}"
|
|
520
|
+
+ _fmt_opts(e.options) + ")")
|
|
521
|
+
if isinstance(e, Regress):
|
|
522
|
+
engine = f" USING {e.engine}" if e.engine else ""
|
|
523
|
+
preds = ", ".join(fmt(p) for p in e.predictors)
|
|
524
|
+
return f"(REGRESS {fmt(e.target)} ON {preds}{engine}" + _fmt_opts(e.options) + ")"
|
|
525
|
+
if isinstance(e, Coord):
|
|
526
|
+
return f"{fmt(e.target)} = {fmt(e.value)}"
|
|
527
|
+
return f"<{type(e).__name__}>"
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _fmt_opts(options: list[Option]) -> str:
|
|
531
|
+
if not options:
|
|
532
|
+
return ""
|
|
533
|
+
return " WITH " + ", ".join(f"{o.name}: {fmt(o.value)}" for o in options)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def dump(node: object, indent: int = 0) -> str:
|
|
537
|
+
"""Tampilan pohon AST yang ringkas, satu node per baris."""
|
|
538
|
+
pad = " " * indent
|
|
539
|
+
if isinstance(node, list):
|
|
540
|
+
return "\n".join(dump(n, indent) for n in node)
|
|
541
|
+
if not is_dataclass(node):
|
|
542
|
+
return f"{pad}{node!r}"
|
|
543
|
+
expr_types = Expr.__args__ # type: ignore[attr-defined]
|
|
544
|
+
if isinstance(node, expr_types):
|
|
545
|
+
return f"{pad}{fmt(node)}"
|
|
546
|
+
lines = [f"{pad}{type(node).__name__} (baris {node.line})"]
|
|
547
|
+
for f in fields(node):
|
|
548
|
+
if f.name in ("line", "col"):
|
|
549
|
+
continue
|
|
550
|
+
value = getattr(node, f.name)
|
|
551
|
+
if value is None or value == [] or value is False:
|
|
552
|
+
continue
|
|
553
|
+
if isinstance(value, list):
|
|
554
|
+
lines.append(f"{pad} {f.name}:")
|
|
555
|
+
for item in value:
|
|
556
|
+
lines.append(dump(item, indent + 2))
|
|
557
|
+
elif is_dataclass(value):
|
|
558
|
+
if isinstance(value, expr_types):
|
|
559
|
+
lines.append(f"{pad} {f.name}: {fmt(value)}")
|
|
560
|
+
else:
|
|
561
|
+
lines.append(f"{pad} {f.name}:")
|
|
562
|
+
lines.append(dump(value, indent + 2))
|
|
563
|
+
else:
|
|
564
|
+
lines.append(f"{pad} {f.name}: {value!r}")
|
|
565
|
+
return "\n".join(lines)
|