Skip to content

Commit

Permalink
string interploation implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
dsyme committed Apr 9, 2020
1 parent 97dd7cc commit 1ba6b74
Show file tree
Hide file tree
Showing 31 changed files with 827 additions and 339 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,5 @@ msbuild.binlog
/fcs/FSharp.Compiler.Service.netstandard/*.fsi
/.ionide/
**/.DS_Store
Session.vim
coc-settings.json
117 changes: 73 additions & 44 deletions src/fsharp/CheckFormatStrings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ let newInfo () =
addZeros = false
precision = false}

let parseFormatStringInternal (m:range) (g: TcGlobals) (context: FormatStringCheckContext option) fmt bty cty =
let parseFormatStringInternal (m:range) (g: TcGlobals) isInterp (context: FormatStringCheckContext option) fmt bty cty =
// Offset is used to adjust ranges depending on whether input string is regular, verbatim or triple-quote.
// We construct a new 'fmt' string since the current 'fmt' string doesn't distinguish between "\n" and escaped "\\n".
let (offset, fmt) =
Expand Down Expand Up @@ -77,7 +77,7 @@ let parseFormatStringInternal (m:range) (g: TcGlobals) (context: FormatStringChe
if acc |> List.forall (fun (p, _) -> p = None) then // without positional specifiers
acc |> List.map snd |> List.rev
else
failwithf "%s" <| FSComp.SR.forPositionalSpecifiersNotPermitted()
raise (Failure (FSComp.SR.forPositionalSpecifiersNotPermitted()))
argtys
elif System.Char.IsSurrogatePair(fmt,i) then
parseLoop acc (i+2, relLine, relCol+2)
Expand All @@ -88,65 +88,65 @@ let parseFormatStringInternal (m:range) (g: TcGlobals) (context: FormatStringChe
let startCol = relCol
let relCol = relCol+1
let i = i+1
if i >= len then failwithf "%s" <| FSComp.SR.forMissingFormatSpecifier()
if i >= len then raise (Failure (FSComp.SR.forMissingFormatSpecifier()))
let info = newInfo()

let rec flags i =
if i >= len then failwithf "%s" <| FSComp.SR.forMissingFormatSpecifier()
if i >= len then raise (Failure (FSComp.SR.forMissingFormatSpecifier()))
match fmt.[i] with
| '-' ->
if info.leftJustify then failwithf "%s" <| FSComp.SR.forFlagSetTwice("-")
if info.leftJustify then raise (Failure (FSComp.SR.forFlagSetTwice("-")))
info.leftJustify <- true
flags(i+1)
| '+' ->
if info.numPrefixIfPos <> None then failwithf "%s" <| FSComp.SR.forPrefixFlagSpacePlusSetTwice()
if info.numPrefixIfPos <> None then raise (Failure (FSComp.SR.forPrefixFlagSpacePlusSetTwice()))
info.numPrefixIfPos <- Some '+'
flags(i+1)
| '0' ->
if info.addZeros then failwithf "%s" <| FSComp.SR.forFlagSetTwice("0")
if info.addZeros then raise (Failure (FSComp.SR.forFlagSetTwice("0")))
info.addZeros <- true
flags(i+1)
| ' ' ->
if info.numPrefixIfPos <> None then failwithf "%s" <| FSComp.SR.forPrefixFlagSpacePlusSetTwice()
if info.numPrefixIfPos <> None then raise (Failure (FSComp.SR.forPrefixFlagSpacePlusSetTwice()))
info.numPrefixIfPos <- Some ' '
flags(i+1)
| '#' -> failwithf "%s" <| FSComp.SR.forHashSpecifierIsInvalid()
| '#' -> raise (Failure (FSComp.SR.forHashSpecifierIsInvalid() ))
| _ -> i

let rec digitsPrecision i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))
match fmt.[i] with
| c when System.Char.IsDigit c -> digitsPrecision (i+1)
| _ -> i

let precision i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadWidth()
if i >= len then raise (Failure (FSComp.SR.forBadWidth()))
match fmt.[i] with
| c when System.Char.IsDigit c -> info.precision <- true; false,digitsPrecision (i+1)
| '*' -> info.precision <- true; true,(i+1)
| _ -> failwithf "%s" <| FSComp.SR.forPrecisionMissingAfterDot()
| _ -> raise (Failure (FSComp.SR.forPrecisionMissingAfterDot()))

