Flow-Sensitive Consumption Tracking for ref struct
#10203
Unanswered
xtofs
asked this question in
Language Ideas
Replies: 1 comment 7 replies
-
|
This doesn't appear to be a language suggestion. Attributes, with associated analysis rules, have been supported in the Roslyn compiler for over a decade. So the above can already be implemented without any additional support needed. |
Beta Was this translation helpful? Give feedback.
7 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal: Flow-Sensitive Consumption Tracking for
ref structAbstract
This proposal introduces a
[MustConsume]attribute and a companion[Consumes]attribute that together enable the compiler to performflow-sensitive tracking of whether a
ref structvalue has beenconsumed. A value of a type marked
[MustConsume]must be passed toexactly one method parameter marked
[Consumes]before it goes out ofscope. Violations are reported as diagnostics — warnings by default,
errors on opt-in — with no change to code generation or the runtime.
The mechanism is purely a compiler flow analysis extension, in the
same spirit as nullable reference types (
string?) and definiteassignment. It requires no new IL, no runtime support, and no changes to
the GC. It applies exclusively to
ref structtypes, where the GC isalready uninvolved.
Motivation
1. Resource safety
C# already has
IDisposableand theusingstatement to encouragecorrect resource cleanup. However, both are opt-in at the call site:
nothing prevents a caller from simply not participating. The type author
has no way to declare "every caller must close this" and have the
compiler enforce it universally.
This means nothing in the language today prevents:
For
ref structresources specifically — file handles, native memoryhandles, cryptographic contexts — the GC is not involved at all, which
means there is no safety net whatsoever. A
ref structthat wraps anative resource can be silently abandoned or double-closed with no
diagnostic.
This proposal lets the type author declare the invariant once, at the
declaration site, and have the compiler enforce it at every call site
automatically.
2. Typestate / protocol enforcement
The typestate pattern uses phantom type parameters to encode the current
state of an object, making invalid method calls a compile-time error.
For example, a query builder that requires
SelectbeforeWhere.However, typestate in C# today has a critical gap: the old state
reference remains live after a transition, so stale use is not prevented:
Consumption tracking closes this gap. After
Selectconsumesb, anyfurther use of
bis a diagnostic.These two cases — resource safety and protocol enforcement — are
instances of the same underlying need: a value that must be handed off
exactly once.
Proposal
Attributes
Borrowing vs consuming
A method that accepts a
[MustConsume]ref struct without marking theparameter
[Consumes]is a borrow: the caller retains ownership andthe value remains live after the call. Only a
[Consumes]parametertransfers ownership.
This distinction is intentional and important: a method like
ReadLinecan be called many times on the same handle — each call borrows it. Only
Closetakes ownership. The presence or absence of[Consumes]on theparameter is how the method author declares which contract applies.
A future
[Borrows]attribute could make borrows explicit rather thanimplicit, but that is left to a later discussion.
Compiler flow analysis
The compiler extends its existing flow state for local variables with a
third state alongside assigned and unassigned:
[Consumes]parameterTransitions:
[Consumes]parameter.
"Variable 'x' has already been consumed."
"Variable 'x' of type T (marked [MustConsume]) was not consumed."
Example: resource safety
Example: typestate / protocol enforcement
The two mechanisms are complementary: the phantom type parameter
prevents wrong-order calls; consumption tracking prevents stale use of
a superseded state.
Relationship to existing features
Nullable reference types
The closest precedent. Nullable reference types added a flow-sensitive
"maybe null" / "not null" state to the compiler with:
<Nullable>enable</Nullable>)This proposal follows the same pattern exactly, adding a "consumed"
state to the flow graph for
ref structlocals.Definite assignment
The compiler already tracks "assigned" vs "unassigned" for all locals
and reports use of unassigned variables. "Consumed" is a natural third
state in the same framework, symmetric with "unassigned" at the other
end of a value's life.
ref structand the GCref structvalues cannot be boxed, cannot be stored in heap fields,and cannot contain managed references. They are entirely invisible to
the GC. This is precisely why consumption tracking is both safe and
self-contained for this type category: there is no aliasing through the
heap, no finalizer safety net, and no GC involvement to reason about.
The compiler has complete visibility into the lifetime of every
ref structlocal.scopedand lifetime annotationsC# 11 introduced
scopedto restrict the escape of aref structvalue. This proposal is orthogonal:
scopedrestricts where a valuecan go spatially (can't escape the current method);
[MustConsume]restricts how many times it can be used temporally (must be handed off
exactly once).
[DoesNotReturn],[NotNull],[MaybeNull]The
System.Diagnostics.CodeAnalysisnamespace already containsattributes that the compiler treats as semantic modifiers affecting flow
analysis.
[MustConsume]and[Consumes]are a natural addition tothis family.
What this proposal does not address
ref struct. Extending it to heap-allocated types would requirereasoning about aliasing through the heap, which is a substantially
harder problem.
branch but not another, the compiler should report a diagnostic. The
exact rules follow the same conventions as definite assignment: a
value is considered consumed after a branch only if it is consumed on
all paths.
once) combined with a must-consume requirement (at least once),
yielding exactly-once for
[MustConsume]values. It does not providegeneral linear types for arbitrary heap-allocated objects.
[Consumes]could in a future version be surfacedas a parameter modifier keyword (e.g.
consume FileHandle handle),as has been done for
[In]/inand[Out]/out. This is left toan implementation discussion.
Open questions
Severity: should violations be warnings (like nullable) or errors
(like definite assignment)? The nullable precedent suggests warnings
with opt-in errors, which allows incremental adoption.
Conditional consumption: should a
[Consumes]parameter on amethod that throws still count as consumed at the call site? The
conservative answer is yes — the value is transferred before the
callee executes, so the caller no longer owns it regardless of
whether the callee throws.
outparameters: should a method be able to return a[MustConsume]value via anoutparameter, transferring ownershipto the caller?
Interop with
using: should the compiler recognizeusing var h = OpenFile(...)as an implicit consume at the end ofthe
usingblock, satisfying the must-consume requirementautomatically?
References
consumingandborrowingparameter modifiers (SE-0377)Beta Was this translation helpful? Give feedback.
All reactions