<div dir="ltr">This looks fine.</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Tue, Oct 29, 2019 at 12:02 PM Peter Wang <<a href="mailto:novalazy@gmail.com">novalazy@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">library/string.m:<br>
    Define first_char/3 to fail if the input string begins with an<br>
    ill-formed code unit sequence.<br>
<br>
    Define the reverse mode to throw an exception on an attempt to<br>
    encode a null character or surrogate code point in the output<br>
    string.<br>
<br>
    Reimplement first_char/3 in Mercury.<br>
<br>
hard_coded/Mmakefile:<br>
hard_coded/string_first_char_ilseq.exp:<br>
hard_coded/string_first_char_ilseq.m:<br>
    Add test case.<br>
---<br>
 library/string.m                             | 373 ++++---------------<br>
 tests/hard_coded/Mmakefile                   |   1 +<br>
 tests/hard_coded/string_first_char_ilseq.exp |  10 +<br>
 tests/hard_coded/string_first_char_ilseq.m   |  97 +++++<br>
 4 files changed, 183 insertions(+), 298 deletions(-)<br>
 create mode 100644 tests/hard_coded/string_first_char_ilseq.exp<br>
 create mode 100644 tests/hard_coded/string_first_char_ilseq.m<br>
<br>
diff --git a/library/string.m b/library/string.m<br>
index cbdbcd128..82bc84fa3 100644<br>
--- a/library/string.m<br>
+++ b/library/string.m<br>
@@ -670,8 +670,13 @@<br>
 % Splitting up strings.<br>
 %<br>
<br>
-    % first_char(String, Char, Rest) is true iff Char is the first character<br>
-    % (code point) of String, and Rest is the remainder.<br>
+    % first_char(String, Char, Rest) is true iff `String' begins with a<br>
+    % well-formed code unit sequence, `Char' is the code point encoded by<br>
+    % that sequence, and `Rest' is the rest of `String' after that sequence.<br>
+    %<br>
+    % The (uo, in, in) mode throws an exception if `Char' cannot be encoded in<br>
+    % a string, or if `Char' is a surrogate code point (for consistency with<br>
+    % the other modes).<br>
     %<br>
     % WARNING: first_char makes a copy of Rest because the garbage collector<br>
     % doesn't handle references into the middle of an object, at least not the<br>
@@ -2395,6 +2400,28 @@ index_next(Str, Index, NextIndex, Char) :-<br>
     end<br>
 ").<br>
