diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs b/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs index 8dc8d46656081..14f0a90213d07 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs @@ -132,21 +132,24 @@ private BoundInterpolatedString BindUnconvertedInterpolatedStringToString(BoundU { // We have 4 possible lowering strategies, dependent on the contents of the string, in this order: // 1. The string is a constant value. We can just use the final value. - // 2. The WellKnownType DefaultInterpolatedStringHandler is available, and none of the interpolation holes contain an await expression. + // 2. The string is composed of 4 or fewer components that are all strings, we can lower to a call to string.Concat without a + // params array. This is very efficient as the runtime can allocate a buffer for the string with exactly the correct length and + // make no intermediate allocations. + // 3. The WellKnownType DefaultInterpolatedStringHandler is available, and none of the interpolation holes contain an await expression. // The builder is a ref struct, and we can guarantee the lifetime won't outlive the stack if the string doesn't contain any // awaits, but if it does we cannot use it. This builder is the only way that ref structs can be directly used as interpolation // hole components, which means that ref structs components and await expressions cannot be combined. It is already illegal for // the user to use ref structs in an async method today, but if that were to ever change, this would still need to be respected. // We also cannot use this method if the interpolated string appears within a catch filter, as the builder is disposable and we // cannot put a try/finally inside a filter block. - // 3. The string is composed entirely of components that are strings themselves. We can turn this into a single call to string.Concat. - // We prefer the builder over this because the builder can use pooling to avoid new allocations, while this call will potentially - // need to allocate a param array. - // 4. The string has heterogeneous data and either InterpolatedStringHandler is unavailable, or one of the holes contains an await + // 4. The string is composed of more than 4 components that are all strings themselves. We can turn this into a single + // call to string.Concat. We prefer the builder over this because the builder can use pooling to avoid new allocations, while this + // call will need to allocate a param array. + // 5. The string has heterogeneous data and either InterpolatedStringHandler is unavailable, or one of the holes contains an await // expression. This is turned into a call to string.Format. // - // We need to do the determination of 1, 2, or 3/4 up front, rather than in lowering, as it affects diagnostics (ref structs not being - // able to be used, for example). However, between 3 and 4, we don't need to know at this point, so that logic is deferred for lowering. + // We need to do the determination of 1, 2, 3, or 4/5 up front, rather than in lowering, as it affects diagnostics (ref structs not being + // able to be used, for example). However, between 4 and 5, we don't need to know at this point, so that logic is deferred for lowering. if (unconvertedInterpolatedString.ConstantValue is not null) { @@ -155,13 +158,21 @@ private BoundInterpolatedString BindUnconvertedInterpolatedStringToString(BoundU return constructWithData(BindInterpolatedStringParts(unconvertedInterpolatedString, diagnostics), data: null); } + // Case 2. Attempt to see if all parts are strings. + if (unconvertedInterpolatedString.Parts.Length <= 4 && + unconvertedInterpolatedString.Parts.All(p => p is BoundLiteral + or BoundStringInsert { Value: { Type: { SpecialType: SpecialType.System_String } }, Alignment: null, Format: null })) + { + return constructWithData(BindInterpolatedStringParts(unconvertedInterpolatedString, diagnostics), data: null); + } + if (tryBindAsHandlerType(out var result)) { - // Case 2 + // Case 3 return result; } - // The specifics of 3 vs 4 aren't necessary for this stage of binding. The only thing that matters is that every part needs to be convertible + // The specifics of 4 vs 5 aren't necessary for this stage of binding. The only thing that matters is that every part needs to be convertible // object. return constructWithData(BindInterpolatedStringParts(unconvertedInterpolatedString, diagnostics), data: null); diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringInterpolation.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringInterpolation.cs index d9d88ab88e963..dc156979c39e3 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringInterpolation.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringInterpolation.cs @@ -282,7 +282,10 @@ public override BoundNode VisitInterpolatedString(BoundInterpolatedString node) _factory.Binary(BinaryOperatorKind.StringConcatenation, node.Type, result, part); } - if (length == 1) + // We need to ensure that the result of the interpolated string is not null. If the single part has a non-null constant value + // or is itself an interpolated string (which by proxy cannot be null), then there's nothing else that needs to be done. Otherwise, + // we need to test for null and ensure "" if it is. + if (length == 1 && result is not ({ Kind: BoundKind.InterpolatedString } or { ConstantValue: { IsString: true } })) { result = _factory.Coalesce(result!, _factory.StringLiteral("")); } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs index 4159c6173ff92..1d37a8c5c1083 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs @@ -213,6 +213,61 @@ static void Main(string[] args) CompileAndVerify(source, expectedOutput: expectedOutput); } + [Fact] + public void OneLiteral() + { + string source = +@"using System; +class Program +{ + static void Main(string[] args) + { + Console.WriteLine( $""Hello"" ); + } +}"; + string expectedOutput = @"Hello"; + var verifier = CompileAndVerify(source, expectedOutput: expectedOutput); + verifier.VerifyIL("Program.Main", @" +{ + // Code size 11 (0xb) + .maxstack 1 + IL_0000: ldstr ""Hello"" + IL_0005: call ""void System.Console.WriteLine(string)"" + IL_000a: ret +} +"); + } + + [Fact] + public void OneInsert() + { + string source = +@"using System; +class Program +{ + static void Main(string[] args) + { + var hello = $""Hello""; + Console.WriteLine( $""{hello}"" ); + } +}"; + string expectedOutput = @"Hello"; + var verifier = CompileAndVerify(source, expectedOutput: expectedOutput); + verifier.VerifyIL("Program.Main", @" +{ + // Code size 20 (0x14) + .maxstack 2 + IL_0000: ldstr ""Hello"" + IL_0005: dup + IL_0006: brtrue.s IL_000e + IL_0008: pop + IL_0009: ldstr """" + IL_000e: call ""void System.Console.WriteLine(string)"" + IL_0013: ret +} +"); + } + [Fact] public void TwoInserts() { @@ -1202,6 +1257,164 @@ static void Main() ); } + [Fact, WorkItem(54702, "https://github.com/dotnet/roslyn/issues/54702")] + public void InterpolatedStringHandler_ConcatPreferencesForAllStringElements() + { + var code = @" +using System; +Console.WriteLine(TwoComponents()); +Console.WriteLine(ThreeComponents()); +Console.WriteLine(FourComponents()); +Console.WriteLine(FiveComponents()); + +string TwoComponents() +{ + string s1 = ""1""; + string s2 = ""2""; + return $""{s1}{s2}""; +} + +string ThreeComponents() +{ + string s1 = ""1""; + string s2 = ""2""; + string s3 = ""3""; + return $""{s1}{s2}{s3}""; +} + +string FourComponents() +{ + string s1 = ""1""; + string s2 = ""2""; + string s3 = ""3""; + string s4 = ""4""; + return $""{s1}{s2}{s3}{s4}""; +} + +string FiveComponents() +{ + string s1 = ""1""; + string s2 = ""2""; + string s3 = ""3""; + string s4 = ""4""; + string s5 = ""5""; + return $""{s1}{s2}{s3}{s4}{s5}""; +} +"; + + var handler = GetInterpolatedStringHandlerDefinition(includeSpanOverloads: false, useDefaultParameters: false, useBoolReturns: false); + + var verifier = CompileAndVerify(new[] { code, handler }, expectedOutput: @" +12 +123 +1234 +value:1 +value:2 +value:3 +value:4 +value:5 +"); + + verifier.VerifyIL("$.<
$>g__TwoComponents|0_0()", @" +{ + // Code size 18 (0x12) + .maxstack 2 + .locals init (string V_0) //s2 + IL_0000: ldstr ""1"" + IL_0005: ldstr ""2"" + IL_000a: stloc.0 + IL_000b: ldloc.0 + IL_000c: call ""string string.Concat(string, string)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("$.<
$>g__ThreeComponents|0_1()", @" +{ + // Code size 25 (0x19) + .maxstack 3 + .locals init (string V_0, //s2 + string V_1) //s3 + IL_0000: ldstr ""1"" + IL_0005: ldstr ""2"" + IL_000a: stloc.0 + IL_000b: ldstr ""3"" + IL_0010: stloc.1 + IL_0011: ldloc.0 + IL_0012: ldloc.1 + IL_0013: call ""string string.Concat(string, string, string)"" + IL_0018: ret +} +"); + + verifier.VerifyIL("$.<
$>g__FourComponents|0_2()", @" +{ + // Code size 32 (0x20) + .maxstack 4 + .locals init (string V_0, //s2 + string V_1, //s3 + string V_2) //s4 + IL_0000: ldstr ""1"" + IL_0005: ldstr ""2"" + IL_000a: stloc.0 + IL_000b: ldstr ""3"" + IL_0010: stloc.1 + IL_0011: ldstr ""4"" + IL_0016: stloc.2 + IL_0017: ldloc.0 + IL_0018: ldloc.1 + IL_0019: ldloc.2 + IL_001a: call ""string string.Concat(string, string, string, string)"" + IL_001f: ret +} +"); + + verifier.VerifyIL("$.<
$>g__FiveComponents|0_3()", @" +{ + // Code size 89 (0x59) + .maxstack 3 + .locals init (string V_0, //s1 + string V_1, //s2 + string V_2, //s3 + string V_3, //s4 + string V_4, //s5 + System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_5) + IL_0000: ldstr ""1"" + IL_0005: stloc.0 + IL_0006: ldstr ""2"" + IL_000b: stloc.1 + IL_000c: ldstr ""3"" + IL_0011: stloc.2 + IL_0012: ldstr ""4"" + IL_0017: stloc.3 + IL_0018: ldstr ""5"" + IL_001d: stloc.s V_4 + IL_001f: ldloca.s V_5 + IL_0021: ldc.i4.0 + IL_0022: ldc.i4.5 + IL_0023: call ""System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"" + IL_0028: ldloca.s V_5 + IL_002a: ldloc.0 + IL_002b: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" + IL_0030: ldloca.s V_5 + IL_0032: ldloc.1 + IL_0033: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" + IL_0038: ldloca.s V_5 + IL_003a: ldloc.2 + IL_003b: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" + IL_0040: ldloca.s V_5 + IL_0042: ldloc.3 + IL_0043: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" + IL_0048: ldloca.s V_5 + IL_004a: ldloc.s V_4 + IL_004c: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" + IL_0051: ldloca.s V_5 + IL_0053: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" + IL_0058: ret +} +"); + } + [Theory] [CombinatorialData] public void InterpolatedStringHandler_OverloadsAndBoolReturns(bool useDefaultParameters, bool useBoolReturns, bool constructorBoolArg) @@ -2948,15 +3161,14 @@ public void NestedInterpolatedStrings() var interpolatedStringBuilder = GetInterpolatedStringHandlerDefinition(includeSpanOverloads: false, useDefaultParameters: false, useBoolReturns: false); - var verifier = CompileAndVerify(new[] { source, interpolatedStringBuilder }, expectedOutput: @"value:value:1"); + var verifier = CompileAndVerify(new[] { source, interpolatedStringBuilder }, expectedOutput: @"value:1"); verifier.VerifyIL("", @" { - // Code size 55 (0x37) - .maxstack 4 + // Code size 32 (0x20) + .maxstack 3 .locals init (int V_0, //i - System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_1, - System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_2) + System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_1) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldloca.s V_1 @@ -2964,20 +3176,12 @@ .locals init (int V_0, //i IL_0005: ldc.i4.1 IL_0006: call ""System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"" IL_000b: ldloca.s V_1 - IL_000d: ldloca.s V_2 - IL_000f: ldc.i4.0 - IL_0010: ldc.i4.1 - IL_0011: call ""System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"" - IL_0016: ldloca.s V_2 - IL_0018: ldloc.0 - IL_0019: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(int)"" - IL_001e: ldloca.s V_2 - IL_0020: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" - IL_0025: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(string)"" - IL_002a: ldloca.s V_1 - IL_002c: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" - IL_0031: call ""void System.Console.WriteLine(string)"" - IL_0036: ret + IL_000d: ldloc.0 + IL_000e: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(int)"" + IL_0013: ldloca.s V_1 + IL_0015: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" + IL_001a: call ""void System.Console.WriteLine(string)"" + IL_001f: ret } "); }