Handles across module boundaries
The intra-function ARIA-032 rule
(lifetimes) was extended across function
and module boundaries by the v0.30.x cycle. This chapter is
the user-facing summary of what flows through use, what
shape the warnings take, and how library authors annotate
FFI ownership when there is no Nitpick body to scan.
TL;DR
use "x.npk".*;brings everypubfunction's borrow summary along with the function itself. The summary carries which parameters get destroyed, which parameter arenas escape through the return value, and (for$$ireturn-borrows) which parameter the borrow comes from.- Cross-module ARIA-032 cases (a callee in another module destroys an arena, or escapes a local arena into a returned handle) are warnings, not errors, for one cycle. Intra-function cases stay errors.
- Externs that have no Nitpick body annotate their FFI
ownership with
#[destroys_arena(<param>)]— see ffi.md. - No sidecar files. Summaries flow in memory through the already-loaded module ASTs; nothing to manage on disk.
What flows through use
Every pub func: declaration in an imported module
contributes a FunctionBorrowSummary to the borrow
checker. The fields the checker reads cross-module are:
| Field | Source | Consumer site |
|---|---|---|
destroys_param_indices |
body scan | direct call: caller's arena marked destroyed (error) |
transitively_destroys_param_indices |
fixpoint over callees | wrapper call: caller's arena marked destroyed (warning) |
destroys_attribute_param_indices |
#[destroys_arena] |
direct call: caller's arena marked destroyed (error) |
escapes_param_arena_indices |
return-path scan + fixpoint | caller binding: handle marked as escaping caller's local arena (warning) |
return_borrow_source_param |
single-borrow heuristic | $$i binding inherits the source path (no diagnostic until misuse) |
is_extern |
declaration form | extern-passthrough warning at call site |
The summary is computed once per module, when the module is loaded. Cross-module call sites read it identically to intra-module call sites; the only visible difference is the diagnostic severity (warning, not error, per IPC-DEC-004).
Cross-module destroy
// helper.npk
use "handle.npk".*;
pub func:wipe = void(int64:a) {
raw HandleArena.destroy(a);
};
// main.npk
use "handle.npk".*;
use "helper.npk".*;
func:main = int32() {
int64:a = raw HandleArena.create();
Handle<int32>:h = raw HandleArena.alloc(a, 4i64);
raw wipe(a); // imported callee destroys a
int64:p = raw HandleArena.deref(h); // [ARIA-032] warning
exit 0;
};
warning[ARIA-032]: Handle 'h' outlives its arena 'a'. The arena
was destroyed transitively through a callee imported from
'helper.npk'. This will become an error in a future release.
Before v0.30.3 this case was silent — the imported
wipe had no summary at the call site. After v0.30.3 the
summary is seeded into the borrow checker before the main
module is analysed.
Cross-module escape
The return-path scan from v0.30.2 also crosses module boundaries:
// helper.npk
use "handle.npk".*;
pub func:make_handle = Handle<int32>(int64:a) {
pass raw HandleArena.alloc(a, 4i64);
};
// main.npk
use "handle.npk".*;
use "helper.npk".*;
func:bad = Handle<int32>() {
int64:a = raw HandleArena.create();
Handle<int32>:h = raw make_handle(a); // h escapes a's lifetime
pass h; // [ARIA-032] warning
};
The escape is inferred from the imported callee's
summary, which records that parameter index 0's arena leaks
out of make_handle through the return value.
Cross-module $$i return-borrow
$$i return borrows track their source parameter
transparently across use:
// helper.npk
pub func:get_a = $$i int32($$i Pair:s) {
pass s.a;
};
// main.npk
use "helper.npk".*;
func:demo = int32($$m Pair:p) {
$$i int32:y = raw get_a(p); // y borrows from p
p.a = 99i32; // [ARIA-026] — p mutated while y live
pass y;
};
No annotation needed on the importer side; the source- parameter mapping rides along in the summary.
When the imported callee takes two or more borrow
parameters and returns $$i, the single-source heuristic
refuses to guess and emits ARIA-023. Provide an explicit
annotation (when the syntax lands) or restructure to a
single-borrow signature.
#[destroys_arena(<param>)] for externs
Externs have no body, so the body-scan path cannot
populate destroys_param_indices. Declarative annotation
fills the gap. See ffi.md for the surface
syntax and self-hosted use; the cross-module half is the
same: an imported extern's attribute flows through the
summary and fires ARIA-032 at any call site that passes a
locally-bound arena.
Local re-declaration wins
The pre-v0.30.3 workaround — locally re-declaring an
imported extern to force the borrow checker to see it — is
no longer required, but it still works. The borrow
checker resolves summaries by function name; the main
module's collectFunctionSummaries runs after
seedImportedSummaries, so a local declaration of the same
name overwrites the imported summary. No duplicate-extern
error, no doubled diagnostic.
Legacy code that carries the workaround compiles
identically. bug321 is the regression test that pins
this contract.
bug277 originally needed the workaround; its v0.30.3
version no longer does.
What does not flow through use
Rules<T>tables. Still file-scoped; unchanged from v0.21.5.- Trait method
$$ireturn signatures. The parser currently rejects$$iintrait:method signatures. When the parser allows it, theimplsummary path already supports it. - The
.npksummarysidecar. IPC-DEC-003 deferred the on-disk format. The current implementation walks the in-memory ASTs thatModuleLoaderalready holds after the type-check pass. This is sufficient until incremental compilation lands.
Warning vs error — the migration window
Per IPC-DEC-004, every newly-detected case introduced in
v0.30.x ships as a warning for one cycle. The intra-
function rule (the original v0.27.9 ARIA-032) stays an
error. The plan is to promote the cross-module and
transitive cases to errors in a later cycle once the
false-positive rate is known. v0.30.6 already pruned the
two known FP shapes (reassign-after-destroy,
destroy-in-exiting-branch).
If you want to treat the warnings as errors today, the
project's diagnostic filter machinery accepts a warning
code list — promote ARIA-032 there.
Validation
| Bug | What it pins |
|---|---|
bug311–bug315 |
v0.30.1 transitive destroy (depth 2/3, fan-out, recursion) |
bug316–bug320 |
v0.30.2 transitive escape (direct, depth 2, local binding) |
bug321 |
v0.30.3 local extern re-decl still shadows imported summary |
bug277 |
v0.30.3 cross-module FFI passthrough warns without workaround |
bug322–bug325 |
v0.30.4 #[destroys_arena] attribute on extern + wrapper |
bug326–bug329 |
v0.30.5 cross-module $$i return-borrow source tracking |
bug330–bug333 |
v0.30.6 precision (reassign / branch-exit FP fixes) |
See also
- Lifetimes — the intra-function rule and its v0.28.x cross-function extensions.
- FFI — the
#[destroys_arena]attribute and the@cast<int64>escape hatch. memory/diagnostics.md#nitpick-032— the full diagnostic reference, including the v0.30.x growth.modules/use_import.md— the surfaceusesyntax that triggers summary ingest.