coretrace-python-analyzer 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.
Files changed (126) hide show
  1. coretrace_python/__init__.py +4 -0
  2. coretrace_python/__main__.py +4 -0
  3. coretrace_python/abstract/__init__.py +45 -0
  4. coretrace_python/abstract/constants.py +226 -0
  5. coretrace_python/abstract/heap.py +252 -0
  6. coretrace_python/abstract/ranges.py +285 -0
  7. coretrace_python/abstract/values.py +56 -0
  8. coretrace_python/analysis/__init__.py +31 -0
  9. coretrace_python/analysis/manager.py +165 -0
  10. coretrace_python/analysis/provider.py +73 -0
  11. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
  12. coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
  13. coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
  14. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
  15. coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
  16. coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
  17. coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
  18. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
  19. coretrace_python/bundled/models/cli/cli_models.py +35 -0
  20. coretrace_python/bundled/models/cli/plugin.toml +9 -0
  21. coretrace_python/bundled/models/credentials/credential_models.py +43 -0
  22. coretrace_python/bundled/models/credentials/plugin.toml +9 -0
  23. coretrace_python/bundled/models/django/django_models.py +123 -0
  24. coretrace_python/bundled/models/django/plugin.toml +9 -0
  25. coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
  26. coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
  27. coretrace_python/bundled/models/flask/flask_models.py +53 -0
  28. coretrace_python/bundled/models/flask/plugin.toml +9 -0
  29. coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
  30. coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
  31. coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
  32. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
  33. coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
  34. coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
  35. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
  36. coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
  37. coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
  38. coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
  39. coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
  40. coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
  41. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
  42. coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
  43. coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
  44. coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
  45. coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
  46. coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
  47. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
  48. coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
  49. coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
  50. coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
  51. coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
  52. coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
  53. coretrace_python/bundled/security/xss/plugin.toml +9 -0
  54. coretrace_python/bundled/security/xss/xss.py +17 -0
  55. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
  56. coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
  57. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
  58. coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
  59. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
  60. coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
  61. coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
  62. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
  63. coretrace_python/cache.py +310 -0
  64. coretrace_python/cfg/__init__.py +46 -0
  65. coretrace_python/cfg/builder.py +589 -0
  66. coretrace_python/cfg/dominance.py +183 -0
  67. coretrace_python/cfg/model.py +166 -0
  68. coretrace_python/cli.py +216 -0
  69. coretrace_python/dataflow/__init__.py +29 -0
  70. coretrace_python/dataflow/lattice.py +78 -0
  71. coretrace_python/dataflow/solver.py +96 -0
  72. coretrace_python/dependency/__init__.py +44 -0
  73. coretrace_python/dependency/advisories.py +168 -0
  74. coretrace_python/dependency/correlation.py +87 -0
  75. coretrace_python/dependency/graph.py +274 -0
  76. coretrace_python/dependency/policy.py +65 -0
  77. coretrace_python/dependency/sbom.py +65 -0
  78. coretrace_python/engine.py +688 -0
  79. coretrace_python/findings/__init__.py +13 -0
  80. coretrace_python/findings/coverage.py +45 -0
  81. coretrace_python/findings/model.py +49 -0
  82. coretrace_python/findings/refutation.py +427 -0
  83. coretrace_python/frontend/__init__.py +18 -0
  84. coretrace_python/frontend/ast_adapter.py +447 -0
  85. coretrace_python/frontend/parser.py +23 -0
  86. coretrace_python/hir/__init__.py +5 -0
  87. coretrace_python/hir/nodes.py +584 -0
  88. coretrace_python/hir/visitors.py +36 -0
  89. coretrace_python/interprocedural/__init__.py +49 -0
  90. coretrace_python/interprocedural/callgraph.py +291 -0
  91. coretrace_python/interprocedural/modulegraph.py +218 -0
  92. coretrace_python/interprocedural/summaries.py +463 -0
  93. coretrace_python/ir/__init__.py +5 -0
  94. coretrace_python/ir/defuse.py +74 -0
  95. coretrace_python/ir/lowering.py +575 -0
  96. coretrace_python/ir/model.py +481 -0
  97. coretrace_python/ir/printer.py +201 -0
  98. coretrace_python/ir/ssa.py +277 -0
  99. coretrace_python/plugins/__init__.py +58 -0
  100. coretrace_python/plugins/api.py +153 -0
  101. coretrace_python/plugins/detectors.py +114 -0
  102. coretrace_python/plugins/loader.py +84 -0
  103. coretrace_python/plugins/manifest.py +112 -0
  104. coretrace_python/plugins/registry.py +34 -0
  105. coretrace_python/plugins/secrets.py +330 -0
  106. coretrace_python/reporters/__init__.py +22 -0
  107. coretrace_python/reporters/json_format.py +48 -0
  108. coretrace_python/reporters/report.py +23 -0
  109. coretrace_python/reporters/sarif.py +70 -0
  110. coretrace_python/reporters/text.py +24 -0
  111. coretrace_python/semantic/__init__.py +9 -0
  112. coretrace_python/semantic/identity.py +39 -0
  113. coretrace_python/semantic/imports.py +131 -0
  114. coretrace_python/semantic/scopes.py +473 -0
  115. coretrace_python/semantic/symbols.py +87 -0
  116. coretrace_python/source/__init__.py +13 -0
  117. coretrace_python/source/manager.py +81 -0
  118. coretrace_python/source/model.py +57 -0
  119. coretrace_python/taint/__init__.py +55 -0
  120. coretrace_python/taint/engine.py +800 -0
  121. coretrace_python/taint/models.py +317 -0
  122. coretrace_python/taint/routes.py +99 -0
  123. coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
  124. coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
  125. coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
  126. coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,481 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, fields, replace
