← board

The FPC compiler corpus asks for two layouts we do not give it

Found 2026-09-04 by frankS, the first thing the new class-1 directive warning said out loud (bug-p-an-unknown-compiler-directive-is-silently-ignored). --mimic-fpc-compiler --target=arm32 now gets past {$i fpcdefs.inc} — that was feature-p-packrecords-c-directive — and immediately reports:

$ pascal26 --target=arm32 --mimic-fpc-compiler fd.pas
pascal26:3: warning: compiler directive {$H-} is recognised but not implemented …
pascal26:9: warning: compiler directive {$PACKENUM} is recognised but not implemented …

/usr/share/fpcsrc/3.2.2/compiler/fpcdefs.inc, lines 1-9:

{$mode objfpc}
{$asmmode default}
{$H-}
{$goto on}
{$inline on}
{$interfaces corba}

{ This reduces the memory requirements a lot }
{$PACKENUM 1}

fpcdefs.inc is included by essentially every unit of that compiler, so this is not one file's opinion — it is the corpus-wide setting, and its own comment says why it is there.

Why this is worth ranking above a directive nicety

{$PACKENUM 1} is a record-layout change, not a size preference. An enum field inside a record moves every field after it. Any pxx-compiled FPC unit that exchanges a record with a differently-compiled one — including the shadow-RTL boundary that goal-compile-fpc-compiler depends on — disagrees about where the fields are, self-consistently, which is the failure shape that bug-a-pascal-nilpy-rust-and-zig-over-align-an-8-byte-member-on-i386 took a mixed link to see.

{$H-} is the string default for the whole corpus; ignoring it means every bare string in FPC's sources is a long string where the source said shortstring. Whether that is a problem for pxx specifically needs measuring — pxx's string model is not FPC's — and that measurement is half this ticket.

Not obviously one job

They are filed together because they arrive together and from one include, not because the fix is shared. Split if the enum half turns out to be the only real one. Measure before scoping, the way [[feature-p-packrecords-c-directive]] did: {$PACKRECORDS C} turned out to be a no-op against our existing rule, and this pair may not be.

Gate

A record with an enum field laid out under {$PACKENUM 1} matching the gcc/FPC oracle for the same declaration, on x86-64 AND a 32-bit target — the pattern in test-packrecords-c-gcc-oracle, whose positive control (a second row whose answer must DIFFER) is the part to copy. Do not assert only that the directive is accepted: accepted-and-ignored is the state this ticket reports.


Landed: the {$PACKENUM} half (frankB, 2026-09-05, compiler 89f51a99f0b3)

The ticket said "measure before scoping" and "split if the enum half turns out to be the only real one". Both halves are real; the enum half is done and the string half is [[feature-p-h-minus-makes-a-bare-string-a-shortstring]].

The value space, measured rather than assumed

Against fpc 3.2.2 on enums whose largest member is 2 / 300 / 70000:

{$PACKENUM 1}  1 2 4     {$PACKENUM 8}   Illegal enum minimum-size specifier
{$PACKENUM 2}  2 2 4     {$PACKENUM 3}   Illegal enum minimum-size specifier
{$PACKENUM 4}  4 4 4     {$PACKENUM ON}  Illegal enum minimum-size specifier
NORMAL/DEFAULT 4 4 4     {$PACKENUM OFF} Illegal enum minimum-size specifier
{$MINENUMSIZE n} = {$PACKENUM n}    {$Z1}/{$Z2}/{$Z4}   {$Z+}=4  {$Z-}=1

It is a MINIMUM, not a sizeTHuge stays 4 bytes even at {$PACKENUM 1}. That is the row a naive "pack enums to a byte" reading gets wrong, and a test carrying only a 3-member enum would pass under both readings, so TBig = (bA, bB = 300) is in both test files as the discriminator.

Two things a layout-only test could not have caught

1. The state has to be PER-TOKEN, and a one-directive file cannot show it. Directives run in the LEX pass, so by the time a type section parses, the global holds the LAST {$PACKENUM} in the file. Every single-directive probe passes under both readings — "value at the declaration" and "last value in the file" are the same number — so the first eleven-row oracle table matched while the mechanism was wrong. It took a file with TWO directives to see it, and both test files now carry one. The fix is TokPackEnum[], the shape TokScopedEnums already uses.

And the control I reached for first was itself unfalsifiable: I checked that {$SCOPEDENUMS OFF} left a later member visible, which passes whether or not the state is positional. The discriminating direction is that the EARLIER type's member must be INVISIBLE.

2. Narrowing the kind silently detached the enum's IDENTITY. Seven sites guarded the identity stamp with kind = tyInteger — exact while every enum was integer-sized, wrong the moment one was not. WriteLn of a packed enum printed an ordinal instead of a member name, through a variable, a record field and a cast, with nothing failing. The kind half is not redundant with enumId >= 0 and could not simply be deleted: set of TCol leaves LastTypeEnumId holding the ELEMENT's id, so the kind test is what stops a bitset inheriting a member name. All seven now ask one predicate, EnumKindMatches(tk, etid), which compares against EnumStorageTypeKind itself rather than a list of kinds — so there is no second table to drift, which is what TypeIsAnyString's header warns about.

Gate

Log

End-to-end on the corpus it was filed for

The unit tests use a hand-written {$PACKENUM 1}, which proves the directive and not the CORPUS. Checked through the real include, compiler 89f51a99f0b3:

program fd; {$i fpcdefs.inc}  type TProbe = (pA, pB, pC);
  pxx --mimic-fpc-compiler : enum under fpcdefs.inc: 1
  fpc 3.2.2                : enum under fpcdefs.inc: 1

So the setting now arrives the way every FPC compiler unit actually receives it — nine lines down inside an include — rather than only when spelled at the top of a test.

This changed correctness, not reach. The march is where it was: cutils, globtype, constexp, version and cstreams COMPILE; cclasses, comphook, finput and cfileutl stop at TFPCHeapStatus ([[feature-b-getfpcheapstatus-needs-always-on-heap-accounting]]); cmsgs stops at an object type cannot have a constructor, which is a decision and not a gap. Those five compiled BEFORE this change too — with 4-byte enums where their own source asked for 1. A silently wrong layout in a unit that builds is worse than a unit that does not, because nothing reports it, so "no new units reached" is the wrong measure of this fix.