@@ -6,7 +6,6 @@ module internal FSharp.Compiler.CheckExpressions
66
77open System
88open System.Collections.Generic
9- open System.Text.RegularExpressions
109
1110open Internal.Utilities.Collections
1211open Internal.Utilities.Library
@@ -146,43 +145,6 @@ exception InvalidInternalsVisibleToAssemblyName of badName: string * fileName: s
146145
147146exception InvalidAttributeTargetForLanguageElement of elementTargets: string array * allowedTargets: string array * range: range
148147
149- //----------------------------------------------------------------------------------------------
150- // Helpers for determining if/what specifiers a string has.
151- // Used to decide if interpolated string can be lowered to a concat call.
152- // We don't care about single- vs multi-$ strings here, because lexer took care of that already.
153- //----------------------------------------------------------------------------------------------
154- [<return: Struct>]
155- let (|HasFormatSpecifier|_|) (s: string) =
156- if
157- Regex.IsMatch(
158- s,
159- // Regex pattern for something like: %[flags][width][.precision][type]
160- """
161- (^|[^%]) # Start with beginning of string or any char other than '%'
162- (%%)*% # followed by an odd number of '%' chars
163- [+-0 ]{0,3} # optionally followed by flags
164- (\d+)? # optionally followed by width
165- (\.\d+)? # optionally followed by .precision
166- [bscdiuxXoBeEfFgGMOAat] # and then a char that determines specifier's type
167- """,
168- RegexOptions.Compiled ||| RegexOptions.IgnorePatternWhitespace)
169- then
170- ValueSome HasFormatSpecifier
171- else
172- ValueNone
173-
174- // Removes trailing "%s" unless it was escaped by another '%' (checks for odd sequence of '%' before final "%s")
175- let (|WithTrailingStringSpecifierRemoved|) (s: string) =
176- if s.EndsWith "%s" then
177- let i = s.AsSpan(0, s.Length - 2).LastIndexOfAnyExcept '%'
178- let diff = s.Length - 2 - i
179- if diff &&& 1 <> 0 then
180- s[..s.Length - 3]
181- else
182- s
183- else
184- s
185-
186148/// Compute the available access rights from a particular location in code
187149let ComputeAccessRights eAccessPath eInternalsVisibleCompPaths eFamilyType =
188150 AccessibleFrom (eAccessPath :: eInternalsVisibleCompPaths, eFamilyType)
@@ -7598,6 +7560,65 @@ and TcFormatStringExpr cenv (overallTy: OverallTy) env m tpenv (fmtString: strin
75987560 mkString g m fmtString, tpenv
75997561 )
76007562
7563+ /// Lower a string-typed interpolated string to a reflection-free System.String.Concat of its parts.
7564+ /// 'holeIsString' flags, in order, the fill expressions that are already of type string.
7565+ and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv, m: range, tpenv: UnscopedTyparEnv, parts: SynInterpolatedStringPart list, holeIsString: bool list) =
7566+ let mSynth = m.MakeSynthetic()
7567+ let strLit (s: string) = SynExpr.Const(SynConst.String(s, SynStringKind.Regular, mSynth), mSynth)
7568+ let paren (e: SynExpr) = SynExpr.Paren(e, range0, None, mSynth)
7569+
7570+ // '(string e)': convert any value to a string using invariant culture.
7571+ let stringOp (e: SynExpr) =
7572+ mkSynApp1 (mkSynLidGet mSynth [ "Microsoft"; "FSharp"; "Core"; "Operators" ] "string") (paren e) mSynth
7573+
7574+ // '(sprintf spec e : string)': format a printf-specifier hole (still reflection-based).
7575+ let sprintfOp (spec: string, e: SynExpr) =
7576+ let f = mkSynApp1 (mkSynLidGet mSynth [ "Microsoft"; "FSharp"; "Core"; "ExtraTopLevelOperators" ] "sprintf") (strLit spec) mSynth
7577+ let call = mkSynApp1 f (paren e) mSynth
7578+ SynExpr.Typed(call, SynType.LongIdent(SynLongIdent([ mkSynId mSynth "string" ], [], [ None ])), mSynth)
7579+
7580+ // 'String.Format(InvariantCulture, "{0,align:format}", e)': format an aligned or '{e:fmt}' hole.
7581+ let stringFormatOp (alignment: SynExpr option, format: Ident option, e: SynExpr) =
7582+ let alignText = match alignment with Some (SynExpr.Const (SynConst.Int32 n, _)) -> "," + string n | _ -> ""
7583+ let formatText = match format with Some n -> ":" + n.idText | None -> ""
7584+ let netFormat = "{0" + alignText + formatText + "}"
7585+ let invariant = mkSynLidGet mSynth [ "System"; "Globalization"; "CultureInfo" ] "InvariantCulture"
7586+ let args = paren (SynExpr.Tuple(false, [ invariant; strLit netFormat; e ], [ range0; range0 ], mSynth))
7587+ mkSynApp1 (mkSynLidGet mSynth [ "System"; "String" ] "Format") args mSynth
7588+
7589+ // Build one string expression per part, consuming one 'holeIsString' flag per fill expression.
7590+ let rec build acc parts (holeIsString: bool list) =
7591+ match parts with
7592+ | [] -> List.rev acc
7593+ | SynInterpolatedStringPart.String ("", _) :: rest -> build acc rest holeIsString
7594+ | SynInterpolatedStringPart.String (s, _) :: rest -> build (strLit (s.Replace("%%", "%")) :: acc) rest holeIsString
7595+ | SynInterpolatedStringPart.FillExpr (e, formatting) :: rest ->
7596+ let isStr, rest' = match holeIsString with b :: bs -> b, bs | [] -> false, []
7597+ let argExpr =
7598+ match formatting with
7599+ // A string hole is already a string (Concat maps null to ""); convert anything else.
7600+ | SynInterpolationFormatting.DotNet (None, None) -> if isStr then e else stringOp e
7601+ | SynInterpolationFormatting.DotNet (alignment, format) -> stringFormatOp (alignment, format, e)
7602+ | SynInterpolationFormatting.Printf (spec, _) -> sprintfOp (spec, e)
7603+ build (argExpr :: acc) rest rest'
7604+
7605+ let argExprs = build [] parts holeIsString
7606+
7607+ let concatLid = mkSynLidGet mSynth [ "System"; "String" ] "Concat"
7608+
7609+ let resultExpr =
7610+ match argExprs with
7611+ | [] -> strLit ""
7612+ | [ single ] -> single
7613+ | _ when List.length argExprs <= 4 ->
7614+ let commas = List.replicate (List.length argExprs - 1) range0
7615+ mkSynApp1 concatLid (paren (SynExpr.Tuple(false, argExprs, commas, mSynth))) mSynth
7616+ | _ ->
7617+ mkSynApp1 concatLid (paren (SynExpr.ArrayOrList(true, argExprs, mSynth))) mSynth
7618+
7619+ TcPropagatingExprLeafThenConvert cenv overallTy cenv.g.string_ty env m (fun () ->
7620+ TcExpr cenv (MustEqual cenv.g.string_ty) env tpenv resultExpr)
7621+
76017622/// Check an interpolated string expression
76027623and [<TailCall>] warnForFunctionValuesInFillExprs (g: TcGlobals) argTys synFillExprs =
76037624 match argTys, synFillExprs with
@@ -7615,11 +7636,7 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn
76157636 parts
76167637 |> List.choose (function
76177638 | SynInterpolatedStringPart.String _ -> None
7618- | SynInterpolatedStringPart.FillExpr (fillExpr, _) ->
7619- match fillExpr with
7620- // Detect "x" part of "...{x,3}..."
7621- | SynExpr.Tuple (false, [e; SynExpr.Const (SynConst.Int32 _align, _)], _, _) -> Some e
7622- | e -> Some e)
7639+ | SynInterpolatedStringPart.FillExpr (fillExpr, _) -> Some fillExpr)
76237640
76247641 let stringFragmentRanges =
76257642 parts
@@ -7687,19 +7704,21 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn
76877704
76887705 let isFormattableString = (match stringKind with Choice2Of2 _ -> true | _ -> false)
76897706
7690- // The format string used for checking in CheckFormatStrings. This replaces interpolation holes with %P
7707+ // The format string used for checking in CheckFormatStrings, reconstructed from the parts: each
7708+ // hole becomes a '%P(...)' marker, prefixed by its printf specifier or alignment.
76917709 let printfFormatString =
76927710 parts
76937711 |> List.map (function
76947712 | SynInterpolatedStringPart.String (s, _) -> s
7695- | SynInterpolatedStringPart.FillExpr (fillExpr, format) ->
7713+ | SynInterpolatedStringPart.FillExpr (_, SynInterpolationFormatting.Printf (spec, _)) ->
7714+ spec + "%P()"
7715+ | SynInterpolatedStringPart.FillExpr (fillExpr, SynInterpolationFormatting.DotNet (alignment, format)) ->
7716+ match fillExpr with
7717+ | SynExpr.Tuple (false, _, _, _) -> errorR(Error(FSComp.SR.tcInvalidAlignmentInInterpolatedString(), m))
7718+ | _ -> ()
76967719 let alignText =
7697- match fillExpr with
7698- // Validate and detect ",3" part of "...{x,3}..."
7699- | SynExpr.Tuple (false, args, _, _) ->
7700- match args with
7701- | [_; SynExpr.Const (SynConst.Int32 align, _)] -> string align
7702- | _ -> errorR(Error(FSComp.SR.tcInvalidAlignmentInInterpolatedString(), m)); ""
7720+ match alignment with
7721+ | Some (SynExpr.Const (SynConst.Int32 align, _)) -> string align
77037722 | _ -> ""
77047723 let formatText = match format with None -> "()" | Some n -> "(" + n.idText + ")"
77057724 "%" + alignText + "P" + formatText )
@@ -7754,55 +7773,18 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn
77547773 let str = mkString g m printfFormatString
77557774 mkCallNewFormat g m printerTy printerArgTy printerResidueTy printerResultTy printerTupleTy str, tpenv
77567775 else
7757- // Type check the expressions filling the holes
77587776 let fillExprs, tpenv = TcExprsNoFlexes cenv env m tpenv argTys synFillExprs
77597777
77607778 if g.langVersion.SupportsFeature LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg then
77617779 warnForFunctionValuesInFillExprs g argTys synFillExprs
77627780
7763- // Take all interpolated string parts and typed fill expressions
7764- // and convert them to typed expressions that can be used as args to System.String.Concat
7765- // return an empty list if there are some format specifiers that make lowering to not applicable
7766- let rec concatenable acc fillExprs parts =
7767- match fillExprs, parts with
7768- | [], [] ->
7769- List.rev acc
7770- | [], SynInterpolatedStringPart.FillExpr _ :: _
7771- | _, [] ->
7772- // This should never happen, there will always be as many typed fill expressions
7773- // as there are FillExprs in the interpolated string parts
7774- error(InternalError("Mismatch in interpolation expression count", m))
7775- | _, SynInterpolatedStringPart.String (WithTrailingStringSpecifierRemoved "", _) :: parts ->
7776- // If the string is empty (after trimming %s of the end), we skip it
7777- concatenable acc fillExprs parts
7778-
7779- | _, SynInterpolatedStringPart.String (WithTrailingStringSpecifierRemoved HasFormatSpecifier, _) :: _
7780- | _, SynInterpolatedStringPart.FillExpr (_, Some _) :: _
7781- | _, SynInterpolatedStringPart.FillExpr (SynExpr.Tuple (isStruct = false; exprs = [_; SynExpr.Const (SynConst.Int32 _, _)]), _) :: _ ->
7782- // There was a format specifier like %20s{..} or {..,20} or {x:hh}, which means we cannot simply concat
7783- []
7784-
7785- | _, SynInterpolatedStringPart.String (s & WithTrailingStringSpecifierRemoved trimmed, m) :: parts ->
7786- let finalStr = trimmed.Replace("%%", "%")
7787- concatenable (mkString g (shiftEnd 0 (finalStr.Length - s.Length) m) finalStr :: acc) fillExprs parts
7788-
7789- | fillExpr :: fillExprs, SynInterpolatedStringPart.FillExpr _ :: parts ->
7790- concatenable (fillExpr :: acc) fillExprs parts
7791-
7792- let canLower =
7793- g.langVersion.SupportsFeature LanguageFeature.LowerInterpolatedStringToConcat
7794- && isString
7795- && argTys |> List.forall (isStringTy g)
7796-
7797- let concatenableExprs = if canLower then concatenable [] fillExprs parts else []
7798-
7799- match concatenableExprs with
7800- | [p1; p2; p3; p4] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkStaticCall_String_Concat4 g m p1 p2 p3 p4, tpenv)
7801- | [p1; p2; p3] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkStaticCall_String_Concat3 g m p1 p2 p3, tpenv)
7802- | [p1; p2] -> TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkStaticCall_String_Concat2 g m p1 p2, tpenv)
7803- | [p1] -> p1, tpenv
7804- | _ ->
7805-
7781+ if isString then
7782+ // String-typed interpolation: lower to a reflection-free System.String.Concat of the parts.
7783+ // A hole whose value is already a string is passed straight through.
7784+ let holeIsString = fillExprs |> List.map (fun fillExpr -> isStringTy g (tyOfExpr g fillExpr))
7785+ TcInterpolatedStringViaConcat (cenv, overallTy, env, m, tpenv, parts, holeIsString)
7786+ else
7787+ // $"...{x}..." used as a PrintfFormat value: build a PrintfFormat that captures the args.
78067788 let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m)
78077789
78087790 let argsExpr = mkArray (g.obj_ty_withNulls, fillExprsBoxed, m)
@@ -7813,15 +7795,7 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn
78137795 let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList
78147796 mkArray (g.system_Type_ty, tyExprs, m)
78157797
7816- let fmtExpr = MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None
7817-
7818- if isString then
7819- TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env (* true *) m (fun () ->
7820- // Make the call to sprintf
7821- mkCall_sprintf g m printerTy fmtExpr [], tpenv
7822- )
7823- else
7824- fmtExpr, tpenv
7798+ MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None, tpenv
78257799
78267800 // The case for $"..." used as type FormattableString or IFormattable
78277801 | Choice2Of2 createFormattableStringMethod ->
0 commit comments