@woosh/meep-engine 2.48.23 → 2.49.1

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.
Files changed (147) hide show
  1. package/editor/tools/GridPaintTool.js +1 -1
  2. package/editor/tools/paint/TerrainPaintTool.js +1 -1
  3. package/editor/view/GridPickCoordinateView.js +1 -1
  4. package/package.json +1 -1
  5. package/src/core/UUID.js +2 -0
  6. package/src/core/assert.js +4 -1
  7. package/src/core/binary/ctz32.js +1 -1
  8. package/src/core/binary/operations/bitCount.spec.js +19 -0
  9. package/src/core/binary/uint82float.spec.js +7 -0
  10. package/src/core/bvh2/binary/IndexedBinaryBVH.spec.js +7 -0
  11. package/src/core/bvh2/bvh3/EBBVHLeafProxy.js +3 -0
  12. package/src/core/bvh2/bvh3/query/compute_tight_near_far_clipping_planes.js +5 -4
  13. package/src/core/bvh2/transform/RotationOptimizer.spec.js +161 -155
  14. package/src/core/codegen/LineBuilder.js +12 -3
  15. package/src/core/codegen/LineBuilder.spec.js +7 -0
  16. package/src/core/collection/CuckooFilter.js +12 -12
  17. package/src/core/collection/HashMap.js +486 -237
  18. package/src/core/collection/HashMap.spec.js +110 -1
  19. package/src/core/collection/array/{typedArrayToDataType.js → typed/typedArrayToDataType.js} +1 -1
  20. package/src/core/collection/array/weightedRandomFromArray.spec.js +20 -0
  21. package/src/core/debug/matchers/AnyOf.js +1 -2
  22. package/src/core/geom/2d/aabb/AABB2.spec.js +1 -1
  23. package/src/core/geom/2d/aabb/aabb2_compute_center_from_multiple.js +19 -0
  24. package/src/core/geom/2d/quad-tree/qt_collect_by_circle.js +3 -3
  25. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +7 -9
  26. package/src/core/geom/3d/aabb/aabb3_compute_plane_side.js +17 -15
  27. package/src/core/geom/3d/aabb/aabb3_compute_plane_side.spec.js +25 -0
  28. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +1 -1
  29. package/src/core/geom/3d/aabb/aabb3_from_v3_array.js +3 -0
  30. package/src/core/geom/3d/aabb/aabb3_from_v3_array.spec.js +32 -0
  31. package/src/core/geom/3d/aabb/aabb3_intersects_aabb3.spec.js +115 -0
  32. package/src/core/geom/3d/aabb/aabb3_raycast.js +6 -1
  33. package/src/core/geom/3d/aabb/serializeAABB3Encoded_v0.js +6 -6
  34. package/src/core/geom/3d/{CircleMath.js → compute_circle_bounding_box.js} +1 -1
  35. package/src/core/geom/3d/decompose_matrix_4_array.js +18 -19
  36. package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +1 -1
  37. package/src/{engine/graphics/ecs/mesh-v2 → core/geom/3d/matrix}/allocate_transform_m4.js +1 -1
  38. package/src/core/geom/3d/normal/hemioct/decode_hemioct_to_unit.js +26 -0
  39. package/src/core/geom/3d/normal/hemioct/encode_unit3_hemioct.js +0 -26
  40. package/src/core/geom/3d/normal/hemioct/unit_hemioct.spec.js +2 -1
  41. package/src/core/geom/3d/plane/computePlaneLineIntersection.js +51 -0
  42. package/src/core/geom/3d/plane/computePlanePlaneIntersection.js +77 -0
  43. package/src/core/geom/3d/plane/computePlaneRayIntersection.js +55 -0
  44. package/src/core/geom/3d/plane/plane3_computeLineSegmentIntersection.js +50 -0
  45. package/src/core/geom/3d/plane/planeRayIntersection.js +14 -0
  46. package/src/core/geom/3d/{tetrahedra/in_sphere_fast.js → sphere/in_sphere3d_fast.js} +1 -1
  47. package/src/core/geom/3d/{tetrahedra/in_sphere_robust.js → sphere/in_sphere3d_robust.js} +1 -1
  48. package/src/core/geom/3d/sphere/sphere_array_intersects_point.js +2 -2
  49. package/src/core/geom/3d/sphere/{sphereIntersectsPoint.js → sphere_intersects_point.js} +7 -4
  50. package/src/core/geom/3d/sphere/sphere_intersects_point.spec.js +134 -0
  51. package/src/core/geom/3d/sphere/sphere_intersects_ray.spec.js +49 -0
  52. package/src/core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js +11 -7
  53. package/src/core/geom/3d/tetrahedra/delaunay/{debug_validate_mesh.js → debug/debug_validate_mesh.js} +1 -1
  54. package/src/core/geom/3d/tetrahedra/delaunay/{push_boundary_with_validation.js → debug/push_boundary_with_validation.js} +1 -1
  55. package/src/core/geom/3d/tetrahedra/delaunay/{validate_cavity_boundary.js → debug/validate_cavity_boundary.js} +2 -2
  56. package/src/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity.js +2 -2
  57. package/src/core/geom/3d/triangle/computeTriangleRayIntersection.js +0 -164
  58. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js +87 -0
  59. package/src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricEdge.js +81 -0
  60. package/src/core/geom/{rayTriangleIntersection.js → 3d/triangle/rayTriangleIntersection.js} +2 -2
  61. package/src/core/geom/ConicRay.js +160 -152
  62. package/src/core/geom/Matrix4.js +2 -0
  63. package/src/core/geom/Quaternion.js +19 -1
  64. package/src/core/geom/packing/max-rect/MaxRectangles.js +4 -214
  65. package/src/core/geom/packing/max-rect/cost/costByBestShortSide.js +11 -0
  66. package/src/core/geom/packing/max-rect/cost/costByRemainingArea.js +14 -0
  67. package/src/core/geom/packing/max-rect/cutArea.js +79 -0
  68. package/src/core/geom/packing/max-rect/findBestContainer.js +58 -0
  69. package/src/core/geom/packing/max-rect/packOneBox.js +49 -0
  70. package/src/core/geom/packing/miniball/Miniball.js +12 -12
  71. package/src/core/geom/packing/miniball/Quality.js +2 -2
  72. package/src/core/geom/packing/miniball/Subspan.js +13 -13
  73. package/src/core/geom/v3_dot.js +1 -1
  74. package/src/core/graph/layout/CircleLayout.js +1 -1
  75. package/src/core/graph/layout/{BoxLayouter.js → box/BoxLayouter.js} +6 -50
  76. package/src/core/graph/layout/box/applyCentralGravityAABB2.js +29 -0
  77. package/src/core/json/resolvePath.spec.js +14 -0
  78. package/src/core/land/reactive/{compiler/ReactiveCompiler.spec.js → compileReactiveExpression.spec.js} +17 -17
  79. package/src/core/math/random/MersenneTwister.spec.js +19 -0
  80. package/src/core/math/random/randomGaussian.spec.js +9 -0
  81. package/src/core/math/statistics/computeStatisticalMean.js +2 -2
  82. package/src/core/model/reactive/model/arithmetic/ReactiveAdd.js +1 -1
  83. package/src/core/model/reactive/model/arithmetic/ReactiveDivide.js +3 -1
  84. package/src/core/model/reactive/model/arithmetic/ReactiveMultiply.js +1 -1
  85. package/src/core/model/reactive/model/arithmetic/ReactiveNegate.js +3 -1
  86. package/src/core/model/reactive/model/arithmetic/ReactiveSubtract.js +1 -1
  87. package/src/core/model/reactive/model/comparative/ReactiveEquals.js +1 -1
  88. package/src/core/model/reactive/model/comparative/ReactiveGreaterThan.js +3 -1
  89. package/src/core/model/reactive/model/comparative/ReactiveGreaterThanOrEqual.js +3 -1
  90. package/src/core/model/reactive/model/comparative/ReactiveLessThan.js +3 -1
  91. package/src/core/model/reactive/model/comparative/ReactiveLessThanOrEqual.js +3 -1
  92. package/src/core/model/reactive/model/comparative/ReactiveNotEquals.js +1 -1
  93. package/src/core/model/reactive/model/logic/ReactiveAnd.js +1 -1
  94. package/src/core/model/reactive/model/logic/ReactiveNot.js +3 -1
  95. package/src/core/model/reactive/model/logic/ReactiveOr.js +1 -1
  96. package/src/core/primitives/numbers/computeHashFloat.spec.js +7 -0
  97. package/src/core/process/task/util/iteratorTask.js +3 -1
  98. package/src/engine/animation/curve/AnimationCurve.js +34 -5
  99. package/src/engine/animation/curve/AnimationCurve.spec.js +100 -0
  100. package/src/engine/asset/AssetTransformer.js +1 -0
  101. package/src/engine/computeStridedIntegerArrayHash.js +4 -2
  102. package/src/engine/ecs/components/Renderable.d.ts +1 -1
  103. package/src/{ecs → engine/ecs}/grid/pick.js +4 -4
  104. package/src/engine/ecs/parent/entity_node_compute_bounding_box.js +1 -1
  105. package/src/engine/ecs/storage/binary/collection/BinaryCollectionSerializer.js +1 -18
  106. package/src/engine/ecs/systems/MotionSystem.js +7 -1
  107. package/src/engine/ecs/systems/SynchronizePositionSystem.js +8 -2
  108. package/src/engine/ecs/transform/Transform.js +1 -1
  109. package/src/engine/graphics/camera/makeScreenScissorFrustum.js +3 -3
  110. package/src/engine/graphics/camera/testClippingPlaneComputation.js +13 -13
  111. package/src/engine/graphics/ecs/camera/Camera.js +1 -1
  112. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js +1 -1
  113. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js +9 -0
  114. package/src/engine/graphics/geometry/MikkT/MikkTSpace.js +1 -1
  115. package/src/engine/graphics/geometry/MikkT/STSpace.js +1 -1
  116. package/src/engine/graphics/geometry/bvh/buffered/BVHGeometryRaycaster.js +1 -1
  117. package/src/engine/graphics/material/optimization/MaterialOptimizationContext.js +1 -1
  118. package/src/engine/graphics/particles/particular/engine/MovingBoundingBox.js +1 -1
  119. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.js +1 -0
  120. package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
  121. package/src/engine/graphics/postprocess/threejs/postprocessing/TexturePass.js +2 -2
  122. package/src/engine/graphics/sh3/path_tracer/GeometryBVHBatched.js +2 -2
  123. package/src/engine/graphics/texture/sampler/Sampler2D.js +1 -1
  124. package/src/engine/graphics/texture/sampler/sampler2d_compute_texel_value_conversion_scale_to_uint8.js +1 -1
  125. package/src/engine/intelligence/behavior/Behavior.spec.js +15 -0
  126. package/src/engine/intelligence/mcts/MoveEdge.js +1 -1
  127. package/src/engine/reference/v1/ReferenceManager.js +3 -0
  128. package/src/engine/reference/v2/Reference.js +33 -37
  129. package/src/engine/sound/sopra/README.md +6 -0
  130. package/src/generation/automata/CaveGeneratorCellularAutomata.js +10 -7
  131. package/src/generation/automata/CaveGeneratorCellularAutomata.spec.js +12 -0
  132. package/src/generation/automata/CellularAutomata.js +5 -4
  133. package/src/generation/filtering/numeric/complex/CellFilterGaussianBlur.js +25 -9
  134. package/src/view/minimap/dom/MinimapCameraView.js +1 -1
  135. package/src/core/geom/Plane.js +0 -250
  136. package/src/core/land/reactive/ReactiveLexer.js +0 -158
  137. package/src/core/land/reactive/ReactiveLexer.ts +0 -181
  138. package/src/core/land/reactive/ReactiveListener.ts +0 -323
  139. package/src/core/land/reactive/ReactiveParser.js +0 -1573
  140. package/src/core/land/reactive/ReactiveParser.ts +0 -1776
  141. package/src/core/land/reactive/ReactiveVisitor.js +0 -1
  142. package/src/core/land/reactive/ReactiveVisitor.ts +0 -218
  143. package/src/core/land/reactive/compiler/ReactiveCompiler.js +0 -350
  144. package/src/core/land/reactive/compiler/ReactiveNearlyCompiler.js +0 -166
  145. package/src/core/land/reactive/compiler/ReactiveParser.js +0 -34
  146. package/src/core/land/reactive/nearley/ReactiveNearley.js +0 -187
  147. /package/src/{engine/graphics/ecs/mesh-v2 → core/geom/3d/vector}/allocate_v3.js +0 -0
