Skip to content

Commit efcec4c

Browse files
committed
docs(dataflow): record Stage 0 Scalpel oracle spike + integration decision
1 parent ddae36c commit efcec4c

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

.claude/SCHEMA_DECISIONS.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,85 @@ additions, all additive:
6969
- `CALL` SDG edges are not projected: the callable-level `PY_CALLS` twin
7070
already carries calls; callsite-statement granularity is recoverable via
7171
`PY_HAS_CALLSITE`/`PY_RESOLVES_TO`.
72+
73+
## Stage 0 — Scalpel oracle spike
74+
75+
Research spike (issue #70) verifying **SMAT-Lab/Scalpel** as the primary L4
76+
may-alias oracle before the interprocedural-dataflow stage writes any
77+
integration. No product code changed; a throwaway probe under `test/spikes/`
78+
was run and deleted.
79+
80+
**Decision.** Primary oracle = **`ScalpelAliasOracle`** implementing the
81+
frozen interface `may_alias(path_a: str, path_b: str) -> bool`; automatic
82+
fallback = the existing `TypeBasedAliasOracle` (for constructs Scalpel can't
83+
resolve and for parse/build failures, keeping the interface total).
84+
85+
**Verdict: Scalpel is VIABLE** as the L4 oracle — consumed as **SSA + copy/const
86+
facts**, not as a turnkey points-to engine (Scalpel ships no Andersen/
87+
Steensgaard heap analysis; its "alias pairs" are copy/const records over SSA).
88+
89+
**Environment.** Installed `python-scalpel==1.0b0` (self-reports
90+
`__version__ == "1.0dev"`) via `uv pip install python-scalpel` into the repo's
91+
uv-managed `.venv`, **CPython 3.12.13** (the project interpreter per
92+
`pyvenv.cfg`/`.envrc`; the bare `python` on PATH is a pyenv shim to 3.14.0 and
93+
is *not* the project env). `import scalpel` and `import codeanalyzer` both work
94+
afterward. `uv pip install` does not touch `uv.lock`; installed packages live
95+
in the gitignored `.venv`.
96+
97+
**Modules / classes / functions to consume, and their output shape:**
98+
99+
1. **CFG substrate**`from scalpel.cfg import CFGBuilder`;
100+
`CFGBuilder().build_from_src(name, src)` (or `build_from_file`). CFG is
101+
**basic-block level**: nested-function CFGs in `cfg.functioncfgs` keyed by
102+
`(entry_id, func_name)`, params in `cfg.function_args`. `Block.statements`
103+
are **real `ast` statement nodes retaining `.lineno`/`.col_offset`**.
104+
2. **SSA + alias** (there is *no* standalone alias module) — `from
105+
scalpel.SSA.const import SSA`; `ssa_results, const_dict =
106+
SSA().compute_SSA(func_cfg)`. **Statement-level** on top of the block CFG.
107+
- `ssa_results`: `dict[block_id] -> [ {var_name: {def_version_ints}} ]` (one
108+
dict per statement) — the use-def chain; a version set with >1 element is
109+
a **phi/merge**; an empty set is a param/global/external use. Attribute
110+
access-path names (`a.field`) appear as keys.
111+
- `const_dict`: `dict[(var_name, version)] -> ast value node` — the **alias
112+
carrier**. Value an `ast.Name` ⇒ a copy edge (`('b',0)->Name 'a'` means
113+
b aliases a); value an `ast.Attribute` ⇒ attribute-path store; `<ret>`
114+
is the return-value pseudo-name. Version counter is function-global, so
115+
`(name, version)` is a stable intra-function SSA identity.
116+
3. **Type inference** (for the type-guided branch) — `from
117+
scalpel.typeinfer.typeinfer import TypeInference`; **file-based**:
118+
`TypeInference(name, entry_point=path).infer_types(); get_types()`
119+
`list[dict]` rows `{file, line_number, function, type: set[str],
120+
variable|parameter}`; `'any'` = unknown.
121+
122+
**Mapping onto access-path strings / `(signature, node_id)`.** Both the repo
123+
(`codeanalyzer/dataflow/cfg.py` `CFGNode` carries `start_line`/`end_line` +
124+
`ast_node`) and Scalpel build from the **same source AST**, so the join is by
125+
source position: function ⇄ `functioncfgs` key; node ⇄ statement AST
126+
`(lineno, col_offset)` equal to `CFGNode.start_line`(/col) ⇒ same `node_id`
127+
(the integration can even reuse the repo AST, making it identity not a match).
128+
Scalpel var names use the same `base(.field|[*])*` grammar (normalize
129+
subscripts to `[*]`, re-`k_limit`). `may_alias` = transitively close
130+
`const_dict` `Name``Name`/attribute copies into per-function equivalence
131+
classes; TRUE iff bases share a class **and** suffixes are prefix-compatible
132+
(reuse `suffix_of`/prefix logic); for unrelated bases consult `TypeInference`
133+
(incompatible concrete types ⇒ not aliased, `any`/unknown ⇒ may-alias); on
134+
anything unresolved, fall back to `TypeBasedAliasOracle`.
135+
136+
**Probe answers.** (a) alias/SSA output **is** keyable to `(function,
137+
line/col)` — verified end-to-end. (b) **CFG block-level, SSA/use-def
138+
statement-level.** (c) modern syntax: `walrus :=` OK, `async`/`await`/`async
139+
with` OK, `match`/`case` **does not crash** but is **not** split into per-arm
140+
CFG branches (block-level imprecision, mitigated by the repo's own
141+
statement-level CFG).
142+
143+
**Concerns carried to Stage 4.** Copy-only + intra-function (no heap
144+
points-to; two params/aliased container elements are not modeled — the
145+
type-guided branch + sound-leaning fallback must cover them); `match` arms
146+
unbranched in Scalpel's CFG; dependency hygiene — `python-scalpel` is a
147+
low-maintenance pre-release that drags in `typed-ast` (C build, historically
148+
fails on 3.13+) and a `dataclasses` backport (harmlessly shadowed on 3.12), so
149+
pin/constrain them, gate the import as a soft dependency (missing/broken
150+
Scalpel → fallback, not a hard failure), and confirm the build across the repo's
151+
supported 3.9–3.13+ range; `compute_SSA`'s return contract is discovered from
152+
source (not a documented public API) — wrap it behind `ScalpelAliasOracle` to
153+
contain upstream drift.

0 commit comments

Comments
 (0)