Repository: tweag/nickel
Pull request #624 (2022-02-24 08:56:56)

Replace `#` by `%` for multilines strings and interpolation
To be able to use `#` for comments instead of `//` as proposed in (TODO: which issue?) we should use a different symbol for multilines strings.
In (this comment, `%` is proposed)[#]. This change imply to change also the character used for interpolation because its linked to it (same number of character to delimit a multiline string than used to start interpolation).


Patch:
diff --git a/benches/records/countLetters.ncl b/benches/records/countLetters.ncl
index 2664d78e..56da0264 100644
--- a/benches/records/countLetters.ncl
+++ b/benches/records/countLetters.ncl
@@ -2,8 +2,8 @@
   run = fun s =>
     let updateDict = fun dict char =>
       if record.has_field char dict then
-        dict -$ char & {"#{char}" = dict."#{char}" + 1}
+        dict -$ char & {"%{char}" = dict."%{char}" + 1}
       else
-        dict & {"#{char}" = 1} in
+        dict & {"%{char}" = 1} in
     list.foldl updateDict {} (string.chars s)
 }
diff --git a/benches/records/merge.ncl b/benches/records/merge.ncl
index 1008d5c4..795155fd 100644
--- a/benches/records/merge.ncl
+++ b/benches/records/merge.ncl
@@ -6,7 +6,7 @@
     let makeRecStep = fun state k =>
       let name = state.prevName ++ (string.from_num k) in
       {
-        value = state.value & {"#{name}" = {}},
+        value = state.value & {"%{name}" = {}},
         prevName = name,
       } in
     let topList = list.generate function.id m in
diff --git a/benches/strings/interpolation.ncl b/benches/strings/interpolation.ncl
index 30923eb8..0066b937 100644
--- a/benches/strings/interpolation.ncl
+++ b/benches/strings/interpolation.ncl
@@ -1,13 +1,13 @@
 {
   run = fun n => if n < 1 then
     ""
-      else m#"
+      else m%"
         5bSVkTosPNAmX5yhmsDM
         Vdj4osv58320QrJPzBao
         NvTwB4CCW7vSDWPc7fe8
         3rERpwXOLNUjezETWmbn
         RgRmE9in3s76V9sZLhzN
-        #{run (n/2)}
+        %{run (n/2)}
         SHEIppXAjGy7vNRmZN04
         d29esHvAvO3a6Zu6Le3i
         nzyq1iuehn935aIVTjEH
@@ -30,7 +30,7 @@
         RmcKhKD4DcTiR5ECcWEy
         EdFOgWopcVR0EXO2AyNX
         FqqcINBbEGzeidZpXsq4
-        #{run (n/2)}
+        %{run (n/2)}
         vl7YkBO8cOTlIZGBc5ri
         CR3TKlFKUbxRcb6EjU0E
         rYrZxe8imLoBIPxrhKY7
@@ -104,5 +104,5 @@
         Ue3d9Jd3nZeSetAzBBqk
         DvN7NvTzdt4EyvUCq4Se
         a9TLAq2eVr0exurkvJZN
-      "#m
+      "%m
 }
diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md
index 31190117..31430c66 100644
--- a/doc/manual/syntax.md
+++ b/doc/manual/syntax.md
@@ -77,32 +77,32 @@ false
 ### Strings
 
 Nickel can work with sequences of characters, or strings.
-Strings are enclosed by `" ... "` for a single line string or by `m#" ... "#m` for a multiline string.
+Strings are enclosed by `" ... "` for a single line string or by `m%" ... "%m` for a multiline string.
 They can be concatenated with the operator `++`.
 Strings must be UTF-8 valid.
 
-The string interpolation syntax is `"#{ < expression that evaluates to a string > }"`.
+The string interpolation syntax is `"%{ < expression that evaluates to a string > }"`.
 
 Examples:
 ```
 > "Hello, World!"
 "Hello, World!"
 
-> m#"Well, if this isn't a multiline string?
-Yes it is, indeed it is"#m
+> m%"Well, if this isn't a multiline string?
+Yes it is, indeed it is"%m
 "Well, if this isn't a string?
 Yes it is, indeed it is"
 
 > "Hello" ++ "World"
 "HelloWorld"
 
-> let h = "Hello" in "#{h} World"
+> let h = "Hello" in "%{h} World"
 "Hello World"
 
-> let n = 5 in "The number #{n}."
+> let n = 5 in "The number %{n}."
 error: Type error
 
-> let n = 5 in "The number #{string.from_num n}."
+> let n = 5 in "The number %{string.from_num n}."
 "The number 5."
 ```
 
@@ -110,12 +110,12 @@ Multiline strings are useful to write indented lines. The indentation is strippe
 
 Example:
 ```
-> m#"
+> m%"
 This line has no identation.
   This line is indented.
     This line is even more indented.
 This line has no more identation.
-"#m
+"%m
 "This line has no indentation.
   This line is indented.
     This line is even more indented.
@@ -126,44 +126,44 @@ The only special sequence in a multiline string is the string interpolation.
 
 Examples:
 ```
-> m#"Multiline\nString?"#m
+> m%"Multiline\nString?"%m
 "Multiline\nString?"
 
-> m#"Multiline#{"\n"}String"#m
+> m%"Multiline%{"\n"}String"%m
 "Multiline
 String"
 ```
 
-A multiline string can be introduced and closed by multiple `#` signs, as long as this amount is equal. If you want to use string interpolation, you must use the same amount of `#` as in the delimiters.
+A multiline string can be introduced and closed by multiple `%` signs, as long as this amount is equal. If you want to use string interpolation, you must use the same amount of `%` as in the delimiters.
 
 Examples:
 ```
-> m##"Hello World"##m
+> m%%"Hello World"%%m
 "Hello World"
 
-> m#####"Hello World"#####m
+> m%%%%%"Hello World"%%%%%m
 "Hello World"
 
-> let w = "World" in m##"Hello #{w}"##m
-"Hello #{w}"
+> let w = "World" in m%%"Hello %{w}"%%m
+"Hello %{w}"
 
-> let w = "World" in m##"Hello ##{w}"##m
+> let w = "World" in m%%"Hello %%{w}"%%m
 "Hello World"
 ```
 
 Multiline strings are "indentation-aware". This means that one could use an indented string interpolation and the indentation would behave as expected:
 ```
-> let log = m#"
+> let log = m%"
 if log:
   print("log:", s)
-"#m in m#"
+"%m in m%"
 def concat(str_list, log=false):
   res = []
   for s in str_list:
-    #{log}
+    %{log}
     res.append(s)
   return res
-"#m
+"%m
 "def concat(str_list, log=false):
   res = []
   for s in str_list:
@@ -259,10 +259,10 @@ It is possible to write records of records via the *piecewise syntax*, where we
 
 When fields are enclosed with double quotes (`"`), you can use string interpolation to create or access fields:
 ```
-> let k = "a" in { "#{k}" = 1 }
+> let k = "a" in { "%{k}" = 1 }
 { a = 1 }
 
-> let k = "a" in { a = 1 }."#{k}"
+> let k = "a" in { a = 1 }."%{k}"
 1
 ```
 
@@ -416,12 +416,12 @@ Examples:
 > 5 | doc "The number five"
 5
 
-> true | Bool | doc m#"
+> true | Bool | doc m%"
     If something is true,
     it is based on facts rather than being invented or imagined,
     and is accurate and reliable.
     (Collins dictionary)
-    "#m
+    "%m
 true
 ```
 
diff --git a/doc/manual/typing.md b/doc/manual/typing.md
index 23615db1..d6409604 100644
--- a/doc/manual/typing.md
+++ b/doc/manual/typing.md
@@ -15,9 +15,9 @@ By default, Nickel code is dynamically typed. For example:
   version = "0.1.1",
   fullname =
     if builtin.is_num version then
-      "hello-v#{string.from_num version}"
+      "hello-v%{string.from_num version}"
     else
-      "hello-#{version}",
+      "hello-%{version}",
 }
 ```
 
@@ -31,9 +31,9 @@ example:
   version = "0.1.1",
   fullname =
     if builtin.is_num version then
-      "hello-v#{string.from_num version}"
+      "hello-v%{string.from_num version}"
     else
-      "hello-#{version + 1}",
+      "hello-%{version + 1}",
 }
 ```
 
@@ -47,7 +47,7 @@ error: Type error
 3 │   version = "0.1.1",
   │             ------- evaluated to this
   ·
-8 │       "hello-#{version + 1}",
+8 │       "hello-%{version + 1}",
   │                ^^^^^^^ This expression has type Str, but Num was expected
   │
   = +, 1st argument
@@ -212,7 +212,7 @@ The following type constructors are available:
   {
     incr : Num -> Num = fun x => x + 1,
     mkPath : Str -> Str -> Str -> Str = fun basepath filename ext =>
-      "#{basepath}/#{filename}.#{ext}",
+      "%{basepath}/%{filename}.%{ext}",
   }
   ```
 
