← board

ok: is printed for an output the compiler failed to write

The fact, measured

A full make test-nilpy on 2026-09-11 reddened exactly one row, test_nilpy_qualified_ctor_does_not_capture_its_args, with Segmentation fault and an empty diff — all six expected lines missing. The compile line above it read:

ok: /tmp/pxx-testtmp-.../test_nilpy_qualctorargs26  [code=1470232B  data=114772B  bss=88940B]

The binary left on disk was 1,163,348 bytes. A good build of the same source is 1,585,236. It was 421,888 bytes short, the compiler reported success with three exact byte counts, and the truncated ELF died on the first instruction it did not have.

The cause was ENOSPC: /tmp went to 0 free during the run. The row passes on rerun at HEAD and under the pin, so nothing is wrong with the codecs path that the row is named for.

Why this is the house failure mode and not a disk problem

The disk filling is an accident. The defect is that the compiler's success message is a statement about its intent rather than about the file: code=, data= and bss= are the sizes it meant to write, printed unconditionally. So the instrument does not error — it answers, correctly, about something else.

The consequence is the expensive part. A short write produces:

A seat attributing that red to a range would have bisected a codecs ticket that was never broken. This one was caught only because the same session had caused the ENOSPC and recognised the timestamps.

Where

compiler/elfwriter.inc, all three write paths, none checked:

line call
2075-2079 syswrite(f,Code,CodeLen), syswrite(f,Data,DataLen), syswrite(f,DbgBuf,DbgLen)
2084 sysclose(f)
1944-1950 the Blockwrite variant, including the padN loop
2382 the third Blockwrite site
453-454 syswrite(f, TokChars, len) then sysclose(f)
459-465 writeU8/writeU16/writeU32Blockwrite, no IOResult

sysclose matters as much as the writes: a filesystem may only report a deferred write error at close, so checking the writes alone still lets this through.

The fix

Check each write's result against the length asked for, check the close, and make a short write a hard error that suppresses the ok: line. The message must name the path and the shortfall, because "wrote 1163348 of 1585236 bytes to X" is self-diagnosing where a segfault twenty minutes later is not.

Verifying it without a full filesystem — no sudo, no mount

ulimit -f caps a file's size in 512-byte blocks and makes the write fail with EFBIG, which is the same short-write shape as ENOSPC:

( ulimit -f 64; ./compiler/pascal26 test/hello.pas /tmp/trunc )   # expect a LOUD failure

That is the positive control, and it is drawn from the right population: the thing under test is "a write that cannot complete", not "a disk that is full". Assert that the ok: line is ABSENT and the exit status nonzero — asserting the error text alone would pass on a compiler that prints both.

What this is not

Not the cause of the tier red it produced — that row is fine. Not a /tmp capacity ticket; the inode/space hazards are already in CLAUDE.md. The defect is the unchecked write, and it is a wrong answer on every filesystem that can fill, every quota, and every NFS mount that drops a write at close.

THIRD AND FOURTH OCCURRENCES, 2026-09-15 — two seats, one hour, one full /tmp

Raised 55 -> 70 on recurrence, not on a new argument. The defect is unchanged; what is new is that it has now voided a measurement three times in four days, and each time the seat that hit it spent real effort attributing a red before finding the disk.

Track N tier. test_nilpy_property26 printed ok: [code=1335064B data=88532B bss=56436B] and then segfaulted with EMPTY output, so the diff against a full .expected deleted every line — the exact shape of a real regression in whatever landed last. It was read as one until the size was checked. The dead binary was 1421312 bytes = 4096 x 347, exactly a filesystem block boundary; the correct rebuild is 1423828, a multiple of nothing.

A NEW AND CHEAPER DISCRIMINATOR THAN THIS TICKET CURRENTLY RECORDS. The existing guidance is to compare the size against a fresh build, which costs a build and needs a known-good tree. It is not needed: a write killed by ENOSPC ends on a page, a block or a power of two, and a real one does not. One ls -l answers it. The lekkerzeilen seat found the same signature independently the same hour in a completely different artefact — a run log that stopped at 458752 bytes = 448 KiB exactly — and the round number was likewise the only thing that gave it away, since every count in that experiment was a grep -c over that file and would simply have reported fewer events.

AND THE BLAST RADIUS IS THE WHOLE RUN, WHICH THIS TICKET DOES NOT SAY. The tier had printed ok: for 490 programs before the one that died, all compiled while the disk was on its way down. A truncated binary only fails if the program reaches the missing part, so any of those 490 could be truncated and green. There is no trustworthy prefix: the run is void, and keeping the 490 while re-running the tail is the trap. The stale truncated binaries must also be cleared from the scratch directory, or they survive into the next run at the path it writes to.

Cheap mitigation available to any caller today, independent of this fix: record df -Pm before and after a long run INSIDE the log, and read it as part of the verdict. It does not prevent the corruption but it makes it attributable in one line instead of an evening. Worked, with both artefacts and the general form: devdocs/dev/debugging-playbook.md, "A WRITE KILLED BY ENOSPC ENDS ON A ROUND NUMBER".