5
+ from typing import Any, TypeAlias, TypeVar
6
+
7
+ from coretrace_python.cfg import BlockId
8
+ from coretrace_python.semantic.symbols import SymbolId
9
+ from coretrace_python.source import SourceSpan
10
+
11
+ # Bumped whenever the instruction set or its meaning changes; part of the cache key.
12
+ PYIR_SCHEMA_VERSION = 1
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Value:
17
+ id: int
18
+
19
+
20
+ class Operands:
21
+ """Generic operand access for instructions and terminators."""
22
+
23
+ def operands(self) -> tuple[Value, ...]:
24
+ found: list[Value] = []
25
+ for field_info in fields(self): # type: ignore[arg-type]
26
+ if field_info.name != "result":
27
+ found.extend(_values_in(getattr(self, field_info.name)))
28
+ return tuple(found)
29
+
30
+
31
+ def _values_in(value: object) -> list[Value]:
32
+ if isinstance(value, Value):
33
+ return [value]
34
+ if isinstance(value, tuple):
35
+ return [v for item in value for v in _values_in(item)]
36
+ return []
37
+
38
+
39
+ def _map_values(value: Any, mapping: Callable[[Value], Value]) -> Any:
40
+ if isinstance(value, Value):
41
+ return mapping(value)
42
+ if isinstance(value, tuple):
43
+ return tuple(_map_values(item, mapping) for item in value)
44
+ return value
45
+
46
+
47
+ N = TypeVar("N", bound=Operands)
48
+
49
+
50
+ def substitute(node: N, mapping: Callable[[Value], Value], *, include_result: bool = False) -> N:
51
+ """Copy ``node`` with every operand (and optionally its result) passed through ``mapping``."""
52
+
53
+ changes: dict[str, Any] = {}
54
+ for field_info in fields(node): # type: ignore[arg-type]
55
+ value = getattr(node, field_info.name)
56
+ if field_info.name == "result":
57
+ if include_result and isinstance(value, Value):
58
+ changes["result"] = mapping(value)
59
+ else:
60
+ changes[field_info.name] = _map_values(value, mapping)
61
+ return replace(node, **changes) # type: ignore[type-var]
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class Constant(Operands):
66
+ result: Value
67
+ location: SourceSpan
68
+ value: object
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class Global(Operands):
73
+ result: Value
74
+ location: SourceSpan
75
+ name: str
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Symbol(Operands):
80
+ result: Value
81
+ location: SourceSpan
82
+ symbol_id: SymbolId
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class BinaryOp(Operands):
87
+ result: Value
88
+ location: SourceSpan
89
+ operator: str
90
+ left: Value
91
+ right: Value
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class UnaryOp(Operands):
96
+ result: Value
97
+ location: SourceSpan
98
+ operator: str
99
+ operand: Value
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class Compare(Operands):
104
+ result: Value
105
+ location: SourceSpan
106
+ operator: str
107
+ left: Value
108
+ right: Value
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class Call(Operands):
113
+ """``starred`` are ``*iterable`` arguments whose positions are unknown; keywords
114
+ with a ``None`` name are ``**mapping`` arguments."""
115
+
116
+ result: Value
117
+ location: SourceSpan
118
+ callee: Value
119
+ arguments: tuple[Value, ...]
120
+ keywords: tuple[tuple[str | None, Value], ...] = ()
121
+ starred: tuple[Value, ...] = ()
122
+
123
+ def argument_values(self) -> tuple[Value, ...]:
124
+ return (*self.arguments, *self.starred, *(value for _, value in self.keywords))
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class BoolOp(Operands):
129
+ result: Value
130
+ location: SourceSpan
131
+ operator: str
132
+ values: tuple[Value, ...]
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class BuildList(Operands):
137
+ result: Value
138
+ location: SourceSpan
139
+ elements: tuple[Value, ...]
140
+ unpacked: tuple[Value, ...] = ()
141
+
142
+
143
+ @dataclass(frozen=True)
144
+ class BuildTuple(Operands):
145
+ result: Value
146
+ location: SourceSpan
147
+ elements: tuple[Value, ...]
148
+ unpacked: tuple[Value, ...] = ()
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class BuildDict(Operands):
153
+ result: Value
154
+ location: SourceSpan
155
+ items: tuple[tuple[Value, Value], ...]
156
+ unpacked: tuple[Value, ...] = ()
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class BuildSet(Operands):
161
+ result: Value
162
+ location: SourceSpan
163
+ elements: tuple[Value, ...]
164
+ unpacked: tuple[Value, ...] = ()
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class MakeFunction(Operands):
169
+ """A function value: a lambda or a nested definition. ``captured`` are the values of
170
+ the variables the body captures, at the definition point, in the order the nested
171
+ function takes them as implicit parameters after its explicit ones."""
172
+
173
+ result: Value
174
+ location: SourceSpan
175
+ name: str
176
+ captured: tuple[Value, ...] = ()
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class MakeClass(Operands):
181
+ """A class defined inside a function, bound to its name; its methods are analysed
182
+ as nested functions."""
183
+
184
+ result: Value
185
+ location: SourceSpan
186
+ name: str
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class BuildString(Operands):
191
+ """An f-string: its constant and formatted parts, in order."""
192
+
193
+ result: Value
194
+ location: SourceSpan
195
+ parts: tuple[Value, ...]
196
+
197
+
198
+ @dataclass(frozen=True)
199
+ class BuildSlice(Operands):
200
+ result: Value
201
+ location: SourceSpan
202
+ lower: Value | None
203
+ upper: Value | None
204
+ step: Value | None
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class WithEnter(Operands):
209
+ result: Value
210
+ location: SourceSpan
211
+ context: Value
212
+
213
+
214
+ @dataclass(frozen=True)
215
+ class Catch(Operands):
216
+ """The exception bound by ``except type as name`` at the start of a handler."""
217
+
218
+ result: Value
219
+ location: SourceSpan
220
+ type: Value | None
221
+
222
+
223
+ @dataclass(frozen=True)
224
+ class Await(Operands):
225
+ result: Value
226
+ location: SourceSpan
227
+ value: Value
228
+
229
+
230
+ @dataclass(frozen=True)
231
+ class Yield(Operands):
232
+ result: Value
233
+ location: SourceSpan
234
+ value: Value | None
235
+
236
+
237
+ @dataclass(frozen=True)
238
+ class GetAttr(Operands):
239
+ result: Value
240
+ location: SourceSpan
241
+ object: Value
242
+ attribute: str
243
+
244
+
245
+ @dataclass(frozen=True)
246
+ class GetItem(Operands):
247
+ result: Value
248
+ location: SourceSpan
249
+ object: Value
250
+ key: Value
251
+
252
+
253
+ @dataclass(frozen=True)
254
+ class GetIter(Operands):
255
+ result: Value
256
+ location: SourceSpan
257
+ iterable: Value
258
+
259
+
260
+ @dataclass(frozen=True)
261
+ class LoadLocal(Operands):
262
+ result: Value
263
+ location: SourceSpan
264
+ name: str
265
+
266
+
267
+ @dataclass(frozen=True)
268
+ class Phi(Operands):
269
+ """Merge of the values of local ``name`` arriving from each predecessor (SSA only)."""
270
+
271
+ result: Value
272
+ location: SourceSpan
273
+ name: str
274
+ incoming: tuple[tuple[Value, BlockId], ...]
275
+
276
+
277
+ @dataclass(frozen=True)
278
+ class Undefined(Operands):
279
+ """The value of local ``name`` on a path where it was never assigned (SSA only)."""
280
+
281
+ result: Value
282
+ location: SourceSpan
283
+ name: str
284
+
285
+
286
+ @dataclass(frozen=True)
287
+ class StoreLocal(Operands):
288
+ result: None
289
+ location: SourceSpan
290
+ name: str
291
+ value: Value
292
+
293
+
294
+ @dataclass(frozen=True)
295
+ class SetAttr(Operands):
296
+ result: None
297
+ location: SourceSpan
298
+ object: Value
299
+ attribute: str
300
+ value: Value
301
+
302
+
303
+ @dataclass(frozen=True)
304
+ class SetItem(Operands):
305
+ result: None
306
+ location: SourceSpan
307
+ object: Value
308
+ key: Value
309
+ value: Value
310
+
311
+
312
+ @dataclass(frozen=True)
313
+ class WithExit(Operands):
314
+ result: None
315
+ location: SourceSpan
316
+ context: Value
317
+
318
+
319
+ @dataclass(frozen=True)
320
+ class Assert(Operands):
321
+ result: None
322
+ location: SourceSpan
323
+ test: Value
324
+ message: Value | None
325
+
326
+
327
+ @dataclass(frozen=True)
328
+ class DelItem(Operands):
329
+ result: None
330
+ location: SourceSpan
331
+ object: Value
332
+ key: Value
333
+
334
+
335
+ @dataclass(frozen=True)
336
+ class DelAttr(Operands):
337
+ result: None
338
+ location: SourceSpan
339
+ object: Value
340
+ attribute: str
341
+
342
+
343
+ @dataclass(frozen=True)
344
+ class SetGlobal(Operands):
345
+ """Assignment to a name declared ``global``."""
346
+
347
+ result: None
348
+ location: SourceSpan
349
+ name: str
350
+ value: Value
351
+
352
+
353
+ @dataclass(frozen=True)
354
+ class Import(Operands):
355
+ """An import executed where it stands: ``module`` as written (relative dots
356
+ included), the canonical ``symbol_id`` bound and the local ``name`` it binds."""
357
+
358
+ result: None
359
+ location: SourceSpan
360
+ module: str
361
+ symbol_id: SymbolId
362
+ name: str
363
+
364
+
365
+ ValueInstruction: TypeAlias = (
366
+ Constant
367
+ | Global
368
+ | Symbol
369
+ | BinaryOp
370
+ | UnaryOp
371
+ | Compare
372
+ | Call
373
+ | GetAttr
374
+ | GetItem
375
+ | GetIter
376
+ | LoadLocal
377
+ | Phi
378
+ | Undefined
379
+ | BoolOp
380
+ | BuildList
381
+ | BuildTuple
382
+ | BuildDict
383
+ | BuildSet
384
+ | BuildString
385
+ | BuildSlice
386
+ | MakeFunction
387
+ | MakeClass
388
+ | WithEnter
389
+ | Catch
390
+ | Await
391
+ | Yield
392
+ )
393
+ EffectInstruction: TypeAlias = (
394
+ StoreLocal | SetAttr | SetItem | WithExit | Assert | Import | SetGlobal | DelItem | DelAttr
395
+ )
396
+ Instruction: TypeAlias = ValueInstruction | EffectInstruction
397
+
398
+
399
+ # --------------------------------------------------------------------------- terminators
400
+
401
+
402
+ @dataclass(frozen=True)
403
+ class Return(Operands):
404
+ location: SourceSpan
405
+ value: Value | None
406
+
407
+
408
+ @dataclass(frozen=True)
409
+ class Branch(Operands):
410
+ location: SourceSpan
411
+ condition: Value
412
+ then_block: BlockId
413
+ else_block: BlockId
414
+
415
+
416
+ @dataclass(frozen=True)
417
+ class Jump(Operands):
418
+ location: SourceSpan
419
+ target: BlockId
420
+
421
+
422
+ @dataclass(frozen=True)
423
+ class Raise(Operands):
424
+ location: SourceSpan
425
+ exception: Value | None
426
+ cause: Value | None = None
427
+
428
+
429
+ @dataclass(frozen=True)
430
+ class ForNext(Operands):
431
+ """Store the next item of ``iterator`` into local ``target`` and enter ``body``, or ``exit``."""
432
+
433
+ location: SourceSpan
434
+ iterator: Value
435
+ target: str
436
+ body: BlockId
437
+ exit: BlockId
438
+ result: Value | None = None
439
+
440
+
441
+ Terminator: TypeAlias = Return | Branch | Jump | Raise | ForNext
442
+
443
+
444
+ @dataclass(frozen=True)
445
+ class BasicBlock:
446
+ id: BlockId
447
+ instructions: tuple[Instruction, ...]
448
+ terminator: Terminator
449
+ exception_targets: tuple[BlockId, ...] = ()
450
+
451
+
452
+ def successors(block: BasicBlock) -> tuple[BlockId, ...]:
453
+ """Terminator targets followed by the exception edges of ``block``."""
454
+
455
+ found = list(_terminator_targets(block.terminator))
456
+ found.extend(t for t in block.exception_targets if t not in found)
457
+ return tuple(found)
458
+
459
+
460
+ def _terminator_targets(terminator: Terminator) -> tuple[BlockId, ...]:
461
+ if isinstance(terminator, Branch):
462
+ return (terminator.then_block, terminator.else_block)
463
+ if isinstance(terminator, Jump):
464
+ return (terminator.target,)
465
+ if isinstance(terminator, ForNext):
466
+ return (terminator.body, terminator.exit)
467
+ return ()
468
+
469
+
470
+ @dataclass(frozen=True)
471
+ class FunctionIR:
472
+ name: str
473
+ parameters: tuple[Value, ...]
474
+ entry: BlockId
475
+ blocks: tuple[BasicBlock, ...]
476
+ location: SourceSpan
477
+
478
+
479
+ @dataclass(frozen=True)
480
+ class ModuleIR:
481
+ functions: tuple[FunctionIR, ...]
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ from coretrace_python.ir.model import (
4
+ Assert,
5
+ Await,
6
+ BinaryOp,
7
+ BoolOp,
8
+ Branch,
9
+ BuildDict,
10
+ BuildList,
11
+ BuildSet,
12
+ BuildSlice,
13
+ BuildString,
14
+ BuildTuple,
15
+ Call,
16
+ Catch,
17
+ Compare,
18
+ Constant,
19
+ DelAttr,
20
+ DelItem,
21
+ ForNext,
22
+ GetAttr,
23
+ GetItem,
24
+ GetIter,
25
+ Global,
26
+ Import,
27
+ Instruction,
28
+ Jump,
29
+ LoadLocal,
30
+ MakeClass,
31
+ MakeFunction,
32
+ ModuleIR,
33
+ Phi,
34
+ Raise,
35
+ Return,
36
+ SetAttr,
37
+ SetGlobal,
38
+ SetItem,
39
+ StoreLocal,
40
+ Symbol,
41
+ Terminator,
42
+ UnaryOp,
43
+ Undefined,
44
+ Value,
45
+ WithEnter,
46
+ WithExit,
47
+ Yield,
48
+ )
49
+
50
+
51
+ def _value(value: Value) -> str:
52
+ return f"%{value.id}"
53
+
54
+
55
+ def _instruction(instruction: Instruction) -> str:
56
+ if isinstance(instruction, Constant):
57
+ return f"{_value(instruction.result)} = const {instruction.value!r}"
58
+ if isinstance(instruction, Global):
59
+ return f"{_value(instruction.result)} = global {instruction.name!r}"
60
+ if isinstance(instruction, Symbol):
61
+ return f"{_value(instruction.result)} = symbol @{instruction.symbol_id}"
62
+ if isinstance(instruction, BinaryOp):
63
+ return (
64
+ f"{_value(instruction.result)} = binary.{instruction.operator} "
65
+ f"{_value(instruction.left)}, {_value(instruction.right)}"
66
+ )
67
+ if isinstance(instruction, UnaryOp):
68
+ return (
69
+ f"{_value(instruction.result)} = unary.{instruction.operator} "
70
+ f"{_value(instruction.operand)}"
71
+ )
72
+ if isinstance(instruction, Compare):
73
+ return (
74
+ f"{_value(instruction.result)} = compare.{instruction.operator} "
75
+ f"{_value(instruction.left)}, {_value(instruction.right)}"
76
+ )
77
+ if isinstance(instruction, Call):
78
+ parts = [_value(argument) for argument in instruction.arguments]
79
+ parts.extend(f"*{_value(value)}" for value in instruction.starred)
80
+ for name, value in instruction.keywords:
81
+ parts.append(f"{name}={_value(value)}" if name is not None else f"**{_value(value)}")
82
+ return f"{_value(instruction.result)} = call {_value(instruction.callee)}({', '.join(parts)})"
83
+ if isinstance(instruction, BoolOp):
84
+ values = ", ".join(_value(v) for v in instruction.values)
85
+ return f"{_value(instruction.result)} = bool_op.{instruction.operator} {values}"
86
+ if isinstance(instruction, BuildList | BuildTuple):
87
+ kind = "build_list" if isinstance(instruction, BuildList) else "build_tuple"
88
+ parts = [_value(v) for v in instruction.elements] + [f"*{_value(v)}" for v in instruction.unpacked]
89
+ return f"{_value(instruction.result)} = {kind} {', '.join(parts)}".rstrip()
90
+ if isinstance(instruction, BuildDict):
91
+ parts = [f"{_value(k)}: {_value(v)}" for k, v in instruction.items]
92
+ parts.extend(f"**{_value(v)}" for v in instruction.unpacked)
93
+ return f"{_value(instruction.result)} = build_dict {', '.join(parts)}".rstrip()
94
+ if isinstance(instruction, BuildSet):
95
+ parts = [_value(v) for v in instruction.elements] + [f"*{_value(v)}" for v in instruction.unpacked]
96
+ return f"{_value(instruction.result)} = build_set {', '.join(parts)}".rstrip()
97
+ if isinstance(instruction, MakeFunction):
98
+ text = f'{_value(instruction.result)} = make_function "{instruction.name}"'
99
+ if instruction.captured:
100
+ text += f" [{', '.join(_value(v) for v in instruction.captured)}]"
101
+ return text
102
+ if isinstance(instruction, DelItem):
103
+ return f"del_item {_value(instruction.object)}, {_value(instruction.key)}"
104
+ if isinstance(instruction, DelAttr):
105
+ return f'del_attr {_value(instruction.object)}, "{instruction.attribute}"'
106
+ if isinstance(instruction, MakeClass):
107
+ return f'{_value(instruction.result)} = make_class "{instruction.name}"'
108
+ if isinstance(instruction, SetGlobal):
109
+ return f'set_global "{instruction.name}", {_value(instruction.value)}'
110
+ if isinstance(instruction, BuildString):
111
+ return f"{_value(instruction.result)} = build_string {', '.join(_value(v) for v in instruction.parts)}".rstrip()
112
+ if isinstance(instruction, BuildSlice):
113
+ bounds = ", ".join("none" if b is None else _value(b) for b in (instruction.lower, instruction.upper, instruction.step))
114
+ return f"{_value(instruction.result)} = build_slice {bounds}"
115
+ if isinstance(instruction, WithEnter):
116
+ return f"{_value(instruction.result)} = with_enter {_value(instruction.context)}"
117
+ if isinstance(instruction, WithExit):
118
+ return f"with_exit {_value(instruction.context)}"
119
+ if isinstance(instruction, Catch):
120
+ suffix = "" if instruction.type is None else f" {_value(instruction.type)}"
121
+ return f"{_value(instruction.result)} = catch{suffix}"
122
+ if isinstance(instruction, Await):
123
+ return f"{_value(instruction.result)} = await {_value(instruction.value)}"
124
+ if isinstance(instruction, Yield):
125
+ suffix = "" if instruction.value is None else f" {_value(instruction.value)}"
126
+ return f"{_value(instruction.result)} = yield{suffix}"
127
+ if isinstance(instruction, SetAttr):
128
+ return f"set_attr {_value(instruction.object)}, {instruction.attribute!r}, {_value(instruction.value)}"
129
+ if isinstance(instruction, SetItem):
130
+ return (
131
+ f"set_item {_value(instruction.object)}, {_value(instruction.key)}, "
132
+ f"{_value(instruction.value)}"
133
+ )
134
+ if isinstance(instruction, Import):
135
+ return f'import "{instruction.module}" as "{instruction.name}" @{instruction.symbol_id}'
136
+ if isinstance(instruction, Assert):
137
+ message = "" if instruction.message is None else f", {_value(instruction.message)}"
138
+ return f"assert {_value(instruction.test)}{message}"
139
+ if isinstance(instruction, GetAttr):
140
+ return (
141
+ f"{_value(instruction.result)} = get_attr {_value(instruction.object)}, "
142
+ f"{instruction.attribute!r}"
143
+ )
144
+ if isinstance(instruction, GetItem):
145
+ return (
146
+ f"{_value(instruction.result)} = get_item {_value(instruction.object)}, "
147
+ f"{_value(instruction.key)}"
148
+ )
149
+ if isinstance(instruction, GetIter):
150
+ return f"{_value(instruction.result)} = get_iter {_value(instruction.iterable)}"
151
+ if isinstance(instruction, LoadLocal):
152
+ return f'{_value(instruction.result)} = load_local "{instruction.name}"'
153
+ if isinstance(instruction, StoreLocal):
154
+ return f'store_local "{instruction.name}", {_value(instruction.value)}'
155
+ if isinstance(instruction, Phi):
156
+ incoming = ", ".join(f"[{_value(v)}, {b}]" for v, b in instruction.incoming)
157
+ return f'{_value(instruction.result)} = phi "{instruction.name}", {incoming}'
158
+ if isinstance(instruction, Undefined):
159
+ return f'{_value(instruction.result)} = undefined "{instruction.name}"'
160
+ raise TypeError(f"unknown instruction: {instruction!r}")
161
+
162
+
163
+ def _terminator(terminator: Terminator) -> str:
164
+ if isinstance(terminator, Return):
165
+ return "return" if terminator.value is None else f"return {_value(terminator.value)}"
166
+ if isinstance(terminator, Branch):
167
+ return (
168
+ f"branch {_value(terminator.condition)}, "
169
+ f"{terminator.then_block}, {terminator.else_block}"
170
+ )
171
+ if isinstance(terminator, Jump):
172
+ return f"jump {terminator.target}"
173
+ if isinstance(terminator, Raise):
174
+ if terminator.exception is None:
175
+ return "raise"
176
+ text = f"raise {_value(terminator.exception)}"
177
+ return text if terminator.cause is None else f"{text} from {_value(terminator.cause)}"
178
+ if isinstance(terminator, ForNext):
179
+ prefix = "" if terminator.result is None else f"{_value(terminator.result)} = "
180
+ return (
181
+ f'{prefix}for_next {_value(terminator.iterator)} -> "{terminator.target}", '
182
+ f"{terminator.body}, {terminator.exit}"
183
+ )
184
+ raise TypeError(f"unknown terminator: {terminator!r}")
185
+
186
+
187
+ def format_module(module: ModuleIR) -> str:
188
+ functions: list[str] = []
189
+ for function in module.functions:
190
+ parameters = ", ".join(_value(parameter) for parameter in function.parameters)
191
+ lines = [f"func @{function.name}({parameters}) {{"]
192
+ for block in function.blocks:
193
+ header = str(block.id)
194
+ if block.exception_targets:
195
+ header += f" [except: {', '.join(str(t) for t in block.exception_targets)}]"
196
+ lines.append(f"{header}:")
197
+ lines.extend(f" {_instruction(instruction)}" for instruction in block.instructions)
198
+ lines.append(f" {_terminator(block.terminator)}")
199
+ lines.append("}")
200
+ functions.append("\n".join(lines))
201
+ return "\n\n".join(functions)