commit 5b5ed0309cc714b0e05fd444067adafc6265d892
parent 0cd6aa8cf9ff39a34a3e4de9f2508aaa7fb6f6ed
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 29 Apr 2026 13:05:47 -0700
tests/cc-libc: cover all P1pp syscalls + complex libc layers
Each P1pp syscall now has a fixture that drives it through the public
libc surface: sys_exit (exit), sys_open/close/write/read (fopen +
fputs + fread), sys_lseek (fseek), sys_unlink (unlink). Adds targeted
fixtures for printf %s / mixed va_args, strlen/strcmp/strchr,
malloc/free cycle, and atoi sign handling.
Diffstat:
18 files changed, 303 insertions(+), 0 deletions(-)
diff --git a/tests/cc-libc/09-exit-code.c b/tests/cc-libc/09-exit-code.c
@@ -0,0 +1,13 @@
+/* exit(N) -> stdlib/exit.c -> stdlib/__exit.c -> lispcc-syscall.c::_exit
+ * -> P1pp sys_exit. 00-exit covers the implicit return-from-main path;
+ * this fixture exercises the explicit terminator that abort() / errors
+ * out of main use. The literal `return 99;` after the exit call must
+ * never run — if it does, the harness sees exit code 99 and FAILs. */
+extern void exit (int code);
+
+int
+main (void)
+{
+ exit (7);
+ return 99;
+}
diff --git a/tests/cc-libc/09-exit-code.expected-exit b/tests/cc-libc/09-exit-code.expected-exit
@@ -0,0 +1 @@
+7
diff --git a/tests/cc-libc/10-file-roundtrip.c b/tests/cc-libc/10-file-roundtrip.c
@@ -0,0 +1,55 @@
+/* fopen("w") + fputs + fclose, then fopen("r") + fread + fclose.
+ *
+ * Smoke-tests the full file I/O stack on top of P1pp sys_open /
+ * sys_close / sys_write / sys_read:
+ * - fopen -> stdio/fopen.c -> _open3 -> sys_open
+ * - fputs -> stdio/fputs.c -> fputc -> _write -> sys_write
+ * - fclose -> stdio/fclose.c -> close -> sys_close
+ * - fread -> stdio/fread.c -> read -> _read -> sys_read
+ *
+ * Mes's fopen returns the fd cast to FILE*, so a single declaration of
+ * FILE as `long` is enough to thread it through. Diagnostics use the
+ * already-proven write() path so a failure inside fopen / fread is
+ * easy to spot from stdout. */
+typedef long FILE;
+typedef long ssize_t;
+typedef unsigned long size_t;
+
+extern FILE *fopen (char const *path, char const *mode);
+extern int fclose (FILE *stream);
+extern int fputs (char const *s, FILE *stream);
+extern size_t fread (void *ptr, size_t size, size_t count, FILE *stream);
+extern ssize_t write (int fd, void const *buf, size_t n);
+
+int
+main (void)
+{
+ char const *path = "/tmp/lispcc-cclibc-10";
+
+ FILE *w = fopen (path, "w");
+ if (!w)
+ {
+ write (1, "fopen-w-fail\n", 13);
+ return 1;
+ }
+ fputs ("hello\n", w);
+ fclose (w);
+
+ FILE *r = fopen (path, "r");
+ if (!r)
+ {
+ write (1, "fopen-r-fail\n", 13);
+ return 2;
+ }
+ char buf[8];
+ size_t n = fread (buf, 1, 6, r);
+ fclose (r);
+
+ if (n != 6)
+ {
+ write (1, "short-read\n", 11);
+ return 3;
+ }
+ write (1, buf, 6);
+ return 0;
+}
diff --git a/tests/cc-libc/10-file-roundtrip.expected b/tests/cc-libc/10-file-roundtrip.expected
@@ -0,0 +1 @@
+hello
diff --git a/tests/cc-libc/11-fseek.c b/tests/cc-libc/11-fseek.c
@@ -0,0 +1,57 @@
+/* fseek -> stdio/fseek.c -> lseek (posix) -> _lseek -> sys_lseek.
+ *
+ * Lays a known payload in a tmp file, opens it for reading, fseek's
+ * past the first 7 bytes, then fread's the next 5. Expected stdout is
+ * "world" — the substring after "hello, ". Anchors the lseek path with
+ * a non-zero, non-end offset so a SEEK_SET that silently no-ops would
+ * read "hello," instead of "world". */
+typedef long FILE;
+typedef long ssize_t;
+typedef unsigned long size_t;
+
+extern FILE *fopen (char const *path, char const *mode);
+extern int fclose (FILE *stream);
+extern int fputs (char const *s, FILE *stream);
+extern int fseek (FILE *stream, long offset, int whence);
+extern size_t fread (void *ptr, size_t size, size_t count, FILE *stream);
+extern ssize_t write (int fd, void const *buf, size_t n);
+
+int
+main (void)
+{
+ char const *path = "/tmp/lispcc-cclibc-11";
+
+ FILE *w = fopen (path, "w");
+ if (!w)
+ {
+ write (1, "fopen-w-fail\n", 13);
+ return 1;
+ }
+ fputs ("hello, world\n", w);
+ fclose (w);
+
+ FILE *r = fopen (path, "r");
+ if (!r)
+ {
+ write (1, "fopen-r-fail\n", 13);
+ return 2;
+ }
+ if (fseek (r, 7, 0)) /* SEEK_SET = 0 */
+ {
+ write (1, "fseek-fail\n", 11);
+ fclose (r);
+ return 3;
+ }
+ char buf[8];
+ size_t n = fread (buf, 1, 5, r);
+ fclose (r);
+
+ if (n != 5)
+ {
+ write (1, "short-read\n", 11);
+ return 4;
+ }
+ buf[5] = '\n';
+ write (1, buf, 6);
+ return 0;
+}
diff --git a/tests/cc-libc/11-fseek.expected b/tests/cc-libc/11-fseek.expected
@@ -0,0 +1 @@
+world
diff --git a/tests/cc-libc/12-unlink.c b/tests/cc-libc/12-unlink.c
@@ -0,0 +1,46 @@
+/* unlink -> lispcc-syscall.c::unlink -> P1pp sys_unlink (= unlinkat
+ * with AT_FDCWD). Drives the syscall by creating a file, removing it,
+ * then re-opening it for read and asserting fopen now returns NULL.
+ * The first fopen("w") must succeed, the second fopen("r") after
+ * unlink must fail — anything else means the path either never
+ * existed or the unlink no-oped. */
+typedef long FILE;
+typedef long ssize_t;
+typedef unsigned long size_t;
+
+extern FILE *fopen (char const *path, char const *mode);
+extern int fclose (FILE *stream);
+extern int fputs (char const *s, FILE *stream);
+extern int unlink (char const *path);
+extern ssize_t write (int fd, void const *buf, size_t n);
+
+int
+main (void)
+{
+ char const *path = "/tmp/lispcc-cclibc-12";
+
+ FILE *w = fopen (path, "w");
+ if (!w)
+ {
+ write (1, "fopen-w-fail\n", 13);
+ return 1;
+ }
+ fputs ("x\n", w);
+ fclose (w);
+
+ if (unlink (path))
+ {
+ write (1, "unlink-fail\n", 12);
+ return 2;
+ }
+
+ FILE *r = fopen (path, "r");
+ if (r)
+ {
+ write (1, "still-there\n", 12);
+ fclose (r);
+ return 3;
+ }
+ write (1, "gone\n", 5);
+ return 0;
+}
diff --git a/tests/cc-libc/12-unlink.expected b/tests/cc-libc/12-unlink.expected
@@ -0,0 +1 @@
+gone
diff --git a/tests/cc-libc/13-printf-string.c b/tests/cc-libc/13-printf-string.c
@@ -0,0 +1,12 @@
+/* printf("%s") — exercises vfprintf's string-conversion branch on top
+ * of the va_arg pull. 04 covers the no-conversions path, 05 covers
+ * %d. This pins %s, which is the conversion the most code paths in
+ * tcc.c rely on (printf("%s\n", error_msg) etc.). */
+extern int printf (char const *fmt, ...);
+
+int
+main (void)
+{
+ printf ("hello, %s\n", "world");
+ return 0;
+}
diff --git a/tests/cc-libc/13-printf-string.expected b/tests/cc-libc/13-printf-string.expected
@@ -0,0 +1 @@
+hello, world
diff --git a/tests/cc-libc/14-printf-mixed.c b/tests/cc-libc/14-printf-mixed.c
@@ -0,0 +1,12 @@
+/* printf with %s + %d + %x in one call — three va_arg pulls of two
+ * different widths/types, plus three vfprintf conversion branches.
+ * Catches bugs where one pull works in isolation (covered by 05/13)
+ * but the second/third pull walks the va_list off the rails. */
+extern int printf (char const *fmt, ...);
+
+int
+main (void)
+{
+ printf ("%s=%d %x\n", "answer", 42, 255);
+ return 0;
+}
diff --git a/tests/cc-libc/14-printf-mixed.expected b/tests/cc-libc/14-printf-mixed.expected
@@ -0,0 +1 @@
+answer=42 ff
diff --git a/tests/cc-libc/15-strops.c b/tests/cc-libc/15-strops.c
@@ -0,0 +1,40 @@
+/* Public string ops the libc surfaces from string/. tests/cc/129
+ * exercises the libp1pp-provided memcpy/memset/strlen labels by way
+ * of bare-name extern decls; this fixture instead drives the calls
+ * through the libc.P1pp surface (where strcmp / strchr come from
+ * mes's string/strcmp.c and string/strchr.c). */
+typedef unsigned long size_t;
+typedef long ssize_t;
+
+extern size_t strlen (char const *s);
+extern int strcmp (char const *a, char const *b);
+extern char *strchr (char const *s, int c);
+extern ssize_t write (int fd, void const *buf, size_t n);
+
+int
+main (void)
+{
+ if (strlen ("abc") != 3)
+ {
+ write (1, "len-fail\n", 9);
+ return 1;
+ }
+ if (strcmp ("foo", "foo") != 0)
+ {
+ write (1, "eq-fail\n", 8);
+ return 2;
+ }
+ if (strcmp ("foo", "bar") == 0)
+ {
+ write (1, "ne-fail\n", 8);
+ return 3;
+ }
+ char *p = strchr ("hello", 'l');
+ if (!p || *p != 'l' || p[1] != 'l')
+ {
+ write (1, "chr-fail\n", 9);
+ return 4;
+ }
+ write (1, "ok\n", 3);
+ return 0;
+}
diff --git a/tests/cc-libc/15-strops.expected b/tests/cc-libc/15-strops.expected
@@ -0,0 +1 @@
+ok
diff --git a/tests/cc-libc/16-malloc-free.c b/tests/cc-libc/16-malloc-free.c
@@ -0,0 +1,44 @@
+/* Two-cycle malloc/free smoke. mes's free is a no-op (linux/malloc.c
+ * is bump-only, by design — tcc-boot2 never reaches a real allocator),
+ * so this fixture just verifies that calling free between two malloc
+ * requests doesn't perturb the bump cursor: the second allocation
+ * must still hand back a usable pointer at strictly higher address.
+ * 07-malloc-roundtrip covers the single-malloc round-trip; this adds
+ * the free + second-malloc layer. */
+typedef unsigned long size_t;
+typedef long ssize_t;
+
+extern void *malloc (size_t size);
+extern void free (void *ptr);
+extern ssize_t write (int fd, void const *buf, size_t n);
+
+int
+main (void)
+{
+ char *p = (char *) malloc (8);
+ if (!p)
+ {
+ write (1, "m1-null\n", 8);
+ return 1;
+ }
+ p[0] = 'A';
+ free (p);
+
+ char *q = (char *) malloc (16);
+ if (!q)
+ {
+ write (1, "m2-null\n", 8);
+ return 2;
+ }
+ if (q <= p)
+ {
+ write (1, "no-advance\n", 11);
+ return 3;
+ }
+ q[0] = 'O';
+ q[1] = 'K';
+ q[2] = '\n';
+ write (1, q, 3);
+ free (q);
+ return 0;
+}
diff --git a/tests/cc-libc/16-malloc-free.expected b/tests/cc-libc/16-malloc-free.expected
@@ -0,0 +1 @@
+OK
diff --git a/tests/cc-libc/17-atoi.c b/tests/cc-libc/17-atoi.c
@@ -0,0 +1,14 @@
+/* atoi -> stdlib/atoi.c -> mes/abtol.c. Negative-sign and digit-loop
+ * paths in one fixture; printf is reused from 14-printf-mixed for the
+ * two %d round-trips, so a regression here that flips the sign bit
+ * surfaces immediately. */
+extern int atoi (char const *s);
+extern int printf (char const *fmt, ...);
+
+int
+main (void)
+{
+ printf ("%d\n", atoi ("123"));
+ printf ("%d\n", atoi ("-42"));
+ return 0;
+}
diff --git a/tests/cc-libc/17-atoi.expected b/tests/cc-libc/17-atoi.expected
@@ -0,0 +1,2 @@
+123
+-42