← board

Four standard Text-file routines are missing

Found 2026-08-22 by an FPC differential sweep over file I/O (fpc -Mobjfpc -O1 3.2.2 vs pxx at a38f1cf8a).

The measurement

Each of these is a compile-time undefined variable, not a wrong result:

routine pxx note
Flush(f) compiles already present
SeekEof(f) undefined variable (SeekEof) missing
SeekEoln(f) undefined variable (SeekEoln) missing
Rename(f, 'new') undefined variable (Rename) missing
SetTextBuf(f, buf) undefined variable (SetTextBuf) missing

Why these four matter, in order

SeekEof / SeekEoln are the important pair. They are not conveniences — they are how a Pascal program reads a whitespace-separated table without tripping on the blank tail of the last line:

while not SeekEof(f) do begin Read(f, n); Sum := Sum + n; end;

Eof(f) is False while trailing blanks or a final newline remain, so the loop above written with Eof reads one junk value at the end. Both routines skip whitespace without consuming the next real token — which is exactly the one-byte-pushback capability lib/rtl/textfile.pas already has in f.Peek / f.HasPeek. SeekEoln additionally stops at the line terminator; SeekEof skips over it.

That makes these two the natural companions of bug-b-read-of-a-number-from-a-text-file-reads-the-whole-line (prio 65): that ticket adds a numeric tokeniser over the same pushback, and these two are the matching loop conditions. Whoever takes that ticket should take this one in the same pass — same file, same mechanism, and the pair is what makes the tokeniser usable.

Rename is nearly free. PalRename already exists (lib/rtl/platform.pas:117,369) and lib/rtl/pxxcio.pas:183 already calls it. What is missing is only the Rename(var f: Text; const NewName: string) wrapper that renames the file a handle is assigned to and updates the handle's stored name. FPC requires the file be closed; do the same rather than inventing a laxer rule.

SetTextBuf is the least important and may be a no-op stub. FPC's contract is "use this caller-supplied buffer for this handle"; a stub that accepts and ignores the buffer is a silent lie about lifetime, so either implement it or leave it missing — a compile error is more honest than a routine that pretends. That is a genuine fork, so if the taker disagrees with implementing it, file a Track U decide-* rather than stubbing it.

Scope

Track B (lib/rtl/textfile.pas, plus a Rename wrapper). No compiler change is expected: Flush(f) already compiles, so the Text-handle dispatch path accepts an RTL routine taking var f: Text without frontend work. If it turns out SeekEof needs a parser arm the way read/readln do, that half is Track P and should be filed separately.

Gate

make lib-test plus a test whose expected output is FPC's, in the shape of the existing text-file tests.

2026-08-28 (frankB, Track B) — RESOLVED. Three of four implemented; SetTextBuf escalated as the ticket instructed.

No compiler change was needed, as predicted: SeekEof/SeekEoln declare like Eof (function(var f: Text): Boolean) and Rename like Erase, and the Text dispatch path took all three without a parser arm. Nothing was filed to Track P.

The skip set is not what it looks like, and that is the whole finding

I measured it rather than assuming, and the obvious rule is wrong. An FPC 3.2.2 oracle wrote each of #1..#40 in front of an 'x' and read back where the cursor landed. Exactly three bytes are stepped over:

skipped:      #9 TAB, #26 SUB, #32 SPACE      (SeekEof also skips #10 and #13)
NOT skipped:  #1..#8, #11, #12, #14..#25, #27..#31, #33 and up

So c <= 32 — the rule anyone would reach for, and the one a later "simplification" will reach for again — is wrong: it eats #1, #31 and every other control byte straight out of a data file. #26 is in the set because it is the DOS end-of-file marker and FPC steps over it like blank space; measured both ways, a file holding only #26 is SeekEof-empty while 'x'#26 still yields its 'x'.

The two routines then differ by exactly the line terminators: SeekEof skips #10/#13, SeekEoln stops on them and answers True without consuming them, so Readln still sees a whole line.

Deliberately not unified with the existing TFIsSpace. That is the numeric tokeniser's delimiter set (#9 #10 #13 #32); this is the seek routines' skip set. They overlap without being one concept — #26 belongs only to the second, and #10/#13 delimit a token but must stop SeekEoln — so folding them together would make one of the two wrong. Noted in the source at both sites, because normalise-dont-special-case.md would otherwise read as an argument to merge them.

Rename

Mirrors Erase (a name-level operation on a closed, assigned handle) over the existing PalRename. Measured on FPC and copied rather than relaxed: an open handle is refused and the file left alone, and after a successful rename the handle follows the fileReset(f) opens the new name and reads its contents. FPC's codes are 102 (open) and 2 (missing); ours stay ours per CLAUDE.md's runtime-error rule, and the test asserts refused, not a number.

SetTextBuf — escalated, not stubbed

lib/rtl/textfile.pas has no buffering at all: one PalRead per byte, with a one-byte pushback slot for lookahead. There is nothing for SetTextBuf to configure, so it cannot be given its FPC meaning without first building buffered Text I/O. Per this ticket's own instruction I filed decide-settextbuf-needs-buffered-text-io-or-stays-missing [U, p55] rather than stubbing: recommendation is leave it missing now, and file buffered Text I/O as its own Track B ticket ranked on the performance case (a syscall per character is the real cost), not on this signature.

Test

test/lib_text_seek_rename.pas, wired into make lib-test, 27 seek rows plus the loop and rename cases, sentinel TEXTSEEK OK on the last line.

Every row records the cursor as well as the Boolean — a test that only checked the answer would pass an implementation that ate the token. The motivating case from this ticket is asserted directly: the SeekEof sum loop over ' 10 20\n30 \n' gives 60 in three turns, and the same loop written with Eof is asserted to take one extra empty turn — the defect the pair exists to remove, pinned rather than described.

#1, #31 and #33 are in the table specifically as the boundary. Negative control run: replacing the predicate with c <= 32 fails the test with 9 errors, on exactly those rows plus the SeekEoln terminator cases. A test that passes on the first run has not yet been shown to be able to fail.

Log