Repository: apple/pkl (2025-08-06T06:22:01Z)

Issue:
Incorrectly formatted YAML strings
This Pkl emits incorrect YAML:

```pkl
foo = "\t\\d"

output {
  renderer = new YamlRenderer {}
}
```


Produces:

```yml
foo: "\t\d"
```

Should be:

```yml
foo: "\t\\d"
```


Discussion:

Patch:
diff --git a/pkl-core/src/main/java/org/pkl/core/util/yaml/YamlEscaper.java b/pkl-core/src/main/java/org/pkl/core/util/yaml/YamlEscaper.java
index 29a21daec..afd32482d 100644
--- a/pkl-core/src/main/java/org/pkl/core/util/yaml/YamlEscaper.java
+++ b/pkl-core/src/main/java/org/pkl/core/util/yaml/YamlEscaper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
+ * Copyright © 2024-2025 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.
@@ -19,33 +19,55 @@
 import org.pkl.core.util.IoUtils;
 import org.pkl.core.util.Nullable;
 
-// https://yaml.org/spec/1.2.2/#57-escaped-characters
+/**
+ * Emits escape sequences for YAML. This is only used when emitting double-quoted strings.
+ *
+ * <p>Note: we don't need to escape space ({@code 0x20}) because we don't generate quoted multiline
+ * strings. We also don't need to escape forward slash ({@code 0x2f}) because a normal forward slash
+ * is also valid YAML.
+ *
+ * @see <a
+ *     href="https://yaml.org/spec/1.2.2/#57-escaped-characters">https://yaml.org/spec/1.2.2/#57-escaped-characters</a>
+ */
 public final class YamlEscaper extends AbstractCharEscaper {
   private static final String[] REPLACEMENTS;
 
   static {
-    REPLACEMENTS = new String[0x22 + 1];
+    REPLACEMENTS = new String[0xA0 + 1];
     for (var i = 0; i < 0x20; i++) {
       REPLACEMENTS[i] = IoUtils.toHexEscape(i);
     }
+    // ns-esc-null
     REPLACEMENTS[0x00] = "\\0";
+    // ns-esc-bell
     REPLACEMENTS[0x07] = "\\a";
+    // ns-esc-backspace
     REPLACEMENTS[0x08] = "\\b";
+    // ns-esc-horizontal-tab
     REPLACEMENTS[0x09] = "\\t";
+    // ns-esc-line-feed
     REPLACEMENTS[0x0A] = "\\n";
+    // ns-esc-vertical-tab
     REPLACEMENTS[0x0B] = "\\v";
+    // ns-esc-form-feed
     REPLACEMENTS[0x0C] = "\\f";
+    // ns-esc-carriage-return
     REPLACEMENTS[0x0D] = "\\r";
+    // ns-esc-escape
     REPLACEMENTS[0x1B] = "\\e";
-    // we don't ever need to escape 0x20 because we don't generate quoted multiline strings
+    // ns-esc-double-quote
     REPLACEMENTS[0x22] = "\\\"";
+    // ns-esc-backslash
+    REPLACEMENTS[0x5c] = "\\\\";
+    // ns-esc-next-line
+    REPLACEMENTS[0x85] = "\\N";
+    // ns-esc-non-breaking-space
+    REPLACEMENTS[0xA0] = "\\_";
   }
 
   @Override
   protected @Nullable String findReplacement(char ch) {
     //noinspection UnnecessaryUnicodeEscape
-    return ch <= '\u0022'
-        ? REPLACEMENTS[ch]
-        : ch == '\u2028' ? "\\L" : ch == '\u2029' ? "\\P" : null;
+    return ch <= 0xA0 ? REPLACEMENTS[ch] : ch == '\u2028' ? "\\L" : ch == '\u2029' ? "\\P" : null;
   }
 }


Test patch:
diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer9.yml.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer9.yml.pkl
new file mode 100644
index 000000000..344d0f27b
--- /dev/null
+++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer9.yml.pkl
@@ -0,0 +1,36 @@
+// test escaping in double quotes
+// every string is prefixed with `\t` s.t. YamlRenderer will emit double-quoted strings.
+
+`null` = "\t\u{00}"
+
+bell = "\t\u{7}"
+
+backspace = "\t\u{8}"
+
+horizontalTab = "\t"
+
+lineFeed = "\t\u{a}"
+
+verticalTab = "\t\u{b}"
+
+formFeed = "\t\u{c}"
+
+carriageReturn = "\t\r"
+
+escape = "\t\u{1b}"
+
+doubleQuote = "\t\""
+
+backslash = "\t\\"
+
+nextLine = "\t\u{85}"
+
+nbsp = "\t\u{a0}"
+
+lineSep = "\t\u{2028}"
+
+paragraphSep = "\t\u{2029}"
+
+output {
+  renderer = new YamlRenderer {}
+}
diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRendererStringsCompat.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRendererStringsCompat.pkl
index b648b5079..7bf2c28b5 100644
--- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRendererStringsCompat.pkl
+++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRendererStringsCompat.pkl
@@ -114,12 +114,12 @@ examples {
     render(".NAN")
     render(".nAn")  // never float
   }
-  
+
   ["tag like strings"] {
     "!!bool true"
     "!!str my string value"
   }
-  
+
   ["number like string keys"] {
     render(new Dynamic {
       `0` = "0"
diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer9.yml b/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer9.yml
new file mode 100644
index 000000000..4196874fc
--- /dev/null
+++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer9.yml
@@ -0,0 +1,15 @@
+'null': "\t\0"
+bell: "\t\a"
+backspace: "\t\b"
+horizontalTab: "\t"
+lineFeed: "\t\n"
+verticalTab: "\t\v"
+formFeed: "\t\f"
+carriageReturn: "\t\r"
+escape: "\t\e"
+doubleQuote: "\t\""
+backslash: "\t\\"
+nextLine: "\t\N"
+nbsp: "\t\_"
+lineSep: "\t\L"
+paragraphSep: "\t\P"