feat(rust): P-022 step 4 — port own-analysis (worklist solver + ownership/lifetime/effect/DI) with layered parity (#214)#249
Conversation
…own-diagnostics types (#214) First of #214's three review checkpoints: the frozen diagnostic-parity fixture and the comparison design, BEFORE any semantic port. Mirrors the #203 CFG-parity ratchet one layer up (verdict seam instead of the CFG seam). - tests/test_diag_fixtures.py + tests/fixtures/diag_parity.json: Python is authoritative; runs the real `check` surface (ownlang.__main__._collect) over corpus/ + examples/ + tests/fixtures/ + 7 curated cases (outcomes computed, never hand-written) and freezes the ordered [line, code] verdict list per input. Auto-discovered by run_tests.py; regenerate with `python tests/test_diag_fixtures.py --write`. Steady state runs zero Rust. - rust/crates/own-diagnostics: data-only port of Severity/Evidence/Diagnostic + the full TITLES vocabulary and the checked constructor (unknown code fails loudly, mirroring the Python __post_init__). DiagKey is the (line, code) parity key. No rendering, no solver state — presentation/SARIF is step 5. - own-diagnostics depends only on the own-ir span leaf (today: nothing), never on own-syntax/own-cfg/own-analysis. tests/dag.rs locks the allowed workspace edge set via `cargo metadata`. - tests/fixture.rs locks the comparison plumbing; the parse->lower->analyse ->compare replay is a TODO(#214, checkpoint 2) pending own-analysis. Scope/decisions recorded in docs/notes/p022-diag-parity-checkpoint1.md: precondition trace (the disappearing findings were the extractor's, explained and fixed by #238; the .own reference is green); effects/DI are OwnIR-fact sidecar families not exercised by .own (their parity lands with own-bridge, step 6); the preserved OWN020-for-parse-errors quirk is matched, not fixed, and flagged as a Python-first future issue. No semantic implementation, no own-analysis crate yet — that is checkpoint 2, after this design is reviewed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…rge) Installed .NET 8 from the web, built the Roslyn extractor, and ran the real own-check pipeline on the #238 regression pins on 4c5a86b: the weaved (Janitor.Fody) empty-Dispose sample is correctly flagged OWN001 (finding does not disappear), and all 8/8 EmptyDisposeSample FLAGGED controls fire while the legitimate enumerator exemptions stay silent. The one documented class of disappearing findings is positively confirmed resolved, not just inferred. Replaces the earlier "dotnet unavailable, could not re-run" caveat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
Cloned ClosedXML at the remeasure note's pinned commit (4e89dced), built the core lib + Examples (net8.0), and ran the real own-check pipeline: 119 OWN001 findings, all undisposed XLWorkbook wb/workbook locals — the exact locals the #233 empty-Dispose/Janitor.Fody over-exemption had silenced. On current main they are flagged again, so the disappearing findings have returned on the real repository, not just the isolated #238 pins. Records the scope difference (Examples/net8.0/flow-locals) vs the note's 263-call-site figure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
- frozen reference marked PROVISIONALLY approved; add the Integration-gate dependency (final semantic-port merge gated on the separate post-batch OSS remeasure run by another agent; point-check does not substitute for it). - reframe the regression as "the #238 regression class introduced by #233 and repaired by #240" — #238 records the problem, it is not a version boundary. - replace the unrestricted "no new false positives" with the bounded claim: the two legitimate IEnumerator<T> empty-Dispose controls stay silent in the regression fixture; broader FP behavior is left to the full OSS remeasure. - add an explicit proven / not-proven guarantee split and the two-branch integration gate (full sweep green -> final; unexplained disappearance -> Python-first fix, stop merge, regenerate golden explicitly, re-parity Rust). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…lver (#214 checkpoint 2a) The domain-agnostic half of #214's analysis heart: a forward dataflow solver in the rustc_mir_dataflow shape the Python _Analyzer.fixpoint ports to. The ownership/lifetime/effect/DI analyses plug in as `Analysis` impls next. - Lattice (bottom + join-into-self returning `changed`), ControlFlowGraph (blocks/entry/successors; preds + reachability derived), Analysis (entry_fact + monotone transfer). solve()/solve_with(Schedule). - RPO worklist by default; the fixpoint is schedule-independent (join is commutative/associative/monotone) — Schedule only changes revisit count. - Convergence guard: counts block visits, asserts a generous bound, fails loudly on a non-monotone/non-idempotent join instead of hanging. Mandated correctness battery (tests/solver.rs, on a concrete union lattice, before any domain rule): - CFG shapes: straight line, diamond, self-loop, nested loop, unreachable block (None + contributes nothing), multiple predecessors, abnormal early exit; - lattice laws exhaustively (idempotent/commutative/associative join, x <= join(x,y), bottom identity) over the 16-subset powerset; - worklist-order independence across Rpo/Postorder/Fifo/Lifo/BlockOrder; - convergence guard fires on a deliberately diverging join (#[should_panic]). Cargo metadata DAG edge test extended: own-analysis may depend on own-ir/own-syntax/own-cfg/own-diagnostics; nothing downstream may depend on it. Solver module is dependency-free; own-cfg/own-diagnostics arrive with ownership. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
… checkpoint 2b) The ownership half of #214's analysis heart, on top of the generic solver. Exact port of ownlang/analysis.py, gated by the frozen diagnostics oracle. own-analysis::ownership: - {OWNED,MOVED,RELEASED,ESCAPED} u8-bit lattice State (per-RID bitset, loans, handle->RID aliasing, move/acquire provenance); join with the Python merge invariants (block-scoped loans equal; 1:1 handle->RID). RID = SymId (u32), the deterministic replacement for Python id(sym). - full step() transfer over every Instr; two-phase run (silent fixpoint via the generic solver, then emitting pass + end-of-function leak checks). Parity (tests/parity.rs, replaying tests/fixtures/diag_parity.json): - 66 covered cases asserted, all pass EXACTLY on the full ordered (line,code) list — no field weakened; - 3 deferred to checkpoint 3 by name (2x OWN014 escape, 1x OWN019 buffer policy). The partition is sound: a case is covered iff none of its codes come from an unported pass (check_lifetimes / validate_policies), so d1+d2 equals Python's full output for every covered case. Mandated tests: - ownership lattice laws EXHAUSTIVE over the finite per-RID state (idempotent/ commutative/associative join, x<=join, bottom identity, the read-OWNED vs join-empty default split); - metamorphic: rename-invariance, repeated-analysis determinism, unreachable code never removes a finding, serde round-trip. Preserved Python behaviors (matched, not fixed): OWN020-for-parse-errors; the OWN003 "twice"/"maybe" and OWN002 "consumed"/"released" splits are message-only (same code+line), merged since only (line,code) is compared at this step. own-cfg: expose BufferMode/BufferInfo::stack_backed (the ownership analysis needs it for the OWN015/OWN016 escaping-stack-buffer checks). Integration gate unchanged: final semantic-port merge stays gated on the separate post-batch OSS remeasure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
… DAG, generated differential (#214) Closes the four blocking items from the checkpoint-2 review. The 66/66 covered parity result is unchanged. B1 — State satisfies the Lattice contract: - solver seeds each merge from the FIRST predecessor with an out-fact (clone) and joins the rest, matching Python in_state_of; join is thus only ever called between real predecessor states, never bottom.join(loaded). - State::join handles the bottom identity explicitly (bottom ∨ x = x even with active loans) and asserts FULL loan-map equality (owner+kind per binding), not just key equality. Python join tightened to full-map equality first (no-op: 132/132 green, fixture byte-identical) so the invariant matches. - tests: bottom-identity-with-loans, compatible-loan comm/assoc, same-key/ different-value fail-loud, real CFG borrow_mut spanning a branch merge. B2 — schedules are materially distinct: - true FIFO (VecDeque front/back) and true LIFO (Vec stack), not block-id-keyed aliases. Visit-order recorder proves >=3 distinct orders while all agree on the fixpoint; successor-order + block-ID permutation invariance; an ownership-specific (real State lattice) schedule-independence test. B3 — no production parser edge: - own-analysis reads the effect type via own_cfg::Effect (re-exported); own- syntax moved to dev-dependencies. DAG edge test filters to production (kind=null) deps; new assertion own_analysis_has_no_production_parser_edge. B4 — generated differential battery: - tests/test_diff_gen_fixtures.py deterministically generates 160 seeded ownership mini-programs (acquire/release/use/move/call/branch/loop/return) with Python (line,code) goldens; own-analysis/tests/diff_gen.rs replays with zero Python and prints seed+source on divergence. 160/160 exact match. Integration gate updated: final #214 merge gated on PR #243 (the separate OSS remeasure) being independently reviewed and merged. Checkpoint-3 scope clarified (lifetime/buffer/effect/DI as independent own-analysis impls; own-bridge wires facts but does not own algorithms; evidence/SARIF stay later steps). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…ete empty state (#214 review r3) Closes the one remaining checkpoint-2 blocker. Round 2's State::is_bottom() inferred bottom from structural emptiness, conflating the lattice ⊥ with a real reachable predecessor carrying an empty state — which let join(Concrete(empty), Concrete(with_loan)) bypass the loan invariant order-independently. - New explicit enum StateFact { Bottom, Reachable(State) } is the solver's Lattice fact. Bottom is the identity, only ever the seed for a block with no evaluated predecessors. - State::join_data (concrete ∨ concrete) enforces the full loan-map invariant unconditionally; no bottom special-casing lives there. - join: ⊥∨x=x, x∨⊥=x; Concrete(empty)∨Concrete(with_loan) and the symmetric direction fail loudly; Concrete(empty) != Bottom. - transfer(Bottom)=Bottom (transient, overwritten once a real predecessor arrives; a reachable block's converged in-fact is always Reachable). New tests: bottom_join_loan_state_is_identity, concrete_empty_join_loan_state_fails_loud, loan_state_join_concrete_empty_fails_loud, concrete_empty_is_not_lattice_bottom. Unchanged after the fix: 66/66 curated parity, 160/160 generated differential, schedule + permutation tests, both Python fixtures byte-identical. No fixture or acceptance change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…214 review r3 follow-up) Non-blocking hardening requested at checkpoint-2 approval. Phase-2 emission no longer degrades an unexpected Bottom/None to State::empty() — a reachable block that converged to Bottom (or has no fact) is a solver regression and now aborts loudly in EVERY build via converged_state(), not a debug assertion. A silent empty state there could mask a leak or use-after-release. - converged_state(fact, bid): Reachable(_) -> clone; Bottom/None -> panic with a precise message. #[allow(clippy::panic)] is the deliberate fail-loud choice. - removed the now-unused StateFact::into_state(). - new test every_reachable_block_converges_to_a_reachable_fact: over a loop+ branch+multi-exit CFG, asserts every graph-reachable block has a Reachable fact and every unreachable block has none. Unchanged: 66/66 parity, 160/160 generated differential, clippy 0, fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…us parity (#214) Ports the lifetime/region analysis and wires buffer-policy validation into a full check surface, closing the 3 deferred cases: the .own diagnostic parity for #214 is now complete (69/69). - own-analysis::lifetime: exact port of ownlang/lifetimes.py (AST-level region analysis). OWN030/031/036 structural validation + OWN014 region-escape (subscribe self to a longer-lived source). Ports _iter_subscribes, _strictly_longer (transitive closure), check_lifetimes, _check_fn. - own-analysis::check_module: exact port of ownlang.__main__.check_module — buffer policy (own_cfg::validate_policies) -> lifetime -> per-fn resolver d1 -> ownership d2, stable-sorted by (line, code). Tie order matches Python's. - Architecture: lifetime reads the AST through own_cfg::ast (re-exported by own-cfg, same pattern as own_cfg::Effect), so own-analysis keeps NO production own-syntax edge. The DAG + no-production-parser-edge tests stay green. Parity: - tests/parity.rs: 69/69 cases asserted, 0 skipped (the 3 previously deferred cases now match via check_module). - tests/diff_gen.rs: 160/160 generated programs, exact. - lifetime.rs unit tests: OWN014 escape, equal-lifetime clean, OWN036 cyclic, OWN030/031 undefined/redeclared, subscribe-nested-in-branch. Both Python fixtures unchanged; no fixture or acceptance change. Effect + DI (fact-driven, own-bridge step 6) are checkpoint 3b. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…lyses (#214) The last two #214 analyses, as independent own-analysis modules. Both are OwnIR-fact sidecar families (the bridge feeds facts; no .own surface), so the algorithms are ported and unit-pinned; end-to-end diagnostic parity lands with own-bridge (step 6). Data-only on the control-flow-relevant facts; evidence/ SARIF presentation metadata omitted (out of #214). - own-analysis::effect: exact port of effects.py — the Stable<Unknown<Unstable stability lattice resolved to a fixpoint over the render-scope binding graph (memoized, cycle-guarded); find_effect_storms flags an IO effect with a provably Unstable dep (EFF001). 7 unit tests. - own-analysis::di: exact port of di.py — the Service registration graph + all five DFS analyses (DI001 captive scoped, DI002 weak capture, DI003 captured transient IDisposable, DI004 root service-location, DI005 scope-cached captive) with transient-follow / singleton-stop / cycle guards. 8 unit tests. All four #214 analyses now ported (ownership, lifetime, effect, DI). Unchanged: .own full parity 69/69, generated differential 160/160, clippy 0, fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
…-level differential (#214 3b review) Addresses the checkpoint-3b blocking items: file and primary line are verdict identity (path, line, code), not presentation. B1 — DI verdict anchors match the bridge (ownir.py): - Service gains file, root_resolve_sites, scope_cache_sites. - DI001/002/003 anchor at the singleton registration site; DI004 at the root-resolution CALL SITE of the entry type (_di004_primary, registration fallback); DI005 at the field-store CACHE SITE of the cached entry (_di005_primary, registration fallback). The analysis selects the anchor; di_verdicts returns primary (path, line, code), combined in bridge append order then sorted (file, line, code). B2 — effect verdict path: - Effect/EffectStorm gain file; find_effect_storms sorts (file, line, dep); effect_verdicts returns (path, line, EFF001). B3 — Python-authored fact-level differential: - tests/test_di_eff_fact_parity.py freezes normalized effect/DI fact inputs + expected (path, line, code) from the real finders + bridge anchors into tests/fixtures/di_eff_fact_parity.json (8 effect / 13 DI cases: direct+ transitive DI001-005, DI004 transitive-disposable entry call-site anchor, DI005 transitive-scoped entry cache-site anchor, dup suppression, cycles, unknown lifetimes, multi-file ordering). own-analysis/tests/fact_parity.rs replays with zero Python. All exact. Unchanged: .own parity 69/69, generated differential 160/160, ownership fail- loud invariant, clippy 0, fmt clean. Python suite green; all fixtures in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
|
Warning Review limit reached
Next review available in: 50 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (31)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
DI004/DI005 select their primary (file, line) from per-entry site
records. Python builds that lookup with a dict comprehension —
`{t: (f, ln) for (t, f, ln) in sites}` — so a duplicate entry type is
last-wins, then `.get(entry)` is used only when its line is >= 1
(registration fallback otherwise). The Rust port iterated forward and
returned the FIRST match, diverging whenever the same entry type had
more than one site record.
Take the LAST matching site (reversed find on type) and apply the same
`>= 1` guard, so a trailing line-0 site falls back exactly as Python
does. Add two Python-authored duplicate-site controls to the fact-level
differential (di004/di005_duplicate_entry_sites_last_wins); the frozen
fixture now anchors both at second.cs (20/40), replayed with zero Python
by fact_parity.rs. diag_parity.json and diag_diff_gen.json unchanged.
Refs #214
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZjB2ggU9qm8vmuTQRtZXR
Что и зачем
Порт «сердца анализа» на Rust (P-022 step 4, #214): новые крейты
own-diagnostics(data-only verdict-типы) иown-analysis(обобщённый monotone-lattice worklist-солвер + четыре анализа — ownership, lifetime, effect, DI). Python остаётся эталоном; расхождение = баг Rust. Полный.own-паритет по(line, code); для fact-only семейств (EFF/DI) — Python-authored fact-level differential по(path, line, code).Тип изменения
Как проверено
python tests/run_tests.py(132/132; все fixture-джобы in sync)ruff check .иmypy(обе зелёные)python scripts/<...>.py --selftest)Дополнительно (Rust):
cd rust && cargo test --workspace— 20 бинарей зелёные (own-analysis: solver 13, ownership+lattice/bottom тесты, lifetime 5, effect 7, DI 8, parity 69/69, generated differential 160/160, fact_parity 2×);cd rust && cargo clippy --workspace --all-targets— 0 предупреждений;cargo fmt --check— чисто;python tests/test_diag_fixtures.py --write,python tests/test_diff_gen_fixtures.py --write,python tests/test_di_eff_fact_parity.py --write.Что внутри (по чекпоинтам ревью)
own-diagnosticsтипы + замороженныйdiag_parity.json+ DAG cargo-metadata edge-тест; precondition разобран живым прогоном на реальном ClosedXML.(line, code)паритетом; явныйStateFact { Bottom, Reachable }(bottom ≠ concrete-empty, full loan-map инвариант); fail-loud, если reachable-блок остался в ⊥.check_lifetimes) + wiring buffer-policy → полный.ownпаритет 69/69 (AST читается черезown_cfg::ast, без прод-ребра на парсер).(path, line)совпадают с bridge (DI004 — call-site, DI005 — cache-site); Python-authored fact-level differential (8 effect / 15 DI кейсов), Rust реплеит с нулём Python. DI-site-lookup соблюдает Python last-wins для дублирующихся записей одного entry-типа (покрыто двумя контролямиdi004/di005_duplicate_entry_sites_last_wins).Вне области #214 (остаётся на потом): паритет текста сообщений, evidence/codeFlows, SARIF-рендер,
own-bridge(парсинг/wiring OwnIR-фактов, step 6).Связанные issue
Closes #214
Финальный merge гейтится на смёрдженный PR #243 (broad OSS remeasure) и финальное pre-merge ревью.
Чеклист
docs/notes/p022-*заметки по чекпоинтам)feat:,fix:,docs:,harden:)🤖 Generated with Claude Code
Generated by Claude Code