let optionalDotAndPrecision i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))
match fmt.[i] with
| '.' -> precision (i+1)
| _ -> false,i

let rec digitsWidthAndPrecision i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))
match fmt.[i] with
| c when System.Char.IsDigit c -> digitsWidthAndPrecision (i+1)
| _ -> optionalDotAndPrecision i

let widthAndPrecision i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))
match fmt.[i] with
| c when System.Char.IsDigit c -> false,digitsWidthAndPrecision i
| '*' -> true,optionalDotAndPrecision (i+1)
| _ -> false,optionalDotAndPrecision i

let rec digitsPosition n i =
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))
match fmt.[i] with
| c when System.Char.IsDigit c -> digitsPosition (n*10 + int c - int '0') (i+1)
| '$' -> Some n, i+1
Expand All @@ -171,22 +171,35 @@ let parseFormatStringInternal (m:range) (g: TcGlobals) (context: FormatStringChe
let widthArg,(precisionArg,i) = widthAndPrecision i
let relCol = relCol + i - oldI

if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
if i >= len then raise (Failure (FSComp.SR.forBadPrecision()))

let acc = if precisionArg then (Option.map ((+)1) posi, g.int_ty) :: acc else acc

let acc = if widthArg then (Option.map ((+)1) posi, g.int_ty) :: acc else acc

let checkNoPrecision c = if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(c.ToString())
let checkNoZeroFlag c = if info.addZeros then failwithf "%s" <| FSComp.SR.forDoesNotSupportZeroFlag(c.ToString())
let checkNoNumericPrefix c = if info.numPrefixIfPos <> None then
failwithf "%s" <| FSComp.SR.forDoesNotSupportPrefixFlag(c.ToString(), (Option.get info.numPrefixIfPos).ToString())
let checkNoPrecision c =
if info.precision then raise (Failure (FSComp.SR.forFormatDoesntSupportPrecision(c.ToString())))

let checkNoZeroFlag c =
if info.addZeros then raise (Failure (FSComp.SR.forDoesNotSupportZeroFlag(c.ToString())))

let checkNoNumericPrefix c =
match info.numPrefixIfPos with
| Some n -> raise (Failure (FSComp.SR.forDoesNotSupportPrefixFlag(c.ToString(), n.ToString())))
| None -> ()

let checkOtherFlags c =
checkNoPrecision c
checkNoZeroFlag c
checkNoNumericPrefix c

let skipInterp i =
// Explicitly typed holes in interpolated strings get '%P' after them as hole place marker
if isInterp then
if i+1 < fmt.Length && fmt.[i] = '%' && fmt.[i+1] = 'P' then i + 2
else raise (Failure (FSComp.SR.forFormatInvalidForInterpolated()))
else i

let collectSpecifierLocation relLine relCol numStdArgs =
let numArgsForSpecifier =
numStdArgs + (if widthArg then 1 else 0) + (if precisionArg then 1 else 0)
Expand All @@ -209,94 +222,110 @@ let parseFormatStringInternal (m:range) (g: TcGlobals) (context: FormatStringChe
parseLoop acc (i+1, relLine, relCol+1)

| ('d' | 'i' | 'o' | 'u' | 'x' | 'X') ->
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())
if info.precision then raise (Failure (FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())))
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i, relLine, relCol+1)

| ('l' | 'L') ->
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())
if info.precision then raise (Failure (FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())))
let relCol = relCol+1
let i = i+1

// "bad format specifier ... In F# code you can use %d, %x, %o or %u instead ..."
if i >= len then
failwithf "%s" <| FSComp.SR.forBadFormatSpecifier()
raise (Failure (FSComp.SR.forBadFormatSpecifier()))
// Always error for %l and %Lx
failwithf "%s" <| FSComp.SR.forLIsUnnecessary()
raise (Failure (FSComp.SR.forLIsUnnecessary()))
match fmt.[i] with
| ('d' | 'i' | 'o' | 'u' | 'x' | 'X') ->
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
| _ -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifier()
let i = skipInterp (i+1)
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i, relLine, relCol+1)
| _ -> raise (Failure (FSComp.SR.forBadFormatSpecifier()))

| ('h' | 'H') ->
failwithf "%s" <| FSComp.SR.forHIsUnnecessary()
raise (Failure (FSComp.SR.forHIsUnnecessary()))

