Skip to content

Commit a68eb28

Browse files
committed
feat(dataflow): make vendored scalpel the default L4 alias oracle
Repoint ScalpelAliasOracle.from_function at the vendored, typed_ast-free codeanalyzer.dataflow.scalpel slice instead of the external python-scalpel package, and drop the now-dead ImportError fallback branch in make_alias_oracle -- Scalpel can no longer be "absent" since it ships in the package. TypeBasedAliasOracle remains the runtime safety net for a per-callable Scalpel build failure only. Reframe the two L4 tests whose premise this repoint invalidates: test_make_alias_oracle_falls_back_when_scalpel_absent now forces a per-callable build failure instead of poisoning sys.modules["scalpel"] (which no longer affects the vendored import path), and test_scalpel_oracle_copy_chain drops its pytest.importorskip("scalpel") guard since the vendored oracle is always present.
1 parent ab088a1 commit a68eb28

3 files changed

Lines changed: 50 additions & 27 deletions

File tree

codeanalyzer/dataflow/scalpel_oracle.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
forks or re-runs Scalpel's solver — and turns the copy/const records into
2323
per-function copy-closure equivalence classes:
2424
25-
``from scalpel.SSA.const import SSA``
25+
``from codeanalyzer.dataflow.scalpel.SSA.const import SSA``
2626
``ssa_results, const_dict = SSA().compute_SSA(func_cfg)``
2727
2828
``const_dict`` maps ``(name, version)`` to the ``ast`` value node that defined
@@ -138,14 +138,14 @@ def from_function(
138138
) -> "ScalpelAliasOracle":
139139
"""Build from a function AST by consuming Scalpel's solved SSA state.
140140
141-
Imports Scalpel lazily (``ImportError`` if the optional dependency is
142-
absent) and reuses the *same source* both graphs are built from — the
141+
Imports the vendored Scalpel slice (``codeanalyzer.dataflow.scalpel``)
142+
and reuses the *same source* both graphs are built from — the
143143
function's unparsed text — so the join is identity, not a fuzzy match.
144144
Raises on any build failure; :func:`make_alias_oracle` is the total,
145145
never-raising entry point callers should prefer.
146146
"""
147-
from scalpel.SSA.const import SSA
148-
from scalpel.cfg import CFGBuilder
147+
from codeanalyzer.dataflow.scalpel.SSA.const import SSA
148+
from codeanalyzer.dataflow.scalpel.cfg import CFGBuilder
149149

150150
src = ast.unparse(func_ast)
151151
fname = name or getattr(func_ast, "name", None)
@@ -250,19 +250,16 @@ def may_alias(self, path_a: str, path_b: str) -> bool:
250250
def make_alias_oracle(pycallable, func_ast, base_types) -> object:
251251
"""Total selector for the L4 may-alias oracle.
252252
253-
Returns a :class:`ScalpelAliasOracle` when ``python-scalpel`` is importable
254-
*and* builds successfully on ``func_ast``; otherwise logs once (INFO) and
255-
returns a :class:`TypeBasedAliasOracle` over ``base_types``. Never raises —
256-
mirrors how ``core._get_pycg_call_graph`` degrades on a missing/failed PyCG.
253+
Returns a :class:`ScalpelAliasOracle` built on the vendored, typed_ast-free
254+
Scalpel slice (``codeanalyzer.dataflow.scalpel``) — the default L4 oracle.
255+
Falls back to :class:`TypeBasedAliasOracle` only when the per-callable
256+
Scalpel build fails on this AST. Never raises.
257257
"""
258258
fallback = TypeBasedAliasOracle(base_types)
259259
try:
260260
return ScalpelAliasOracle.from_function(
261261
func_ast, base_types=base_types, fallback=fallback
262262
)
263-
except ImportError:
264-
_note_fallback("python-scalpel not installed")
265-
return fallback
266263
except Exception:
267264
_note_fallback("scalpel alias build failed")
268265
logger.debug("scalpel alias oracle build error", exc_info=True)

test/test_v2_l4.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import sys
66
from pathlib import Path
77

8-
import pytest
9-
108
from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
119
from codeanalyzer.dataflow.identity import IdentityMap
1210
from codeanalyzer.dataflow.sdg import ParamNode
@@ -104,18 +102,19 @@ def emit(self, record):
104102
self.records.append(record)
105103

106104

