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.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
)
|