Skip to content

Commit 372db10

Browse files
committed
docs(dataflow): record vendored scalpel as the default L4 oracle
1 parent a68eb28 commit 372db10

4 files changed

Lines changed: 62 additions & 7 deletions

File tree

.claude/SCHEMA_DECISIONS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ supported 3.9–3.13+ range; `compute_SSA`'s return contract is discovered from
152152
source (not a documented public API) — wrap it behind `ScalpelAliasOracle` to
153153
contain upstream drift.
154154

155+
**Follow-up (2026-07-22): Scalpel vendored, now the default.** The Stage-0
156+
"dependency hygiene" concern proved fatal for a hard dependency: `python-scalpel`
157+
drags `typed_ast`, which has no wheel for Python 3.12+ and does not build from
158+
source there, so `pip install python-scalpel` fails on 3.12/3.13/3.14. Since
159+
`typed_ast` is imported only by `scalpel/typeinfer` (unused here), the 9-module
160+
`SSA`/`cfg`/`core` slice the oracle loads was **vendored** into
161+
`codeanalyzer/dataflow/scalpel/` (Apache-2.0, verbatim but for a `graphviz`-lazy
162+
patch). `ScalpelAliasOracle` is now the shipping default on all supported Python;
163+
`TypeBasedAliasOracle` is the runtime safety net only. See
164+
`docs/superpowers/specs/2026-07-22-vendored-scalpel-default-oracle-design.md`.
165+
155166
## Stage 5 — keystone conformance sweep (issue #98, schema_version 2.0.0)
156167

157168
The stage-5 pre-release conformance check against the canonical schema-v2

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- The Scalpel-backed L4 points-to oracle is now **vendored** (`typed_ast`-free)
12+
and the default on all supported Python (3.9–3.14); `python-scalpel` is no
13+
longer an optional dependency and the `[scalpel]` extra is removed. On Python
14+
3.12+ (and anywhere the `[scalpel]` extra was not installed), L4
15+
`prov:["points-to"]` data-dependence edges are now Scalpel-precise rather than
16+
the coarser type-based over-approximation; the `prov:["ssa"]` set and the
17+
`L3 ⊆ L4` monotonicity invariant are unchanged. Adds `astor` as a runtime
18+
dependency.
19+
1020
## [1.0.3] - 2026-07-21
1121

1222
### Fixed

CLAUDE.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,13 @@ declared callables by their `can://` tree id, imported/builtin targets by a
122122
(`codeanalyzer/dataflow/scalpel_oracle.py`) consumes `python-scalpel`'s **solved
123123
SSA + copy/const** state (it never forks the solver) as a copy-closure union-find
124124
over access paths, behind the frozen `may_alias(path_a, path_b) -> bool`
125-
interface. `python-scalpel` is an **optional** dependency: if it is absent or a
126-
build/query fails, the analyzer falls back to the total `TypeBasedAliasOracle`
127-
(`alias.py`) and degrades, never raising. The type-based oracle is the sanctioned
128-
fallback, not the shipping default.
125+
interface. Scalpel is **vendored** (`codeanalyzer/dataflow/scalpel/`, a
126+
`typed_ast`-free 9-module slice of `python-scalpel 1.0b0`, Apache-2.0) so it is
127+
the **shipping default** L4 oracle on every supported Python — there is no
128+
external `python-scalpel`/`typed_ast` dependency. `TypeBasedAliasOracle`
129+
(`alias.py`) is retained only as the runtime safety net: on a per-callable
130+
Scalpel build failure or a per-query unresolved access path, `may_alias`
131+
degrades to it, never raising.
129132
- **CLI gating** (`codeanalyzer/__main__.py`): `-a` max is 4; `--graphs sdg`
130133
requires `-a 4` (a flag error below that); `cfg,dfg,pdg` require `-a 3`;
131134
`--graph-field-depth` (the `k_limit`) is valid at L3+.

test/test_vendored_scalpel.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
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."""
3+
import ast
34
import sys
45

6+
import pytest
7+
58

69
def test_vendored_scalpel_imports_and_computes_ssa_typed_ast_free():
710
# No external scalpel shadowing the vendored copy.
@@ -25,9 +28,6 @@ def test_vendored_scalpel_imports_and_computes_ssa_typed_ast_free():
2528
assert not any("typeinfer" in m for m in loaded), f"typeinfer leaked: {loaded}"
2629

2730

28-
import ast
29-
30-
3131
def test_make_alias_oracle_defaults_to_scalpel_without_typed_ast():
3232
import sys
3333
assert "typed_ast" not in sys.modules
@@ -51,3 +51,34 @@ def test_make_alias_oracle_is_deterministic():
5151
o2 = make_alias_oracle(None, _ast.parse(src).body[0], {})
5252
pairs = [("a", "b"), ("b", "c"), ("a", "c"), ("b", "b")]
5353
assert [o1.may_alias(x, y) for x, y in pairs] == [o2.may_alias(x, y) for x, y in pairs]
54+
55+
56+
def _pip_scalpel_available():
57+
try:
58+
import importlib.util
59+
# the EXTERNAL package, not our vendored copy
60+
return importlib.util.find_spec("scalpel") is not None and \
61+
not importlib.util.find_spec("scalpel").origin.endswith(
62+
"codeanalyzer/dataflow/scalpel/__init__.py")
63+
except Exception:
64+
return False
65+
66+
67+
@pytest.mark.skipif(not _pip_scalpel_available(),
68+
reason="upstream python-scalpel not installed (3.12+ can't build typed_ast)")
69+
def test_vendored_ssa_matches_upstream():
70+
"""On a Python where pip python-scalpel installs (<=3.11), the vendored copy
71+
must produce identical SSA const_dict keys as upstream on the same source."""
72+
import scalpel.SSA.const as up_ssa
73+
import scalpel.cfg as up_cfg
74+
from codeanalyzer.dataflow.scalpel.SSA.const import SSA as VSSA
75+
from codeanalyzer.dataflow.scalpel.cfg import CFGBuilder as VCFG
76+
77+
src = "def f(a):\n b = a\n if a:\n b = 2\n return b\n"
78+
up_c = up_cfg.CFGBuilder().build_from_src("m", src)
79+
v_c = VCFG().build_from_src("m", src)
80+
up_fn = list(up_c.functioncfgs.values())[0]
81+
v_fn = list(v_c.functioncfgs.values())[0]
82+
_, up_const = up_ssa.SSA().compute_SSA(up_fn)
83+
_, v_const = VSSA().compute_SSA(v_fn)
84+
assert sorted(map(str, up_const)) == sorted(map(str, v_const))

0 commit comments

Comments
 (0)