107-
def test_make_alias_oracle_falls_back_when_scalpel_absent(monkeypatch):
105+
def test_make_alias_oracle_falls_back_on_build_failure(monkeypatch):
108106
"""`make_alias_oracle` degrades to TypeBasedAliasOracle behavior and logs
109-
once when Scalpel cannot be imported — regardless of whether the optional
110-
dependency is actually installed in the runner."""
107+
once when the (now-vendored, always-present) Scalpel oracle fails to build
108+
for a given callable — the only surviving fallback path now that Scalpel
109+
can no longer be "absent" (it is vendored, not an optional import)."""
111110
import codeanalyzer.dataflow.scalpel_oracle as so
112111

113-
# Force `import scalpel...` to raise ImportError even if it is installed:
114-
# a None entry in sys.modules makes the import machinery halt. Cover the
115-
# top-level package and every submodule the oracle imports so a previously
116-
# cached submodule cannot satisfy the import.
117-
for mod in ("scalpel", "scalpel.cfg", "scalpel.SSA", "scalpel.SSA.const"):
118-
monkeypatch.setitem(sys.modules, mod, None)
112+
# Force the per-callable Scalpel build to fail, regardless of input.
113+
monkeypatch.setattr(
114+
so.ScalpelAliasOracle,
115+
"from_function",
116+
classmethod(lambda cls, *a, **k: (_ for _ in ()).throw(RuntimeError("boom"))),
117+
)
119118

120119
# Reset the process-wide "logged once" guard and capture on the actual
121120
# (propagate=False) codeanalyzer logger.
@@ -144,9 +143,9 @@ def test_make_alias_oracle_falls_back_when_scalpel_absent(monkeypatch):
144143

145144

146145
def test_scalpel_oracle_copy_chain():
147-
"""When Scalpel is importable, the copy chain `b = a; c = b` places a, b, c
148-
in one copy-closure class: a/b may-alias, an unrelated name does not."""
149-
pytest.importorskip("scalpel")
146+
"""Scalpel is vendored and always importable now, so the copy chain
147+
`b = a; c = b` always places a, b, c in one copy-closure class: a/b
148+
may-alias, an unrelated name does not."""
150149
from codeanalyzer.dataflow.scalpel_oracle import ScalpelAliasOracle
151150

152151
func_ast = ast.parse(COPY_CHAIN).body[0]

test/test_vendored_scalpel.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""The vendored, typed_ast-free scalpel slice: it must import and compute SSA
22
with no external scalpel / typed_ast / graphviz, and never pull in typeinfer."""
33
import sys
4-
import importlib
54

65

76
def test_vendored_scalpel_imports_and_computes_ssa_typed_ast_free():
@@ -24,3 +23,31 @@ def test_vendored_scalpel_imports_and_computes_ssa_typed_ast_free():
2423
assert "typed_ast" not in sys.modules
2524
loaded = [m for m in sys.modules if m.startswith("codeanalyzer.dataflow.scalpel")]
2625
assert not any("typeinfer" in m for m in loaded), f"typeinfer leaked: {loaded}"
26+
27+
28+
import ast
29+
30+
31+
def test_make_alias_oracle_defaults_to_scalpel_without_typed_ast():
32+
import sys
33+
assert "typed_ast" not in sys.modules
34+
from codeanalyzer.dataflow.scalpel_oracle import make_alias_oracle, ScalpelAliasOracle
35+
36+
src = "def f(a):\n b = a\n c = b\n return c\n"
37+
func_ast = ast.parse(src).body[0]
38+
oracle = make_alias_oracle(pycallable=None, func_ast=func_ast, base_types={})
39+
# The whole point: scalpel is the default, not the type-based fallback.
40+
assert isinstance(oracle, ScalpelAliasOracle), type(oracle).__name__
41+
# It answers queries (copies alias; unrelated locals do not).
42+
assert oracle.may_alias("b", "c") is True
43+
44+
45+
def test_make_alias_oracle_is_deterministic():
46+
import ast as _ast
47+
from codeanalyzer.dataflow.scalpel_oracle import make_alias_oracle
48+
src = "def f(a):\n b = a\n c = b\n return c\n"
49+
fa = _ast.parse(src).body[0]
50+
o1 = make_alias_oracle(None, _ast.parse(src).body[0], {})
51+
o2 = make_alias_oracle(None, _ast.parse(src).body[0], {})
52+
pairs = [("a", "b"), ("b", "c"), ("a", "c"), ("b", "b")]
53+
assert [o1.may_alias(x, y) for x, y in pairs] == [o2.may_alias(x, y) for x, y in pairs]

0 commit comments

Comments
 (0)