diff --git a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md index af927a7ceee..80d327e5d9e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -29,3 +29,4 @@ * Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323), [PR #16567](https://github.com/dotnet/fsharp/pull/16567)) * Reverted [#16348](https://github.com/dotnet/fsharp/pull/16348) `ThreadStatic` `CancellationToken` changes to improve test stability and prevent potential unwanted cancellations. ([PR #16536](https://github.com/dotnet/fsharp/pull/16536)) * Refactored parenthesization API. ([PR #16461])(https://github.com/dotnet/fsharp/pull/16461)) +* Optimize some interpolated strings by lowering to string concatenation. ([PR #16556](https://github.com/dotnet/fsharp/pull/16556)) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 832dd924a44..d1880b4dd85 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -8,3 +8,7 @@ ### Fixed * Allow extension methods without type attribute work for types from imported assemblies. ([PR #16368](https://github.com/dotnet/fsharp/pull/16368)) + +### Changed + +* Lower interpolated strings to string concatenation. ([PR #16556](https://github.com/dotnet/fsharp/pull/16556)) \ No newline at end of file diff --git a/src/Compiler/Checking/CheckExpressions.fs b/src/Compiler/Checking/CheckExpressions.fs index e59fb6bd5ff..89268e1ee6e 100644 --- a/src/Compiler/Checking/CheckExpressions.fs +++ b/src/Compiler/Checking/CheckExpressions.fs @@ -6,6 +6,7 @@ module internal FSharp.Compiler.CheckExpressions open System open System.Collections.Generic +open System.Text.RegularExpressions open Internal.Utilities.Collections open Internal.Utilities.Library @@ -140,6 +141,43 @@ exception StandardOperatorRedefinitionWarning of string * range exception InvalidInternalsVisibleToAssemblyName of badName: string * fileName: string option +//---------------------------------------------------------------------------------------------- +// Helpers for determining if/what specifiers a string has. +// Used to decide if interpolated string can be lowered to a concat call. +// We don't care about single- vs multi-$ strings here, because lexer took care of that already. +//---------------------------------------------------------------------------------------------- +[] +let (|HasFormatSpecifier|_|) (s: string) = + if + Regex.IsMatch( + s, + // Regex pattern for something like: %[flags][width][.precision][type] + """ + (^|[^%]) # Start with beginning of string or any char other than '%' + (%%)*% # followed by an odd number of '%' chars + [+-0 ]{0,3} # optionally followed by flags + (\d+)? # optionally followed by width + (\.\d+)? # optionally followed by .precision + [bscdiuxXoBeEfFgGMOAat] # and then a char that determines specifier's type + """, + RegexOptions.Compiled ||| RegexOptions.IgnorePatternWhitespace) + then + ValueSome HasFormatSpecifier + else + ValueNone + +// Removes trailing "%s" unless it was escaped by another '%' (checks for odd sequence of '%' before final "%s") +let (|WithTrailingStringSpecifierRemoved|) (s: string) = + if s.EndsWith "%s" then + let i = s.AsSpan(0, s.Length - 2).LastIndexOfAnyExcept '%' + let diff = s.Length - 2 - i + if diff &&& 1 <> 0 then + s[..i] + else + s + else + s + /// Compute the available access rights from a particular location in code let ComputeAccessRights eAccessPath eInternalsVisibleCompPaths eFamilyType = AccessibleFrom (eAccessPath :: eInternalsVisibleCompPaths, eFamilyType) @@ -7336,25 +7374,68 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn // Type check the expressions filling the holes let fillExprs, tpenv = TcExprsNoFlexes cenv env m tpenv argTys synFillExprs - let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m) + // Take all interpolated string parts and typed fill expressions + // and convert them to typed expressions that can be used as args to System.String.Concat + // return an empty list if there are some format specifiers that make lowering to not applicable + let rec concatenable acc fillExprs parts = + match fillExprs, parts with + | [], [] -> + List.rev acc + | [], SynInterpolatedStringPart.FillExpr _ :: _ + | _, [] -> + // This should never happen, there will always be as many typed fill expressions + // as there are FillExprs in the interpolated string parts + error(InternalError("Mismatch in interpolation expression count", m)) + | _, SynInterpolatedStringPart.String (WithTrailingStringSpecifierRemoved "", _) :: parts -> + // If the string is empty (after trimming %s of the end), we skip it + concatenable acc fillExprs parts + + | _, SynInterpolatedStringPart.String (WithTrailingStringSpecifierRemoved HasFormatSpecifier, _) :: _ + | _, SynInterpolatedStringPart.FillExpr (_, Some _) :: _ + | _, SynInterpolatedStringPart.FillExpr (SynExpr.Tuple (isStruct = false; exprs = [_; SynExpr.Const (SynConst.Int32 _, _)]), _) :: _ -> + // There was a format specifier like %20s{..} or {..,20} or {x:hh}, which means we cannot simply concat + [] - let argsExpr = mkArray (g.obj_ty, fillExprsBoxed, m) - let percentATysExpr = - if percentATys.Length = 0 then - mkNull m (mkArrayType g g.system_Type_ty) - else - let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList - mkArray (g.system_Type_ty, tyExprs, m) + | _, SynInterpolatedStringPart.String (s & WithTrailingStringSpecifierRemoved trimmed, m) :: parts -> + let finalStr = trimmed.Replace("%%", "%") + concatenable (mkString g (shiftEnd 0 (finalStr.Length - s.Length) m) finalStr :: acc) fillExprs parts - let fmtExpr = MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None + | fillExpr :: fillExprs, SynInterpolatedStringPart.FillExpr _ :: parts -> + concatenable (fillExpr :: acc) fillExprs parts - if isString then - TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env (* true *) m (fun () -> - // Make the call to sprintf - mkCall_sprintf g m printerTy fmtExpr [], tpenv - ) - else - fmtExpr, tpenv + let canLower = + g.langVersion.SupportsFeature LanguageFeature.LowerInterpolatedStringToConcat + && isString + && argTys |> List.forall (isStringTy g) + + let concatenableExprs = if canLower then concatenable [] fillExprs parts else [] + + match concatenableExprs with + | [p1; p2; p3; p4] -> mkStaticCall_String_Concat4 g m p1 p2 p3 p4, tpenv + | [p1; p2; p3] -> mkStaticCall_String_Concat3 g m p1 p2 p3, tpenv + | [p1; p2] -> mkStaticCall_String_Concat2 g m p1 p2, tpenv + | [p1] -> p1, tpenv + | _ -> + + let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m) + + let argsExpr = mkArray (g.obj_ty, fillExprsBoxed, m) + let percentATysExpr = + if percentATys.Length = 0 then + mkNull m (mkArrayType g g.system_Type_ty) + else + let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList + mkArray (g.system_Type_ty, tyExprs, m) + + let fmtExpr = MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None + + if isString then + TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env (* true *) m (fun () -> + // Make the call to sprintf + mkCall_sprintf g m printerTy fmtExpr [], tpenv + ) + else + fmtExpr, tpenv // The case for $"..." used as type FormattableString or IFormattable | Choice2Of2 createFormattableStringMethod -> diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index a9fc2692157..1fef46084d0 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1594,6 +1594,7 @@ featureWarningIndexedPropertiesGetSetSameType,"Indexed properties getter and set featureChkTailCallAttrOnNonRec,"Raises warnings if the 'TailCall' attribute is used on non-recursive functions." featureUnionIsPropertiesVisible,"Union case test properties" featureBooleanReturningAndReturnTypeDirectedPartialActivePattern,"Boolean-returning and return-type-directed partial active patterns" +featureLowerInterpolatedStringToConcat,"Optimizes interpolated strings in certain cases, by lowering to concatenation" 3354,tcNotAFunctionButIndexerNamedIndexingNotYetEnabled,"This value supports indexing, e.g. '%s.[index]'. The syntax '%s[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation." 3354,tcNotAFunctionButIndexerIndexingNotYetEnabled,"This expression supports indexing, e.g. 'expr.[index]'. The syntax 'expr[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation." 3355,tcNotAnIndexerNamedIndexingNotYetEnabled,"The value '%s' is not a function and does not support index notation." diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index e7c2a25ee3d..66547a12e29 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -85,6 +85,7 @@ type LanguageFeature = | WarningIndexedPropertiesGetSetSameType | WarningWhenTailCallAttrOnNonRec | BooleanReturningAndReturnTypeDirectedPartialActivePattern + | LowerInterpolatedStringToConcat /// LanguageVersion management type LanguageVersion(versionText) = @@ -197,6 +198,7 @@ type LanguageVersion(versionText) = LanguageFeature.WarningWhenTailCallAttrOnNonRec, previewVersion LanguageFeature.UnionIsPropertiesVisible, previewVersion LanguageFeature.BooleanReturningAndReturnTypeDirectedPartialActivePattern, previewVersion + LanguageFeature.LowerInterpolatedStringToConcat, previewVersion ] static let defaultLanguageVersion = LanguageVersion("default") @@ -340,6 +342,7 @@ type LanguageVersion(versionText) = | LanguageFeature.WarningWhenTailCallAttrOnNonRec -> FSComp.SR.featureChkTailCallAttrOnNonRec () | LanguageFeature.BooleanReturningAndReturnTypeDirectedPartialActivePattern -> FSComp.SR.featureBooleanReturningAndReturnTypeDirectedPartialActivePattern () + | LanguageFeature.LowerInterpolatedStringToConcat -> FSComp.SR.featureLowerInterpolatedStringToConcat () /// Get a version string associated with the given feature. static member GetFeatureVersionString feature = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 29d6c2c33a3..582c191ac8a 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -76,6 +76,7 @@ type LanguageFeature = | WarningIndexedPropertiesGetSetSameType | WarningWhenTailCallAttrOnNonRec | BooleanReturningAndReturnTypeDirectedPartialActivePattern + | LowerInterpolatedStringToConcat /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/Utilities/ReadOnlySpan.fs b/src/Compiler/Utilities/ReadOnlySpan.fs index 05683eadb9e..a01439bee63 100644 --- a/src/Compiler/Utilities/ReadOnlySpan.fs +++ b/src/Compiler/Utilities/ReadOnlySpan.fs @@ -59,4 +59,16 @@ type ReadOnlySpanExtensions = i <- i - 1 if found then i else -1 + + [] + static member LastIndexOfAnyExcept(span: ReadOnlySpan, value: char) = + let mutable i = span.Length - 1 + let mutable found = false + + while not found && i >= 0 do + let c = span[i] + + if c <> value then found <- true else i <- i - 1 + + if found then i else -1 #endif diff --git a/src/Compiler/Utilities/ReadOnlySpan.fsi b/src/Compiler/Utilities/ReadOnlySpan.fsi index 67591a03f88..b772ab58642 100644 --- a/src/Compiler/Utilities/ReadOnlySpan.fsi +++ b/src/Compiler/Utilities/ReadOnlySpan.fsi @@ -17,4 +17,7 @@ type internal ReadOnlySpanExtensions = [] static member LastIndexOfAnyInRange: span: ReadOnlySpan * lowInclusive: char * highInclusive: char -> int + + [] + static member LastIndexOfAnyExcept: span: ReadOnlySpan * value: char -> int #endif diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy index 6c5415667d0..4080fc7ec8b 100644 --- a/src/Compiler/pars.fsy +++ b/src/Compiler/pars.fsy @@ -6776,8 +6776,8 @@ interpolatedStringParts: | INTERP_STRING_END { [ SynInterpolatedStringPart.String(fst $1, rhs parseState 1) ] } - | INTERP_STRING_PART interpolatedStringFill interpolatedStringParts - { SynInterpolatedStringPart.String(fst $1, rhs parseState 1) :: SynInterpolatedStringPart.FillExpr $2 :: $3 } + | INTERP_STRING_PART interpolatedStringFill interpolatedStringParts + { SynInterpolatedStringPart.String(fst $1, rhs parseState 1) :: SynInterpolatedStringPart.FillExpr $2 :: $3 } | INTERP_STRING_PART interpolatedStringParts { let rbrace = parseState.InputEndPosition 1 diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index c7dbc64bac7..27098c6bd1e 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -372,6 +372,11 @@ rozhraní s vícenásobným obecným vytvářením instancí + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Povolit duplikát malými písmeny při atributu RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index d5800420f00..c89e0988f8e 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -372,6 +372,11 @@ Schnittstellen mit mehrfacher generischer Instanziierung + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute DU in Kleinbuchstaben zulassen, wenn requireQualifiedAccess-Attribut diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index dcfce175697..de54c17c1f3 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -372,6 +372,11 @@ interfaces con creación de instancias genéricas múltiples + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Permitir DU en minúsculas con el atributo RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 590c4fb8e15..17807523602 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -372,6 +372,11 @@ interfaces avec plusieurs instanciations génériques + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Autoriser les DU en minuscules pour l'attribut RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 70613984b76..1661484a63c 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -372,6 +372,11 @@ interfacce con più creazioni di istanze generiche + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Consentire l’unione discriminata minuscola quando l'attributo RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 84a9ca6ef70..a5a04b7c1b0 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -372,6 +372,11 @@ 複数のジェネリックのインスタンス化を含むインターフェイス + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute RequireQualifiedAccess 属性の場合に小文字 DU を許可する diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 38ffac7d32d..d419bd0d99b 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -372,6 +372,11 @@ 여러 제네릭 인스턴스화가 포함된 인터페이스 + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute RequireQualifiedAccess 특성이 있는 경우 소문자 DU 허용 diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index ac1d54f63c4..65d582f3796 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -372,6 +372,11 @@ interfejsy z wieloma ogólnymi wystąpieniami + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Zezwalaj na małą literę DU, gdy występuje RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 6beb1946267..93af3d22e75 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -372,6 +372,11 @@ interfaces com várias instanciações genéricas + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Permitir DU em minúsculas quando o atributo RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 1b83dc7cc6b..b13f061163b 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -372,6 +372,11 @@ интерфейсы с множественным универсальным созданием экземпляра + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute Разрешить du в нижнем регистре, если атрибут RequireQualifiedAccess diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 43f5069b2eb..ad43bfe169b 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -372,6 +372,11 @@ birden çok genel örnek oluşturma içeren arabirimler + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute RequireQualifiedAccess özniteliğinde küçük harf DU'ya izin ver diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 91cad01e99e..d7013ad94e7 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -372,6 +372,11 @@ 具有多个泛型实例化的接口 + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute 当 RequireQualifiedAccess 属性时允许小写 DU diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index f12b3fd0b5e..902612c1b64 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -372,6 +372,11 @@ 具有多個泛型具現化的介面 + + Optimizes interpolated strings in certain cases, by lowering to concatenation + Optimizes interpolated strings in certain cases, by lowering to concatenation + + Allow lowercase DU when RequireQualifiedAccess attribute RequireQualifiedAccess 屬性時允許小寫 DU diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index 4b9dcaebf16..8528710b76f 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -42,4 +42,4 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) { CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 287232 # Check net7.0 trimmed assemblies -CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8821248 +CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8820736 diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs index 0ef7d23422e..3038460f577 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs @@ -5,8 +5,8 @@ namespace EmittedIL open Xunit open FSharp.Test.Compiler -#if !DEBUG // sensitive to debug-level code coming across from debug FSharp.Core module ``StringFormatAndInterpolation`` = +#if !DEBUG // sensitive to debug-level code coming across from debug FSharp.Core [] let ``Interpolated string with no holes is reduced to a string or simple format when used in printf``() = FSharp """ @@ -34,3 +34,61 @@ IL_0017: ret"""] #endif + [] + let ``Interpolated string with 2 parts consisting only of strings is lowered to concat`` () = + FSharp $""" +module StringFormatAndInterpolation + +let f (s: string) = $"ab{{s}}" + """ + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldstr "ab" +IL_0005: ldarg.0 +IL_0006: call string [runtime]System.String::Concat(string, + string) +IL_000b: ret"""] + + [] + let ``Interpolated string with 3 parts consisting only of strings is lowered to concat`` () = + //let str = "$\"\"\"ab{\"c\"}d\"\"\"" + FSharp $""" +module StringFormatAndInterpolation + +let c = "c" +let str = $"ab{{c}}d" + """ + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldstr "ab" +IL_0005: ldstr "c" +IL_000a: ldstr "d" +IL_000f: call string [runtime]System.String::Concat(string, + string, + string)"""] + + [] + let ``Interpolated string with 4 parts consisting only of strings is lowered to concat`` () = + let str = "$\"\"\"a{\"b\"}{\"c\"}d\"\"\"" + FSharp $""" +module StringFormatAndInterpolation + +let str () = {str} + """ + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldstr "a" +IL_0005: ldstr "b" +IL_000a: ldstr "c" +IL_000f: ldstr "d" +IL_0014: call string [runtime]System.String::Concat(string, + string, + string, + string) +IL_0019: ret"""] \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs index c8f2eafa937..7c82ac3d88e 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs @@ -128,4 +128,49 @@ type Foo () = x """ |> compile - |> shouldSucceed \ No newline at end of file + |> shouldSucceed + + [] + // Test different number of interpolated string parts + [] + [] + [] + let ``Interpolated expressions are strings`` (strToPrint: string) = + Fsx $""" +let x = {strToPrint} +printfn "%%s" x + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "abcde" + + let ``Multiline interpolated expression is a string`` () = + let strToPrint = String.Join(Environment.NewLine, "$\"\"\"a", "b", "c", "{\"d\"}", "e\"\"\"") + Fsx $""" +let x = {strToPrint} +printfn "%%s" x + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains """a +b +c +d +e""" + + [] + [] + [] + [] + let ``In FormattableString, interpolated expressions are strings`` (formattableStr: string, argCount: int) = + Fsx $""" +let x = {formattableStr} : System.FormattableString +assert(x.ArgumentCount = {argCount}) +printfn "%%s" (System.Globalization.CultureInfo "en-US" |> x.ToString) + """ + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "abcde" \ No newline at end of file