@zakkster/lite-fastbit32 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/FastBit32.d.ts CHANGED
@@ -84,12 +84,6 @@ export class FastBit32 {
84
84
  * Equivalent to `(value & mask) === mask`.
85
85
  *
86
86
  * @param mask - Bitmask to test against.
87
- *
88
- * @example
89
- * ```js
90
- * const PHYSICS = (1 << 1) | (1 << 4);
91
- * if (entity.hasAll(PHYSICS)) { /* run physics *\/ }
92
- * ```
93
87
  */
94
88
  hasAll(mask: number): boolean;
95
89
 
@@ -194,6 +188,17 @@ export class FastBit32 {
194
188
  */
195
189
  clone(): FastBit32;
196
190
 
191
+ // ── Iteration ───────────────────────────────────────
192
+
193
+ /**
194
+ * O(k) iteration over active bits, where k is the number of set bits.
195
+ * Visits each active bit index in ascending order.
196
+ *
197
+ * @param callback - Called with the bit index of each active bit.
198
+ * @returns `this` for chaining.
199
+ */
200
+ forEach(callback: (bit: number) => void): this;
201
+
197
202
  // ── Serialization ───────────────────────────────────
198
203
 
199
204
  /**
@@ -212,3 +217,151 @@ export class FastBit32 {
212
217
  */
213
218
  static deserialize(value: number): FastBit32;
214
219
  }
220
+
221
+ /**
222
+ * BitMapper — The Human-to-Hardware Bridge.
223
+ *
224
+ * Translates semantic string names into 32-bit integer indices and raw masks.
225
+ * Protects developers from raw integer math while keeping the engine hot-path fast.
226
+ */
227
+ export class BitMapper {
228
+ /**
229
+ * Creates a new BitMapper dictionary.
230
+ *
231
+ * @param names - Array of component/flag names (Maximum 32).
232
+ * @throws Error if more than 32 flags are provided.
233
+ */
234
+ constructor(names?: string[]);
235
+
236
+ /**
237
+ * Gets the raw bit index (0-31) for a specific flag name.
238
+ *
239
+ * @param name - The semantic string name of the flag.
240
+ * @returns The integer bit index.
241
+ * @throws Error if the flag name is not registered.
242
+ */
243
+ get(name: string): number;
244
+
245
+ /**
246
+ * Generates a raw 32-bit integer mask from an array of flag names.
247
+ * Ideal for building System signatures in an ECS.
248
+ *
249
+ * @param names - Array of semantic flag names to combine into a mask.
250
+ * @returns The generated unsigned 32-bit mask.
251
+ */
252
+ getMask(names: string[]): number;
253
+
254
+ /**
255
+ * Helper to extract which semantic strings are active inside a FastBit32 instance.
256
+ * Excellent for console.log debugging.
257
+ *
258
+ * @param fastBit32Instance - The FastBit32 instance to evaluate.
259
+ * @returns An array of active string names.
260
+ */
261
+ getActiveNames(fastBit32Instance: FastBit32): string[];
262
+
263
+ /**
264
+ * O(1) reverse lookup — integer bit index to string name.
265
+ *
266
+ * @param bit - The bit index (0–31).
267
+ * @returns The registered name, or `undefined` if no name is registered at that index.
268
+ */
269
+ getName(bit: number): string | undefined;
270
+ }
271
+
272
+ // ── O(k) Iteration Helpers ─────────────────────────────────
273
+
274
+ /**
275
+ * Iterates active bits in a mask, calling back with the corresponding array element.
276
+ *
277
+ * @param mask - FastBit32 instance whose active bits select array indices.
278
+ * @param array - Data array indexed by bit position.
279
+ * @param callback - Called with `(element, bitIndex)` for each active bit.
280
+ */
281
+ export function forEachArray<T>(
282
+ mask: FastBit32,
283
+ array: T[],
284
+ callback: (element: T, bit: number) => void
285
+ ): void;
286
+
287
+ /**
288
+ * Iterates active bits, mapping through a keys array to object properties.
289
+ *
290
+ * @param mask - FastBit32 instance whose active bits select key indices.
291
+ * @param keys - Array of property keys indexed by bit position.
292
+ * @param obj - Object to read values from.
293
+ * @param callback - Called with `(value, key, bitIndex)` for each active bit.
294
+ */
295
+ export function forEachObject<T>(
296
+ mask: FastBit32,
297
+ keys: string[],
298
+ obj: Record<string, T>,
299
+ callback: (value: T, key: string, bit: number) => void
300
+ ): void;
301
+
302
+ /**
303
+ * Iterates active bits, resolving each to a string name via a BitMapper.
304
+ *
305
+ * @param mask - FastBit32 instance.
306
+ * @param mapper - BitMapper for reverse lookup.
307
+ * @param callback - Called with `(name, bitIndex)` for each active bit.
308
+ */
309
+ export function forEachMapped(
310
+ mask: FastBit32,
311
+ mapper: BitMapper,
312
+ callback: (name: string, bit: number) => void
313
+ ): void;
314
+
315
+ /**
316
+ * Iterates active bits, resolving names via BitMapper and reading object values.
317
+ *
318
+ * @param mask - FastBit32 instance.
319
+ * @param mapper - BitMapper for reverse lookup.
320
+ * @param obj - Object to read values from.
321
+ * @param callback - Called with `(value, key, bitIndex)` for each active bit.
322
+ */
323
+ export function forEachMappedObject<T>(
324
+ mask: FastBit32,
325
+ mapper: BitMapper,
326
+ obj: Record<string, T>,
327
+ callback: (value: T, key: string, bit: number) => void
328
+ ): void;
329
+
330
+ /**
331
+ * Iterates bits that are active in BOTH masks (intersection / AND).
332
+ *
333
+ * @param maskA - First FastBit32 instance.
334
+ * @param maskB - Second FastBit32 instance.
335
+ * @param callback - Called with `(bitIndex)` for each shared active bit.
336
+ */
337
+ export function forEachMaskPair(
338
+ maskA: FastBit32,
339
+ maskB: FastBit32,
340
+ callback: (bit: number) => void
341
+ ): void;
342
+
343
+ /**
344
+ * Iterates bits active in A but not in B (difference / A AND NOT B).
345
+ *
346
+ * @param maskA - First FastBit32 instance.
347
+ * @param maskB - Second FastBit32 instance.
348
+ * @param callback - Called with `(bitIndex)` for each bit in A not in B.
349
+ */
350
+ export function forEachMaskDiff(
351
+ maskA: FastBit32,
352
+ maskB: FastBit32,
353
+ callback: (bit: number) => void
354
+ ): void;
355
+
356
+ /**
357
+ * Iterates bits active in either mask (union / OR).
358
+ *
359
+ * @param maskA - First FastBit32 instance.
360
+ * @param maskB - Second FastBit32 instance.
361
+ * @param callback - Called with `(bitIndex)` for each active bit in A or B.
362
+ */
363
+ export function forEachMaskUnion(
364
+ maskA: FastBit32,
365
+ maskB: FastBit32,
366
+ callback: (bit: number) => void
367
+ ): void;
package/Fastbit32.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager
3
+ * Engineered for 60fps hot-path execution, ECS masking, and object pooling.
4
+ * * ⚠️ ENGINE PRIMITIVE CAVEATS:
5
+ * - SILENT WRAPAROUND: JS bitwise shifts implicitly apply modulo 32.
6
+ * Calling `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
7
+ * - TRUNCATION: Floats and negative numbers are silently truncated and
8
+ * converted to unsigned 32-bit integers (e.g., `-1 >>> 0` becomes `4294967295`).
9
+ * Sanitize your inputs upstream if your domain logic requires strict bounds!
10
+ */
11
+ export class Fastbit32 {
12
+ constructor(initial = 0) {
13
+ this.value = initial >>> 0;
14
+ }
15
+
16
+ add(bit) {
17
+ this.value |= (1 << bit);
18
+ return this;
19
+ }
20
+
21
+ remove(bit) {
22
+ this.value &= ~(1 << bit);
23
+ return this;
24
+ }
25
+
26
+ toggle(bit) {
27
+ this.value ^= (1 << bit);
28
+ return this;
29
+ }
30
+
31
+ has(bit) {
32
+ return (this.value & (1 << bit)) !== 0;
33
+ }
34
+
35
+ hasAll(mask) {
36
+ return (this.value & mask) === mask;
37
+ }
38
+
39
+ hasAny(mask) {
40
+ return (this.value & mask) !== 0;
41
+ }
42
+
43
+ hasNone(mask) {
44
+ return (this.value & mask) === 0;
45
+ }
46
+
47
+ clear() {
48
+ this.value = 0;
49
+ return this;
50
+ }
51
+
52
+ union(mask) {
53
+ this.value |= mask;
54
+ return this;
55
+ }
56
+
57
+ difference(mask) {
58
+ this.value &= ~mask;
59
+ return this;
60
+ }
61
+
62
+ intersect(mask) {
63
+ this.value &= mask;
64
+ return this;
65
+ }
66
+
67
+ // ── Advanced AAA Engine Helpers ──────────────────────────
68
+
69
+ /**
70
+ * O(1) loop-free popcount (Hamming Weight).
71
+ * Returns the number of active bits (e.g., how many flags are active).
72
+ */
73
+ count() {
74
+ let v = this.value;
75
+ v = v - ((v >>> 1) & 0x55555555);
76
+ v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
77
+ return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
78
+ }
79
+
80
+ /**
81
+ * O(1) loop-free popcount applying a mask first.
82
+ * Useful for counting active flags within a specific subsystem.
83
+ */
84
+ countMasked(mask) {
85
+ let v = this.value & mask;
86
+ v = v - ((v >>> 1) & 0x55555555);
87
+ v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
88
+ return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
89
+ }
90
+
91
+ /**
92
+ * Instantly finds the index of the lowest active bit.
93
+ * Incredibly useful for Object Pools (finding the first available slot).
94
+ * Returns -1 if empty.
95
+ */
96
+ lowest() {
97
+ if (this.value === 0) return -1;
98
+ // Isolate lowest set bit, then use native Count Leading Zeros to find index
99
+ return Math.clz32(this.value & -this.value) ^ 31;
100
+ }
101
+
102
+ /**
103
+ * Instantly finds the index of the highest active bit.
104
+ * Useful for determining the bounds of active arrays/spatial grids.
105
+ * Returns -1 if empty.
106
+ */
107
+ highest() {
108
+ if (this.value === 0) return -1;
109
+ return 31 - Math.clz32(this.value);
110
+ }
111
+
112
+ isEmpty() {
113
+ return this.value === 0;
114
+ }
115
+
116
+ clone() {
117
+ return new Fastbit32(this.value);
118
+ }
119
+
120
+ // ── Serialization (For ECS Save States) ──────────────────
121
+
122
+ /**
123
+ * Exports the raw 32-bit integer for ultra-lightweight JSON/binary storage.
124
+ */
125
+ serialize() {
126
+ return this.value;
127
+ }
128
+
129
+ /**
130
+ * Instantiates a new FastBit32 from a serialized integer.
131
+ */
132
+ static deserialize(value) {
133
+ return new Fastbit32(value);
134
+ }
135
+
136
+ /**
137
+ * O(k) loop-free iteration over active bits.
138
+ * @param {Function} callback - Called with (bit)
139
+ */
140
+ forEach(callback) {
141
+ let v = this.value;
142
+ while (v !== 0) {
143
+ const bit = Math.clz32(v & -v) ^ 31;
144
+ callback(bit);
145
+ v &= v - 1; // Clear lowest active bit
146
+ }
147
+ return this;
148
+ }
149
+ }
150
+
151
+ export class BitMapper {
152
+ constructor(names = []) {
153
+ if (names.length > 32) throw new Error('BitMapper: Maximum 32 flags supported.');
154
+
155
+ this._map = new Map();
156
+ this._reverse = new Array(32); // O(1) array lookup
157
+
158
+ names.forEach((name, index) => {
159
+ this._map.set(name, index);
160
+ this._reverse[index] = name;
161
+ });
162
+ }
163
+
164
+ get(name) {
165
+ const bit = this._map.get(name);
166
+ if (bit === undefined) throw new Error(`BitMapper: Unknown flag "${name}"`);
167
+ return bit;
168
+ }
169
+
170
+ getMask(names) {
171
+ let mask = 0;
172
+ for (let i = 0; i < names.length; i++) mask |= (1 << this.get(names[i]));
173
+ return mask >>> 0;
174
+ }
175
+
176
+ getActiveNames(fastBit32Instance) {
177
+ const active = [];
178
+ for (const [name, bit] of this._map.entries()) {
179
+ if (fastBit32Instance.has(bit)) active.push(name);
180
+ }
181
+ return active;
182
+ }
183
+
184
+ /**
185
+ * O(1) Reverse lookup. Integer -> String.
186
+ */
187
+ getName(bit) {
188
+ return this._reverse[bit];
189
+ }
190
+ }
191
+
192
+ // ── O(k) Iteration Helpers ─────────────────────────────────
193
+
194
+ export function forEachArray(mask, array, callback) {
195
+ let v = mask.value;
196
+ while (v !== 0) {
197
+ const bit = Math.clz32(v & -v) ^ 31;
198
+ callback(array[bit], bit);
199
+ v &= v - 1;
200
+ }
201
+ }
202
+
203
+ export function forEachObject(mask, keys, obj, callback) {
204
+ let v = mask.value;
205
+ while (v !== 0) {
206
+ const bit = Math.clz32(v & -v) ^ 31;
207
+ const key = keys[bit];
208
+ callback(obj[key], key, bit);
209
+ v &= v - 1;
210
+ }
211
+ }
212
+
213
+ export function forEachMapped(mask, mapper, callback) {
214
+ let v = mask.value;
215
+ while (v !== 0) {
216
+ const bit = Math.clz32(v & -v) ^ 31;
217
+ callback(mapper.getName(bit), bit);
218
+ v &= v - 1;
219
+ }
220
+ }
221
+
222
+ export function forEachMappedObject(mask, mapper, obj, callback) {
223
+ let v = mask.value;
224
+ while (v !== 0) {
225
+ const bit = Math.clz32(v & -v) ^ 31;
226
+ const key = mapper.getName(bit);
227
+ callback(obj[key], key, bit);
228
+ v &= v - 1;
229
+ }
230
+ }
231
+
232
+ export function forEachMaskPair(maskA, maskB, callback) {
233
+ let v = (maskA.value & maskB.value) >>> 0;
234
+ while (v !== 0) {
235
+ const bit = Math.clz32(v & -v) ^ 31;
236
+ callback(bit);
237
+ v &= v - 1;
238
+ }
239
+ }
240
+
241
+ export function forEachMaskDiff(maskA, maskB, callback) {
242
+ let v = (maskA.value & ~maskB.value) >>> 0;
243
+ while (v !== 0) {
244
+ const bit = Math.clz32(v & -v) ^ 31;
245
+ callback(bit);
246
+ v &= v - 1;
247
+ }
248
+ }
249
+
250
+ export function forEachMaskUnion(maskA, maskB, callback) {
251
+ let v = (maskA.value | maskB.value) >>> 0;
252
+ while (v !== 0) {
253
+ const bit = Math.clz32(v & -v) ^ 31;
254
+ callback(bit);
255
+ v &= v - 1;
256
+ }
257
+ }
package/README.md CHANGED
@@ -20,6 +20,8 @@ Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object poo
20
20
  | **Branchless** | **Yes** | No | No |
21
21
  | **O(1) popcount** | **Yes** | No | No |
22
22
  | **O(1) lowest/highest** | **Yes** | No | No |
23
+ | **O(k) iteration** | **Yes** | No | No |
24
+ | **BitMapper** | **Yes** | No | No |
23
25
  | **BigInt support** | No | No | No |
24
26
  | **ECS-ready** | **Yes** | Yes | Yes |
25
27
  | **Object pool scan** | **Yes** | No | No |
@@ -37,7 +39,7 @@ npm install @zakkster/lite-fastbit32
37
39
  ## Quick Start
38
40
 
39
41
  ```javascript
40
- import { FastBit32 } from '@zakkster/lite-fastbit32';
42
+ import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';
41
43
 
42
44
  const flags = new FastBit32();
43
45
 
@@ -47,6 +49,12 @@ flags.count(); // 2 — O(1) popcount
47
49
  flags.lowest(); // 1 — O(1) bit-scan forward
48
50
  flags.remove(1); // Clear bit 1
49
51
  flags.serialize(); // Raw uint32 for storage
52
+
53
+ // Human-readable flag management
54
+ const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
55
+ const entity = new FastBit32();
56
+ entity.add(mapper.get('Physics')).add(mapper.get('Input'));
57
+ mapper.getActiveNames(entity); // ['Physics', 'Input']
50
58
  ```
51
59
 
52
60
  ---
@@ -83,6 +91,10 @@ lowest = Math.clz32(value & -value) ^ 31
83
91
 
84
92
  `highest` uses `31 - Math.clz32(value)` directly.
85
93
 
94
+ ### O(k) Iteration
95
+
96
+ All iteration helpers visit only active bits using the `v &= v - 1` trick to clear the lowest bit each step. Complexity is O(k) where k is the number of set bits — not 32.
97
+
86
98
  ### Caveats
87
99
 
88
100
  - **Silent wraparound:** JS bitwise shifts apply modulo 32. `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
@@ -131,7 +143,7 @@ Tested on Apple M2 Pro, Node 22, V8 12.x. All values in **ops/ms**.
131
143
  ## Recipes
132
144
 
133
145
  <details>
134
- <summary><strong>🎮 ECS Component Masks</strong></summary>
146
+ <summary><strong>ECS Component Masks</strong></summary>
135
147
 
136
148
  ```javascript
137
149
  import { FastBit32 } from '@zakkster/lite-fastbit32';
@@ -152,32 +164,23 @@ if (entity.hasAll(PHYSICS_QUERY)) runPhysics(entity);
152
164
  if (entity.hasAll(RENDER_QUERY)) drawSprite(entity);
153
165
  ```
154
166
 
155
- > **⚠️ Bit 31 (Sign Bit) Warning:** In JavaScript, `1 << 31` evaluates to `-2147483648` — a negative number. FastBit32 handles this correctly under the hood, but if you log raw mask values to the console, you will see negative integers and assume a bug. This also affects `serialize()`: masks using bit 31 produce negative numbers in JSON. **Recommendation:** Keep ECS component indices to 0–30 (31 components). If you must use all 32, compare serialized values with `>>> 0` to force unsigned representation.
167
+ > **Bit 31 (Sign Bit) Warning:** In JavaScript, `1 << 31` evaluates to `-2147483648` — a negative number. FastBit32 handles this correctly under the hood, but if you log raw mask values to the console, you will see negative integers and assume a bug. This also affects `serialize()`: masks using bit 31 produce negative numbers in JSON. **Recommendation:** Keep ECS component indices to 0–30 (31 components). If you must use all 32, compare serialized values with `>>> 0` to force unsigned representation.
156
168
 
157
169
  </details>
158
170
 
159
171
  <details>
160
- <summary><strong>🏊 Object Pool — First Free Slot</strong></summary>
172
+ <summary><strong>Object Pool — First Free Slot</strong></summary>
161
173
 
162
174
  ```javascript
163
175
  import { FastBit32 } from '@zakkster/lite-fastbit32';
164
176
 
165
- // Bit = 1 means slot is occupied
166
177
  const pool = new FastBit32();
167
178
  const objects = new Array(32);
168
179
 
169
180
  function allocate() {
170
- // Invert to find free slots, mask to pool size
171
181
  const free = new FastBit32(~pool.value & 0xFFFFFFFF);
172
182
  const slot = free.lowest();
173
-
174
- // ⚠️ CRITICAL: Always check for -1.
175
- // If the pool is full, lowest() returns -1.
176
- // Accessing objects[-1] bypasses V8's array bounds optimization,
177
- // triggering a dictionary-mode fallback on the entire array —
178
- // a massive de-optimization penalty that persists for the array's lifetime.
179
- if (slot === -1) return null; // Pool exhausted — expand or drop
180
-
183
+ if (slot === -1) return null;
181
184
  pool.add(slot);
182
185
  return slot;
183
186
  }
@@ -195,7 +198,7 @@ allocate(); // 0 — immediately reused
195
198
  </details>
196
199
 
197
200
  <details>
198
- <summary><strong>🎹 Input State Manager</strong></summary>
201
+ <summary><strong>Input State Manager</strong></summary>
199
202
 
200
203
  ```javascript
201
204
  import { FastBit32 } from '@zakkster/lite-fastbit32';
@@ -219,7 +222,6 @@ window.addEventListener('keyup', e => {
219
222
  if (e.code === 'Space') input.remove(KEY_JUMP);
220
223
  });
221
224
 
222
- // In game loop — zero-branch checks
223
225
  if (input.has(KEY_JUMP)) jump();
224
226
  if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
225
227
  ```
@@ -227,56 +229,7 @@ if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
227
229
  </details>
228
230
 
229
231
  <details>
230
- <summary><strong>💥 Collision Layer Masks</strong></summary>
231
-
232
- ```javascript
233
- import { FastBit32 } from '@zakkster/lite-fastbit32';
234
-
235
- const LAYER_PLAYER = 0;
236
- const LAYER_ENEMY = 1;
237
- const LAYER_BULLET = 2;
238
- const LAYER_WALL = 3;
239
- const LAYER_PICKUP = 4;
240
-
241
- const playerMask = new FastBit32();
242
- playerMask.add(LAYER_ENEMY).add(LAYER_PICKUP).add(LAYER_WALL);
243
-
244
- const bulletMask = new FastBit32();
245
- bulletMask.add(LAYER_ENEMY).add(LAYER_WALL);
246
-
247
- function canCollide(entityLayer, targetMask) {
248
- return targetMask.has(entityLayer);
249
- }
250
- ```
251
-
252
- </details>
253
-
254
- <details>
255
- <summary><strong>🌐 Networking Flags</strong></summary>
256
-
257
- ```javascript
258
- import { FastBit32 } from '@zakkster/lite-fastbit32';
259
-
260
- const FLAG_RELIABLE = 0;
261
- const FLAG_ORDERED = 1;
262
- const FLAG_COMPRESSED = 2;
263
- const FLAG_ENCRYPTED = 3;
264
-
265
- const packet = new FastBit32();
266
- packet.add(FLAG_RELIABLE).add(FLAG_ENCRYPTED);
267
-
268
- const raw = packet.serialize(); // Send as uint32
269
- // ... network transport ...
270
- const restored = FastBit32.deserialize(raw);
271
- if (restored.has(FLAG_RELIABLE)) ack(packet);
272
- ```
273
-
274
- </details>
275
-
276
- <details>
277
- <summary><strong>🧠 State Machine — Mutex Flags</strong></summary>
278
-
279
- In a strict FSM, only one state should be active at any time. Never use `remove(OLD).add(NEW)` — if the remove and add target the same bit index by mistake, you silently corrupt the state. Use `.clear().add()` to guarantee zero overlapping bits.
232
+ <summary><strong>State Machine Mutex Flags</strong></summary>
280
233
 
281
234
  ```javascript
282
235
  import { FastBit32 } from '@zakkster/lite-fastbit32';
@@ -290,39 +243,60 @@ const INVINCIBLE = 4;
290
243
  const state = new FastBit32();
291
244
  state.add(IDLE);
292
245
 
293
- // ✅ CORRECT — mutex transition: clear ALL bits, then set the new state.
294
- // Guarantees zero overlap regardless of previous state.
295
246
  function startAttack() {
296
247
  state.clear().add(ATTACKING).add(INVINCIBLE);
297
248
  }
298
249
 
299
- // ✅ CORRECT — return to single state after compound state ends.
300
250
  function endAttack() {
301
251
  state.clear().add(IDLE);
302
252
  }
303
253
 
304
- // ❌ WRONG — remove/add leaves stale bits if you forget one:
305
- // state.remove(ATTACKING).remove(INVINCIBLE).add(IDLE);
306
- // If INVINCIBLE was already cleared by another system, this still "works"
307
- // but masks a logic error. clear() is unconditional and safe.
308
-
309
254
  console.log(state.count()); // 1 — proof of mutex
310
255
  ```
311
256
 
312
257
  </details>
313
258
 
314
259
  <details>
315
- <summary><strong>💾 ECS Save State</strong></summary>
260
+ <summary><strong>BitMapper + Iteration (v1.1.0)</strong></summary>
316
261
 
317
262
  ```javascript
318
- import { FastBit32 } from '@zakkster/lite-fastbit32';
263
+ import { FastBit32, BitMapper, forEachMapped, forEachMappedObject } from '@zakkster/lite-fastbit32';
319
264
 
320
- // Save
321
- const entities = [entityA.serialize(), entityB.serialize()];
322
- const json = JSON.stringify(entities); // "[18, 7]" — bytes, not objects
265
+ const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
266
+ const entity = new FastBit32();
267
+ entity.add(components.get('Position')).add(components.get('AI'));
268
+
269
+ console.log(components.getActiveNames(entity)); // ['Position', 'AI']
323
270
 
324
- // Load
325
- const restored = JSON.parse(json).map(FastBit32.deserialize);
271
+ forEachMapped(entity, components, (name, bit) => {
272
+ console.log(`Component ${name} at bit ${bit}`);
273
+ });
274
+
275
+ const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
276
+ forEachMappedObject(entity, components, systems, (system, name) => {
277
+ system(entity);
278
+ });
279
+ ```
280
+
281
+ </details>
282
+
283
+ <details>
284
+ <summary><strong>Mask Set Operations (v1.1.0)</strong></summary>
285
+
286
+ ```javascript
287
+ import { FastBit32, forEachMaskPair, forEachMaskDiff, forEachMaskUnion } from '@zakkster/lite-fastbit32';
288
+
289
+ const required = new FastBit32().add(0).add(1).add(3);
290
+ const available = new FastBit32().add(0).add(3).add(5);
291
+
292
+ forEachMaskPair(required, available, bit => console.log('matched:', bit));
293
+ // matched: 0, matched: 3
294
+
295
+ forEachMaskDiff(required, available, bit => console.log('missing:', bit));
296
+ // missing: 1
297
+
298
+ forEachMaskUnion(required, available, bit => console.log('all:', bit));
299
+ // all: 0, all: 1, all: 3, all: 5
326
300
  ```
327
301
 
328
302
  </details>
@@ -373,6 +347,12 @@ const restored = JSON.parse(json).map(FastBit32.deserialize);
373
347
  | `.highest()` | `number` | O(1) index of most significant active bit. `-1` if empty. |
374
348
  | `.isEmpty()` | `boolean` | True if value is `0`. |
375
349
 
350
+ ### Iteration
351
+
352
+ | Method | Returns | Description |
353
+ |---|---|---|
354
+ | `.forEach(callback)` | `this` | O(k) iteration over active bits. `callback(bit)`. |
355
+
376
356
  ### Utility
377
357
 
378
358
  | Method | Returns | Description |
@@ -381,6 +361,43 @@ const restored = JSON.parse(json).map(FastBit32.deserialize);
381
361
  | `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
382
362
  | `FastBit32.deserialize(n)` | `FastBit32` | Restore from a serialized uint32. |
383
363
 
364
+ ### `new BitMapper(names?)`
365
+
366
+ | Method | Returns | Description |
367
+ |---|---|---|
368
+ | `.get(name)` | `number` | Bit index for a flag name. Throws if unknown. |
369
+ | `.getMask(names)` | `number` | Combined uint32 mask from flag name array. |
370
+ | `.getActiveNames(fb32)` | `string[]` | Active flag names from a FastBit32 instance. |
371
+ | `.getName(bit)` | `string \| undefined` | O(1) reverse lookup — bit index to name. |
372
+
373
+ ### Standalone Iteration Helpers
374
+
375
+ | Function | Description |
376
+ |---|---|
377
+ | `forEachArray(mask, array, cb)` | `cb(element, bit)` for each active bit. |
378
+ | `forEachObject(mask, keys, obj, cb)` | `cb(value, key, bit)` via keys array. |
379
+ | `forEachMapped(mask, mapper, cb)` | `cb(name, bit)` via BitMapper. |
380
+ | `forEachMappedObject(mask, mapper, obj, cb)` | `cb(value, key, bit)` via BitMapper + object. |
381
+ | `forEachMaskPair(maskA, maskB, cb)` | `cb(bit)` for intersection (A & B). |
382
+ | `forEachMaskDiff(maskA, maskB, cb)` | `cb(bit)` for difference (A & ~B). |
383
+ | `forEachMaskUnion(maskA, maskB, cb)` | `cb(bit)` for union (A \| B). |
384
+
385
+ ---
386
+
387
+ ## Changelog
388
+
389
+ ### v1.1.0
390
+
391
+ **New: BitMapper** — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via `getName(bit)`.
392
+
393
+ **New: `forEach(callback)`** — O(k) iteration on FastBit32 instances. Visits only active bits in ascending order using `v &= v - 1` bit-clearing. Returns `this` for chaining.
394
+
395
+ **New: 7 standalone iteration helpers** — `forEachArray`, `forEachObject`, `forEachMapped`, `forEachMappedObject`, `forEachMaskPair`, `forEachMaskDiff`, `forEachMaskUnion`. All O(k). Connect masks directly to arrays, objects, BitMapper dictionaries, and mask set operations without intermediate allocations.
396
+
397
+ ### v1.0.0
398
+
399
+ Initial release. FastBit32 core: single-bit ops, bulk mask ops, in-place set math, O(1) popcount, O(1) bit-scan (lowest/highest), clone, serialize/deserialize.
400
+
384
401
  ---
385
402
 
386
403
  ## License
@@ -389,4 +406,4 @@ MIT
389
406
 
390
407
  ## Part of the @zakkster ecosystem
391
408
 
392
- Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.
409
+ Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @zakkster/lite-fastbit32 v1.0.0
1
+ # @zakkster/lite-fastbit32 v1.1.0
2
2
 
3
3
  > Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code.
4
4
 
@@ -13,7 +13,13 @@ FastBit32 is a single-class library that wraps a plain unsigned 32-bit integer w
13
13
  ## Import
14
14
 
15
15
  ```javascript
16
- import { FastBit32 } from '@zakkster/lite-fastbit32';
16
+ import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';
17
+
18
+ // Standalone iteration helpers
19
+ import {
20
+ forEachArray, forEachObject, forEachMapped,
21
+ forEachMappedObject, forEachMaskPair, forEachMaskDiff, forEachMaskUnion
22
+ } from '@zakkster/lite-fastbit32';
17
23
  ```
18
24
 
19
25
  ## Constructor
@@ -67,6 +73,12 @@ All mutating methods return `this` for chaining unless noted otherwise.
67
73
  | `.highest()` | `number` | Index of most significant set bit (0–31). Returns `-1` if empty. |
68
74
  | `.isEmpty()` | `boolean` | True if value is 0. |
69
75
 
76
+ ### Iteration
77
+
78
+ | Method | Returns | Description |
79
+ |---|---|---|
80
+ | `.forEach(callback)` | `this` | O(k) iteration over active bits in ascending order. Callback receives `(bit)`. |
81
+
70
82
  ### Utility
71
83
 
72
84
  | Method | Returns | Description |
@@ -81,6 +93,70 @@ All mutating methods return `this` for chaining unless noted otherwise.
81
93
  |---|---|---|
82
94
  | `.value` | `number` | The raw 32-bit integer. Safe to read directly in hot paths. |
83
95
 
96
+ ## BitMapper — Human-to-Hardware Bridge
97
+
98
+ Translates semantic string names into bit indices and masks. Keeps engine hot paths fast while letting developers think in words.
99
+
100
+ ```javascript
101
+ const mapper = new BitMapper(['Position', 'Velocity', 'Health', 'Magic']);
102
+ ```
103
+
104
+ ### Constructor
105
+
106
+ ```javascript
107
+ new BitMapper(names?: string[])
108
+ ```
109
+
110
+ - `names` — Array of up to 32 flag names. Each name maps to its array index (0–31).
111
+ - Throws if more than 32 names are provided.
112
+
113
+ ### BitMapper Methods
114
+
115
+ | Method | Returns | Description |
116
+ |---|---|---|
117
+ | `.get(name)` | `number` | Bit index (0–31) for a flag name. Throws if unknown. |
118
+ | `.getMask(names)` | `number` | Combined uint32 mask from an array of flag names. |
119
+ | `.getActiveNames(fb32)` | `string[]` | Array of active flag names from a FastBit32 instance. |
120
+ | `.getName(bit)` | `string \| undefined` | O(1) reverse lookup — bit index to string name. |
121
+
122
+ ### BitMapper Example
123
+
124
+ ```javascript
125
+ const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
126
+ const entity = new FastBit32();
127
+ entity.add(mapper.get('Physics')).add(mapper.get('Input'));
128
+
129
+ mapper.getActiveNames(entity); // ['Physics', 'Input']
130
+
131
+ const PHYSICS_QUERY = mapper.getMask(['Physics', 'Render']);
132
+ entity.hasAll(PHYSICS_QUERY); // false — missing Render
133
+ ```
134
+
135
+ ## Standalone Iteration Helpers
136
+
137
+ All helpers are O(k) where k = number of active bits. No loops over 32 slots — only active bits are visited.
138
+
139
+ | Function | Signature | Description |
140
+ |---|---|---|
141
+ | `forEachArray` | `(mask, array, cb)` | Active bits → array indices. `cb(element, bit)` |
142
+ | `forEachObject` | `(mask, keys, obj, cb)` | Active bits → keys array → object values. `cb(value, key, bit)` |
143
+ | `forEachMapped` | `(mask, mapper, cb)` | Active bits → BitMapper names. `cb(name, bit)` |
144
+ | `forEachMappedObject` | `(mask, mapper, obj, cb)` | Active bits → BitMapper names → object values. `cb(value, key, bit)` |
145
+ | `forEachMaskPair` | `(maskA, maskB, cb)` | Intersection (A & B). `cb(bit)` |
146
+ | `forEachMaskDiff` | `(maskA, maskB, cb)` | Difference (A & ~B). `cb(bit)` |
147
+ | `forEachMaskUnion` | `(maskA, maskB, cb)` | Union (A \| B). `cb(bit)` |
148
+
149
+ ### Iteration Example
150
+
151
+ ```javascript
152
+ const mapper = new BitMapper(['Idle', 'Run', 'Jump', 'Attack']);
153
+ const mask = new FastBit32().add(1).add(3); // Run + Attack
154
+
155
+ forEachMapped(mask, mapper, (name, bit) => {
156
+ console.log(name); // 'Run', then 'Attack'
157
+ });
158
+ ```
159
+
84
160
  ## Critical Caveats
85
161
 
86
162
  1. **Silent modulo-32 wraparound.** JavaScript bitwise shifts apply `% 32` implicitly. `add(32)` is identical to `add(0)`. `add(40)` is identical to `add(8)`. There is no bounds checking.
@@ -162,6 +238,23 @@ function startAttack() {
162
238
  }
163
239
  ```
164
240
 
241
+ ### ECS with BitMapper and Iteration
242
+
243
+ ```javascript
244
+ const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
245
+ const entity = new FastBit32();
246
+ entity.add(components.get('Position')).add(components.get('AI'));
247
+
248
+ // Debug: see what's active
249
+ console.log(components.getActiveNames(entity)); // ['Position', 'AI']
250
+
251
+ // Iterate only active components
252
+ const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
253
+ forEachMappedObject(entity, components, systems, (system, name, bit) => {
254
+ system(entity); // Only calls updatePos and think
255
+ });
256
+ ```
257
+
165
258
  ### Serialization Round-Trip
166
259
 
167
260
  ```javascript
@@ -213,5 +306,6 @@ flags.has(10); // false — clone is independent
213
306
  - All single-bit operations: one bitwise instruction, zero branches.
214
307
  - `count()`: 5 bitwise operations (Hacker's Delight parallel popcount), zero loops.
215
308
  - `lowest()` / `highest()`: 1–2 operations using `Math.clz32`, zero loops.
309
+ - `forEach` and all standalone helpers: O(k) where k = active bits, not 32.
216
310
  - All methods except `clone()` and `deserialize()`: zero allocations.
217
- - V8 monomorphic: single hidden class for the lifetime of the object.
311
+ - V8 monomorphic: single hidden class for the lifetime of the object.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@zakkster/lite-fastbit32",
3
3
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
- "main": "FastBit32.js",
8
+ "main": "Fastbit32.js",
9
9
  "types": "FastBit32.d.ts",
10
10
  "exports": {
11
11
  ".": {
@@ -15,7 +15,7 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "FastBit32.js",
18
+ "Fastbit32.js",
19
19
  "FastBit32.d.ts",
20
20
  "llms.txt",
21
21
  "README.md"
@@ -33,6 +33,7 @@
33
33
  "keywords": [
34
34
  "bitflags",
35
35
  "bitmask",
36
+ "bitmap",
36
37
  "bitwise",
37
38
  "state-machine",
38
39
  "ecs",
package/FastBit32.js DELETED
@@ -1,134 +0,0 @@
1
- /**
2
- * FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager
3
- * Engineered for 60fps hot-path execution, ECS masking, and object pooling.
4
- * * ⚠️ ENGINE PRIMITIVE CAVEATS:
5
- * - SILENT WRAPAROUND: JS bitwise shifts implicitly apply modulo 32.
6
- * Calling `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
7
- * - TRUNCATION: Floats and negative numbers are silently truncated and
8
- * converted to unsigned 32-bit integers (e.g., `-1 >>> 0` becomes `4294967295`).
9
- * Sanitize your inputs upstream if your domain logic requires strict bounds!
10
- */
11
- export class FastBit32 {
12
- constructor(initial = 0) {
13
- // Enforce unsigned 32-bit integer immediately for V8 inline caching
14
- this.value = initial >>> 0;
15
- }
16
-
17
- // ── Single Bit Ops (Zero-Branching) ──────────────────────
18
-
19
- add(bit) {
20
- this.value |= (1 << bit);
21
- return this;
22
- }
23
-
24
- remove(bit) {
25
- this.value &= ~(1 << bit);
26
- return this;
27
- }
28
-
29
- toggle(bit) {
30
- this.value ^= (1 << bit);
31
- return this;
32
- }
33
-
34
- has(bit) {
35
- return (this.value & (1 << bit)) !== 0;
36
- }
37
-
38
- // ── Bulk Mask Ops ────────────────────────────────────────
39
-
40
- hasAll(mask) { return (this.value & mask) === mask; }
41
- hasAny(mask) { return (this.value & mask) !== 0; }
42
- hasNone(mask) { return (this.value & mask) === 0; }
43
-
44
- // ── In-Place Mutations (Zero-GC Set Math) ────────────────
45
-
46
- clear() {
47
- this.value = 0;
48
- return this;
49
- }
50
-
51
- union(mask) {
52
- this.value |= mask;
53
- return this;
54
- }
55
-
56
- difference(mask) {
57
- this.value &= ~mask;
58
- return this;
59
- }
60
-
61
- intersect(mask) {
62
- this.value &= mask;
63
- return this;
64
- }
65
-
66
- // ── Advanced AAA Engine Helpers ──────────────────────────
67
-
68
- /**
69
- * O(1) loop-free popcount (Hamming Weight).
70
- * Returns the number of active bits (e.g., how many flags are active).
71
- */
72
- count() {
73
- let v = this.value;
74
- v = v - ((v >>> 1) & 0x55555555);
75
- v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
76
- return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
77
- }
78
-
79
- /**
80
- * O(1) loop-free popcount applying a mask first.
81
- * Useful for counting active flags within a specific subsystem.
82
- */
83
- countMasked(mask) {
84
- let v = this.value & mask;
85
- v = v - ((v >>> 1) & 0x55555555);
86
- v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
87
- return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
88
- }
89
-
90
- /**
91
- * Instantly finds the index of the lowest active bit.
92
- * Incredibly useful for Object Pools (finding the first available slot).
93
- * Returns -1 if empty.
94
- */
95
- lowest() {
96
- if (this.value === 0) return -1;
97
- // Isolate lowest set bit, then use native Count Leading Zeros to find index
98
- return Math.clz32(this.value & -this.value) ^ 31;
99
- }
100
-
101
- /**
102
- * Instantly finds the index of the highest active bit.
103
- * Useful for determining the bounds of active arrays/spatial grids.
104
- * Returns -1 if empty.
105
- */
106
- highest() {
107
- if (this.value === 0) return -1;
108
- return 31 - Math.clz32(this.value);
109
- }
110
-
111
- isEmpty() {
112
- return this.value === 0;
113
- }
114
-
115
- clone() {
116
- return new FastBit32(this.value);
117
- }
118
-
119
- // ── Serialization (For ECS Save States) ──────────────────
120
-
121
- /**
122
- * Exports the raw 32-bit integer for ultra-lightweight JSON/binary storage.
123
- */
124
- serialize() {
125
- return this.value;
126
- }
127
-
128
- /**
129
- * Instantiates a new FastBit32 from a serialized integer.
130
- */
131
- static deserialize(value) {
132
- return new FastBit32(value);
133
- }
134
- }