curayious Language Lab

C — lab notes

The working file. Wrong turns, compiler errors, and the fix that worked. Kept because on re-read these are worth more than the polished README.


The build, on this machine

gcc is not on PATH by default here — it lives in MSYS2's UCRT64 tree, installed for the COBOL track:

export PATH="/c/msys64/ucrt64/bin:$PATH"
gcc -std=c99 -Wall -Wextra -O2 -o build/logagg.exe logagg.c

Clean first time with -Wall -Wextra, which is not usually how this goes.

UCRT64 and not MINGW64. The two MSYS2 environments link against different Microsoft C runtimes — MSVCRT is the old one and is not C99-compatible; UCRT is what Visual Studio itself uses (the MSYS2 docs explain the split). The concrete difference here: MSVCRT's printf does not handle %lld correctly — it wants %I64d instead — while UCRT follows the standard. Getting this wrong would have produced garbage in every numeric column, and the failure would have been silent.

Three things that would have broken the byte-exact diff

1. Windows opens stdout in text mode

Every \n printed becomes \r\n. The whole diff fails and the failure is invisible in a terminal, which renders both identically.

#ifdef _WIN32
#  include <io.h>
#  include <fcntl.h>
    _setmode(_fileno(stdout), _O_BINARY);
#endif

There is no portable way to express this — it is a real platform difference. All three tracks hit it and all three solve it differently (Java writes the bytes itself, Python uses sys.stdout.buffer). That comparison is more instructive than any of the three fixes.

verify.py has a dedicated check for it, because the symptom is so unhelpful:

if b"\r" in actual and b"\r" not in expected:
    return "output contains CR -- stdout is in text mode (SPEC 4 requires LF)"

2. %-20s pads but does not truncate

SPEC 4 says the service column is 20 wide and truncated at 20. printf("%-20s", name) pads a short name and happily runs long on a 25-character one, shifting every subsequent column. The precision field is what truncates:

printf("%-20.20s%9lld...", nd->service, ...);

No service in the fixture is over 20 characters, so this would have passed the diff and sat there as a latent bug. Caught by reading the spec rather than by testing — the lesson is uncomfortable but real.

3. strcmp, not strcoll

SPEC 3 wants byte-wise ordering. strcoll sorts by the current locale's collation, so the answer would change with LC_COLLATE. Same trap as Java's Collator and, in a different costume, as Python's default str.lower() on Turkish input. Anything the machine reads must not have the user's locale in the path.

realloc — the one-line leak

char *p = realloc(l->buf, cap);
if (!p) return 0;
l->buf = p;

Writing l->buf = realloc(l->buf, cap) directly is the classic C memory bug. On failure realloc returns NULL and leaves the original block allocated — so the assignment overwrites the only pointer to it:

correct:   p = realloc(l->buf, cap) → NULL
           l->buf still points at the old block; caller frees it later at line_free

wrong:     l->buf = realloc(l->buf, cap) → NULL
           l->buf is now NULL; the old block is allocated, unreachable, leaked

The correct form needs a temporary at every call site. The behaviour is documented — the realloc man page states the original block is left untouched on failure — and CERT C's memory-management (MEM) rules cover the pattern, because the failure is a leak on the error path: exactly the path nobody tests.

The ownership rule that has no compiler support

split_fields writes NUL bytes into the line buffer and returns pointers into it. Nothing is allocated and nothing survives the next read_line. Which means:

n->service = malloc(strlen(service) + 1);
strcpy(n->service, service);

The table must copy its keys. Store the borrowed pointer instead and every key in the hash table silently becomes the last line of the file — a bug that produces a plausible-looking report with completely wrong numbers. The README's Q3 diagram draws the two pointer kinds side by side.

Nothing in the type system distinguishes "pointer I own" from "pointer I am borrowing". The distinction lives in a comment. Rust's borrow checker is that same distinction moved into the compiler, and it is worth maintaining the comment version once before reading the enforced version.

Things not done yet