A set is 32 bytes whatever its bounds
PARKED 2026-09-02 BY THE OWNER. "let's park it for now. i'd say, for now, our sets are just always 32 byte. and advise against using records with sets for file-io or document it."
rainy-day, not rejected and not low-prio — the measurement is true, the improvement is real, and it is a future plan rather than a non-goal. Narrowing would buy three things at once (FPC-identical
SizeOf, blittable records forfile of T, and ESP memory) for one ABI change; the owner judged the overhead not worth it now, having seen all three. The decision and every measurement behind it are in [[decide-a-what-a-set-costs-bits-bytes-bounds-and-what-file-of-t-writes-to-disk]].THE MECHANISM IS ALREADY DESIGNED, and it is cheaper than the estimate this was parked on. The owner's
smallsetproposal — a hidden second type kind, samesetkeyword in source — avoids variable-width sets entirely, because a small set is a DWORD riding existing scalar machinery. Measured: of 115tySetsites, 54 are bare kind tests (mechanical) and only 9 are size or copy decisions. Design it with ONEIsSetKind()predicate, never 54 two-armed comparisons. Full write-up in the decide.Reopen it with: a real program that the 32-byte width makes infeasible — an ESP target that runs out of RAM on set-heavy code, or a record-file format that must blit. Not a
SizeOfprobe: under the 2026-09-02 ruleSizeOf(set of 0..7)= 32 is a true statement about our representation.
Why this is its own ticket
It arrived as third (2) of compat-pascal-four-type-sizes-…, beside the
string[N] third. frankb-a9 asked for the split after fixing the string
half and the reason is ranking, not tidiness: bundled, the set half inherits a
priority the string half earned and reads as comparably tractable to whoever
picks it up. It is not. The string half was a matter of asking the right sizing
function; this one changes an ABI class.
Measured
set of 0..7 — pxx 32, FPC 4. FPC's rule is the small-set word: 4 bytes
when the high bound is ≤ 31, 32 above it, and it does not rebase to lo —
so set of 200..207 is 32 in both compilers and only the low-bounded case
diverges. That is the common case; set of Char and set of 0..255 are
already agreed.
FPC's rule measured first-hand 2026-09-02 (3.2.2, -O-): the width is a
function of the HIGH BOUND ALONE — 4 if hi <= 31, else 32. No rebasing, no
span term: set of 32..63 spans exactly 32 values and still costs 32 bytes,
and set of 'x'..'z' costs 32 for three bits. This confirms frankb-a9's
no-rebase measurement and is now independently sourced.
Corroborated 2026-09-02 by two sources that fail differently — frankb-a9's measurement against FPC, and a grep of our own definitions:
compiler/defs.inc:1097: IR_SET_COPY = 40; { ... copy full 32-byte set }
compiler/defs.inc:2003: tySet, { 21: Set — 32-byte bitset }
The width is not in a table someone can widen. It is in the type kind's own
definition and in an IR opcode's documented contract. 115 tySet sites; 39
IR_SET_COPY/IR_SET_LIT sites.
What makes it hard, and what does not
Not hard: no value is wrong. Every set operation produces the right answer at 32 bytes, on every target, which is exactly why nothing has caught it — a differential probe on set behaviour passes.
Hard: the size is part of the by-value ABI class. A set narrower than 32
bytes is passed and returned differently, so this is not a front-end mapping
change. IR_SET_COPY and IR_SET_LIT both carry the constant in their
contract and every backend implements them.
Relation to the umbrella
Wired to [[umbrella-sizeof-is-one-answer]] as the one member where the size
oracle is not the defect — TypeSlotSize(tySet) is honest about what pxx
builds. This is the case where the layout itself is the thing to change,
which is worth having inside the umbrella precisely because it is the opposite
shape: fixing the oracles cannot touch it, and it would otherwise look done when
they are.
Prio
prio: 30 is its own intrinsic worth — a real efficiency defect, no wrong value,
no program blocked. It inherits p75 from the umbrella, which is the number that
should route it.
Built and banked, 2026-09-02 (frankA) — the park stands, the work is on disk
The owner's smallset design above was implemented while this ticket was being
parked; the two crossed in flight. It is not landed — a park is a live
decision and this is a fork of intent, so the tree is back at 32 bytes and the
change sits as a patch:
devdocs/dev/parked-patches/smallset-4-byte-set-storage-class.patch # 22 files, applies clean at ff62bb870
Measured at ff62bb870 with the patch applied (converged after 1 round(s),
binary d374f4a8bdd7): the new test/test_small_set_width.pas matches the FPC
3.2.2 oracle byte-for-byte natively and on i386, aarch64, arm32 and riscv32;
a 57-test set corpus gives 50 same / 1 diff (test_rtti InstanceSize 80 → 48,
the intended win) / 0 new failures; 32/32 cross rows SAME; gate.sh quick
GREEN with the FPC seed canary PASS. Positive controls both ways:
test_set_subrange binaries DIFFER on all four targets, test_sets and
test_const_set are byte-identical on all four, and the new test FAILS against
the pre-change compiler. Whether that changes the park is the owner's call —
[[decide-a-the-smallset-mechanism-is-built-and-green-does-that-change-the-park]]
has the full table and the counter-argument.
One measurement worth keeping whatever is decided
FPC's Include/Exclude with a variable element is two steps, not one: fold
the element to a BYTE (and 255), then skip the write if that byte index is
outside this set's storage. Measured on the raw bytes of the set object, same
at -O2 and -O-: on set of 0..7, Include(s, 100) is a no-op while
Include(s, 20) does set bit 20; on set of 0..255, Include(s, 300) sets
bit 44. So the boundary is the STORAGE width, not the declared high bound, and
pxx already agrees with FPC on every row that a 32-byte set can reach.
It stops agreeing the moment a set is 4 bytes: the naive narrowing — and elemMask — folds an out-of-storage element onto a valid bit (Include(s, 100) sets bit 4, since 100 and 31 = 4) and looks correct in every existing
test, because no current test can distinguish it. A future attempt at this
ticket that does not carry a range GUARD will ship that silently.