Skip to content

Commit 87f6df1

Browse files
committed
docs: add # P-016 — Deep C# fact extraction: CFG + flow lowering
1 parent 78dab71 commit 87f6df1

2 files changed

Lines changed: 159 additions & 1 deletion

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# P-016 — Deep C# fact extraction: CFG + flow lowering
2+
3+
- **Status:** draft (P1 — the "make the core actually bite real C#" track)
4+
- **Depends on:**
5+
- [P-014](P-014-semantic-resolution.md) Tier A — the `SemanticModel` (**DONE**).
6+
The hard prerequisite: typed ownership facts are impossible without binding.
7+
- [P-001](P-001-csharp-extractor.md) — the extractor → OwnIR → core seam, and the
8+
`ownir_version` contract.
9+
- [P-005](P-005-idisposable-ownership.md) — the first deep profile this unlocks
10+
(IDisposable acquire/release on all paths); [P-004](P-004-wpf-lifetime-profile.md)
11+
(lifetime), [P-007](P-007-arraypool-span.md) (borrow/Span).
12+
- `spec/OwnCore.md`, `spec/Lifetimes.md` — the fact vocabulary the core *already*
13+
checks (acquire / move / borrow / use / release / escape / control-flow).
14+
- **Strategy hub:** [`docs/ROADMAP.md`](../ROADMAP.md).
15+
16+
## Motivation
17+
18+
The core (`ownlang`) is a sound, tested, flow-sensitive ownership / borrow /
19+
lifetime checker — **but only on the `.own` DSL.** On real C#, the Roslyn frontend
20+
feeds it a *flat list of pattern-matched resource facts* (`event +=`, IDisposable
21+
field, pool, local-disposable). None of the core's deep machinery —
22+
release-on-all-paths across branches, use-after-release, double-release, move,
23+
borrow aliasing, lifetime ordering — is exercised on real code. The engine runs;
24+
the fuel line to real C# is a trickle. Two concrete gaps:
25+
26+
1. **The frontend emits no control flow and no per-statement operations.** It does
27+
not lower real method bodies into acquire / use / release / move / borrow over a
28+
CFG, so OWN001/002/003/005/… never fire on real C# beyond the shallow patterns.
29+
2. **The core does not model loops.** `cfg.py`: *"There are no loops, so the CFG is
30+
a DAG and a single topological pass suffices — this is exactly where loop support
31+
(worklist + fixpoint) would later plug in."* `OWN020` marks loops/async
32+
unsupported. Real C# is full of loops.
33+
34+
P-014 Tier A removed the hard prerequisite (the frontend now has a `SemanticModel`).
35+
This proposal is the plan to feed the *existing* core real facts — a CFG plus
36+
acquire/use/release/move/borrow — from real C#, **one fact-type per increment**,
37+
plus the core loop support that lets those facts be checked on real (loopy) methods.
38+
39+
This is the honest answer to "does Own.NET work, or only on paper?": prove one real
40+
flow-sensitive verdict on real C# end-to-end. If the thin vertical slice (B0+B2
41+
below) lights up a true OWN001 on GTM, the concept is proven; if it proves
42+
intractable, far better to learn that now than after more breadth.
43+
44+
## Dependencies — what we depend on to start
45+
46+
| # | Dependency | Status |
47+
|---|------------|--------|
48+
| 1 | Types in the frontend (no typed ownership facts without binding) |**DONE — P-014 Tier A `SemanticModel`** |
49+
| 2 | A control-flow graph from real C# | Roslyn `ControlFlowGraph` / `IOperation` exists — needs lowering |
50+
| 3 | OwnIR (and the bridge) able to carry per-method operations + a CFG | schema growth — see **B0** |
51+
| 4 | The core handles loops | ❌ missing (`OWN020`); worklist + fixpoint — the set-of-states lattice is finite & union-merged, so it converges — see **A1** |
52+
| 5 | An honest-skip path for shapes we cannot model (async, exotic flow) | ✅ already the project's philosophy |
53+
54+
Replace the "single topological pass" with a worklist, and the flat fact list with
55+
a CFG-carrying bridge, and the existing core checks real code.
56+
57+
## Scope — the increments (two tracks that meet at the rich-fact bridge)
58+
59+
### Track A — core only (pure DSL, no frontend, `.own`-tested)
60+
61+
- **A1 — Loops.** Replace the single topological pass over the DAG (`cfg.py`,
62+
`analysis.py`) with a worklist + fixpoint over back-edges. The lattice is the
63+
finite set-of-states (OwnCore §3, union at merges) → monotone → it converges
64+
(confirm whether widening is even needed). Removes the `OWN020` "loops" clause.
65+
Fully independent of the frontend; pinned by new `.own` loop cases + the gallery.
66+
67+
### Track B — frontend depth (needs the `SemanticModel`, now present)
68+
69+
- **B0 — Direct-Module bridge (and kill the double parse).** Two steps:
70+
- **B0a (refactor, no behavior change):** today the bridge lowers OwnIR facts to
71+
`.own` **source text** (`ownir.py` `to_own`) and the core **re-parses that text**
72+
(`__main__._collect``parse`) before checking — a parse of a tree we just
73+
built, a round-trip that has existed since P-001 and doubles the lowering work
74+
(every new fact must be expressible as, *and survive a round-trip through*,
75+
generated `.own` text). Replace it: build the core `Module` AST
76+
(`ast_nodes`) **directly** from facts and call `check_module` — no text, no
77+
re-parse. The seam is already split for this (`ownir.py`: *"builds a Module and
78+
calls `__main__.check_module` directly … the seam is already split so that
79+
switch is additive, not a rewrite"*). Pin against the existing fixtures: same
80+
findings, one fewer parse.
81+
- **B0b (enabler):** extend OwnIR — and the direct `Module` construction — to carry
82+
per-method **basic blocks + acquire / use / release / move / borrow / return**
83+
operations: the schema the deep checks need. Bumps `OWNIR_VERSION` (a new fact
84+
*category*, NOT additive like P-014's OWN050; the version gate already forces the
85+
extractor and core to move together).
86+
- **B1 / B2 — IDisposable flow for locals.** Lower a method's `new` / `using` /
87+
`.Dispose()` / `return` into acquire / release / escape over a Roslyn CFG → real
88+
**OWN001** (leaked on an exception/early-return path), **OWN002** (use after
89+
dispose), **OWN003** (double dispose) on live C#. The IDisposable story (P-005) —
90+
the most common .NET resource bug, and the first time the core's flow analysis
91+
bites real code. B2 (`using`/try-finally → scoped release) is the smallest first
92+
slice; do it before the general case.
93+
- **B3 — Move / ownership transfer.** `return disposable`, or passing it to a callee
94+
that consumes it → move / escape (P-005 D5). Intraprocedural, driven by declared
95+
signatures, not whole-program tracing.
96+
- **B4 — Borrow / Span.** `Rent` → view → `Return`, `Span`/`ref` aliasing (P-007) —
97+
the borrow checker's crown jewel, hardest on C# (ref structs, Span lifetimes).
98+
- **B5 — Lifetime ordering.** WPF005 escape (`OWN014`) and DI captive (P-006,
99+
partly built) — the hardest; needs an explicit lifetime model.
100+
101+
**Dependency order:** `B0 → B1 → B3 → B4 / B5`. `A1` is independent but needed for
102+
B1 to cover loopy methods — until A1 lands, a method containing a loop is honestly
103+
skipped (`OWN020`), not guessed. B0+B2 and A1 can proceed in parallel.
104+
105+
## Non-goals
106+
107+
- A full C# semantic front-end. We lower the operations the core already models, not
108+
the language. `async`/`await` stays an honest skip (`OWN020`) until a real bug
109+
demands it; whole-program/interprocedural analysis beyond signature-declared
110+
transfer; the XAML/binding engine. The "refuse the soul-eating version" rule holds.
111+
- Rewriting the core. A1 *extends* the existing flow engine (worklist) — it does not
112+
replace the lattice. B0–B5 only feed the core; they decide no verdicts.
113+
114+
## Sketch
115+
116+
```text
117+
A1: worklist+fixpoint (loops) ─────────────┐
118+
v
119+
real *.cs ─[Roslyn CFG + IOperation]─[B1..B5: acquire/use/release/move/borrow]
120+
─[B0b: per-method ops+blocks OwnIR]─[B0a: build Module directly]
121+
─[the one core]──> OWN001/002/003/005/014 @ the C# line
122+
```
123+
124+
## Relationship to the spec & docs (anti-drift)
125+
126+
- **`spec/` core semantics: unchanged by Track B.** The core already models
127+
acquire/borrow/move/lifetime for `.own`; B0–B5 *feed* it those facts from C#, they
128+
do not change what it means. **A1 does change the core** (it analyses loops): when
129+
it lands, `spec/OwnCore.md`'s "loops out of scope" (§10) and the `OWN020`
130+
loops clause are updated to describe the worklist — spec follows code.
131+
- **OwnIR contract:** B0b **bumps `OWNIR_VERSION`** — rich per-method facts are a new
132+
category, not an additive optional field (contrast P-014's OWN050, which did not
133+
bump). The load-time version gate already makes a mismatched extractor/core fail
134+
loudly, so the bump is safe by construction.
135+
- **No new diagnostic codes** — B1–B5 reuse the existing OWN001/002/003/005/008/014.
136+
The only catalogue change is *removing* the loops clause from OWN020 when A1 ships.
137+
- **"One checker" preserved:** B0a removes a parse, not a decider; the core remains
138+
the single source of truth.
139+
140+
## Open questions
141+
142+
1. **B0b schema shape.** Grow the OwnIR JSON with a per-function ops+blocks array
143+
the bridge maps to a `Module`, or have the extractor emit a serialized `Module`
144+
directly? (Lean: ops+blocks JSON — keeps the extractor decider-free.)
145+
2. **try-finally / `using` modeling.** Map to a "release on all paths" region, or to
146+
explicit release on each CFG exit edge?
147+
3. **How much of Roslyn's `ControlFlowGraph` to consume** (its basic blocks + the
148+
operations we map) vs build our own CFG from syntax. (Lean: consume Roslyn's — it
149+
already normalizes `using`/`try`/short-circuits.)
150+
4. **Loop fixpoint:** does the finite set-of-states lattice converge fast enough
151+
as-is, or is widening warranted on pathological back-edges? (Likely converges.)
152+
5. **Honest-skip granularity:** on an unsupported construct, skip the whole method,
153+
or analyse the modelable prefix and skip the rest? (Conservative: skip the method,
154+
emit `OWN020` once.)
155+
6. **Which slice proves the concept fastest** — B0a+B2 on loop-free methods is the
156+
proposed existential spike; confirm it surfaces a real OWN001 on GTM before
157+
investing in B3–B5.

docs/proposals/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ proposal is marked `done` with a pointer.
3737
| [P-013](P-013-distribution-surface.md) | Distribution surface (how people run Own.NET) | v0 built (CI/Action + dotnet tool) |
3838
| [P-014](P-014-semantic-resolution.md) | Project-local semantic resolution (kills `+=` false positives) | draft (P0) |
3939
| [P-015](P-015-configuration-surface.md) | Configuration surface (check selection & per-category severity) | draft (stub) |
40+
| [P-016](P-016-deep-fact-extraction.md) | Deep C# fact extraction (CFG + flow lowering; loops) | draft |
4041

4142
> For priorities, milestones, the framing, and the design philosophy across all
42-
> of these, see the strategy hub: [`docs/ROADMAP.md`](../ROADMAP.md). P-004 … P-015
43+
> of these, see the strategy hub: [`docs/ROADMAP.md`](../ROADMAP.md). P-004 … P-016
4344
> capture ideas raised in design discussion — they are *on the record for
4445
> consideration*, drafts, not commitments.
4546

0 commit comments

Comments
 (0)