| 'M' ->
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, mkFlexibleDecimalFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, mkFlexibleDecimalFormatTypar g m) :: acc) (i, relLine, relCol+1)

| ('f' | 'F' | 'e' | 'E' | 'g' | 'G') ->
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, mkFlexibleFloatFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, mkFlexibleFloatFormatTypar g m) :: acc) (i, relLine, relCol+1)

| 'b' ->
checkOtherFlags ch
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, g.bool_ty) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, g.bool_ty) :: acc) (i, relLine, relCol+1)

| 'c' ->
checkOtherFlags ch
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, g.char_ty) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, g.char_ty) :: acc) (i, relLine, relCol+1)

| 's' ->
checkOtherFlags ch
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, g.string_ty) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, g.string_ty) :: acc) (i, relLine, relCol+1)

| 'O' ->
checkOtherFlags ch
collectSpecifierLocation relLine relCol 1
let i = skipInterp (i+1)
parseLoop ((posi, NewInferenceType ()) :: acc) (i, relLine, relCol+1)

// residue of hole "...{n}..." in interpolated strings
| 'P' when isInterp ->
checkOtherFlags ch
parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1)

| 'A' ->
match info.numPrefixIfPos with
| None // %A has BindingFlags=Public, %+A has BindingFlags=Public | NonPublic
| Some '+' ->
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1)
| Some _ -> failwithf "%s" <| FSComp.SR.forDoesNotSupportPrefixFlag(ch.ToString(), (Option.get info.numPrefixIfPos).ToString())
let i = skipInterp (i+1)
parseLoop ((posi, NewInferenceType ()) :: acc) (i, relLine, relCol+1)
| Some n -> raise (Failure (FSComp.SR.forDoesNotSupportPrefixFlag(ch.ToString(), n.ToString())))

