Skip to content

P-016 A1: loop support in the core (worklist fixpoint over back-edges)#17

Merged
PhysShell merged 2 commits into
mainfrom
claude/zen-pasteur-76hfs1
Jun 16, 2026
Merged

P-016 A1: loop support in the core (worklist fixpoint over back-edges)#17
PhysShell merged 2 commits into
mainfrom
claude/zen-pasteur-76hfs1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 16, 2026

Copy link
Copy Markdown
Owner

P-016 A1 — loops in the core

Replaces the single topological pass (which excluded any block in a cycle) with a forward worklist to a fixpoint, so the core now analyses while loops instead of rejecting them as OWN020. Builds on #16 (GTM UnitOfWork pin + never/every-path wording split + Option B ownership-transfer sample), which already landed in main.

Why it converges

The per-symbol lattice {OWNED, MOVED, RELEASED, ESCAPED} is finite (height 4) and union-merged at joins; the transfer is monotone, so iterating until no out-state changes terminates — no widening needed. On a loop-free CFG it reduces to one pass per block, so loop-free behaviour is identical to the old topological walk (and __main__ sorts diagnostics by (line, code), so emission order is unchanged).

What changed

  • lexer / AST / parser: while (cond) { body } (condition opaque, like if); while graduates out of REJECTED_KEYWORDSfor / loop / async stay OWN020.
  • cfg: lower_while emits a header block with a back-edge from the body exit (the CFG may now contain cycles).
  • analysis: two-phase run — a silent fixpoint converges the in-states (a looped block is transferred many times), then one emitting pass reports diagnostics on the converged state, so there are no per-iteration duplicates. Borrows stay block-scoped (a loan opened in a loop body closes within the same iteration), so the "loans equal at every merge" invariant holds across back-edges too.
  • codegen / report / lifetimes: handle / descend into While (keeps mypy --strict exhaustive and avoids dropping moves / buffers / subscribes inside a loop body).

What it now catches

Cross-iteration faults a single pass cannot see:

fn f(n: int){ let c = acquire Conn(1); while (n) { use c; release c; } }
→ OWN001 (leak on the 0-trip path) + OWN003 (double-release on turn 2) + OWN009 (use after last turn's release)

Balanced acquire/release — and a borrow that opens and closes within the body — stay clean (no false positive).

Tests / docs

  • tests/test_loops.py (new): exact code-sets for clean / leak / cross-iteration / nested loops + regression guard that for/async still raise OWN020.
  • examples/gallery/10_leak_in_loop.own (new) wired into test_gallery.
  • tests/run_tests.py CASES: clean / leak / cross-iteration double-release.
  • spec/OwnCore.md, README.md, docs/proposals/P-016 updated (A1 marked done).

Full suite green locally (analysis 125/125, loops 20/20, gallery 11/11, fuzz 3000 clean, spec 22/22, ownir 45/45) and ruff check . clean. mypy --strict not run locally (not installed) — verified by hand that both assert_never sites stay exhaustive; CI confirms.

Follow-ons (not in this PR)

The flow extractor still bails on loop bodies, so loopy C# methods are honestly skipped at the frontend — lowering while to back-edge flow facts is the natural Track-B next step now that the core handles loops.

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • while loops are now supported end-to-end, including correct resource/ownership checking across loop iterations.
    • Ownership diagnostics for loop control-flow now stabilize before being reported, improving consistency.
  • Documentation

    • Updated README, proposal docs, and the OwnCore spec to reflect revised while semantics and the updated support/rejection matrix (with for/loop/async still rejected).
  • Tests

    • Added a dedicated while regression suite and a new gallery example covering loop leak scenarios and expected error codes.

…k-edges)

Replace the single topological pass (which excluded any block in a cycle) with a
forward worklist to a fixpoint, so the core analyses `while` loops instead of
rejecting them as OWN020. The per-symbol lattice {OWNED,MOVED,RELEASED,ESCAPED}
is finite and union-merged, the transfer is monotone, so it converges without
widening. On a loop-free CFG it reduces to one pass per block — behaviour is
identical to the old walk (and __main__ sorts diagnostics by (line, code), so
emission order is unchanged).

Key pieces:
- lexer/AST/parser: `while (cond) { body }` (cond opaque like `if`); `while`
  graduates out of REJECTED_KEYWORDS (`for`/`loop`/async stay OWN020).
- cfg: `lower_while` emits a header block with a back-edge from the body exit.
- analysis: two-phase run — a SILENT fixpoint converges the in-states (a looped
  block is transferred many times), then ONE emitting pass reports diagnostics on
  the converged state (so no per-iteration duplicates). Borrows stay block-scoped,
  so the loan-set-equal-at-merge invariant holds across back-edges too.
