[m-rev.] for review: make test hard_coded/string_class test more comprehensive

Julien Fischer jfischer at opturion.com
Sun Sep 20 20:08:30 AEST 2026


For review by anyone.

--------------------------------

Make the hard_coded/string_class test more comprehensive.

- Test each test predicate on every test string.
- Include is_empty/1 is_all_alnum/1 in the predicates to test.
- Add some tests the boundaries of the ASCII range.
- Add some tests involving ill-formed strings.

Generate the output as a table for ease of comparison.

tests/hard_coded/string_class.m:
tests/hard_coded/string_class.exp:
    Make the above changes.

tests/hard_coded/string_class.exp2:
    Alternative expected output for UTF-16 systems.

Julien.

diff --git a/tests/hard_coded/string_class.exp
b/tests/hard_coded/string_class.exp
index 58ab1656d..c71324457 100644
--- a/tests/hard_coded/string_class.exp
+++ b/tests/hard_coded/string_class.exp
@@ -1,40 +1,83 @@
-is_all_alpha("")
-is_all_alpha("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alpha("abcdefghijklmnopqrstuvwxyz")
-not is_all_alpha("0123456789")
-not is_all_alpha("_")
-not is_all_alpha("aΓŸΞΎε••π€€.")
-not is_all_alpha("ΓŸΞΎε••π€€")
+empty  = is_all_empty      alpha_ = is_all_alpha_or_underscore
+alpha  = is_all_alpha      alnum_ = is_all_alnum_or_underscore
+alnum  = is_all_alnum      match  = all_match(ascii_alpha)
+digit  = is_all_digits     nonasc = all_match(nonascii)

-is_all_alpha_or_underscore("")
-is_all_alpha_or_underscore("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alpha_or_underscore("abcdefghijklmnopqrstuvwxyz")
-not is_all_alpha_or_underscore("0123456789")
-is_all_alpha_or_underscore("_")
-not is_all_alpha_or_underscore("aΓŸΞΎε••π€€.")
-not is_all_alpha_or_underscore("ΓŸΞΎε••π€€")
-
-is_all_alnum_or_underscore("")
-is_all_alnum_or_underscore("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alnum_or_underscore("abcdefghijklmnopqrstuvwxyz")
-is_all_alnum_or_underscore("0123456789")
-is_all_alnum_or_underscore("_")
-not is_all_alnum_or_underscore("aΓŸΞΎε••π€€.")
-not is_all_alnum_or_underscore("ΓŸΞΎε••π€€")
-
-is_all_digits("")
-not is_all_digits("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-not is_all_digits("abcdefghijklmnopqrstuvwxyz")
-is_all_digits("0123456789")
-not is_all_digits("_")
-not is_all_digits("aΓŸΞΎε••π€€.")
-not is_all_digits("ΓŸΞΎε••π€€")
-
-all_match(nonascii) ("")
-not all_match(nonascii) ("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-not all_match(nonascii) ("abcdefghijklmnopqrstuvwxyz")
-not all_match(nonascii) ("0123456789")
-not all_match(nonascii) ("_")
-not all_match(nonascii) ("aΓŸΞΎε••π€€.")
-all_match(nonascii) ("ΓŸΞΎε••π€€")
+alpha and match should agree on every well-formed string.

+string                               empty alpha alnum alpha_ alnum_
digit match nonasc
+""                                   T     T     T     T      T
T     T     T
+" "                                  F     F     F     F      F
F     F     F
+"  "                                 F     F     F     F      F
F     F     F
+"\t"                                 F     F     F     F      F
F     F     F
+"\n"                                 F     F     F     F      F
F     F     F
+"\r"                                 F     F     F     F      F
F     F     F
+" \t\n\r"                            F     F     F     F      F
F     F     F
+"\u00a0"                             F     F     F     F      F
F     F     T
+"\u2002"                             F     F     F     F      F
F     F     T
+"\u3000"                             F     F     F     F      F
F     F     T
+"\u200b"                             F     F     F     F      F
F     F     T
+"\ufeff"                             F     F     F     F      F
F     F     T
+"\u200e"                             F     F     F     F      F
F     F     T
+"\u0301"                             F     F     F     F      F
F     F     T
+"a"                                  F     T     T     T      T
F     T     F
+"Z"                                  F     T     T     T      T
F     T     F
+"0"                                  F     F     T     F      T
T     F     F
+"_"                                  F     F     F     T      T
F     F     F
+"."                                  F     F     F     F      F
F     F     F
+" "                                  F     F     F     F      F
F     F     F
+"ABCDEFGHIJKLMNOPQRSTUVWXYZ"         F     T     T     T      T
F     T     F
+"abcdefghijklmnopqrstuvwxyz"         F     T     T     T      T
F     T     F
+"0123456789"                         F     F     T     F      T
T     F     F
+"___"                                F     F     F     T      T
F     F     F
+"abc123"                             F     F     T     F      T
F     F     F
+"123abc"                             F     F     T     F      T
F     F     F
+"a1b2c3"                             F     F     T     F      T
F     F     F
+"abc_123"                            F     F     F     F      T
F     F     F
+"_abc"                               F     F     F     T      T
F     F     F
+"abc_"                               F     F     F     T      T
F     F     F
+"a_1"                                F     F     F     F      T
F     F     F
+"_1"                                 F     F     F     F      T
F     F     F
+"!abc"                               F     F     F     F      F
F     F     F
+"ab!c"                               F     F     F     F      F
F     F     F
+"abc!"                               F     F     F     F      F
F     F     F
+"!123"                               F     F     F     F      F
F     F     F
+"12!3"                               F     F     F     F      F
F     F     F
+"123!"                               F     F     F     F      F
F     F     F
+"@"                                  F     F     F     F      F
F     F     F
+"["                                  F     F     F     F      F
F     F     F
+"`"                                  F     F     F     F      F
F     F     F
+"{"                                  F     F     F     F      F
F     F     F
+"/"                                  F     F     F     F      F
F     F     F
+":"                                  F     F     F     F      F
F     F     F
+"@A"                                 F     F     F     F      F
F     F     F
+"Z["                                 F     F     F     F      F
F     F     F
+"`a"                                 F     F     F     F      F
F     F     F
+"z{"                                 F     F     F     F      F
F     F     F
+"/0"                                 F     F     F     F      F
F     F     F
+"9:"                                 F     F     F     F      F
F     F     F
+" ab"                                F     F     F     F      F
F     F     F
+"ab "                                F     F     F     F      F
F     F     F
+"a b"                                F     F     F     F      F
F     F     F
+" 12"                                F     F     F     F      F
F     F     F
+"12 "                                F     F     F     F      F
F     F     F
+"1 2"                                F     F     F     F      F
F     F     F
+"\u00e9"                             F     F     F     F      F
F     F     T
+"\u00df"                             F     F     F     F      F
F     F     T
+"\u03be"                             F     F     F     F      F
F     F     T
+"\u5555"                             F     F     F     F      F
F     F     T
+"\U00010000"                         F     F     F     F      F
F     F     T
+"\u00b2"                             F     F     F     F      F
F     F     T
+"\u0660"                             F     F     F     F      F
F     F     T
+"\uff11"                             F     F     F     F      F
F     F     T
+"a\u00df\u03be\u5555\U00010000."     F     F     F     F      F
F     F     F
+"\u00df\u03be\u5555\U00010000"       F     F     F     F      F
F     F     T
+"abc\u00e9"                          F     F     F     F      F
F     F     F
+"\u00e9abc"                          F     F     F     F      F
F     F     F
+"\xf0\"                              F     F     F     F      F
F     F     F
+"\x9f\\x98\\x80\"                    F     F     F     F      F
F     F     F
+"abc\xf0\"                           F     F     F     F      F
F     F     F
+"\xf0\abc"                           F     F     F     F      F
F     F     F
+"ab\xf0\cd"                          F     F     F     F      F
F     F     F
+"123\x9f\\x98\\x80\"                 F     F     F     F      F
F     F     F
+"\U0001f600"                         F     F     F     F      F
F     F     T
diff --git a/tests/hard_coded/string_class.exp2
b/tests/hard_coded/string_class.exp2
new file mode 100644
index 000000000..ecff47f5a
--- /dev/null
+++ b/tests/hard_coded/string_class.exp2
@@ -0,0 +1,83 @@
+empty  = is_all_empty      alpha_ = is_all_alpha_or_underscore
+alpha  = is_all_alpha      alnum_ = is_all_alnum_or_underscore
+alnum  = is_all_alnum      match  = all_match(ascii_alpha)
+digit  = is_all_digits     nonasc = all_match(nonascii)
+
+alpha and match should agree on every well-formed string.
+
+string                               empty alpha alnum alpha_ alnum_
digit match nonasc
+""                                   T     T     T     T      T
T     T     T
+" "                                  F     F     F     F      F
F     F     F
+"  "                                 F     F     F     F      F
F     F     F
+"\t"                                 F     F     F     F      F
F     F     F
+"\n"                                 F     F     F     F      F
F     F     F
+"\r"                                 F     F     F     F      F
F     F     F
+" \t\n\r"                            F     F     F     F      F
F     F     F
+"\u00a0"                             F     F     F     F      F
F     F     T
+"\u2002"                             F     F     F     F      F
F     F     T
+"\u3000"                             F     F     F     F      F
F     F     T
+"\u200b"                             F     F     F     F      F
F     F     T
+"\ufeff"                             F     F     F     F      F
F     F     T
+"\u200e"                             F     F     F     F      F
F     F     T
+"\u0301"                             F     F     F     F      F
F     F     T
+"a"                                  F     T     T     T      T
F     T     F
+"Z"                                  F     T     T     T      T
F     T     F
+"0"                                  F     F     T     F      T
T     F     F
+"_"                                  F     F     F     T      T
F     F     F
+"."                                  F     F     F     F      F
F     F     F
+" "                                  F     F     F     F      F
F     F     F
+"ABCDEFGHIJKLMNOPQRSTUVWXYZ"         F     T     T     T      T
F     T     F
+"abcdefghijklmnopqrstuvwxyz"         F     T     T     T      T
F     T     F
+"0123456789"                         F     F     T     F      T
T     F     F
+"___"                                F     F     F     T      T
F     F     F
+"abc123"                             F     F     T     F      T
F     F     F
+"123abc"                             F     F     T     F      T
F     F     F
+"a1b2c3"                             F     F     T     F      T
F     F     F
+"abc_123"                            F     F     F     F      T
F     F     F
+"_abc"                               F     F     F     T      T
F     F     F
+"abc_"                               F     F     F     T      T
F     F     F
+"a_1"                                F     F     F     F      T
F     F     F
+"_1"                                 F     F     F     F      T
F     F     F
+"!abc"                               F     F     F     F      F
F     F     F
+"ab!c"                               F     F     F     F      F
F     F     F
+"abc!"                               F     F     F     F      F
F     F     F
+"!123"                               F     F     F     F      F
F     F     F
+"12!3"                               F     F     F     F      F
F     F     F
+"123!"                               F     F     F     F      F
F     F     F
+"@"                                  F     F     F     F      F
F     F     F
+"["                                  F     F     F     F      F
F     F     F
+"`"                                  F     F     F     F      F
F     F     F
+"{"                                  F     F     F     F      F
F     F     F
+"/"                                  F     F     F     F      F
F     F     F
+":"                                  F     F     F     F      F
F     F     F
+"@A"                                 F     F     F     F      F
F     F     F
+"Z["                                 F     F     F     F      F
F     F     F
+"`a"                                 F     F     F     F      F
F     F     F
+"z{"                                 F     F     F     F      F
F     F     F
+"/0"                                 F     F     F     F      F
F     F     F
+"9:"                                 F     F     F     F      F
F     F     F
+" ab"                                F     F     F     F      F
F     F     F
+"ab "                                F     F     F     F      F
F     F     F
+"a b"                                F     F     F     F      F
F     F     F
+" 12"                                F     F     F     F      F
F     F     F
+"12 "                                F     F     F     F      F
F     F     F
+"1 2"                                F     F     F     F      F
F     F     F
+"\u00e9"                             F     F     F     F      F
F     F     T
+"\u00df"                             F     F     F     F      F
F     F     T
+"\u03be"                             F     F     F     F      F
F     F     T
+"\u5555"                             F     F     F     F      F
F     F     T
+"\U00010000"                         F     F     F     F      F
F     F     T
+"\u00b2"                             F     F     F     F      F
F     F     T
+"\u0660"                             F     F     F     F      F
F     F     T
+"\uff11"                             F     F     F     F      F
F     F     T
+"a\u00df\u03be\u5555\U00010000."     F     F     F     F      F
F     F     F
+"\u00df\u03be\u5555\U00010000"       F     F     F     F      F
F     F     T
+"abc\u00e9"                          F     F     F     F      F
F     F     F
+"\u00e9abc"                          F     F     F     F      F
F     F     F
+"\ud83d"                             F     F     F     F      F
F     F     T
+"\ude00"                             F     F     F     F      F
F     F     T
+"abc\ud83d"                          F     F     F     F      F
F     F     F
+"\ud83dabc"                          F     F     F     F      F
F     F     F
+"ab\ud83dcd"                         F     F     F     F      F
F     F     F
+"123\ude00"                          F     F     F     F      F
F     F     F
+"\U0001f600"                         F     F     F     F      F
F     F     T
diff --git a/tests/hard_coded/string_class.m b/tests/hard_coded/string_class.m
index c98ea03a8..9e07c0750 100644
--- a/tests/hard_coded/string_class.m
+++ b/tests/hard_coded/string_class.m
@@ -1,6 +1,20 @@
 %---------------------------------------------------------------------------%
 % vim: ts=4 sw=4 et ft=mercury
 %---------------------------------------------------------------------------%
+%
+% Test the string predicates that test if every code point in a string
+% belongs to some class of characters.
+%
+% Each row of the output gives the result of every such predicate on one
+% test string.
+%
+% The .exp file is for backends using UTF-8 string encoding.
+% The .exp2 file is for backends using UTF-16 string encoding.
+% The two outputs differ only in the rows for ill-formed strings: on UTF-8
+% backends an ill-formed code unit is a lone byte, while on UTF-16 backends
+% it is an unpaired surrogate code point.
+%
+%---------------------------------------------------------------------------%

 :- module string_class.
 :- interface.
@@ -18,37 +32,71 @@
 :- import_module int.
 :- import_module list.
 :- import_module string.
+:- import_module uint8.

 %---------------------------------------------------------------------------%

 main(!IO) :-
-    test_is_all(is_all_alpha, "is_all_alpha", !IO),
-    test_is_all(is_all_alpha_or_underscore, "is_all_alpha_or_underscore", !IO),
-    test_is_all(is_all_alnum_or_underscore, "is_all_alnum_or_underscore", !IO),
-    test_is_all(is_all_digits, "is_all_digits", !IO),
-    test_is_all(all_match(nonascii), "all_match(nonascii) ", !IO).
-
-:- pred test_is_all(pred(string)::in(pred(in) is semidet), string::in,
-    io::di, io::uo) is det.
-
-test_is_all(Pred, Name, !IO) :-
-    test_is_all_2(Pred, Name, "", !IO),
-    test_is_all_2(Pred, Name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", !IO),
-    test_is_all_2(Pred, Name, "abcdefghijklmnopqrstuvwxyz", !IO),
-    test_is_all_2(Pred, Name, "0123456789", !IO),
-    test_is_all_2(Pred, Name, "_", !IO),
-    test_is_all_2(Pred, Name, "aΓŸΞΎε••π€€.", !IO),
-    test_is_all_2(Pred, Name, "ΓŸΞΎε••π€€", !IO),
-    io.nl(!IO).
-
-:- pred test_is_all_2(pred(string)::in(pred(in) is semidet), string::in,
-    string::in, io::di, io::uo) is det.
-
-test_is_all_2(Pred, Name, Chars, !IO) :-
-    ( if Pred(Chars) then
-        io.format("%s(""%s"")\n", [s(Name), s(Chars)], !IO)
-    else
-        io.format("not %s(""%s"")\n", [s(Name), s(Chars)], !IO)
+    io.write_string(legend, !IO),
+    write_row("string",
+        "empty", "alpha", "alnum", "alpha_", "alnum_", "digit",
+        "match", "nonasc", !IO),
+    list.foldl(test_string, test_strings, !IO).
+
+:- func legend = string.
+
+legend =
+    "empty  = is_all_empty      alpha_ = is_all_alpha_or_underscore\n" ++
+    "alpha  = is_all_alpha      alnum_ = is_all_alnum_or_underscore\n" ++
+    "alnum  = is_all_alnum      match  = all_match(ascii_alpha)\n" ++
+    "digit  = is_all_digits     nonasc = all_match(nonascii)\n" ++
+    "\n" ++
+    "alpha and match should agree on every well-formed string.\n" ++
+    "\n".
+
+%---------------------------------------------------------------------------%
+
+:- pred test_string(string::in, io::di, io::uo) is det.
+
+test_string(Str, !IO) :-
+    write_row(describe(Str),
+        tf(is_empty(Str)),
+        tf(is_all_alpha(Str)),
+        tf(is_all_alnum(Str)),
+        tf(is_all_alpha_or_underscore(Str)),
+        tf(is_all_alnum_or_underscore(Str)),
+        tf(is_all_digits(Str)),
+        tf(all_match(ascii_alpha, Str)),
+        tf(all_match(nonascii, Str)),
+        !IO).
+
+:- pred write_row(string::in, string::in, string::in, string::in, string::in,
+    string::in, string::in, string::in, string::in, io::di, io::uo) is det.
+
+write_row(Str, Empty, Alpha, Alnum, AlphaU, AlnumU, Digit, Match, NonAscii,
+        !IO) :-
+    io.format("%-36s %-5s %-5s %-5s %-6s %-6s %-5s %-5s %-6s\n",
+        [s(Str), s(Empty), s(Alpha), s(Alnum), s(AlphaU), s(AlnumU),
+        s(Digit), s(Match), s(NonAscii)], !IO).
+
+    % Call the given predicate, returning "T" if it succeeds and "F" if it
+    % fails.
+    %
+:- func tf((pred)::in((pred) is semidet)) = (string::out) is det.
+
+tf(Pred) = ( if Pred then "T" else "F" ).
+
+%---------------------------------------------------------------------------%
+
+    % A local definition of the class of characters that is_all_alpha is
+    % documented to accept.
+    %
+:- pred ascii_alpha(char::in) is semidet.
+
+ascii_alpha(Char) :-
+    char.to_int(Char, Int),
+    ( 0'A =< Int, Int =< 0'Z
+    ; 0'a =< Int, Int =< 0'z
     ).

 :- pred nonascii(char::in) is semidet.
@@ -56,3 +104,196 @@ test_is_all_2(Pred, Name, Chars, !IO) :-
 nonascii(Char) :-
     char.to_int(Char, Int),
     Int > 0x7f.
+
+%---------------------------------------------------------------------------%
+
+:- func test_strings = list(string).
+
+test_strings = [
+    % The empty string.
+    "",
+
+    % Strings that look empty, but are not.
+    " ",
+    "  ",
+    "\t",
+    "\n",
+    "\r",
+    " \t\n\r",
+    "\u00a0",       % no-break space
+    "\u2002",       % en space
+    "\u3000",       % ideographic space
+    "\u200b",       % zero width space
+    "\ufeff",       % zero width no-break space (BOM)
+    "\u200e",       % left-to-right mark
+    "\u0301",       % combining acute accent β€” a mark with no base
+
+    % One character from each relevant class.
+    "a",
+    "Z",
+    "0",
+    "_",
+    ".",
+    " ",
+
+    % Whole classes.
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+    "abcdefghijklmnopqrstuvwxyz",
+    "0123456789",
+    "___",
+
+    % Mixtures.
+    "abc123",
+    "123abc",
+    "a1b2c3",
+    "abc_123",
+    "_abc",
+    "abc_",
+    "a_1",
+    "_1",
+
+    % The nonmatching character first, in the middle, and last.
+    "!abc",
+    "ab!c",
+    "abc!",
+    "!123",
+    "12!3",
+    "123!",
+
+    % The characters immediately outside each accepted ASCII range:
+    % '@' is 'A'-1, '[' is 'Z'+1, '`' is 'a'-1, '{' is 'z'+1,
+    % '/' is '0'-1 and ':' is '9'+1.
+    "@",
+    "[",
+    "`",
+    "{",
+    "/",
+    ":",
+    "@A",
+    "Z[",
+    "`a",
+    "z{",
+    "/0",
+    "9:",
+
+    % Whitespace next to accepted characters.
+    " ab",
+    "ab ",
+    "a b",
+    " 12",
+    "12 ",
+    "1 2",
+
+    % Non-ASCII letters and digits. Every predicate here except the
+    % all_match(nonascii) column is documented to accept ASCII only,
+    % so all of these should fail.
+    "\u00e9",              % e with acute accent
+    "\u00df",              % sharp s
+    "\u03be",              % greek small letter xi
+    "\u5555",              % a CJK ideograph
+    "\U00010000",          % a non-BMP code point
+    "\u00b2",              % superscript two
+    "\u0660",              % arabic-indic digit zero
+    "\uff11",              % fullwidth digit one
+    "aΓŸΞΎε••π€€.",             % from the original version of this test
+    "ΓŸΞΎε••π€€",               % from the original verison of this test
+    "abc\u00e9",
+    "\u00e9abc",
+
+    % Ill-formed code unit sequences, alone and adjacent to characters that
+    % the predicates do accept.
+    ilseq_head,
+    ilseq_tail,
+    "abc" ++ ilseq_head,
+    ilseq_head ++ "abc",
+    "ab" ++ ilseq_head ++ "cd",
+    "123" ++ ilseq_tail,
+    ilseq_head ++ ilseq_tail
+].
+
+    % The first code unit of a code point whose encoding needs
+    % more than one code unit.
+    %
+:- func ilseq_head = string.
+
+ilseq_head = string.between(non_bmp, 0, 1).
+
+    % The remaining code units of that same code point above.
+    % Each is an ill-formed string on its own, on every backend.
+    %
+:- func ilseq_tail = string.
+
+ilseq_tail = string.between(non_bmp, 1, string.length(non_bmp)).
+
+:- func non_bmp = string.
+
+non_bmp = "\U0001F600".
+
+%---------------------------------------------------------------------------%
+
+:- func describe(string) = string.
+
+describe(Str) = Quoted :-
+    describe_loop(Str, 0, [], RevPieces),
+    Quoted = "\"" ++ string.append_list(list.reverse(RevPieces)) ++ "\"".
+
+:- pred describe_loop(string::in, int::in,
+    list(string)::in, list(string)::out) is det.
+
+describe_loop(Str, Index, !RevPieces) :-
+    ( if
+        string.index_next_repl(Str, Index, NextIndex, Char, MaybeReplaced)
+    then
+        (
+            MaybeReplaced = replaced_code_unit(CodeUnit),
+            Piece = string.format("\\x%02x\\",
+                [i(uint8.to_int(CodeUnit))])
+        ;
+            MaybeReplaced = not_replaced,
+            Piece = describe_char(Char)
+        ),
+        !:RevPieces = [Piece | !.RevPieces],
+        describe_loop(Str, NextIndex, !RevPieces)
+    else
+        true
+    ).
+
+:- func describe_char(char) = string.
+
+describe_char(Char) = Piece :-
+    char.to_int(Char, Int),
+    ( if
+        Char = ('\\')
+    then
+        Piece = "\\\\"
+    else if
+        Char = ('"')
+    then
+        Piece = "\\\""
+    else if
+        Char = '\t'
+    then
+        Piece = "\\t"
+    else if
+        Char = '\r'
+    then
+        Piece = "\\r"
+    else if
+        Char = '\n'
+    then
+        Piece = "\\n"
+    else if
+        0x20 =< Int, Int =< 0x7e
+    then
+        Piece = char_to_string(Char)
+    else if
+        Int =< 0xffff
+    then
+        Piece = string.format("\\u%04x", [i(Int)])
+    else
+        Piece = string.format("\\U%08x", [i(Int)])
+    ).
+
+%---------------------------------------------------------------------------%
+:- end_module string_class.
+%---------------------------------------------------------------------------%


More information about the reviews mailing list