Skip to content

Incorrect compilation of triple-quoted ~b with escape sequences #11423

Description

@gabthau

Describe the bug
Under certain circumstances, triple-quoted strings created with the ~b containing hexadecimal escape sequences seem to be incorrectly compiled. More specifically, it seems that the leading \ of the escape sequence is lost, causing the x{XXXX} to appear verbatim in the binary.

This seems related to #11365 , except that it affect compiled module instead of functions defined in the shell.

I have assembled a few repro modules, but I have not been able to pin down exactly what triggers the issue. The exact same triple-quoted string in two different modules, or even within the same module, might or might not exhibit the issue, depending on the content of the module.

Note: I have not tested the behaviour for other (or no) sigils. Other sigils producing binaries do not respect escape sequence in triple-quoted strings.

To Reproduce
There are 4 files. All files compile successfully. Functions return a binary. There are eunit tests at the bottom of each file.

  • triple_quote_ok: all as expected.
  • triple_quote_ok2: all as expected.
  • triple_quote_fail1: the same file as triple_quote_ok2, but with two additional functions triple_quote5/0 and triple_quote6/0. Two failing tests: triple_quote1_test and triple_quote6_test.
  • triple_quote_fail2: the same file as triple_quote_fail1, but with an additional function triple_quote4a/0. One failing test: triple_quote5_test. triple_quote1_test and triple_quote6_test do not fail in this module.
-module(triple_quote_ok).

-include_lib("eunit/include/eunit.hrl").

-export([
    expected/0,
    triple_quote/0
]).

expected() ->
    String = "__\x{200C}\x{06DD}\x{070F}\x{180E}__",
    unicode:characters_to_binary(String).

triple_quote() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote_test() ->
    ?assertEqual(expected(), triple_quote()).
-module(triple_quote_ok2).

-include_lib("eunit/include/eunit.hrl").

-export([
    expected/0,
    triple_quote1/0,
    triple_quote2/0,
    triple_quote3/0,
    triple_quote4/0
]).

expected() ->
    String = "__\x{200C}\x{06DD}\x{070F}\x{180E}\x{206F}__",
    unicode:characters_to_binary(String).

triple_quote1() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote2() ->
    ~b"""
    __\x{200C}__
    """.

triple_quote3() ->
    ~b"""
    __\x{200C}\x{06DD}__
    """.

triple_quote4() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}__
    """.

triple_quote1_test() ->
    String = triple_quote1(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote2_test() ->
    String = triple_quote2(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote3_test() ->
    String = triple_quote3(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote4_test() ->
    String = triple_quote4(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).
-module(triple_quote_fail1).

-include_lib("eunit/include/eunit.hrl").

-export([
    expected/0,
    triple_quote1/0,
    triple_quote2/0,
    triple_quote3/0,
    triple_quote4/0,
    triple_quote5/0,
    triple_quote6/0
]).

expected() ->
    String = "__\x{200C}\x{06DD}\x{070F}\x{180E}\x{206F}__",
    unicode:characters_to_binary(String).

triple_quote1() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote2() ->
    ~b"""
    __\x{200C}__
    """.

triple_quote3() ->
    ~b"""
    __\x{200C}\x{06DD}__
    """.

triple_quote4() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}__
    """.

triple_quote5() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote6() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}\x{206F}__
    """.

triple_quote1_test() ->
    String = triple_quote1(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote2_test() ->
    String = triple_quote2(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote3_test() ->
    String = triple_quote3(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote4_test() ->
    String = triple_quote4(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote5_test() ->
    String = triple_quote5(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote6_test() ->
    ?assertEqual(expected(), triple_quote6()).
-module(triple_quote_fail2).

-include_lib("eunit/include/eunit.hrl").

-export([
    expected/0,
    triple_quote1/0,
    triple_quote2/0,
    triple_quote3/0,
    triple_quote4/0,
    triple_quote4a/0,
    triple_quote5/0,
    triple_quote6/0
]).

expected() ->
    String = "__\x{200C}\x{06DD}\x{070F}\x{180E}\x{206F}__",
    unicode:characters_to_binary(String).

triple_quote1() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote2() ->
    ~b"""
    __\x{200C}__
    """.

triple_quote3() ->
    ~b"""
    __\x{200C}\x{06DD}__
    """.

triple_quote4a() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}__
    """.

triple_quote4() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}__
    """.

triple_quote5() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}__
    """.

triple_quote6() ->
    ~b"""
    __\x{200C}\x{06DD}\x{070F}\x{180E}\x{206F}__
    """.

