Quality gate, lifetimes module (WPF leaks), spec + proposals, and a real C# leak pipeline#8
Conversation
Python was chosen for prototype speed, but untyped dispatch hid a class of 'forgot a variant' bugs (the kind the old codegen kept reintroducing). Tighten the screws: - pyproject.toml: ruff (E,W,F,I,B,UP,C4,RUF) over the tree; mypy --strict on the ownlang package (tests are dynamic fuzzer code, ruff-only). - typing.assert_never in every node dispatch (lower_stmt / step / _stmt_inline): an unhandled union variant is now a type error. This already surfaced a real gap -- a buffer let falling through the inline emitter. - Fix all 39 strict-mypy findings (type params, untyped defs, union-attr narrowing, None-flow) and ruff findings; no behavior change (suite green: analysis 123/123, codegen content 23/23, fuzz, gallery 10/10, corpus 2/2). - CI: new 'lint' job runs ruff + mypy on every push/PR. - README: document the gate next to the regression net.
Make every ast_nodes dataclass frozen=True. Verified no code mutates AST nodes after construction (container fields are only appended to, which frozen still allows), so this is behavior-preserving and now blocks accidental post-parse mutation at runtime. Gate + suite stay green.
Captures the direction before touching the parser: reframe reachability as linear ownership (Subscribe returns an Owned token that must be Disposed), proof that the core WPF leak already trips OWN001 today, the lifetime-region syntax for the genuinely-new part, an OWN-WPF code catalogue mapped to slices, and the honesty/scope caveats. Two open forks left for sign-off.
The core ownership checker already catches the main class of WPF lifetime leaks once a ViewModel is modelled as a scope (constructor = scope start, Dispose = scope end): a subscription/timer that is never disposed is OWN001, touched after dispose is OWN002. Slice #1 makes that usable without inventing WPF-specific codes: - resource declarations gain an optional, contextual 'kind "..."' attribute (no new reserved word). It is domain-neutral metadata threaded onto the owning Symbol and surfaced on diagnostics as a '[resource: <kind>]' suffix -- the seam a later WPF profile / Roslyn front-end keys off, with the core staying generic. - corpus/wpf/: two self-checking real-pattern cases (zombie ViewModel -> OWN001, handler-use-after-dispose -> OWN002) with before/after C#, case.own, expected codes and honesty notes; tests/test_wpf.py pins both the codes AND that the kind tag reaches rendered output. - docs/lifetimes.md captures the full module plan (lifetime regions, OWN-WPF catalogue, slice boundaries); README gains a business-application section. - bonus: assert_never now also guards lower_let's rhs dispatch; the strict gate caught a real 'kind' name collision in analysis.step during this work. Gate + suite green: analysis 123/123, codegen 23/23, fuzz, gallery 10/10, corpus 2/2, wpf 2/2.
The genuinely-new analysis on top of the ownership core: it reasons about region
escape (the WPF 'zombie ViewModel promoted to App lifetime' theorem), not just
release within a scope.
Language:
- lifetime declarations with a strict order: 'lifetime Window < App;'
(transitive; cycles -> OWN036, undefined refs -> OWN030, redef -> OWN031).
- lifetime annotations on the object a function sets up and on its service
params: 'fn VM(bus: EventBus lifetime App) lifetime ViewModel'.
- a strong-capture statement: 'subscribe self to SOURCE;'
('self'/'to' contextual; only 'lifetime'/'subscribe' are reserved; '<' lexed).
Analysis (ownlang/lifetimes.py, check_lifetimes):
- region escape OWN014: if SOURCE strictly outlives self, the strong capture
promotes self to SOURCE's lifetime and it leaks. The *ordering* is what makes
it a leak -- a same/shorter-lived source is clean. Per fork B the code is
domain-neutral (OWN014 'escapes to a longer-lived region'), not WPF-branded.
- the mitigation (a disposable token released on close) is the slice-#1
acquire/release pattern, so both halves of the theorem now exist.
Wiring/tests:
- CLI 'check' runs check_lifetimes; the new Subscribe stmt is a no-op for the
loans/permissions flow and a schematic emit in codegen (both assert_never
dispatchers updated; lower_let rhs got assert_never too).
- corpus/wpf/viewmodel-escapes-to-app (before/after C# + notes); test_wpf made
tolerant of non-kinded cases. tests/test_lifetimes.py: 10 region cases.
- docs/lifetimes.md + README updated to mark slice #2 built.
Gate + suite green: mypy --strict, ruff, analysis 123/123, codegen 23/23, fuzz,
gallery 10/10, corpus 2/2, wpf 3/3, lifetimes 10/10.
…ilot Split the project's design material into two clearly-separated tracks so aspirational docs can never lie about the code: spec/ -- NORMATIVE, descriptive: what OwnLang IS today, derived from the working checker and pinned by tests. - OwnCore.md: affine ownership + borrow permissions; identity, states, loans, numbered rules R1-R12, call boundary. - BufferPolicies.md (B1-B7), Lifetimes.md (L1-L4), Diagnostics.md (every OWN code linked to the rule that raises it), CodegenContract.md (C1-C4: codegen may reject a sound program but must never emit unsafe C#). docs/proposals/ -- FORWARD-LOOKING RFCs for what is NOT built yet, with explicit non-goals (the discipline is refusing the soul-eating version): - P-001 C# -> OwnIR extractor (the WPF leak spike; recommended seam + v0 scope). - P-002 verification backend (Boogie/Dafny, horizon). - P-003 lifetime visualization (RustOwl-style, horizon). Conformance pilot: tests/test_spec.py runs one canonical program per normative rule and asserts its code fires (15/15), so spec and checker cannot drift. Wired into run_tests; README structure updated. Gate + suite green: ruff, mypy --strict, analysis 123/123, codegen 23/23, fuzz, gallery 10/10, corpus 2/2, wpf 3/3, lifetimes 10/10, spec 15/15.
…n conformance
Round out the normative spec so every existing surface has a reference point for
future changes:
- spec/Grammar.md: the full surface syntax -- tokens (reserved vs contextual vs
rejected), EBNF, and a construct->spec map. Was only in a parser docstring.
- spec/CLI.md: check / emit / cfg / report and their exit semantics.
- spec/BufferPolicies.md: add the 'policy {}' block + options spec (rule B8:
unknown/malformed keys -> OWN030).
- spec/README.md index + reading order updated.
Conformance pilot broadened to 22 rules pinning ~21 distinct codes (added
Buffer B1/B4/B8 and structural OWN031/033/035/041); the remaining codes
(maybe-variants, buffer specifics) stay covered by the buffer/analysis suites.
Gate + suite green; markdown cross-links validated.
First time the tool bites real C#, along the recommended seam: a Roslyn extractor produces facts, the existing Python core renders the verdict -- one checker, not two. - ownlang/ownir.py + 'python -m ownlang ownir facts.json': ingests OwnIR facts, lowers them to a synthetic .own sketch (subscription = owned resource, += = acquire, -= = release), runs the core, and maps OWN001 back to the C# location with the [resource: subscription token] tag. - frontend/roslyn/OwnSharp.Extractor: syntax-only Roslyn that scans .cs for 'target += handler' with no matching '-=' and emits OwnIR JSON. CI-only (no local dotnet). - CI job 'wpf-extractor': real .cs -> extractor -> facts -> core -> leak at the C# line; the disposed sample stays silent. - tests/test_ownir.py (5 checks) pins the bridge locally against hand-written facts; wired into run_tests. docs/proposals/P-001 -> in progress (v0 built). Scope v0 is exactly the event-subscription leak; timers / IDisposable fields / region facts are next. Gate + suite green (ownir 5/5).
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds lifetime-region analysis (new ChangesOwnLang Lifetime Regions, OwnIR Bridge, WPF Corpus, and Spec
Sequence Diagram(s)sequenceDiagram
participant src as .own source (with lifetimes)
participant parser as parser.parse_module
participant lf as lifetimes.check_lifetimes
participant cfg as cfg.collect_kinds + build_cfg
participant an as analysis.analyze
parser->>parser: parse lifetime decls + subscribe stmts
parser-->>src: Module(lifetimes=[...], fns=[...])
lf->>lf: validate ordering, transitive closure
lf->>lf: per-fn subscribe escape check
lf-->>src: OWN014/OWN030/OWN031/OWN036 diagnostics
cfg->>cfg: collect_kinds + lower subscribe as no-op
cfg->>cfg: set Symbol.resource_kind on acquire/move
an->>an: leak_check / Release / Return with resource_kind
an-->>src: OWN001/OWN002 diagnostics + [resource: kind] suffix
sequenceDiagram
participant cs as C# source files
participant ext as OwnSharp.Extractor (Roslyn)
participant json as facts.json
participant ownir as ownir.check_facts
participant core as OwnLang core (_collect)
participant cli as python -m ownlang ownir
cli->>ext: run ownsharp-extract *.cs -o facts.json
ext->>cs: parse syntax trees
ext->>json: emit JSON (components + subscriptions)
cli->>ownir: load(facts.json)
ownir->>ownir: to_own(facts) → synthetic .own module
ownir->>core: parse + collect_kinds + build_cfg + analyze
core-->>ownir: OWN001 diagnostics with sub_<id> subjects
ownir->>ownir: extract handle, map back to C# file:line
ownir-->>cli: Finding list (CustomerViewModel.cs:12 OWN001)
cli->>cli: print findings + exit code
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ownlang/__main__.py (1)
79-92:⚠️ Potential issue | 🟠 Major
cmd_cfgis missing theresource_kindsargument passed elsewhere.Line 45 passes all five arguments to
build_cfgincludingkinds, but Line 90 passes only four (omittingresource_kinds). While this won't crash at runtime becauseresource_kindshas a default value, the inconsistency suggests this call was not updated during the refactor. Verify whethercmd_cfgshould passresource_kindsexplicitly or if relying on the default value is intentional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ownlang/__main__.py` around lines 79 - 92, The build_cfg function call in cmd_cfg is missing the resource_kinds argument that is passed explicitly at line 45 elsewhere in the code. Update the build_cfg call on line 90 (within the for loop iterating over mod.functions) to include the resource_kinds argument explicitly, making it consistent with other invocations of build_cfg in the codebase. Determine the appropriate value for resource_kinds based on how it is computed at the other call site to ensure consistency across all build_cfg invocations.docs/lifetimes.md (1)
1-171:⚠️ Potential issue | 🟡 MinorFix missing code block language specifier and align documentation language with project norms.
The fenced code block at line 23–29 lacks a language identifier; it should be marked
```ownlanginstead of bare```(MD040 violation).Additionally,
docs/lifetimes.mdis written entirely in Russian while all specification documents (spec/*.md) and project documentation are in English. Althoughspec/Lifetimes.mdprovides an English normative source, this draft design document should be translated to English for consistency and accessibility, or clearly documented as a secondary non-normative artifact if the dual-language approach is intentional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/lifetimes.md` around lines 1 - 171, Add the `ownlang` language identifier to the fenced code block at lines 23–29 that currently lacks one (change ` ``` ` to ` ```ownlang `). Additionally, translate the entire `docs/lifetimes.md` document from Russian to English for consistency with the project's specification documents and normative documentation (all `spec/*.md` files), or if maintaining a dual-language approach is intentional, add a clear notice at the beginning stating that this is a secondary non-normative artifact with `spec/Lifetimes.md` as the authoritative English source.
🧹 Nitpick comments (1)
corpus/wpf/viewmodel-escapes-to-app/notes.md (1)
29-32: ⚡ Quick winClarify C# ingestion scope to avoid stale wording.
Line 29 currently says OwnLang has no C# front-end, but this PR introduces C# ingestion via the Roslyn extractor + OwnIR bridge. Please reword this to distinguish “no direct C# parsing in
ownlang check” from “C# facts are supported viaownir,” and apply the same wording tocorpus/wpf/handler-use-after-dispose/notes.mdLine 20.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@corpus/wpf/viewmodel-escapes-to-app/notes.md` around lines 29 - 32, The note at corpus/wpf/viewmodel-escapes-to-app/notes.md Line 29 incorrectly states there is no C# front-end, but the PR now enables C# ingestion through the Roslyn extractor and OwnIR bridge. Reword Line 29 to clarify that `ownlang check` has no direct C# parser, but C# facts are supported via the `ownir` intermediate representation. Apply the same clarified wording to corpus/wpf/handler-use-after-dispose/notes.md Line 20 to maintain consistent documentation across both files and accurately reflect that C# ingestion is now available through the OwnIR bridge rather than being completely unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 10-17: The CI workflow is relying on default GitHub token
permissions which may be overly broad. Add an explicit permissions block at the
workflow level to set a read-only baseline (permissions: read-only) and then
only elevate specific permissions for the lint job or other jobs that require
additional access. This follows the principle of least privilege for the
GITHUB_TOKEN.
In `@corpus/wpf/zombie-viewmodel/notes.md`:
- Around line 29-31: The statement in notes.md at the identified location
contains outdated information claiming OwnLang has no C# front-end, which now
conflicts with the addition of the Roslyn extractor/OwnIR bridge to the stack.
Update the wording to acknowledge that while a C# front-end (Roslyn extractor)
now exists, the specific case.own example discussed in this document is a
hand-reduced version created manually rather than directly produced from the
extractor output. This clarification should help readers understand the
relationship between the extracted pattern and the manual example being
analyzed.
In `@docs/lifetimes.md`:
- Around line 23-29: The fenced code block containing lifetime and function
declarations is missing a language identifier. Add ownlang as the language
specifier to the opening fence (the triple backticks before the lifetime App;
line) to enable proper syntax highlighting and follow markdown best practices
for code blocks.
In `@docs/proposals/P-001-csharp-extractor.md`:
- Line 21: The document contains inconsistent claims about OWN014 region
facts—line 21 lists them as "next" (future work), but the v0 scope section
(lines 57-58) indicates they are already produced. Review both locations to
determine whether OWN014 region facts are actually part of the current v0
implementation or are planned for future work, then update line 21 accordingly
to match the v0 scope claim. Additionally, audit the resource-kind label
terminology throughout the document and ensure it aligns with the actual
implemented naming convention of "subscription token" to prevent confusion about
what features are delivered versus planned.
In `@ownlang/ast_nodes.py`:
- Around line 52-53: Frozen dataclasses with mutable container fields can be
modified after construction, breaking the immutability contract. Convert all
list fields to tuple types (e.g., args: list[Expr] becomes args: tuple[Expr,
...]) and all dict fields to frozendict types (e.g., options: dict[str, Expr]
becomes options: frozendict[str, Expr]) across all affected nodes. Specifically,
in the Acquire class convert the args field, in the Call class convert the args
field, in BufferIntent convert the options field, in BorrowBlock convert the
body field, in the If class convert both then_body and else_body fields, in
ResourceDecl convert the members field, in ExternDecl convert the params field,
in FnDecl convert both params and body fields, in PolicyDecl convert the
settings field, and in the Module class update all fields that use
default_factory to return frozen containers instead. You may need to import
frozendict from an appropriate library or create a frozen dict type if not
already available.
In `@ownlang/lifetimes.py`:
- Around line 86-87: The early return at lines 86-87 exits before validating
function and parameter lifetime annotations, which causes undeclared lifetime
annotations to not emit the OWN030 diagnostic and skips subscribe checks for
those functions. Move the early return to after the validation logic (such as
the _check_fn calls) so that all functions are properly validated for undeclared
lifetime annotations regardless of whether mod.lifetimes is empty, then return
the diagnostics after validation is complete.
In `@ownlang/ownir.py`:
- Around line 66-93: The to_own function assumes every item in the components
list and subscriptions list is a dictionary without validating the schema first.
Add schema validation at the start of the to_own function to check that
facts.get("components", []) contains only dictionaries, and that each
component's subscriptions (from comp.get("subscriptions", [])) also contains
only dictionaries. If validation fails, raise a ValueError with a descriptive
message instead of allowing the code to crash when calling .get() on
non-dictionary items. This validation should occur before the iteration loops at
lines starting with "for comp in facts.get" and "for sub in comp.get".
In `@spec/OwnCore.md`:
- Around line 132-134: The spec documentation in spec/OwnCore.md at lines
132-134 inaccurately describes the test behavior, claiming that
tests/test_spec.py asserts the exact code when it actually only checks
membership (verifying that the rule code is among the produced diagnostics).
Update the wording in this section to accurately reflect that the suite checks
for rule code membership in the diagnostics rather than asserting exact code
equivalence.
In `@spec/README.md`:
- Line 25: The lifetime rules range mentioned in the README is incomplete and
does not reflect all normative rules defined in the Lifetimes specification.
Update the text that currently states "Lifetimes L1–L3" to "Lifetimes L1–L4" to
include the L4 rule (resource kind metadata) that is defined as normative in the
provided Lifetimes.md specification document.
In `@tests/test_lifetimes.py`:
- Around line 163-166: Add a guard check before accessing `diags[0]` to verify
that the diagnostics list is not empty. If `check_lifetimes(parse(escape_src))`
returns no diagnostics, the code will raise an IndexError and fail the test
suite unexpectedly. Insert a check (such as an assertion or conditional) that
ensures `diags` contains at least one element before calling
`diags[0].render_pretty()`, so the test fails with a clear diagnostic message
instead of crashing.
In `@tests/test_wpf.py`:
- Around line 101-104: The `set()` conversion on line 101 removes duplicate
diagnostic codes, which masks regressions with duplicate diagnostics. Replace
`sorted(set(codes))` with `sorted(codes)` to preserve the multiplicity of each
diagnostic code in the comparison between `got` and `want`.
---
Outside diff comments:
In `@docs/lifetimes.md`:
- Around line 1-171: Add the `ownlang` language identifier to the fenced code
block at lines 23–29 that currently lacks one (change ` ``` ` to ` ```ownlang
`). Additionally, translate the entire `docs/lifetimes.md` document from Russian
to English for consistency with the project's specification documents and
normative documentation (all `spec/*.md` files), or if maintaining a
dual-language approach is intentional, add a clear notice at the beginning
stating that this is a secondary non-normative artifact with `spec/Lifetimes.md`
as the authoritative English source.
In `@ownlang/__main__.py`:
- Around line 79-92: The build_cfg function call in cmd_cfg is missing the
resource_kinds argument that is passed explicitly at line 45 elsewhere in the
code. Update the build_cfg call on line 90 (within the for loop iterating over
mod.functions) to include the resource_kinds argument explicitly, making it
consistent with other invocations of build_cfg in the codebase. Determine the
appropriate value for resource_kinds based on how it is computed at the other
call site to ensure consistency across all build_cfg invocations.
---
Nitpick comments:
In `@corpus/wpf/viewmodel-escapes-to-app/notes.md`:
- Around line 29-32: The note at corpus/wpf/viewmodel-escapes-to-app/notes.md
Line 29 incorrectly states there is no C# front-end, but the PR now enables C#
ingestion through the Roslyn extractor and OwnIR bridge. Reword Line 29 to
clarify that `ownlang check` has no direct C# parser, but C# facts are supported
via the `ownir` intermediate representation. Apply the same clarified wording to
corpus/wpf/handler-use-after-dispose/notes.md Line 20 to maintain consistent
documentation across both files and accurately reflect that C# ingestion is now
available through the OwnIR bridge rather than being completely unavailable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c5199251-b9c6-42a6-b920-b3cae065a168
📒 Files selected for processing (59)
.github/workflows/ci.ymlREADME.mdcorpus/wpf/handler-use-after-dispose/after.cscorpus/wpf/handler-use-after-dispose/before.cscorpus/wpf/handler-use-after-dispose/case.owncorpus/wpf/handler-use-after-dispose/expected-diagnostics.txtcorpus/wpf/handler-use-after-dispose/notes.mdcorpus/wpf/viewmodel-escapes-to-app/after.cscorpus/wpf/viewmodel-escapes-to-app/before.cscorpus/wpf/viewmodel-escapes-to-app/case.owncorpus/wpf/viewmodel-escapes-to-app/expected-diagnostics.txtcorpus/wpf/viewmodel-escapes-to-app/notes.mdcorpus/wpf/zombie-viewmodel/after.cscorpus/wpf/zombie-viewmodel/before.cscorpus/wpf/zombie-viewmodel/case.owncorpus/wpf/zombie-viewmodel/expected-diagnostics.txtcorpus/wpf/zombie-viewmodel/notes.mddocs/lifetimes.mddocs/proposals/P-001-csharp-extractor.mddocs/proposals/P-002-verification-backend.mddocs/proposals/P-003-lifetime-visualization.mddocs/proposals/README.mdexamples/golden_arraypool/verify_emit.pyfrontend/roslyn/OwnSharp.Extractor/OwnSharp.Extractor.csprojfrontend/roslyn/OwnSharp.Extractor/Program.csfrontend/roslyn/README.mdfrontend/roslyn/samples/CustomerViewModel.csfrontend/roslyn/samples/OrdersViewModel.csownlang/__main__.pyownlang/analysis.pyownlang/ast_nodes.pyownlang/buffers.pyownlang/cfg.pyownlang/codegen.pyownlang/diagnostics.pyownlang/lexer.pyownlang/lifetimes.pyownlang/ownir.pyownlang/parser.pyownlang/report.pypyproject.tomlspec/BufferPolicies.mdspec/CLI.mdspec/CodegenContract.mdspec/Diagnostics.mdspec/Grammar.mdspec/Lifetimes.mdspec/OwnCore.mdspec/README.mdtests/fixtures/ownir/sample.facts.jsontests/run_tests.pytests/test_codegen.pytests/test_codegen_props.pytests/test_corpus.pytests/test_gallery.pytests/test_lifetimes.pytests/test_ownir.pytests/test_spec.pytests/test_wpf.py
| ``` | ||
| ownlang/ | ||
| core states/lattice/dataflow/diagnostics (= нынешние analysis/cfg/diagnostics) | ||
| buffers профиль OwnSharp.Performance (есть) | ||
| lifetimes профиль OwnSharp.Lifetimes (этот док) | ||
| frontend/csharp Roslyn-ингест (далёкая фаза) | ||
| ``` |
There was a problem hiding this comment.
Add language specifier to fenced code block.
The code block at line 23–29 is missing a language identifier. Based on context (lifetime and function declarations), it should specify ownlang.
📝 Proposed fix
-```
+```ownlang
lifetime App;
lifetime Window < App; // Window строго короче App
lifetime ViewModel < Window;
fn CustomerViewModel(bus: EventBus lifetime App) lifetime ViewModel {
subscribe self to bus; // bus сильно держит self
}
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 23-23: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/lifetimes.md` around lines 23 - 29, The fenced code block containing
lifetime and function declarations is missing a language identifier. Add ownlang
as the language specifier to the opening fence (the triple backticks before the
lifetime App; line) to enable proper syntax highlighting and follow markdown
best practices for code blocks.
Code / correctness: - lifetimes.py: drop the early-return on empty mod.lifetimes so a 'fn/param lifetime X' annotation referencing an undeclared region still emits OWN030 (was silently accepted with no declarations). Real soundness gap. - ownir.py: shape-check OwnIR facts (root/components/subscriptions) in load() AND to_own(), raising a clear ValueError on malformed input instead of a deep traceback. Tests: - test_lifetimes.py: guard the headline render check against empty diags (pick the OWN014 diagnostic explicitly). - test_wpf.py: compare sorted(codes), not sorted(set(...)), so a duplicate diagnostic is a regression, not masked. CI hardening: - ci.yml: add least-privilege 'permissions: contents: read' (action SHA-pinning stays deferred per README #7). Docs (stale-claim cleanup now that the P-001 extractor exists): - corpus notes + P-001: reword 'no C# front-end' to 'not direct extractor output; the P-001 extractor is narrow (event subscriptions)'. - P-001: v0 produces OWN001 with [resource: subscription token]; OWN014 is the next increment, not v0. - spec/OwnCore.md: conformance asserts the code is *among* produced codes (membership), matching test_spec. - docs/lifetimes.md: add 'text' language to the directory-tree fence (MD040). Gate + suite green (analysis 123/123, spec 22/22, wpf 3/3, lifetimes 10/10, ownir 5/5).
…wtonsoft run First entries where Own.NET (not the oracle) over-reports, triaged from the 4 product own-only findings on JamesNK/Newtonsoft.Json: - #7 event '+=' on a freshly-created, RETURNED publisher (JsonSerializer:717): the dual of ownership transfer; bounded by the returned object's lifetime, so no '-=' needed. Already tiered as an advisory warning ('injected'), not a hard error — surfaces only under --severity warning. Proper fix is interprocedural publisher provenance. - #8 owning field whose IDisposable holds no unmanaged resource (TraceJsonReader/ Writer StringWriter/JsonTextWriter): a StringWriter is StringBuilder-backed, not a handle. Fixable by adding System.IO.StringWriter/StringReader to the existing IsDisposeOptional allowlist (mirrors Task/DataTable); the JsonTextWriter wrapper stays a low-value residual. Both verdicts: not real leaks, criticality ~0. CodeQL/Infer# correctly silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r
A StringWriter is StringBuilder-backed and a StringReader reads a string — neither holds an OS handle or unmanaged resource, so Dispose() frees nothing real and an undisposed owning field of these is not a leak. Add System.IO StringWriter/StringReader to IsDisposeOptional (the same lever already used for Task/ValueTask/DataTable/DataSet/ DataView), so the owned-field detector stays silent on them. MemoryStream is deliberately NOT exempted — it can own a real buffer and still warns. Surfaced by the oracle on Newtonsoft.Json (field-notes #8): TraceJsonReader/TraceJsonWriter own undisposed StringWriter/JsonTextWriter fields; CodeQL/Infer# correctly stay silent on the StringWriter (not a real resource), Own.NET over-reported. Adds a HolderWithStringWriter control to ResolvedDisposableSample + a CI assertion it stays silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r
…pply shape bounded The dual of ownership transfer, interprocedural: a += on a PARAMETER publisher is locally indistinguishable from a DI-singleton bus, so it honestly warns (OWN001, injected tier). The extractor now runs a lazy, memoized, compilation-wide provenance pass: when the subscribing method is private/internal and EVERY visible caller passes a freshly-constructed local that escapes only into the call / its own return (and the callee never lets the param escape), the subscription is stamped source_provenance: "returned_fresh" and the bridge drops it — bounded by the returned publisher's lifetime, same boundedness as a locally-constructed source. Instance-level provenance deliberately beats the type-level DI hop. Precision-first denials (all keep the warning, pinned by ReturnedPublisherSample.cs in CI + test_ownir.py): public candidate, method-group reference, named/ref/omitted argument, non-fresh/non-local argument, any other use of the caller's local (field store, other callee, lambda capture, reassignment), param->param forwarding, zero visible callers. Constructors are excluded by construction (MethodKind gate), so the ctor-injected DI shape can never be silenced. Clears the mined Newtonsoft JsonSerializer.Create over-report (field-notes #8); its oracle-fp-baseline entry is REMOVED so a regression resurfaces in triage instead of being swallowed by the allowlist. Spec'd in OwnIR.md §4 + the JSON schema (additive optional field, no version bump per IR rules). Closes #146 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsWw4Ay8KLTHFom1HvRu3U
…nction denial cases CodeRabbit nitpicks on #208: the returned-fresh denial-case enumerations in field-notes #8 and oracle-known-fps predate c3cb656, so they omitted the two local-function closure captures; and the disposition summary still counted serializer.Error as baselined (Fixed 6->7, Baselined 5->4, 8 findings/7 rules -> 7 findings/6 rules). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsWw4Ay8KLTHFom1HvRu3U
…ent-leaks, fallback, Action, docs) Addresses the REQUEST CHANGES review on PR #284. Scope stays B0+B1. 1. Handler-shape guard. MatchesDeclaredWeakSubscribe now runs the second argument through NormalizeHandler + IsHandler, so a declared overload whose 2nd parameter is not a delegate -- AddPropertyChanged(source, 42) -- is NOT minted as a subscription. New negative control NonHandlerSecondArgument (zero facts). 2. --no-event-leaks respected. The new detector is gated on `emitEvents && weakSubscribe.Count > 0` (was just Count > 0), and the Rx-Subscribe suppression only fires when emitEvents is on. New facts case: sample + --no-event-leaks + declared -> neither the weak wrapper nor the ordinary += is emitted. 3. Real unresolved-external fixture. WeakSubscribeUnresolvedSample.cs calls External.WeakEvents.AddPropertyChanged (type not in the compilation) so the syntactic receiver-name fallback is exercised (acceptance #8), plus an unresolved negative control with a different final receiver type name. 4. Composite Action exposes the feature. action.yml gains an optional `config:` input, forwarded as OWN_CONFIG and passed to own-check (--config) on both the SARIF and the normal path. A real Action-level test (action-marketplace-readiness.yml, uses: ./): a weak wrapper is a finding WITHOUT config and silent WITH it, and a malformed config hard-errors through the Action. 5. Docs synced to reality. P-035 status + "What exists today" (method-call wrapper is detected, config is consumed; method detector folded into P-035; A and C deferred); P-015 (format resolved: explicit --config own.toml TOML carrier, rest deferred); spec/CLI.md (the `config` command + own-check --config). Verified locally via WSL: extractor facts for all four configs (on/off/no-events/ unresolved) pass tests/check_weak_subscribe_facts.py; the WeakBus fixture is OWN001 without --config and silent with it; ruff + mypy --strict + run_tests.py all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
What this is
A stack of work taking the PoC from "sound borrow checker on hand-written
.own"to "finds a real leak in real C#", with a quality gate and a normative spec under
it. Every step keeps the suite + gate green.
1. Quality gate (
ruff+mypy --strict+assert_never)pyproject.toml: ruff over the tree,mypy --stricton theownlangpackage.typing.assert_neverin every node dispatch — an unhandled union variant isnow a type error. This already caught a real gap (a buffer-let falling through
the inline emitter) and a name collision in
analysis.step.frozen=True(verified no code mutates them).lintjob.2.
lifetimesmodule — WPF/business leaks (the killer use case)disposed is the core's OWN001, a touch-after-dispose is OWN002. New
domain-neutral
kind "..."resource tag surfaces as[resource: ...]— theseam a WPF profile keys off without the core knowing about WPF.
lifetime Window < App;) and asubscribe self to sourcecapture. A short-lived object capturedby a longer-lived source is promoted and leaks → OWN014 (region escape).
The ordering is what makes it a leak.
corpus/wpf/(before/after C# + notes),tests/test_wpf.py,tests/test_lifetimes.py.3.
spec/(normative) +docs/proposals/(forward-looking)spec/retro-documents what the language is today, pinned by tests:OwnCore(rules R1–R12),BufferPolicies(B1–B8),Lifetimes(L1–L4),Diagnostics,CodegenContract(C1–C4),Grammar,CLI.tests/test_spec.py— a conformance pilot: every normative rule fires on acanonical example (22/22), so spec and checker can't drift.
docs/proposals/— RFCs for what is not built (P-001 extractor, P-002verification backend, P-003 visualization), each with explicit non-goals.
4. P-001 v0 — real C# → OwnIR → core
First time the tool bites real C#, along the recommended seam (one checker, not
two):
frontend/roslyn/OwnSharp.Extractor(syntax-only Roslyn) scans.csforevent += handlerwith no matching-=, emits OwnIR facts (JSON).ownlang/ownir.py+python -m ownlang ownir facts.jsonlowers facts to asynthetic
.ownsketch, runs the existing core, and reports OWN001 at theC# location with the subscription-token tag.
wpf-extractorruns the whole chain on sample.cs(dotnet is CI-only;the Python bridge is tested locally in
tests/test_ownir.py).Verification
Gate + suite green:
ruffclean ·mypy --strictclean · analysis 123/123 · codegen 23/23 ·property fuzz 3000 · gallery 10/10 · corpus 2/2 · wpf 3/3 · lifetimes 10/10 ·
spec 22/22 · ownir 5/5. The golden ArrayPool lowering and the new C# extractor
chain are compiled/run in CI.
Honesty / scope
corpus/wpfand the extractor samples are real-shaped but small; the extractoris syntactic (heuristic, no semantic event resolution) and CI-only. Formal
soundness (Boogie/Dafny) and the broader C# patterns are tracked as proposals,
not claimed here.
https://claude.ai/code/session_01GbuRjHJERQsNaBWNibxFHv
Generated by Claude Code
Summary by CodeRabbit
Release Notes
New Features
ownlang ownircommand to check event-subscription facts bridged from C#.ownsharp-extractC# syntax extractor to emit JSON facts for CI validation.Documentation
Tests / Quality