# C# Language Design Meeting for April 29th, 2026

## Agenda

- [Unsafe evolution](#unsafe-evolution)
    - [Finer-grained opt-in](#finer-grained-opt-in)
    - [More gradual opt-in](#more-gradual-opt-in)
    - [Members with pointers in their signature](#members-with-pointers-in-their-signature)

## Quote of the Day

- "What happens in `unsafe` stays in `unsafe`"

## Discussion

### Unsafe evolution

Champion issue: https://github.com/dotnet/csharplang/issues/9704  
Spec: https://github.com/dotnet/csharplang/blob/a9445cbaa27a5b8569567fc35940bf1ea6923c65/proposals/unsafe-evolution.md  
Working group notes: https://github.com/dotnet/csharplang/blob/a9445cbaa27a5b8569567fc35940bf1ea6923c65/meetings/working-groups/unsafe-evolution/unsafe-simple-core-model.md

We continued our work on the unsafe evolution proposal, picking up several remaining open questions
about the opt-in mechanism and continuing the discussion of members with pointers in their signature
that we started in our [previous meeting](./LDM-2026-04-22.md#unsafe-evolution).

#### Finer-grained opt-in

https://github.com/dotnet/csharplang/blob/a9445cbaa27a5b8569567fc35940bf1ea6923c65/meetings/working-groups/unsafe-evolution/unsafe-simple-core-model.md#finer-grained-opt-in

The first question was whether to provide a fine-grained, region-based opt-in mechanism analogous to
the one we built for nullable reference types, where users can use directives to enable the feature
for specific regions of source code. The argument for nullable was that adopting it across vast code
bases benefits from the ability to gradually annotate file-by-file or region-by-region.

For unsafe evolution, however, there is no evidence that users will need this level of granularity.
The existing ability to add `unsafe` to individual members and to nest `unsafe` blocks already
provides plenty of flexibility for incremental adoption. If concrete user scenarios show that a
finer-grained opt-in is needed, we can add it later without disruption.

##### Conclusion

No fine-grained, region-based opt-in mechanism. The project-wide opt-in is sufficient for now.

#### More gradual opt-in

https://github.com/dotnet/csharplang/blob/a9445cbaa27a5b8569567fc35940bf1ea6923c65/meetings/working-groups/unsafe-evolution/unsafe-simple-core-model.md#more-gradual-opt-in

A separate, more interesting question is whether the project-wide opt-in should be more than just a
binary choice between full compat mode and full opt-in. Today, opting in gives you two things at
once: enforcement of the unsafe rules in your own code, and a signal published to your consumers
(via an assembly-level attribute) that your annotations are intentional. There may be users who want
to start getting the enforcement diagnostics as they annotate, without being ready to publish that
they have fully annotated their assembly.

A workflow-oriented suggestion was that the diagnostics could be graduated: a "middle" opt-in level
would surface the unsafe diagnostics as warnings, and the full opt-in would promote them to errors.
This mirrors the way large code bases typically attack a "treat warnings as errors" goal: drive the
warning count down first, then flip the switch. The counter-argument is that "treat warnings as
errors" is itself a common build setting, so a persistent warning-only mode does not actually let
users keep this setting in their project file during the transition. An analyzer was also raised as
an alternative, but felt duplicative given that the compiler is already doing the analysis.

##### Conclusion

A middle option may eventually be useful, but it is low priority and should not block hitting a
preview deadline. We expect to benefit from real user scenarios when designing it. The implementer
has the discretion to defer it.

#### Members with pointers in their signature

https://github.com/dotnet/csharplang/blob/a9445cbaa27a5b8569567fc35940bf1ea6923c65/meetings/working-groups/unsafe-evolution/unsafe-simple-core-model.md#members-with-pointers-in-their-signature

Last time we decided that when neither the caller nor the callee is opted in, calling a member that
has pointers in its signature should still require an `unsafe` context, even though pointers
themselves are unconditionally usable outside `unsafe` in the new world. This is an ad-hoc rule
designed to prevent a "dip" in the developer experience where calls that are required to be in
`unsafe` blocks today would temporarily not need to be, only to be forced back in once the rest of
the world opts in.

We did not decide what severity that diagnostic should have, and we did not consider the other
combinations of opt-in. The case where both sides are opted in is just the new feature working
normally and needs no special treatment. That leaves the two mixed cases.

##### Non-opted-in caller, opted-in callee

In this case the callee is opted in, has pointers in its signature, and is not marked `unsafe`,
which means the library author has explicitly vouched that the member is safe to call. The caller is
not opted in, which by default means the caller does not heed the callee's annotations at all.

Following our previous decision strictly would mean we still require the call to be inside an
`unsafe` context just because there is a pointer in the signature. That flips the usual relationship
where the non-opted-in world is more lenient than the opted-in world: it would mean that opting in
removes errors that the non-opted-in code suffers from. We were uncomfortable with that inversion,
because it discourages the natural workflow of annotating first and then opting in. Since the callee
has vouched for the safety of the call, there is also no "dip" to protect against in this case.

A counter-perspective was that the non-opted-in caller really is the old language as far as it is
concerned, so it should uniformly apply the old-world view and require the `unsafe` context anyway.
That view, however, was less popular.

###### Conclusion

When the callee is opted in and the member with pointers is not marked `unsafe`, the caller does not
need to be in an `unsafe` context, even when the caller is not opted in.

##### Opted-in caller, non-opted-in callee

This is the "compat-mode" case from the spec. The caller has signaled that they care about the new
unsafe model, but the callee is in the old world and has not annotated anything. The original
proposal is to use the presence of pointers in the callee's signature as a heuristic to treat such
members as caller-unsafe. Since the caller is opted in, this naturally produces an error outside of
an `unsafe` context.

The discussion then explored whether we should expand the heuristic to catch more obviously unsafe
APIs in non-opted-in libraries. A brief analysis of `System.Private.CoreLib` showed that 75–80% of
signatures containing `IntPtr`/`UIntPtr` are also caller-unsafe (with `System.Numerics` skewing the
"safe" side of the ratio). Other suggestions included flagging `extern`/`DllImport` members. A
related idea was to give the user fine-grained diagnostic IDs per heuristic so they could turn off
just the noisy ones (for example the `IntPtr` heuristic when working in numerics-heavy code) without
disabling all of them.

There was significant pushback on heuristics beyond pointers. `nint` and `nuint` are routinely used
to represent integers, particularly in machine learning code, and 64-bit integer types are commonly
used as handles, especially on Linux. Treating those as "looks unsafe" based on a vibe would create
a lot of false positives in code that has historically not been considered unsafe at all. By
contrast, pointers in a signature have always implied unsafety, so flagging them does not violate
existing user expectations.

A different alternative was a single blanket warning when an opted-in assembly references any
non-opted-in assembly, telling the user that their reference is not annotated and recommending they
upgrade. This would let the user suppress the warning per reference and otherwise leave call-site
diagnostics alone (other than the existing pointer rule). The concern raised here was that this
warning would essentially always fire in the early days of the feature, and that it would be too
noisy in pure-IL projects with no P/Invokes, no interop, and no `unsafe` at all. People expect they
would just immediately suppress it and never re-engage with it.

A separate observation in favor of the existing pointer rule (regardless of whether we expand the
heuristic) was that having opted-in callers flag pointer-bearing members as caller-unsafe gives
library authors a smoother transition: when they later opt in and explicitly mark such members
`unsafe`, their callers see the obligation get tighter or stay the same rather than experiencing a
new break.

###### Conclusion

The set of members treated as caller-unsafe in the opted-in caller / non-opted-in callee case is the
same as the set we already singled out for the non-opted-in / non-opted-in case: members with
pointers in their signature. The language will not introduce additional heuristics (such as `nint`,
`nuint`, or `DllImport`); other signals of unsafety can be discovered by analyzers if needed.

There will not be a blanket warning when an opted-in assembly references a non-opted-in assembly.

##### Severity of these diagnostics

For the opted-in caller, the diagnostic is just the normal caller-unsafe error, which is already an
error outside of an `unsafe` context.

For the non-opted-in / non-opted-in case, the question was whether to make the new "members with
pointers must be called inside `unsafe`" rule a warning or an error. Since the way to address the
diagnostic is simply to wrap the call in an `unsafe` block, an error is appropriate and consistent
with how callers handle the same kinds of members today.

###### Conclusion

All of the decided-on diagnostics in this area are errors. When the caller is opted in and the
callee is not, members with pointers in their signature are simply considered caller-unsafe, which
naturally produces the error.
