← Back to AILP Home

FAQ

Short answers to recurring questions about Drop. For the long form, follow the chapter links.

When should I import drop.npk?

When every binding of the supported region shapes in your program follows simple stack-like lifetime — allocated in a function, freed at scope exit. That's almost every short-lived allocation. If your lifetime model is more complex (long-lived, conditionally freed, transferred across module boundaries in non-trivial ways), keep the explicit-free contract.

RAII vs manual npk_free — which is "the Nitpick way"?

Both are. Drop is opt-in additive; manual free is always available. The compiler does not force a choice. The trade-off:

In the stdlib, most internal helpers stay manual because they already had explicit-free contracts before v0.29.x and the cycle is intentionally non-breaking. New code is encouraged to import drop.npk.

Why two flags for HandleArena vs Handle<T> auto-free?

The RAII layer has separate gates for int64:arena = HandleArena.create(); (handle_arena_raii_enabled, v0.29.5) and Handle<T>:h = HandleArena.alloc(a, N); (handle_raii_enabled, v0.31.5.x). One use "drop.npk".*; import flips both — but the two-flag split (HANDLE-DEC-002) lets the cycle's borrow-checker mirror (local_arenas_ vs local_handles_) stay independent. It also left room for the per-binding opt-out spelling that landed in v0.31.6.x as the nodrop keyword (W-04), which works at the binding granularity and is independent of which tier RAII is enabled for.

The in-source escape hatch is now nodrop at the declaration site:

Handle<int64>:h = nodrop raw HandleArena.alloc(a, 8i64);
//                ^^^^^^ skips the borrow-checker enrolment
//                and the IR-gen recogniser — this binding
//                stays on the explicit-free contract.

Earlier cycles used a bare raw(...) wrapper as a probe; that still works for the IR-gen recogniser but does not disable borrow-checker enrolment, so nodrop is the recommended spelling going forward.

How does NLL (Non-Lexical Lifetimes) Drop work?

Landed in v0.31.7.x — see nll.md for the full cookbook.

The default policy is still lexical: drops fire at scope end, and you can tell by reading the source where each drop runs. Lexical is predictable and remains the recommended default.

When you need a binding to die at its last-use point instead (typically to free a hot-loop scratch resource before a long pure tail), annotate the function with #[nll_drop]. A backward CFG liveness pass retimes that function's auto-emitted drops to fire at the earliest point after which each binding is dead on all outgoing paths. Per-binding opt-out via #[lexical_drop]. See nll.md for the full surface including ARIA-052 (redundant attribute) and the borrow- lifetime tightening this introduces.

If you do not want the attribute and just need a binding to die early, the sub-block idiom still works:

{
    wildx int8->:scratch = wildx_alloc(64i64);
    // ...use scratch...
}   // scratch drops here
// ...continue without scratch...

Why no Drop for primitives?

Primitives (int32, bool, flt64, …) have no resource to release. They live on the stack frame and the frame disappears at function return. There is nothing to dispatch.

Sema rejects impl:Drop:for:int32 with the "type does not have a struct decl" check, by design.

Can a drop method allocate?

It can — the compiler does not statically forbid it — but don't. A drop method that allocates risks running the allocator inside the cleanup of a previous allocation, which in pathological cases recurses through the drop_stack_. The v0.29.x built-in sentinels all have empty bodies (pass;). For user types (surface-only this cycle), keep drop side-effect-free or restricted to releasing other owned resources.

Can a drop method pass or fail?

DROP-DEC-009: destructor failure is undefined behaviour this cycle. pass; (no value) is fine and is what the built-in sentinels use. pass v; and fail e; have unspecified semantics.

Why does Jit.compile_add_i32() return a wildx int8-> without freeing it?

Because of DROP-DEC-004 (bare-identifier move). Inside Jit_compile_add_i32, the function does:

wildx int8->:page = wildx_alloc(64i64);
// ...write bytes, seal...
pass page;   // drop for `page` is SKIPPED, ownership moves to caller

The caller binds the returned page to its own variable, which becomes the new RAII binding (re-recognized via the JitFn shape). Without bare-identifier move, the page would be freed inside Jit_compile_add_i32 and the caller would receive a dangling pointer.

This is also the entire reason any factory function in Nitpick works under RAII.

What's the difference between defer and drop?

Aspect defer Drop
Surface defer { ... } use "drop.npk".*;
Per-binding no — block-scoped yes — one drop per binding
What runs arbitrary user code hand-emitted free for region
Ordering reverse defer order reverse declaration order
Vs. each other runs after Drop runs before defers
Skipped by exit yes yes (DROP-DEC-008)

Use defer for ad-hoc cleanup (closing a file, logging a metric). Use Drop for owned resources in the four supported regions. Mix freely when you want both.

Does Drop replace failsafe?

No. failsafe is the top-level fail-propagation hook in main. Drop runs the destructor at the failing scope; the TBB-error then propagates up. If it bubbles all the way out of the call chain without being captured into a Result, failsafe is the last stop before process exit. They compose.

Why is there a separate flag for each region?

DROP-DEC-007. The four flags (wild_raii_enabled, wildx_raii_enabled, handle_arena_raii_enabled, jit_fn_raii_enabled) are independent internally, even though drop.npk flips all four with one import. This leaves room for a future cycle to ship per-region opt-in (e.g. use "drop_handle.npk".*;) without changing the IR-generator gates.

What happens if I forget use "drop.npk".*;?

Nothing automatic. The bindings work exactly as in v0.29.2 and earlier — you call npk_free / wildx_free / HandleArena.destroy / Jit.free yourself, and ARIA-014 fires at scope exit if you forget.

Does Drop work across FFI?

No. The drop.npk recognizers see only Nitpick-emitted allocator calls. A pointer that crosses an extern boundary is opaque; Drop does not auto-emit cleanup for resources allocated by C code, nor does it interfere with explicit C-side cleanup. The FFI contract is unchanged.

Will Drop ever get NLL / move-for-arbitrary-expressions / per-binding opt-out?

Probably yes, in separate cycles. The v0.29.x cycle deliberately shipped the minimum useful surface and left these gaps:

See META/NITPICK/ROADMAP/0.29/PLAN.md for the v0.29.x scope envelope.

When should I use nodrop vs raw vs drop?

The three wrappers operate at different layers and compose predictably (each one peels the outermost layer):

nodrop is outer-only (NODROP-DEC-011): only the outermost wrapper of an initializer expression is consulted. Composing raw nodrop alloc(N) or nodrop raw HandleArena.alloc(a, N) is the supported shape; an inner nodrop buried under a function call (e.g. identity(nodrop f())) does NOT opt the binding out — this is intentional.

Where do I look for examples?