Skip to content

feat(cache): deepen cross-file ownership summaries (#20)#26

Merged
ericsssan merged 1 commit into
mainfrom
feat/crossfile-may-free-fields
Jun 11, 2026
Merged

feat(cache): deepen cross-file ownership summaries (#20)#26
ericsssan merged 1 commit into
mainfrom
feat/crossfile-may-free-fields

Conversation

@ericsssan

Copy link
Copy Markdown
Owner

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_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 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_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 gains an explicit anyerror!void to 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

  • Deterministic: same-file may_free_fields keep + 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).
  • Best-effort: the end-to-end cross-file integration tests assert only when resolution fires. Cross-file resolution in the tmp test harness is environment-dependent — findMethodCrossFile only falls back to the @import traversal 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 + full zig build test green.

Refs #20 (does not close it — named-module resolution and cross-file effect propagation remain).

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
@ericsssan
ericsssan merged commit 8e02dfd into main Jun 11, 2026
1 check passed
@ericsssan
ericsssan deleted the feat/crossfile-may-free-fields branch June 11, 2026 13:17
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.

1 participant