| 'a' ->
checkOtherFlags ch
let xty = NewInferenceType ()
let fty = bty --> (xty --> cty)
collectSpecifierLocation relLine relCol 2
parseLoop ((Option.map ((+)1) posi, xty) :: (posi, fty) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((Option.map ((+)1) posi, xty) :: (posi, fty) :: acc) (i, relLine, relCol+1)

| 't' ->
checkOtherFlags ch
collectSpecifierLocation relLine relCol 1
parseLoop ((posi, bty --> cty) :: acc) (i+1, relLine, relCol+1)
let i = skipInterp (i+1)
parseLoop ((posi, bty --> cty) :: acc) (i, relLine, relCol+1)

| c -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifierGeneral(String.make 1 c)
| c -> raise (Failure (FSComp.SR.forBadFormatSpecifierGeneral(String.make 1 c)))

| '\n' -> parseLoop acc (i+1, relLine+1, 0)
| _ -> parseLoop acc (i+1, relLine, relCol+1)

let results = parseLoop [] (0, 0, m.StartColumn)
results, Seq.toList specifierLocations

let ParseFormatString m g formatStringCheckContext fmt bty cty dty =
let argtys, specifierLocations = parseFormatStringInternal m g formatStringCheckContext fmt bty cty
let ParseFormatString m g isInterp formatStringCheckContext fmt bty cty dty =
let argtys, specifierLocations = parseFormatStringInternal m g isInterp formatStringCheckContext fmt bty cty
let aty = List.foldBack (-->) argtys dty
let ety = mkRefTupledTy g argtys
(aty, ety), specifierLocations
(argtys, aty, ety), specifierLocations

let TryCountFormatStringArguments m g fmt bty cty =
let TryCountFormatStringArguments m g isInterp fmt bty cty =
try
let argtys, _specifierLocations = parseFormatStringInternal m g None fmt bty cty
let argtys, _specifierLocations = parseFormatStringInternal m g isInterp None fmt bty cty
Some argtys.Length
with _ ->
None
Expand Down
5 changes: 3 additions & 2 deletions src/fsharp/CheckFormatStrings.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ module internal FSharp.Compiler.CheckFormatStrings

open FSharp.Compiler
open FSharp.Compiler.NameResolution
open FSharp.Compiler.Range
open FSharp.Compiler.TypedTree
open FSharp.Compiler.TcGlobals

val ParseFormatString : Range.range -> TcGlobals -> formatStringCheckContext: FormatStringCheckContext option -> fmt: string -> bty: TType -> cty: TType -> dty: TType -> (TType * TType) * (Range.range * int) list
val ParseFormatString : m: range -> g: TcGlobals -> isInterp: bool -> formatStringCheckContext: FormatStringCheckContext option -> fmt: string -> bty: TType -> cty: TType -> dty: TType -> (TType list * TType * TType) * (range * int) list

val TryCountFormatStringArguments : m:Range.range -> g:TcGlobals -> fmt:string -> bty:TType -> cty:TType -> int option
val TryCountFormatStringArguments : m:Range.range -> g:TcGlobals -> isInterp: bool -> fmt:string -> bty:TType -> cty:TType -> int option
4 changes: 4 additions & 0 deletions src/fsharp/CompileOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,10 @@ let OutputPhasedErrorR (os: StringBuilder) (err: PhasedDiagnostic) (canSuggestNa
| Parser.TOKEN_EOF -> getErrorString("Parser.TOKEN.EOF")
| Parser.TOKEN_CONST -> getErrorString("Parser.TOKEN.CONST")
| Parser.TOKEN_FIXED -> getErrorString("Parser.TOKEN.FIXED")
| Parser.TOKEN_INTERP_STRING_BEGIN_END -> getErrorString("Parser.TOKEN.INTERP.STRING.BEGIN.END")
| Parser.TOKEN_INTERP_STRING_BEGIN_PART -> getErrorString("Parser.TOKEN.INTERP.STRING.BEGIN.PART")
| Parser.TOKEN_INTERP_STRING_PART -> getErrorString("Parser.TOKEN.INTERP.STRING.PART")
| Parser.TOKEN_INTERP_STRING_END -> getErrorString("Parser.TOKEN.INTERP.STRING.END")
| unknown ->
Debug.Assert(false, "unknown token tag")
let result = sprintf "%+A" unknown
Expand Down
7 changes: 6 additions & 1 deletion src/fsharp/ErrorLogger.fs
Original file line number Diff line number Diff line change
Expand Up @@ -688,4 +688,9 @@ let internal tryLanguageFeatureError langVersion langFeature m =
tryLanguageFeatureErrorAux langVersion langFeature m error

let internal tryLanguageFeatureErrorRecover langVersion langFeature m =
tryLanguageFeatureErrorAux langVersion langFeature m errorR
tryLanguageFeatureErrorAux langVersion langFeature m errorR

let internal languageFeatureNotSupportedInLibraryError (langVersion: LanguageVersion) (langFeature: LanguageFeature) (m: range) =
let featureStr = langVersion.GetFeatureString langFeature
let suggestedVersionStr = langVersion.GetFeatureVersionString langFeature
error (Error(FSComp.SR.chkFeatureNotSupportedInLibrary(featureStr, suggestedVersionStr), m))
5 changes: 5 additions & 0 deletions src/fsharp/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,8 @@ notAFunctionButMaybeDeclaration,"This value is not a function and cannot be appl
3350,chkFeatureNotLanguageSupported,"Feature '%s' is not available in F# %s. Please use language version %s or greater."
3351,chkFeatureNotRuntimeSupported,"Feature '%s' is not supported by target runtime."
3352,typrelInterfaceMemberNoMostSpecificImplementation,"Interface member '%s' does not have a most specific implementation."
3353,chkFeatureNotSupportedInLibrary,"Feature '%s' requires the F# library for language version %s or greater."
3360,lexByteStringMayNotBeInterpolated,"a byte string may not be interpolated"
useSdkRefs,"Use reference assemblies for .NET framework references when available (Enabled by default)."
fSharpBannerVersion,"%s for F# %s"
optsLangVersion,"Display the allowed values for language version, specify language version such as 'latest' or 'preview'"
Expand All @@ -1509,3 +1511,6 @@ featureFixedIndexSlice3d4d,"fixed-index slice 3d/4d"
featureAndBang,"applicative computation expressions"
featureNullableOptionalInterop,"nullable optional interop"
featureDefaultInterfaceMemberConsumption,"default interface member consumption"
featureStringInterpolation,"string interpolation"
3361,tcInterpolationMixedWithPercent,"Mismatch in interpolated string. Interpolated strings may not use '%%' formats unless each is given an expression, e.g. '%%d{{1+1}}'"
forFormatInvalidForInterpolated,"Invalid interpolated string. interpolated strings may not use '%%' formats unless each is given an expression, e.g. '%%d{{1+1}}'"
Loading

0 comments on commit 1ba6b74

Please sign in to comment.