- codegen/report/lifetimes: handle/descend into `While` (keeps mypy --strict
  exhaustive and avoids dropping moves/buffers/subscribes inside a loop body).

This catches cross-iteration faults a single pass cannot see: a resource released
in a loop is double-released on the next turn (OWN003) and used after release
(OWN009); one acquired each turn and not released leaks (OWN001). Balanced
acquire/release — and a borrow that opens and closes within the body — stay clean.

Pinned by tests/test_loops.py (exact code sets, incl. nested loops), gallery
10_leak_in_loop.own, and run_tests CASES. Spec/README/P-016 updated.

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32775644-d727-45b6-bf3e-9521d1094a12

📥 Commits

Reviewing files that changed from the base of the PR and between d733ee2 and 8f6163e.

📒 Files selected for processing (1)
  • examples/gallery/10_leak_in_loop.own
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/gallery/10_leak_in_loop.own

📝 Walkthrough

Walkthrough

Adds end-to-end while loop support to OwnLang. The frontend gains a Tok.WHILE token, A.While AST node, and Parser.parse_while. The CFG lowerer emits cyclic header/body/after blocks with a back-edge. The ownership analyzer is replaced with a two-phase forward worklist fixpoint that converges silently then emits diagnostics. All downstream passes (codegen, lifetimes, report) are updated to handle A.While, and the regression suite, gallery example, spec, and README are aligned.

Changes

while Loop End-to-End Support

Layer / File(s) Summary
AST node, lexer token, and parser
ownlang/ast_nodes.py, ownlang/lexer.py, ownlang/parser.py
While frozen dataclass added with cond_text/body/line, appended to Stmt union; Tok.WHILE added, "while" moved from REJECTED_KEYWORDS to KEYWORDS; parse_while method added with nested-paren tracking, dispatched from parse_stmt; MVP rejection message updated to name for/loop/async specifically.
CFG while lowering with back-edge
ownlang/cfg.py
lower_while constructs while.header, while.body, while.after blocks, links body-exit back to header (cycle), pushes/pops a borrow scope around the body, and returns the after-block; dispatch in lower_stmt extended; module docstring updated for cyclic CFGs.
Two-phase worklist fixpoint analyzer
ownlang/analysis.py
Single topological traversal replaced by a forward worklist fixpoint; phase 1 iterates silently (new silent flag gates err()) until in-states over {OWNED, MOVED, RELEASED, ESCAPED} converge; phase 2 re-runs transfers to emit diagnostics once; join() updated to assert active-loan-set equality across all merge predecessors including back-edges.
Codegen, lifetimes, and report pass updates
ownlang/codegen.py, ownlang/lifetimes.py, ownlang/report.py
_stmt_inline emits C# while loops; A.While treated as branchy for try/finally selection; buffer-flow, buffer-presence, native-buffer, and buffer-mode helpers all descend into A.While bodies; _iter_subscribes descends into A.While for OWN014; _walk_buffers yields from A.While bodies alongside A.BorrowBlock.
Regression tests and gallery example
tests/test_loops.py, tests/run_tests.py, tests/test_gallery.py, examples/gallery/10_leak_in_loop.own
New test_loops.py covers balanced, leak, nested-leak, double-release, and use-after-release while cases; asserts OWN003/OWN009 cross-iteration detection and no OWN020 for while; guards async/for still yield OWN020. Gallery adds 10_leak_in_loop.own (per-iteration Conn leak). run_tests.py wires test_loops.run() into overall pass/fail.
Spec, proposal, and README documentation updates
spec/OwnCore.md, README.md, docs/proposals/P-016-deep-fact-extraction.md
OwnCore.md describes while as a back-edge with worklist fixpoint convergence, strengthens borrow-merge to an assertion, and narrows OWN020 to for/loop/async. README.md updated across loans model, OWN020 entry, CFA section, OWN010 rationale, "Where it cheats" list, and lexer comment. Proposal updated to reflect completed A1 loop support and remaining Track B flow-extractor gap.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Source,CFGBuilder: Frontend
    Source->>Lexer: "while" keyword
    Lexer->>Parser: Tok.WHILE
    Parser->>Parser: parse_while() — collect cond_text, parse body block
    Parser->>AST: A.While(cond_text, body, line)
  end
  rect rgba(144, 238, 144, 0.5)
    Note over CFGBuilder,Analyzer: Analysis pipeline
    AST->>CFGBuilder: lower_while(A.While)
    CFGBuilder->>CFGBuilder: emit while.header, while.body, while.after
    CFGBuilder->>CFGBuilder: add back-edge body→header
    CFGBuilder-->>Analyzer: cyclic CFG
    Analyzer->>Analyzer: phase 1 — worklist fixpoint (silent), join() asserts loan-set equality at merges
    Analyzer->>Analyzer: phase 2 — re-run transfers, emit OWN001/OWN003/OWN009 diagnostics
  end
  rect rgba(255, 182, 193, 0.5)
    Note over Analyzer,Codegen: Code generation
    Analyzer-->>Codegen: converged ownership states
    Codegen->>Codegen: _stmt_inline emits C# while(...){...}
    Codegen-->>Output: C# source with while loop
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/Own.NET#6: Extends the gallery test harness by adding the 10_leak_in_loop.own entry to tests/test_gallery.py's MANIFEST, which aligns with this PR's creation and validation of that gallery example.

