diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 5d4bd0fe6ac..41940c7f37d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -100,5 +100,9 @@ * Improvements in error and warning messages: new error FS3885 when `let!`/`use!` is the final expression in a computation expression; new warning FS3886 when a list literal contains a single tuple element (likely missing `;` separator); improved wording for FS0003, FS0025, FS0039, FS0072, FS0247, FS0597, FS0670, FS3082, and SRTP operator-not-in-scope hints. ([PR #19398](https://github.com/dotnet/fsharp/pull/19398)) * Exception field serialization (`GetObjectData` and field-restoring constructor) is now gated behind `langversion:11` (`LanguageFeature.ExceptionFieldSerializationSupport`). With langversion ≤10, exception codegen is unchanged from pre-#19342 behavior. ([PR #19746](https://github.com/dotnet/fsharp/pull/19746)) +* Lower string-typed interpolated strings to `System.String.Concat` rather than the reflection-based `printf` engine, making them trim- and NativeAOT-compatible. This generalizes and ungates the previous all-string `String.Concat` optimization, so it now applies to every string-typed interpolation. ([Language suggestion #1108](https://github.com/fsharp/fslang-suggestions/issues/1108), [PR #19971](https://github.com/dotnet/fsharp/pull/19971)) +* Interpolated string holes (e.g. `$"{x}"`) are now formatted with invariant culture (via the `string` operator) instead of the current thread culture. ([PR #19971](https://github.com/dotnet/fsharp/pull/19971)) ### Breaking Changes + +* `FSharp.Compiler.Syntax.SynInterpolatedStringPart.FillExpr` now carries a `SynInterpolationFormatting` value (separating .NET alignment/format from printf specifiers) instead of an `Ident option`. ([PR #19971](https://github.com/dotnet/fsharp/pull/19971)) diff --git a/src/Compiler/Checking/CheckFormatStrings.fsi b/src/Compiler/Checking/CheckFormatStrings.fsi index eb8120f712d..7b22639f4dc 100644 --- a/src/Compiler/Checking/CheckFormatStrings.fsi +++ b/src/Compiler/Checking/CheckFormatStrings.fsi @@ -12,6 +12,12 @@ open FSharp.Compiler.TcGlobals open FSharp.Compiler.Text open FSharp.Compiler.TypedTree +/// A flexible type variable constrained to the integer types accepted by the '%d'/'%i'/'%u' specifiers. +val mkFlexibleIntFormatTypar: g: TcGlobals -> m: range -> TType + +/// A flexible type variable constrained to 'decimal', as accepted by the '%M' specifier. +val mkFlexibleDecimalFormatTypar: g: TcGlobals -> m: range -> TType + val ParseFormatString: m: range -> fragmentRanges: range list -> diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 070ce6b3e8e..ae2aca5c255 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -6,7 +6,6 @@ module internal FSharp.Compiler.CheckExpressions open System open System.Collections.Generic -open System.Text.RegularExpressions open Internal.Utilities.Collections open Internal.Utilities.Library @@ -146,43 +145,6 @@ exception InvalidInternalsVisibleToAssemblyName of badName: string * fileName: s exception InvalidAttributeTargetForLanguageElement of elementTargets: string array * allowedTargets: string array * range: range -//---------------------------------------------------------------------------------------------- -// 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[..s.Length - 3] - else - s - else - s - /// Compute the available access rights from a particular location in code let ComputeAccessRights eAccessPath eInternalsVisibleCompPaths eFamilyType = AccessibleFrom (eAccessPath :: eInternalsVisibleCompPaths, eFamilyType) @@ -7598,6 +7560,103 @@ and TcFormatStringExpr cenv (overallTy: OverallTy) env m tpenv (fmtString: strin mkString g m fmtString, tpenv ) +/// Lower a string-typed interpolated string to a reflection-free System.String.Concat of its parts, +/// type-checking each part in place. +and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv, m: range, tpenv: UnscopedTyparEnv, parts: SynInterpolatedStringPart list) = + let g = cenv.g + let mSynth = m.MakeSynthetic() + let strLit (s: string) = SynExpr.Const(SynConst.String(s, SynStringKind.Regular, mSynth), mSynth) + let paren (e: SynExpr) = SynExpr.Paren(e, range0, None, mSynth) + + // '(sprintf spec e : string)': format a printf-specifier hole (still reflection-based). + let sprintfOp (spec: string, e: SynExpr) = + let f = mkSynApp1 (mkSynLidGet mSynth [ "Microsoft"; "FSharp"; "Core"; "ExtraTopLevelOperators" ] "sprintf") (strLit spec) mSynth + let call = mkSynApp1 f (paren e) mSynth + SynExpr.Typed(call, SynType.LongIdent(SynLongIdent([ mkSynId mSynth "string" ], [], [ None ])), mSynth) + + // 'String.Format(InvariantCulture, "{0,align:format}", e)': format an aligned or '{e:fmt}' hole. + let stringFormatOp (alignment: SynExpr option, format: Ident option, e: SynExpr) = + let alignText = match alignment with Some (SynExpr.Const (SynConst.Int32 n, _)) -> "," + string n | _ -> "" + let formatText = match format with Some n -> ":" + n.idText | None -> "" + let netFormat = "{0" + alignText + formatText + "}" + let invariant = mkSynLidGet mSynth [ "System"; "Globalization"; "CultureInfo" ] "InvariantCulture" + let args = paren (SynExpr.Tuple(false, [ invariant; strLit netFormat; e ], [ range0; range0 ], mSynth)) + mkSynApp1 (mkSynLidGet mSynth [ "System"; "String" ] "Format") args mSynth + + // Type-check one hole and convert it to a (string expression, may-be-null) pair. + let convertHole (synFill: SynExpr, formatting: SynInterpolationFormatting, tpenv: UnscopedTyparEnv) = + // Constrain the hole to 'constraintTy', then render it with 'string' as for a plain '{x}' hole. Used for + // bare specifiers (no flags/width/precision) that act only as a type annotation: the value renders the + // same through 'string' as through the specifier. ('%u' is not one of these: it reinterprets a signed + // value as unsigned, so it does not match 'string' - e.g. '%u' of -1 is "4294967295".) + let convertViaString constraintTy = + let fill, tpenv = TcExpr cenv (MustEqual constraintTy) env tpenv synFill + (mkCallStringOperator g m (tyOfExpr g fill) fill, false), tpenv + match formatting with + | SynInterpolationFormatting.Printf (spec, _) -> + match spec with + // A bare '%s' requires a string; pass it through (it may be null) instead of formatting via 'sprintf'. + | "%s" -> + let fill, tpenv = TcExpr cenv (MustEqual g.string_ty) env tpenv synFill + (fill, true), tpenv + | "%c" -> convertViaString g.char_ty + | "%d" | "%i" -> convertViaString (CheckFormatStrings.mkFlexibleIntFormatTypar g m) + | "%M" -> convertViaString (CheckFormatStrings.mkFlexibleDecimalFormatTypar g m) + | _ -> + let arg, tpenv = TcExpr cenv (MustEqual g.string_ty) env tpenv (sprintfOp (spec, synFill)) + (arg, false), tpenv + | SynInterpolationFormatting.DotNet (alignment, format) -> + // Type-checking the hole here is also where a function value gets warned about. + let fill, tpenv = TcExprFlex2 cenv (NewInferenceType g) env false tpenv synFill + let fillTy = tyOfExpr g fill + if g.langVersion.SupportsFeature LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg && (isFunTy g fillTy || isDelegateTy g fillTy) then + warning (Error(FSComp.SR.tcFunctionValueUsedAsInterpolatedStringArg (), synFill.Range)) + match alignment, format with + | None, None -> (if isStringTy g fillTy then (fill, true) else (mkCallStringOperator g m fillTy fill, false)), tpenv + | _ -> + // Format the already-checked hole via a synthesized 'String.Format', binding its boxed value + // to a temporary so the hole is not type-checked a second time. Re-checking 'synFill' would + // duplicate any error in it; boxing to 'obj' keeps the 'Format' overload unambiguous (so a + // hole that already failed to check doesn't also leak a confusing 'Format' overload error). + let boxedFill = mkCallBox g m fillTy fill + let tmpVal, _ = mkLocal mSynth "interpHole" (tyOfExpr g boxedFill) + let envInner = AddLocalVal g cenv.tcSink mSynth tmpVal env + let tmpRef = SynExpr.Ident(mkSynId mSynth tmpVal.LogicalName) + let arg, tpenv = TcExpr cenv (MustEqual g.string_ty) envInner tpenv (stringFormatOp (alignment, format, tmpRef)) + (mkCompGenLet mSynth tmpVal boxedFill arg, false), tpenv + + // One (string expression, may-be-null) per non-empty part; a builder (not map) since 'tpenv' threads + // through the holes. Literals and conversions are never null; only a raw string passthrough may be. + let argExprs, tpenv = + let ra = ResizeArray() + let mutable tpenvAcc = tpenv + for part in parts do + match part with + | SynInterpolatedStringPart.String (s, _) -> + if s <> "" then + ra.Add((mkString g m (s.Replace("%%", "%")), false)) + | SynInterpolatedStringPart.FillExpr (synFill, formatting) -> + let argExpr, tpenvAfter = convertHole (synFill, formatting, tpenvAcc) + ra.Add argExpr + tpenvAcc <- tpenvAfter + List.ofSeq ra, tpenvAcc + + let resultExpr = + match argExprs with + | [] -> mkString g m "" + // A lone arg has no Concat to map its null to ""; a possibly-null one coalesces via 'string'. + | [ (single, mustCallStringOp) ]-> + if mustCallStringOp then mkCallStringOperator g m g.string_ty single + else single + | [ (a, _); (b, _) ] -> mkStaticCall_String_Concat2 g m a b + | [ (a, _); (b, _); (c, _) ] -> mkStaticCall_String_Concat3 g m a b c + | [ (a, _); (b, _); (c, _); (d, _) ] -> mkStaticCall_String_Concat4 g m a b c d + | _ -> + let exprs = argExprs |> List.map fst + mkStaticCall_String_Concat_Array g m (mkArray (g.string_ty, exprs, m)) + + TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> resultExpr, tpenv) + /// Check an interpolated string expression and [] warnForFunctionValuesInFillExprs (g: TcGlobals) argTys synFillExprs = match argTys, synFillExprs with @@ -7615,11 +7674,7 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn parts |> List.choose (function | SynInterpolatedStringPart.String _ -> None - | SynInterpolatedStringPart.FillExpr (fillExpr, _) -> - match fillExpr with - // Detect "x" part of "...{x,3}..." - | SynExpr.Tuple (false, [e; SynExpr.Const (SynConst.Int32 _align, _)], _, _) -> Some e - | e -> Some e) + | SynInterpolatedStringPart.FillExpr (fillExpr, _) -> Some fillExpr) let stringFragmentRanges = parts @@ -7687,19 +7742,21 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn let isFormattableString = (match stringKind with Choice2Of2 _ -> true | _ -> false) - // The format string used for checking in CheckFormatStrings. This replaces interpolation holes with %P + // The format string used for checking in CheckFormatStrings, reconstructed from the parts: each + // hole becomes a '%P(...)' marker, prefixed by its printf specifier or alignment. let printfFormatString = parts |> List.map (function | SynInterpolatedStringPart.String (s, _) -> s - | SynInterpolatedStringPart.FillExpr (fillExpr, format) -> + | SynInterpolatedStringPart.FillExpr (_, SynInterpolationFormatting.Printf (spec, _)) -> + spec + "%P()" + | SynInterpolatedStringPart.FillExpr (fillExpr, SynInterpolationFormatting.DotNet (alignment, format)) -> + match fillExpr with + | SynExpr.Tuple (false, _, _, _) -> errorR(Error(FSComp.SR.tcInvalidAlignmentInInterpolatedString(), m)) + | _ -> () let alignText = - match fillExpr with - // Validate and detect ",3" part of "...{x,3}..." - | SynExpr.Tuple (false, args, _, _) -> - match args with - | [_; SynExpr.Const (SynConst.Int32 align, _)] -> string align - | _ -> errorR(Error(FSComp.SR.tcInvalidAlignmentInInterpolatedString(), m)); "" + match alignment with + | Some (SynExpr.Const (SynConst.Int32 align, _)) -> string align | _ -> "" let formatText = match format with None -> "()" | Some n -> "(" + n.idText + ")" "%" + alignText + "P" + formatText ) @@ -7753,75 +7810,28 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn else let str = mkString g m printfFormatString mkCallNewFormat g m printerTy printerArgTy printerResidueTy printerResultTy printerTupleTy str, tpenv + elif isString then + // String-typed interpolation: lower to a reflection-free System.String.Concat of the parts, + // type-checking each hole in place (no separate batch, no flat fill-expression list). + TcInterpolatedStringViaConcat (cenv, overallTy, env, m, tpenv, parts) else - // Type check the expressions filling the holes + // $"...{x}..." used as a PrintfFormat value: build a PrintfFormat that captures the args. let fillExprs, tpenv = TcExprsNoFlexes cenv env m tpenv argTys synFillExprs if g.langVersion.SupportsFeature LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg then warnForFunctionValuesInFillExprs g argTys synFillExprs - // 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 - [] - - | _, 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 - - | fillExpr :: fillExprs, SynInterpolatedStringPart.FillExpr _ :: parts -> - concatenable (fillExpr :: acc) fillExprs parts + let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m) - 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] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkStaticCall_String_Concat4 g m p1 p2 p3 p4, tpenv) - | [p1; p2; p3] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkStaticCall_String_Concat3 g m p1 p2 p3, tpenv) - | [p1; p2] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> 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_withNulls, 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 - ) + let argsExpr = mkArray (g.obj_ty_withNulls, fillExprsBoxed, m) + let percentATysExpr = + if percentATys.Length = 0 then + mkNull m (mkArrayType g g.system_Type_ty) else - fmtExpr, tpenv + let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList + mkArray (g.system_Type_ty, tyExprs, m) + + MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None, tpenv // The case for $"..." used as type FormattableString or IFormattable | Choice2Of2 createFormattableStringMethod -> diff --git a/src/Compiler/Service/SynExpr.fs b/src/Compiler/Service/SynExpr.fs index d1d0c6eb4c3..9300855b240 100644 --- a/src/Compiler/Service/SynExpr.fs +++ b/src/Compiler/Service/SynExpr.fs @@ -1090,7 +1090,7 @@ module SynExpr = | SynExpr.InterpolatedString(contents = contents), Dangling.Problematic _ -> contents |> List.exists (function - | SynInterpolatedStringPart.FillExpr(qualifiers = Some _) -> true + | SynInterpolatedStringPart.FillExpr(formatting = SynInterpolationFormatting.DotNet(format = Some _)) -> true | _ -> false) // { (!x) with … } diff --git a/src/Compiler/SyntaxTree/ParseHelpers.fs b/src/Compiler/SyntaxTree/ParseHelpers.fs index b329d48ee34..134f7029ea1 100644 --- a/src/Compiler/SyntaxTree/ParseHelpers.fs +++ b/src/Compiler/SyntaxTree/ParseHelpers.fs @@ -69,6 +69,50 @@ let rhs2 (parseState: IParseState) i j = /// Get the range corresponding to one of the r.h.s. symbols of a grammar rule while it is being reduced let rhs parseState i = rhs2 parseState i i +/// Split a trailing printf specifier (e.g. "%d") off an interpolated-string literal that precedes a +/// hole. '%%' is a literal escape, not a specifier. +let peelTrailingPrintfSpecifier (litText: string) : string * string option = + let n = litText.Length + let mutable i = 0 + let mutable specStart = -1 + + while i < n && specStart < 0 do + if litText[i] = '%' then + if i + 1 < n && litText[i + 1] = '%' then + i <- i + 2 // '%%' escape, keep scanning + else + specStart <- i // start of a real specifier + else + i <- i + 1 + + // A real printf specifier ends, immediately before the hole, with a type character. Anything else + // (for example the explicit '%P(' placeholder syntax) is left in the literal untouched. + if specStart < 0 || "bscdiuxXoBeEfFgGMOAat".IndexOf litText[n - 1] < 0 then + litText, None + else + litText[.. specStart - 1], Some litText[specStart..] + +/// Build the [String literal; FillExpr hole] pair for one interpolation hole, splitting the '{x,n}' +/// alignment out of its tuple encoding and peeling a trailing printf specifier onto the hole. +let mkInterpolatedStringFillParts (litText: string, litRange: range, fill: SynExpr * Ident option) = + let fillExpr, qualifier = fill + + let holeExpr, alignment = + match fillExpr with + | SynExpr.Tuple(false, [ e; (SynExpr.Const(SynConst.Int32 _, _) as n) ], _, _) -> e, Some n + | _ -> fillExpr, None + + let litValue, formatting = + match qualifier, alignment with + | None, None -> + match peelTrailingPrintfSpecifier litText with + | lit, Some spec -> lit, SynInterpolationFormatting.Printf(spec, litRange) + | _, None -> litText, SynInterpolationFormatting.DotNet(None, None) + | _ -> litText, SynInterpolationFormatting.DotNet(alignment, qualifier) + + [ SynInterpolatedStringPart.String(litValue, litRange) + SynInterpolatedStringPart.FillExpr(holeExpr, formatting) ] + //------------------------------------------------------------------------ // Parsing/lexing: status of #if/#endif processing in lexing, used for continuations // for whitespace tokens in parser specification. diff --git a/src/Compiler/SyntaxTree/ParseHelpers.fsi b/src/Compiler/SyntaxTree/ParseHelpers.fsi index aae952d210c..56b48e38e90 100644 --- a/src/Compiler/SyntaxTree/ParseHelpers.fsi +++ b/src/Compiler/SyntaxTree/ParseHelpers.fsi @@ -38,6 +38,16 @@ val rhs2: parseState: IParseState -> i: int -> j: int -> range val rhs: parseState: IParseState -> i: int -> range +/// Peel a trailing printf specifier (e.g. "%d") off an interpolated-string literal that precedes a +/// hole, returning the literal without it and the specifier text. '%%' is a literal escape. +val peelTrailingPrintfSpecifier: litText: string -> string * string option + +/// Build the [String literal; FillExpr hole] pair for one interpolation hole, splitting the +/// '{x,n}' alignment out of its tuple encoding and peeling a trailing printf specifier off the +/// literal onto the hole. +val mkInterpolatedStringFillParts: + litText: string * litRange: range * fill: (SynExpr * Ident option) -> SynInterpolatedStringPart list + type LexerIfdefStackEntry = | IfDefIf | IfDefElse diff --git a/src/Compiler/SyntaxTree/SyntaxTree.fs b/src/Compiler/SyntaxTree/SyntaxTree.fs index f35bb3297de..0e92555a087 100644 --- a/src/Compiler/SyntaxTree/SyntaxTree.fs +++ b/src/Compiler/SyntaxTree/SyntaxTree.fs @@ -875,7 +875,12 @@ type SynExprRecordField = [] type SynInterpolatedStringPart = | String of value: string * range: range - | FillExpr of fillExpr: SynExpr * qualifiers: Ident option + | FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting + +[] +type SynInterpolationFormatting = + | DotNet of alignment: SynExpr option * format: Ident option + | Printf of specifier: string * range: range [] type SynSimplePat = diff --git a/src/Compiler/SyntaxTree/SyntaxTree.fsi b/src/Compiler/SyntaxTree/SyntaxTree.fsi index 8b152ba2d69..c100fa99089 100644 --- a/src/Compiler/SyntaxTree/SyntaxTree.fsi +++ b/src/Compiler/SyntaxTree/SyntaxTree.fsi @@ -999,7 +999,16 @@ type SynExprRecordField = [] type SynInterpolatedStringPart = | String of value: string * range: range - | FillExpr of fillExpr: SynExpr * qualifiers: Ident option + | FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting + +/// Represents how an interpolation hole in an interpolated string is formatted. +[] +type SynInterpolationFormatting = + /// .NET-style formatting: optional alignment '{x,n}' and optional format '{x:fmt}'. + | DotNet of alignment: SynExpr option * format: Ident option + + /// printf-style formatting: a single specifier, the '%d' in '%d{x}'. + | Printf of specifier: string * range: range /// Represents a syntax tree for simple F# patterns [] diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 6dfbd42f98c..4d5678b6c3a 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -792,6 +792,7 @@ type TcGlobals( let v_byte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "byte" , None , Some "ToByte", [vara], ([[varaTy]], v_byte_ty)) let v_sbyte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "sbyte" , None , Some "ToSByte", [vara], ([[varaTy]], v_sbyte_ty)) + let v_string_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "string" , None , Some "ToString", [vara], ([[varaTy]], v_string_ty)) let v_int16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int16" , None , Some "ToInt16", [vara], ([[varaTy]], v_int16_ty)) let v_uint16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "uint16" , None , Some "ToUInt16", [vara], ([[varaTy]], v_uint16_ty)) let v_int32_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int32" , None , Some "ToInt32", [vara], ([[varaTy]], v_int32_ty)) @@ -1594,6 +1595,7 @@ type TcGlobals( member _.byte_operator_info = v_byte_operator_info member _.sbyte_operator_info = v_sbyte_operator_info + member _.string_operator_info = v_string_operator_info member _.int16_operator_info = v_int16_operator_info member _.uint16_operator_info = v_uint16_operator_info member _.int32_operator_info = v_int32_operator_info @@ -1707,7 +1709,6 @@ type TcGlobals( member _.seq_map_info = v_seq_map_info member _.seq_singleton_info = v_seq_singleton_info member _.seq_empty_info = v_seq_empty_info - member _.sprintf_info = v_sprintf_info member _.new_format_info = v_new_format_info member _.unbox_info = v_unbox_info member _.get_generic_comparer_info = v_get_generic_comparer_info diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index e27bc1605a2..e525f8777ca 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -939,6 +939,8 @@ type internal TcGlobals = member sbyte_operator_info: IntrinsicValRef + member string_operator_info: IntrinsicValRef + member sbyte_tcr: TypedTree.EntityRef member sbyte_ty: TypedTree.TType @@ -1003,8 +1005,6 @@ type internal TcGlobals = member splice_raw_expr_vref: TypedTree.ValRef - member sprintf_info: IntrinsicValRef - member sprintf_vref: TypedTree.ValRef member string_ty: TypedTree.TType diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs index bdebaf40ff5..bcb9ba6bd74 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs @@ -1366,6 +1366,9 @@ module internal Makers = let mkCallNewFormat (g: TcGlobals) m aty bty cty dty ety formatStringExpr = mkApps g (typedExprForIntrinsic g m g.new_format_info, [ [ aty; bty; cty; dty; ety ] ], [ formatStringExpr ], m) + let mkCallStringOperator (g: TcGlobals) m argTy e = + mkApps g (typedExprForIntrinsic g m g.string_operator_info, [ [ argTy ] ], [ e ], m) + let tryMkCallBuiltInWitness (g: TcGlobals) traitInfo argExprs m = let info, tinst = g.MakeBuiltInWitnessInfo traitInfo let vref = ValRefForIntrinsic info @@ -1449,9 +1452,6 @@ module internal Makers = let mkCallSeqEmpty g m ty1 = mkApps g (typedExprForIntrinsic g m g.seq_empty_info, [ [ ty1 ] ], [], m) - let mkCall_sprintf (g: TcGlobals) m funcTy fmtExpr fillExprs = - mkApps g (typedExprForIntrinsic g m g.sprintf_info, [ [ funcTy ] ], fmtExpr :: fillExprs, m) - let mkCallDeserializeQuotationFSharp20Plus g m e1 e2 e3 e4 = let args = [ e1; e2; e3; e4 ] mkApps g (typedExprForIntrinsic g m g.deserialize_quoted_FSharp_20_plus_info, [], [ mkRefTupledNoTypes g m args ], m) diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi index ad90c5c818c..c9bbcdb0ddf 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi @@ -208,6 +208,9 @@ module internal Makers = val mkCallNewFormat: TcGlobals -> range -> TType -> TType -> TType -> TType -> TType -> formatStringExpr: Expr -> Expr + /// Build a call to the 'string' operator (Operators.ToString) at the given argument type. + val mkCallStringOperator: TcGlobals -> range -> argTy: TType -> Expr -> Expr + val mkCallGetGenericComparer: TcGlobals -> range -> Expr val mkCallGetGenericEREqualityComparer: TcGlobals -> range -> Expr @@ -401,9 +404,6 @@ module internal Makers = val mkCallSeqEmpty: TcGlobals -> range -> TType -> Expr - /// Make a call to the 'isprintf' function for string interpolation - val mkCall_sprintf: g: TcGlobals -> m: range -> funcTy: TType -> fmtExpr: Expr -> fillExprs: Expr list -> Expr - val mkCallDeserializeQuotationFSharp20Plus: TcGlobals -> range -> Expr -> Expr -> Expr -> Expr -> Expr val mkCallDeserializeQuotationFSharp40Plus: TcGlobals -> range -> Expr -> Expr -> Expr -> Expr -> Expr -> Expr diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy index 01120123a36..6048b59f7c2 100644 --- a/src/Compiler/pars.fsy +++ b/src/Compiler/pars.fsy @@ -7162,7 +7162,7 @@ interpolatedStringParts: { [ SynInterpolatedStringPart.String(fst $1, rhs parseState 1) ] } | INTERP_STRING_PART interpolatedStringFill interpolatedStringParts - { SynInterpolatedStringPart.String(fst $1, rhs parseState 1) :: SynInterpolatedStringPart.FillExpr $2 :: $3 } + { mkInterpolatedStringFillParts (fst $1, rhs parseState 1, $2) @ $3 } | INTERP_STRING_PART interpolatedStringParts { let rbrace = parseState.InputEndPosition 1 @@ -7176,7 +7176,7 @@ interpolatedStringParts: interpolatedString: | INTERP_STRING_BEGIN_PART interpolatedStringFill interpolatedStringParts { let s, synStringKind, _ = $1 - SynInterpolatedStringPart.String(s, rhs parseState 1) :: SynInterpolatedStringPart.FillExpr $2 :: $3, synStringKind } + mkInterpolatedStringFillParts (s, rhs parseState 1, $2) @ $3, synStringKind } | INTERP_STRING_BEGIN_END { let s, synStringKind, _ = $1 diff --git a/tests/AheadOfTime/NativeAOT/NativeAOT_Test.fsproj b/tests/AheadOfTime/NativeAOT/NativeAOT_Test.fsproj new file mode 100644 index 00000000000..1fa87907110 --- /dev/null +++ b/tests/AheadOfTime/NativeAOT/NativeAOT_Test.fsproj @@ -0,0 +1,36 @@ + + + + Exe + net9.0 + preview + true + + + + true + true + true + true + win-x64 + + + + $(LocalFSharpBuildBinPath)/FSharp.Build.dll + $(LocalFSharpBuildBinPath)/fsc.dll + $(LocalFSharpBuildBinPath)/fsc.dll + False + True + + + + + + + + + + + + + diff --git a/tests/AheadOfTime/NativeAOT/Program.fs b/tests/AheadOfTime/NativeAOT/Program.fs new file mode 100644 index 00000000000..dce1bbaf53e --- /dev/null +++ b/tests/AheadOfTime/NativeAOT/Program.fs @@ -0,0 +1,34 @@ +module Program + +open System + +// Check a rendering against an expected string literal; a mismatch prints a "FAILED" line. +let check (actual: string, expected: string) = + if actual <> expected then + Console.WriteLine $"FAILED: expected '{expected}' but got '{actual}'" + +let runChecks () = + let x = 42 + let name = "world" + let pi = 3.14159 + let initial = 'F' + check ($"answer = {x}", "answer = 42") + check ($"hello {name}", "hello world") + check ($"pi ~ {pi:F2}", "pi ~ 3.14") + check ($"padded:{x,6}", "padded: 42") + check ($"greeting %s{name}", "greeting world") + // Bare '%d'/'%i'/'%c'/'%M' specifiers lower to the same reflection-free path as a plain hole. + check ($"answer = %d{x}", "answer = 42") + check ($"initial = %c{initial}", "initial = F") + + // The following use printf specifiers that still route through 'sprintf', so they would make the + // NativeAOT publish fail with IL2026/IL2070/IL3050. + // check ($"pi ~ %.2f{pi}", "pi ~ 3.14") + // check ($"value = %A{x}", "value = 42") + +[] +let main _ = + runChecks () + // Success sentinel; a failed check above printed a "FAILED" line first, so the output won't be just this. + Console.WriteLine "Finished" + 0 diff --git a/tests/AheadOfTime/NativeAOT/check.cmd b/tests/AheadOfTime/NativeAOT/check.cmd new file mode 100644 index 00000000000..4eefff011c5 --- /dev/null +++ b/tests/AheadOfTime/NativeAOT/check.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0check.ps1"""" diff --git a/tests/AheadOfTime/NativeAOT/check.ps1 b/tests/AheadOfTime/NativeAOT/check.ps1 new file mode 100644 index 00000000000..dc69fb765df --- /dev/null +++ b/tests/AheadOfTime/NativeAOT/check.ps1 @@ -0,0 +1,37 @@ +# Publish the test project with NativeAOT and check that it runs. +# +# The point of this check is that the publish succeeds: a string-typed interpolated string +# must lower to a reflection-free form (System.String.Concat), not the reflection-based +# printf engine. If it regresses to printf, FSharp.Reflection becomes statically reachable, +# NativeAOT analysis emits IL2026/IL2070/IL3050, TreatWarningsAsErrors turns them into errors, +# and this publish fails. + +$ErrorActionPreference = "Stop" + +$root = "NativeAOT_Test" +$tfm = "net9.0" + +$cwd = Get-Location +Set-Location $PSScriptRoot + +dotnet publish -restore -c release -f:$tfm "$root.fsproj" -bl:"$PSScriptRoot/../../../artifacts/log/Release/AheadOfTime/NativeAOT/$root.binlog" +if (-not ($LASTEXITCODE -eq 0)) { + Set-Location $cwd + Write-Error "NativeAOT publish failed with exit code $LASTEXITCODE" -ErrorAction Stop +} + +$exe = Join-Path $PSScriptRoot "bin/release/$tfm/win-x64/publish/$root.exe" +$output = (& $exe) -join "`n" +$exitCode = $LASTEXITCODE +Set-Location $cwd + +# The app prints a "FAILED" line per mismatch and "Finished" last, so its output is exactly "Finished" only if all checks passed. +if (-not ($exitCode -eq 0)) { + Write-Error "NativeAOT app crashed with exit code $exitCode.`nOutput:`n$output" -ErrorAction Stop +} + +if ($output.Trim() -ne "Finished") { + Write-Error "NativeAOT interpolation checks failed.`nOutput:`n$output" -ErrorAction Stop +} + +Write-Host "NativeAOT interpolated-string test passed." diff --git a/tests/AheadOfTime/check.ps1 b/tests/AheadOfTime/check.ps1 index e8fd72b57e5..5c1de83b903 100644 --- a/tests/AheadOfTime/check.ps1 +++ b/tests/AheadOfTime/check.ps1 @@ -2,3 +2,4 @@ Write-Host "AheadOfTime: check1.ps1" Equality\check.ps1 Trimming\check.ps1 +NativeAOT\check.ps1 diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs index 38729fc70be..57b3c0ae5ec 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/StringFormatAndInterpolation.fs @@ -90,6 +90,90 @@ IL_0014: call string [runtime]System.String::Concat(string, string) IL_0019: ret"""] + [] + let ``Interpolated string with more than 4 parts is lowered to a System.String.Concat array`` () = + FSharp """ +module StringFormatAndInterpolation + +let f (a: string, b: string, c: string, d: string, e: string) = $"{a}{b}{c}{d}{e}" + """ + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldc.i4.5 +IL_0001: newarr [runtime]System.String +IL_0006: dup +IL_0007: ldc.i4.0 +IL_0008: ldarg.0 +IL_0009: stelem [runtime]System.String +IL_000e: dup +IL_000f: ldc.i4.1 +IL_0010: ldarg.1 +IL_0011: stelem [runtime]System.String +IL_0016: dup +IL_0017: ldc.i4.2 +IL_0018: ldarg.2 +IL_0019: stelem [runtime]System.String +IL_001e: dup +IL_001f: ldc.i4.3 +IL_0020: ldarg.3 +IL_0021: stelem [runtime]System.String +IL_0026: dup +IL_0027: ldc.i4.4 +IL_0028: ldarg.s e +IL_002a: stelem [runtime]System.String +IL_002f: call string [runtime]System.String::Concat(string[]) +IL_0034: ret"""] + + [] + let ``String-typed interpolation holes are concatenated directly, with no string conversion or null check`` () = + FSharp """ +module StringFormatAndInterpolation + +let f (a: string, b: string) = $"{a}{b.ToLower()}" + """ + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldarg.0 +IL_0001: ldarg.1 +IL_0002: callvirt instance string [runtime]System.String::ToLower() +IL_0007: call string [runtime]System.String::Concat(string, + string) +IL_000c: ret"""] + + [] + let ``Interpolated string with a single float hole is rendered via an invariant-culture ToString`` () = + FSharp """ +module StringFormatAndInterpolation + +let f (x: float) = $"{x}" + """ + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldarga.s x +IL_0002: ldnull +IL_0003: call class [netstandard]System.Globalization.CultureInfo [netstandard]System.Globalization.CultureInfo::get_InvariantCulture() +IL_0008: call instance string [netstandard]System.Double::ToString(string, + class [netstandard]System.IFormatProvider) +IL_000d: ret"""] + + [] + let ``Interpolated string with a single bool hole is rendered via ToString`` () = + FSharp """ +module StringFormatAndInterpolation + +let f (x: bool) = $"{x}" + """ + |> compile + |> shouldSucceed + |> verifyIL [""" +IL_0000: ldarga.s x +IL_0002: constrained. [runtime]System.Boolean +IL_0008: callvirt instance string [netstandard]System.Object::ToString() +IL_000d: ret"""] + [] let ``Interpolated string with concat converts to span implicitly`` () = let compilation = diff --git a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs index 9894fa68126..b2d7a191fd9 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs @@ -102,6 +102,16 @@ printfn \"%s\" s" |> shouldSucceed |> withStdOutContains "% 42" + [] + let ``Interpolation holes are rendered with invariant culture`` () = + Fsx """ +System.Threading.Thread.CurrentThread.CurrentCulture <- System.Globalization.CultureInfo "de-DE" +printf "%s" $"{1.5}" + """ + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "1.5" + [] let ``Percent signs separated by format specifier's flags`` () = Fsx """ diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 4034ba8064d..2292e107142 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -8044,8 +8044,8 @@ FSharp.Compiler.Syntax.SynInterfaceImpl: Microsoft.FSharp.Core.FSharpOption`1[FS FSharp.Compiler.Syntax.SynInterfaceImpl: System.String ToString() FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: FSharp.Compiler.Syntax.SynExpr fillExpr FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: FSharp.Compiler.Syntax.SynExpr get_fillExpr() -FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident] get_qualifiers() -FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident] qualifiers +FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: FSharp.Compiler.Syntax.SynInterpolationFormatting formatting +FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr: FSharp.Compiler.Syntax.SynInterpolationFormatting get_formatting() FSharp.Compiler.Syntax.SynInterpolatedStringPart+String: FSharp.Compiler.Text.Range get_range() FSharp.Compiler.Syntax.SynInterpolatedStringPart+String: FSharp.Compiler.Text.Range range FSharp.Compiler.Syntax.SynInterpolatedStringPart+String: System.String get_value() @@ -8056,7 +8056,7 @@ FSharp.Compiler.Syntax.SynInterpolatedStringPart: Boolean IsFillExpr FSharp.Compiler.Syntax.SynInterpolatedStringPart: Boolean IsString FSharp.Compiler.Syntax.SynInterpolatedStringPart: Boolean get_IsFillExpr() FSharp.Compiler.Syntax.SynInterpolatedStringPart: Boolean get_IsString() -FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInterpolatedStringPart NewFillExpr(FSharp.Compiler.Syntax.SynExpr, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident]) +FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInterpolatedStringPart NewFillExpr(FSharp.Compiler.Syntax.SynExpr, FSharp.Compiler.Syntax.SynInterpolationFormatting) FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInterpolatedStringPart NewString(System.String, FSharp.Compiler.Text.Range) FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInterpolatedStringPart+FillExpr FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInterpolatedStringPart+String @@ -8064,6 +8064,28 @@ FSharp.Compiler.Syntax.SynInterpolatedStringPart: FSharp.Compiler.Syntax.SynInte FSharp.Compiler.Syntax.SynInterpolatedStringPart: Int32 Tag FSharp.Compiler.Syntax.SynInterpolatedStringPart: Int32 get_Tag() FSharp.Compiler.Syntax.SynInterpolatedStringPart: System.String ToString() +FSharp.Compiler.Syntax.SynInterpolationFormatting+DotNet: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident] format +FSharp.Compiler.Syntax.SynInterpolationFormatting+DotNet: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident] get_format() +FSharp.Compiler.Syntax.SynInterpolationFormatting+DotNet: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.SynExpr] alignment +FSharp.Compiler.Syntax.SynInterpolationFormatting+DotNet: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.SynExpr] get_alignment() +FSharp.Compiler.Syntax.SynInterpolationFormatting+Printf: FSharp.Compiler.Text.Range get_range() +FSharp.Compiler.Syntax.SynInterpolationFormatting+Printf: FSharp.Compiler.Text.Range range +FSharp.Compiler.Syntax.SynInterpolationFormatting+Printf: System.String get_specifier() +FSharp.Compiler.Syntax.SynInterpolationFormatting+Printf: System.String specifier +FSharp.Compiler.Syntax.SynInterpolationFormatting+Tags: Int32 DotNet +FSharp.Compiler.Syntax.SynInterpolationFormatting+Tags: Int32 Printf +FSharp.Compiler.Syntax.SynInterpolationFormatting: Boolean IsDotNet +FSharp.Compiler.Syntax.SynInterpolationFormatting: Boolean IsPrintf +FSharp.Compiler.Syntax.SynInterpolationFormatting: Boolean get_IsDotNet() +FSharp.Compiler.Syntax.SynInterpolationFormatting: Boolean get_IsPrintf() +FSharp.Compiler.Syntax.SynInterpolationFormatting: FSharp.Compiler.Syntax.SynInterpolationFormatting NewDotNet(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.SynExpr], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.Ident]) +FSharp.Compiler.Syntax.SynInterpolationFormatting: FSharp.Compiler.Syntax.SynInterpolationFormatting NewPrintf(System.String, FSharp.Compiler.Text.Range) +FSharp.Compiler.Syntax.SynInterpolationFormatting: FSharp.Compiler.Syntax.SynInterpolationFormatting+DotNet +FSharp.Compiler.Syntax.SynInterpolationFormatting: FSharp.Compiler.Syntax.SynInterpolationFormatting+Printf +FSharp.Compiler.Syntax.SynInterpolationFormatting: FSharp.Compiler.Syntax.SynInterpolationFormatting+Tags +FSharp.Compiler.Syntax.SynInterpolationFormatting: Int32 Tag +FSharp.Compiler.Syntax.SynInterpolationFormatting: Int32 get_Tag() +FSharp.Compiler.Syntax.SynInterpolationFormatting: System.String ToString() FSharp.Compiler.Syntax.SynLetOrUse: Boolean IsBang FSharp.Compiler.Syntax.SynLetOrUse: Boolean IsFromSource FSharp.Compiler.Syntax.SynLetOrUse: Boolean IsRecursive diff --git a/tests/fsharp/core/quotes/test.fsx b/tests/fsharp/core/quotes/test.fsx index 30ed5ba331a..54364a56125 100644 --- a/tests/fsharp/core/quotes/test.fsx +++ b/tests/fsharp/core/quotes/test.fsx @@ -5884,10 +5884,8 @@ module Interpolation = let interpolatedWithLiteralQuoted = <@ $"abc {1} def" @> let actual2 = interpolatedWithLiteralQuoted.ToString() checkStrings "brewbreebrwhat2" actual2 - """Call (None, PrintFormatToString, - [NewObject (PrintfFormat`5, Value ("abc %P() def"), - NewArray (Object, Call (None, Box, [Value (1)])), - Value ())])""" + """Call (None, Concat, + [Value ("abc "), Call (None, ToString, [Value (1)]), Value (" def")])""" module TestQuotationWithIdenticalStaticInstanceMethods = type C() = diff --git a/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInModule.fs.bsl b/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInModule.fs.bsl index d7fb308c07b..1a16a38174d 100644 --- a/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInModule.fs.bsl +++ b/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInModule.fs.bsl @@ -21,7 +21,8 @@ ImplFile InterpolatedString ([String (" ", (3,8--4,1)); - FillExpr (Const (Int32 0, (4,1--4,2)), None); + FillExpr + (Const (Int32 0, (4,1--4,2)), DotNet (None, None)); String ("", (4,2--4,4))], Regular, (3,8--4,4)), (2,8--2,9), Yes (2,4--4,4), { LeadingKeyword = Let (2,4--2,7) diff --git a/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInNestedLet.fs.bsl b/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInNestedLet.fs.bsl index 1455d3f425e..0c57f36ed8a 100644 --- a/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInNestedLet.fs.bsl +++ b/tests/service/data/SyntaxTree/String/InterpolatedStringOffsideInNestedLet.fs.bsl @@ -27,9 +27,10 @@ ImplFile InterpolatedString ([String (" ", (3,8--4,1)); - FillExpr (Const (Int32 0, (4,1--4,2)), None); - String ("", (4,2--4,4))], Regular, (3,8--4,4)), - (2,8--2,9), Yes (2,4--4,4), + FillExpr + (Const (Int32 0, (4,1--4,2)), + DotNet (None, None)); String ("", (4,2--4,4))], + Regular, (3,8--4,4)), (2,8--2,9), Yes (2,4--4,4), { LeadingKeyword = Let (2,4--2,7) InlineKeyword = None EqualsRange = Some (2,10--2,11) })] diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl index bcf55431e8f..9edcf8db177 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl @@ -26,7 +26,8 @@ ImplFile (None, SynValInfo ([], SynArgInfo ([], false, None)), None), Named (SynIdent (x, None), false, None, (2,4--2,5)), None, InterpolatedString - ([String ("", (2,7--2,10)); FillExpr (Ident n, None); + ([String ("", (2,7--2,10)); + FillExpr (Ident n, DotNet (None, None)); String ("", (2,11--2,13))], Regular, (2,7--2,13)), (2,4--2,5), Yes (2,0--2,13), { LeadingKeyword = Let (2,0--2,3) InlineKeyword = None diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindRegular.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindRegular.fs.bsl index 7026b9a1034..46da672fdeb 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindRegular.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindRegular.fs.bsl @@ -14,7 +14,8 @@ ImplFile Named (SynIdent (s, None), false, None, (2,4--2,5)), None, InterpolatedString ([String ("yo ", (2,8--2,14)); - FillExpr (Const (Int32 42, (2,14--2,16)), None); + FillExpr + (Const (Int32 42, (2,14--2,16)), DotNet (None, None)); String ("", (2,16--2,18))], Regular, (2,8--2,18)), (2,4--2,5), Yes (2,0--2,18), { LeadingKeyword = Let (2,0--2,3) InlineKeyword = None diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindTripleQuote.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindTripleQuote.fs.bsl index 9e42b6455ce..3945da38dce 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindTripleQuote.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindTripleQuote.fs.bsl @@ -17,7 +17,8 @@ ImplFile Named (SynIdent (s, None), false, None, (2,4--2,5)), None, InterpolatedString ([String ("yo ", (2,8--2,16)); - FillExpr (Const (Int32 42, (2,16--2,18)), None); + FillExpr + (Const (Int32 42, (2,16--2,18)), DotNet (None, None)); String ("", (2,18--2,22))], TripleQuote, (2,8--2,22)), (2,4--2,5), Yes (2,0--2,22), { LeadingKeyword = Let (2,0--2,3) InlineKeyword = None diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindVerbatim.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindVerbatim.fs.bsl index 2fb03900ebf..6c84a3c2f0f 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindVerbatim.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithSynStringKindVerbatim.fs.bsl @@ -16,15 +16,15 @@ ImplFile Named (SynIdent (s, None), false, None, (2,4--2,5)), None, InterpolatedString ([String ("Migrate notes of file "", (2,8--2,36)); - FillExpr (Ident oldId, None); + FillExpr (Ident oldId, DotNet (None, None)); String ("" to new file "", (2,41--2,60)); - FillExpr (Ident newId, None); String ("".", (2,65--2,70))], - Verbatim, (2,8--2,70)), (2,4--2,5), Yes (2,0--2,70), - { LeadingKeyword = Let (2,0--2,3) - InlineKeyword = None - EqualsRange = Some (2,6--2,7) })], (2,0--2,70), - { InKeyword = None })], PreXmlDocEmpty, [], None, (2,0--3,0), - { LeadingKeyword = None })], (true, true), + FillExpr (Ident newId, DotNet (None, None)); + String ("".", (2,65--2,70))], Verbatim, (2,8--2,70)), + (2,4--2,5), Yes (2,0--2,70), { LeadingKeyword = Let (2,0--2,3) + InlineKeyword = None + EqualsRange = Some (2,6--2,7) })], + (2,0--2,70), { InKeyword = None })], PreXmlDocEmpty, [], None, + (2,0--3,0), { LeadingKeyword = None })], (true, true), { ConditionalDirectives = [] WarnDirectives = [] CodeComments = [] }, set [])) diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars.fs.bsl index 3bbd9b2ba46..3e12edd2257 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars.fs.bsl @@ -17,9 +17,11 @@ ImplFile Named (SynIdent (s, None), false, None, (2,4--2,5)), None, InterpolatedString ([String ("1 + ", (2,8--2,21)); - FillExpr (Const (Int32 41, (2,21--2,23)), None); + FillExpr + (Const (Int32 41, (2,21--2,23)), DotNet (None, None)); String (" = ", (2,23--2,32)); - FillExpr (Const (Int32 6, (2,32--2,33)), None); + FillExpr + (Const (Int32 6, (2,32--2,33)), DotNet (None, None)); String (" * 7", (2,33--2,43))], TripleQuote, (2,8--2,43)), (2,4--2,5), Yes (2,0--2,43), { LeadingKeyword = Let (2,0--2,3) InlineKeyword = None diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars2.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars2.fs.bsl index ca0ac31fffc..c280d8d831b 100644 --- a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars2.fs.bsl +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringWithTripleQuoteMultipleDollars2.fs.bsl @@ -10,7 +10,7 @@ ImplFile [Expr (InterpolatedString ([String ("", (2,0--2,9)); - FillExpr (Const (Int32 5, (2,9--2,10)), None); + FillExpr (Const (Int32 5, (2,9--2,10)), DotNet (None, None)); String ("", (2,10--2,16))], TripleQuote, (2,0--2,16)), (2,0--2,16))], PreXmlDocEmpty, [], None, (2,0--2,16), { LeadingKeyword = None })], (true, true),