diff --git a/examples/config-gcc/config-gcc.ncl b/examples/config-gcc/config-gcc.ncl
index 8628b5d9..b770094e 100644
--- a/examples/config-gcc/config-gcc.ncl
+++ b/examples/config-gcc/config-gcc.ncl
@@ -9,14 +9,14 @@ let GccFlag =
       list.any (fun x => x == string.substring 0 1 value) available then
       value
     else
-      contract.blame_with "unknown flag #{value}" label
+      contract.blame_with "unknown flag %{value}" label
   else if builtin.is_record value then
     if record.has_field "flag" value && record.has_field "arg" value then
       if list.any (fun x => x == value.flag) available then
         //Normalize the tag to a string
         value.flag ++ value.arg
       else
-        contract.blame_with "unknown flag #{value.flag}" label
+        contract.blame_with "unknown flag %{value.flag}" label
     else
       contract.blame_with
         "bad record structure: missing field `flag` or `arg`"
@@ -25,7 +25,7 @@ let GccFlag =
     contract.blame_with "expected record or string" label in
 
 let Path =
-  let pattern = m#"^(.+)/([^/]+)$"#m in
+  let pattern = m%"^(.+)/([^/]+)$"%m in
   fun label value =>
     if builtin.is_str value then
       if string.is_match pattern value then
@@ -37,7 +37,7 @@ let Path =
 
 let SharedObjectFile = fun label value =>
   if builtin.is_str value then
-    if string.is_match m#"\.so$"#m value then
+    if string.is_match m%"\.so$"%m value then
       value
     else
       contract.blame_with "not an .so file" label
