← board

Buffered Text I/O and SetTextBuf

Implements the ruling in decided/decide-settextbuf-needs-buffered-text-io-or-stays-missing.md. Read that for the measurements; this is the work list.

Read side

Text today ends in an inline array:

Buffered: Boolean; BufPos: Integer; BufLen: Integer;
Buf: array[0..TF_BUFSIZE - 1] of Byte;    { TF_BUFSIZE = 4096 }

Change to BufPtr: PByte; BufSize: Integer, plus a default inline array that BufPtr points at on Assign, so the common case still allocates nothing and the record stays self-contained. Then SetTextBuf is FPC's four assignments: set pointer, set size, zero BufPos, zero BufLen.

Match FPC exactly on all three observable side effects — they are the contract, not accidents:

  1. Read-ahead leaves the fd ahead of the logical position. Do not seek back.
  2. Mid-stream SetTextBuf discards pending buffered bytes. No copy, no reseek.
  3. The buffer is caller-owned and retained. Do not copy it, do not track it.

One deliberate divergence, ruled: keep the existing guard

f.Buffered := (f.Handle >= 0) and (PalSeek(f.Handle, 0, PAL_SEEK_CUR) >= 0);

as the default — we do not read ahead on pipes and terminals, where the position is unrecoverable and FPC gets it wrong — but let an explicit SetTextBuf override it. A caller naming a buffer is a different signal from us choosing to buffer.

Write side

Writes go straight to the fd today (PalWrite per Write). Add buffering under C99 §7.19.3p7's policy, not FPC's: stderr not fully buffered; stdout line-buffered when it refers to an interactive device; fully buffered otherwise. Flush on Close and on normal and abnormal exit — FPC's runtime-error path flushes and ours must too (measured: FPC's segfault run exited 216 with all output intact).

Flush becomes real; its comment at lib/rtl/textfile.pas:143 ("PXX text writes go straight to the fd (no RTL-side buffer), so Flush only…") must be rewritten in the same commit.

Cross-RTL ordering

Coordinate with feature-c-crtl-stdio-buffering-and-setvbuf: both sides register a flusher per destination, and each flushes any other dirty registered buffer for that destination before writing into its own. Neither RTL names the other. Do not land write buffering on only one side — that is the one combination that reorders output in a program that mixes WriteLn and printf, which is exactly what pxx exists to link together.

Gate

make lib-test. Carry a repro that writes to stdout and stderr with stdout on a pipe and asserts the interleaving is preserved — that is the property FPC fails and the whole reason for the policy divergence, and no existing test covers it.

2026-09-04 — READ SIDE LANDED. Write side and the registry are still open.

Text now carries BufPtr: PByte / BufSize: Integer / BufForced: Boolean over an inline DefBuf, and SetTextBuf exists. The write side, the C99 buffering policy and the flush registry are not in this change and the ticket stays open for them.

What the read side does, and where it deliberately differs

Acceptance — byte-identical to FPC 3.2.2's own run

test/lib_settextbuf.pas, seven rows, compared against the oracle's output rather than literals, because the read-ahead positions are FPC's contract and hardcoding them would be the test asserting our implementation back at itself:

small-line=line1....   small-pos=16
big-line=line1....     big-pos=100
count=20 last=line20...
midstream-first=line1....   midstream-next=line11...

THE POSITION IS THE INSTRUMENT AND THE TEXT IS NOT. A reader using the default buffer returns exactly the same lines, so a content-only test passes against a SetTextBuf that does nothing at all. The two sizes are 16 and 100 — neither is TF_BUFSIZE (4096) nor FPC's default (256) — so no row can be satisfied by a default value. midstream-next=line11 is the discarded-bytes contract, measured under FPC and copied on purpose.

The positive control is a SEGFAULT, not a mismatch. With the read size taken from TF_BUFSIZE again instead of from f.BufSize, and nothing else changed, PalRead writes 4096 bytes into the caller's 16-byte array and the binary dies. So "ignoring the size" is not a benign no-op here — it is a stack smash, which is a stronger reason to honour BufSize than parity is.

make compiler/pascal26 converged (the compiler reads its own sources through this record, so the fixedpoint is a real consumer of the change).

Still to do — and the interlock

Write buffering, the C99 §7.19.3p7 policy, a real Flush, and the flush registry shared with feature-c-crtl-stdio-buffering-and-setvbuf. Do not land the write side alone. Flush's comment at textfile.pas and the interface comment above it both still say writes are unbuffered; they are true today and must be rewritten in the same commit that stops being true.

2026-09-04 — WHOEVER TAKES THE WRITE SIDE, READ THIS FIRST

writeln does not go through this unit. Measured, with a positive control.

The ticket's write-side plan says "Writes go straight to the fd (PalWrite per Write)" and reads as though buffering Output in textfile.pas would buffer writeln. It would not. Every backend lowers IR_WRITE/IR_WRITELN to its own emitted write — ir_codegen.inc (x86-64), 386, arm32, aarch64, riscv32, xtensa, wasm32 all carry the node — and the Text record is never consulted.

Not inferred from the syscall trace, which cannot separate the two: writeln gives write(1,"hello",5) then write(1,"\n",1), and TextWriteLn would give exactly the same pair. The discriminator is a marker inside TextWrite. With PalWrite(2, '[TW]', 4) added to its first line and nothing else changed:

writeln('plain');            ->  plain            { no marker }
TextWrite(Output, 'viaText') ->  [TW]viaText      { marker }

So the write half is not "add a buffer to Text". It is either

riscv32 already routes through builtinheap.pas's PXXWrite* helpers rather than inline codegen, so a runtime hook is not unprecedented — but those helpers call PXXSysWrite directly and are equally invisible to this unit.

It also changes the interlock, and makes it WORSE rather than better

The ruling's registry assumes both writers can be made to consult it. crtl's can. Pascal's cannot, today, because the writer is emitted code with no call site to hook. So the moment lib/crtl buffers, a mixed program's writeln output jumps ahead of its buffered printf output and there is nowhere in lib/rtl to put the flush. That is a Track A dependency the C half acquires, and it is not in either ticket.

Two comments that must be rewritten in the same commit

lib/rtl/textfile.pas:164-165 (interface, above Flush) and the body of Flush both state that writes are unbuffered and there is nothing to drain. Both are true today. Whoever adds write buffering rewrites both in that commit, or the tree gets a comment that disagrees with its code — the case CLAUDE.md says you cannot resolve by looking, because you cannot tell which half is wrong.