Fsharp: Collection of findings related to string interpolation

Created on 7 Aug 2020  路  24Comments  路  Source: dotnet/fsharp

EDIT: if you want to test this awesome feature yourself (or any other "latest" feature), I've written up (and today, 9 Aug, corrected) a little instruction here: https://github.com/dotnet/fsharp/issues/9893#issuecomment-671040560

Just some findings I collect here about the upcoming string interpolation feature. Some may be "by design", or may already have been reported, but I offered @cartermp to do some testing, so here it goes ;).

For: https://github.com/dotnet/fsharp/pull/8907 (I used the latest successful build of the PR, of earlier today). Details of feature in RFC: https://github.com/fsharp/fslang-design/blob/master/preview/FS-1001-StringInterpolation.md.

Some additional notes (but not bugs afaict) in this comment by @cartermp: https://github.com/dotnet/fsharp/pull/8907#issuecomment-666599264

Summary and todo-list

  • [x] Half of the expressions are not evaluated [fixed&tested]
  • [x] Non-escaped closing curly considered legal [fixed&tested]
  • [ ] Empty expr between curlies
  • [ ] Prefix operators don't work on interp strings
  • [ ] Colors of curlies in tooling
  • [ ] Expression past closing quote considered part of the expression, making comments part of the interp string
  • [ ] Wrong error range for missing closing curly, or extra open curly-at-end
  • [ ] With combinator for Printf.StringFormat, errors can be all over the place
  • [ ] Use with literal gives too many errors
  • [ ] Really weird behavior when (wrongly) using compiler directives + multiline expr in interp strings
  • [ ] Multiline woes: wrong error-underline calculation
  • [ ] Multiline woes: coloring is off, part of string default color
  • [ ] Multiline woes: mysterious errors that are not an error when compiled
  • [ ] Potential error improvements