triple_quote1_test() ->
    String = triple_quote1(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote2_test() ->
    String = triple_quote2(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote3_test() ->
    String = triple_quote3(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote4_test() ->
    String = triple_quote4(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote5_test() ->
    String = triple_quote5(),
    ExpectedPrefix = byte_size(String) -2,
    ?assertEqual(ExpectedPrefix, binary:longest_common_prefix([String, expected()])).

triple_quote6_test() ->
    ?assertEqual(expected(), triple_quote6()).

Output:

Eshell V16.3 (press Ctrl+G to abort, type help(). for help)
1> c(triple_quote_ok).
{ok,triple_quote_ok}
2> c(triple_quote_ok2).
{ok,triple_quote_ok2}
3> c(triple_quote_fail1).
{ok,triple_quote_fail1}
4> c(triple_quote_fail2).
{ok,triple_quote_fail2}
5> eunit:test(triple_quote_ok).
  Test passed.
ok
6> eunit:test(triple_quote_ok2).
  All 4 tests passed.
ok
7> eunit:test(triple_quote_fail1).
triple_quote_fail1: triple_quote1_test...*failed*
in function triple_quote_fail1:triple_quote1_test/0 (triple_quote_fail1.erl, line 52)
in call from eunit_test:'-mf_wrapper/2-fun-0-'/2 (eunit_test.erl, line 285)
in call from eunit_test:run_testfun/1 (eunit_test.erl, line 83)
in call from eunit_proc:run_test/1 (eunit_proc.erl, line 554)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 379)
in call from eunit_proc:handle_test/2 (eunit_proc.erl, line 537)
in call from eunit_proc:tests_inorder/3 (eunit_proc.erl, line 479)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 369)
**error:{assertEqual,[{module,triple_quote_fail1},
              {line,52},
              {expression,"binary : longest_common_prefix ( [ String , expected ( ) ] )"},
              {expected,16},
              {value,2}]}
  output:<<"">>

triple_quote_fail1: triple_quote6_test...*failed*
in function triple_quote_fail1:triple_quote6_test/0 (triple_quote_fail1.erl, line 75)
in call from eunit_test:'-mf_wrapper/2-fun-0-'/2 (eunit_test.erl, line 285)
in call from eunit_test:run_testfun/1 (eunit_test.erl, line 83)
in call from eunit_proc:run_test/1 (eunit_proc.erl, line 554)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 379)
in call from eunit_proc:handle_test/2 (eunit_proc.erl, line 537)
in call from eunit_proc:tests_inorder/3 (eunit_proc.erl, line 479)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 369)
**error:{assertEqual,[{module,triple_quote_fail1},
              {line,75},
              {expression,"triple_quote6 ( )"},
              {expected,<<95,95,226,128,140,219,157,220,143,225,160,...>>},
              {value,<<95,95,226,128,140,219,157,220,143,225,...>>}]}
  output:<<"">>

=======================================================
  Failed: 2.  Skipped: 0.  Passed: 4.
error
8> eunit:test(triple_quote_fail2).
triple_quote_fail2: triple_quote5_test...*failed*
in function triple_quote_fail2:triple_quote5_test/0 (triple_quote_fail2.erl, line 78)
in call from eunit_test:'-mf_wrapper/2-fun-0-'/2 (eunit_test.erl, line 285)
in call from eunit_test:run_testfun/1 (eunit_test.erl, line 83)
in call from eunit_proc:run_test/1 (eunit_proc.erl, line 554)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 379)
in call from eunit_proc:handle_test/2 (eunit_proc.erl, line 537)
in call from eunit_proc:tests_inorder/3 (eunit_proc.erl, line 479)
in call from eunit_proc:with_timeout/3 (eunit_proc.erl, line 369)
**error:{assertEqual,[{module,triple_quote_fail2},
              {line,78},
              {expression,"binary : longest_common_prefix ( [ String , expected ( ) ] )"},
              {expected,17},
              {value,7}]}
  output:<<"">>

=======================================================
  Failed: 1.  Skipped: 0.  Passed: 5.
error
9> {triple_quote_ok2:triple_quote1(), triple_quote_fail1:triple_quote1(), triple_quote_fail2:triple_quote1()}.
{<<95,95,226,128,140,219,157,220,143,225,160,142,95,95>>,
 <<95,95,120,123,50,48,48,67,125,219,157,220,143,225,160,
   142,95,95>>,
 <<95,95,226,128,140,219,157,220,143,225,160,142,95,95>>}
10> binary:part(triple_quote_fail1:triple_quote1(), 0, 9).
<<"__x{200C}">>

Expected behavior
Triple-quoted string behave as documented: when using the ~b sigil, escape sequences are in effect.

Affected versions
Tested on 28.4.1 and 29.0.2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssue is reported as a bugteam:PSAssigned to OTP team PS

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions