Skip to content

Commit

Permalink
[RFC FS-1132] Interpolated strings syntax with multiple dollar signs (#…
Browse files Browse the repository at this point in the history
…14640)

New language feature - extended string interpolation - RFC FS-1132
  • Loading branch information
abonie authored Apr 27, 2023
1 parent 9627d33 commit 4ae20b6
Show file tree
Hide file tree
Showing 64 changed files with 1,269 additions and 260 deletions.
64 changes: 44 additions & 20 deletions src/Compiler/Checking/CheckFormatStrings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,28 @@ let makeFmts (context: FormatStringCheckContext) (fragRanges: range list) (fmt:
let sourceText = context.SourceText
let lineStartPositions = context.LineStartPositions

// Number of curly braces required to delimiter interpolation holes
// = Number of $ chars starting a (triple quoted) string literal
// Set when we process first fragment range, default = 1
let mutable delimLen = 1

let mutable nQuotes = 1
[ for i, r in List.indexed fragRanges do
if r.StartLine - 1 < lineStartPositions.Length && r.EndLine - 1 < lineStartPositions.Length then
let startIndex = lineStartPositions[r.StartLine - 1] + r.StartColumn
let rLength = lineStartPositions[r.EndLine - 1] + r.EndColumn - startIndex
let offset =
if i = 0 then
match sourceText.GetSubTextString(startIndex, rLength) with
| PrefixedBy "$\"\"\"" len
| PrefixedBy "\"\"\"" len ->
let fullRangeText = sourceText.GetSubTextString(startIndex, rLength)
delimLen <-
fullRangeText
|> Seq.takeWhile (fun c -> c = '$')
|> Seq.length
let tripleQuotePrefix =
[String.replicate delimLen "$"; "\"\"\""]
|> String.concat ""
match fullRangeText with
| PrefixedBy tripleQuotePrefix len ->
nQuotes <- 3
len
| PrefixedBy "$@\"" len
Expand All @@ -91,13 +103,13 @@ let makeFmts (context: FormatStringCheckContext) (fragRanges: range list) (fmt:
| _ -> 1
else
1 // <- corresponds to '}' that's closing an interpolation hole
let fragLen = rLength - offset - (if i = numFrags - 1 then nQuotes else 1)
let fragLen = rLength - offset - (if i = numFrags - 1 then nQuotes else delimLen)
(offset, sourceText.GetSubTextString(startIndex + offset, fragLen), r)
else (1, fmt, r)
]
], delimLen


module internal Parsing =
module internal Parse =

let flags (info: FormatInfoRegister) (fmt: string) (fmtPos: int) =
let len = fmt.Length
Expand Down Expand Up @@ -231,10 +243,10 @@ let parseFormatStringInternal
//
let escapeFormatStringEnabled = g.langVersion.SupportsFeature Features.LanguageFeature.EscapeDotnetFormattableStrings

let fmt, fragments =
let fmt, fragments, delimLen =
match context with
| Some context when fragRanges.Length > 0 ->
let fmts = makeFmts context fragRanges fmt
let fmts, delimLen = makeFmts context fragRanges fmt

// Join the fragments with holes. Note this join is only used on the IDE path,
// the CheckExpressions.fs does its own joining with the right alignments etc. substituted
Expand All @@ -245,11 +257,11 @@ let parseFormatStringInternal
(0, fmts) ||> List.mapFold (fun i (offset, fmt, fragRange) ->
(i, offset, fragRange), i + fmt.Length + 4) // the '4' is the length of '%P()' joins

fmt, fragments
fmt, fragments, delimLen
| _ ->
// Don't muck with the fmt when there is no source code context to go get the original
// source code (i.e. when compiling or background checking)
(if escapeFormatStringEnabled then escapeDotnetFormatString fmt else fmt), [ (0, 1, m) ]
(if escapeFormatStringEnabled then escapeDotnetFormatString fmt else fmt), [ (0, 1, m) ], 1

let len = fmt.Length

Expand Down Expand Up @@ -299,32 +311,44 @@ let parseFormatStringInternal

and parseSpecifier acc (i, fragLine, fragCol) fragments =
let startFragCol = fragCol
let fragCol = fragCol+1
if fmt[i..(i+1)] = "%%" then
let nPercentSigns =
fmt[i..]
|> Seq.takeWhile (fun c -> c = '%')
|> Seq.length
if delimLen <= 1 && fmt[i..(i+1)] = "%%" then
match context with
| Some _ ->
specifierLocations.Add(
(Range.mkFileIndexRange m.FileIndex
(Position.mkPos fragLine startFragCol)
(Position.mkPos fragLine (fragCol + 1))), 0)
(Position.mkPos fragLine fragCol)
(Position.mkPos fragLine (fragCol+2))), 0)
| None -> ()
appendToDotnetFormatString "%"
parseLoop acc (i+2, fragLine, fragCol+1) fragments
parseLoop acc (i+2, fragLine, fragCol+2) fragments
elif delimLen > 1 && nPercentSigns < delimLen then
appendToDotnetFormatString fmt[i..(i+nPercentSigns-1)]
parseLoop acc (i + nPercentSigns, fragLine, fragCol + nPercentSigns) fragments
else
let i = i+1
let fragCol, i =
if delimLen > 1 then
if nPercentSigns > delimLen then
"%" |> String.replicate (nPercentSigns - delimLen) |> appendToDotnetFormatString
fragCol + nPercentSigns, i + nPercentSigns
else
fragCol + 1, i + 1
if i >= len then failwith (FSComp.SR.forMissingFormatSpecifier())
let info = newInfo()

let oldI = i
let posi, i = Parsing.position fmt i
let posi, i = Parse.position fmt i
let fragCol = fragCol + i - oldI

let oldI = i
let i = Parsing.flags info fmt i
let i = Parse.flags info fmt i
let fragCol = fragCol + i - oldI

let oldI = i
let widthArg,(widthValue, (precisionArg,i)) = Parsing.widthAndPrecision info fmt i
let widthArg,(widthValue, (precisionArg,i)) = Parse.widthAndPrecision info fmt i
let fragCol = fragCol + i - oldI

if i >= len then failwith (FSComp.SR.forBadPrecision())
Expand All @@ -340,7 +364,7 @@ let parseFormatStringInternal
| Some n -> failwith (FSComp.SR.forDoesNotSupportPrefixFlag(c.ToString(), n.ToString()))
| None -> ()

let skipPossibleInterpolationHole pos = Parsing.skipPossibleInterpolationHole isInterpolated isFormattableString fmt pos
let skipPossibleInterpolationHole pos = Parse.skipPossibleInterpolationHole isInterpolated isFormattableString fmt pos

// Implicitly typed holes in interpolated strings are translated to '... %P(...)...' in the
// type checker. They should always have '(...)' after for format string.
Expand Down
5 changes: 5 additions & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,10 @@ lexIfOCaml,"IF-FSHARP/IF-CAML regions are no longer supported"
1245,lexInvalidUnicodeLiteral,"\U%s is not a valid Unicode character escape sequence"
1246,tcCallerInfoWrongType,"'%s' must be applied to an argument of type '%s', but has been applied to an argument of type '%s'"
1247,tcCallerInfoNotOptional,"'%s' can only be applied to optional arguments"
1248,lexTooManyLBracesInTripleQuote,"The interpolated triple quoted string literal does not start with enough '$' characters to allow this many consecutive opening braces as content."
1249,lexUnmatchedRBracesInTripleQuote,"The interpolated string contains unmatched closing braces."
1250,lexTooManyPercentsInTripleQuote,"The interpolated triple quoted string literal does not start with enough '$' characters to allow this many consecutive '%%' characters."
1251,lexExtendedStringInterpolationNotSupported,"Extended string interpolation is not supported in this version of F#."
# reshapedmsbuild.fs
1300,toolLocationHelperUnsupportedFrameworkVersion,"The specified .NET Framework version '%s' is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion."
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -1567,6 +1571,7 @@ featureWarningWhenCopyAndUpdateRecordChangesAllFields,"Raises warnings when an c
featureStaticMembersInInterfaces,"Static members in interfaces"
featureNonInlineLiteralsAsPrintfFormat,"String values marked as literals and IL constants as printf format"
featureNestedCopyAndUpdate,"Nested record field copy-and-update"
featureExtendedStringInterpolation,"Extended string interpolation similar to C# raw string literals."
3353,fsiInvalidDirective,"Invalid directive '#%s %s'"
3354,tcNotAFunctionButIndexerNamedIndexingNotYetEnabled,"This value supports indexing, e.g. '%s.[index]'. The syntax '%s[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation."
3354,tcNotAFunctionButIndexerIndexingNotYetEnabled,"This expression supports indexing, e.g. 'expr.[index]'. The syntax 'expr[index]' requires /langversion:preview. See https://aka.ms/fsharp-index-notation."
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/LanguageFeatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type LanguageFeature =
| StaticMembersInInterfaces
| NonInlineLiteralsAsPrintfFormat
| NestedCopyAndUpdate
| ExtendedStringInterpolation

/// LanguageVersion management
type LanguageVersion(versionText) =
Expand Down Expand Up @@ -155,6 +156,7 @@ type LanguageVersion(versionText) =
LanguageFeature.StaticMembersInInterfaces, previewVersion
LanguageFeature.NonInlineLiteralsAsPrintfFormat, previewVersion
LanguageFeature.NestedCopyAndUpdate, previewVersion
LanguageFeature.ExtendedStringInterpolation, previewVersion

]

Expand Down Expand Up @@ -276,6 +278,7 @@ type LanguageVersion(versionText) =
| LanguageFeature.StaticMembersInInterfaces -> FSComp.SR.featureStaticMembersInInterfaces ()
| LanguageFeature.NonInlineLiteralsAsPrintfFormat -> FSComp.SR.featureNonInlineLiteralsAsPrintfFormat ()
| LanguageFeature.NestedCopyAndUpdate -> FSComp.SR.featureNestedCopyAndUpdate ()
| LanguageFeature.ExtendedStringInterpolation -> FSComp.SR.featureExtendedStringInterpolation ()

/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/LanguageFeatures.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type LanguageFeature =
| StaticMembersInInterfaces
| NonInlineLiteralsAsPrintfFormat
| NestedCopyAndUpdate
| ExtendedStringInterpolation

/// LanguageVersion management
type LanguageVersion =
Expand Down
24 changes: 17 additions & 7 deletions src/Compiler/Service/FSharpCheckerResults.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2352,15 +2352,19 @@ module internal ParseAndCheckFile =

let rec matchBraces stack =
match lexfun lexbuf, stack with
| tok2, (tok1, m1) :: stackAfterMatch when parenTokensBalance tok1 tok2 ->
| tok2, (tok1, braceOffset, m1) :: stackAfterMatch when parenTokensBalance tok1 tok2 ->
let m2 = lexbuf.LexemeRange

// For INTERP_STRING_PART and INTERP_STRING_END grab the one character
// range that corresponds to the "}" at the start of the token
let m2Start =
match tok2 with
| INTERP_STRING_PART _
| INTERP_STRING_END _ -> mkFileIndexRange m2.FileIndex m2.Start (mkPos m2.Start.Line (m2.Start.Column + 1))
| INTERP_STRING_END _ ->
mkFileIndexRange
m2.FileIndex
(mkPos m2.Start.Line (m2.Start.Column - braceOffset))
(mkPos m2.Start.Line (m2.Start.Column + 1))
| _ -> m2

matchingBraces.Add(m1, m2Start)
Expand All @@ -2371,15 +2375,15 @@ module internal ParseAndCheckFile =
match tok2 with
| INTERP_STRING_PART _ ->
let m2End =
mkFileIndexRange m2.FileIndex (mkPos m2.End.Line (max (m2.End.Column - 1) 0)) m2.End
mkFileIndexRange m2.FileIndex (mkPos m2.End.Line (max (m2.End.Column - 1 - braceOffset) 0)) m2.End

(tok2, m2End) :: stackAfterMatch
(tok2, braceOffset, m2End) :: stackAfterMatch
| _ -> stackAfterMatch

matchBraces stackAfterMatch

| LPAREN | LBRACE _ | LBRACK | LBRACE_BAR | LBRACK_BAR | LQUOTE _ | LBRACK_LESS as tok, _ ->
matchBraces ((tok, lexbuf.LexemeRange) :: stack)
matchBraces ((tok, 0, lexbuf.LexemeRange) :: stack)

// INTERP_STRING_BEGIN_PART corresponds to $"... {" at the start of an interpolated string
//
Expand All @@ -2389,12 +2393,18 @@ module internal ParseAndCheckFile =
//
// Either way we start a new potential match at the last character
| INTERP_STRING_BEGIN_PART _ | INTERP_STRING_PART _ as tok, _ ->
let braceOffset =
match tok with
| INTERP_STRING_BEGIN_PART (_, SynStringKind.TripleQuote, (LexerContinuation.Token (_, (_, _, dl, _) :: _))) ->
dl - 1
| _ -> 0

let m = lexbuf.LexemeRange

let m2 =
mkFileIndexRange m.FileIndex (mkPos m.End.Line (max (m.End.Column - 1) 0)) m.End
mkFileIndexRange m.FileIndex (mkPos m.End.Line (max (m.End.Column - 1 - braceOffset) 0)) m.End

matchBraces ((tok, m2) :: stack)
matchBraces ((tok, braceOffset, m2) :: stack)

| (EOF _ | LEX_FAILURE _), _ -> ()
| _ -> matchBraces stack
Expand Down
Loading

0 comments on commit 4ae20b6

Please sign in to comment.