diff --git a/include/ctype.h b/include/ctype.h new file mode 100644 index 0000000..65c8be9 --- /dev/null +++ b/include/ctype.h @@ -0,0 +1,164 @@ +#ifndef VLIBC_CTYPE_H +#define VLIBC_CTYPE_H + +/* + * vlibc — . + * + * 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 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 + +#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 */ diff --git a/src/ctype/ctype.c b/src/ctype/ctype.c new file mode 100644 index 0000000..c671a93 --- /dev/null +++ b/src/ctype/ctype.c @@ -0,0 +1,172 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * 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; +} diff --git a/src/ctype/isascii.c b/src/ctype/isascii.c new file mode 100644 index 0000000..06b5aac --- /dev/null +++ b/src/ctype/isascii.c @@ -0,0 +1,15 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * 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; +} diff --git a/src/ctype/toascii.c b/src/ctype/toascii.c new file mode 100644 index 0000000..c90379f --- /dev/null +++ b/src/ctype/toascii.c @@ -0,0 +1,15 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * 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; +} diff --git a/tests/test_ctype.c b/tests/test_ctype.c new file mode 100644 index 0000000..6fba627 --- /dev/null +++ b/tests/test_ctype.c @@ -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: is vlibc's own (-Iinclude shadows the system + * one), and every diagnostic goes through the raw SYS_write helpers from + * . 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 + +#include + +/* 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; +}