← board

Buffered FILE writes, a real setvbuf, and the flush registry

Implements the C half of decided/decide-settextbuf-needs-buffered-text-io-or-stays-missing.md.

The two defects

Unbuffered. Every write path calls __pxx_write directly:

int fputc(int c, FILE *stream) {
  char ch = (char)c;
  if (__pxx_write(stream->fd, &ch, 1) < 0) { stream->err = 1; return -1; }

One syscall per character for any putchar/fputc loop. fwrite and fputs pass whole buffers through, so the exposure is loop-shaped code rather than printf-shaped — the win is unmeasured; measure it before pricing this higher.

A lying stub. lib/crtl/src/stdio.c:1051:

int setvbuf(FILE *stream, char *buf, int mode, size_t size) { (void)stream; (void)buf; (void)mode; (void)size; return 0; }

Accepts a caller-supplied buffer, ignores it, reports success. A C caller that checks the return is told its buffer was adopted when it was not — the lifetime lie the SetTextBuf ruling rejected, already shipped here with the opposite answer.

The work

The flush registry — do not skip this half

Today ordering between Pascal WriteLn and C printf is correct only because both sides are unbuffered. Buffering either side alone reorders output inside a single program. Register a flusher per destination; before writing into your own buffer, flush any other registered dirty buffer for that destination. O(N) in live streams, no direct reference between lib/crtl and lib/rtl, and a null check when only one is linked.

With the C policy above, the registry only has to handle the same-fd case; two descriptors onto one terminal would need fstat plus st_dev/st_ino comparison and the policy removes the need.

Land in step with feature-b-buffered-text-io-and-settextbuf.

Gate

C tests + self-host + cross. Add a mixed-frontend repro: a Pascal WriteLn and a C printf alternating into a pipe, asserting order. Nothing covers that today, and it is the property this pair of tickets can break.

RELEASE-RISK: SILENT-WRONG

The program compiles, runs, and is WRONG with no diagnostic — so a user cannot discover it from a message and cannot work around what they cannot see. Marked 2026-09-06 for the beta 0.1 release sweep; a beta may ship known REFUSALS, but an unenumerated silent-wrong is the class it must not ship.

The SILENT-WRONG half is setvbuf, not the missing buffering: lib/crtl/src/stdio.c defines it as { (void)stream; (void)buf; (void)mode; (void)size; return 0; } -- it discards every argument and returns 0, which C99 7.19.5.6 defines as SUCCESS. A caller that correctly checks the return is told its buffering request was honoured when nothing happened. Unbuffered output on its own is slow, not wrong; a stub that reports success is the dishonest-stub shape.