@@ -1,6 +1,38 @@
1
1
  import { assert } from "../assert.js";
2
2
  import { invokeObjectEquals } from "../model/object/invokeObjectEquals.js";
3
3
  import { invokeObjectHash } from "../model/object/invokeObjectHash.js";
4
+ import { isPowerOfTwo } from "../math/isPowerOrTwo.js";
5
+ import { ctz32 } from "../binary/ctz32.js";
6
+ import { ceilPowerOfTwo } from "../binary/operations/ceilPowerOfTwo.js";
7
+ import { UintArrayForCount } from "./array/typed/uint_array_for_count.js";
8
+ import { array_copy } from "./array/copyArray.js";
9
+ import { min2 } from "../math/min2.js";
10
+ import { arraySwapElements } from "./array/arraySwapElements.js";
11
+
12
+ /*
13
+ * Heavily inspired by ruby's "new" (circa 2016) hash table implementation
14
+ * @see https://github.com/ruby/ruby/blob/82995d4615e993f1d13f3e826b93fbd65c47e19e/st.c
15
+ * @see https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding#hash-changes
16
+ */
17
+
18
+ /**
19
+ * Formula: Xn+1 = (a * Xn + c ) % m
20
+ *
21
+ * According the Hull-Dobell theorem a generator
22
+ * "Xnext = (a*Xprev + c) mod m" is a full cycle generator if and only if
23
+ * o m and c are relatively prime
24
+ * o a-1 is divisible by all prime factors of m
25
+ * o a-1 is divisible by 4 if m is divisible by 4.
26
+ * For our case a is 5, c is 1, and m is a power of two.
27
+ * @param index
28
+ * @param mask used to execute division, this is a number equal to (2^K - 1) where 2^K is the size of the "bins" array
29
+ * @see https://en.wikipedia.org/wiki/Linear_congruential_generator
30
+ */
31
+ export function generate_next_linear_congruential_index(index, mask) {
32
+ const index5 = (index << 2) + index; // this is just a faster version of index*5
33
+
34
+ return (index5 + 1) & mask;
35
+ }
4
36
 