Poem

🐇 Hoppin' through loops, I never get lost,
The worklist converges — no matter the cost!
Back-edges and fixpoints, two phases to find
Each leaked little Conn that slipped from your mind.
for and loop? Rejected, still out of scope!
But while hops along with ownership hope. 🔄

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and clearly summarizes the main change: implementing loop support via worklist fixpoint analysis, which is the primary objective stated in the PR summary.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/zen-pasteur-76hfs1

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
examples/gallery/10_leak_in_loop.own (1)

1-12: 💤 Low value

Minor inconsistency between comment and code pattern.

The comment on line 2 describes the C# analog as while (reader.Read()) (which naturally terminates when the reader is exhausted), but the actual code uses while (n) where n is never modified. While both patterns correctly demonstrate the per-iteration leak (OWN001), the difference might confuse readers about why the loop conditions differ.

📝 Suggested clarification

Consider either:

  1. Matching the code to the comment: while (n > 0) { n = n - 1; ... } (though this adds noise), or
  2. Clarifying the comment: // Real C#: while (condition) { var conn = Open(...); Use(conn); } — the loop body leaks regardless of how the loop terminates.
🤖 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 `@examples/gallery/10_leak_in_loop.own` around lines 1 - 12, The comment on
line 2 describes a C# pattern using `while (reader.Read())` which naturally
terminates, but the actual code in the drain function uses `while (n)` where n
is never modified, creating an infinite loop. To resolve this inconsistency,
update the comment to clarify that the resource leak occurs regardless of the
loop's termination condition. Change the comment to explain that both patterns
(naturally-terminating loops and explicit condition checks) demonstrate the same
OWN001 leak issue—the key point is that a resource is acquired on every
iteration but never released. This makes it clear to readers that the specific
loop condition type is not the focus; the leak mechanism is.
🤖 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.

Nitpick comments:
In `@examples/gallery/10_leak_in_loop.own`:
- Around line 1-12: The comment on line 2 describes a C# pattern using `while
(reader.Read())` which naturally terminates, but the actual code in the drain
function uses `while (n)` where n is never modified, creating an infinite loop.
To resolve this inconsistency, update the comment to clarify that the resource
leak occurs regardless of the loop's termination condition. Change the comment
to explain that both patterns (naturally-terminating loops and explicit
condition checks) demonstrate the same OWN001 leak issue—the key point is that a
resource is acquired on every iteration but never released. This makes it clear
to readers that the specific loop condition type is not the focus; the leak
mechanism is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee1bbf15-117b-44c7-84bd-cc2b49aa7987

📥 Commits

Reviewing files that changed from the base of the PR and between dc47048 and d733ee2.

📒 Files selected for processing (15)
  • README.md
  • docs/proposals/P-016-deep-fact-extraction.md
  • examples/gallery/10_leak_in_loop.own
  • ownlang/analysis.py
  • ownlang/ast_nodes.py
  • ownlang/cfg.py
  • ownlang/codegen.py
  • ownlang/lexer.py
  • ownlang/lifetimes.py
  • ownlang/parser.py
  • ownlang/report.py
  • spec/OwnCore.md
  • tests/run_tests.py
  • tests/test_gallery.py
  • tests/test_loops.py

…he loop condition)

The C# analog cited `while (reader.Read())` (which terminates) while the .own code
is `while (n)` with an opaque condition — address a review nitpick by noting the
condition is opaque to the checker and the per-iteration leak holds regardless of
what ends the loop. Comment-only; the OWN001 verdict is unchanged.

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell
PhysShell merged commit 9962555 into main Jun 16, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants