Skip to content

Unsafe evolution: unsafe expressions - #84012

Merged
jjonescz merged 14 commits into
dotnet:mainfrom
jjonescz:Unsafe-39-UnsafeExpressions
Jun 16, 2026
Merged

jjonescz merged 14 commits into
dotnet:mainfrom
jjonescz:Unsafe-39-UnsafeExpressions

Conversation

@jjonescz

@jjonescz jjonescz commented Jun 4, 2026 •

Copy link
Copy Markdown
Member

Test plan: #81207
Speclet update: dotnet/csharplang#10196

Microsoft Reviewers: Open in CodeFlow

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

This PR modifies public API files. Please follow the instructions at https://github.com/dotnet/roslyn/blob/main/docs/contributing/API%20Review%20Process.md for ensuring all public APIs are reviewed before merging.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds C# parser/binder and public syntax model support for a new primary-function form unsafe(<expr>) (Unsafe Evolution), including updates to syntax generation outputs and extensive parsing/semantic tests.

Changes:

  • Introduces SyntaxKind.UnsafeExpression / UnsafeExpressionSyntax, parsing (LanguageParser.ParseUnsafeExpression) and binding (BindUnsafeExpression + unsafe-region binder flags).
  • Updates semantic model/binder mapping utilities to treat unsafe(<expr>) like other parenthesized constructs for binder association and “bindable node” selection.
  • Adds parsing tests, await-parsing coverage, and CSharp15 Unsafe Evolution semantic tests (diagnostics, constant value, IOperation, flow graph, IL).

Reviewed changes

Copilot reviewed 13 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs Adds parse-tree coverage for unsafe(<expr>) and common postfix forms.
src/Compilers/CSharp/Test/Syntax/Parsing/AwaitParsingTests.cs Ensures await unsafe(...) parses as AwaitExpression over UnsafeExpression.
src/Compilers/CSharp/Test/Syntax/Generated/Syntax.Test.xml.Generated.cs Adds generated factory/rewriter tests for UnsafeExpressionSyntax.
src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs Adds end-to-end tests for diagnostics and semantics of unsafe(<expr>) under updated memory safety rules.
src/Compilers/CSharp/Portable/Syntax/SyntaxNodeExtensions.cs Allows UnsafeExpression to have an associated local binder.
src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs Maps UnsafeKeyword as a “primary function” keyword to UnsafeExpression.
src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs Introduces SyntaxKind.UnsafeExpression (experimental).
src/Compilers/CSharp/Portable/Syntax/Syntax.xml Defines UnsafeExpressionSyntax node in the syntax model generator input.
src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt Tracks new public API surface for UnsafeExpressionSyntax and visitors/factory methods.
src/Compilers/CSharp/Portable/Parser/LanguageParser.cs Parses unsafe(<expr>) and disambiguates statement vs expression uses.
src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Syntax.Generated.cs Generated red node for UnsafeExpressionSyntax.
src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs Generated visitors/rewriter + SyntaxFactory.UnsafeExpression API.
src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs Generated green node + internal factory support for UnsafeExpression.
src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4 Updates grammar to include unsafe_expression in expressions.
src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs Treats UnsafeExpressionSyntax similarly to other parenthesized constructs for binder lookup and binding selection.
src/Compilers/CSharp/Portable/Binder/LocalBinderFactory.cs Creates an unsafe-region binder for UnsafeExpressionSyntax.
src/Compilers/CSharp/Portable/Binder/EarlyWellKnownAttributeBinder.cs Allows UnsafeExpression as a syntactically valid attribute-argument form (rebinding later produces diagnostics as needed).
src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs Binds UnsafeExpressionSyntax and enforces /unsafe + feature availability.

Comment thread src/Compilers/CSharp/Portable/Syntax/Syntax.xml Outdated
Comment thread src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
@jjonescz
jjonescz marked this pull request as ready for review June 4, 2026 15:19
@jjonescz
jjonescz requested a review from a team as a code owner June 4, 2026 15:19
Copilot AI review requested due to automatic review settings June 4, 2026 15:19
@jjonescz
jjonescz requested a review from a team as a code owner June 4, 2026 15:19
@jjonescz
jjonescz requested review from 333fred and AlekseyTs June 4, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 20 changed files in this pull request and generated 1 comment.

Comment thread src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs
@jjonescz

jjonescz commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@333fred @AlekseyTs for reviews, thanks

@AlekseyTs

Copy link
Copy Markdown
Contributor

Does this PR introduce a breaking change?

@jjonescz

jjonescz commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Does this PR introduce a breaking change?

No, I don't think so. unsafe was always a non-contextual keyword, so I don't think any of the new forms of its usage could be valid previously.

@AlekseyTs

AlekseyTs commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor
        TestSingleParamFunctionalOperator(SyntaxKind.RefTypeKeyword);

Consider adding a call for unsafe keyword too #Closed


Refers to: src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs:865 in 765aab7. [](commit_id = 765aab7, deletion_comment = False)

{
return _syntaxFactory.GlobalStatement(ParseUnsafeStatement(attributes));
}
else if (this.PeekToken(1).Kind == SyntaxKind.OpenParenToken)

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else if (this.PeekToken(1).Kind == SyntaxKind.OpenParenToken)

Why is this necessary? It looks like we do not have special handling for checked/unchecked #Closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the following comment below explains that