<br>
+%---------------------%<br>
+<br>
+    % XXX ILSEQ Provide public interfaces to index into strings while<br>
+    % signalling if we encountered an ill-formed sequence.<br>
+    %<br>
+:- pred index_next_not_replaced(string::in, int::in, int::out, char::uo)<br>
+    is semidet.<br>
+<br>
+index_next_not_replaced(Str, Index, NextIndex, Char) :-<br>
+    index_next(Str, Index, NextIndex, Char0),<br>
+    ( if<br>
+        internal_encoding_is_utf8,<br>
+        Char0 = '\ufffd'<br>
+    then<br>
+        unsafe_between(Str, Index, NextIndex, "\ufffd")<br>
+    else<br>
+        true<br>
+    ),<br>
+    unsafe_promise_unique(Char0, Char).<br>
+<br>
+%---------------------%<br>
+<br>
 prev_index(Str, Index, PrevIndex, Char) :-<br>
     Len = length(Str),<br>
     ( if index_check(Index - 1, Len) then<br>
@@ -2507,6 +2534,8 @@ do_unsafe_prev_index(Str, Index) -><br>
     end.<br>
 ").<br>
<br>
+%---------------------%<br>
+<br>
     % XXX We should consider making this routine a compiler built-in.<br>
     %<br>
 :- pred index_check(int::in, int::in) is semidet.<br>
@@ -3896,302 +3925,50 @@ join_list_loop(Sep, [H | T]) = Sep ++ H ++ join_list_loop(Sep, T).<br>
 % Splitting up strings.<br>
 %<br>
<br>
-% XXX ILSEQ Behaviour depends on target language.<br>
-%  - C: fails if the string begins with ill-formed sequence<br>
-%  - Java/C#: succeeds if the string begins with an unpaired surrogate<br>
-<br>
-:- pragma foreign_proc("C",<br>
-    first_char(Str::in, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,<br>
-        does_not_affect_liveness, no_sharing],<br>
-"<br>
-    MR_Integer pos = 0;<br>
-    int c = MR_utf8_get_next(Str, &pos);<br>
-    SUCCESS_INDICATOR = (<br>
-        c == First &&<br>
-        First != '\\0' &&<br>
-        strcmp(Str + pos, Rest) == 0<br>
-    );<br>
-").<br>
-:- pragma foreign_proc("C#",<br>
-    first_char(Str::in, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    int len = Str.Length;<br>
-    if (First <= 0xffff) {<br>
-        SUCCESS_INDICATOR = (<br>
-            len > 0 &&<br>
-            Str[0] == First &&<br>
-            System.String.CompareOrdinal(Str, 1, Rest, 0, len) == 0<br>
-        );<br>
-    } else {<br>
-        string firstchars = System.Char.ConvertFromUtf32(First);<br>
-        SUCCESS_INDICATOR = (<br>
-            len > 1 &&<br>
-            Str[0] == firstchars[0] &&<br>
-            Str[1] == firstchars[1] &&<br>
-            System.String.CompareOrdinal(Str, 2, Rest, 0, len) == 0<br>
-        );<br>
-    }<br>
-").<br>
-:- pragma foreign_proc("Java",<br>
-    first_char(Str::in, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    int toffset = java.lang.Character.charCount(First);<br>
-    SUCCESS_INDICATOR = (<br>
-        Str.length() > 0 &&<br>
-        Str.codePointAt(0) == First &&<br>
-        Str.regionMatches(toffset, Rest, 0, Rest.length())<br>
-    );<br>
-").<br>
-:- pragma foreign_proc("Erlang",<br>
-    first_char(Str::in, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    case Str of<br>
-        <<First/utf8, Rest/binary>> -><br>
-            SUCCESS_INDICATOR = true;<br>
-        _ -><br>
-            SUCCESS_INDICATOR = false<br>
-    end<br>
-").<br>
-<br>
-:- pragma foreign_proc("C",<br>
-    first_char(Str::in, First::uo, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,<br>
-        does_not_affect_liveness, no_sharing],<br>
-"<br>
-    MR_Integer pos = 0;<br>
-    First = MR_utf8_get_next(Str, &pos);<br>
-    SUCCESS_INDICATOR = (First > 0 && strcmp(Str + pos, Rest) == 0);<br>
-").<br>
-:- pragma foreign_proc("C#",<br>
-    first_char(Str::in, First::uo, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    try {<br>
-        int len = Str.Length;<br>
-        char c1 = Str[0];<br>
-        if (System.Char.IsHighSurrogate(c1)) {<br>
-            char c2 = Str[1];<br>
-            First = System.Char.ConvertToUtf32(c1, c2);<br>
-            SUCCESS_INDICATOR =<br>
-                (System.String.CompareOrdinal(Str, 2, Rest, 0, len) == 0);<br>
-        } else {<br>
-            First = c1;<br>
-            SUCCESS_INDICATOR =<br>
-                (System.String.CompareOrdinal(Str, 1, Rest, 0, len) == 0);<br>
-        }<br>
-    } catch (System.IndexOutOfRangeException) {<br>
-        SUCCESS_INDICATOR = false;<br>
-        First = (char) 0;<br>
-    } catch (System.ArgumentOutOfRangeException) {<br>
-        SUCCESS_INDICATOR = false;<br>
-        First = (char) 0;<br>
-    }<br>
-").<br>
-:- pragma foreign_proc("Java",<br>
-    first_char(Str::in, First::uo, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    int toffset;<br>
-    if (Str.length() > 0) {<br>
-        First = Str.codePointAt(0);<br>
-        toffset = java.lang.Character.charCount(First);<br>
-        SUCCESS_INDICATOR =<br>
-            Str.regionMatches(toffset, Rest, 0, Rest.length());<br>
-    } else {<br>
-        SUCCESS_INDICATOR = false;<br>
-        // XXX to avoid uninitialized var warning<br>
-        First = 0;<br>
-    }<br>
-").<br>
-:- pragma foreign_proc("Erlang",<br>
-    first_char(Str::in, First::uo, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    case Str of<br>
-        <<First/utf8, Rest/binary>> -><br>
-            SUCCESS_INDICATOR = true;<br>
-        _ -><br>
-            SUCCESS_INDICATOR = false,<br>
-            First = 0<br>
-    end<br>
-").<br>
-<br>
-:- pragma foreign_proc("C",<br>
-    first_char(Str::in, First::in, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,<br>
-        does_not_affect_liveness, no_sharing],<br>
-"{<br>
-    MR_Integer pos = 0;<br>
-    int c = MR_utf8_get_next(Str, &pos);<br>
-    if (c != First || First == '\\0') {<br>
-        SUCCESS_INDICATOR = MR_FALSE;<br>
-    } else {<br>
-        Str += pos;<br>
-        // We need to make a copy to ensure that the pointer is word-aligned.<br>
-        MR_allocate_aligned_string_msg(Rest, strlen(Str), MR_ALLOC_ID);<br>
-        strcpy(Rest, Str);<br>
-        SUCCESS_INDICATOR = MR_TRUE;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("C#",<br>
-    first_char(Str::in, First::in, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    int len = Str.Length;<br>
-<br>
-    if (len > 0) {<br>
-        if (First <= 0xffff) {<br>
-            SUCCESS_INDICATOR = (First == Str[0]);<br>
-            Rest = Str.Substring(1);<br>
-        } else {<br>
-            string firststr = System.Char.ConvertFromUtf32(First);<br>
-            SUCCESS_INDICATOR =<br>
-                (System.String.CompareOrdinal(Str, 0, firststr, 0, 2) == 0);<br>
-            Rest = Str.Substring(2);<br>
-        }<br>
-    } else {<br>
-        SUCCESS_INDICATOR = false;<br>
-        Rest = null;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("Java",<br>
-    first_char(Str::in, First::in, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    int len = Str.length();<br>
-<br>
-    if (len > 0) {<br>
-        SUCCESS_INDICATOR = (First == Str.codePointAt(0));<br>
-        Rest = Str.substring(java.lang.Character.charCount(First));<br>
-    } else {<br>
-        SUCCESS_INDICATOR = false;<br>
-        // XXX to avoid uninitialized var warning<br>
-        Rest = null;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("Erlang",<br>
-    first_char(Str::in, First::in, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    case Str of<br>
-        <<First/utf8, Rest/binary>> -><br>
-            SUCCESS_INDICATOR = true;<br>
-        _ -><br>
-            SUCCESS_INDICATOR = false,<br>
-            Rest = <<>><br>
-    end<br>
-").<br>
-<br>
-:- pragma foreign_proc("C",<br>
-    first_char(Str::in, First::uo, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,<br>
-        does_not_affect_liveness, no_sharing],<br>
-"{<br>
-    MR_Integer pos = 0;<br>
-    First = MR_utf8_get_next(Str, &pos);<br>
-    if (First < 1) {<br>
-        SUCCESS_INDICATOR = MR_FALSE;<br>
-    } else {<br>
-        Str += pos;<br>
-        // We need to make a copy to ensure that the pointer is word-aligned.<br>
-        MR_allocate_aligned_string_msg(Rest, strlen(Str), MR_ALLOC_ID);<br>
-        strcpy(Rest, Str);<br>
-        SUCCESS_INDICATOR = MR_TRUE;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("C#",<br>
-    first_char(Str::in, First::uo, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    try {<br>
-        char c1 = Str[0];<br>
-        if (System.Char.IsHighSurrogate(c1)) {<br>
-            char c2 = Str[1];<br>
-            First = System.Char.ConvertToUtf32(c1, c2);<br>
-            Rest = Str.Substring(2);<br>
-        } else {<br>
-            First = Str[0];<br>
-            Rest = Str.Substring(1);<br>
-        }<br>
-        SUCCESS_INDICATOR = true;<br>
-    } catch (System.IndexOutOfRangeException) {<br>
-        SUCCESS_INDICATOR = false;<br>
-        First = (char) 0;<br>
-        Rest = null;<br>
-    } catch (System.ArgumentOutOfRangeException) {<br>
-        SUCCESS_INDICATOR = false;<br>
-        First = (char) 0;<br>
-        Rest = null;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("Java",<br>
-    first_char(Str::in, First::uo, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    if (Str.length() == 0) {<br>
-        SUCCESS_INDICATOR = false;<br>
-        First = (char) 0;<br>
-        Rest = null;<br>
-    } else {<br>
-        First = Str.codePointAt(0);<br>
-        Rest = Str.substring(java.lang.Character.charCount(First));<br>
-        SUCCESS_INDICATOR = true;<br>
-    }<br>
-}").<br>
-:- pragma foreign_proc("Erlang",<br>
-    first_char(Str::in, First::uo, Rest::uo),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    case Str of<br>
-        <<First/utf8, Rest/binary>> -><br>
-            SUCCESS_INDICATOR = true;<br>
-        _ -><br>
-            SUCCESS_INDICATOR = false,<br>
-            First = 0,<br>
-            Rest = <<>><br>
-    end<br>
-").<br>
-<br>
-:- pragma foreign_proc("C",<br>
-    first_char(Str::uo, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,<br>
-        does_not_affect_liveness, no_sharing],<br>
-"{<br>
-    size_t firstw = MR_utf8_width(First);<br>
-    size_t len = firstw + strlen(Rest);<br>
-    MR_allocate_aligned_string_msg(Str, len, MR_ALLOC_ID);<br>
-    MR_utf8_encode(Str, First);<br>
-    strcpy(Str + firstw, Rest);<br>
-}").<br>
-:- pragma foreign_proc("C#",<br>
-    first_char(Str::uo, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    string FirstStr;<br>
-    if (First <= 0xffff) {<br>
-        FirstStr = new System.String((char) First, 1);<br>
-    } else {<br>
-        FirstStr = System.Char.ConvertFromUtf32(First);<br>
-    }<br>
-    Str = FirstStr + Rest;<br>
-}").<br>
-:- pragma foreign_proc("Java",<br>
-    first_char(Str::uo, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"{<br>
-    String FirstStr = new String(Character.toChars(First));<br>
-    Str = FirstStr.concat(Rest);<br>
-}").<br>
-:- pragma foreign_proc("Erlang",<br>
-    first_char(Str::uo, First::in, Rest::in),<br>
-    [will_not_call_mercury, promise_pure, thread_safe],<br>
-"<br>
-    Str = unicode:characters_to_binary([First, Rest])<br>
-").<br>
+:- pragma promise_equivalent_clauses(first_char/3).<br>
+<br>
+first_char(Str::in, First::in, Rest::in) :-<br>
+    first_char_rest_in(Str, First, Rest).<br>
+first_char(Str::in, First::uo, Rest::in) :-<br>
+    first_char_rest_in(Str, First, Rest).<br>
+first_char(Str::in, First::in, Rest::uo) :-<br>
+    first_char_rest_out(Str, First, Rest).<br>
+first_char(Str::in, First::uo, Rest::uo) :-<br>
+    first_char_rest_out(Str, First, Rest).<br>
+first_char(Str::uo, First::in, Rest::in) :-<br>
+    first_char_str_out(Str, First, Rest).<br>
+<br>
+:- pred first_char_rest_in(string, char, string).<br>
+:- mode first_char_rest_in(in, in, in) is semidet.<br>
+:- mode first_char_rest_in(in, uo, in) is semidet.<br>
+<br>
+first_char_rest_in(Str, First, Rest) :-<br>
+    index_next_not_replaced(Str, 0, NextIndex, First0),<br>
+    not is_surrogate(First0),<br>
+    unsafe_promise_unique(First0, First),<br>
+    unsafe_compare_substrings((=), Str, NextIndex, Rest, 0, length(Rest)).<br>
+<br>
+:- pred first_char_rest_out(string, char, string).<br>
+:- mode first_char_rest_out(in, in, uo) is semidet.<br>
+:- mode first_char_rest_out(in, uo, uo) is semidet.<br>
+<br>
+first_char_rest_out(Str, First, Rest) :-<br>
+    index_next_not_replaced(Str, 0, NextIndex, First0),<br>
+    not is_surrogate(First0),<br>
+    unsafe_promise_unique(First0, First),<br>
+    unsafe_between(Str, NextIndex, length(Str), Rest).<br>
+<br>
+:- pred first_char_str_out(string, char, string).<br>
+:- mode first_char_str_out(uo, in, in) is det.<br>
+<br>
+first_char_str_out(Str, First, Rest) :-<br>
+    ( if char.to_int(First, 0) then<br>
+        unexpected($pred, "null character")<br>
+    else if char.is_surrogate(First) then<br>
+        unexpected($pred, "surrogate code point")<br>
+    else<br>
+        Str = char_to_string(First) ++ Rest<br>
+    ).<br>
<br>
 %---------------------%<br>
<br>
diff --git a/tests/hard_coded/Mmakefile b/tests/hard_coded/Mmakefile<br>
index 47e8ec5a8..048827e4a 100644<br>
--- a/tests/hard_coded/Mmakefile<br>
+++ b/tests/hard_coded/Mmakefile<br>
@@ -364,6 +364,7 @@ ORDINARY_PROGS = \<br>
        string_compare_substrings \<br>
        string_count_codepoints_ilseq \<br>
        string_first_char \<br>
+       string_first_char_ilseq \<br>
        string_fold_ilseq \<br>
        string_from_char_list_ilseq \<br>
        string_index_ilseq \<br>
diff --git a/tests/hard_coded/string_first_char_ilseq.exp b/tests/hard_coded/string_first_char_ilseq.exp<br>
new file mode 100644<br>
index 000000000..1e8d934f3<br>
--- /dev/null<br>
+++ b/tests/hard_coded/string_first_char_ilseq.exp<br>
@@ -0,0 +1,10 @@<br>
+first_char(in, out, in) failed<br>
+first_char(in, out, out) failed<br>
+first_char(in, in, in) failed<br>
+first_char(in, in, out) failed<br>
+first_char(in, in, in) failed<br>
+first_char(in, in, out) failed<br>
+first_char(in, in, in) failed<br>
+first_char(in, in, out) failed<br>
+first_char(out, in, in) threw exception: software_error("predicate `string.first_char_str_out\'/3: Unexpected: surrogate code point")<br>
+first_char(out, in, in) threw exception: software_error("predicate `string.first_char_str_out\'/3: Unexpected: surrogate code point")<br>
diff --git a/tests/hard_coded/string_first_char_ilseq.m b/tests/hard_coded/string_first_char_ilseq.m<br>
new file mode 100644<br>
index 000000000..107189580<br>
--- /dev/null<br>
+++ b/tests/hard_coded/string_first_char_ilseq.m<br>
@@ -0,0 +1,97 @@<br>
+%---------------------------------------------------------------------------%<br>
+% vim: ts=4 sw=4 et ft=mercury<br>
+%---------------------------------------------------------------------------%<br>
+<br>
+:- module string_first_char_ilseq.<br>
+:- interface.<br>
+<br>
+:- import_module io.<br>
+<br>
+:- pred main(io::di, io::uo) is cc_multi.<br>
+<br>
+%---------------------------------------------------------------------------%<br>
+<br>
+:- implementation.<br>
+<br>
+:- import_module char.<br>
+:- import_module string.<br>
+<br>
+%---------------------------------------------------------------------------%<br>
+<br>
+main(!IO) :-<br>
+    S0 = "😀",                              % UTF-16: 0xD83D 0xDE00<br>
+    S1 = string.between(S0, 1, length(S0)), % UTF-16: 0xDE00<br>
+    Rest = "rest",<br>
+    S = S1 ++ Rest,<br>
+<br>
+    test_first_char_ioi(S, Rest, !IO),<br>
+    test_first_char_ioo(S, !IO),<br>
+<br>
+    % An implementation might return U+FFFD as the first char of a string<br>
+    % beginning with an ill-formed code unit sequence, which is wrong by our<br>
+    % definition.<br>
+    Replacement = char.det_from_int(0xFFFD),<br>
+    test_first_char_iii(S, Replacement, Rest, !IO),<br>
+    test_first_char_iio(S, Replacement, !IO),<br>
+<br>
+    % An implementation might separate out a surrogate code point as the<br>
+    % first code point, which is wrong by our definition.<br>
+    HiSurr = char.det_from_int(0xD83D),<br>
+    LoSurr = char.det_from_int(0xDE00),<br>
+    test_first_char_iii(S, HiSurr, Rest, !IO),<br>
+    test_first_char_iio(S, HiSurr, !IO),<br>
+    test_first_char_iii(S, LoSurr, Rest, !IO),<br>
+    test_first_char_iio(S, LoSurr, !IO),<br>
+<br>
+    % Prepending a surrogate code point is disallowed.<br>
+    test_first_char_oii(HiSurr, S, !IO),<br>
+    test_first_char_oii(LoSurr, S, !IO).<br>
+<br>
+:- pred test_first_char_iii(string::in, char::in, string::in, io::di, io::uo)<br>
+    is det.<br>
+<br>
+test_first_char_iii(Str, FirstChar, Rest, !IO) :-<br>
+    ( if string.first_char(Str, FirstChar, Rest) then<br>
+        io.write_string("first_char(in, in, in) succeeded\n", !IO)<br>
+    else<br>
+        io.write_string("first_char(in, in, in) failed\n", !IO)<br>
+    ).<br>
+<br>
+:- pred test_first_char_ioi(string::in, string::in, io::di, io::uo) is det.<br>
+<br>
+test_first_char_ioi(Str, Rest, !IO) :-<br>
+    ( if string.first_char(Str, _FirstChar, Rest) then<br>
+        io.write_string("first_char(in, out, in) succeeded\n", !IO)<br>
+    else<br>
+        io.write_string("first_char(in, out, in) failed\n", !IO)<br>
+    ).<br>
+<br>
+:- pred test_first_char_iio(string::in, char::in, io::di, io::uo) is det.<br>
+<br>
+test_first_char_iio(Str, FirstChar, !IO) :-<br>
+    ( if string.first_char(Str, FirstChar, _Rest) then<br>
+        io.write_string("first_char(in, in, out) succeeded\n", !IO)<br>
+    else<br>
+        io.write_string("first_char(in, in, out) failed\n", !IO)<br>
+    ).<br>
+<br>
+:- pred test_first_char_ioo(string::in, io::di, io::uo) is det.<br>
+<br>
+test_first_char_ioo(Str, !IO) :-<br>
+    ( if string.first_char(Str, _FirstChar, _Rest) then<br>
+        io.write_string("first_char(in, out, out) succeeded\n", !IO)<br>
+    else<br>
+        io.write_string("first_char(in, out, out) failed\n", !IO)<br>
+    ).<br>
+<br>
+:- pred test_first_char_oii(char::in, string::in, io::di, io::uo) is cc_multi.<br>
+<br>
+test_first_char_oii(FirstChar, Rest, !IO) :-<br>
+    ( try []<br>
+        string.first_char(_Str, FirstChar, Rest)<br>
+    then<br>
+        io.write_string("first_char(out, in, in) succeeded\n", !IO)<br>
+    catch_any Excp -><br>
+        io.write_string("first_char(out, in, in) threw exception: ", !IO),<br>
+        io.print_line(Excp, !IO)<br>
+    ).<br>
-- <br>
2.23.0<br>
<br>
_______________________________________________<br>
reviews mailing list<br>
<a href="mailto:reviews@lists.mercurylang.org" target="_blank">reviews@lists.mercurylang.org</a><br>
<a href="https://lists.mercurylang.org/listinfo/reviews" rel="noreferrer" target="_blank">https://lists.mercurylang.org/listinfo/reviews</a><br>
</blockquote></div>