Drop interaction
Drop (the v0.29.x opt-in RAII layer documented in
guide/drop/) fires correctly in async
function bodies in v0.31.x. This page records what was verified
in v0.31.0.5 (D-10) and what is still deferred.
What works (verified in v0.31.0.5)
(a) Drop on async completion
When an async func: body reaches its closing } via
fall-through, pass v, or return Result{...}, every Drop in
scope fires before the coroutine resolves the promise.
use "drop.npk".*;
struct DropMe { int32:tag; };
impl:Drop:for:DropMe = {
func:drop = NIL($$m DropMe:self) {
println("DROP");
};
};
async func:work = int32() {
wild DropMe:d = DropMe{ tag: 1i32 };
pass 0i32; // Drop of d fires BEFORE the promise resolves
};
Regression bug351: the DROP marker appears in stdout before
run_tests() completes and main's exit 0 runs.
(c) Drop on fail unwind
fail e inside an async body also runs Drops before storing
the error Result<T> to the promise:
async func:falling = int32() {
wild DropMe:d = DropMe{ tag: 2i32 };
fail 7i32; // Drop of d fires, THEN the future resolves to is_error=true
};
Regression bug352: the DROP marker appears in stdout, and
the awaiter sees is_error == true afterwards. The async body
"unwinds" through the Drops the same way a sync function does.
LIFO ordering across nested scopes
Drops walk innermost-out, reverse declaration order — same as
sync. The inner scope's Drops run at the inner } before the
outer scope's Drops run at the outer }:
async func:nested = int32() {
wild DropMe:a = DropMe{ tag: 1i32 };
{
wild DropMe:b = DropMe{ tag: 2i32 };
// inner `}` here → drop(b) fires first
}
// outer `}` here → drop(a) fires
pass 0i32;
};
Regression bug353: stdout shows DROP-TwoB before DROP-TwoA
even though a was declared first. The "TwoB / TwoA" naming in
the fixture is for marker ordering — it confirms the inner-scope
Drop fires at the inner }, not deferred to the outer }.
What is not yet verified: (b) shutdown of a suspended task
The third Drop case from the audit (D-10 (b)) — "what happens to
Drops when the executor shuts down with tasks still suspended
mid-await" — is not reachable from the user surface in
v0.31.x.
The reason: no user-surface awaitable produces real machine-
level suspension. Every await over an async func: call
resolves synchronously (because the awaited coroutine itself
runs to completion in the same npk_executor_run drain before
the awaiter resumes). And await does not accept the runtime's
NitpickFutureHandle — the only thing that would really
suspend.
Until a user-surface real-suspend primitive lands (either an
awaiter bridge to NitpickFutureHandle, or an explicit
yield_now() / sleep primitive), this case cannot be tested
through .npk fixtures. The runtime side also has a known gap:
Executor's destructor does not call __nitpick_coro_destroy on
unfinished tasks, so suspended-task Drops would currently leak
even if the test could be written.
Deferred to a later cycle (Phase 2 network, Phase 1 awaiter bridge slice, or whichever lands first).
exit N still skips Drop
This is unchanged from sync:
async func:bad = int32() {
wild DropMe:d = DropMe{ tag: 1i32 };
exit 1; // hard process exit — d.drop() does NOT run
};
DROP-DEC-008 (exit skips Drops) applies inside async func:
the same way it applies inside func:. If you need
destructors to run, use pass / fail and let the awaiter
handle the result.
The compiler-injected npk_executor_run(NULL) runs before
main's explicit exit N. Any async task that completes
during that drain will run its Drops. Any task still
suspended at the start of exit is abandoned (the (b) case
above).
Move semantics across pass
The DROP-DEC-004 bare-identifier move applies in async bodies the same way:
async func:hand_off = int32() {
wild DropMe:d = DropMe{ tag: 1i32 };
pass d.tag; // d.tag is a field expr, not a bare ident → d is dropped
};
async func:hand_off_bare = wild DropMe() {
wild DropMe:d = DropMe{ tag: 1i32 };
pass d; // bare ident → ownership moves to caller, Drop skipped on d
};
The async lowering does not change the move rules. The
Result<T> that gets stored to the promise wraps the moved-out
value the same way it wraps a regular pass return.
Drops vs defer
Defers fire after Drops, same as sync (DROP-DEC-010). The
suspension-point question — "do defer blocks survive an
await?" — has the same answer as the borrow question: the
defer itself survives (it's part of the coroutine frame),
but any borrows it captures must follow the ARIA-041 rule.
See borrow.md.
See also
guide/drop/— the full Drop / RAII cookbook (v0.29.x). Everything in that document still applies insideasync func:bodies.guide/drop/ordering.md— DROP-DEC-003 (reverse declaration), DROP-DEC-008 (exitskips), and DROP-DEC-010 (drops before defers) — all hold in async too.