// All modifiers that might start an expression are processed above.

}
else
{
CheckFeatureAvailability(node.UnsafeKeyword, MessageID.IDS_FeatureUnsafeEvolution, diagnostics);

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckFeatureAvailability(node.UnsafeKeyword, MessageID.IDS_FeatureUnsafeEvolution, diagnostics);

Consider performing this check independently from value of this.Compilation.Options.AllowUnsafe. #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done intentionally to match what Binder.BindUnsafeStatement does. But I agree I don't see much advantage in this.

return ParseExpressionStatement(attributes);
}

return IsPossibleUnsafeStatement() ? ParseUnsafeStatement(attributes) : null;

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsPossibleUnsafeStatement

It looks like this is the only call site. Consider inlining the helper and requesting PeekToken(1).Kind only once. #Closed

// checked and unchecked expressions
case SyntaxKind.UncheckedExpression:
case SyntaxKind.CheckedExpression:
case SyntaxKind.UnsafeExpression:

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case SyntaxKind.UnsafeExpression:

Do we have a test observing significance of this case? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding UnsafeEvolutionTests.UnsafeExpression_AttributeArgument_DefaultParameterValue, thanks.

var expectedOperationTree = """
IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return unsafe(*p);')
ReturnedValue:
IOperation: (OperationKind.None, Type: System.Int32) (Syntax: '*p')

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOperation: (OperationKind.None, Type: System.Int32) (Syntax: '*p')

Apparently we haven't implemented support for dereference (and likely other pointer specific operations) For IOperation tests, consider using supported forms of expressions. Also, given the feature, should we add support for the pointer related operations?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, given the feature, should we add support for the pointer related operations?

The feature only makes pointer types safe, pointer dereference is still unsafe as it always was, i.e., adding IOperation for it doesn't seem particularly motivated by this feature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature only makes pointer types safe, pointer dereference is still unsafe as it always was, i.e., adding IOperation for it doesn't seem particularly motivated by this feature.

Pointers and operations on pointers were "outside" of the language. It is my understanding, that with this feature, they truly become part of the language.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, I am not suggesting to do the IOperation work in this PR

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointers and operations on pointers were "outside" of the language. It is my understanding, that with this feature, they truly become part of the language.

That's a good point, filed #84081.

Predecessors: [B0]
Statements (0)
Next (Return) Block[B2]
IOperation: (OperationKind.None, Type: System.Int32) (Syntax: '*p')

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOperation: (OperationKind.None, Type: System.Int32) (Syntax: '*p')

Similar suggestion to use supported expressions #Closed

CreateCompilation(source,
options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules())
.VerifyDiagnostics(
// (2,1): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// (2,1): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement

Do we report the same error for checked/unchecked? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

{
if (LookupPosition.IsBetweenTokens(position, unsafeExpression.OpenParenToken, unsafeExpression.CloseParenToken))
{
binder = rootBinder.GetBinder(current);

@AlekseyTs AlekseyTs Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

binder = rootBinder.GetBinder(current);

Do we have an explicit test covering this code path? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding UnsafeExpression_SemanticModel_EnclosingBinder, thanks.

@AlekseyTs

Copy link
Copy Markdown
Contributor

Done with review pass (commit 7)

@AlekseyTs

Copy link
Copy Markdown
Contributor

Done with review pass (commit 10). It looks like there are legitimate CI failures

Copilot AI review requested due to automatic review settings June 9, 2026 16:20
@jjonescz
jjonescz requested a review from AlekseyTs June 9, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 20 changed files in this pull request and generated no new comments.

@AlekseyTs AlekseyTs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 11)

@jjonescz

Copy link
Copy Markdown
Member Author

@333fred for a second review, thanks

</Field>
<Field Name="Expression" Type="ExpressionSyntax">
<PropertyComment>
<summary>Argument of the primary function.</summary>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't make sense to me?

@jjonescz jjonescz Jun 16, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was copied from the CheckedExpressionSyntax, but it doesn't make sense to me either. I will improve it here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So copilot found a fun edge case:

void M()
{
    unsafe (int, int) Local() => (1, 2);   // valid before this PR, broken after.
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test in a catch filter block, as they can do interesting things sometimes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test in a catch filter block, as they can do interesting things sometimes.

We are already testing that in UnsafeEvolutionTests.UnsafeExpression_ExceptionFilter.

=> PeekToken(1).Kind == SyntaxKind.OpenParenToken ? ParseUsingStatement(attributes) : ParseLocalDeclarationStatement(attributes);

// Checking for brace to disambiguate between unsafe statement and unsafe local function
// Checking for brace or parenthesis to disambiguate between unsafe statement, unsafe expression, and unsafe local function

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plural

Suggested change
// Checking for brace or parenthesis to disambiguate between unsafe statement, unsafe expression, and unsafe local function
// Checking for brace or parentheses to disambiguate between unsafe statement, unsafe expression, and unsafe local function

{
int M(int i)
/*<bind>*/{
return unsafe(i);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a version of this test with flow inside the unsafe, like a ternary or null-coalscing expression.

@jjonescz
jjonescz merged commit 610e1e1 into dotnet:main Jun 16, 2026
28 checks passed
@jjonescz
jjonescz deleted the Unsafe-39-UnsafeExpressions branch June 16, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Unsafe Evolution Needs API Review Needs to be reviewed by the API review council

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants