Skip to content

Commit 1951198

Browse files
charlesroddieclaude
andcommitted
Lower string interpolation by type-checking each part in place
Rewrite TcInterpolatedStringViaConcat to type-check each interpolation part in place and convert it to a string expression, then String.Concat them. This removes the parallel 'holeIsString' bool list and the flat-fillExprs/dense-parts interleave entirely: 'build' now walks a single list (the parts), threading only tpenv. - Plain '{x}' holes are built directly in the typed tree: a string is passed through raw (matching dotnet#16556's lean IL), anything else is converted via the 'string' operator, emitted through a new string_operator_info intrinsic + mkCallStringOperator helper. - Aligned/formatted and printf holes are checked from a small synthesized String.Format/sprintf expression, so name resolution still does the BCL work. - The function-value warning is re-homed per-hole. Known follow-ups: ill-typed formatted holes currently report their error twice (the formatted arm type-checks the hole once for the warning and again inside String.Format); the warning wants to move to its own pass over hole types, which also removes that double check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c7beb02 commit 1951198

5 files changed

Lines changed: 66 additions & 49 deletions

File tree

src/Compiler/Checking/Expressions/CheckExpressions.fs

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7560,17 +7560,17 @@ and TcFormatStringExpr cenv (overallTy: OverallTy) env m tpenv (fmtString: strin
75607560
mkString g m fmtString, tpenv
75617561
)
75627562

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) =
7563+
/// Lower a string-typed interpolated string to a reflection-free System.String.Concat of its parts,
7564+
/// type-checking each part in place. A literal becomes its text; a plain '{x}' hole is built straight in
7565+
/// the typed tree (passed through if already a string, else converted with the 'string' operator); an
7566+
/// aligned/formatted or printf hole is checked from a small synthesized 'String.Format'/'sprintf' so name
7567+
/// resolution applies.
7568+
and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv, m: range, tpenv: UnscopedTyparEnv, parts: SynInterpolatedStringPart list) =
7569+
let g = cenv.g
75667570
let mSynth = m.MakeSynthetic()
75677571
let strLit (s: string) = SynExpr.Const(SynConst.String(s, SynStringKind.Regular, mSynth), mSynth)
75687572
let paren (e: SynExpr) = SynExpr.Paren(e, range0, None, mSynth)
75697573

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-
75747574
// '(sprintf spec e : string)': format a printf-specifier hole (still reflection-based).
75757575
let sprintfOp (spec: string, e: SynExpr) =
75767576
let f = mkSynApp1 (mkSynLidGet mSynth [ "Microsoft"; "FSharp"; "Core"; "ExtraTopLevelOperators" ] "sprintf") (strLit spec) mSynth
@@ -7586,38 +7586,47 @@ and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv,
75867586
let args = paren (SynExpr.Tuple(false, [ invariant; strLit netFormat; e ], [ range0; range0 ], mSynth))
75877587
mkSynApp1 (mkSynLidGet mSynth [ "System"; "String" ] "Format") args mSynth
75887588

7589-
// Build one string expression per part, consuming one 'holeIsString' flag per fill expression.
7590-
let rec build (acc: SynExpr list, parts: SynInterpolatedStringPart list, holeIsString: bool list) =
7589+
// Type-check one hole and convert it to a typed string expression. A '%spec' hole is constrained by
7590+
// its specifier (a function there is a type error), so it goes straight to 'sprintf'. A plain or
7591+
// aligned/formatted hole is checked here first, so its value can be warned about if it is a function.
7592+
let convertHole (synFill: SynExpr, formatting: SynInterpolationFormatting, tpenv: UnscopedTyparEnv) =
7593+
match formatting with
7594+
| SynInterpolationFormatting.Printf (spec, _) -> TcExpr cenv (MustEqual g.string_ty) env tpenv (sprintfOp (spec, synFill))
7595+
| SynInterpolationFormatting.DotNet (alignment, format) ->
7596+
let fill, tpenv = TcExprFlex2 cenv (NewInferenceType g) env false tpenv synFill
7597+
let fillTy = tyOfExpr g fill
7598+
if g.langVersion.SupportsFeature LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg && (isFunTy g fillTy || isDelegateTy g fillTy) then
7599+
warning (Error(FSComp.SR.tcFunctionValueUsedAsInterpolatedStringArg (), synFill.Range))
7600+
match alignment, format with
7601+
// A plain hole reuses the checked expression; an aligned or formatted hole goes via String.Format.
7602+
| None, None -> (if isStringTy g fillTy then fill else mkCallStringOperator g m fillTy fill), tpenv
7603+
| _ -> TcExpr cenv (MustEqual g.string_ty) env tpenv (stringFormatOp (alignment, format, synFill))
7604+
7605+
// Build one string expression per part, type-checking each hole in place.
7606+
let rec build (parts: SynInterpolatedStringPart list, tpenv: UnscopedTyparEnv) =
75917607
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+
| [] -> [], tpenv
7609+
| SynInterpolatedStringPart.String ("", _) :: rest -> build (rest, tpenv)
7610+
| SynInterpolatedStringPart.String (s, _) :: rest ->
7611+
let args, tpenv = build (rest, tpenv)
7612+
mkString g m (s.Replace("%%", "%")) :: args, tpenv
7613+
| SynInterpolatedStringPart.FillExpr (synFill, formatting) :: rest ->
7614+
let argExpr, tpenv = convertHole (synFill, formatting, tpenv)
7615+
let args, tpenv = build (rest, tpenv)
7616+
argExpr :: args, tpenv
7617+
7618+
let argExprs, tpenv = build (parts, tpenv)
76087619

76097620
let resultExpr =
76107621
match argExprs with
7611-
| [] -> strLit ""
7622+
| [] -> mkString g m ""
76127623
| [ 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
7624+
| [ a; b ] -> mkStaticCall_String_Concat2 g m a b
7625+
| [ a; b; c ] -> mkStaticCall_String_Concat3 g m a b c
7626+
| [ a; b; c; d ] -> mkStaticCall_String_Concat4 g m a b c d
7627+
| args -> mkStaticCall_String_Concat_Array g m (mkArray (g.string_ty, args, m))
76187628

7619-
TcPropagatingExprLeafThenConvert cenv overallTy cenv.g.string_ty env m (fun () ->
7620-
TcExpr cenv (MustEqual cenv.g.string_ty) env tpenv resultExpr)
7629+
TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> resultExpr, tpenv)
76217630

76227631
/// Check an interpolated string expression
76237632
and [<TailCall>] warnForFunctionValuesInFillExprs (g: TcGlobals) argTys synFillExprs =
@@ -7772,30 +7781,28 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn
77727781
else
77737782
let str = mkString g m printfFormatString
77747783
mkCallNewFormat g m printerTy printerArgTy printerResidueTy printerResultTy printerTupleTy str, tpenv
7784+
elif isString then
7785+
// String-typed interpolation: lower to a reflection-free System.String.Concat of the parts,
7786+
// type-checking each hole in place (no separate batch, no flat fill-expression list).
7787+
TcInterpolatedStringViaConcat (cenv, overallTy, env, m, tpenv, parts)
77757788
else
7789+
// $"...{x}..." used as a PrintfFormat value: build a PrintfFormat that captures the args.
77767790
let fillExprs, tpenv = TcExprsNoFlexes cenv env m tpenv argTys synFillExprs
77777791

77787792
if g.langVersion.SupportsFeature LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg then
77797793
warnForFunctionValuesInFillExprs g argTys synFillExprs
77807794

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.
7788-
let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m)
7795+
let fillExprsBoxed = (argTys, fillExprs) ||> List.map2 (mkCallBox g m)
77897796

