Class RetainReleaseBalanceVerifier

java.lang.Object
org.ek9lang.compiler.phase9.RetainReleaseBalanceVerifier

final class RetainReleaseBalanceVerifier extends Object
THE backend-independent verifier of the ARC RETAIN/RELEASE/SCOPE balance invariant for the +1 ownership-transfer convention. Use this — do NOT re-derive a scope-balance walk elsewhere.

Once main is on the +1 convention it has no native backend and no runtime leak gate, so nothing else structurally checks that a RETAIN was dropped correctly during IR generation. The JVM backend treats RETAIN/RELEASE as no-ops and SCOPE_* as debug-only, so a mis-balanced scope compiles and runs fine on the JVM yet is a native leak or use-after-free. This verifier makes the +1 invariant STRUCTURAL and backend-independent: it runs at compile time on every construct (not just corpus runtime paths), so "if it compiles, it's good" extends to ARC balance.

Invariant checked (report-only for now — see IRAnalysis): within a single method body, a scope that is SCOPE_EXIT'd or SCOPE_REGISTER'd into must first be SCOPE_ENTER'd, and a scope that is entered must be exited on at least one path. These are branch-independent: an orphan exit is a double-release and an enter with no exit anywhere is an outright leak. Deliberately NOT flagged is SCOPE_ENTER count != SCOPE_EXIT count — a method with multiple return paths correctly emits one SCOPE_EXIT per path for a single SCOPE_ENTER (e.g. _isSet enters once and exits on both the return-false and return-true branches), so counts diverge legitimately and only one exit runs on any path. Detecting the true per-path leak (a scope not exited on SOME path) needs control-flow-graph path analysis and is the prerequisite for promoting this phase to enforcing.

The retain heuristic (a RETAIN of a non-return, non-field temp with no matching SCOPE_REGISTER) is reported as informational only — it flags likely leaks but has known false-positive shapes (escaping results, argument ownership transfer) that the report-only corpus run is meant to characterise before any promotion to enforcing.

Collection recurses into the two nested-block carriers (ControlFlowChainInstr guard/condition/ body/default/try/finally lists and ForRangePolymorphicInstr init/body/dispatch lists) so a scope entered in one nested list and exited in another still balances. The return variable is always rtn (ownership transferred to the caller) and field paths contain '.' (store-site release/retain pair) — both are excluded from the retain heuristic.