Significant Whitespace at the End of Multiline String

I’m converting a previously concatenated string to the new multi-line string syntax. It contains signifiant whitespace at the end of lines, so I have to use \n\ to explicitly properly terminate the line where it should be, to protect it from editor (Xcode) that strips the whitespace from the end of the line. Problem is with the last line, where I don’t want the newline to be part of the string, but cannot use just a single \ because it gives me error:

Escaped newline at the last line is not allowed

Fix offered by Xcode looked suspicious...
Fixme-escaped-newline
...but I clicked it anyway, to have my string corrupted (removing leading whitespace and just stripping the \).

In searching for solution I have looked at following sources:

An escape character at the end of the last line of a literal is an error, as no newlines follow.

… which is a consequence of the implementation using regular-expression: /\\[ \t]*\n/.

Reasons for Status Quo

My impression is that we currently have two rules for multiline strings:

  1. remove newlines following and preceding the """ delimiter
  2. remove (whitespace and) newline following the \

Theoretically they can create a conflict, when we put the \ on last line before the """ delimiter, this wants to consume two newlines where there’s only one.

But in my opinion this is completely harmless. I can not think of a situation where this is a genuine error. I see no point in enforcing this rule. From cursory look at the implementation, my impression is that PR #11199 had to add extra complexity on top of initial SE-0182 implementation just to enforce the third rule from the proposal. In my opinion, the third rule, even though technically correct consequence of judicious regex application, is entirely unnecessary.

In fact it keeps me from writing a multi-line string with significant whitespace at the end, without a newline in natural way:

let asciiArt = """
 \ /   \n\
  V    \
"""

Current Workaround

Reading back through the discussion of [Accepted] SE-0168, where the issues related to trailing whitespace were raised before, I found this idea (from a completely different context), that offers a workaround for my issue:

So the first workaround I found after the multi-hour, above-documented, research ordeal was to abuse string interpolation:

let asciiArt = """
 \ /   \n\
  V    \("")
"""

Thinking more about the rules, it finally downed on me that to write a multiline string that ends with significant whitespace and doesn’t contain a trailing newline, I have to insert one more newline:

let asciiArt = """
 \ /   \n\
  V    \

"""

Questions About Future

Is the current implementation in desired state? If yes, I should probably post my workaround to Stack Overflow to save other Swift programmers hours of frustration.

Can we do better and just drop the pointless rule and its enforcement?

1 Like

You could simply deactivate the whitespace stripping in Xcode- it’s a pure editor setting…

Forcing ourselves to work to make our tools happy is stupid, when one can easily change the tool to work as desired.

1 Like

Hi @palimondo and thank you for bringing this up again. Your observation is correct and this is by design of SE-0182. That being said, I'm not saying it should stay that way I'm saying that the trailing backslash is treated like a new line escaping character only at this moment of time. Since the last line before the bottom """ delimiter does not inject any new line character there is nothing for \ to escape.

During the pitch and review process of SE-0168 I raised clearly that there will be potential edge cases like this which we would not solve elegantly. There was some support to extend the rules for the trailing backslash but the community felt it would complicate the overall multi-line string literal rules and also considered trailing whitespaces as harmless, which I personally did not agreed with.

Can you maybe present a more critical and significant example for the problem you're facing rather then the small asciiArt example? I think it would help to drive this discussion forward and I also think we can potentially push this towards a formal proposal which would make the trailing backslash not only an escaping character but a trailing whitespace limiter (that should solve the problem and furthermore warn about invisible trailing whitespaces to the reader).

I can not. I’m sorry if this looks frivolous, but what I’m doing at the moment is writing a CheckResult assertion for a benchmark that renders Mandelbrot set as ASCII (based on this beautiful Haskell :gem:).

1 Like

Just a few notes before I got to leave for work. The rules that we must add must stay additive, which is easily possible since introducing warnings does not break code.

  • In any line, except the very last line, between the """ delimiters if there are trailing whitespaces we should raise a warning and provide three fix-it's:

    • Add \n\ if you want to keep the whitespaces but also need a line break
    • Add \ if you only want to keep the whitespaces and escape the line injection
    • Delete all the whitespaces
  • If the last line contains whitespaces raise a warning and provide a two fix-it's:

    • Add \ if you only want to keep the whitespaces and escape the line injection
    • Delete all the whitespaces
  • The trailing \ will have two functionalities: it will escape new line characters and should be used as a whitespace character limiter.