5
37
 
6
38
  /**
@@ -40,28 +72,151 @@ class MapEntry {
40
72
  * @readonly
41
73
  * @type {number}
42
74
  */
43
- const DEFAULT_INITIAL_CAPACITY = 16;
75
+ const DEFAULT_INITIAL_CAPACITY_POWER = 4;
44
76
 
45
77
  /**
46
78
  * @readonly
47
79
  * @type {number}
48
80
  */
49
- const DEFAULT_LOAD_FACTOR = 0.75;
81
+ const DEFAULT_INITIAL_CAPACITY = 2 ** DEFAULT_INITIAL_CAPACITY_POWER;
50
82
 
51
83
  /**
52
- * Multiply previous size by this number when growing the table
53
84
  * @readonly
54
85
  * @type {number}
55
86
  */
56
- const GROWTH_FACTOR = 2;
87
+ const DEFAULT_LOAD_FACTOR = 0.75;
88
+
89
+
90
+ /**
91
+ * Reserved value that we store in "bins" array to indicate an empty slot
92
+ * @type {number}
93
+ */
94
+ const BIN_RESERVED_VALUE_EMPTY = 0;
95
+
96
+ /**
97
+ * Reserved value that we store in "bins" array to indicate a deleted entry
98
+ * @type {number}
99
+ */
100
+ const BIN_RESERVED_VALUE_DELETED = 1;
101
+
102
+ /**
103
+ * Real index offset into entry array
104
+ * @type {number}
105
+ */
106
+ const ENTRY_BASE = 2;
107
+
108
+ /**
109
+ * Special hash value used to indicate "dead" entries
110
+ * If key hashes to this value - we will replace it
111
+ * @type {number}
112
+ */
113
+ const RESERVED_HASH = 4294967295;
114
+ /**
115
+ * Used as a replacement for reserved hash
116
+ * @type {number}
117
+ */
118
+ const RESERVED_HASH_SUBSTITUTE = 0;
119
+
120
+ /**
121
+ *
122
+ * @type {number}
123
+ */
124
+ const UNDEFINED_BIN_INDEX = ~0;
125
+
126
+
127
+ /**
128
+ * @template K,V
129
+ * @param {MapEntry<K,V>} record
130
+ * @param {number} hash
131
+ * @param {K} key
132
+ * @param {function(a:K,b:K):boolean} equality_op
133
+ */
134
+ function entry_equality_check(record, hash, key, equality_op) {
135
+ if (record.hash !== hash) {
136
+ return false;
137
+ }
138
+
139
+ if (record.key === key) {
140
+ return true;
141
+ }
142
+
143
+ const result = equality_op(record.key, key);
144
+
145
+ assert.isBoolean(result, `result(a=${record.key},b=${key})`);
146
+
147
+ return result;
148
+ }
149
+
150
+
151
+ const EMPTY_BINS = new Uint32Array(0);
57
152
 