7790-
let argsExpr = mkArray (g.obj_ty_withNulls, fillExprsBoxed, m)
7791-
let percentATysExpr =
7792-
if percentATys.Length = 0 then
7793-
mkNull m (mkArrayType g g.system_Type_ty)
7794-
else
7795-
let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList
7796-
mkArray (g.system_Type_ty, tyExprs, m)
7797+
let argsExpr = mkArray (g.obj_ty_withNulls, fillExprsBoxed, m)
7798+
let percentATysExpr =
7799+
if percentATys.Length = 0 then
7800+
mkNull m (mkArrayType g g.system_Type_ty)
7801+
else
7802+
let tyExprs = percentATys |> Array.map (mkCallTypeOf g m) |> Array.toList
7803+
mkArray (g.system_Type_ty, tyExprs, m)
77977804

7798-
MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None, tpenv
7805+
MakeMethInfoCall cenv.amap m newFormatMethod [] [mkString g m printfFormatString; argsExpr; percentATysExpr] None, tpenv
77997806

78007807
// The case for $"..." used as type FormattableString or IFormattable
78017808
| Choice2Of2 createFormattableStringMethod ->

src/Compiler/TypedTree/TcGlobals.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ type TcGlobals(
792792

793793
let v_byte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "byte" , None , Some "ToByte", [vara], ([[varaTy]], v_byte_ty))
794794
let v_sbyte_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "sbyte" , None , Some "ToSByte", [vara], ([[varaTy]], v_sbyte_ty))
795+
let v_string_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "string" , None , Some "ToString", [vara], ([[varaTy]], v_string_ty))
795796
let v_int16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int16" , None , Some "ToInt16", [vara], ([[varaTy]], v_int16_ty))
796797
let v_uint16_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "uint16" , None , Some "ToUInt16", [vara], ([[varaTy]], v_uint16_ty))
797798
let v_int32_operator_info = makeIntrinsicValRef(fslib_MFOperators_nleref, "int32" , None , Some "ToInt32", [vara], ([[varaTy]], v_int32_ty))
@@ -1594,6 +1595,7 @@ type TcGlobals(
15941595

15951596
member _.byte_operator_info = v_byte_operator_info
15961597
member _.sbyte_operator_info = v_sbyte_operator_info
1598+
member _.string_operator_info = v_string_operator_info
15971599
member _.int16_operator_info = v_int16_operator_info
15981600
member _.uint16_operator_info = v_uint16_operator_info
15991601
member _.int32_operator_info = v_int32_operator_info

src/Compiler/TypedTree/TcGlobals.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,8 @@ type internal TcGlobals =
939939

940940
member sbyte_operator_info: IntrinsicValRef
941941

942+
member string_operator_info: IntrinsicValRef
943+
942944
member sbyte_tcr: TypedTree.EntityRef
943945

944946
member sbyte_ty: TypedTree.TType

src/Compiler/TypedTree/TypedTreeOps.ExprOps.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,9 @@ module internal Makers =
13661366
let mkCallNewFormat (g: TcGlobals) m aty bty cty dty ety formatStringExpr =
13671367
mkApps g (typedExprForIntrinsic g m g.new_format_info, [ [ aty; bty; cty; dty; ety ] ], [ formatStringExpr ], m)
13681368

1369+
let mkCallStringOperator (g: TcGlobals) m argTy e =
1370+
mkApps g (typedExprForIntrinsic g m g.string_operator_info, [ [ argTy ] ], [ e ], m)
1371+
13691372
let tryMkCallBuiltInWitness (g: TcGlobals) traitInfo argExprs m =
13701373
let info, tinst = g.MakeBuiltInWitnessInfo traitInfo
13711374
let vref = ValRefForIntrinsic info

src/Compiler/TypedTree/TypedTreeOps.ExprOps.fsi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ module internal Makers =
208208
val mkCallNewFormat:
209209
TcGlobals -> range -> TType -> TType -> TType -> TType -> TType -> formatStringExpr: Expr -> Expr
210210

211+
/// Build a call to the 'string' operator (Operators.ToString) at the given argument type.
212+
val mkCallStringOperator: TcGlobals -> range -> argTy: TType -> Expr -> Expr
213+
211214
val mkCallGetGenericComparer: TcGlobals -> range -> Expr
212215

213216
val mkCallGetGenericEREqualityComparer: TcGlobals -> range -> Expr

0 commit comments

Comments
 (0)