Seems like a lot of extra warnings and rules to me for trailing whitespace, especially given that it was already considered the first time around and seems generally benign to me. Allowing \ at the end of last line seems like a more targeted solution for this case, and makes logical sense given the behaviour of \ at the end of other lines.

1 Like

I agree with @jawbroken here. To illustrate the full debugging experience: I had several mistakes in my code, where the assertion was failing and I had to investigate why… given the white space involved I had to compare print output from console and copy/paste it to TextEdit and do visual diff in two overlayed windows anyway. That’s how I caught my initial of-by-one error in dimensions. But spotting the stripped white space issue at the line ends was easy once I’ve selected the source in Xcode for copying… I can’t image how more compiler warnings would bring anything other then additional frustration.

However this would be rather a hack then an alignment of the behavior for the trailing backslash. I think we should not add fixes for edge cases but rather envision everything as a whole and provide a more general solution. I'm not entirely against that what you said but I'd personally would go all or nothing and for the sake of the formal proposal it can be mentioned in the alternative section at least. ;)

I don’t see it as a hack. The third rule from the proposal is a self-inflicted wound, which comes from overly literal interpretation of the regex. That corner case exists only when you take it ad-absurdum.

Hmm I probably had not enough coffee yet, we can also go the full silent way without any warnings and make the trailing backslash a whitespace limiter for all the lines, although I'd prefer warnings if you have trailing whitespace characters at the end of any line and they are not marked explicitly with \ or \n\, otherwise those whitespaces should be trimmed anyway (that's where the whole idea of 2-3 warnings comes from).

Feel free to prepare a draft proposal. ;) I'll be there to support it and provide feedback.

Let’s not put the carriage before the horse. First I would like to hear some feedback from original proposal authors and implementers (@johnno1962, @beccadax, @rintaro), maybe there’s a reason for the third rule I’m not seeing…
It would also help if somebody from core team (@Joe_Groff, @Chris_Lattner3) could recall the reasoning that made them support the third rule “in this go-around."

:wave: One author is here, the other authors are @hartbit and @johnno1962 . The original proposal had no trailing backslash functionality at all (that design was dropped during/before review). We were asked by Chris Lattner to tackle another proposal which added the rules for the new line escaping.

Ah, I’m sorry I’ve copied authors from SE-0168 by mistake.

\ is not allowed at the end of the last line because there is no newline to escape.

IMO, the biggest problem here is:

The fix-it should offer an option to add an extra blank line to make it:

let asciiArt = """
 \ /   \n\
  V    \

"""

I think this is the recommended way to achieve your intention.

3 Likes

That is both technically correct and an absurd technicality without a purpose. I was hoping for a reason that makes practical sense. So you are also not aware of a real problem this rule is trying to prevent?

I think the rationale for the last line to not include a new line is to mimic something like a text document where the last line won't add an additional new line which makes total sense. I personally don't like both workaround, the empty line nor the \(""). I'd rather extend the rules for \ a little.

This seems fairly analogous to a trailing comma in an array literal. There's no following element, so in one way it's kind of nonsensical to allow it. But it offers a real tangible benefit for the way people use the syntax, and the benefit outweighs the "cost".

5 Likes

No, I don't know any.

However, I don't like the idea \ at the end of the line will have two meanings at the same time ("escape new line characters" and "whitespace character limiter")

Instead, how about introducing "noop" escape sequence which is replaced with empty string by the compiler? It's basically the same as \(""), but I think it's easy to read/type, and makes typechecking fast.

For example:

let preformattedString = """
     *   * \_
      * *  \_
       *   \_
    """

is parsed as:

let preformattedString = " *   * \n  * *  \n   *   "
1 Like

You could think of it as “escaping the newline that would be removed anyway” instead if that helps. I don't really see it as two roles, and I think the @nnnnnnnn analogy to a trailing comma in an array literal is a great way to think about it. I'm not seeing the advantage of introducing new syntax like \_ here.