boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

libc.c (24406B)


      1 /* libc.c — slim libc closure for tcc-boot2.
      2  *
      3  * What's here: just enough libc for tcc.flat.c, the cc-libc tests, and
      4  * the boot4 hello smoke binary to link. Started from mes-libc 0.24.
      5  *
      6  * Provides:
      7  *   syscalls      _read _write _open3 close lseek brk unlink _exit
      8  *                 raise abort environ getenv __libc_init
      9  *                 ENOSYS stubs: access execve execvp fsync rmdir stat strtod
     10  *   I/O           stdin/stdout/stderr (FILE *), FILE = long alias for fd
     11  *                 fopen fdopen fclose fflush fseek ftell remove
     12  *                 fread fwrite fputs fputc fgetc puts strdup
     13  *                 fprintf printf snprintf sprintf
     14  *                 vfprintf vsnprintf vprintf vsprintf
     15  *   stdlib        malloc free realloc qsort exit atoi
     16  *                 strtol strtoul strtoll strtoull strtof
     17  *   string        strlen strcmp strcpy strncmp strncpy strchr strrchr
     18  *                 strstr strcat strdup memmem
     19  *   ctype         isdigit islower isnumber isspace isxdigit toupper
     20  *   assertions    __assert_fail
     21  *
     22  * External: memcpy memmove memset memcmp come from libp1pp / cc/mem.c.
     23  *           sys_read sys_write sys_open sys_close sys_lseek sys_brk
     24  *           sys_unlink sys_exit are P1pp.P1pp labels.
     25  *
     26  * Order constraints:
     27  *   1. The syscall + FILE-globals block has to come first — globals
     28  *      (errno, stdin/stdout/stderr, __brk, __FILEDES_MAX-sized arrays)
     29  *      are referenced by everything below.
     30  *   2. malloc must precede the file-IO helpers because __read_cache is
     31  *      lazy-malloc'd on first read.
     32  *
     33  * License: GPLv3+ (mes upstream); see LICENSE.
     34  */
     35 
     36 #include <fcntl.h>
     37 #include <errno.h>
     38 #include <stdio.h>
     39 #include <unistd.h>
     40 #include <sys/stat.h>
     41 #include <ctype.h>
     42 #include <string.h>
     43 #include <stdlib.h>
     44 #include <limits.h>
     45 #include <stdarg.h>
     46 #include <stddef.h>
     47 #include <stdint.h>
     48 
     49 #define __FILEDES_MAX 4096
     50 
     51 /* ───────── globals ───────────────────────────────────────────────── */
     52 
     53 /* Our sysinclude stdio.h declares stdin/stdout/stderr as extern FILE*
     54    (not macros). The #undefs below are no-ops kept for compatibility. */
     55 #undef stdin
     56 #undef stdout
     57 #undef stderr
     58 FILE *stdin  = (FILE *) 0;
     59 FILE *stdout = (FILE *) 1;
     60 FILE *stderr = (FILE *) 2;
     61 
     62 int errno;
     63 char *__brk = 0;
     64 char **environ;
     65 
     66 
     67 /* ───────── P1pp syscall labels and thin wrappers ─────────────────── */
     68 
     69 /* forward decl: defined in the stdio block, but _open3/close need it. */
     70 static size_t read_cache_clear_ (int fd);
     71 
     72 extern long sys_read   (long fd, long buf, long n);
     73 extern long sys_write  (long fd, long buf, long n);
     74 extern long sys_open   (long path, long flags, long mode);
     75 extern long sys_close  (long fd);
     76 extern long sys_lseek  (long fd, long off, long whence);
     77 extern long sys_brk    (long addr);
     78 extern long sys_unlink (long path);
     79 extern long sys_exit   (long code);
     80 
     81 ssize_t
     82 _read (int fd, void *buf, size_t n)
     83 {
     84   return sys_read (fd, (long) buf, (long) n);
     85 }
     86 
     87 ssize_t
     88 _write (int fd, void const *buf, size_t n)
     89 {
     90   return sys_write (fd, (long) buf, (long) n);
     91 }
     92 
     93 int
     94 _open3 (char const *path, int flags, int mode)
     95 {
     96   long r = sys_open ((long) path, flags, mode);
     97   if (r >= __FILEDES_MAX) {
     98     errno = EMFILE;
     99     return -1;
    100   }
    101   /* fd numbers are kernel-recycled; if we hand out a fd whose previous
    102      life left bytes in the per-fd read buffer, the next read on the
    103      new file picks them up and tcc_object_type sees a corrupted ELF
    104      header. Mes's _open3 clears the cache for the same reason. */
    105   if (r > 2)
    106     read_cache_clear_ (r);
    107   return (int) r;
    108 }
    109 
    110 int
    111 close (int fd)
    112 {
    113   long r = sys_close (fd);
    114   if (r < 0) {
    115     errno = -r;
    116     return -1;
    117   }
    118   errno = 0;
    119   return (int) r;
    120 }
    121 
    122 /* mes's lseek drains a per-fd read buffer before letting the kernel
    123    see the seek; we replicate that in fseek directly (it owns the
    124    buffer state), so this layer is just the raw syscall. */
    125 off_t
    126 _lseek (int fd, off_t off, int whence)
    127 {
    128   return sys_lseek (fd, off, whence);
    129 }
    130 
    131 long
    132 brk (void *addr)
    133 {
    134   return sys_brk ((long) addr);
    135 }
    136 
    137 int
    138 unlink (char const *path)
    139 {
    140   long r = sys_unlink ((long) path);
    141   if (r < 0) {
    142     errno = -r;
    143     return -1;
    144   }
    145   errno = 0;
    146   return 0;
    147 }
    148 
    149 void
    150 _exit (int status)
    151 {
    152   sys_exit (status);
    153 }
    154 
    155 /* abort() (mes's version) calls raise(SIGABRT). We don't ship signals
    156    so just exit; tcc's tcc_error path goes here on hard failures. */
    157 int
    158 raise (int sig)
    159 {
    160   sys_exit (128 + sig);
    161   return -1;
    162 }
    163 
    164 void
    165 abort (void)
    166 {
    167   sys_exit (128 + 6);
    168 }
    169 
    170 void
    171 __libc_init (int argc, char **argv)
    172 {
    173   /* Linux exec entry stack: argc, argv[0..argc-1], NULL, envp[..], NULL.
    174      :_start passes (argc, argv); environ follows argv's NULL.
    175      Without it, getenv() dereferences NULL on first call. */
    176   char **p;
    177   (void) argc;
    178   p = argv;
    179   while (*p)
    180     p++;
    181   environ = p + 1;
    182 }
    183 
    184 /* Stubs for libc-internal references not exercised by the tcc-boot2
    185    golden path (fread/fopen/etc. include <sys/stat.h>; the closure
    186    we pull in references these). If any surfaces in a real workload,
    187    replace with a P1pp label + thin wrapper. */
    188 char *
    189 _getcwd (char *buf, size_t size)
    190 {
    191   (void) buf; (void) size; errno = ENOSYS; return 0;
    192 }
    193 
    194 int
    195 access (char const *path, int mode)
    196 {
    197   (void) path; (void) mode; errno = ENOSYS; return -1;
    198 }
    199 
    200 int
    201 execve (char const *path, char *const argv[], char *const envp[])
    202 {
    203   (void) path; (void) argv; (void) envp; errno = ENOSYS; return -1;
    204 }
    205 
    206 int
    207 execvp (char const *file, char *const argv[])
    208 {
    209   (void) file; (void) argv; errno = ENOSYS; return -1;
    210 }
    211 
    212 int
    213 fsync (int fd)
    214 {
    215   (void) fd; return 0;
    216 }
    217 
    218 int
    219 rmdir (char const *path)
    220 {
    221   (void) path; errno = ENOSYS; return -1;
    222 }
    223 
    224 int
    225 stat (char const *path, struct stat *buf)
    226 {
    227   (void) path; (void) buf; errno = ENOSYS; return -1;
    228 }
    229 
    230 double
    231 strtod (char const *s, char **tail)
    232 {
    233   /* tcc.flat.c never reaches strtod under our defines (HAVE_FLOAT off);
    234      stdlib/strtof.c only forwards. cc.scm rejects FP literals, so we
    235      cast a zero int — same numeric value, no `0.0` token. */
    236   if (tail)
    237     *tail = (char *) s;
    238   return (double) 0;
    239 }
    240 
    241 
    242 /* ───────── ctype ────────────────────────────────────────────────── */
    243 
    244 int isdigit  (int c) { return c >= '0' && c <= '9'; }
    245 int islower  (int c) { return c >= 'a' && c <= 'z'; }
    246 int isspace  (int c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
    247 int isxdigit (int c) { return isdigit (c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
    248 int toupper  (int c) { return islower (c) ? c - 'a' + 'A' : c; }
    249 
    250 int
    251 isnumber (int c, int base)
    252 {
    253   if (base <= 10)
    254     return c >= '0' && c < '0' + base;
    255   return isdigit (c)
    256     || (c >= 'a' && c < 'a' + base - 10)
    257     || (c >= 'A' && c < 'A' + base - 10);
    258 }
    259 
    260 
    261 /* ───────── string ───────────────────────────────────────────────── */
    262 
    263 size_t
    264 strlen (char const *s)
    265 {
    266   size_t n = 0;
    267   while (s[n])
    268     n++;
    269   return n;
    270 }
    271 
    272 int
    273 strcmp (char const *a, char const *b)
    274 {
    275   while (*a && *a == *b) { a++; b++; }
    276   return *(unsigned char *) a - *(unsigned char *) b;
    277 }
    278 
    279 int
    280 strncmp (char const *a, char const *b, size_t n)
    281 {
    282   while (n && *a && *a == *b) { a++; b++; n--; }
    283   if (!n) return 0;
    284   return *(unsigned char *) a - *(unsigned char *) b;
    285 }
    286 
    287 char *
    288 strcpy (char *dst, char const *src)
    289 {
    290   char *r = dst;
    291   while ((*dst++ = *src++)) ;
    292   return r;
    293 }
    294 
    295 char *
    296 strncpy (char *dst, char const *src, size_t n)
    297 {
    298   char *r = dst;
    299   while (n && (*dst++ = *src++)) n--;
    300   while (n--) *dst++ = 0;
    301   return r;
    302 }
    303 
    304 char *
    305 strcat (char *dst, char const *src)
    306 {
    307   char *r = dst;
    308   while (*dst) dst++;
    309   while ((*dst++ = *src++)) ;
    310   return r;
    311 }
    312 
    313 char *
    314 strchr (char const *s, int c)
    315 {
    316   for (; *s; s++)
    317     if (*s == (char) c)
    318       return (char *) s;
    319   return c ? 0 : (char *) s;
    320 }
    321 
    322 char *
    323 strrchr (char const *s, int c)
    324 {
    325   char const *last = 0;
    326   for (; *s; s++)
    327     if (*s == (char) c)
    328       last = s;
    329   if (!c) return (char *) s;
    330   return (char *) last;
    331 }
    332 
    333 char *
    334 strstr (char const *hay, char const *needle)
    335 {
    336   size_t n = strlen (needle);
    337   if (!n) return (char *) hay;
    338   for (; *hay; hay++)
    339     if (!strncmp (hay, needle, n))
    340       return (char *) hay;
    341   return 0;
    342 }
    343 
    344 void *
    345 memmem (void const *hay, int haylen, void const *needle, int needlelen)
    346 {
    347   char const *h = hay;
    348   char const *n = needle;
    349   int i;
    350   if (needlelen <= 0) return (void *) hay;
    351   if (needlelen > haylen) return 0;
    352   for (i = 0; i + needlelen <= haylen; i++) {
    353     int j = 0;
    354     while (j < needlelen && h[i + j] == n[j])
    355       j++;
    356     if (j == needlelen)
    357       return (char *) h + i;
    358   }
    359   return 0;
    360 }
    361 
    362 char *
    363 strdup (char const *s)
    364 {
    365   size_t n = strlen (s) + 1;
    366   char *p = malloc (n);
    367   if (p) memcpy (p, s, n);
    368   return p;
    369 }
    370 
    371 
    372 /* ───────── malloc — bump allocator on top of brk ─────────────────── */
    373 
    374 void *
    375 malloc (size_t size)
    376 {
    377   long want;
    378   char *p;
    379   if (!__brk)
    380     __brk = (char *) brk (0);
    381   /* 16-byte align — matches the largest scalar (long long / pointer / double). */
    382   __brk = (char *) (((uintptr_t) __brk + 16 - 1) & -16);
    383   /* Linux brk(2) returns the (unchanged) break on failure rather than
    384      -1, so compare against the requested address — a refused growth
    385      surfaces here as malloc returning NULL. */
    386   want = (long) (__brk + size);
    387   if (brk (__brk + size) < want)
    388     return 0;
    389   p = __brk;
    390   __brk += size;
    391   return p;
    392 }
    393 
    394 void
    395 free (void *p)
    396 {
    397   (void) p; /* bump allocator; reclamation is not implemented. */
    398 }
    399 
    400 void *
    401 realloc (void *p, size_t size)
    402 {
    403   void *q = malloc (size);
    404   if (p && q)
    405     memcpy (q, p, size);
    406   return q;
    407 }
    408 
    409 
    410 /* ───────── stdlib: integer parsing and exit ──────────────────────── */
    411 
    412 /* Internal parser shared by atoi / strtol / strtoul / strtoull and the
    413    width/precision parser inside vformat_. `long long` accumulator so
    414    values that don't fit in 32-bit signed don't sign-extend through the
    415    parse — tcc3 needs this for `-Wl,-Ttext=0x80200000` and the like. */
    416 static long long
    417 parse_int_ (char const **p, int base)
    418 {
    419   char const *s = *p;
    420   long long n = 0;
    421   int neg = 0;
    422   if (base == 0) base = 10;
    423   while (isspace (*s)) s++;
    424   if (*s == '+') s++;
    425   else if (*s == '-') { neg = 1; s++; }
    426   while (isnumber (*s, base)) {
    427     int d;
    428     if      (*s >= 'a') d = *s - 'a' + 10;
    429     else if (*s >= 'A') d = *s - 'A' + 10;
    430     else                d = *s - '0';
    431     n = n * base + d;
    432     s++;
    433   }
    434   *p = s;
    435   return neg ? -n : n;
    436 }
    437 
    438 int
    439 atoi (char const *s)
    440 {
    441   return (int) parse_int_ (&s, 10);
    442 }
    443 
    444 long
    445 strtol (char const *s, char **tail, int base)
    446 {
    447   char const *p;
    448   long r;
    449   if (!strncmp (s, "0x", 2)) { s += 2; base = 16; }
    450   p = s;
    451   r = (long) parse_int_ (&p, base);
    452   if (tail) *tail = (char *) p;
    453   return r;
    454 }
    455 
    456 unsigned long      strtoul  (char const *s, char **tail, int base) { return strtol (s, tail, base); }
    457 long long          strtoll  (char const *s, char **tail, int base) { return strtol (s, tail, base); }
    458 unsigned long long strtoull (char const *s, char **tail, int base) { return strtol (s, tail, base); }
    459 float              strtof   (char const *s, char **tail)           { return strtod (s, tail); }
    460 
    461 void (*__call_at_exit) (void);
    462 
    463 void
    464 exit (int status)
    465 {
    466   if (__call_at_exit)
    467     __call_at_exit ();
    468   _exit (status);
    469   /* not reached */
    470   while (1) ;
    471 }
    472 
    473 /* qsort: lifted from mes — straightforward Lomuto-partition recursion. */
    474 static void
    475 qswap_ (char *a, char *b, size_t size)
    476 {
    477   do {
    478     char t = *a;
    479     *a++ = *b;
    480     *b++ = t;
    481   } while (--size);
    482 }
    483 
    484 static size_t
    485 qpart_ (char *base, size_t count, size_t size, int (*cmp) (void *, void *))
    486 {
    487   char *pivot = base + count * size;
    488   size_t i = 0;
    489   size_t j;
    490   int c;
    491   for (j = 0; j < count; j++) {
    492     c = cmp (base + j * size, pivot);
    493     if (c < 0) {
    494       qswap_ (base + i * size, base + j * size, size);
    495       i++;
    496     } else if (c == 0) {
    497       i++;
    498     }
    499   }
    500   if (cmp (pivot, base + i * size) < 0)
    501     qswap_ (base + i * size, pivot, size);
    502   return i;
    503 }
    504 
    505 void
    506 qsort (void *base, size_t count, size_t size, int (*cmp) (void *, void *))
    507 {
    508   size_t p;
    509   if (count > 1) {
    510     p = qpart_ (base, count - 1, size, cmp);
    511     qsort (base, p, size, cmp);
    512     qsort ((char *) base + p * size, count - p, size, cmp);
    513   }
    514 }
    515 
    516 
    517 /* ───────── getenv ────────────────────────────────────────────────── */
    518 
    519 char *
    520 getenv (char const *name)
    521 {
    522   size_t n = strlen (name);
    523   char **p;
    524   for (p = environ; p && *p; p++) {
    525     if (!strncmp (*p, name, n) && (*p)[n] == '=')
    526       return *p + n + 1;
    527   }
    528   return 0;
    529 }
    530 
    531 
    532 /* ───────── stdio: FILE * = (long) fd ─────────────────────────────── */
    533 
    534 /* Block-buffered read, per fd. Without it, tcc lexing tcc.flat.c does
    535    ~500K single-byte syscalls; with a 128-byte cache it's ~4K. */
    536 #define READ_BUFFER_MAX 128
    537 
    538 struct read_buffer {
    539   ssize_t size;
    540   char    bytes[READ_BUFFER_MAX];
    541 };
    542 static struct read_buffer *read_cache;
    543 
    544 static void
    545 read_cache_init_ (void)
    546 {
    547   if (!read_cache)
    548     read_cache = malloc (sizeof (struct read_buffer) * __FILEDES_MAX);
    549 }
    550 
    551 /* Returns how many buffered bytes were dropped — fseek/lseek need that
    552    to back out the unconsumed portion. */
    553 static size_t
    554 read_cache_clear_ (int fd)
    555 {
    556   size_t n;
    557   read_cache_init_ ();
    558   n = read_cache[fd].size;
    559   read_cache[fd].size = 0;
    560   return n;
    561 }
    562 
    563 static ssize_t
    564 read_buffered_ (int fd, void *buf, size_t size)
    565 {
    566   struct read_buffer *c;
    567   char *p = buf;
    568   size_t todo = size;
    569   read_cache_init_ ();
    570   c = &read_cache[fd];
    571   if (!c->size && size > READ_BUFFER_MAX)
    572     return _read (fd, buf, size);
    573   while (c->size > 0 && todo) {
    574     todo--;
    575     *p++ = c->bytes[READ_BUFFER_MAX - c->size--];
    576   }
    577   if (todo) {
    578     ssize_t got;
    579     if (todo > READ_BUFFER_MAX)
    580       return size - todo + _read (fd, p, todo);
    581     got = _read (fd, c->bytes, READ_BUFFER_MAX);
    582     if (got < 0) return -1;
    583     if (got > 0) {
    584       c->size = got;
    585       if (got < READ_BUFFER_MAX)
    586         memmove (c->bytes + READ_BUFFER_MAX - got, c->bytes, got);
    587       return size - todo + read_buffered_ (fd, p, todo);
    588     }
    589   }
    590   return size - todo;
    591 }
    592 
    593 ssize_t
    594 read (int fd, void *buf, size_t size)
    595 {
    596   return read_buffered_ (fd, buf, size);
    597 }
    598 
    599 ssize_t
    600 write (int fd, void const *buf, size_t size)
    601 {
    602   size_t skip = read_cache_clear_ (fd);
    603   ssize_t r;
    604   if (skip)
    605     sys_lseek (fd, -(long) skip, SEEK_CUR);
    606   r = _write (fd, buf, size);
    607   if (r < 0) {
    608     errno = -r;
    609     return -1;
    610   }
    611   errno = 0;
    612   return r;
    613 }
    614 
    615 off_t
    616 lseek (int fd, off_t off, int whence)
    617 {
    618   size_t skip = read_cache_clear_ (fd);
    619   if (whence == SEEK_CUR)
    620     off -= skip;
    621   return sys_lseek (fd, off, whence);
    622 }
    623 
    624 int
    625 open (char const *path, int flags, ...)
    626 {
    627   int mode = 0;
    628   if (flags & O_CREAT) {
    629     va_list ap;
    630     va_start (ap, flags);
    631     mode = va_arg (ap, int);
    632     va_end (ap);
    633   }
    634   return _open3 (path, flags, mode);
    635 }
    636 
    637 FILE *
    638 fopen (char const *path, char const *opentype)
    639 {
    640   int fd;
    641   int mode = 0600;
    642   if ((opentype[0] == 'a' || !strcmp (opentype, "r+")) && !access (path, O_RDONLY)) {
    643     int flags = O_RDWR;
    644     if (opentype[0] == 'a') flags |= O_APPEND;
    645     fd = _open3 (path, flags, mode);
    646   } else if (opentype[0] == 'w' || opentype[0] == 'a' || !strcmp (opentype, "r+")) {
    647     int flags = strchr (opentype, '+') ? O_RDWR | O_CREAT : O_WRONLY | O_CREAT | O_TRUNC;
    648     fd = _open3 (path, flags, mode);
    649   } else {
    650     fd = _open3 (path, O_RDONLY, 0);
    651   }
    652   if (fd < 0) fd = 0;
    653   return (FILE *) (long) fd;
    654 }
    655 
    656 FILE *fdopen (int fd, char const *mode) { (void) mode; return (FILE *) (long) fd; }
    657 
    658 int fclose (FILE * f)                          { return close ((long) f); }
    659 int fflush (FILE * f)                          { int fd = (long) f; return fd < 3 ? 0 : fsync (fd); }
    660 
    661 long ftell (FILE * f)                          { return lseek ((long) f, 0, SEEK_CUR); }
    662 
    663 int
    664 fseek (FILE * f, long off, int whence)
    665 {
    666   return lseek ((long) f, off, whence) >= 0 ? 0 : -1;
    667 }
    668 
    669 int
    670 fgetc (FILE * f)
    671 {
    672   unsigned char c;
    673   if (read_buffered_ ((long) f, &c, 1) != 1)
    674     return -1;
    675   return c;
    676 }
    677 
    678 size_t
    679 fread (void *data, size_t size, size_t count, FILE * f)
    680 {
    681   ssize_t got;
    682   if (!size || !count) return 0;
    683   got = read_buffered_ ((long) f, data, size * count);
    684   return got > 0 ? got / size : 0;
    685 }
    686 
    687 size_t
    688 fwrite (void const *data, size_t size, size_t count, FILE * f)
    689 {
    690   ssize_t put;
    691   if (!size || !count) return 0;
    692   put = write ((long) f, data, size * count);
    693   return put > 0 ? put / size : 0;
    694 }
    695 
    696 int
    697 fputc (int c, FILE * f)
    698 {
    699   unsigned char b = c;
    700   return write ((long) f, &b, 1) == 1 ? c : -1;
    701 }
    702 
    703 int
    704 fputs (char const *s, FILE * f)
    705 {
    706   size_t n = strlen (s);
    707   return (size_t) write ((long) f, s, n) == n ? (int) n : -1;
    708 }
    709 
    710 int
    711 puts (char const *s)
    712 {
    713   if (fputs (s, stdout) < 0) return -1;
    714   return fputc ('\n', stdout);
    715 }
    716 
    717 int
    718 remove (char const *path)
    719 {
    720   return unlink (path);
    721 }
    722 
    723 
    724 /* ───────── printf family ─────────────────────────────────────────── */
    725 
    726 /* One sink type for both fd-backed (fprintf/printf) and buffer-backed
    727    (snprintf/sprintf) output. emit_() drops bytes once a buffer sink
    728    is full; vsnprintf still has to count them so the C99 return value
    729    (bytes that would have been written) is correct. */
    730 struct sink {
    731   int    fd;       /* -1 if buffer sink */
    732   char  *buf;
    733   size_t cap;      /* SIZE_MAX for unbounded sprintf */
    734   size_t len;      /* bytes counted (may exceed cap) */
    735 };
    736 
    737 static void
    738 emit_ (struct sink *s, char c)
    739 {
    740   if (s->fd >= 0) {
    741     sys_write (s->fd, (long) &c, 1);
    742   } else if (s->len < s->cap) {
    743     s->buf[s->len] = c;
    744   }
    745   s->len++;
    746 }
    747 
    748 static void
    749 emit_str_ (struct sink *s, char const *p, size_t n)
    750 {
    751   while (n--) emit_ (s, *p++);
    752 }
    753 
    754 /* unsigned 64-bit → ASCII into `out` (at least 24 bytes). Returns
    755    pointer to first character; signed prefix is added by caller. */
    756 static char *
    757 utoa_ (unsigned long long u, unsigned base, char *out)
    758 {
    759   char *p = out + 23;
    760   *p = 0;
    761   do {
    762     unsigned d = u % base;
    763     u /= base;
    764     *--p = d > 9 ? 'a' + d - 10 : '0' + d;
    765   } while (u);
    766   return p;
    767 }
    768 
    769 static int
    770 vformat_ (struct sink *s, char const *fmt, va_list ap)
    771 {
    772   size_t start = s->len;
    773   while (*fmt) {
    774     int  left, long_p, precision, width;
    775     char pad;
    776     char c;
    777     char nbuf[24];
    778     char const *body;
    779     size_t bodylen;
    780     char sign;
    781 
    782     if (*fmt != '%') { emit_ (s, *fmt++); continue; }
    783     fmt++;
    784 
    785     left = 0; long_p = 0; precision = -1; width = -1;
    786     pad = ' ';
    787     body = 0;
    788     bodylen = 0;
    789     sign = 0;
    790 
    791     if (*fmt == '-') { left = 1; fmt++; }
    792     if (*fmt == ' ') { pad  = ' '; fmt++; }
    793     if (*fmt == '0') { pad  = '0'; fmt++; }
    794     if (*fmt >= '0' && *fmt <= '9') {
    795       width = (int) parse_int_ (&fmt, 10);
    796     } else if (*fmt == '*') {
    797       width = va_arg (ap, int);
    798       fmt++;
    799     }
    800     if (*fmt == '.') {
    801       fmt++;
    802       if (*fmt >= '0' && *fmt <= '9') {
    803         precision = (int) parse_int_ (&fmt, 10);
    804       } else if (*fmt == '*') {
    805         precision = va_arg (ap, int);
    806         fmt++;
    807       }
    808     }
    809     /* `l` length modifier; LP64 → ll == l, so collapse. Tracking it
    810        matters: amd64 SysV spills an `int` arg into the low 32 bits of
    811        an 8-byte slot, leaving the upper bits unspecified. va_arg(long)
    812        on a `%d` would then leak garbage. */
    813     if (*fmt == 'l') { long_p = 1; fmt++; }
    814     if (*fmt == 'l') fmt++;
    815 
    816     c = *fmt;
    817 
    818     switch (c) {
    819     case '%': emit_ (s, '%'); break;
    820     case 'c': emit_ (s, (char) va_arg (ap, int)); break;
    821 
    822     case 's': {
    823       body = va_arg (ap, char const *);
    824       if (!body) body = "(null)";
    825       bodylen = strlen (body);
    826       if (precision >= 0 && (size_t) precision < bodylen)
    827         bodylen = precision;
    828       goto pad_emit;
    829     }
    830 
    831     case 'd': case 'i': {
    832       long long v = long_p ? va_arg (ap, long long) : (long long) va_arg (ap, int);
    833       unsigned long long u = v < 0 ? -(unsigned long long) v : (unsigned long long) v;
    834       if (v < 0) sign = '-';
    835       body = utoa_ (u, 10, nbuf);
    836       bodylen = strlen (body);
    837       goto numeric_emit;
    838     }
    839 
    840     case 'u': case 'o': case 'x': case 'X': case 'p': {
    841       unsigned long long u;
    842       unsigned base = c == 'o' ? 8 : (c == 'x' || c == 'X' || c == 'p') ? 16 : 10;
    843       if (c == 'p') {
    844         u = (unsigned long long) (uintptr_t) va_arg (ap, void *);
    845         long_p = 1;
    846       } else if (long_p) {
    847         u = (unsigned long long) va_arg (ap, unsigned long long);
    848       } else {
    849         u = (unsigned long long) va_arg (ap, unsigned int);
    850       }
    851       body = utoa_ (u, base, nbuf);
    852       bodylen = strlen (body);
    853       if (c == 'X') {
    854         char *t;
    855         for (t = (char *) body; *t; t++)
    856           *t = toupper (*t);
    857       }
    858       goto numeric_emit;
    859     }
    860 
    861     default:
    862       /* Unsupported specifier — emit %X for diagnostics. tcc's only
    863          use of an exotic specifier is %p in an unreachable error
    864          path. */
    865       emit_ (s, '%');
    866       emit_ (s, c);
    867       break;
    868     }
    869     fmt++;
    870     continue;
    871 
    872   numeric_emit: {
    873       int total = (sign ? 1 : 0) + (int) bodylen;
    874       int npad;
    875       int zpad;
    876       if (precision < 0) precision = (int) bodylen;
    877       npad = width > total ? width - (precision > (int) bodylen ? precision : total) : 0;
    878       zpad = precision > (int) bodylen ? precision - (int) bodylen : 0;
    879       if (!left && pad == ' ') {
    880         while (npad-- > 0) emit_ (s, ' ');
    881       }
    882       if (sign) emit_ (s, sign);
    883       if (!left && pad == '0') {
    884         while (npad-- > 0) emit_ (s, '0');
    885       }
    886       while (zpad-- > 0) emit_ (s, '0');
    887       emit_str_ (s, body, bodylen);
    888       while (left && npad-- > 0) emit_ (s, ' ');
    889       fmt++;
    890       continue;
    891     }
    892 
    893   pad_emit: {
    894       int npad = width > (int) bodylen ? width - (int) bodylen : 0;
    895       if (!left) while (npad-- > 0) emit_ (s, pad);
    896       emit_str_ (s, body, bodylen);
    897       if (left) while (npad-- > 0) emit_ (s, pad);
    898       fmt++;
    899       continue;
    900     }
    901   }
    902   if (s->fd < 0 && s->len < s->cap)
    903     s->buf[s->len] = 0;
    904   else if (s->fd < 0 && s->cap)
    905     s->buf[s->cap - 1] = 0;
    906   return (int) (s->len - start);
    907 }
    908 
    909 int
    910 vfprintf (FILE * f, char const *fmt, va_list ap)
    911 {
    912   struct sink s = { .fd = (int) (long) f, .buf = 0, .cap = 0, .len = 0 };
    913   return vformat_ (&s, fmt, ap);
    914 }
    915 
    916 int
    917 vprintf (char const *fmt, va_list ap)
    918 {
    919   return vfprintf (stdout, fmt, ap);
    920 }
    921 
    922 int
    923 vsnprintf (char *str, size_t size, char const *fmt, va_list ap)
    924 {
    925   struct sink s = { .fd = -1, .buf = str, .cap = size, .len = 0 };
    926   return vformat_ (&s, fmt, ap);
    927 }
    928 
    929 int
    930 vsprintf (char *str, char const *fmt, va_list ap)
    931 {
    932   return vsnprintf (str, (size_t) -1, fmt, ap);
    933 }
    934 
    935 int
    936 fprintf (FILE * f, char const *fmt, ...)
    937 {
    938   va_list ap;
    939   int r;
    940   va_start (ap, fmt);
    941   r = vfprintf (f, fmt, ap);
    942   va_end (ap);
    943   return r;
    944 }
    945 
    946 int
    947 printf (char const *fmt, ...)
    948 {
    949   va_list ap;
    950   int r;
    951   va_start (ap, fmt);
    952   r = vfprintf (stdout, fmt, ap);
    953   va_end (ap);
    954   return r;
    955 }
    956 
    957 int
    958 snprintf (char *str, size_t size, char const *fmt, ...)
    959 {
    960   va_list ap;
    961   int r;
    962   va_start (ap, fmt);
    963   r = vsnprintf (str, size, fmt, ap);
    964   va_end (ap);
    965   return r;
    966 }
    967 
    968 int
    969 sprintf (char *str, char const *fmt, ...)
    970 {
    971   va_list ap;
    972   int r;
    973   va_start (ap, fmt);
    974   r = vsprintf (str, fmt, ap);
    975   va_end (ap);
    976   return r;
    977 }
    978 
    979 
    980 /* ───────── assertions ────────────────────────────────────────────── */
    981 
    982 void
    983 __assert_fail (char const *msg, char const *file, unsigned line, char const *func)
    984 {
    985   fprintf (stderr, "%s:%u:", file, line);
    986   if (func) fprintf (stderr, "%s:", func);
    987   fprintf (stderr, "assert fail: %s\n", msg);
    988   exit (1);
    989 }
    990 
    991 /* mes/lib.h declares assert_msg as the no-include-needed assert form
    992    used inside libc. Provide a body here so anything still calling it
    993    (e.g. mes-style builtins under cc.scm) links. */
    994 void
    995 assert_msg (int check, char *msg)
    996 {
    997   if (!check) {
    998     fputs (msg, stderr);
    999     exit (1);
   1000   }
   1001 }