Summary of things tested so far (8/8/2020)

  • [x] Multiline strings with continuation character (bugs found, see above)
  • [x] #if statement in multiline expressions _just work_ as expected
  • [x] # lineno statements _just work_ as expected inside interp expr
  • [x] #compdirective, when invalid, wreaks havoc, much more than it used to (see "weird behavior" issue above)
  • [x] breakpoints in multiline expressions _just work_ and tooltips show correct values
  • [x] Coloring and ranges tested, some findings, see above
  • [x] Combinators and other functions working on StringFormat tested, some findings, see above
  • [x] Attempted to break the "no nested interp strings unless single-inside-triple", works as expected
  • [x] Escape sequences in strings, works as expected
  • [x] Non-closing curlies and other bad combinations, works as expected but some issues with tooling (see above)
  • [x] Quotations (see @cartermp reference above), works as expected
  • [x] Go-to-definition, works
  • [x] Nested curlied expressions (records, anonymous records, seq) work as expected (require space between curlies)
  • [x] Type inference works as expected ($"%i{x} requires x to be integer)
  • [x] Mixing %-style and interp-style disallowed, this is as expected
  • [x] Interaction with kprintf works
  • [x] Interaction with fully-typed StringFormat does not work with interp strings, I guess this is as expected:
    f# let log msg a = sprintf msg a log "%d" 24 // fine let f x = log "{x}" // error, wrong arity
  • [x] Outdent leniency of nested expr: not supported. Perhaps this can be added? (not a bug, though).
    f# // warning on wrong indentation: $"Some interpolated string { let x = 12 let y = 14 x + y }"
    Can be fixed by outdenting very far:
    f# $"Some interpolated string { let x = 12 let y = 13 x + y }"
  • [x] Use with literals: some issues, see above, but works in general.
  • [x] Use of invalid keywords in expr like module and type give the proper errors.

~Half of the expressions are not evaluated~ _fixed/tested_

It turns out that there's an issue with the third and subsequent string interpolation section in a composed string, leading to weird results (note how the "." get misplaced). It appears thatonly noOfExpr idiv 2 + 1 get evaluated:

```f#
// Missing data, half the expressions are not evaluated, or in the wrong position:
printfn $"""{1}{2}{3}""" // prints "12"
printfn $"""{1}{2}{3}{4}{5}{6}""" // prints "123"
printfn $"""{1}{2}{3}{4}{5}{6}{7}""" // prints "1234"
printfn $"""{$"{1}"}{$"{2}"}{$"{3}"}""" // prints "12"
printfn $"""{1},{2}{3}{4}.{5}""" // prints "1,23.4"
Console.WriteLine $"""{1}{2}{3}""" // prints "12"
Console.WriteLine $"""{1},{2}{3}{4}.{5}""" // prints "1,23.4"


## ~Non-escaped closing curly considered legal~ _fixed/tested_

The following came as a surprise to me, but may be based on how C# does things? This came as a surprise, I'd expect orthogonality here:

```f#
// inconsistency with escaping: "{" must be escaped as "{{", but "}" can be either literal, or escaped as "}}"
// Is this by accident? It makes more sense to always require escaping "{" AND "}", which means this is invalid:
printfn $"{1}}"    // should be invalid, but gives "1}"
printfn $"{1}}}"   // is valid and correctly gives "1}"

Empty expr between curlies

I'd prefer this to be legal, esp in light of autogen tools, that now have to put in null or None to eval to nothing, or remove the expr part:

```f#
let x = $"{}" // error


## Prefix operators don't work on interp strings

It's (quite) customary to (re)define unary prefix operators and they used to work "just fine" with strings, but they don't work the same way with interpolated strings, because `$` is considered part of the prefix operator. Since it is already forbidden to have an operator contain the dollar sign (except for dollar-only operators), I think this should be legal:

![image](https://user-images.githubusercontent.com/16015770/89680287-f3ab5a80-d8f2-11ea-9a59-e8490a460023.png)

```f#
let (!) x = x
printfn !$"{42}"   // workaround: use parens

Colors of curlies in tooling

Maybe we can distinguish colors of active { vs escaped {? It's pretty hard to notice the difference. Maybe we can make the active ones bright-red or something?

Expression past closing quote considered part of the expression, making comments part of the interp string

This gives "this value is not a function an cannot be applied" error, and wrongly colors the rest of the line:

```f#
printfn $"{1}{" // gives wrong error "1}"


![image](https://user-images.githubusercontent.com/16015770/89679044-96160e80-d8f0-11ea-8665-d22dc090d7f2.png)

This same behavior leads to other weird stuff, where the syntax checker keeps looking beyond the last quote:

![image](https://user-images.githubusercontent.com/16015770/89679338-2a807100-d8f1-11ea-9c49-4c4611ddee99.png)

Or this one, each line getting red to the end of file:

![image](https://user-images.githubusercontent.com/16015770/89679507-83e8a000-d8f1-11ea-8dfb-1dbc91c4482c.png)



## Wrong error range for missing closing curly, or extra open curly-at-end

Like this:

![image](https://user-images.githubusercontent.com/16015770/89592328-a07ecc80-d84c-11ea-8acc-bfa23986f349.png)

## With combinator for `Printf.StringFormat`, errors can be all over the place
_(it's actually awesome that this works mostly flawlessly and that I can re-use existing stringformat combinators!)_
The following code gets errors all over the place in the whole file:

```f#
/// indent each line by 4 spaces
let inline (<|>) a b = Printf.kprintf (sprintf "%s\n    %s" a) b

let f x =                  // wrong error on let, the string is closed, should not happen here
    sprintf ""
            <|> "Line one is twelve: %d" <| 12
            <|> $"Line two is twentyfour: {24}"
            <|> $"{1}{2}{3"      // error should be only here and coloring should not be gone...

f 1 |> printfn "%s"        // wrong error on interp string, this is NOT an interp string and not an error
exit 0
// wrong error at end-of-file for unfinished let, which is untrue, the string has a closing quote

See the 4 (!) errors in this animation, it should ideally be only one on wrong interp string:

8020843a-8db8-4fba-aec6-0576f30549cc

Use with literal gives too many errors

I can understand that use with literals is not allowed, not even when it can be assessed that the result of an interp string will be constant. But there are two errors raised, instead of one:

image

Really weird behavior when (wrongly) using compiler directives + multiline expr in interp strings

I've no idea what happens here, there seems to be nothing consistent. Everything gets green and improper errors pop up. The red # is as it is now (unexpected symbol in binding), the rest getting green not at all. Notice that this behavior disappears when the interpolated expression part is a single constant.

Repo code:

```f#
let x = 1 in printfn "\4{42}%d{x}" x

/// indent each line by 4 spaces
let inline (<|>) a b = Printf.kprintf (sprintf "%s\n %s" a) b
let log msg a = sprintf msg a

let x = log "%d" 12
//let x = log $"%O{x}" // not possible

let f x =
#nowarn "12" // any wrong compiler directive makes everything green and shows improper errors
sprintf $"ML str {
let x = 12 // replace this multiline with single expr and the green and wrong errors disappear
x }"


![4b3d8078-3db2-4241-b11d-ffbb21656148](https://user-images.githubusercontent.com/16015770/89713123-e4c9b400-d995-11ea-8832-22fd24fdace6.gif)


## Multiline woes, wrong error-underline calculation

```f#
let f configFile =
    sprintf $"This \
               Is \
                    A \
                            Multiline string error '{configFile}' %s"

image

Multiline woes, coloring is off, part of string default color

image

Multiline woes: mysterious errors that are not an error when compiled

This situation appears to depend on the size of the input (and is not necessarily related to the new feature, I can confirm this was the case previously):

Error is depending on text length in last line, it seems:

4b649c89-ac05-45f6-a603-97dbaf9548d4

Potential error improvements

Here are some areas where we may want to improve error reporting.

  1. Something about hinting like "remove whitespace between '{' and % expression" perhaps?
    image

  2. Text doesn't match the error, should be something like "Unclosed opening bracket in interpolated string found at position X in string":
    image

  3. Not sure how this can be improved, but the user here escaped the curly, but then gets an error that (s)he needs to escape the curly? Maybe we can report all situations where openingCurlies - closingCurlies <> 0 (excluding escapes) as a variant of "missing opening or closing bracket"? This would solve a range of error issues, I think.
    image

This is cool!

I just have to say I really like it that the result inside {...} is not just a string, but keeps type information, and that the result can be formatted. So this is entirely expected (and %f fixes the error):

image

I'll update this list if I find more stuff (it's getting a bit late over here ;) ).

Anybody else who'd like to try out this new feature and iron out some bugs, here's how to use the absolute latest, I've written some instructions in the PR: https://github.com/dotnet/fsharp/issues/9893#issuecomment-671040560

Area-Compiler Severity-Medium bug

All 24 comments

@abelbraaksma Thanks so much for trying this out! I'll get these fixed right now

It turns out that there's an issue with the third and subsequent string interpolation section in a composed string, leading to weird results (note how the "." get misplaced). It appears thatonly noOfExpr idiv 2 + 1 get evaluated:

Thanks, this bug hits when the specifiers are next to each other (which wasn't covered in our test suite).

I've fixed it now in the feature branch

printfn $"{1}}" // should be invalid, but gives "1}"

I agree this should be invalid, thanks

The first two problems are fixed in feature/string-interp

Maybe we can distinguish colors of active { vs escaped {? It's pretty hard to notice the difference. Maybe we can make the active ones bright-red or something?

@cartermp would like this too, I think it's reasonable.

Interesting, Rider and ReSharper highlight the "holes" in interpolated strings and string.Format calls differently:

  • interpolation holes ({ and }) are plaintext
  • string.format holes {0}, {1}, etc. are green

So that's some interesting prior art

Glad it was helpful and could be fixed easily 馃憤. But this got auto-closed due to the merge, with parts of the issue still open/unaddressed. Shall I post the rest again separately, or can you (@kevinransom, @dsyme) reopen this? (I don't have enough rights to do that).

I assume subsequent fixes will go to master and the feature branch gets removed?

@abelbraaksma , issues will be addressed as bugs in the normal way.

@abelbraaksma can you go through and add a checklist item for each thing you noticed, marking the ones that are addressed?

@cartermp, done, I gave each bug a sensible (sub)title and on top there's a simple checklist of each of them.

@dsyme, I've added a bunch of issues in the last hour or so. Nothing egregious, I think.

I've also added a list of "Summary of things tested so far", so that others don't have to re-asses those areas. I'm sure there are gaps there, please advice if there's an area that needs extra attention (@dsyme, @cartermp).

I tried @abelbraaksma's directions and got FS3353 saying I need language version preview, even though I have <LangVersion>preview</LangVersion> defined. I'll probably just wait for a formal preview for now but will point out I noticed that there are two 3353's in FSComp.txt in case that's an issue:

3353,chkFeatureNotSupportedInLibrary,"Feature '%s' requires the F# library for language version %s or greater."
3353,fsiInvalidDirective,"Invalid directive '#%s %s'"

Nice work @abelbraaksma .

let x = $"{}" // error

This should be an error. Representing an empty string, or "nothing", or null, or None, as a empty space is not allowed in other places in F# (e.g. let x = // error) and would create confusion and irregularity.

saying I need language version preview, even though I have preview

EDIT: there was an error in my original instruction, I wrote <LanguageVersion>, which should be <LangVersion>, but it appears you got that right, tx ;).

But the error you saw, @zanaptak, is about the reference to the FSharp.Core.dll library. You need to also add <DisableImplicitFSharpCoreReference>true</... and you must explicitly add a reference to the local FSharp.Core.dll build. See next message for a full example of an fsproj file. Could you please try it again?

@charlesroddie, you've got a good point. There's actually a PR right now that's addressing that very thing to get a more meaningful error.

@zanaptak, for reference, I'll copy the instructions here as well (and created an issue to smoothen the situation in the future: https://github.com/dotnet/fsharp/issues/9904):

For anybody wanting to play around with the latest build of this feature (esp. those that aren't aware how to do this and are curious where to start):

  • Checkout this feature branch (feature/string-interp)
  • Build by running build.cmd (if you already had a build before switching branches, use git clean -xdf first)
  • Open VisualFSharp.sln, select Release and Build All
  • Select VisualFSharpFull as startup project, hit Ctrl-F5

A new VS instance is started with the new features selected, but when you create a project, it will, by default, reference an FSharp.Core.dll from NuGet. To fix this:

  • Open your test fsproj file and add a line <DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference> in the top property group
  • In the same property group, add <LangVersion>preview</LangVersion>
  • OR: In the same property group, add <OtherFlags>--langversion:preview</OtherFlags>
  • Add a standard file assembly reference to [fsharp-checkoutdir]\artifacts\bin\VisualFSharpFull\Release\net472\FSharp.Core.dll

A minimal project file for netcoreapp3.1 could look something like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
    <LangVersion>preview</LangVersion>
    <!-- either the prev. line, or the following line -->
    <OtherFlags>--langversion:preview</OtherFlags>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <WarningsAsErrors>3239;25</WarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="FSharp.Core">
      <HintPath>D:\Projects\OpenSource\FSharp\artifacts\bin\VisualFSharpFull\Release\net472\FSharp.Core.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

Now you can start typing your code without errors along the line of "Feature 'string interpolation' requires the F# library for language version 4.7 or greater." (an error you will also get if your project _actually references 4.7.0_, but that's a bug for another time, or maybe I just screwed something up locally).

I already had the disabling and the file reference in place, I'll try the new flag later. Should we use master branch now since it's merged and may get ongoing fixes?

Yes, you can use the master branch. Feel free to ping me on slack if you have trouble, I'm also trying to get a clear picture of the issues with running from a fresh build.

Btw, you do need to run build from a vs 2019 developer command prompt, I think.

printfn $"{1}{" // gives wrong error "1}"

I don't get this. This is an interpolated single quote string containing a single quote string applied to 1 as a function

Single quote strings containing a single quote string are not allowed but that is separate.

Adding some spacing may help see what's going on:

printfn $"{1}{   "<---->"   1     }"

Thank you so much for your amazing QA work here!!

(I'll be reading the rest more closely on Monday)

I don't get this. This is an interpolated single quote string containing a single quote string applied to 1 as a function

Single quote strings containing a single quote string are not allowed but that is separate.

@dsyme I meant to say (see screenshot) that I expect the error to end at the invalid last opening { of the first string.

$" starts the string, the first following, non escaped " closes the string. So the error should be about printfn $"{1}{" only, and complain that there's a non terminated { at the end.

The rest of that line is whitespace and comment (but becomes part of the error).

It appears as if there's a greedy regex match going on, instead of a non greedy one. This may also be because currently, the parser tries to find out whether the user attempts to nest another interpolated string inside it, and is conflicts with the user being half way inside typing a valid expression (he just opened the next curly), coloring the rest of the file as error.

I'm first trying to work out if there are must-fix issues here

I think these are by-design:

  • Empty expr between curlies - this was never intended to work.

  • "Expression past closing quote considered part of the expression" - I don't think so, see analysis above, which I believe still holds

  • "Wrong error range for missing closing curly, or extra open curly-at-end" - I think this is similar

  • "With combinator for Printf.StringFormat, errors can be all over the place" - i think this is similar, again 3"... is being treated as a interpolation fill expression where 3 is being applied to a string".... This is just always going to create strange errors. But it is hard to see how they can be easily improved.

These are all potential future improvements?

  • Prefix operators don't work on interp strings: Seems reasonable, could be done later

  • Colors of curlies in tooling: Seems reasonable, could be done later

  • Use with literal gives too many errors

  • Really weird behavior when (wrongly) using compiler directives + multiline expr in interp strings

  • Multiline woes: wrong error-underline calculation

  • Multiline woes: coloring is off, part of string default color

  • Multiline woes: mysterious errors that are not an error when compiled

  • Potential error improvements

These are all potential future improvements?

I would consider some of them bugs, and not just improvements (esp the multiline issues), but fixing a bug is an improvement, isn't it? ;). Though none of them are likely showstoppers, before this gets wildly used, it may be good if we can iron out some of these.

I don't think so, see analysis above, which I believe still holds

@dsyme The problem with these is that $"{SomeExpr}{" (i.e. a valid string holding an opening curly, being invalid interp. string) causes issues with the underlining of the errors. Since it is invalid to have a nested quoted interpolated string in a single-quoted string, the error here is not that the last { starts a new expression with an expression starting with a " (quote), but that the whole expression here is (ignoring escaped quotes) the regex \$"[^]*?", and the error-checker (or recovery) doesn't consider this, instead it greedily runs to the next quote (as if the regex \$"[^]*" were used instead).

Surprising stuff happens when you are just typing, which creates a scary experience while you are in de middle of an interpolated string. In heavy files, the constant "whole file is an error" experience may lead to heavy spinning of VS.

@abelbraaksma, reporting this from the slack channel:

There seems to be the following issue with embedding string literals inside interpolated strings. I was able to produce the following error in FSI:

$"{""}" // error FS0010: Unexpected interpolated string (final part) in interaction
$"""{""}""" // works fine

This is my fsi version info:

Microsoft (R) F# Interactive version 11.0.0.0 for F# 5.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

My dotnet info:

.NET SDK (reflecting any global.json):
 Version:   5.0.100-preview.8.20417.9
 Commit:    fc62663a35

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  20.04
 OS Platform: Linux
 RID:         ubuntu.20.04-x64
 Base Path:   /usr/share/dotnet/sdk/5.0.100-preview.8.20417.9/

Host (useful for support):
  Version: 5.0.0-preview.8.20407.11
  Commit:  bf456654f9

.NET SDKs installed:
  3.1.201 [/usr/share/dotnet/sdk]
  3.1.301 [/usr/share/dotnet/sdk]
  5.0.100-preview.3.20216.6 [/usr/share/dotnet/sdk]
  5.0.100-preview.7.20366.6 [/usr/share/dotnet/sdk]
  5.0.100-preview.8.20417.9 [/usr/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 3.1.3 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.5 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0-preview.3.20215.14 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0-preview.7.20365.19 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0-preview.8.20414.8 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.3 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.5 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0-preview.3.20214.6 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0-preview.7.20364.11 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0-preview.8.20407.11 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download
-----------------------------------------------

my FSharp.Core version is 4.7.2 per my paket.lock file.

Was this page helpful?
0 / 5 - 0 ratings