Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/modules/language-reference/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,7 @@ String literals are enclosed in double quotes:
"Hello, World!"
----

TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences,
have stricter rules for line indentation in multiline strings, and do not have a line continuation character.],
TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences and stricter rules for line indentation in multiline strings.],
String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them!

Inside a string literal, the following character escape sequences have special meaning:
Expand Down Expand Up @@ -305,7 +304,8 @@ To write a string that spans multiple lines, use a multiline string literal:
----
"""
Although the Dodo is extinct,
the species will be remembered.
the species \
will be remembered.
"""
----

Expand All @@ -314,9 +314,10 @@ String content and closing delimiter must each start on a new line.

The content of a multiline string starts on the first line after the opening quotes and ends on the last line before the closing quotes.
Line breaks are included in the string and normalized to `\n`.
Line breaks following a `\` are ignored.

The previous multiline string is equivalent to this single-line string.
Notice that there is no leading or trailing whitespace.
Notice that there is no leading or trailing whitespace and the second newline is not present.

[source%tested,{pkl-expr}]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.pkl.parser.Token.FALSE;
import static org.pkl.parser.Token.NULL;
import static org.pkl.parser.Token.STRING_ESCAPE_BACKSLASH;
import static org.pkl.parser.Token.STRING_ESCAPE_CONTINUATION;
import static org.pkl.parser.Token.STRING_ESCAPE_NEWLINE;
import static org.pkl.parser.Token.STRING_ESCAPE_QUOTE;
import static org.pkl.parser.Token.STRING_ESCAPE_RETURN;
Expand All @@ -41,7 +42,8 @@ private SyntaxHighlighter() {}
STRING_ESCAPE_RETURN,
STRING_ESCAPE_QUOTE,
STRING_ESCAPE_BACKSLASH,
STRING_ESCAPE_UNICODE);
STRING_ESCAPE_UNICODE,
STRING_ESCAPE_CONTINUATION);

private static final EnumSet<Token> constant = EnumSet.of(TRUE, FALSE, NULL);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ examples {
"""
\u{9}\u{30}\u{100}\u{1000}\u{10000}\u{010000}\u{0010000}\u{00010000}
"""

"""
hello \
world
"""

#"""
hello \#
world
"""#

"""
hello \
\
world
"""
}

["dollar sign has no special meaning"] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
foo = """
hello \
\
world
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo = "hello \
world"
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ examples {

"""
"\t0Āက𐀀𐀀𐀀𐀀"
"hello world"
"hello world"
"hello world"
}
["dollar sign has no special meaning"] {
"123$ $123 $&% $"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
–– Pkl Error ––
Line must match or exceed indentation of the String's last line.

x | \
^
at parser19 (file:///$snippetsDir/input/errors/parser19.pkl)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
–– Pkl Error ––
Invalid line continuation escape sequence.

Line continuations are only allowed in multi-line strings.

x | foo = "hello \
^^
at parser20 (file:///$snippetsDir/input/errors/parser20.pkl)
5 changes: 4 additions & 1 deletion pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ internal class Builder(sourceText: String, private val grammarVersion: GrammarVe
NodeType.SHEBANG,
NodeType.OPERATOR -> Text(node.text(source))
NodeType.STRING_NEWLINE -> mustForceLine()
NodeType.STRING_CONTINUATION ->
Nodes(listOf(Text(node.text(source).dropLast(1)), mustForceLine()))
NodeType.MODULE_DECLARATION -> formatModuleDeclaration(node)
NodeType.MODULE_DEFINITION -> formatModuleDefinition(node)
NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node)
Expand Down Expand Up @@ -887,7 +889,8 @@ internal class Builder(sourceText: String, private val grammarVersion: GrammarVe
if (elem.type == NodeType.TERMINAL && elem.text().endsWith("(")) {
isInStringInterpolation = true
}
add(format(elem))
val formatted = format(elem)
if (formatted is Nodes) addAll(formatted.nodes) else add(formatted)
prev = elem
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ quux {
"""
}

// line continuations
corge {

"""
hello \
world
"""

#"""
hello \#
world
"""#

"""
hello \
\
world
"""
}

obj {
data {
["bar"] = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ quux {
"""
}

// line continuations
corge {
"""
hello \
world
"""

#"""
hello \#
world
"""#

"""
hello \
\
world
"""
}

obj {
data {
["bar"] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@ private Node parseSingleLineStringLiteralExpr() {
STRING_ESCAPE_RETURN,
STRING_ESCAPE_UNICODE ->
children.add(make(NodeType.STRING_ESCAPE, next().span));
case STRING_ESCAPE_CONTINUATION ->
throw parserError("invalidLineContinuationEscapeSequence");
case INTERPOLATION_START -> {
children.add(makeTerminal(next()));
ff(children);
Expand Down Expand Up @@ -1011,6 +1013,8 @@ private Node parseMultiLineStringLiteralExpr() {
}
}
case STRING_NEWLINE -> children.add(make(NodeType.STRING_NEWLINE, next().span));
case STRING_ESCAPE_CONTINUATION ->
children.add(make(NodeType.STRING_CONTINUATION, next().span));
case STRING_ESCAPE_NEWLINE,
STRING_ESCAPE_TAB,
STRING_ESCAPE_QUOTE,
Expand Down Expand Up @@ -1060,7 +1064,8 @@ private void validateStringIndentation(List<Node> nodes) {
throw parserError(ErrorMessages.create("stringIndentationMustMatchLastLine"), child.span);
}
}
previousNewline = child.type == NodeType.STRING_NEWLINE;
previousNewline =
child.type == NodeType.STRING_NEWLINE || child.type == NodeType.STRING_CONTINUATION;
}
}

Expand Down
1 change: 1 addition & 0 deletions pkl-parser/src/main/java/org/pkl/parser/Lexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ private Token lexEscape() {
yield Token.INTERPOLATION_START;
}
case 'u' -> lexUnicodeEscape();
case '\n' -> Token.STRING_ESCAPE_CONTINUATION;
default ->
throw lexError(
ErrorMessages.create("invalidCharacterEscapeSequence", "\\" + (char) ch, "\\"),
Expand Down
7 changes: 6 additions & 1 deletion pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,8 @@ private Expr parseSingleLineStringLiteralExpr() {
end = tk.span;
builder.append(parseUnicodeEscape(tk));
}
case STRING_ESCAPE_CONTINUATION ->
throw parserError("invalidLineContinuationEscapeSequence");
case INTERPOLATION_START -> {
var istart = next().span;
if (!builder.isEmpty()) {
Expand Down Expand Up @@ -1159,7 +1161,8 @@ private Expr parseMultiLineStringLiteralExpr() {
STRING_ESCAPE_QUOTE,
STRING_ESCAPE_BACKSLASH,
STRING_ESCAPE_RETURN,
STRING_ESCAPE_UNICODE ->
STRING_ESCAPE_UNICODE,
STRING_ESCAPE_CONTINUATION ->
stringTokens.add(new TempNode(next(), null));
case INTERPOLATION_START -> {
var istart = next();
Expand Down Expand Up @@ -1225,6 +1228,7 @@ private List<StringPart> renderString(List<TempNode> nodes, String commonIndent)
builder.append('\n');
isNewLine = true;
}
case STRING_ESCAPE_CONTINUATION -> isNewLine = true;
case STRING_PART -> {
var text = token.text(lexer);
if (isNewLine) {
Expand Down Expand Up @@ -1629,6 +1633,7 @@ private String getEscapeText(FullToken tk) {
case STRING_ESCAPE_BACKSLASH -> "\\";
case STRING_ESCAPE_TAB -> "\t";
case STRING_ESCAPE_RETURN -> "\r";
case STRING_ESCAPE_CONTINUATION -> "";
case STRING_ESCAPE_UNICODE -> parseUnicodeEscape(tk);
default -> throw new RuntimeException("Unreacheable code");
};
Expand Down
1 change: 1 addition & 0 deletions pkl-parser/src/main/java/org/pkl/parser/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public enum Token {
STRING_ESCAPE_QUOTE,
STRING_ESCAPE_BACKSLASH,
STRING_ESCAPE_UNICODE,
STRING_ESCAPE_CONTINUATION,
STRING_END,
STRING_PART;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,6 +70,7 @@ public enum NodeType {
STRING_CHARS,
OPERATOR,
STRING_NEWLINE,
STRING_CONTINUATION,
STRING_ESCAPE,

// members
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ Invalid Unicode escape sequence `{0}`.\n\
\n\
Valid Unicode escape sequences are {1}'{'0'}' to {1}'{'10FFFF'}' (1-6 hexadecimal characters).

invalidLineContinuationEscapeSequence=\
Invalid line continuation escape sequence.\n\
\n\
Line continuations are only allowed in multi-line strings.

missingDelimiter=\
Missing `{0}` delimiter.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class GenericSexpRenderer(code: String) {
NodeType.TERMINAL,
NodeType.OPERATOR,
NodeType.STRING_NEWLINE,
NodeType.STRING_CONTINUATION,
)

private val UNPACK_CHILDREN =
Expand Down
Loading