Eight occupied flags can fit in one byte value when objects already live in fixed slots. The useful part is not merely finding a free bit: the game must keep that bit set until the object is actually released.

Include and initialise
Reserve public S/R/O/U/M/Q, private I/K/T and lines 3000–3270. There are no arrays. Private I must not be a caller’s outer FOR variable. Combine routines.bas and the caller using build.py and prevent fall-through with END, STOP or a jump. C64/BBC use GOSUB; Spectrum uses GO SUB.
Call 3000 once at an intended pool-reset boundary. It clears every public field. U is the retained occupied mask: bit zero belongs to slot zero, through bit seven for slot seven. Initialise any real object payloads separately.
Allocate or release
Set M=0 and call 3100 to allocate the lowest free slot. Success sets its occupied bit and returns S=0..7,R=1,O=1. A full mask returns S=255,R=0,O=1. The mask survives a failed allocation.
Set M=1 and Q to the slot zero through seven to release. Releasing an occupied slot clears its bit and returns that slot with R=1,O=1. An already free slot is a valid no-op with S=255,R=0. Allocation ignores Q’s slot meaning, but Q must still be an integral byte.
Reusable numbered routines
3000 LET S=0: LET R=0: LET O=0: LET U=0: LET M=0: LET Q=0: RETURN
3100 LET S=255: LET R=0: LET O=0
3110 IF U<0 OR U>255 OR U<>INT (U) THEN RETURN
3120 IF Q<0 OR Q>255 OR Q<>INT (Q) THEN RETURN
3130 IF M<>0 AND M<>1 THEN RETURN
3140 IF M=1 AND Q>7 THEN RETURN
3150 LET O=1
3160 IF M=1 THEN GOTO 3250
3170 LET K=1
3180 FOR I=0 TO 7
3190 LET T=INT (U/K)
3200 IF T-2*INT (T/2)=0 THEN LET U=U+K: LET S=I: LET R=1: RETURN
3210 LET K=K*2: NEXT I: RETURN
3250 LET K=2^Q: LET T=INT (U/K)
3260 IF T-2*INT (T/2)=0 THEN RETURN
3270 LET U=U-K: LET S=Q: LET R=1: RETURN| Fields | Use |
|---|---|
| U | Retained occupied mask |
| M / Q | Operation / requested release slot |
| S / R / O | Returned slot / successful change / valid input |
U/Q must be integral bytes and M must be zero or one. Invalid state, mode or release index returns S=255,R=0,O=0 without changing U/M/Q. Check O/R before using S: the sentinel 255 is not an array subscript. Every service replaces the result flags, so consume them immediately.
Retain live occupancy
The normal caller starts empty and allocates slots zero then one. The custom caller starts with every slot except zero already occupied, so its second allocation reports full. Both release zero and allocate again. The early checks retain the second allocation result and mask.
Example calls from the maintained caller
70 GOSUB 3000: LET AA=0: LET BB=0
80 LET U=0
90 LET M=0: LET Q=0: GOSUB 3100: GOSUB 2000
100 LET M=0: LET Q=0: GOSUB 3100: GOSUB 2000
110 IF S=1 THEN LET AA=1
120 IF U=3 THEN LET BB=1
130 LET M=1: LET Q=0: GOSUB 3100: GOSUB 2000
140 LET M=0: LET Q=0: GOSUB 3100: GOSUB 2000
150 PRINT "SLOT RESULTS";AA;BB
160 ?(MB+995)=AA: ?(MB+996)=BB: ?(MB+997)=GD: ?(MB+998)=WC-256*INT (WC/256): ?(MB+999)=INT (WC/256): ?(MB+1000)=239: STOPLine 2000, AA/BB/JJ (Z on Spectrum) and the mailbox are diagnostics. A game initialises all payload fields after a successful allocation, before other code reads that object. Releasing occupancy does not clear positions, velocities, sprites, sound ownership or old references. A reused slot number can name a different object; references that outlive release need an identity policy such as generations.
Compare the array-based pool
The existing object-pool routine also maintains numeric flags and optional lifetime ageing. This compact allocator does not provide lifetimes or ageing. It replaces eight occupancy values with one mask value, while payload storage remains elsewhere. In BASIC, that mask is still an interpreter numeric variable, not a one-byte allocation.
A free list can reduce repeated searches for a larger pool, at the cost of more state and invariants. A wider bitset needs wider arithmetic or several groups. Separate pools need renamed fields/line references or a context wrapper. Give occupancy and object creation one owner.
Mistake and repair
The wrong caller clears U before every service. Its second allocation reuses zero even though the first object is still live. Both saved slot and mask checks fail. Initialise once and retain occupancy until a genuine release. Repaired source and loaded program match normal exactly.

Cost and Maths for Games
Allocation tests at most eight bits; release tests one selected bit. There is no payload scan, allocation from a heap or device call. Fewer occupancy values trade storage for arithmetic; measure the complete component rather than assuming that one compact field makes the whole game smaller or faster.
Maths for Games connects sets, bit positions and bounded search. The Craft connects object budgets and safe reuse to predictable game behaviour.
Global names and line numbers
BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.
Machine and measured size
This helper uses scalar numeric fields and allocates no arrays. Its diagnostic uses MODE 7 and HIMEM=&6000 to separate program storage, mailbox and screen. The helper itself changes neither mode nor HIMEM.
The numbered helper occupies 396 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, JJ and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through BBC Micro’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.
An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.
Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-decisions.py bitset-slot-allocation bbc-micro --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-decisions.py bitset-slot-allocation.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.