Skip to content

Commit 4405aa0

Browse files
committed
docs(spec): vendored typed_ast-free scalpel as the default L4 oracle
Vendor the 9-module SSA/cfg/core slice of python-scalpel 1.0b0 (Apache-2.0, typed_ast-free) into codeanalyzer/dataflow/scalpel/, making ScalpelAliasOracle the shipping-default L4 oracle on Python 3.9-3.14+ with no external python-scalpel/typed_ast dependency. Type-based oracle demotes to the runtime safety net. Includes feasibility evidence, oracle rewiring, the L4 precision behavior change, and the testing plan.
1 parent 8fdd50d commit 4405aa0

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Vendored, typed_ast-free Scalpel as the default L4 oracle
2+
3+
- **Date:** 2026-07-22
4+
- **Status:** Design approved; ready for an implementation plan
5+
- **Scope:** Make the Scalpel-backed points-to oracle (`ScalpelAliasOracle`) the
6+
shipping default for L4 dataflow on **every** supported Python, by vendoring a
7+
minimal, `typed_ast`-free slice of `python-scalpel` into the package.
8+
9+
## Motivation
10+
11+
`python-scalpel` is the primary L4 may-alias oracle, but it ships today as an
12+
**optional** extra with a type-based fallback — because `python-scalpel 1.0b0`
13+
hard-depends on **`typed_ast`**, an abandoned package whose last release
14+
(`1.5.5`) has no wheel for Python 3.12+ and fails to compile from source on a
15+
modern compiler. So on the interpreters most users (and this repo's own dev env)
16+
now run — 3.12, 3.13, 3.14 — scalpel cannot be installed at all, and L4 silently
17+
degrades to the coarser type-based oracle.
18+
19+
We want scalpel to be the **default** L4 oracle everywhere. It cannot be a hard
20+
PyPI dependency (the `typed_ast` wall), and a Python-version cap to reach it
21+
(≤3.11) would drop the 3.12–3.14 support the analyzer just added. The way
22+
through is to **vendor** the small slice of scalpel we use, minus the one module
23+
that drags `typed_ast`.
24+
25+
## Feasibility (verified, not assumed)
26+
27+
- **`typed_ast` is spurious for our use.** It is imported in exactly one scalpel
28+
module — `scalpel/typeinfer/analysers.py` — which codeanalyzer never touches.
29+
The oracle uses only `scalpel.SSA.const` and `scalpel.cfg`.
30+
- **The slice runs without `typed_ast`.** On Python 3.12 with `typed_ast` *not*
31+
installed, `cfg.CFGBuilder` + `SSA.const.SSA` import and `compute_SSA` returns
32+
the exact SSA/const facts the oracle consumes (verified: a copy chain yields
33+
`('b',0), ('c',0), ('<ret>',0)`).
34+
- **Exact closure = 9 modules**, provably free of `typeinfer`/`typed_ast`
35+
(checked via `sys.modules` after importing the two entry points):
36+
`scalpel/__init__.py`, `SSA/{__init__,const}.py`,
37+
`cfg/{__init__,builder,model}.py`, `core/{__init__,func_call_visitor,vars_visitor}.py`.
38+
- **Third-party imports touched:** `astor` (used at runtime — `SSA/const.py:191`
39+
`astor.to_source`), `graphviz` (imported at module load in `cfg/model.py` but
40+
only used by the unused `build_visual()`), `networkx` (already a dependency).
41+
- **License = Apache-2.0** (confirmed on `SMAT-Lab/Scalpel` and in the wheel's
42+
`LICENSE`) — vendorable with attribution, same family as the already-bundled
43+
PyCG.
44+
- **Upstream is abandoned** (`1.0b0`, frozen) — the usual "vendored copy drifts
45+
from upstream" cost does not apply.
46+
47+
## Goals
48+
49+
- `ScalpelAliasOracle` is the **default** L4 oracle on Python 3.9–3.14+, with no
50+
external `python-scalpel`/`typed_ast` dependency and no `pip install` breakage.
51+
- The type-based oracle (`TypeBasedAliasOracle`) is retained purely as the
52+
runtime safety net (per-callable build failure, per-query unresolved path).
53+
- Behavior for users who already had the `[scalpel]` extra on ≤3.11 is unchanged
54+
(same code). Behavior on 3.12+ *improves* (real points-to instead of the
55+
type-based over-approximation).
56+
57+
## Non-goals
58+
59+
- No change to the L4 *schema* (`prov` values `ssa`/`points-to` unchanged; the
60+
DDG shape is the same).
61+
- No `requires-python` change — stays `>=3.9`.
62+
- No vendoring of scalpel's `typeinfer`, `call_graph`, `pycg`, `import_graph`,
63+
`scope_graph`, or `dataflow` packages — only the 9-module `SSA`/`cfg`/`core`
64+
closure the oracle actually loads.
65+
- No change to `defuse.py`'s two-rule DDG contract or the `k_limit` machinery.
66+
67+
## Design
68+
69+
### The vendored package: `codeanalyzer/dataflow/scalpel/`
70+
71+
A normal package (no `_vendor/` layer), mirroring upstream's layout so scalpel's
72+
relative imports (`from ..core.vars_visitor import get_vars`) resolve within
73+
`codeanalyzer.dataflow.scalpel`:
74+
75+
```
76+
codeanalyzer/dataflow/scalpel/
77+
__init__.py
78+
SSA/__init__.py, const.py
79+
cfg/__init__.py, builder.py, model.py
80+
core/__init__.py, func_call_visitor.py, vars_visitor.py
81+
LICENSE # upstream Apache-2.0, copied verbatim
82+
README.md # provenance + the one patch (see below)
83+
```
84+
85+
The `scalpel/` wrapper name is kept (rather than flattening `SSA`/`cfg`/`core`
86+
into `dataflow/`) because `codeanalyzer/dataflow/cfg.py` already exists and
87+
scalpel ships its own `cfg/` package — the wrapper namespaces them apart.
88+
89+
The 9 files are copied **verbatim** from `python-scalpel 1.0b0`, with **exactly
90+
one patch**:
91+
92+
- `cfg/model.py`: the module-load `import graphviz as gv` (line 12) is moved to a
93+
lazy import *inside* the unused `build_visual()` method. This removes `graphviz`
94+
as a runtime dependency (we never render). No other line changes; the
95+
SSA/CFG-computation code paths are byte-identical to upstream.
96+
97+
### Attribution (Apache-2.0)
98+
99+
- `codeanalyzer/dataflow/scalpel/LICENSE` — upstream's Apache-2.0 license, copied.
100+
- `codeanalyzer/dataflow/scalpel/README.md` — records: source repo
101+
(`SMAT-Lab/Scalpel`), the pinned version (`1.0b0`), the exact 9-file list, the
102+
single `graphviz`-lazy patch, and that `typeinfer` (the sole `typed_ast` user)
103+
was deliberately excluded.
104+
- The repo's top-level `NOTICE` gains a one-line entry crediting vendored Scalpel
105+
(Apache-2.0), alongside the existing attributions.
106+
107+
### Dependencies (`pyproject.toml`)
108+
109+
- **Add** `astor` to core `dependencies` (genuine runtime dep; pure-Python;
110+
installs on every platform/Python).
111+
- **Remove** the `[project.optional-dependencies].scalpel` extra (scalpel is now
112+
built in).
113+
- `networkx` unchanged (already core); `graphviz` and `typed_ast` are **not**
114+
dependencies.
115+
- `requires-python` stays `>=3.9`.
116+
117+
### Oracle rewiring: `codeanalyzer/dataflow/scalpel_oracle.py`
118+
119+
- `ScalpelAliasOracle.from_function` imports from the vendored path:
120+
`from codeanalyzer.dataflow.scalpel.SSA.const import SSA` and
121+
`from codeanalyzer.dataflow.scalpel.cfg import CFGBuilder`.
122+
- `make_alias_oracle`: the `ImportError` ("python-scalpel not installed") branch
123+
becomes dead — the vendored import cannot be absent — and is **removed**. The
124+
per-callable **build-failure** `except` (returns the type-based fallback) and
125+
the per-query unresolved-path fallback inside `may_alias` both **stay**, so the
126+
total, never-raises contract is preserved. `ScalpelAliasOracle` is now the
127+
default the selector returns.
128+
- Docstrings/comments updated to drop the "optional / not installed" framing.
129+
130+
### Behavior change
131+
132+
On Python 3.12+/3.14 — and for anyone who never installed the old `[scalpel]`
133+
extra — L4's `prov:["points-to"]` DDG edges were previously derived from the
134+
*type-based* over-approximation; they now become **scalpel-precise** (a tighter
135+
subset). The `prov:["ssa"]` edges (the L3 subset) are unchanged, so the
136+
**monotonicity invariant `L3 ⊆ L4` still holds** (the ssa set is untouched;
137+
points-to is an additive overlay). Users who already had the `[scalpel]` extra on
138+
≤3.11 see no change. This L4 precision improvement is recorded in `CHANGELOG.md`.
139+
140+
### Docs
141+
142+
`CLAUDE.md` and `.claude/SCHEMA_DECISIONS.md` are updated to record: scalpel is
143+
now **vendored** (`codeanalyzer/dataflow/scalpel/`, `typed_ast`-free) and the
144+
**shipping default** L4 oracle on all supported Python — superseding the earlier
145+
"the type-based oracle is the sanctioned fallback, not the shipping default"
146+
statement (it is now the runtime safety net, not the default). The Stage-0
147+
`SCHEMA_DECISIONS` entry gains a follow-up noting the `typed_ast` wall on 3.12+
148+
that forced vendoring.
149+
150+
## Testing plan
151+
152+
1. **Import hygiene.** `import codeanalyzer.dataflow.scalpel` and building the
153+
oracle succeed with no external `scalpel`/`typed_ast`/`graphviz` installed;
154+
assert `typeinfer` is never imported (scan `sys.modules` after building the
155+
oracle).
156+
2. **`typed_ast`-free property gate.** Assert `typed_ast` is not importable in
157+
the environment, yet `make_alias_oracle` builds a `ScalpelAliasOracle` and
158+
`may_alias` returns verdicts on a copy-chain fixture — the whole point,
159+
encoded as a regression gate.
160+
3. **Vendored-fidelity check.** On Python ≤3.11 (where pip `python-scalpel`
161+
installs), compare the vendored `compute_SSA`/CFG output against the
162+
pip-installed scalpel on a set of fixtures — proves the copy is faithful.
163+
`skipif` the pip package can't install (3.12+); the copy is verbatim, so this
164+
is a belt-and-suspenders check.
165+
4. **L4 regression (the live acceptance test).** `test_v2_l4.py`,
166+
`test_v2_l4_summary.py`, `test_dataflow_sdg.py`, `test_dataflow_defuse.py` now
167+
exercise the **scalpel** path by default — the first time scalpel-L4 runs on
168+
the 3.14 dev env. They must stay green and now genuinely test scalpel-derived
169+
points-to rather than the fallback.
170+
5. **Determinism.** Run L4 twice on a fixture and assert identical output (the
171+
scalpel SSA feeds `may_alias`; the `#99` `PYTHONHASHSEED=0` pin + the oracle's
172+
access-path normalization must keep it stable).
173+
174+
## Risks and rollback
175+
176+
- **Incomplete closure** — a missed transitive import would surface as an
177+
`ImportError` at oracle build, which the per-callable fallback swallows into a
178+
silent type-based degrade. Mitigation: the import-hygiene test (1) builds the
179+
oracle and asserts it is a `ScalpelAliasOracle`, so a broken closure fails
180+
loudly rather than degrading silently.
181+
- **Vendored-copy divergence from upstream behavior** — mitigated by the verbatim
182+
copy (only the `graphviz` import patched) and the fidelity check (3).
183+
- **L4 output change on 3.12+** is intended, documented in the CHANGELOG, and
184+
bounded by the preserved monotonicity invariant.
185+
- **Rollback** is a single revert: delete `codeanalyzer/dataflow/scalpel/`,
186+
restore the `[scalpel]` extra + the `make_alias_oracle` ImportError branch, and
187+
drop `astor`. No schema or id changes to unwind.
188+
189+
## Out of scope
190+
191+
- Replacing `astor.to_source` with stdlib `ast.unparse` to drop the `astor` dep
192+
(a behavior-risking change to vendored SSA logic — deferred; `astor` is tiny
193+
and universal).
194+
- A scalpel-installed CI lane / capturing the L4 (`a4`) equivalence goldens from
195+
the separate analysis-pipeline branch — that work belongs to that branch once
196+
it merges; here scalpel simply becomes available for it.
197+
- Vendoring or using scalpel's type-inference (`typeinfer`) for the type-guided
198+
alias branch — the type-based oracle already covers that fallback.

0 commit comments

Comments
 (0)