← board

The smallset mechanism is built and green — does that change the park?

The fork

The park is explicit and dated: "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." (owner, 2026-09-02).

I am not disputing that. I raise it because the rainy-day ticket states the park's own premise, and the premise is the part that moved:

THE MECHANISM IS ALREADY DESIGNED, and it is cheaper than the estimate this was parked on. … the owner judged the overhead not worth it now.

The overhead is spent. The implementation below was already complete and measured when the park landed — the two crossed in flight, not in disagreement. So the decision in front of the owner is narrower than the one that was made: not "is narrowing worth building?" but "now that it is built and green, is it worth carrying?"

Either answer is cheap from here. Landing is git apply. Declining costs a rm of the patch and this file, and the rainy-day ticket already records the whole thing correctly.

What was built

Exactly the owner's design, no variable-width sets: a second type kind tySmallSet (ordinal 32) as the NARROW storage class, 4 bytes, chosen when the element's high ordinal ≤ 31 — modelled on the codebase's own tyPromoInt32/tyPromoInt64 precedent. Source spelling is unchanged; set of 0..7 is still written set of 0..7.

Measured — all at ff62bb870, patch applied, converged after 1 round(s), binary d374f4a8bdd7

what result
FPC 3.2.2 oracle, test/test_small_set_width.pas (18 rows) identical, native
same test, i386 / aarch64 / arm32 / riscv32 under qemu identical to the FPC oracle on all four
57-test set corpus, base vs patched, output equality 50 same, 1 diff, 0 new failures, 6 both-fail
the 1 diff test_rtti InstanceSize: 80 → 48 — the intended win, hand-verified (8+8+8+4+4+pad+16), asserted nowhere
the 6 both-fail 4 negative *_fail tests + macronest_fpcmode + aoc_ovl_unit_fmt; diagnostics byte-identical in both arms
8 set tests × 4 cross targets, base vs patched 32/32 SAME, rc=0
tools/gate.sh quick GREEN, and the FPC seed canary PASS (ran uncommitted, so it was not the SKIP path)

Positive controls, because a sweep of SAMEs proves nothing on its own. test_set_subrange binaries DIFFER on all four cross targets (1486 / 1907 / 2028 / 3213 bytes) — the narrow path fires — while test_sets and test_const_set are byte-identical on all four, which is the wide class demonstrably unchanged. And the new test FAILS against the pre-change compiler (sz 32 32 32 32 32 32 / rec 48 arr 96 against the expected sz 4 4 4 32 32 32 / rec 12 arr 12), so it is a test that can fail.

Two real bugs the loud-failure design caught, worth reading even if the answer is no

1. A pointer-sized slot held a 4-byte size. On x86-64/aarch64/arm32 ABIParamSlotHoldsValueAddr is True for sets — the slot holds the value's ADDRESS — but ParamValueSize returned the TYPE's 4 bytes, so an 8-byte pointer went into a 4-byte slot and test_set_default_param_b282 segfaulted. Invisible before because 32 ≥ 8 on every target; the fix is a pointer floor in ParamSlotWordSize, ParamValueSize and AllocParam's alignment. Any future type narrower than a pointer in an address-passing ABI class hits this, so it is worth knowing regardless of this decision.

2. lib/rtl/typinfo.pas's RTTI width table had no row for the new kind and fell through to the 8-byte default, so a published set of TColor property read the four bytes AFTER the field (test_streaming_enumset printed Colors=8484786405250170885 for 5) — and SetOrdProp uses the same width to choose its STORE size, so the write direction would have clobbered the neighbour. A width table keyed by kind is a second copy of the sizing rule and it goes stale silently.

What the measurement says about FPC, and this part stands whatever is decided

Chasing the last diverging row produced a first-hand result the rainy-day ticket does not have, and it corrects an assumption in our own lowering.

FPC's Include/Exclude with a variable element is TWO steps, not one: fold the element to a BYTE (and 255 — a set element is a byte ordinal in FPC's model, 256 elements max), then skip the write entirely if that byte index lies outside this set's storage. Measured on the raw bytes of the set object, 2026-09-02, identical at -O2 and -O-:

FPC pxx today
set of 0..7, Include(s, 20) sets bit 20 sets bit 20
set of 0..7, Include(s, 100) no-op sets bit 100
set of 0..255, Include(s, 300) sets bit 44 sets bit 44

So the boundary FPC enforces is the storage width, not the declared high bound — Include(s, 20) on a set of 0..7 genuinely does set bit 20 in both compilers, which is pre-existing shared behaviour and not a defect of either. Only the out-of-storage case differs, and today it is unreachable in pxx because every set is 32 bytes: the offending index lands inside the object. It becomes reachable the moment a set is 4 bytes, which is why the patch carries the guard, emitted only for the narrow class so wide-set codegen stays byte-exact.

This is not an argument for landing. It is the reason the patch is not a naive width change, and it is the piece a future attempt would otherwise have to rediscover — a plain and elemMask folds Include(s, 100) onto bit 4 and looks correct in every existing test.

Recommendation

Land it, but only because the cost that justified the park is already sunk and the evidence is unusually complete for an ABI-class change: five targets against a real oracle, a positive control in both directions, and a green gate. It delivers the three things the decide named — FPC-identical SizeOf, blittable records for file of T, ESP memory — and removes the doc obligation to advise against records-with-sets for file IO.

The counter-argument is real and I am not hiding it: the owner accepted that constraint knowingly, feature-pascal-typed-and-untyped-files and a Track D doc obligation were written on it hours ago, and landing this makes those freshly-written words wrong. That is a coordination cost the measurements above say nothing about, and it is the owner's call, not mine.

If the answer is no: rm devdocs/dev/parked-patches/smallset-4-byte-set-storage-class.patch and this ticket. Nothing else needs undoing — the tree is at 32 bytes and the rainy-day ticket is the record.

How to land it, if that is the answer

git apply devdocs/dev/parked-patches/smallset-4-byte-set-storage-class.patch
rm -f compiler/.pascal26.fixedpoint && make compiler/pascal26   # expect: converged after 1 round(s)
tools/gate.sh quick                                            # run BEFORE committing: the FPC seed canary only fires on a dirty tree

The patch already contains the regression test and its two Makefile rows (native pinned to the FPC literal; i386 pinned to native).