feat(cache): deepen cross-file ownership summaries (#20)#26
Merged
Conversation
Two cross-file ownership increments. Types already resolve whole-project
via the global index; this deepens the behavioral OWNERSHIP summaries
that were shallow.
1. Cross-file may_free_fields. filterMayFreeFields previously dropped any
field whose type lived in another file (the `ns != null` bail), losing
the common composed-owner shape `self.inner.deinit()` where `inner`'s
type is imported. It now resolves the cleanup method cross-file via
summaryByMethodCrossFile, mirroring the same-file keep-logic exactly
(keep iff callee takes ownership OR method is cleanup-named, only after
confirming the method exists across imports). Sound: unresolvable -> drop.
2. Cross-file TRANSITIVE takes_ownership_of. summaryByMethodCrossFile did
direct-takes inference only. It now also resolves transitive takes
WITHIN the foreign file: a method that delegates ownership to a
same-file callee (`pub fn kill(self,g){ self.reallyFree(g); }`) is now
seen as taking ownership. Implemented by running the foreign file's own
same-file fixed point over a throwaway PROJECT-LESS FileCache — being
project-less is the termination guarantee (its own cross-file fallback
is a no-op, so no mutual recursion). Bounded to one foreign file; a
second file hop is future work. resolveTransitiveTakes gets an explicit
anyerror!void to break the inferred-error-set cycle the re-entry creates.
Tests: deterministic same-file keep/drop regressions for the
filterMayFreeFields refactor; a deterministic project-less-transitive-takes
mechanism test; and best-effort cross-file integration tests (tmp-path
resolution is environment-dependent — findMethodCrossFile only falls back
to the @import traversal on a global-index error — so they assert only when
resolution fires, consistent with the existing cross-file tests here).
Refs #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two cross-file ownership summary increments for #20. (Note: the issue title's "1 import hop" is stale for type resolution — types already resolve whole-project via the global index. The real walls were in behavioral ownership summaries, which this PR deepens.)
1. Cross-file
may_free_fieldsfilterMayFreeFieldspreviously dropped any field whose type lived in another file (thens != nullbail), losing the common composed-owner shapeself.inner.deinit()whereinner's type is imported. It now resolves the cleanup method cross-file viasummaryByMethodCrossFile, mirroring the same-file keep-logic exactly: keep iff the callee takes ownership OR the method is cleanup-named, and only after confirming the method exists across imports. Feeds the cleanup / double-free / missing-deinit / asymmetric-field-free rule families.2. Cross-file transitive
takes_ownership_ofsummaryByMethodCrossFiledid direct-takes inference only. It now also resolves transitive takes within the foreign file — a method that delegates ownership to a same-file callee (pub fn kill(self,g){ self.reallyFree(g); }) is now seen as taking ownership. Implemented by running the foreign file's own same-file fixed point over a throwaway project-lessFileCache; being project-less is the termination guarantee (its own cross-file fallback is a no-op, so no mutual recursion). Bounded to one foreign file — a second file hop is future work.resolveTransitiveTakesgains an explicitanyerror!voidto break the inferred-error-set cycle the re-entry introduces.Soundness
Conservative by construction: unresolvable callee → field dropped / takes stays null (no false positive). Symmetric with the long-standing same-file logic.
Testing — and an honest caveat
may_free_fieldskeep + drop regressions (guard the refactor); a project-less-transitive-takes mechanism test (locks the invariant increment 2 relies on). Verified meaningful by neutering (the same-file cross-file test fails when the fix is removed).findMethodCrossFileonly falls back to the@importtraversal when the global index errors, so a tmp file under.zig-cache(skipped by the project walk) may not resolve. This matches the existing best-effort cross-file tests in this file. In real multi-file analysis (dogfooding bun/ghostty) cross-file resolution works; the gap is the unit-test harness, which is itself a Inter-procedural analysis bounded to 1 import hop / private free fns #20-adjacent limitation.zig build+ fullzig build testgreen.Refs #20 (does not close it — named-module resolution and cross-file effect propagation remain).