diff --git a/examples/record-contract/record-contract.ncl b/examples/record-contract/record-contract.ncl
index abcaae44..3da17f63 100644
--- a/examples/record-contract/record-contract.ncl
+++ b/examples/record-contract/record-contract.ncl
@@ -69,7 +69,7 @@ let metadata_ = {
           containers = [
             {
               name = name_,
-              image = "k8s.gcr.io/#{name_}:v3",
+              image = "k8s.gcr.io/%{name_}:v3",
               ports = [
                 {
                   name = "http-server",
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index e597e4b3..905f44d4 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -487,12 +487,12 @@ StrChunks: RichTerm = {
 
 StringStart : StringKind = {
     "\"" => StringKind::Standard,
-    "m#\"" => StringKind::Multiline,
+    "m%\"" => StringKind::Multiline,
 };
 
 StringEnd : StringKind = {
     "\"" => StringKind::Standard,
-    "\"#m" => StringKind::Multiline,
+    "\"%m" => StringKind::Multiline,
 };
 
 ChunkLiteral : String =
@@ -507,9 +507,9 @@ ChunkLiteral : String =
         })
     };
 
-ChunkExpr: StrChunk<RichTerm> = HashBrace <t: WithPos<Term>> "}" => StrChunk::Expr(t, 0);
+ChunkExpr: StrChunk<RichTerm> = Interpolation <t: WithPos<Term>> "}" => StrChunk::Expr(t, 0);
 
-HashBrace = { "#{", "multstr #{" };
+Interpolation = { "%{", "multstr %{" };
 
 StaticString: String = StringStart <s: ChunkLiteral?> StringEnd => s.unwrap_or_default();
 
@@ -814,8 +814,8 @@ extern {
         "&" => Token::Normal(NormalToken::Ampersand),
         "." => Token::Normal(NormalToken::Dot),
         "$[" => Token::Normal(NormalToken::DollarBracket),
-        "#{" => Token::Str(StringToken::HashBrace),
-        "multstr #{" => Token::MultiStr(MultiStringToken::Interpolation),
+        "%{" => Token::Str(StringToken::Interpolation),
+        "multstr %{" => Token::MultiStr(MultiStringToken::Interpolation),
         "-$" => Token::Normal(NormalToken::MinusDollar),
 
         "+" => Token::Normal(NormalToken::Plus),
@@ -837,12 +837,11 @@ extern {
         "|>" => Token::Normal(NormalToken::RightPipe),
         "->" => Token::Normal(NormalToken::SimpleArrow),
         "=>" => Token::Normal(NormalToken::DoubleArrow),
-        "#" => Token::Normal(NormalToken::Hash),
         "`" => Token::Normal(NormalToken::Backtick),
         "_" => Token::Normal(NormalToken::Underscore),
         "\"" => Token::Normal(NormalToken::DoubleQuote),
-        "\"#m" => Token::MultiStr(MultiStringToken::End),
-        "m#\"" => Token::Normal(NormalToken::MultiStringStart(<usize>)),
+        "\"%m" => Token::MultiStr(MultiStringToken::End),
+        "m%\"" => Token::Normal(NormalToken::MultiStringStart(<usize>)),
 
         "Num" => Token::Normal(NormalToken::Num),
         "Dyn" => Token::Normal(NormalToken::Dyn),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index ac237c61..8543b7e2 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -140,13 +140,11 @@ pub enum NormalToken<'input> {
     SimpleArrow,
     #[token("=>")]
     DoubleArrow,
-    #[token("#")]
-    Hash,
     #[token("`")]
     Backtick,
     #[token("_")]
     Underscore,
-    #[regex("m(#+)\"", |lex| lex.slice().len())]
+    #[regex("m(%+)\"", |lex| lex.slice().len())]
     MultiStringStart(usize),
 
     #[token("%tag%")]
@@ -296,15 +294,15 @@ pub enum StringToken<'input> {
     #[error]
     Error,
 
-    #[regex("[^\"#\\\\]+")]
-    // Has lower matching priority than `HashBrace` according to Logos' rules.
-    #[token("#")]
+    #[regex("[^\"%\\\\]+")]
+    // Has lower matching priority than `Interpolation` according to Logos' rules.
+    #[token("%")]
     Literal(&'input str),
 
     #[token("\"")]
     DoubleQuote,
-    #[token("#{")]
-    HashBrace,
+    #[token("%{")]
+    Interpolation,
     #[regex("\\\\.", |lex| lex.slice().chars().nth(1))]
     EscapedChar(char),
     // Repetition range `{2}` was not supported at the time of writing this regex.
@@ -318,28 +316,28 @@ pub enum MultiStringToken<'input> {
     #[error]
     Error,
 
-    #[regex("[^\"#]+")]
+    #[regex("[^\"%]+")]
     // A token that starts as a multiline end delimiter or an interpolation sequence but is not
     // one.  These ones should have lowest matching priority according to Logos' rules, and
     // CandidateEnd and CandidateInterpolation should be matched first.
     #[token("\"")]
-    #[regex("#+")]
+    #[regex("%+")]
     Literal(&'input str),
 
     /// A candidate end. A multiline string starting delimiter `MultiStringStart` can have a
-    /// variable number of `#` character, so the lexer matches candidate end delimiter, compare the
+    /// variable number of `%` character, so the lexer matches candidate end delimiter, compare the
     /// number of characters, and either emit the `End` token above, or turn the `CandidateEnd` to a
     /// `FalseEnd` otherwise
-    #[regex("\"#+m")]
+    #[regex("\"%+m")]
     CandidateEnd(&'input str),
     /// Same as `CandidateEnd`, but for interpolation
-    #[regex("#+\\{")]
+    #[regex("%+\\{")]
     CandidateInterpolation(&'input str),
     /// Unfortunate consequence of Logos' [issue #200](https://github.com/maciejhirsz/logos/issues/200).
     /// The other rules should be sufficient to match this as a double quote followed by a
     /// `CandidateInterpolation`, but if we omit this token, the lexer can fail unexpectedly on
     /// valid inputs because of #200.
-    #[regex("\"#+\\{")]
+    #[regex("\"%+\\{")]
     QuotesCandidateInterpolation(&'input str),
     /// Token emitted by the modal lexer for the parser once it has decided that a `CandidateEnd` is
     /// an actual end token.
@@ -579,13 +577,13 @@ impl<'input> Iterator for Lexer<'input> {
             Some(MultiStr(MultiStringToken::Interpolation)) => self.enter_normal(),
             // If we encouter a `QuotesCandidateInterpolation` token with the right number of
             // characters, we need to split it into two tokens:
-            // - a literal starting by a `"` followed by between 0 and k hashes `#`
+            // - a literal starting by a `"` followed by between 0 and k `%`
             // - an interpolation token
             // The interpolation token is put in the buffer such that it will be returned next
             // time.
             //
-            // For example, in `m##""###{exp}"##m`, the `"###{` is a `QuotesCandidateInterpolation`
-            // which is split as a `"#` literal followed by an interpolation token.
+            // For example, in `m%%""%%%{exp}"%%m`, the `"%%%{` is a `QuotesCandidateInterpolation`
+            // which is split as a `"%` literal followed by an interpolation token.
             Some(MultiStr(MultiStringToken::QuotesCandidateInterpolation(s)))
                 if s.len() >= self.count =>
             {
@@ -609,7 +607,7 @@ impl<'input> Iterator for Lexer<'input> {
             | Some(MultiStr(MultiStringToken::QuotesCandidateInterpolation(s))) => {
                 token = Some(MultiStr(MultiStringToken::Literal(s)))
             }
-            Some(Str(StringToken::HashBrace)) => self.enter_normal(),
+            Some(Str(StringToken::Interpolation)) => self.enter_normal(),
             // Convert escape sequences to the corresponding character.
             Some(Str(StringToken::EscapedChar(c))) => {
                 if let Some(esc) = escape_char(*c) {
@@ -663,7 +661,7 @@ fn escape_char(chr: char) -> Option<char> {
         '\'' => Some('\''),
         '"' => Some('"'),
         '\\' => Some('\\'),
-        '#' => Some('#'),
+        '%' => Some('%'),
         'n' => Some('\n'),
         'r' => Some('\r'),
         't' => Some('\t'),
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index 196c39ac..b8d1b885 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -17,7 +17,7 @@ use crate::{
 };
 
 /// Distinguish between the standard string separators `"`/`"` and the multi-line string separators
-/// `m#"`/`"#m` in the parser.
+/// `m%"`/`"%m` in the parser.
 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
 pub enum StringKind {
     Standard,
@@ -36,7 +36,7 @@ pub enum SwitchCase {
 pub enum FieldPathElem {
     /// A static field declaration: `{ foo = .. }`
     Ident(Ident),
-    /// A quoted field declaration: `{ "#{protocol}" = .. }`
+    /// A quoted field declaration: `{ "%{protocol}" = .. }`
     ///
     /// In practice, the argument must always be `StrChunks`, but since we also need to keep track
     /// of the associated span it's handier to just use a `RichTerm`.
@@ -48,7 +48,7 @@ pub type FieldPath = Vec<FieldPathElem>;
 /// A string chunk literal atom, being either a string or a single char.
 ///
 /// Because of the way the lexer handles escaping and interpolation, a contiguous static string
-/// `"Some \\ \#{escaped} string"` will be lexed as a sequence of such atoms.
+/// `"Some \\ \%{escaped} string"` will be lexed as a sequence of such atoms.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum ChunkLiteralPart<'input> {
     Str(&'input str),
@@ -212,7 +212,7 @@ where
             // fields with special characters are also parsed as an `Expr(e)`:
             //
             // ```
-            // let x = "dynamic" in {"I#am.static" = false, "#{x}" = true}
+            // let x = "dynamic" in {"I#am.static" = false, "%{x}" = true}
             // ```
             //
             // Here, both fields are parsed as `Expr(e)`, but the first field is actually a static
@@ -377,11 +377,11 @@ pub fn min_indent(chunks: &[StrChunk<RichTerm>]) -> usize {
 ///
 /// ```text
 /// let x = "I\nam\nindented" in
-/// m#"
+/// m%"
 ///   baseline
 ///     ${x}
 ///   end
-/// "#m
+/// "%m
 /// ```
 ///
 /// gives
@@ -398,11 +398,11 @@ pub fn min_indent(chunks: &[StrChunk<RichTerm>]) -> usize {
 ///
 /// ```text
 /// let x = "I\nam\nnot" in
-/// m#"
+/// m%"
 ///   baseline
 ///     ${x} sth
 ///   end
-/// "#m
+/// "%m
 /// ```
 ///
 /// gives
@@ -427,12 +427,12 @@ pub fn strip_indent(mut chunks: Vec<StrChunk<RichTerm>>) -> Vec<StrChunk<RichTer
     // When processing a line with an indented interpolated expression, as in:
     //
     // ```
-    // m#"
+    // m%"
     //  some
     //    ${x} ${y}
     //    ${x}
     //  string
-    // "#m
+    // "%m
     // ```
     //
     // We don't know at the time we process the expression `${x}` if it wil have to be re-indented,
diff --git a/src/serialize.rs b/src/serialize.rs
index 48fe291b..5c4ea598 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -332,7 +332,7 @@ mod tests {
         assert_json_eq!("null", null);
 
         assert_json_eq!("if true then false else true", false);
-        assert_json_eq!(r##""Hello, #{"world"}!""##, "Hello, world!");
+        assert_json_eq!(r##""Hello, %{"world"}!""##, "Hello, world!");
         assert_json_eq!("`foo", "foo");
     }
 
@@ -341,7 +341,7 @@ mod tests {
         assert_json_eq!("[]", json!([]));
         assert_json_eq!("[null, (1+1), (2+2), (3+3)]", json!([null, 2, 4, 6]));
         assert_json_eq!(
-            r##"[`a, ("b" ++ "c"), "d#{"e"}f", "g"]"##,
+            r##"[`a, ("b" ++ "c"), "d%{"e"}f", "g"]"##,
             json!(["a", "bc", "def", "g"])
         );
         assert_json_eq!(
diff --git a/stdlib/builtin.ncl b/stdlib/builtin.ncl
index 4b4d9d33..34a7678f 100644
--- a/stdlib/builtin.ncl
+++ b/stdlib/builtin.ncl
@@ -1,7 +1,7 @@
 {
   builtin = {
     is_num : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a number.
 
       For example:
@@ -11,11 +11,11 @@
         is_num "Hello, World!" =>
           false
       ```
-      "#m
+      "%m
     = fun x => %is_num% x,
 
     is_bool : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a boolean.
 
       For example:
@@ -25,11 +25,11 @@
         is_bool 42 =>
           false
       ```
-      "#m
+      "%m
     = fun x => %is_bool% x,
 
     is_str : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a string.
 
       For example:
@@ -39,11 +39,11 @@
         is_bool "Hello, World!" =>
           true
       ```
-      "#m
+      "%m
     = fun x => %is_str% x,
 
     is_fun : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a function.
 
       For example
@@ -53,11 +53,11 @@
         is_fun 42 =>
           false
       ```
-      "#m
+      "%m
     = fun x => %is_fun% x,
 
     is_list : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a list.
 
       For example
@@ -67,11 +67,11 @@
         is_list 42 =>
           false
       ```
-      "#m
+      "%m
     = fun x => %is_list% x,
 
     is_record : Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given value is a record.
 
       For example
@@ -81,7 +81,7 @@
         is_record { hello = "Hello", world = "World" } =>
           true
       ```
-      "#m
+      "%m
     = fun x => %is_record% x,
 
     typeof : Dyn -> [|
@@ -92,7 +92,7 @@
       TypeList,
       TypeRecord,
       Other |]
-    | doc m#"
+    | doc m%"
       Results in a value representing the type of the typed value.
 
       For example:
@@ -102,7 +102,7 @@
         typeof (fun x => x) =>
           `TypeFun
       ```
-      "#m
+      "%m
     = fun x =>
       if %is_num% x then `TypeNum
       else if %is_bool% x then `TypeBool
@@ -113,7 +113,7 @@
       else `Other,
 
     seq : forall a. Dyn -> a -> a
-    | doc m#"
+    | doc m%"
       `seq x y` forces the evaluation of `x`, before resulting in `y`.
 
       For example:
@@ -125,11 +125,11 @@
         seq { tooFar = 42 / 0 } 37 =>
           37
       ```
-      "#m
+      "%m
     = fun x y => %seq% x y,
 
     deep_seq : forall a. Dyn -> a -> a
-    | doc m#"
+    | doc m%"
       `deep_seq x y` forces a deep evaluation `x`, before resulting in `y`.
 
       For example:
@@ -141,11 +141,11 @@
         deep_seq { tooFar = 42 / 0 } 37 =>
           error
       ```
-      "#m
+      "%m
     = fun x y => %deep_seq% x y,
 
     hash : [| Md5, Sha1, Sha256, Sha512 |] -> Str -> Str
-    | doc m#"
+    | doc m%"
       Hashes the given string provided the desired hash algorithm.
 
       For example:
@@ -153,11 +153,11 @@
         hash `Md5 "hunter2" =>
           "2ab96390c7dbe3439de74d0c9b0b1767"
       ```
-      "#m
+      "%m
     = fun type s => %hash% type s,
 
     serialize : [| Json, Toml, Yaml |] -> Dyn -> Str
-    | doc m#"
+    | doc m%"
       Serializes the given value to the desired representation.
 
       For example:
@@ -168,11 +168,11 @@
             "world": "World"
           }"
       ```
-      "#m
+      "%m
     = fun format x => %serialize% format (%deep_seq% x x),
 
     deserialize : [| Json, Toml, Yaml |] -> Str -> Dyn
-    | doc m#"
+    | doc m%"
       Deserializes the given string to a nickel value given the encoding of the string.
 
       For example:
@@ -180,7 +180,7 @@
         deserialize `Json "{ \"hello\": \"Hello\", \"world\": \"World\" }"
           { hello = "Hello", world = "World" }
       ```
-      "#m
+      "%m
     = fun format x => %deserialize% format x,
   }
 }
diff --git a/stdlib/contract.ncl b/stdlib/contract.ncl
index 7ba1758f..381710c5 100644
--- a/stdlib/contract.ncl
+++ b/stdlib/contract.ncl
@@ -47,11 +47,11 @@
 
   "$record_extend" = fun field contr cont acc l t =>
       if %has_field% field t then
-          let acc = acc$[field = %assume% contr (%go_field% field l) (t."#{field}")] in
+          let acc = acc$[field = %assume% contr (%go_field% field l) (t."%{field}")] in
           let t = t -$ field in
           cont acc l t
       else
-          %blame% (%tag% "missing field `#{field}`" l),
+          %blame% (%tag% "missing field `%{field}`" l),
 
   "$forall_tail" = fun sy pol acc l t =>
       let magic_fld = "_%wrapped" in
@@ -60,10 +60,10 @@
               let rest = (t -$ magic_fld) in
               if rest == {} then
                   let fail = %blame% (%tag% "polymorphic tail mismatch" l) in
-                  let inner = %unwrap% sy (t."#{magic_fld}") fail in
+                  let inner = %unwrap% sy (t."%{magic_fld}") fail in
                   acc & inner
               else
-                  %blame% (%tag% "extra field `#{%head% (%fields% rest)}`" l)
+                  %blame% (%tag% "extra field `%{%head% (%fields% rest)}`" l)
           else
               %blame% (%tag% "missing polymorphic part" l)
       else
@@ -73,11 +73,11 @@
 
   "$empty_tail" = fun acc l t =>
       if t == {} then acc
-      else %blame% (%tag% "extra field `#{%head% (%fields% t)}`" l),
+      else %blame% (%tag% "extra field `%{%head% (%fields% t)}`" l),
 
   contract = {
     blame
-      | doc m#"
+      | doc m%"
         Raise blame for a given label.
 
         Type: `forall a. Lbl -> a`
@@ -93,11 +93,11 @@
           if value == 0 then value
           else contracts.blame label
         ```
-        "#m
+        "%m
       = fun l => %blame% l,
 
     blame_with
-      | doc m#"
+      | doc m%"
         Raise blame with respect to a given label and a custom error message.
 
         Type: `forall a. Str -> Lbl -> a`
@@ -114,11 +114,11 @@
           else contracts.blame_with "Not zero" label in
         0 | IsZero
         ```
-        "#m
+        "%m
       = fun msg l => %blame% (%tag% msg l),
 
     from_predicate
-      | doc m#"
+      | doc m%"
         Generate a contract from a boolean predicate.
 
         Type: `(Dyn -> Bool) -> (Lbl -> Dyn -> Dyn)`
@@ -129,11 +129,11 @@
         let IsZero = contracts.from_predicate (fun x => x == 0) in
         0 | IsZero
         ```
-        "#m
+        "%m
       = fun pred l v => if pred v then v else %blame% l,
 
     tag
-      | doc m#"
+      | doc m%"
         Attach a tag, or a custom error message, to a label. If a tag was
         previously set, it is erased.
 
@@ -152,11 +152,11 @@
             value in
         5 | Contract
         ```
-        "#m
+        "%m
       = fun msg l => %tag% msg l,
 
     apply
-      | doc m#"
+      | doc m%"
         Apply a contract to a label and a value.
 
         Type: `Contract -> Lbl -> Dyn -> Dyn`
@@ -177,7 +177,7 @@
         let Contract = Nullable {foo | Num} in
         ({foo = 1} | Contract)
         ```
-        "#m
+        "%m
       = fun contract label value => %assume% contract label value,
   },
 }
diff --git a/stdlib/list.ncl b/stdlib/list.ncl
index 4852f789..77ab2fd3 100644
--- a/stdlib/list.ncl
+++ b/stdlib/list.ncl
@@ -1,7 +1,7 @@
 {
   list = {
     NonEmpty
-      | doc m#"
+      | doc m%"
         Contract to ensure the given list is not empty.
 
         For example:
@@ -11,7 +11,7 @@
           ([ 1 ] | NonEmpty) =>
             [ 1 ]
         ```
-        "#m
+        "%m
       = fun label value =>
        if %is_list% value then
          if %length% value != 0 then
@@ -22,7 +22,7 @@
          %blame% (%tag% "not a list" label),
 
     head : forall a. List a -> a
-      | doc m#"
+      | doc m%"
         Results in the head (first element) of the given list.
 
         For example:
@@ -30,11 +30,11 @@
           head [ "this is the head", "this is not" ] =>
             "this is the head"
         ```
-        "#m
+        "%m
       = fun l => %head% l,
 
     tail : forall a. List a -> List a
-      | doc m#"
+      | doc m%"
         Results in the tail (everything but the first element) of the given list.
 
         For example:
@@ -42,11 +42,11 @@
           tail [ 1, 2, 3 ] =>
             [ 2, 3 ]
         ```
-        "#m
+        "%m
       = fun l => %tail% l,
 
     length : forall a. List a -> Num
-      | doc m#"
+      | doc m%"
         Results in a number representing the length of the given list.
 
         For example:
@@ -54,11 +54,11 @@
           length [ "Hello,", " World!" ] =>
             2
         ```
-        "#m
+        "%m
       = fun l => %length% l,
 
     map : forall a b. (a -> b) -> List a -> List b
-      | doc m#"
+      | doc m%"
         `map f [x1, x2, ..., xn]` applies function `f` to every element in the list,
         resulting in `[f x1, f x2, ... f xn]`
 
@@ -67,11 +67,11 @@
           map (fun x => x + 1) [ 1, 2, 3 ] =>
             [ 2, 3, 4 ]
         ```
-        "#m
+        "%m
       = fun f l => %map% l f,
 
     elem_at : forall a. Num -> List a -> a
-      | doc m#"
+      | doc m%"
         Retrieves the n'th element from a list (0-indexed).
 
         For example:
@@ -79,11 +79,11 @@
           elem_at 3 [ "zero" "one" "two" "three" "four" ] =>
             "three"
         ```
-        "#m
+        "%m
       = fun n l => %elem_at% l n,
 
     concat : forall a. List a -> List a -> List a
-      | doc m#"
+      | doc m%"
         Concatenates two lists such that the second list is appended to the first.
 
         For example:
@@ -91,11 +91,11 @@
           concat [ 1, 2, 3 ] [ 4, 5, 6 ] =>
             [ 1, 2, 3, 4, 5, 6 ]
         ```
-        "#m
+        "%m
       = fun l1 l2 => l1 @ l2,
 
     foldl : forall a b. (a -> b -> a) -> a -> List b -> a
-      | doc m#"
+      | doc m%"
         Fold a function over a list.
         `foldl f init [x1, x2, ..., xn]` results in `f (... (f (f init x1) x2) ...) xn`.
 
@@ -107,7 +107,7 @@
             (((0 + 1) + 2) 3) =>
             6
         ```
-        "#m
+        "%m
       = fun f acc l =>
         if %length% l == 0 then
           acc
@@ -116,7 +116,7 @@
           %seq% newAcc (foldl f newAcc (%tail% l)),
 
     fold : forall a b. (a -> b -> b) -> b -> List a -> b
-      | doc m#"
+      | doc m%"
         Fold a function over a list.
         `fold f init [x1, x2, ..., xn]` results in `f x1 (f x2 (... (f xn init) ...))`.
 
@@ -126,7 +126,7 @@
             ((([] @ [3]) @ [2]) @ [1]) =>
             [ 3, 2, 1 ]
         ```
-        "#m
+        "%m
       = fun f fst l =>
         if %length% l == 0 then
           fst
@@ -134,7 +134,7 @@
           f (%head% l) (fold f fst (%tail% l)),
 
     cons : forall a. a -> List a -> List a
-      | doc m#"
+      | doc m%"
         Construct a list given the head and tail of the list.
 
         For example:
@@ -142,11 +142,11 @@
           cons 1 [ 2, 3 ] =>
             [ 1, 2, 3 ]
         ```
-        "#m
+        "%m
       = fun x l => [x] @ l,
 
     reverse : forall a. List a -> List a
-      | doc m#"
+      | doc m%"
         Reverses the order of a list.
 
         For example:
@@ -154,11 +154,11 @@
           reverse [ 1, 2, 3 ] =>
             [ 3, 2, 1 ]
         ```
-        "#m
+        "%m
       = fun l => foldl (fun acc e => [e] @ acc) [] l,
 
     filter : forall a. (a -> Bool) -> List a -> List a
-      | doc m#"
+      | doc m%"
         `filter f xs` keeps all elements from `xs` given that satisfy `f`.
 
         For example:
@@ -166,11 +166,11 @@
           filter (fun x => x <= 3) [ 4, 3, 2, 5, 1 ] =>
             [ 3, 2, 1 ]
         ```
-        "#m
+        "%m
       = fun pred l => foldl (fun acc x => if pred x then acc @ [x] else acc) [] l,
 
     flatten : forall a. List (List a) -> List a
-      | doc m#"
+      | doc m%"
         Flatten a list of lists to a single list, essentially concatenating all lists in the original list.
 
         For example:
@@ -178,11 +178,11 @@
           flatten [[1, 2], [3, 4]] =>
             [1, 2, 3, 4]
         ```
-        "#m
+        "%m
       = fun l => fold (fun l acc => l @ acc) [] l,
 
     all : forall a. (a -> Bool) -> List a -> Bool
-      | doc m#"
+      | doc m%"
         Results in true if all elements in the given list satisfy the predicate, false otherwise.
 
         For example:
@@ -192,11 +192,11 @@
           all (fun x => x < 3) [ 1, 2 3 ] =>
             false
         ```
-        "#m
+        "%m
       = fun pred l => fold (fun x acc => if pred x then acc else false) true l,
 
     any : forall a. (a -> Bool) -> List a -> Bool
-      | doc m#"
+      | doc m%"
         Results in false if no elements in the given list satisfy the predicate, true otherwise.
 
         For example:
@@ -206,11 +206,11 @@
           any (fun x => x < 3) [ 5, 6, 7, 8 ] =>
             false
         ```
-        "#m
+        "%m
       = fun pred l => fold (fun x acc => if pred x then true else acc) false l,
 
     elem : Dyn -> List -> Bool
-      | doc m#"
+      | doc m%"
         Results in true if the given element is a member of the list, false otherwise.
 
         For example:
@@ -218,11 +218,11 @@
           elem 3 [ 1, 2, 3, 4, 5 ] =>
             true
         ```
-        "#m
+        "%m
       = fun elt => any (fun x => x == elt),
 
     partition : forall a. (a -> Bool) -> List a -> {right: List a, wrong: List a}
-      | doc m#"
+      | doc m%"
         Partitions the given list in two new lists: those containing the elements that satisfy the predicate, and those
         that do not.
 
@@ -231,7 +231,7 @@
           partition (fun x => x < 5) [ 2, 4, 5, 3, 7, 8, 6 ] =>
             { right = [ 3, 4, 2 ], wrong = [ 6, 8, 7, 5 ] }
         ```
-        "#m
+        "%m
       = fun pred l =>
         let aux = fun acc x =>
           if (pred x) then
@@ -242,7 +242,7 @@
         foldl aux {right = [], wrong = []} l,
 
     generate : forall a. (Num -> a) -> Num -> List a
-      | doc m#"
+      | doc m%"
         `generate f n` produces a list of length `n` by applying `f` on increasing numbers:
          `[ f 0, f 1, ..., f (n - 1) ]`.
 
@@ -251,11 +251,11 @@
           generate functions.id 4 =>
             [ 0, 1, 2, 3 ]
         ```
-        "#m
+        "%m
       = fun f n => %generate% n f,
 
     sort : forall a. (a -> a -> [| Less, Equal, Greater |]) -> List a -> List a
-      | doc m#"
+      | doc m%"
         Sorts the given lists based on the provided comparison operator.
 
         For example:
@@ -263,7 +263,7 @@
           sort (fun x y => if x < y then `Less else if (x == y) then `Equal else `Greater) [ 4, 5, 1, 2 ] =>
             [ 1, 2, 4, 5 ]
         ```
-        "#m
+        "%m
       = fun cmp l =>
         let first = %head% l in
         let parts = partition (fun x => (cmp x first == `Less)) (%tail% l) in
diff --git a/stdlib/num.ncl b/stdlib/num.ncl
index db712b3b..ddd71612 100644
--- a/stdlib/num.ncl
+++ b/stdlib/num.ncl
@@ -1,7 +1,7 @@
 {
   num = {
     Int
-    | doc m#"
+    | doc m%"
       Contract to enforce a number is an integer.
 
       For example:
@@ -11,7 +11,7 @@
         (42 | Int) =>
           42
       ```
-      "#m
+      "%m
     = fun label value =>
       if %is_num% value then
         if value % 1 == 0 then
@@ -22,7 +22,7 @@
         %blame% (%tag% "not a number" label),
 
     Nat
-    | doc m#"
+    | doc m%"
       Contract to enforce a number is a natural number (including 0).
 
       For example:
@@ -34,7 +34,7 @@
         (-4 | Nat) =>
           error
       ```
-      "#m
+      "%m
     = fun label value =>
       if %is_num% value then
         if value % 1 == 0 && value >= 0 then
@@ -45,7 +45,7 @@
         %blame% (%tag% "not a number" label),
 
     PosNat
-    | doc m#"
+    | doc m%"
       Contract to enforce a number is a positive natural number.
 
       For example:
@@ -57,7 +57,7 @@
         (-4 | PosNat) =>
           error
       ```
-      "#m
+      "%m
     = fun label value =>
       if %is_num% value then
         if value % 1 == 0 && value > 0 then
@@ -68,7 +68,7 @@
         %blame% (%tag% "not a number" label),
 
     NonZero
-    | doc m#"
+    | doc m%"
       Contract to enforce a number is anything but zero.
 
       For example:
@@ -78,7 +78,7 @@
         (0.0 | NonZero) =>
           error
       ```
-      "#m
+      "%m
     = fun label value =>
       if %is_num% value then
         if value != 0 then
@@ -89,7 +89,7 @@
         %blame% (%tag% "not a number" label),
 
     is_int : Num -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the given number is an integer.
 
       For example:
@@ -99,11 +99,11 @@
         is_int 1.5 =>
           false
       ```
-      "#m
+      "%m
     = fun x => %is_num% x && (x % 1 == 0),
 
     min : Num -> Num -> Num
-    | doc m#"
+    | doc m%"
       Results in the lowest of the given two numbers.
 
       For example:
@@ -111,11 +111,11 @@
         min (-1337) 42 =>
           -1337
       ```
-      "#m
+      "%m
     = fun x y => if x <= y then x else y,
 
     max : Num -> Num -> Num
-    | doc m#"
+    | doc m%"
       Results in the highest of the given two numbers.
 
       For example:
@@ -123,11 +123,11 @@
         max (-1337) 42 =>
           42
       ```
-      "#m
+      "%m
     = fun x y => if x >= y then x else y,
 
     floor : Num -> Num
-    | doc m#"
+    | doc m%"
       Rounds the number down to the next integer.
 
       For example:
@@ -135,14 +135,14 @@
         floor 42.5 =>
           42
       ```
-      "#m
+      "%m
     = fun x =>
       if x >= 0
       then x - (x % 1)
       else x - 1 - (x % 1),
 
     abs : Num -> Num
-    | doc m#"
+    | doc m%"
       Results in the absolute value of the given number.
 
       For example:
@@ -152,11 +152,11 @@
         abs 42 =>
           42
       ```
-      "#m
+      "%m
     = fun x => if x < 0 then -x else x,
 
     fract : Num -> Num
-    | doc m#"
+    | doc m%"
       Results in the fractional part of the given number.
 
       For example:
@@ -166,11 +166,11 @@
         fract 42 =>
           0
       ```
-      "#m
+      "%m
     = fun x => x % 1,
 
     trunc : Num -> Num
-    | doc m#"
+    | doc m%"
       Truncates the given number.
 
       For example:
@@ -180,11 +180,11 @@
         trunc 42.5 =>
           42
       ```
-      "#m
+      "%m
     = fun x => x - (x % 1),
 
     pow : Num -> Num -> Num
-    | doc m#"
+    | doc m%"
       `pow x y` results in `x` to the power of `y`.
 
       For example:
@@ -192,7 +192,7 @@
         pow 2 8 =>
           256
       ```
-      "#m
+      "%m
     = fun x n => %pow% x n,
   }
 }
diff --git a/stdlib/record.ncl b/stdlib/record.ncl
index f5cf36a6..3fb48c55 100644
--- a/stdlib/record.ncl
+++ b/stdlib/record.ncl
@@ -1,7 +1,7 @@
 {
   record = {
     map : forall a b. (Str -> a -> b) -> {_: a} -> {_: b}
-    | doc m#"
+    | doc m%"
       Maps a function on every field of a record. The string argument of the function argument is the name of the
       field.
 
@@ -12,33 +12,33 @@
         map (fun s x => x + 1) { hello = 1, world = 2 } =>
           { hello = 2, world = 3 }
       ```
-      "#m
+      "%m
     = fun f r => %record_map% r f,
 
     fields | { ; Dyn} -> List Str
-    | doc m#"
+    | doc m%"
       Given a record, results in a list of the string representation of all fields in the record.
 
       ```nickel
         fields { one = 1, two = 2 } =>
           [ "one", "two" ]
       ```
-      "#m
+      "%m
     = fun r => %fields% r,
 
     values | { ; Dyn} -> List
-    | doc m#"
+    | doc m%"
       Given a record, results in a list containing all the values in that record.
 
       ```nickel
         values { one = 1, world = "world" }
           [ 1, "world" ]
       ```
-      "#m
+      "%m
     = fun r => %values% r,
 
     has_field : Str -> Dyn -> Bool
-    | doc m#"
+    | doc m%"
       Given the name of a field and a record, checks if the record contains the given field.
 
       ```nickel
@@ -46,7 +46,7 @@
           false
         has_field "one" { one = 1, two = 2 } =>
           true
-      "#m
+      "%m
     = fun field r => %has_field% field r,
   }
 }
diff --git a/stdlib/string.ncl b/stdlib/string.ncl
index 5f791c37..50109f1d 100644
--- a/stdlib/string.ncl
+++ b/stdlib/string.ncl
@@ -1,7 +1,7 @@
 {
   string = {
     BoolLiteral
-    | doc m#"
+    | doc m%"
       Contract to enforce the value is a string that represents a boolean literal. Additionally casts "True" to "true"
       and "False" to "false".
 
@@ -14,7 +14,7 @@
         (true | BoolLiteral) =>
           error
       ```
-      "#m
+      "%m
     = fun l s =>
       if %is_str% s then
         if s == "true" || s == "True" then
@@ -22,12 +22,12 @@
         else if s == "false" || s == "False" then
           "false"
         else
-          %blame% (%tag% "expected \"true\" or \"false\", got #{s}" l)
+          %blame% (%tag% "expected \"true\" or \"false\", got %{s}" l)
       else
         %blame% (%tag% "not a string" l),
 
     NumLiteral
-    | doc m#"
+    | doc m%"
       Contract to enforce the value is a string that represends a numerical value.
 
       For example:
@@ -39,8 +39,8 @@
         (42 | NumLiteral) =>
           error
       ```
-      "#m
-    = let pattern = m#"^[+-]?(\d+(\.\d*)?(e[+-]?\d+)?|\.\d+(e[+-]?\d+)?)$"#m in
+      "%m
+    = let pattern = m%"^[+-]?(\d+(\.\d*)?(e[+-]?\d+)?|\.\d+(e[+-]?\d+)?)$"%m in
       fun l s =>
         if %is_str% s then
           if %str_is_match% s pattern then
@@ -51,7 +51,7 @@
           %blame% (%tag% "not a string" l),
 
     CharLiteral
-    | doc m#"
+    | doc m%"
       Contract to enforce the value is a character literal (i.e. a string of length 1).
 
       For example:
@@ -65,7 +65,7 @@
         (1 | CharLiteral) =>
           error
       ```
-      "#m
+      "%m
     = fun l s =>
       if %is_str% s then
         if length s == 1 then
@@ -76,7 +76,7 @@
         %blame% (%tag% "not a string" l),
 
     Ident
-    | doc m#"
+    | doc m%"
       Contract to enforce the value is a valid Nickel identifier. Can be used in conjunction with the record field
       function (e.g. `has_field`).
 
@@ -86,9 +86,9 @@
           true
         records.has_field ("9" | Ident) { hello = "hi!" } =>
           error (invalid enum tag)
-      "#m
+      "%m
     = fun l s =>
-      let pattern = m#"_?[a-zA-Z][_a-zA-Z0-9]*"#m in
+      let pattern = m%"_?[a-zA-Z][_a-zA-Z0-9]*"%m in
       if %is_str% s then
         if %str_is_match% s pattern then
             s
@@ -98,7 +98,7 @@
         %blame% (%tag% "not a string" l),
 
     NonEmpty
-    | doc m#"
+    | doc m%"
       Contract to enforce the value is a non-empty string.
 
       For example:
@@ -110,7 +110,7 @@
         (42 | NonEmpty) =>
           error
       ```
-      "#m
+      "%m
     = fun l s =>
       if %is_str% s then
         if %str_length% s > 0 then
@@ -122,7 +122,7 @@
 
     // using a contract instead of type for now because of https://github.com/tweag/nickel/issues/226
     join | Str -> List Str -> Str
-    | doc m#"
+    | doc m%"
       Joins a list of strings given a seperator.
 
       For example:
@@ -130,7 +130,7 @@
         join ", " [ "Hello", "World!" ] =>
           "Hello, World!"
       ```
-      "#m
+      "%m
     = fun sep l =>
       if %length% l == 0 then
         ""
@@ -138,7 +138,7 @@
         %head% l ++ list.foldl (fun acc s => acc ++ sep ++ s) "" (%tail% l),
 
     split : Str -> Str -> List Str
-    | doc m#"
+    | doc m%"
       Splits a string based on a separator string. The separator string is not included in any string.
 
       For example:
@@ -148,11 +148,11 @@
       split "." "1,2,3" =>
         [ "1,2,3" ]
       ```
-      "#m
+      "%m
     = fun sep s => %str_split% s sep,
 
     trim : Str -> Str
-    | doc m#"
+    | doc m%"
       Trims whitespace from the start and end of the string.
 
       For example:
@@ -162,11 +162,11 @@
       trim "1   2   3   " =>
         "1   2   3"
       ```
-      "#m
+      "%m
     = fun s => %str_trim% s,
 
     chars : Str -> List Str
-    | doc m#"
+    | doc m%"
       Separates a string into its individual characters.
 
       For example:
@@ -174,11 +174,11 @@
         chars "Hello" =>
           [ "H", "e", "l", "l", "o" ]
       ```
-      "#m
+      "%m
     = fun s => %str_chars% s,
 
     code | CharLiteral -> Num
-    | doc m#"
+    | doc m%"
       Results in the ascii code of the given character.
 
       For example:
@@ -190,11 +190,11 @@
         code "å" =>
           error
       ```
-      "#m
+      "%m
     = fun s => %char_code% s,
 
     from_code | Num -> CharLiteral
-    | doc m#"
+    | doc m%"
       Results in the character for a given ascii code. Any number outside the ascii range results in an error.
 
       For example:
@@ -206,11 +206,11 @@
         from_code 128 =>
           error
       ```
-      "#m
+      "%m
     = fun s => %char_from_code% s,
 
     uppercase : Str -> Str
-    | doc m#"
+    | doc m%"
       Results in the uppercase version of the given character (including non-ascii characters) if it exists, the same
       character if not.
 
@@ -223,11 +223,11 @@
         uppercase "." =>
           "."
       ```
-      "#m
+      "%m
     = fun s => %str_uppercase% s,
 
     lowercase : Str -> Str
-    | doc m#"
+    | doc m%"
       Results in the lowercase version of the given character (including non-ascii characters) if it exists, the same
       character if not.
 
@@ -240,11 +240,11 @@
         lowercase "." =>
           "."
       ```
-      "#m
+      "%m
     = fun s => %str_lowercase% s,
 
     contains: Str -> Str -> Bool
-    | doc m#"
+    | doc m%"
       Checks if the first string is part of the second string.
 
       For example:
@@ -256,11 +256,11 @@
         contains "ghj" "abcdef" =>
           false
       ```
-      "#m
+      "%m
     = fun subs s => %str_contains% s subs,
 
     replace: Str -> Str -> Str -> Str
-    | doc m#"
+    | doc m%"
       `replace sub repl str` replaces every occurence of `sub` in `str` with `repl`.
 
       For example:
@@ -270,12 +270,12 @@
         replace "" "A" "abcdef" =>
           "AaAbAcAdAeAfA"
       ```
-      "#m
+      "%m
     = fun pattern replace s =>
        %str_replace% s pattern replace,
 
     replace_regex: Str -> Str -> Str -> Str
-    | doc m#"
+    | doc m%"
       `replace_regex regex repl str` replaces every match of `regex` in `str` with `repl`.
 
       For example:
@@ -285,12 +285,12 @@
         replace_regex "\\d+" "\"a\" is not" "This 37 is a number." =>
           "This \"a\" is not a number."
       ```
-      "#m
+      "%m
     = fun pattern replace s =>
        %str_replace_regex% s pattern replace,
 
     is_match : Str -> Str -> Bool
-    | doc m#"
+    | doc m%"
       `is_match regex str` checks if `str` matches `regex`.
 
       For example:
@@ -300,11 +300,11 @@
         is_match "\\d{4}" "123" =>
           false
       ```
-      "#m
+      "%m
     = fun regex s => %str_is_match% s regex,
 
     match : Str -> Str -> {match: Str, index: Num, groups: List Str}
-    | doc m#"
+    | doc m%"
       `match regex str` matches `str` given `regex`. Results in the part of `str` that matched, the index of the
       first character that was part of the match in `str`, and a lists of all capture groups if any.
 
@@ -315,11 +315,11 @@
         match "3" "01234" =>
           { match = "3", index = 3, groups = [ ] }
       ```
-      "#m
+      "%m
     = fun regex s => %str_match% s regex,
 
     length : Str -> Num
-    | doc m#"
+    | doc m%"
       Results in the length of the given string.
 
       For example:
@@ -329,11 +329,11 @@
         length "hi" =>
           2
       ```
-      "#m
+      "%m
     = fun s => %str_length% s,
 
     substring: Num -> Num -> Str -> Str
-    | doc m#"
+    | doc m%"
       Takes a slice from the string. Errors if either index is out of range.
 
       For example:
@@ -345,21 +345,21 @@
         substring (-3) 4 "abcdef" =>
           error
       ```
-      "#m
+      "%m
     = fun start end s => %str_substr% s start end,
 
     from_num | Num -> Str
-    | doc m#"
+    | doc m%"
       Converts a number to its string representation.
 
       from_num 42 =>
         "42"
-      "#m
+      "%m
     = fun n => %to_str% n,
 
     // from_enum | < | Dyn> -> Str = fun tag => %to_str% tag,
     from_enum | Dyn -> Str
-    | doc m#"
+    | doc m%"
       Converts an enum variant to its string representation.
 
       For example:
@@ -367,11 +367,11 @@
       from_enum `MyEnum =>
         "MyEnum"
       ```
-      "#m
+      "%m
     = fun tag => %to_str% tag,
 
     from_bool | Bool -> Str
-    | doc m#"
+    | doc m%"
       Converts a boolean value to its string representation.
 
       For example:
@@ -379,11 +379,11 @@
         from_bool true =>
           "true"
       ```
-      "#m
+      "%m
     = fun b => %to_str% b,
 
     to_num | NumLiteral -> Num
-    | doc m#"
+    | doc m%"
       Converts a string that represents an integer to that integer.
 
       For example:
@@ -391,11 +391,11 @@
         to_num "123" =>
           123
       ```
-      "#m
+      "%m
     = fun s => %num_from_str% s,
 
     to_bool | BoolLiteral -> Bool
-    | doc m#"
+    | doc m%"
       Converts a string that represents a boolean to that boolean.
 
       For example:
@@ -406,12 +406,12 @@
           true
         to_bool "false" =>
           false
-      "#m
+      "%m
     = fun s => s == "true",
 
     // to_enum | Ident -> < | Dyn> = fun s => %enum_from_str% s,
     to_enum | Ident -> Dyn
-    | doc m#"
+    | doc m%"
       Converts any string that represents an enum variant to that enum variant.
 
       For example:
@@ -419,7 +419,7 @@
         to_enum "Hello" =>
           `Hello
       ```
-      "#m
+      "%m
     = fun s => %enum_from_str% s,
   }
 }


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 84795587..bff0f86d 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -185,7 +185,7 @@ fn record_terms() {
     );
 
     assert_eq!(
-        parse_without_pos("{ a = 1, \"#{123}\" = (if 4 then 5 else 6), d = 42}"),
+        parse_without_pos("{ a = 1, \"%{123}\" = (if 4 then 5 else 6), d = 42}"),
         RecRecord(
             vec![
                 (Ident::from("a"), Num(1.).into()),
@@ -203,11 +203,11 @@ fn record_terms() {
     );
 
     assert_eq!(
-        parse_without_pos("{ a = 1, \"\\\"#}#\" = 2}"),
+        parse_without_pos("{ a = 1, \"\\\"%}%\" = 2}"),
         RecRecord(
             vec![
                 (Ident::from("a"), Num(1.).into()),
-                (Ident::from("\"#}#"), Num(2.).into()),
+                (Ident::from("\"%}%"), Num(2.).into()),
             ]
             .into_iter()
             .collect(),
@@ -246,11 +246,11 @@ fn string_lexing() {
     );
 
     assert_eq!(
-        lex_without_pos("\"1 + #{ 1 } + 2\""),
+        lex_without_pos("\"1 + %{ 1 } + 2\""),
         Ok(vec![
             Token::Normal(NormalToken::DoubleQuote),
             Token::Str(StringToken::Literal("1 + ")),
-            Token::Str(StringToken::HashBrace),
+            Token::Str(StringToken::Interpolation),
             Token::Normal(NormalToken::NumLiteral(1.0)),
             Token::Normal(NormalToken::RBrace),
             Token::Str(StringToken::Literal(" + 2")),
@@ -259,13 +259,13 @@ fn string_lexing() {
     );
 
     assert_eq!(
-        lex_without_pos("\"1 + #{ \"#{ 1 }\" } + 2\""),
+        lex_without_pos("\"1 + %{ \"%{ 1 }\" } + 2\""),
         Ok(vec![
             Token::Normal(NormalToken::DoubleQuote),
             Token::Str(StringToken::Literal("1 + ")),
-            Token::Str(StringToken::HashBrace),
+            Token::Str(StringToken::Interpolation),
             Token::Normal(NormalToken::DoubleQuote),
-            Token::Str(StringToken::HashBrace),
+            Token::Str(StringToken::Interpolation),
             Token::Normal(NormalToken::NumLiteral(1.0)),
             Token::Normal(NormalToken::RBrace),
             Token::Normal(NormalToken::DoubleQuote),
@@ -276,10 +276,10 @@ fn string_lexing() {
     );
 
     assert_eq!(
-        lex_without_pos(r##"m#""#"#m"##),
+        lex_without_pos(r##"m%""%"%m"##),
         Ok(vec![
             Token::Normal(NormalToken::MultiStringStart(3)),
-            Token::MultiStr(MultiStringToken::Literal("\"#")),
+            Token::MultiStr(MultiStringToken::Literal("\"%")),
             Token::MultiStr(MultiStringToken::End),
         ])
     );
@@ -296,12 +296,12 @@ fn str_escape() {
         mk_single_chunk("str\twith\nescapes"),
     );
     assert_eq!(
-        parse_without_pos("\"\\#\\#{ }\\#\""),
-        mk_single_chunk("##{ }#"),
+        parse_without_pos("\"\\%\\%{ }\\%\""),
+        mk_single_chunk("%%{ }%"),
     );
     assert_eq!(
-        parse_without_pos("\"#a#b#c\\#{d#\""),
-        mk_single_chunk("#a#b#c#{d#"),
+        parse_without_pos("\"%a%b%c\\%{d%\""),
+        mk_single_chunk("%a%b%c%{d%"),
     );
 }
 
@@ -337,24 +337,24 @@ fn ascii_escape() {
     assert_eq!(parse_without_pos("\"\\x08\""), mk_single_chunk("\x08"));
     assert_eq!(parse_without_pos("\"\\x7F\""), mk_single_chunk("\x7F"));
 
-    assert_eq!(parse_without_pos("m#\"\\x[f\"#m"), mk_single_chunk("\\x[f"));
-    assert_eq!(parse_without_pos("m#\"\\x0\"#m"), mk_single_chunk("\\x0"));
-    assert_eq!(parse_without_pos("m#\"\\x0z\"#m"), mk_single_chunk("\\x0z"));
-    assert_eq!(parse_without_pos("m#\"\\x00\"#m"), mk_single_chunk("\\x00"));
-    assert_eq!(parse_without_pos("m#\"\\x08\"#m"), mk_single_chunk("\\x08"));
-    assert_eq!(parse_without_pos("m#\"\\x7F\"#m"), mk_single_chunk("\\x7F"));
+    assert_eq!(parse_without_pos("m%\"\\x[f\"%m"), mk_single_chunk("\\x[f"));
+    assert_eq!(parse_without_pos("m%\"\\x0\"%m"), mk_single_chunk("\\x0"));
+    assert_eq!(parse_without_pos("m%\"\\x0z\"%m"), mk_single_chunk("\\x0z"));
+    assert_eq!(parse_without_pos("m%\"\\x00\"%m"), mk_single_chunk("\\x00"));
+    assert_eq!(parse_without_pos("m%\"\\x08\"%m"), mk_single_chunk("\\x08"));
+    assert_eq!(parse_without_pos("m%\"\\x7F\"%m"), mk_single_chunk("\\x7F"));
 }
 
 /// Regression test for [#230](https://github.com/tweag/nickel/issues/230).
 #[test]
 fn multiline_str_escape() {
     assert_eq!(
-        parse_without_pos(r##"m#"#Hel##lo###"#m"##),
-        mk_single_chunk("#Hel##lo###"),
+        parse_without_pos(r##"m%"%Hel%%lo%%%"%m"##),
+        mk_single_chunk("%Hel%%lo%%%"),
     );
     assert_eq!(
-        parse_without_pos(r##"m#"#Hel##{lo###{"#m"##),
-        mk_single_chunk("#Hel##{lo###{"),
+        parse_without_pos(r##"m%"%Hel%%{lo%%%{"%m"##),
+        mk_single_chunk("%Hel%%{lo%%%{"),
     );
 }
 
diff --git a/tests/basics_fail.rs b/tests/basics_fail.rs
index f6b6083d..98053871 100644
--- a/tests/basics_fail.rs
+++ b/tests/basics_fail.rs
@@ -58,7 +58,7 @@ fn boolean_ops() {
 #[test]
 fn string_chunks() {
     assert_matches!(
-        eval(r##""bad type #{1 + 1}""##),
+        eval(r##""bad type %{1 + 1}""##),
         Err(Error::EvalError(EvalError::TypeError(..)))
     );
 }
diff --git a/tests/pass/eq.ncl b/tests/pass/eq.ncl
index 61514d70..720e4364 100644
--- a/tests/pass/eq.ncl
+++ b/tests/pass/eq.ncl
@@ -4,7 +4,7 @@ let Assert = fun l x => x || %blame% l in
   // basic
   0 == 0 + 0 + 0,
   true == (if true then true else false),
-  "a" ++ "b" ++ "c" == "#{"a" ++ "b"}" ++ "c",
+  "a" ++ "b" ++ "c" == "%{"a" ++ "b"}" ++ "c",
   `Less == `Less,
   `small == `small,
 
diff --git a/tests/pass/record-defs.ncl b/tests/pass/record-defs.ncl
index 7773db04..8ab2e442 100644
--- a/tests/pass/record-defs.ncl
+++ b/tests/pass/record-defs.ncl
@@ -18,7 +18,7 @@ let Assert = fun l x => x || %blame% l in
   // quoted_fields
   {"foo$-^bar" = "foo"}."foo$-^bar" == "foo",
   let x = "fo" ++ "o" in
-    {"#{x}-bar" = "foo"}."foo-bar" == "foo",
+    {"%{x}-bar" = "foo"}."foo-bar" == "foo",
 
   {"foo-bar"."baz+baz" = false}."foo-bar"."baz+baz" == false,
   {foo-bar.baz-baz = false}.foo-bar.baz-baz == false,
@@ -37,7 +37,7 @@ let Assert = fun l x => x || %blame% l in
   {foo.bar.baz | Bool = true}.foo.bar.baz,
 
   // recursive_dynamic_fields
-  let x = "foo" in {"#{x}" = bar, bar = 1} == {foo = 1, bar = 1},
-  ({"#foo"."#bar".baz = other + 1, other = 0}."#foo"."#bar".baz == 1),
+  let x = "foo" in {"%{x}" = bar, bar = 1} == {foo = 1, bar = 1},
+  ({"%foo"."%bar".baz = other + 1, other = 0}."%foo"."%bar".baz == 1),
 ]
 |> list.foldl (fun x y => (x | Assert) && y) true
diff --git a/tests/pass/records.ncl b/tests/pass/records.ncl
index 6040a845..fa310cea 100644
--- a/tests/pass/records.ncl
+++ b/tests/pass/records.ncl
@@ -3,11 +3,11 @@ let Assert = fun l x => x || %blame% l in
 [
   // accesses
   ({foo = 3, bar = true}).bar == true,
-  {"#{if true then "foo" else "bar"}" = false, bar = true}.foo
+  {"%{if true then "foo" else "bar"}" = false, bar = true}.foo
     == false,
 
   ({foo = 3, bar = true})."bar" == true,
-  {"#{if true then "foo" else "bar"}" = false, bar = true}."#{"foo"}"
+  {"%{if true then "foo" else "bar"}" = false, bar = true}."%{"foo"}"
    == false,
 
   ({bar = 3}$["foo" = true]).foo == true,
diff --git a/tests/pass/strings.ncl b/tests/pass/strings.ncl
index 42296ccb..55990991 100644
--- a/tests/pass/strings.ncl
+++ b/tests/pass/strings.ncl
@@ -2,23 +2,23 @@ let Assert = fun l x => x || %blame% l in
 
 [
   // interpolation
-  "simple #{"interp" ++ "olation"} here" == "simple interpolation here",
-  "#{"alone"}" == "alone",
-  "nested #{ "#{(fun x => "#{x}") "expression"}" }" == "nested expression",
-  "#{"some"}#{" " ++ "se" ++ "qu"}#{"#{"ence"}"}" == "some sequence",
-  "nested #{ {str = {a = "braces"}.a}.str } !" == "nested braces !",
+  "simple %{"interp" ++ "olation"} here" == "simple interpolation here",
+  "%{"alone"}" == "alone",
+  "nested %{ "%{(fun x => "%{x}") "expression"}" }" == "nested expression",
+  "%{"some"}%{" " ++ "se" ++ "qu"}%{"%{"ence"}"}" == "some sequence",
+  "nested %{ {str = {a = "braces"}.a}.str } !" == "nested braces !",
   (let x = "world" in
-    "Hello, #{x}! Welcome in #{let y = "universe" in "the #{x}-#{y}"}")
+    "Hello, %{x}! Welcome in %{let y = "universe" in "the %{x}-%{y}"}")
     == "Hello, world! Welcome in the world-universe",
 
   // regression test for issue #361 (https://github.com/tweag/nickel/issues/361)
-  m#""#{"foo"}""#m == "\"foo\"",
-  m#"""#m == "\"",
-  m#""#"#"#"#m == "\"#\"#\"#",
+  m%""%{"foo"}""%m == "\"foo\"",
+  m%"""%m == "\"",
+  m%""%"%"%"%m == "\"%\"%\"%",
 
   // regression test for issue #596 (https://github.com/tweag/nickel/issues/596)
-  let s = "Hello" in m##""##{s}" World"##m == "\"Hello\" World",
-  let s = "Hello" in m##""###{s}" World"##m == "\"#Hello\" World",
-  m##""##s"##m == "\"##s",
+  let s = "Hello" in m%%""%%{s}" World"%%m == "\"Hello\" World",
+  let s = "Hello" in m%%""%%%{s}" World"%%m == "\"%Hello\" World",
+  m%%""%%s"%%m == "\"%%s",
 ]
 |> list.foldl (fun x y => (x | Assert) && y) true
diff --git a/tests/pass/typechecking.ncl b/tests/pass/typechecking.ncl
index 6d10de5e..b1761a76 100644
--- a/tests/pass/typechecking.ncl
+++ b/tests/pass/typechecking.ncl
@@ -89,8 +89,8 @@ let typecheck = [
     else
       (f {bla = true, blo = 3, ble = 4, bloppo = `bloppop, }) : Num),
 
-  ({ "#{if true then "foo" else "bar"}" = 2, } : {_ : Num}),
-  ({ "#{if true then "foo" else "bar"}" = 2, }."#{"bl" ++ "a"}" : Num),
+  ({ "%{if true then "foo" else "bar"}" = 2, } : {_ : Num}),
+  ({ "%{if true then "foo" else "bar"}" = 2, }."%{"bl" ++ "a"}" : Num),
   ({ foo = 3, bar = 4, } : {_ : Num}),
 
   // seq
@@ -153,7 +153,7 @@ let typecheck = [
   // shallow_inference
   let x = 1 in (x + 1 : Num),
   let x = "a" in (x ++ "a" : Str),
-  let x = "a#{"some str inside"}" in (x ++ "a" : Str),
+  let x = "a%{"some str inside"}" in (x ++ "a" : Str),
   let x = false in (x || true : Bool),
   let x = false in let y = x in let z = y in (z : Bool),
   // Regression test following, see [#297](https://github.com/tweag/nickel/pull/297). Check that
diff --git a/tests/records_fail.rs b/tests/records_fail.rs
index 66e8c6ab..514e57a3 100644
--- a/tests/records_fail.rs
+++ b/tests/records_fail.rs
@@ -10,7 +10,7 @@ fn records_access() {
         Err(Error::EvalError(EvalError::FieldMissing(..)))
     );
     assert_matches!(
-        eval("({ \"#{(if false then \"foo\" else \"bar\")}\" = false, bar = true, }).foo"),
+        eval("({ \"%{(if false then \"foo\" else \"bar\")}\" = false, bar = true, }).foo"),
         Err(Error::EvalError(EvalError::Other(msg, ..))) if msg.starts_with("$[ .. ]"));
 }
 
@@ -45,7 +45,7 @@ fn non_mergeable_piecewise() {
 #[test]
 fn dynamic_not_recursive() {
     assert_matches!(
-        eval("let x = \"foo\" in {\"#{x}\" = 1, bar = foo}.bar"),
+        eval("let x = \"foo\" in {\"%{x}\" = 1, bar = foo}.bar"),
         Err(Error::TypecheckError(TypecheckError::UnboundIdentifier(..)))
     );
 }
diff --git a/tests/typecheck_fail.rs b/tests/typecheck_fail.rs
index 2334c563..f23f3829 100644
--- a/tests/typecheck_fail.rs
+++ b/tests/typecheck_fail.rs
@@ -129,7 +129,7 @@ fn static_record_simple() {
 #[test]
 fn dynamic_record_simple() {
     assert_typecheck_fails!(
-        "({ \"#{if true then \"foo\" else \"bar\"}\" = 2, \"foo\" = true, }.\"bla\") : Num"
+        "({ \"%{if true then \"foo\" else \"bar\"}\" = 2, \"foo\" = true, }.\"bla\") : Num"
     );
 }
 
@@ -219,7 +219,7 @@ fn shallow_type_inference() {
 #[test]
 fn dynamic_record_field() {
     assert_matches!(
-        type_check_expr("let x = \"foo\" in {\"#{x}\" = 1} : {foo: Num}"),
+        type_check_expr("let x = \"foo\" in {\"%{x}\" = 1} : {foo: Num}"),
         Err(TypecheckError::TypeMismatch(..))
     );
 }