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
- Give
struct PxxCrtlFilea buffer (pointer + size + position, sosetvbufcan point it at caller memory), and routefputc/putc/putchar/fputs/fwrite/printfthrough it. - Buffering policy is C99 §7.19.3p7 and it is normative, not a preference: stderr not fully buffered; stdout line-buffered when it refers to an interactive device; fully buffered otherwise.
setvbufhonours_IOFBF/_IOLBF/_IONBFand the caller's buffer, and returns nonzero when it cannot.setbuffollows from it.- Flush on
fclose, onexit, and on the abnormal-exit path.
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.