Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lock object feature #71716

Merged
merged 16 commits into from
Feb 7, 2024
7 changes: 7 additions & 0 deletions src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ BoundExpression createConversion(
if (!hasErrors && conversion.Exists)
{
ensureAllUnderlyingConversionsChecked(syntax, source, conversion, wasCompilerGenerated, destination, diagnostics);

if (conversion.Kind == ConversionKind.ImplicitReference &&
source.Type is { } sourceType &&
sourceType.IsWellKnownTypeLock())
{
diagnostics.Add(ErrorCode.WRN_ConvertingLock, source.Syntax);
}
}

return new BoundConversion(
Expand Down
12 changes: 10 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3182,13 +3182,21 @@ private BoundExpression BindOutVariableDeclarationArgument(
/// <summary>
/// Reports an error when a bad special by-ref local was found.
/// </summary>
internal static void CheckRestrictedTypeInAsyncMethod(Symbol containingSymbol, TypeSymbol type, BindingDiagnosticBag diagnostics, SyntaxNode syntax, bool forUsingExpression = false)
internal static void CheckRestrictedTypeInAsyncMethod(Symbol containingSymbol, TypeSymbol type, BindingDiagnosticBag diagnostics, SyntaxNode syntax, ErrorCode errorCode = ErrorCode.ERR_BadSpecialByRefLocal)
{
Debug.Assert(errorCode is ErrorCode.ERR_BadSpecialByRefLocal or ErrorCode.ERR_BadSpecialByRefUsing or ErrorCode.ERR_BadSpecialByRefLock);
if (containingSymbol.Kind == SymbolKind.Method
&& ((MethodSymbol)containingSymbol).IsAsync
&& type.IsRestrictedType())
{
Error(diagnostics, forUsingExpression ? ErrorCode.ERR_BadSpecialByRefUsing : ErrorCode.ERR_BadSpecialByRefLocal, syntax, type);
if (errorCode == ErrorCode.ERR_BadSpecialByRefLock)
{
Error(diagnostics, errorCode, syntax);
}
else
{
Error(diagnostics, errorCode, syntax, type);
}
}
}

Expand Down
90 changes: 86 additions & 4 deletions src/Compilers/CSharp/Portable/Binder/LockBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CSharp.Symbols;
Expand All @@ -13,6 +11,8 @@

namespace Microsoft.CodeAnalysis.CSharp
{
using LockTypeInfo = (MethodSymbol EnterLockScopeMethod, TypeSymbol ScopeType, MethodSymbol ScopeDisposeMethod);

internal sealed class LockBinder : LockOrUsingBinder
{
private readonly LockStatementSyntax _syntax;
Expand All @@ -37,11 +37,11 @@ internal override BoundStatement BindLockStatementParts(BindingDiagnosticBag dia
// a reference type.
ExpressionSyntax exprSyntax = TargetExpressionSyntax;
BoundExpression expr = BindTargetExpression(diagnostics, originalBinder);
TypeSymbol exprType = expr.Type;
TypeSymbol? exprType = expr.Type;

bool hasErrors = false;

if ((object)exprType == null)
if (exprType is null)
{
if (expr.ConstantValueOpt != ConstantValue.Null || Compilation.FeatureStrictEnabled) // Dev10 allows the null literal.
{
Expand All @@ -55,9 +55,91 @@ internal override BoundStatement BindLockStatementParts(BindingDiagnosticBag dia
hasErrors = true;
}

if (exprType?.IsWellKnownTypeLock() == true &&
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
TryFindLockTypeInfo(exprType, diagnostics, exprSyntax) is { } lockTypeInfo)
{
CheckFeatureAvailability(exprSyntax, MessageID.IDS_LockObject, diagnostics);

// Report use-site errors for members we will use in lowering.
_ = diagnostics.ReportUseSite(lockTypeInfo.EnterLockScopeMethod, exprSyntax) ||
diagnostics.ReportUseSite(lockTypeInfo.ScopeType, exprSyntax) ||
diagnostics.ReportUseSite(lockTypeInfo.ScopeDisposeMethod, exprSyntax);

CheckRestrictedTypeInAsyncMethod(
originalBinder.ContainingMemberOrLambda,
lockTypeInfo.ScopeType,
diagnostics,
exprSyntax,
errorCode: ErrorCode.ERR_BadSpecialByRefLock);
}

BoundStatement stmt = originalBinder.BindPossibleEmbeddedStatement(_syntax.Statement, diagnostics);
Debug.Assert(this.Locals.IsDefaultOrEmpty);
return new BoundLockStatement(_syntax, expr, stmt, hasErrors);
}

internal static LockTypeInfo? TryFindLockTypeInfo(TypeSymbol lockType, BindingDiagnosticBag diagnostics, SyntaxNode syntax)
{
const string LockTypeFullName = "System.Threading.Lock";
const string EnterLockScopeMethodName = "EnterLockScope";
const string LockScopeTypeName = "Scope";

var enterLockScopeMethod = TryFindPublicVoidParameterlessMethod(lockType, EnterLockScopeMethodName);
if (enterLockScopeMethod is not { ReturnsVoid: false, RefKind: RefKind.None })
{
Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, LockTypeFullName, EnterLockScopeMethodName);
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

var scopeType = enterLockScopeMethod.ReturnType;
if (!(scopeType is NamedTypeSymbol { Name: LockScopeTypeName, Arity: 0, IsValueType: true, IsRefLikeType: true, DeclaredAccessibility: Accessibility.Public } &&
TypeSymbol.Equals(scopeType.ContainingType, lockType, TypeCompareKind.ConsiderEverything)))
{
Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, LockTypeFullName, EnterLockScopeMethodName);
return null;
}

var disposeMethod = TryFindPublicVoidParameterlessMethod(scopeType, WellKnownMemberNames.DisposeMethodName);
if (disposeMethod is not { ReturnsVoid: true })
{
Error(diagnostics, ErrorCode.ERR_MissingPredefinedMember, syntax, $"{LockTypeFullName}+{LockScopeTypeName}", WellKnownMemberNames.DisposeMethodName);
return null;
}

return new LockTypeInfo
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
{
EnterLockScopeMethod = enterLockScopeMethod,
ScopeType = scopeType,
ScopeDisposeMethod = disposeMethod,
};
}

private static MethodSymbol? TryFindPublicVoidParameterlessMethod(TypeSymbol type, string name)
{
var members = type.GetMembers(name);
MethodSymbol? result = null;
foreach (var member in members)
{
if (member is MethodSymbol
{
ParameterCount: 0,
Arity: 0,
IsStatic: false,
DeclaredAccessibility: Accessibility.Public,
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
MethodKind: MethodKind.Ordinary,
} method)
{
if (result is not null)
{
// Ambiguous method found.
return null;
}

result = method;
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo
Debug.Assert(expressionOpt is not null);
if (expressionOpt.Type is not null)
{
CheckRestrictedTypeInAsyncMethod(originalBinder.ContainingMemberOrLambda, expressionOpt.Type, diagnostics, expressionOpt.Syntax, forUsingExpression: true);
CheckRestrictedTypeInAsyncMethod(originalBinder.ContainingMemberOrLambda, expressionOpt.Type, diagnostics, expressionOpt.Syntax, errorCode: ErrorCode.ERR_BadSpecialByRefUsing);
}
}
else
Expand Down
12 changes: 12 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -7839,4 +7839,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="IDS_ImplicitIndexerInitializer" xml:space="preserve">
<value>implicit indexer initializer</value>
</data>
<data name="WRN_ConvertingLock" xml:space="preserve">
<value>A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement.</value>
</data>
<data name="WRN_ConvertingLock_Title" xml:space="preserve">
<value>A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement.</value>
</data>
<data name="ERR_BadSpecialByRefLock" xml:space="preserve">
<value>A lock statement on a value of type 'System.Threading.Lock' cannot be used in async methods or async lambda expressions.</value>
</data>
<data name="IDS_LockObject" xml:space="preserve">
<value>Lock object</value>
</data>
</root>
3 changes: 3 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,9 @@ internal enum ErrorCode

#endregion

WRN_ConvertingLock = 9214,
ERR_BadSpecialByRefLock = 9215,

// Note: you will need to do the following after adding warnings:
// 1) Re-generate compiler code (eng\generate-compiler-code.cmd).
}
Expand Down
3 changes: 3 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ internal static int GetWarningLevel(ErrorCode code)
case ErrorCode.WRN_Experimental:
case ErrorCode.WRN_CollectionExpressionRefStructMayAllocate:
case ErrorCode.WRN_CollectionExpressionRefStructSpreadMayAllocate:
case ErrorCode.WRN_ConvertingLock:
return 1;
default:
return 0;
Expand Down Expand Up @@ -2411,6 +2412,8 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code)
case ErrorCode.ERR_InvalidExperimentalDiagID:
case ErrorCode.ERR_SpreadMissingMember:
case ErrorCode.ERR_CollectionExpressionTargetNoElementType:
case ErrorCode.WRN_ConvertingLock:
case ErrorCode.ERR_BadSpecialByRefLock:
return false;
default:
// NOTE: All error codes must be explicitly handled in this switch statement
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ internal enum MessageID
IDS_StringEscapeCharacter = MessageBase + 12839,

IDS_ImplicitIndexerInitializer = MessageBase + 12840,
IDS_LockObject = MessageBase + 12841,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -461,6 +462,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
// C# preview features.
case MessageID.IDS_StringEscapeCharacter:
case MessageID.IDS_ImplicitIndexerInitializer:
case MessageID.IDS_LockObject:
return LanguageVersion.Preview;

// C# 12.0 features.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ namespace Microsoft.CodeAnalysis.CSharp
internal sealed partial class LocalRewriter
{
/// <summary>
/// Lowers a lock statement to a try-finally block that calls Monitor.Enter and Monitor.Exit
/// before and after the body, respectively.
/// Lowers a lock statement to a try-finally block that calls (before and after the body, respectively):
/// <list type="bullet">
/// <item>Lock.EnterLockScope and Lock+Scope.Dispose if the argument is of type Lock, or</item>
Copy link
Contributor

@AlekseyTs AlekseyTs Jan 23, 2024

Choose a reason for hiding this comment

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

Lock.EnterLockScope and Lock+Scope.Dispose if the argument is of type Lock, or<

It looks like ControlFlowGraphBuilder.VisitLock should be adjusted to support the new form of lowering. #Pending

/// <item>Monitor.Enter and Monitor.Exit.</item>
/// </list>
/// </summary>
public override BoundNode VisitLockStatement(BoundLockStatement node)
{
Expand All @@ -37,6 +40,45 @@ public override BoundNode VisitLockStatement(BoundLockStatement node)
argumentType); //need to have a non-null type here for TempHelpers.StoreToTemp.
}

if (argumentType.IsWellKnownTypeLock())
{
if (LockBinder.TryFindLockTypeInfo(argumentType, _diagnostics, rewrittenArgument.Syntax) is not { } lockTypeInfo)
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
{
Debug.Fail("We should have reported an error during binding if lock type info cannot be found.");
return node.Update(rewrittenArgument, rewrittenBody).WithHasErrors();
}

// lock (x) { body } -> using (x.EnterLockScope()) { body }
jjonescz marked this conversation as resolved.
Show resolved Hide resolved

var tryBlock = rewrittenBody is BoundBlock block ? block : BoundBlock.SynthesizedNoLocals(lockSyntax, rewrittenBody);

var enterLockScopeCall = BoundCall.Synthesized(
rewrittenArgument.Syntax,
rewrittenArgument,
initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
lockTypeInfo.EnterLockScopeMethod);

BoundLocal boundTemp = _factory.StoreToTemp(enterLockScopeCall,
out BoundAssignmentOperator tempAssignment,
syntaxOpt: rewrittenArgument.Syntax,
kind: SynthesizedLocalKind.Using);
var expressionStatement = new BoundExpressionStatement(rewrittenArgument.Syntax, tempAssignment);

BoundStatement tryFinally = RewriteUsingStatementTryFinally(
rewrittenArgument.Syntax,
rewrittenArgument.Syntax,
tryBlock,
boundTemp,
awaitKeywordOpt: default,
awaitOpt: null,
patternDisposeInfo: MethodArgumentInfo.CreateParameterlessMethod(lockTypeInfo.ScopeDisposeMethod));

return new BoundBlock(
lockSyntax,
locals: ImmutableArray.Create(boundTemp.LocalSymbol),
statements: ImmutableArray.Create(expressionStatement, tryFinally));
}

if (argumentType.Kind == SymbolKind.TypeParameter)
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
{
// If the argument has a type parameter type, then we'll box it right away
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,12 @@ internal static bool IsWellKnownTypeIsExternalInit(this TypeSymbol typeSymbol)

internal static bool IsWellKnownTypeOutAttribute(this TypeSymbol typeSymbol) => typeSymbol.IsWellKnownInteropServicesTopLevelType("OutAttribute");

internal static bool IsWellKnownTypeLock(this TypeSymbol typeSymbol)
{
return typeSymbol is NamedTypeSymbol { Name: "Lock", Arity: 0, ContainingType: null } &&
typeSymbol.IsContainedInNamespace("System", "Threading");
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
}

private static bool IsWellKnownInteropServicesTopLevelType(this TypeSymbol typeSymbol, string name)
{
if (typeSymbol.Name != name || typeSymbol.ContainingType is object)
Expand Down
20 changes: 20 additions & 0 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading