Class NDScopedCoveredAttribute
- Namespace
- NumSharp
- Assembly
- NumSharp.dll
Marks a method (or property accessor) as covered by an ambient NDScope
opened by its caller — a [NDScoped] OR [NDScopedAsync] boundary (or a
hand-written scope) — so its NDArray temporaries are reclaimed by that
caller's scope, and the NDW012 leak analyzer must treat the method as covered
instead of flagging its transients.
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class NDScopedCoveredAttribute : Attribute
- Inheritance
-
NDScopedCoveredAttribute
- Inherited Members
Remarks
What it is — and is NOT. Unlike NDScopedAttribute /
NDScopedAsyncAttribute, this attribute is NOT a scope boundary: the IL
weaver never weaves it, no NDScope is opened here, and it is completely inert at
runtime. It exists ONLY to inform the compile-time leak analyzer
(NumSharp.Build.Analyzer): NDW012 is a per-method dataflow pass with no
call-graph, so it cannot see that a helper's transients are reclaimed by a
[NDScoped]/[NDScopedAsync] (or hand-scoped) CALLER's ambient scope — the
documented "scope the boundary, helpers ride the ambient scope" pattern
(DISPOSAL-GUIDELINES.md). This attribute is the author's assertion of exactly that,
so the analyzer exempts the method.
The coverage contract the author asserts. Because NDScope tracks
every array constructed while a scope is open on the current thread (the constructor funnel
NDScope.Track), a helper is genuinely covered iff EVERY call path that reaches it does
so synchronously, on the same thread, while an ambient scope is open. Marking a method
[NDScopedCovered] asserts that invariant holds — typically because its only callers are
[NDScoped]/[NDScopedAsync] boundary methods (or other [NDScopedCovered]s
below them). If the method is ever invoked WITHOUT an ambient scope (a public entry point that
was not scoped, a call from a lambda/Task.Run that runs after the scope closed,
another thread), its temporaries fall back to the finalizer backstop — a real leak the analyzer
will no longer report. The assertion is the author's responsibility, the same way a wrong
hand-written using is.
Async boundaries ([NDScopedAsync]) are covered too. An
[NDScopedAsync] method holds ONE invocation scope across its awaits (the
state-machine seam re-installs it on the resuming thread at every MoveNext), so a helper
it calls synchronously within a segment allocates under that installed scope and is
reclaimed exactly as under a synchronous [NDScoped] caller — this attribute applies
identically to both. Two async-specific caveats follow from the seam and are the author's to
honour: (1) the scope is UNINSTALLED across an await (suspended before the continuation
is scheduled), so a covered helper that is ITSELF async only rides the caller's scope for
the work it does BEFORE its first await — allocations after an await (possibly on another
thread) are not tracked; and (2) an awaited callee keeps the caller's tracked temps alive until
the async method completes, which is the intended behaviour, not a leak. Mark a helper
[NDScopedCovered] when it does its NDArray allocation synchronously under the boundary.
Why not just mark it [NDScoped]? That also silences the analyzer, but it
WEAVES a nested scope into the helper (a per-call Open() +
Returns<T>(T)). Nested scopes compose correctly, but they are not free —
for a hot helper called under a boundary that already owns a scope, the nested scope is pure
overhead. [NDScopedCovered] is the zero-runtime-cost choice when the caller's scope
already does the reclamation.
No weaver, no NDW013. The weaver collects targets by the exact type names
NumSharp.NDScopedAttribute / NumSharp.NDScopedAsyncAttribute, so this attribute
is never a weave target and never a target-gate (NDW002–011) subject. The
"you used [NDScoped] but the weaver is absent" guard (NDW013) and the weaver's own
usage pre-scan match those two names precisely (not the shared NDScoped prefix), so a
project using only [NDScopedCovered] — which needs no weaver — draws neither.
Inheritance, and the opt-out role. Like the two scope attributes this one is
inherited along the override/implementation chain: a virtual/abstract/interface member marked
[NDScopedCovered] asserts coverage for every override too. And because the nearest
scope-family attribute up the chain wins, placing it ON an override whose base declaration is
[NDScoped]/[NDScopedAsync] is the way to opt that override OUT of the inherited
weave — the override then rides its caller's ambient scope, on the same author's-assertion
terms as any covered helper.
PUBLIC for the same reason as the scope attributes: a consumer that adopts the
[NDScoped] pattern can annotate its own always-ambient helpers to keep its build clean.