58
153
  /**
59
154
  * Implements part of {@link Map} interface
155
+ * NOTE: as with any hash-based data structure, keys are assumed to be immutable. If you modified keys after inserting them into the map, it will cause the hash table to become invalid. You can fix this by forcing rehashing, but generally - try to avoid changing keys in the first place.
156
+ *
60
157
  * @copyright Alex Goldring (c) 2023
61
158
  * @template K,V
62
159
  * @extends Map<K,V>
63
160
  */
64
161
  export class HashMap {
162
+ /**
163
+ * Index pointers to entries array,
164
+ * number of bins is always power or two
165
+ * @type {Uint32Array}
166
+ */
167
+ #bins = EMPTY_BINS;
168
+
169
+ /**
170
+ * Note that dead entries are marked as such with a special reserved hash values, so records can be reused for new entries
171
+ * @type {MapEntry<K,V>[]}
172
+ */
173
+ #entries = new Array(0);
174
+
175
+ /**
176
+ * Pointer to the end of allocated entries segment
177
+ * @type {number}
178
+ */
179
+ #entries_bound = 0;
180
+
181
+ /**
182
+ *
183
+ * @type {number}
184
+ */
185
+ #entries_start = 0;
186
+
187
+ /**
188
+ * number of records in the map
189
+ * @type {number}
190
+ */
191
+ #size = 0;
192
+
193
+ #bin_count = 0;
194
+
195
+ /**
196
+ * Always exactly half of the number of bins
197
+ * @type {number}
198
+ */
199
+ #entries_allocated_count = 0;
200
+
201
+ #bin_count_power_of_two = 0;
202
+
203
+ #entries_count_power_of_two = 0;
204
+
205
+ /**
206
+ * Mask used to map from hash to a bin index
207
+ * @type {number}
208
+ */
209
+ #bin_count_mask = 0;
210
+
211
+ /**
212
+ * How full the table can get before number of buckets is increased
213
+ * @type {number}
214
+ * @private
215
+ */
216
+ #load_factor = DEFAULT_LOAD_FACTOR;
217
+
218
+ #version = 0;
219
+
65
220
  /**
66
221
  * @template K, V
67
222
  * @param {function(K):number} [keyHashFunction]
@@ -99,33 +254,13 @@ export class HashMap {
99
254
  */
100
255
  this.keyEqualityFunction = keyEqualityFunction;
101
256
 
102
- /**
103
- * Sparse array of map entries
104
- * @type {MapEntry[][]}
105
- * @private
106
- */
107
- this.records = [];
108
-
109
- /**
110
- *
111
- * @type {number}
112
- */
113
- this.size = 0;
114
-
257
+ this.#load_factor = loadFactor;
115
258
 
116
- /**
117
- *
118
- * @type {number}
119
- * @private
120
- */
121
- this.__bucket_count = capacity;
259
+ this.#setBinCount(ceilPowerOfTwo(capacity));
260
+ }
122
261
 
123
- /**
124
- * How full the table can get before number of buckets is increased
125
- * @type {number}
126
- * @private
127
- */
128
- this.__load_factor = loadFactor;
262
+ get size() {
263
+ return this.#size;
129
264
  }
130
265
 
131
266
  /**
@@ -133,61 +268,43 @@ export class HashMap {
133
268
  * @returns {number}
134
269
  */
135
270
  getCurrentLoad() {
136
- return this.size / this.__bucket_count;
271
+ return this.#size / this.#bin_count;
137
272
  }
138
273
 
139
274
  /**
140
- * Proportion of buckets that are occupied with at least 1 record
141
- * @returns {number}
275
+ * Note: this method is not intended for public use
276
+ * @param {number} count
142
277
  */
