Skip to content

Commit f571df9

Browse files
charlesroddieclaude
andcommitted
Lower string-typed interpolation to String.Concat (reflection-free)
A string-typed interpolated string is lowered to System.String.Concat of its parts rather than the reflection-based printf engine: a string-typed hole is passed through directly, any other plain hole is converted with `string x`, an aligned/formatted hole with `String.Format(InvariantCulture, ...)`, and a printf-specifier hole with `sprintf`. This removes the reflection dependency on the common path, so these interpolations become trim- and NativeAOT-compatible. This generalizes and replaces the language-version-gated String.Concat optimization (dotnet#16556), which only handled all-string holes: the lowering now applies to every string-typed interpolation, ungated. The reflection path is used only for PrintfFormat/FormattableString-typed interpolation. The syntax tree now carries each hole's formatting explicitly, so a printf specifier no longer leaks into an adjacent literal and alignment is no longer a fake tuple: type SynInterpolatedStringPart = | String of value: string * range: range | FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting type SynInterpolationFormatting = | DotNet of alignment: SynExpr option * format: Ident option | Printf of specifier: string * range: range Behavioural change: plain `{x}` holes now render with invariant culture (the F# `string` operator) rather than the current thread culture, matching `string`. Adds a NativeAOT regression test under tests/AheadOfTime/NativeAOT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a0b1a78 commit f571df9

27 files changed

Lines changed: 309 additions & 143 deletions

src/Compiler/Checking/Expressions/CheckExpressions.fs

Lines changed: 78 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ module internal FSharp.Compiler.CheckExpressions
66

77
open System
88
open System.Collections.Generic
9-
open System.Text.RegularExpressions
109

1110
open Internal.Utilities.Collections
1211
open Internal.Utilities.Library
@@ -146,43 +145,6 @@ exception InvalidInternalsVisibleToAssemblyName of badName: string * fileName: s
146145

147146
exception 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
187149
let 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
76027623
and [<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 ->

src/Compiler/Service/SynExpr.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@ module SynExpr =
10901090
| SynExpr.InterpolatedString(contents = contents), Dangling.Problematic _ ->
10911091
contents
10921092
|> List.exists (function
1093-
| SynInterpolatedStringPart.FillExpr(qualifiers = Some _) -> true
1093+
| SynInterpolatedStringPart.FillExpr(formatting = SynInterpolationFormatting.DotNet(format = Some _)) -> true
10941094
| _ -> false)
10951095
10961096
// { (!x) with … }

src/Compiler/SyntaxTree/ParseHelpers.fs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,50 @@ let rhs2 (parseState: IParseState) i j =
6969
/// Get the range corresponding to one of the r.h.s. symbols of a grammar rule while it is being reduced
7070
let rhs parseState i = rhs2 parseState i i
7171

72+
/// Split a trailing printf specifier (e.g. "%d") off an interpolated-string literal that precedes a
73+
/// hole. '%%' is a literal escape, not a specifier.
74+
let peelTrailingPrintfSpecifier (litText: string) : string * string option =
75+
let n = litText.Length
76+
let mutable i = 0
77+
let mutable specStart = -1
78+
79+
while i < n && specStart < 0 do
80+
if litText[i] = '%' then
81+
if i + 1 < n && litText[i + 1] = '%' then
82+
i <- i + 2 // '%%' escape, keep scanning
83+
else
84+
specStart <- i // start of a real specifier
85+
else
86+
i <- i + 1
87+
88+
// A real printf specifier ends, immediately before the hole, with a type character. Anything else
89+
// (for example the explicit '%P(' placeholder syntax) is left in the literal untouched.
90+
if specStart < 0 || "bscdiuxXoBeEfFgGMOAat".IndexOf litText[n - 1] < 0 then
91+
litText, None
92+
else
93+
litText[.. specStart - 1], Some litText[specStart..]
94+
95+
/// Build the [String literal; FillExpr hole] pair for one interpolation hole, splitting the '{x,n}'
96+
/// alignment out of its tuple encoding and peeling a trailing printf specifier onto the hole.
97+
let mkInterpolatedStringFillParts (litText: string, litRange: range, fill: SynExpr * Ident option) =
98+
let fillExpr, qualifier = fill
99+
100+
let holeExpr, alignment =
101+
match fillExpr with
102+
| SynExpr.Tuple(false, [ e; (SynExpr.Const(SynConst.Int32 _, _) as n) ], _, _) -> e, Some n
103+
| _ -> fillExpr, None
104+
105+
let litValue, formatting =
106+
match qualifier, alignment with
107+
| None, None ->
108+
match peelTrailingPrintfSpecifier litText with
109+
| lit, Some spec -> lit, SynInterpolationFormatting.Printf(spec, litRange)
110+
| _, None -> litText, SynInterpolationFormatting.DotNet(None, None)
111+
| _ -> litText, SynInterpolationFormatting.DotNet(alignment, qualifier)
112+
113+
[ SynInterpolatedStringPart.String(litValue, litRange)
114+
SynInterpolatedStringPart.FillExpr(holeExpr, formatting) ]
115+
72116
//------------------------------------------------------------------------
73117
// Parsing/lexing: status of #if/#endif processing in lexing, used for continuations
74118
// for whitespace tokens in parser specification.

src/Compiler/SyntaxTree/ParseHelpers.fsi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ val rhs2: parseState: IParseState -> i: int -> j: int -> range
3838

3939
val rhs: parseState: IParseState -> i: int -> range
4040

41+
/// Peel a trailing printf specifier (e.g. "%d") off an interpolated-string literal that precedes a
42+
/// hole, returning the literal without it and the specifier text. '%%' is a literal escape.
43+
val peelTrailingPrintfSpecifier: litText: string -> string * string option
44+
45+
/// Build the [String literal; FillExpr hole] pair for one interpolation hole, splitting the
46+
/// '{x,n}' alignment out of its tuple encoding and peeling a trailing printf specifier off the
47+
/// literal onto the hole.
48+
val mkInterpolatedStringFillParts:
49+
litText: string * litRange: range * fill: (SynExpr * Ident option) -> SynInterpolatedStringPart list
50+
4151
type LexerIfdefStackEntry =
4252
| IfDefIf
4353
| IfDefElse

src/Compiler/SyntaxTree/SyntaxTree.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,12 @@ type SynExprRecordField =
875875
[<NoEquality; NoComparison; RequireQualifiedAccess>]
876876
type SynInterpolatedStringPart =
877877
| String of value: string * range: range
878-
| FillExpr of fillExpr: SynExpr * qualifiers: Ident option
878+
| FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting
879+
880+
[<NoEquality; NoComparison; RequireQualifiedAccess>]
881+
type SynInterpolationFormatting =
882+
| DotNet of alignment: SynExpr option * format: Ident option
883+
| Printf of specifier: string * range: range
879884

880885
[<NoEquality; NoComparison; RequireQualifiedAccess>]
881886
type SynSimplePat =

src/Compiler/SyntaxTree/SyntaxTree.fsi

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,16 @@ type SynExprRecordField =
999999
[<NoEquality; NoComparison; RequireQualifiedAccess>]
10001000
type SynInterpolatedStringPart =
10011001
| String of value: string * range: range
1002-
| FillExpr of fillExpr: SynExpr * qualifiers: Ident option
1002+
| FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting
1003+
1004+
/// Represents how an interpolation hole in an interpolated string is formatted.
1005+
[<NoEquality; NoComparison; RequireQualifiedAccess>]
1006+
type SynInterpolationFormatting =
1007+
/// .NET-style formatting: optional alignment '{x,n}' and optional format '{x:fmt}'.
1008+
| DotNet of alignment: SynExpr option * format: Ident option
1009+
1010+
/// printf-style formatting: a single specifier, the '%d' in '%d{x}'.
1011+
| Printf of specifier: string * range: range
10031012

10041013
/// Represents a syntax tree for simple F# patterns
10051014
[<NoEquality; NoComparison; RequireQualifiedAccess>]

src/Compiler/TypedTree/TcGlobals.fs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1707,7 +1707,6 @@ type TcGlobals(
17071707
member _.seq_map_info = v_seq_map_info
17081708
member _.seq_singleton_info = v_seq_singleton_info
17091709
member _.seq_empty_info = v_seq_empty_info
1710-
member _.sprintf_info = v_sprintf_info
17111710
member _.new_format_info = v_new_format_info
17121711
member _.unbox_info = v_unbox_info
17131712
member _.get_generic_comparer_info = v_get_generic_comparer_info

src/Compiler/TypedTree/TcGlobals.fsi

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

10041004
member splice_raw_expr_vref: TypedTree.ValRef
10051005

1006-
member sprintf_info: IntrinsicValRef
1007-
10081006
member sprintf_vref: TypedTree.ValRef
10091007

10101008
member string_ty: TypedTree.TType

0 commit comments

Comments
 (0)