feat(ctype): classification and case-mapping tables

This commit is contained in:
2026-09-03 20:03:07 -04:00
parent 858fc1762c
commit 20f514d91c
5 changed files with 691 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
#ifndef VLIBC_CTYPE_H
#define VLIBC_CTYPE_H
/*
* vlibc — <ctype.h>.
*
* Character classification and case mapping. Every function takes an int
* that represents an unsigned char value or EOF (-1). The implementation
* always casts the argument to unsigned char before any table lookup, so a
* negative char value can never index out of bounds; EOF classifies false
* in every is* function, and tolower/toupper pass it (and every non-letter)
* through unchanged. All results are for the "C" locale.
*
* Levels are cumulative (see include/vlibc/features.h):
* Level 1 (onlyposix): ISO C — the classification and case-mapping
* functions.
* Level 2 (muslmimic): XSI — isascii, toascii.
*
* This header includes <vlibc/features.h> itself, so the gates below always
* see the configured VLIBC_LEVEL even when the caller included no vlibc
* header first. The classification functions return exactly 1 when the
* character is in the class and 0 otherwise, never any other nonzero value.
*/
#include <vlibc/features.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Level 1: ISO C (always present). */
/*
* Nonzero when c is alphanumeric: a letter or a decimal digit.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isalnum(int c);
/*
* Nonzero when c is a letter.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isalpha(int c);
/*
* Nonzero when c is blank: space or horizontal tab.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isblank(int c);
/*
* Nonzero when c is a control character (0x00..0x1f or 0x7f).
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
iscntrl(int c);
/*
* Nonzero when c is a decimal digit.
* const: bit arithmetic on the argument alone (also matches GCC's const
* builtin declaration for isdigit).
*/
__attribute__((const)) int
isdigit(int c);
/*
* Nonzero when c has a visible representation (printable, except space).
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isgraph(int c);
/*
* Nonzero when c is a lowercase letter.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
islower(int c);
/*
* Nonzero when c is printable (0x20..0x7e).
* pure: the implementation is bit arithmetic on the argument alone, but
* GCC's isprint builtin is declared pure, and a const redeclaration would
* conflict with it.
*/
__attribute__((pure)) int
isprint(int c);
/*
* Nonzero when c is a punctuation character: printable and not alphanumeric
* and not space.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
ispunct(int c);
/*
* Nonzero when c is white space (space, form feed, newline, carriage
* return, horizontal or vertical tab).
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isspace(int c);
/*
* Nonzero when c is an uppercase letter.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
isupper(int c);
/*
* Nonzero when c is a hexadecimal digit (0-9, a-f, A-F).
* const: bit arithmetic on the argument alone (also matches GCC's const
* builtin declaration for isxdigit).
*/
__attribute__((const)) int
isxdigit(int c);
/*
* Return the lowercase counterpart of c when c is an uppercase letter
* ('A'..'Z' in the "C" locale); otherwise return c unchanged, including
* EOF.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
tolower(int c);
/*
* Return the uppercase counterpart of c when c is a lowercase letter
* ('a'..'z' in the "C" locale); otherwise return c unchanged, including
* EOF.
* pure: reads the classification table, no side effects.
*/
__attribute__((pure)) int
toupper(int c);
#if VLIBC_LEVEL_GE(2)
/* Level 2 (muslmimic): XSI. */
/*
* Nonzero when c is a 7-bit ASCII value (0..127). EOF and negative values
* are outside the range.
* const: depends only on its argument.
*/
__attribute__((const)) int
isascii(int c);
/*
* Clear every bit above the low 7 of c (c & 0x7f).
* const: depends only on its argument.
*/
__attribute__((const)) int
toascii(int c);
#endif /* VLIBC_LEVEL_GE(2) */
#ifdef __cplusplus
}
#endif
#endif /* VLIBC_CTYPE_H */
+172
View File
@@ -0,0 +1,172 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* The "C" locale character-class table: one entry per unsigned char value,
* holding the OR of the class bits below. The table is laid out as an
* explicit 8-entries-per-row grid so each byte can be audited against the
* ASCII ranges the standard specifies. The 0x80..0xff half is deliberately
* not written: a static initializer zero-fills the elements it does not
* mention, so every high byte — entry 255 included, which is where EOF
* lands after the unsigned char cast — classifies false. Because every
* lookup casts the argument to unsigned char first:
*
* - a negative char value can never index out of bounds;
* - EOF classifies false in every table-driven is* function without a
* special case;
* - tolower/toupper pass EOF (and every non-letter) through unchanged,
* returning the argument as given.
*
* isdigit, isxdigit and isprint are not table-driven: they are single
* unsigned comparisons on the argument (see below), which makes them
* genuinely const-capable — but only isdigit/isxdigit are declared const:
* GCC predeclares isdigit/isxdigit as const builtins and isprint as a pure
* builtin, and the header's attribute must match each builtin's declaration
* or the weaker one is rejected with a warning.
*/
#define CTYPE_UPPER 0x01U
#define CTYPE_LOWER 0x02U
#define CTYPE_DIGIT 0x04U
#define CTYPE_CNTRL 0x08U
#define CTYPE_PUNCT 0x10U
#define CTYPE_SPACE 0x20U
#define CTYPE_BLANK 0x80U
/* Combined masks, mirroring the standard's definitions. */
#define CTYPE_ALPHA (CTYPE_UPPER | CTYPE_LOWER)
#define CTYPE_ALNUM (CTYPE_ALPHA | CTYPE_DIGIT)
#define CTYPE_GRAPH (CTYPE_ALNUM | CTYPE_PUNCT)
static const unsigned char ctype_class[256] = {
/* 0 1 2 3 4 5 6 7 */
/* 0x00 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x08 */ 0x08, 0xa8, 0x28, 0x28, 0x28, 0x28, 0x08, 0x08,
/* 0x10 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x18 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x20 */ 0xa0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x28 */ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x30 */ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
/* 0x38 */ 0x04, 0x04, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x40 */ 0x10, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x48 */ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x50 */ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x58 */ 0x01, 0x01, 0x01, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x60 */ 0x10, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x68 */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x70 */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x78 */ 0x02, 0x02, 0x02, 0x10, 0x10, 0x10, 0x10, 0x08,
/* 0x80..0xff stay zero by the static initializer's default. */
};
int
isalnum(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_ALNUM) != 0;
}
int
isalpha(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_ALPHA) != 0;
}
int
isblank(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_BLANK) != 0;
}
int
iscntrl(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_CNTRL) != 0;
}
/*
* Pure arithmetic (no table): the unsigned cast turns every value below '0'
* into a huge number, so exactly 0..9 pass the range check.
*/
int
isdigit(int c)
{
return (unsigned int)c - '0' < 10U;
}
int
isgraph(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_GRAPH) != 0;
}
int
islower(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_LOWER) != 0;
}
/*
* Pure arithmetic (no table): the printable range 0x20..0x7e.
*/
int
isprint(int c)
{
return (unsigned int)c - 0x20 < 0x5fU;
}
int
ispunct(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_PUNCT) != 0;
}
int
isspace(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_SPACE) != 0;
}
int
isupper(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_UPPER) != 0;
}
/*
* Pure arithmetic (no table): c | 0x20 folds 'A'..'F' to 'a'..'f'; the
* unsigned cast first keeps every negative value out of the range check.
*/
int
isxdigit(int c)
{
return (unsigned int)c - '0' < 10U || (unsigned int)(c | 0x20) - 'a' < 6U;
}
/*
* Map an uppercase letter to lowercase and pass everything else — non-letters
* and EOF alike — through unchanged. The class bit is authoritative, so the
* unsigned char cast already keeps EOF and negative values out of the
* mapping branch.
*/
int
tolower(int c)
{
if ((ctype_class[(unsigned char)c] & CTYPE_UPPER) != 0)
{
return c + ('a' - 'A');
}
return c;
}
int
toupper(int c)
{
if ((ctype_class[(unsigned char)c] & CTYPE_LOWER) != 0)
{
return c - ('a' - 'A');
}
return c;
}
+15
View File
@@ -0,0 +1,15 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* True when c is a 7-bit ASCII value. EOF and every other negative value are
* outside the range, so they classify false without a cast.
*/
int
isascii(int c)
{
return c >= 0 && c < 128;
}
+15
View File
@@ -0,0 +1,15 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* Clear every bit above the low 7 of c. Pure bit arithmetic: defined for
* every int value, EOF included (toascii(-1) is 0x7f).
*/
int
toascii(int c)
{
return c & 0x7f;
}
+325
View File
@@ -0,0 +1,325 @@
/*
* vlibc — ctype.h classification + case mapping test (todo 9).
*
* Coverage:
*
* 1. classification sweep: every is* function, for all 256 unsigned char
* values AND EOF, must agree with the "C" locale ASCII ranges the
* standard specifies — the golden model below derives its expectation
* straight from those ranges, independent of the implementation's
* table;
* 2. case-mapping sweep: tolower/toupper over all 256 values + EOF (map
* A-Z/a-z, pass every non-letter through unchanged, EOF -> EOF);
* 3. negative-char sweep: every negative signed-char value passed as int
* must classify false in all is* functions, case-map to itself, and
* never fault — proving the unsigned char cast keeps negative values
* from indexing out of bounds (isalpha((char)0xe9) == 0 included);
* 4. spot checks from the todo acceptance list;
* 5. -f: the negative QA — isprint('\n') == 0, isalpha(EOF) == 0,
* tolower(EOF)/toupper(EOF) == EOF (a nonzero isprint('\n') is the
* defect).
*
* No host headers: <ctype.h> is vlibc's own (-Iinclude shadows the system
* one), and every diagnostic goes through the raw SYS_write helpers from
* <vlibc/internal/test.h>. Compiled with -DVLIBC_LEVEL=2 so both the L1
* functions and the L2 isascii/toascii gate are exercised (mirrors
* libvlibc-check.a, which builds the level-2 surface).
*
* Not part of the library proper; compiled manually for this todo (the
* tests/ + make check wiring is owned by a later todo).
*/
#include <ctype.h>
#include <vlibc/internal/test.h>
/* EOF as the standard defines it; ctype.h deliberately does not provide it. */
#define TEST_EOF (-1)
static int failures;
/* ---- Golden model: the "C" locale classes, as the standard defines them ---- */
enum
{
CLS_ALNUM,
CLS_ALPHA,
CLS_BLANK,
CLS_CNTRL,
CLS_DIGIT,
CLS_GRAPH,
CLS_LOWER,
CLS_PRINT,
CLS_PUNCT,
CLS_SPACE,
CLS_UPPER,
CLS_XDIGIT
};
typedef int (*ctype_pred)(int);
/* The real functions under test, indexed by the CLS_* enum above. */
static const ctype_pred cls_preds[] = {isalnum, isalpha, isblank, iscntrl, isdigit, isgraph,
islower, isprint, ispunct, isspace, isupper, isxdigit};
static const char *const cls_names[] = {"isalnum", "isalpha", "isblank", "iscntrl",
"isdigit", "isgraph", "islower", "isprint",
"ispunct", "isspace", "isupper", "isxdigit"};
static int
expect_class(int c, int cls) // NOLINT(bugprone-easily-swappable-parameters)
{
if (c < 0 || c > 127)
{
return 0;
}
// NOLINTBEGIN(bugprone-switch-missing-default-case)
switch (cls)
{
case CLS_UPPER:
return c >= 'A' && c <= 'Z';
case CLS_LOWER:
return c >= 'a' && c <= 'z';
case CLS_DIGIT:
return c >= '0' && c <= '9';
case CLS_ALPHA:
return expect_class(c, CLS_UPPER) || expect_class(c, CLS_LOWER);
case CLS_ALNUM:
return expect_class(c, CLS_ALPHA) || expect_class(c, CLS_DIGIT);
case CLS_XDIGIT:
return expect_class(c, CLS_DIGIT) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
case CLS_CNTRL:
return (c >= 0x00 && c <= 0x1f) || c == 0x7f;
case CLS_BLANK:
return c == ' ' || c == '\t';
case CLS_SPACE:
return c == ' ' || (c >= '\t' && c <= '\r');
case CLS_PUNCT:
return (c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') ||
(c >= '{' && c <= '~');
case CLS_GRAPH:
return expect_class(c, CLS_ALNUM) || expect_class(c, CLS_PUNCT);
case CLS_PRINT:
return expect_class(c, CLS_GRAPH) || c == ' ';
}
// NOLINTEND(bugprone-switch-missing-default-case)
return 0;
}
static int
expect_tolower(int c)
{
return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
}
static int
expect_toupper(int c)
{
return c >= 'a' && c <= 'z' ? c - ('a' - 'A') : c;
}
/* ---- Sweeps ---- */
/* 1. All 256 byte values + EOF against the golden model, per class. */
static void
classification_sweep(void)
{
size_t k;
for (k = 0; k < sizeof(cls_preds) / sizeof(cls_preds[0]); k++)
{
int c;
for (c = 0; c < 256; c++)
{
if ((cls_preds[k](c) != 0) == (expect_class(c, (int)k) != 0))
{
continue;
}
vlibc_test_say(2, "mismatch: ");
vlibc_test_say(2, cls_names[k]);
vlibc_test_say(2, "(0x");
vlibc_test_say_hex(2, (unsigned long long)c);
vlibc_test_say(2, ")\n");
failures++;
break;
}
if (cls_preds[k](TEST_EOF) != 0)
{
vlibc_test_say(2, "mismatch: ");
vlibc_test_say(2, cls_names[k]);
vlibc_test_say(2, "(EOF)\n");
failures++;
}
}
}
/* 2. Case mapping over all 256 byte values + EOF. */
static void
case_mapping_sweep(void)
{
int c;
for (c = 0; c < 256; c++)
{
if (tolower(c) != expect_tolower(c))
{
vlibc_test_say(2, "tolower mismatch at 0x");
vlibc_test_say_hex(2, (unsigned long long)c);
vlibc_test_say(2, "\n");
failures++;
}
if (toupper(c) != expect_toupper(c))
{
vlibc_test_say(2, "toupper mismatch at 0x");
vlibc_test_say_hex(2, (unsigned long long)c);
vlibc_test_say(2, "\n");
failures++;
}
}
if (tolower(TEST_EOF) != TEST_EOF || toupper(TEST_EOF) != TEST_EOF)
{
vlibc_test_say(2, "case mapping of EOF did not return EOF unchanged\n");
failures++;
}
}
/* 3. Every negative signed-char value: classify false, case-map to itself. */
static void
negative_char_sweep(void)
{
signed char sc;
size_t k;
for (sc = (signed char)-128; (int)sc < 0; sc++)
{
int c = (int)sc;
for (k = 0; k < sizeof(cls_preds) / sizeof(cls_preds[0]); k++)
{
if (cls_preds[k](c) != 0)
{
vlibc_test_say(2, "negative value classified nonzero by ");
vlibc_test_say(2, cls_names[k]);
vlibc_test_say(2, "\n");
failures++;
}
}
if (tolower(c) != c || toupper(c) != c)
{
vlibc_test_say(2, "negative value case-mapped instead of passing through\n");
failures++;
}
#if VLIBC_LEVEL_GE(2)
if (isascii(c) != 0)
{
vlibc_test_say(2, "negative value classified ASCII by isascii\n");
failures++;
}
if (toascii(c) != (c & 0x7f))
{
vlibc_test_say(2, "toascii mismatch on a negative value\n");
failures++;
}
#endif /* VLIBC_LEVEL_GE(2) */
}
}
/* 4. Acceptance-list spot checks. */
static void
spot_checks(void)
{
TEST_ASSERT_EQ(tolower('A'), 'a');
TEST_ASSERT_EQ(toupper('a'), 'A');
TEST_ASSERT_EQ(tolower('a'), 'a'); /* lowercase identity */
TEST_ASSERT_EQ(toupper('Z'), 'Z'); /* uppercase identity */
TEST_ASSERT_EQ(tolower('Z'), 'z');
TEST_ASSERT_EQ(toupper('z'), 'Z');
TEST_ASSERT_EQ(tolower('0'), '0'); /* digit identity */
TEST_ASSERT_TRUE(isupper('A'));
TEST_ASSERT_TRUE(islower('a'));
TEST_ASSERT_TRUE(isalpha(TEST_EOF) == 0);
TEST_ASSERT_TRUE(isprint('\n') == 0); /* negative QA: no false positive */
TEST_ASSERT_TRUE(isdigit('5'));
TEST_ASSERT_TRUE(!isalpha('5'));
TEST_ASSERT_TRUE(isspace(' '));
TEST_ASSERT_TRUE(isspace('\n'));
TEST_ASSERT_TRUE(ispunct('!'));
TEST_ASSERT_TRUE(!ispunct('A'));
TEST_ASSERT_TRUE(isgraph('~'));
TEST_ASSERT_TRUE(!isgraph(' '));
TEST_ASSERT_TRUE(isprint(' '));
TEST_ASSERT_TRUE(isblank('\t'));
TEST_ASSERT_TRUE(isblank(' '));
TEST_ASSERT_TRUE(!isblank('x'));
TEST_ASSERT_TRUE(iscntrl(0x7f));
TEST_ASSERT_TRUE(!iscntrl(' '));
TEST_ASSERT_TRUE(isxdigit('f'));
TEST_ASSERT_TRUE(isxdigit('F'));
TEST_ASSERT_TRUE(!isxdigit('g'));
TEST_ASSERT_TRUE(!isalnum('@'));
TEST_ASSERT_TRUE(isalnum('A'));
TEST_ASSERT_TRUE(isalpha((char)0xe9) == 0); /* negative char: (char)0xe9 == -23 */
#if VLIBC_LEVEL_GE(2)
TEST_ASSERT_TRUE(!isascii(0x80));
TEST_ASSERT_TRUE(isascii(0x00));
TEST_ASSERT_TRUE(isascii(0x7f));
TEST_ASSERT_TRUE(!isascii(TEST_EOF));
TEST_ASSERT_EQ(toascii('A'), 'A'); /* c & 0x7f, not a predicate */
TEST_ASSERT_EQ(toascii(0x80), 0);
TEST_ASSERT_EQ(toascii(TEST_EOF), 0x7f);
#endif /* VLIBC_LEVEL_GE(2) */
}
/*
* Failure scenario (-f): the negative QA from the todo. Exits 0 only when
* each defect is absent.
*/
static int
failure_scenario(void)
{
if (isprint('\n') != 0)
{
vlibc_test_say(1, "isprint('\\n') != 0 (false positive)\n");
return 1;
}
if (isalpha(TEST_EOF) != 0)
{
vlibc_test_say(1, "isalpha(EOF) != 0\n");
return 1;
}
if (tolower(TEST_EOF) != TEST_EOF || toupper(TEST_EOF) != TEST_EOF)
{
vlibc_test_say(1, "tolower/toupper(EOF) did not return EOF unchanged\n");
return 1;
}
vlibc_test_say(1, "negative QA: isprint('\\n') == 0, isalpha(EOF) == 0, "
"tolower/toupper(EOF) == EOF\n");
return 0;
}
int
main(int argc, char **argv)
{
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
return failure_scenario();
}
classification_sweep();
case_mapping_sweep();
negative_char_sweep();
spot_checks();
if (failures > 0 || vlibc_test_failures > 0)
{
vlibc_test_say(2, "FAILED (");
// NOLINTNEXTLINE(bugprone-misplaced-widening-cast)
vlibc_test_say_dec(2, (unsigned long)(failures + vlibc_test_failures));
vlibc_test_say(2, " check(s))\n");
return 1;
}
vlibc_test_say(1, "all ctype tests passed\n");
return 0;
}