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,575 @@
1
+ """Lower parser-independent PyHIR into analysis-oriented PyIR, one CFG block at a time."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import ClassVar, NoReturn
7
+
8
+ from coretrace_python import cfg as control_flow
9
+ from coretrace_python.analysis import (
10
+ Analysis,
11
+ AnalysisContext,
12
+ AnalysisManager,
13
+ AnyAnalysis,
14
+ FunctionAnalysis,
15
+ )
16
+ from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
17
+ from coretrace_python.hir import nodes
18
+ from coretrace_python.hir.visitors import Node, children
19
+ from coretrace_python.ir.model import (
20
+ Assert,
21
+ Await,
22
+ BasicBlock,
23
+ BinaryOp,
24
+ BoolOp,
25
+ Branch,
26
+ BuildDict,
27
+ BuildList,
28
+ BuildSet,
29
+ BuildSlice,
30
+ BuildString,
31
+ BuildTuple,
32
+ Call,
33
+ Catch,
34
+ Compare,
35
+ Constant,
36
+ DelAttr,
37
+ DelItem,
38
+ EffectInstruction,
39
+ ForNext,
40
+ FunctionIR,
41
+ GetAttr,
42
+ GetItem,
43
+ GetIter,
44
+ Global,
45
+ Import,
46
+ Instruction,
47
+ Jump,
48
+ LoadLocal,
49
+ MakeClass,
50
+ MakeFunction,
51
+ ModuleIR,
52
+ Raise,
53
+ Return,
54
+ SetAttr,
55
+ SetGlobal,
56
+ SetItem,
57
+ StoreLocal,
58
+ Symbol,
59
+ Terminator,
60
+ UnaryOp,
61
+ Value,
62
+ ValueInstruction,
63
+ WithEnter,
64
+ WithExit,
65
+ Yield,
66
+ )
67
+ from coretrace_python.semantic import SEMANTIC_ANALYSES
68
+ from coretrace_python.semantic.scopes import (
69
+ BindingKind,
70
+ Resolution,
71
+ ResolutionKind,
72
+ Scope,
73
+ ScopeAnalysis,
74
+ ScopeKind,
75
+ ScopeTable,
76
+ )
77
+ from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId, SymbolTable
78
+ from coretrace_python.source import SourceSpan
79
+
80
+
81
+ class LoweringError(Exception):
82
+ """Raised when PyHIR uses constructs outside the current PyIR subset."""
83
+
84
+
85
+ @dataclass
86
+ class _FunctionLowerer:
87
+ symbols: SymbolTable
88
+ scopes: ScopeTable
89
+ scope: Scope
90
+ cfg: CFG
91
+ next_value_id: int = 0
92
+ parameters: dict[str, Value] = field(default_factory=dict)
93
+ instructions: list[Instruction] = field(default_factory=list)
94
+ iterators: dict[BlockId, Value] = field(default_factory=dict)
95
+ contexts: dict[SourceSpan, Value] = field(default_factory=dict)
96
+
97
+ def fail(
98
+ self,
99
+ node: nodes.Statement | nodes.Expression,
100
+ message: str | None = None,
101
+ ) -> NoReturn:
102
+ detail = message or f"unsupported syntax: {type(node).__name__}"
103
+ raise LoweringError(f"{node.span.display()}: {detail}")
104
+
105
+ def new_value(self) -> Value:
106
+ value = Value(self.next_value_id)
107
+ self.next_value_id += 1
108
+ return value
109
+
110
+ def emit(self, instruction: ValueInstruction) -> Value:
111
+ self.instructions.append(instruction)
112
+ return instruction.result
113
+
114
+ def emit_effect(self, instruction: EffectInstruction) -> None:
115
+ self.instructions.append(instruction)
116
+
117
+ def resolve(self, name: str) -> Resolution:
118
+ if name in self.cfg.synthetic_locals:
119
+ return Resolution(ResolutionKind.LOCAL, self.scope.id)
120
+ return self.scopes.resolve(self.scope.id, name)
121
+
122
+ # ------------------------------------------------------------------ expressions
123
+
124
+ def imported_symbol(self, node: nodes.Expression) -> SymbolId | None:
125
+ if isinstance(node, nodes.Name):
126
+ return self.symbols.resolve(self.scope.id, node.identifier)
127
+ if isinstance(node, nodes.Attribute):
128
+ parent = self.imported_symbol(node.value)
129
+ return parent.attribute(node.name) if parent is not None else None
130
+ return None
131
+
132
+ def expression(self, node: nodes.Expression) -> Value:
133
+ imported_symbol = self.imported_symbol(node)
134
+ if imported_symbol is not None:
135
+ return self.emit(Symbol(self.new_value(), node.span, imported_symbol))
136
+ if isinstance(node, nodes.Name):
137
+ resolution = self.resolve(node.identifier)
138
+ if resolution.kind is ResolutionKind.FREE:
139
+ # A captured variable is an implicit parameter of the nested function; a
140
+ # ``nonlocal`` one lives in a local slot initialised from it.
141
+ if node.identifier in self.nonlocals:
142
+ return self.emit(LoadLocal(self.new_value(), node.span, node.identifier))
143
+ if node.identifier in self.parameters:
144
+ return self.parameters[node.identifier]
145
+ self.fail(node, "closures are not supported yet")
146
+ if resolution.kind is not ResolutionKind.LOCAL:
147
+ return self.emit(Global(self.new_value(), node.span, node.identifier))
148
+ if node.identifier in self.parameters:
149
+ return self.parameters[node.identifier]
150
+ return self.emit(LoadLocal(self.new_value(), node.span, node.identifier))
151
+ if isinstance(node, nodes.Constant):
152
+ return self.emit(Constant(self.new_value(), node.span, node.value))
153
+ if isinstance(node, nodes.BinaryOp):
154
+ left = self.expression(node.left)
155
+ right = self.expression(node.right)
156
+ return self.emit(BinaryOp(self.new_value(), node.span, node.operator, left, right))
157
+ if isinstance(node, nodes.UnaryOp):
158
+ operand = self.expression(node.operand)
159
+ return self.emit(UnaryOp(self.new_value(), node.span, node.operator, operand))
160
+ if isinstance(node, nodes.Compare):
161
+ left = self.expression(node.left)
162
+ right = self.expression(node.right)
163
+ return self.emit(Compare(self.new_value(), node.span, node.operator, left, right))
164
+ if isinstance(node, nodes.Call):
165
+ callee = self.expression(node.callee)
166
+ arguments, starred = self.spread(node.arguments)
167
+ keywords = tuple((k.name, self.expression(k.value)) for k in node.keywords)
168
+ return self.emit(Call(self.new_value(), node.span, callee, arguments, keywords, starred))
169
+ if isinstance(node, nodes.BoolOp):
170
+ values = tuple(self.expression(value) for value in node.values)
171
+ return self.emit(BoolOp(self.new_value(), node.span, node.operator, values))
172
+ if isinstance(node, nodes.List | nodes.Tuple):
173
+ elements, unpacked = self.spread(node.elements)
174
+ builder = BuildList if isinstance(node, nodes.List) else BuildTuple
175
+ return self.emit(builder(self.new_value(), node.span, elements, unpacked))
176
+ if isinstance(node, nodes.Dict):
177
+ items = tuple((self.expression(k), self.expression(v)) for k, v in node.items if k is not None)
178
+ unpacked = tuple(self.expression(v) for k, v in node.items if k is None)
179
+ return self.emit(BuildDict(self.new_value(), node.span, items, unpacked))
180
+ if isinstance(node, nodes.FormattedString):
181
+ parts = tuple(self.expression(part) for part in node.parts)
182
+ return self.emit(BuildString(self.new_value(), node.span, parts))
183
+ if isinstance(node, nodes.Slice):
184
+ bounds = [self.expression(b) if b is not None else None for b in (node.lower, node.upper, node.step)]
185
+ return self.emit(BuildSlice(self.new_value(), node.span, bounds[0], bounds[1], bounds[2]))
186
+ if isinstance(node, nodes.Starred):
187
+ self.fail(node, "a starred expression is only supported in calls, lists and tuples")
188
+ if isinstance(node, nodes.Set):
189
+ elements, unpacked = self.spread(node.elements)
190
+ return self.emit(BuildSet(self.new_value(), node.span, elements, unpacked))
191
+ if isinstance(node, nodes.Lambda):
192
+ synthesized = lambda_function(node)
193
+ captured = tuple(self.captured_value(name, node.span) for name in captured_names(synthesized, self.scopes))
194
+ return self.emit(MakeFunction(self.new_value(), node.span, synthesized.name, captured))
195
+ if isinstance(node, nodes.Conditional | nodes.Comprehension):
196
+ self.fail(node, "expression-level control flow must be laid out by the CFG builder")
197
+ if isinstance(node, nodes.Await):
198
+ return self.emit(Await(self.new_value(), node.span, self.expression(node.value)))
199
+ if isinstance(node, nodes.Yield):
200
+ value = self.expression(node.value) if node.value is not None else None
201
+ return self.emit(Yield(self.new_value(), node.span, value))
202
+ if isinstance(node, nodes.Attribute):
203
+ object_value = self.expression(node.value)
204
+ return self.emit(GetAttr(self.new_value(), node.span, object_value, node.name))
205
+ if isinstance(node, nodes.Subscript):
206
+ object_value = self.expression(node.value)
207
+ key = self.expression(node.key)
208
+ return self.emit(GetItem(self.new_value(), node.span, object_value, key))
209
+ self.fail(node)
210
+
211
+ def spread(self, elements: tuple[nodes.Expression, ...]) -> tuple[tuple[Value, ...], tuple[Value, ...]]:
212
+ """Plain elements and ``*iterable`` elements of a display or an argument list."""
213
+
214
+ plain: list[Value] = []
215
+ unpacked: list[Value] = []
216
+ for element in elements:
217
+ if isinstance(element, nodes.Starred):
218
+ unpacked.append(self.expression(element.value))
219
+ else:
220
+ plain.append(self.expression(element))
221
+ return tuple(plain), tuple(unpacked)
222
+
223
+ # ------------------------------------------------------------------ statements
224
+
225
+ def store(self, target: nodes.Target, value: Value) -> None:
226
+ if isinstance(target, nodes.Name):
227
+ resolution = self.resolve(target.identifier)
228
+ if resolution.kind is ResolutionKind.GLOBAL:
229
+ self.emit_effect(SetGlobal(None, target.span, target.identifier, value))
230
+ return
231
+ if resolution.kind is not ResolutionKind.LOCAL and target.identifier not in self.nonlocals:
232
+ self.fail(target, "assignment to a free variable that is not declared nonlocal")
233
+ self.emit_effect(StoreLocal(None, target.span, target.identifier, value))
234
+ elif isinstance(target, nodes.Attribute):
235
+ object_value = self.expression(target.value)
236
+ self.emit_effect(SetAttr(None, target.span, object_value, target.name, value))
237
+ elif isinstance(target, nodes.Subscript):
238
+ object_value = self.expression(target.value)
239
+ key = self.expression(target.key)
240
+ self.emit_effect(SetItem(None, target.span, object_value, key, value))
241
+ else:
242
+ for index, element in enumerate(target.elements):
243
+ position = self.emit(Constant(self.new_value(), element.span, index))
244
+ item = self.emit(GetItem(self.new_value(), element.span, value, position))
245
+ assert isinstance(element, nodes.Name | nodes.Attribute | nodes.Subscript | nodes.Tuple)
246
+ self.store(element, item)
247
+
248
+ def statement(self, node: nodes.Statement) -> None:
249
+ if isinstance(node, nodes.Assign):
250
+ self.store(node.target, self.expression(node.value))
251
+ return
252
+ if isinstance(node, nodes.Declaration):
253
+ return
254
+ if isinstance(node, nodes.Class):
255
+ # A local class is a value bound to its name; bases and decorators run here,
256
+ # its methods are analysed as nested functions.
257
+ for decorator in node.decorators:
258
+ self.expression(decorator)
259
+ for base in node.bases:
260
+ self.expression(base)
261
+ made_class = self.emit(MakeClass(self.new_value(), node.span, node.name))
262
+ self.store(nodes.Name(node.name, node.span), made_class)
263
+ return
264
+ if isinstance(node, nodes.Delete):
265
+ for target in node.targets:
266
+ if isinstance(target, nodes.Subscript):
267
+ obj, key = self.expression(target.value), self.expression(target.key)
268
+ self.emit_effect(DelItem(None, target.span, obj, key))
269
+ elif isinstance(target, nodes.Attribute):
270
+ self.emit_effect(DelAttr(None, target.span, self.expression(target.value), target.name))
271
+ # ``del name`` unbinds a local; the SSA form has no slot to clear.
272
+ return
273
+ if isinstance(node, nodes.Function):
274
+ # A nested definition is a value bound to its name; its body is its own
275
+ # scope and is not lowered here.
276
+ for decorator in node.decorators:
277
+ self.expression(decorator)
278
+ for parameter in node.parameters:
279
+ if parameter.default is not None:
280
+ self.expression(parameter.default)
281
+ captured = tuple(self.captured_value(name, node.span) for name in captured_names(node, self.scopes))
282
+ made = self.emit(MakeFunction(self.new_value(), node.span, node.name, captured))
283
+ self.store(nodes.Name(node.name, node.span), made)
284
+ return
285
+ if isinstance(node, nodes.AugAssign):
286
+ current = self.expression(node.target)
287
+ operand = self.expression(node.value)
288
+ result = self.emit(BinaryOp(self.new_value(), node.span, node.operator, current, operand))
289
+ self.store(node.target, result)
290
+ return
291
+ if isinstance(node, nodes.Assert):
292
+ test = self.expression(node.test)
293
+ message = self.expression(node.message) if node.message is not None else None
294
+ self.emit_effect(Assert(None, node.span, test, message))
295
+ return
296
+ if isinstance(node, nodes.EnterWith):
297
+ context = self.expression(node.item.context)
298
+ self.contexts[node.item.span] = context
299
+ entered = self.emit(WithEnter(self.new_value(), node.item.span, context))
300
+ if node.item.target is not None:
301
+ self.store(node.item.target, entered)
302
+ return
303
+ if isinstance(node, nodes.ExitWith):
304
+ self.emit_effect(WithExit(None, node.item.span, self.contexts[node.item.span]))
305
+ return
306
+ if isinstance(node, nodes.EnterHandler):
307
+ handler = node.handler
308
+ caught_type = self.expression(handler.type) if handler.type is not None else None
309
+ caught = self.emit(Catch(self.new_value(), handler.span, caught_type))
310
+ if handler.name is not None:
311
+ self.store(nodes.Name(handler.name, handler.span), caught)
312
+ return
313
+ if isinstance(node, nodes.ExpressionStatement):
314
+ self.expression(node.expression)
315
+ return
316
+ if isinstance(node, nodes.Import | nodes.ImportFrom):
317
+ # The binding is already applied by the semantic analyses; the instruction
318
+ # records that the import runs here (§39 rule 3).
319
+ module = "." * node.level + (node.module or "") if isinstance(node, nodes.ImportFrom) else ""
320
+ for alias in node.names:
321
+ bound = alias.as_name or alias.name.partition(".")[0]
322
+ symbol = self.symbols.resolve(self.scope.id, bound)
323
+ if symbol is None:
324
+ self.fail(node, f"unresolved import of {bound!r}")
325
+ written = alias.name if isinstance(node, nodes.Import) else module
326
+ self.emit_effect(Import(None, alias.span, written, symbol, bound))
327
+ return
328
+ if isinstance(node, nodes.Pass | nodes.Global | nodes.Nonlocal):
329
+ # Declarations are already applied by the semantic analyses.
330
+ return
331
+ self.fail(node)
332
+
333
+ # ------------------------------------------------------------------ blocks
334
+
335
+ def terminator(self, block: control_flow.BasicBlock) -> Terminator:
336
+ terminator = block.terminator
337
+ if isinstance(terminator, control_flow.Return):
338
+ value = self.expression(terminator.value) if terminator.value is not None else None
339
+ return Return(terminator.span, value)
340
+ if isinstance(terminator, control_flow.Branch):
341
+ condition = self.expression(terminator.condition)
342
+ return Branch(terminator.span, condition, terminator.then_block, terminator.else_block)
343
+ if isinstance(terminator, control_flow.Raise):
344
+ exception = (
345
+ self.expression(terminator.exception) if terminator.exception is not None else None
346
+ )
347
+ cause = self.expression(terminator.cause) if terminator.cause is not None else None
348
+ return Raise(terminator.span, exception, cause)
349
+ if isinstance(terminator, control_flow.Jump):
350
+ self.enter_loop(block.id, terminator.target)
351
+ return Jump(terminator.span, terminator.target)
352
+ target = terminator.target
353
+ if self.resolve(target.identifier).kind is not ResolutionKind.LOCAL:
354
+ self.fail(target, "assignment to a global or nonlocal name is not supported yet")
355
+ return ForNext(
356
+ terminator.span,
357
+ self.iterators[block.id],
358
+ target.identifier,
359
+ terminator.body,
360
+ terminator.exit,
361
+ )
362
+
363
+ def enter_loop(self, source: BlockId, target: BlockId) -> None:
364
+ """Take the iterator of a ``for`` loop in the block that enters its header."""
365
+
366
+ header = self.cfg.block(target).terminator
367
+ if isinstance(header, control_flow.ForEach) and (source, target) not in self.cfg.back_edges():
368
+ iterable = self.expression(header.iterable)
369
+ self.iterators[target] = self.emit(GetIter(self.new_value(), header.span, iterable))
370
+
371
+ def captured_value(self, name: str, span: SourceSpan) -> Value:
372
+ """The current value of a variable a nested function captures."""
373
+
374
+ return self.expression(nodes.Name(name, span))
375
+
376
+ def function(self, node: nodes.Function) -> FunctionIR:
377
+ captured = captured_names(node, self.scopes)
378
+ parameter_values = tuple(self.new_value() for _ in (*node.parameters, *captured))
379
+ reassigned = _reassigned_parameters(node)
380
+ self.parameters = {
381
+ parameter.name: value
382
+ for parameter, value in zip(node.parameters, parameter_values, strict=False)
383
+ if parameter.name not in reassigned
384
+ }
385
+ # Captured variables come after the explicit parameters. A ``nonlocal`` one is
386
+ # assigned in this body, so it lives in a local slot initialised from the
387
+ # captured value; the others are never reassigned.
388
+ bindings = self.scopes.scope_for(node).bindings
389
+ self.nonlocals = frozenset(
390
+ name for name in captured if name in bindings and bindings[name].kind is BindingKind.NONLOCAL
391
+ )
392
+ captured_values = dict(zip(captured, parameter_values[len(node.parameters) :], strict=True))
393
+ self.parameters.update({n: v for n, v in captured_values.items() if n not in self.nonlocals})
394
+ blocks: list[BasicBlock] = []
395
+ for cfg_block in self.cfg.blocks.values():
396
+ self.instructions = []
397
+ if cfg_block.id == self.cfg.entry:
398
+ # Reassigned parameters live in locals so every block reads the same slot.
399
+ for parameter, value in zip(node.parameters, parameter_values, strict=False):
400
+ if parameter.name in reassigned:
401
+ self.emit_effect(StoreLocal(None, parameter.span, parameter.name, value))
402
+ for name in sorted(self.nonlocals):
403
+ self.emit_effect(StoreLocal(None, node.span, name, captured_values[name]))
404
+ for statement in cfg_block.statements:
405
+ self.statement(statement)
406
+ terminator = self.terminator(cfg_block)
407
+ blocks.append(
408
+ BasicBlock(
409
+ cfg_block.id, tuple(self.instructions), terminator, cfg_block.exception_targets
410
+ )
411
+ )
412
+ return FunctionIR(
413
+ qualified_name(self.scopes, node), parameter_values, self.cfg.entry, tuple(blocks), node.span
414
+ )
415
+
416
+
417
+ def qualified_name(scopes: ScopeTable, function: nodes.Function) -> str:
418
+ """``Class.method`` for methods, ``outer.inner`` for nested functions, the bare name
419
+ for module-level functions; lambda and comprehension scopes keep identifier names."""
420
+
421
+ names = [function.name]
422
+ scope = scopes.scope_for(function)
423
+ parent = scopes.scope(scope.parent) if scope.parent else None
424
+ while parent is not None and parent.kind is not ScopeKind.MODULE:
425
+ names.append(parent.name.strip("<>"))
426
+ parent = scopes.scope(parent.parent) if parent.parent else None
427
+ return ".".join(reversed(names))
428
+
429
+
430
+ def lambda_function(node: nodes.Lambda) -> nodes.Function:
431
+ """A lambda as a function named after its position, returning its body."""
432
+
433
+ return nodes.Function(
434
+ f"lambda_{node.span.start_line}_{node.span.start_column}",
435
+ node.parameters,
436
+ (nodes.Return(node.body, node.span),),
437
+ False,
438
+ node.span,
439
+ )
440
+
441
+
442
+ def captured_names(function: nodes.Function, scopes: ScopeTable) -> tuple[str, ...]:
443
+ """The enclosing-function variables ``function`` reads, sorted: its implicit
444
+ parameters after the explicit ones."""
445
+
446
+ scope = scopes.scope_for(function)
447
+ names: set[str] = set()
448
+
449
+ def walk(node: Node) -> None:
450
+ if isinstance(node, nodes.Name):
451
+ names.add(node.identifier)
452
+ for child in children(node):
453
+ walk(child)
454
+
455
+ for statement in function.body:
456
+ walk(statement)
457
+ for parameter in function.parameters:
458
+ if parameter.default is not None:
459
+ walk(parameter.default)
460
+ return tuple(sorted(n for n in names if scopes.resolve(scope.id, n).kind is ResolutionKind.FREE))
461
+
462
+
463
+ def _reassigned_parameters(function: nodes.Function) -> frozenset[str]:
464
+ """Parameters that the function body assigns to, outside nested scopes."""
465
+
466
+ assigned: set[str] = set()
467
+
468
+ def names(target: nodes.Target | None) -> None:
469
+ if isinstance(target, nodes.Name):
470
+ assigned.add(target.identifier)
471
+ elif isinstance(target, nodes.Tuple):
472
+ for element in target.elements:
473
+ names(element) # type: ignore[arg-type]
474
+
475
+ def walk(node: Node) -> None:
476
+ if isinstance(node, nodes.Assign | nodes.AugAssign | nodes.For | nodes.WithItem):
477
+ names(node.target)
478
+ elif isinstance(node, nodes.ExceptHandler) and node.name is not None:
479
+ assigned.add(node.name)
480
+ if isinstance(node, nodes.Function | nodes.Class | nodes.Comprehension | nodes.Lambda):
481
+ return
482
+ for child in children(node):
483
+ walk(child)
484
+
485
+ for statement in function.body:
486
+ walk(statement)
487
+ return frozenset(assigned & {parameter.name for parameter in function.parameters})
488
+
489
+
490
+ class PyIRAnalysis(FunctionAnalysis[FunctionIR]):
491
+ """Lower one function to PyIR on demand, following its control-flow graph."""
492
+
493
+ name: ClassVar[str] = "ir.pyir"
494
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
495
+ {ScopeAnalysis, SymbolAnalysis, CFGAnalysis}
496
+ )
497
+
498
+ @classmethod
499
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> FunctionIR:
500
+ scopes = ctx.get(ScopeAnalysis)
501
+ lowerer = _FunctionLowerer(
502
+ ctx.get(SymbolAnalysis),
503
+ scopes,
504
+ scopes.scope_for(function),
505
+ ctx.get(CFGAnalysis, function),
506
+ )
507
+ return lowerer.function(function)
508
+
509
+
510
+ def analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
511
+ """Top-level functions, the methods of top-level classes, and the functions and
512
+ lambdas nested inside them, in source order."""
513
+
514
+ functions: list[nodes.Function] = []
515
+ for statement in module.body:
516
+ if isinstance(statement, nodes.Function):
517
+ _collect(statement, functions)
518
+ elif isinstance(statement, nodes.Class):
519
+ for member in statement.body:
520
+ if isinstance(member, nodes.Function):
521
+ _collect(member, functions)
522
+ return tuple(functions)
523
+
524
+
525
+ def _collect(function: nodes.Function, into: list[nodes.Function]) -> None:
526
+ into.append(function)
527
+
528
+ def walk(node: Node) -> None:
529
+ if isinstance(node, nodes.Function):
530
+ _collect(node, into)
531
+ return
532
+ if isinstance(node, nodes.Lambda):
533
+ _collect(lambda_function(node), into)
534
+ return
535
+ if isinstance(node, nodes.Class):
536
+ # The methods of a class defined inside a function are nested functions.
537
+ for member in node.body:
538
+ if isinstance(member, nodes.Function):
539
+ _collect(member, into)
540
+ return
541
+ for child in children(node):
542
+ walk(child)
543
+
544
+ for statement in function.body:
545
+ walk(statement)
546
+
547
+
548
+ class ModuleIRAnalysis(Analysis[ModuleIR]):
549
+ """Assemble the PyIR of every top-level function of the module."""
550
+
551
+ name: ClassVar[str] = "ir.module"
552
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
553
+
554
+ @classmethod
555
+ def compute(cls, ctx: AnalysisContext) -> ModuleIR:
556
+ return ModuleIR(
557
+ tuple(ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module))
558
+ )
559
+
560
+
561
+ def lower_module(module: nodes.Module, *, ssa: bool = False) -> ModuleIR:
562
+ """Lower a whole module through a fresh analysis manager, optionally to SSA form."""
563
+
564
+ manager = AnalysisManager(module)
565
+ manager.register(*SEMANTIC_ANALYSES, CFGAnalysis, PyIRAnalysis, ModuleIRAnalysis)
566
+ if not ssa:
567
+ return manager.get(ModuleIRAnalysis)
568
+ # Imported here because the SSA pass is built on top of this module's analyses.
569
+ from coretrace_python.cfg.dominance import DominanceAnalysis
570
+ from coretrace_python.ir.ssa import SSAAnalysis
571
+
572
+ manager.register(DominanceAnalysis, SSAAnalysis)
573
+ return ModuleIR(
574
+ tuple(manager.get(SSAAnalysis, function) for function in analyzable_functions(module))
575
+ )