143
- getOccupancy() {
144
- let occupied_bucket_count = 0;
145
-
146
- const c = this.__bucket_count;
147
- for (let i = 0; i < c; i++) {
148
- const bucket = this.records[i];
278
+ #setBinCount(count) {
279
+ assert.greaterThanOrEqual(count, 1, 'bucket count must be at least 1');
280
+ assert.isNonNegativeInteger(count, 'count');
281
+ assert.equal(isPowerOfTwo(count), true, `count must be a power of two, instead was ${count}`);
149
282
 
150
- if (bucket !== undefined && bucket.length > 0) {
151
- occupied_bucket_count++;
152
- }
283
+ if (count < this.#size) {
284
+ throw new Error(`count must be at least equal to must of records in the map (=${this.#size}), instead was ${count}`);
153
285
  }
154
286
 
155
- return occupied_bucket_count / this.__bucket_count;
156
- }
157
287
 
158
- /**
159
- * Number of collisions, that is elements beyond first that occupy the same bucket in the table
160
- * @returns {number}
161
- */
162
- getCollisionCount() {
163
- let count = 0;
288
+ this.#entries_count_power_of_two = ctz32(count);
289
+ this.#bin_count_power_of_two = this.#entries_count_power_of_two + 1;
164
290
 
165
- const c = this.__bucket_count;
166
- for (let i = 0; i < c; i++) {
167
- const bucket = this.records[i];
291
+ this.#bin_count = 2 ** this.#bin_count_power_of_two;
292
+ this.#bin_count_mask = this.#bin_count - 1;
168
293
 
169
- if (bucket !== undefined) {
170
- const occupancy = bucket.length;
171
- const collisions = occupancy - 1;
172
- if (collisions > 0) {
173
- count += collisions;
174
- }
175
- }
176
- }
294
+ this.#entries_allocated_count = 2 ** this.#entries_count_power_of_two;
177
295
 
178
- return count;
179
- }
296
+ const BinsArray = UintArrayForCount(this.#entries_allocated_count + ENTRY_BASE);
180
297
 
181
- /**
182
- *
183
- * @param {number} count
184
- */
185
- setBucketCount(count) {
186
- assert.greaterThanOrEqual(count, 1, 'bucket count must be at least 1');
187
- assert.isNonNegativeInteger(count, 'count');
298
+ this.#bins = new BinsArray(this.#bin_count);
299
+
300
+ const new_entries = new Array(this.#entries_allocated_count);
301
+ const old_entries = this.#entries;
302
+
303
+ this.#entries = new_entries;
304
+
305
+ array_copy(old_entries, 0, new_entries, 0, min2(old_entries.length, this.#entries_allocated_count));
188
306
 
189
- this.__bucket_count = count;
190
- if (this.size > 0) {
307
+ if (this.#size > 0) {
191
308
  // re-hash
192
309
  this.rebuild();
193
310
  }
@@ -199,7 +316,7 @@ export class HashMap {
199
316
  * @returns {number}
200
317
  * @private
201
318
  */
202
- __compute_bucket_index(hash) {
319
+ #compute_bin_index(hash) {
203
320
  assert.isInteger(hash, 'hash');
204
321
 
205
322
  // mix the input hash to minimize potential impact of poor hash function spread
@@ -208,76 +325,141 @@ export class HashMap {
208
325
  // force index to unsigned integer
209
326
  const index = mixed_hash >>> 0;
210
327
 
211
- return index % this.__bucket_count;
328
+ return index & this.#bin_count_mask;
212
329
  }
213
330
 
214
331
  /**
215
332
  *
216
333
  * @param {K} key
217
- * @param {V} value
334
+ * @return {number}
218
335
  */
219
- set(key, value) {
220
- const raw_hash = this.keyHashFunction(key);
221
- const bucket_index = this.__compute_bucket_index(raw_hash);
336
+ #build_key_hash(key) {
337
+ const original = this.keyHashFunction(key);
222
338
 
223
- assert.isFiniteNumber(bucket_index, 'hash');
339
+ return original === RESERVED_HASH ? RESERVED_HASH_SUBSTITUTE : original;
340
+ }
224
341
 
225
- const records = this.records;
342
+ /**
343
+ *
344
+ * @param {K} k
345
+ * @param {V} v
346
+ * @param {number} hash
347
+ * @return {number}
348
+ */
349
+ #allocate_entry(k, v, hash) {
226
350
 
227
- let bucket = records[bucket_index];
351
+ const i = this.#entries_bound;
228
352
 
229
- if (bucket === undefined) {
353
+ this.#entries_bound++;
230
354
 
231
- // bucket for this value doesn't exist
355
+ if (this.#entries[i] !== undefined) {
356
+ const entry = this.#entries[i];
357
+ entry.hash = hash;
358
+ entry.key = k;
359
+ entry.value = v;
360
+ } else {
361
+ this.#entries[i] = new MapEntry(k, v, hash);
362
+ }
232
363
 
233
- bucket = [];
364
+ return i;
365
+ }
234
366
 
235
- records[bucket_index] = bucket;
367
+ /**
368
+ *
369
+ * @param {MapEntry<K,V>} entry
370
+ */
371
+ #deallocate(entry) {
236
372
 
237
- } else {
373
+ // clear out entry to allow values/keys to be garbage collected
374
+ entry.key = null;
375
+ entry.value = null;
376
+ entry.hash = RESERVED_HASH; // mark as dead via hash
238
377
 
239
- // bucket exists
378
+ }
240
379
 
241
- const bucketSize = bucket.length;
380
+ #rebuild_if_necessary() {
381
+ if (this.#entries_bound === this.#entries_allocated_count) {
382
+ if (this.#size === this.#entries_allocated_count) {
383
+ // used up all allocated entries
384
+ // bin count must always be larger than end of the entries table
385
+ this.#grow();
386
+ } else {
387
+ // exhausted entries array, perform compaction
388
+ this.rebuild();
389
+ }
390
+ }
391
+ }
242
392
 
243
- //check if key already exists
244
- for (let i = 0; i < bucketSize; i++) {
245
- const entry = bucket[i];
393
+ /**
394
+ *
395
+ * @param {K} key
396
+ * @param {V} value
397
+ */
398
+ set(key, value) {
399
+ this.#rebuild_if_necessary();
246
400
 
247
- const entryKey = entry.key;
401
+ const raw_hash = this.#build_key_hash(key);
402
+ let bin_index = this.#compute_bin_index(raw_hash);
403
+ assert.isFiniteNumber(bin_index, 'hash');
248
404
 
405
+ let first_deleted_bin_index = UNDEFINED_BIN_INDEX;
249
406
 
250
- if (
251
- entry.hash === raw_hash
252
- && (entryKey === key || this.keyEqualityFunction(entryKey, key))
253
- ) {
407
+ for (; ;) {
408
+ const bin = this.#bins[bin_index];
254
409
 
255
- // found record with matching key, replace the value
410
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
411
+ // bin is occupied
256
412
 
257
- entry.value = value;
413
+ // check if it's the entry that we're looking for
414
+ const entry = this.#entries[bin - ENTRY_BASE];
258
415
 
259
- // done, return
416
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
417
+ // found the right entry
418
+ entry.value = value;
260
419
  return;
420
+ }
421
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
422
+ // bin is empty
261
423
 
424
+ if (first_deleted_bin_index !== UNDEFINED_BIN_INDEX) {
425
+ // reused bin of deleted entity
426
+ bin_index = first_deleted_bin_index;
262
427
  }
263
428
 
429
+ const entry_index = this.#allocate_entry(key, value, raw_hash);
430
+
431
+ assert.defined(this.#entries[entry_index], 'entry');
432
+
433
+ assert.equal(this.#entries[entry_index].hash, raw_hash, 'entry.hash');
434
+ assert.equal(this.#entries[entry_index].value, value, 'entry.value');
435
+ assert.equal(this.#entries[entry_index].key, key, 'entry.key');
436
+
437
+ this.#bins[bin_index] = entry_index + ENTRY_BASE;
438
+
439
+ break;
440
+
441
+ } else if (first_deleted_bin_index === UNDEFINED_BIN_INDEX) {
442
+ // bin is deleted
443
+ first_deleted_bin_index = bin_index;
444
+
264
445
  }
265
446
 
266
- }
447
+ // perform secondary hashing
448
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
267
449
 
268
- bucket.push(new MapEntry(key, value, raw_hash));
450
+ }
269
451
 
270
- const old_size = this.size;
452
+ const old_size = this.#size;
271
453
  const new_size = old_size + 1;
272
- this.size = new_size;
454
+ this.#size = new_size;
273
455
 
274
456
  // compute actual current load
275
- const bucket_count = this.__bucket_count;
457
+ const bucket_count = this.#bin_count;
276
458
  const load = new_size / bucket_count;
277
459
 
278
- if (load > this.__load_factor) {
460
+ if (load > this.#load_factor) {
279
461
  // current load is too high, increase table size
280
- this.setBucketCount(bucket_count * GROWTH_FACTOR);
462
+ this.#grow();
281
463
  }
282
464
  }
283
465
 
@@ -287,33 +469,34 @@ export class HashMap {
287
469
  * @returns {V|undefined}
288
470
  */
289
471
  get(key) {
290
- const raw_hash = this.keyHashFunction(key);
291
- const bucket_index = this.__compute_bucket_index(raw_hash);
292
- const bucket = this.records[bucket_index];
472
+ const raw_hash = this.#build_key_hash(key);
293
473
 
294
- if (bucket !== undefined) {
474
+ let bin_index = this.#compute_bin_index(raw_hash);
295
475
 
296
- const bucketSize = bucket.length;
476
+ for (; ;) {
477
+ const bin = this.#bins[bin_index];
297
478
 
298
- //check if key already exists
299
- for (let i = 0; i < bucketSize; i++) {
300
- const entry = bucket[i];
479
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
480
+ // bin is occupied
301
481
 
302
- const entryKey = entry.key;
482
+ // check if the entry is what we're looking for
483
+ const entry = this.#entries[bin - ENTRY_BASE];
303
484
 
304
- if (
305
- entry.hash === raw_hash
306
- && (entryKey === key || this.keyEqualityFunction(entryKey, key))
307
- ) {
485
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
486
+ // found the right entry
308
487
  return entry.value;
309
-
310
488
  }
489
+
490
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
491
+ // bin is empty
492
+ return undefined;
311
493
  }
312
494
 
495
+ // perform secondary hashing
496
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
497
+
313
498
  }
314
499
 
315
- //not found
316
- return undefined;
317
500
  }
318
501
 
319
502
  /**
@@ -357,6 +540,26 @@ export class HashMap {
357
540
  return value;
358
541
  }
359
542
 
543
+ /**
544
+ *
545
+ * @param {number} bin
546
+ */
547
+ #update_range_for_deleted(bin) {
548
+ if (this.#entries_start === bin) {
549
+ let start = bin + 1;
550
+ let bound = this.#entries_bound;
551
+
552
+ const entries = this.#entries;
553
+
554
+ while (start < bound && entries[start].hash === RESERVED_HASH) {
555
+ start++;
556
+ }
557
+
558
+ assert.greaterThanOrEqual(bound - start, this.#size, `live entity bounds must span at least number of entries equal to map size(=${this.#size}), instead got start(=${start}), and end(=${bound})`)
559
+
560
+ this.#entries_start = start;
561
+ }
562
+ }
360
563
 
361
564
  /**
362
565
  *
@@ -365,38 +568,49 @@ export class HashMap {
365
568
  */
366
569
  delete(key) {
367
570
 
368
- const raw_hash = this.keyHashFunction(key);
369
- const bucket_index = this.__compute_bucket_index(raw_hash);
370
- const bucket = this.records[bucket_index];
571
+ const raw_hash = this.#build_key_hash(key);
572
+ let bin_index = this.#compute_bin_index(raw_hash);
371
573
 
372
- if (bucket === undefined) {
373
- return false;
374
- }
574
+ assert.isFiniteNumber(bin_index, 'hash');
375
575
 
576
+ const bins = this.#bins;
577
+ const entries = this.#entries;
376
578
 
377
- const bucketSize = bucket.length;
579
+ for (; ;) {
580
+ const bin = bins[bin_index];
378
581
 
379
- //check if key already exists
380
- for (let i = 0; i < bucketSize; i++) {
381
- const entry = bucket[i];
582
+ if (bin > BIN_RESERVED_VALUE_DELETED) {
583
+ // bin is occupied
382
584
 
383
- const entryKey = entry.key;
384
- if (entryKey === key || this.keyEqualityFunction(entryKey, key)) {
585
+ // check if the entry is what we're looking for
586
+ const entry_index = bin - ENTRY_BASE;
587
+ const entry = entries[entry_index];
385
588
 
386
- if (bucket.length <= 1) {
387
- //last element in the bucket
388
- delete this.records[bucket_index];
389
- } else {
390
- bucket.splice(i, 1);
589
+ if (entry_equality_check(entry, raw_hash, key, this.keyEqualityFunction)) {
590
+ // found the right entry
591
+
592
+ // record entry as dead
593
+ this.#deallocate(entry);
594
+
595
+ // mark slot as removed
596
+ bins[bin_index] = BIN_RESERVED_VALUE_DELETED;
597
+
598
+ this.#size--;
599
+
600
+ this.#update_range_for_deleted(entry_index);
601
+
602
+ return true;
391
603
  }
392
604
 
393
- this.size--;
394
- return true;
605
+ } else if (bin === BIN_RESERVED_VALUE_EMPTY) {
606
+ // bin is empty
607
+ return false;
395
608
  }
396
- }
397
609
 
398
- //not found
399
- return false;
610
+ // perform secondary hashing
611
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
612
+
613
+ }
400
614
  }
401
615
 
402
616
  /**
@@ -408,130 +622,134 @@ export class HashMap {
408
622
  verifyHashes(callback, thisArg) {
409
623
  let all_hashes_valid = true;
410
624
 
411
- let h, i;
412
-
413
- const records = this.records;
625
+ const count = this.#bin_count;
626
+ for (let j = 0; j < count; j++) {
627
+ const bin = this.#bins[j];
414
628
 
415
- for (h in records) {
416
- if (!records.hasOwnProperty(h)) {
629
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
630
+ // unoccupied
417
631
  continue;
418
632
  }
419
633
 
420
- const bucket = records[h];
421
-
422
- const bucketSize = bucket.length;
423
-
424
- //check if key already exists
425
- for (i = 0; i < bucketSize; i++) {
426
-
427
- /**
428
- * @type {MapEntry<K,V>}
429
- */
430
- const entry = bucket[i];
634
+ /**
635
+ * @type {MapEntry<K,V>}
636
+ */
637
+ const entry = this.#entries[bin - ENTRY_BASE];
431
638
 
432
- //check hash
433
- const raw_hash = this.keyHashFunction(entry.key);
639
+ //check hash
640
+ const raw_hash = this.#build_key_hash(entry.key);
434
641
 
435
- if (entry.hash !== raw_hash) {
436
- callback.call(thisArg, `Hash stored on the entry(=${entry.hash}) is different from the computed key hash(=${raw_hash}).`, entry.key, entry.value)
642
+ if (entry.hash !== raw_hash) {
643
+ callback.call(thisArg, `Hash stored on the entry(=${entry.hash}) is different from the computed key hash(=${raw_hash}).`, entry.key, entry.value)
437
644
 
438
- all_hashes_valid = false;
439
- }
440
-
441
- const actual_bucket_index = this.__compute_bucket_index(raw_hash);
442
-
443
- const stored_bucket_index = parseInt(h);
444
-
445
- if (actual_bucket_index !== stored_bucket_index) {
446
- callback.call(thisArg, `Hash of key has changed. old=${stored_bucket_index}, new=${actual_bucket_index}`, entry.key, entry.value);
447
-
448
- all_hashes_valid = false;
449
- }
645
+ all_hashes_valid = false;
450
646
  }
647
+
451
648
  }
452
649
 
453
650
  return all_hashes_valid;
454
651
  }
455
652
 
653
+ #grow() {
654
+ this.#setBinCount(this.#entries_allocated_count * 2);
655
+ }
656
+
657
+
456
658
  /**
457
- * Rebuild table, useful for when table is resized or hashes of stored keys change
659
+ * Rebuild table, useful for when table is resized
458
660
  */
459
661
  rebuild() {
662
+ const entries_bound = this.#entries_bound;
663
+ const entries = this.#entries;
664
+
665
+ // reset all bins
666
+ const bins = this.#bins;
667
+ bins.fill(BIN_RESERVED_VALUE_EMPTY);
460
668
 
461
- let string_bucket_index, i, l;
669
+ let written_entries = 0;
462
670
 
463
- const records = this.records;
671
+ for (let existing_entry_index = this.#entries_start; existing_entry_index < entries_bound; existing_entry_index++) {
672
+ const entry = entries[existing_entry_index];
464
673
 
465
- for (string_bucket_index in records) {
466
- const stored_bucket_index = parseInt(string_bucket_index);
674
+ const hash = entry.hash;
467
675
 
468
- if (!records.hasOwnProperty(string_bucket_index)) {
676
+ if (hash === RESERVED_HASH) {
677
+ // entry is dead
469
678
  continue;
470
679
  }
471
680
 
472
- const bucket = records[stored_bucket_index];
681
+ const new_index = written_entries;
682
+ written_entries++;
473
683
 
474
- for (i = 0, l = bucket.length; i < l; i++) {
475
- /**
476
- * @type {MapEntry<K,V>}
477
- */
478
- const entry = bucket[i];
684
+ if (new_index !== existing_entry_index) {
685
+ // move entries to the new position, compacting holes
686
+ arraySwapElements(entries, new_index, existing_entry_index);
687
+ }
479
688
 
480
- //check hash
481
- const raw_hash = this.keyHashFunction(entry.key);
689
+ let bin_index = this.#compute_bin_index(hash);
482
690
 
483
- entry.hash = raw_hash;
691
+ for (; ;) {
692
+ const bin = bins[bin_index];
484
693
 
485
- const actual_bucket_index = this.__compute_bucket_index(raw_hash);
694
+ if (bin === BIN_RESERVED_VALUE_EMPTY) {
695
+ // empty slot, take it
696
+ bins[bin_index] = new_index + ENTRY_BASE;
697
+ break;
698
+ }
486
699
 
700
+ // perform secondary hashing
701
+ bin_index = generate_next_linear_congruential_index(bin_index, this.#bin_count_mask);
487
702
 
488
- if (actual_bucket_index !== stored_bucket_index) {
489
- //remove entry from the bucket
703
+ }
704
+ }
490
705
 
491
- bucket.splice(i, 1);
492
- //update iterator
493
- l--;
494
- i--;
706
+ assert.equal(written_entries, this.#size, `live entries(=${written_entries}) should match size(=${this.#size})`);
495
707
 
496
- if (bucket.length === 0) {
497
- //delete empty bucket
498
- delete records[stored_bucket_index];
499
- }
708
+ this.#entries_start = 0;
709
+ this.#entries_bound = this.#size;
710
+ this.#version++;
711
+ }
500
712
 
501
- const newBucket = records[actual_bucket_index];
713
+ #count_live_entities() {
714
+ let count = 0;
715
+ for (let i = this.#entries_start; i < this.#entries_bound; i++) {
716
+ const entry = this.#entries[i];
502
717
 
503
- if (newBucket === undefined) {
504
- records[actual_bucket_index] = [entry];
505
- } else {
506
- newBucket.push(entry);
507
- }
718
+ if (entry.hash !== RESERVED_HASH) {
719
+ count++;
508
720
 
509
- }
721
+ assert.equal(entry.hash, this.#build_key_hash(entry.key));
722
+ } else {
723
+ assert.isNull(entry.key, 'key');
510
724
  }
511
725
  }
726
+
727
+ return count;
512
728
  }
513
729
 
514
730
  forEach(callback, thisArg) {
515
- let h, i, l;
731
+ const count = this.#bin_count;
732
+ const entries = this.#entries;
733
+ const bins = this.#bins;
734
+ const start_version = this.#version;
735
+
736
+ for (let j = 0; j < count; j++) {
737
+ assert.equal(start_version, this.#version, 'HashMap modified during traversal');
516
738
 
517
- const records = this.records;
739
+ const bin = bins[j];
518
740
 
519
- for (h in records) {
520
- if (!records.hasOwnProperty(h)) {
741
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
742
+ // unoccupied
521
743
  continue;
522
744
  }
523
745
 
524
- const bucket = records[h];
746
+ /**
747
+ * @type {MapEntry<K,V>}
748
+ */
749
+ const entry = entries[bin - ENTRY_BASE];
525
750
 
526
- for (i = 0, l = bucket.length; i < l; i++) {
527
- /**
528
- * @type {MapEntry<K,V>}
529
- */
530
- const entry = bucket[i];
531
-
532
- // Signature based on MDN docs of Map.prototype.forEach()
533
- callback.call(thisArg, entry.value, entry.key, this);
534
- }
751
+ // Signature based on MDN docs of Map.prototype.forEach()
752
+ callback.call(thisArg, entry.value, entry.key, this);
535
753
  }
536
754
  }
537
755
 
@@ -548,30 +766,61 @@ export class HashMap {
548
766
  * Remove all data from the Map
549
767
  */
550
768
  clear() {
551
- this.records = [];
552
- this.size = 0;
769
+
770
+ // clear out all
771
+ const bins = this.#bins;
772
+ const count = this.#bin_count;
773
+
774
+ for (let i = 0; i < count; i++) {
775
+ const bin = bins[i];
776
+
777
+ if (bin !== BIN_RESERVED_VALUE_EMPTY) {
778
+
779
+ if (bin !== BIN_RESERVED_VALUE_DELETED) {
780
+ // occupied, move to deleted
781
+ const entry_index = bin - ENTRY_BASE;
782
+
783
+ this.#deallocate(this.#entries[entry_index]);
784
+ }
785
+
786
+ // mark as empty
787
+ bins[i] = BIN_RESERVED_VALUE_EMPTY;
788
+ }
789
+
790
+
791
+ }
792
+
793
+ this.#size = 0;
794
+
795
+ this.#entries_start = 0;
796
+ this.#entries_bound = 0;
553
797
  }
554
798
 
555
799
  * [Symbol.iterator]() {
556
- let h, i, l;
557
800
 
558
- const records = this.records;
801
+ const count = this.#bin_count;
559
802
 
560
- for (h in records) {
561
- if (!records.hasOwnProperty(h)) {
562
- continue;
563
- }
803
+ const bins = this.#bins;
804
+ const entries = this.#entries;
805
+
806
+ const start_version = this.#version;
564
807
 
565
- const bucket = records[h];
808
+ for (let j = 0; j < count; j++) {
809
+ assert.equal(start_version, this.#version, 'HashMap modified during traversal');
566
810
 
567
- for (i = 0, l = bucket.length; i < l; i++) {
568
- /**
569
- * @type {MapEntry<K,V>}
570
- */
571
- const entry = bucket[i];
811
+ const bin = bins[j];
572
812
 
573
- yield [entry.key, entry.value];
813
+ if (bin <= BIN_RESERVED_VALUE_DELETED) {
814
+ // unoccupied
815
+ continue;
574
816
  }
817
+
818
+ /**
819
+ * @type {MapEntry<K,V>}
820
+ */
821
+ const entry = entries[bin - ENTRY_BASE];
822
+
823
+ yield [entry.key, entry.value];
575
824
  }
576
825
 
577
826
  }