@graphty/graph-format 0.0.0 → 0.2.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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -43
  3. package/dist/graph-format.d.ts +1 -0
  4. package/dist/graph-format.d.ts.map +1 -0
  5. package/dist/graph-format.js +15798 -0
  6. package/dist/graph-format.js.map +1 -0
  7. package/dist/src/builder/arena.d.ts +85 -0
  8. package/dist/src/builder/arena.d.ts.map +1 -0
  9. package/dist/src/builder/arena.js +154 -0
  10. package/dist/src/builder/arena.js.map +1 -0
  11. package/dist/src/builder/compact.d.ts +515 -0
  12. package/dist/src/builder/compact.d.ts.map +1 -0
  13. package/dist/src/builder/compact.js +1553 -0
  14. package/dist/src/builder/compact.js.map +1 -0
  15. package/dist/src/builder/counting-sort.d.ts +96 -0
  16. package/dist/src/builder/counting-sort.d.ts.map +1 -0
  17. package/dist/src/builder/counting-sort.js +430 -0
  18. package/dist/src/builder/counting-sort.js.map +1 -0
  19. package/dist/src/builder/freeze.d.ts +72 -0
  20. package/dist/src/builder/freeze.d.ts.map +1 -0
  21. package/dist/src/builder/freeze.js +548 -0
  22. package/dist/src/builder/freeze.js.map +1 -0
  23. package/dist/src/builder/graph-builder.d.ts +602 -0
  24. package/dist/src/builder/graph-builder.d.ts.map +1 -0
  25. package/dist/src/builder/graph-builder.js +1940 -0
  26. package/dist/src/builder/graph-builder.js.map +1 -0
  27. package/dist/src/builder/options.d.ts +16 -0
  28. package/dist/src/builder/options.d.ts.map +1 -0
  29. package/dist/src/builder/options.js +21 -0
  30. package/dist/src/builder/options.js.map +1 -0
  31. package/dist/src/columns/bitmap.d.ts +96 -0
  32. package/dist/src/columns/bitmap.d.ts.map +1 -0
  33. package/dist/src/columns/bitmap.js +184 -0
  34. package/dist/src/columns/bitmap.js.map +1 -0
  35. package/dist/src/columns/column.d.ts +220 -0
  36. package/dist/src/columns/column.d.ts.map +1 -0
  37. package/dist/src/columns/column.js +2109 -0
  38. package/dist/src/columns/column.js.map +1 -0
  39. package/dist/src/columns/dictionary.d.ts +60 -0
  40. package/dist/src/columns/dictionary.d.ts.map +1 -0
  41. package/dist/src/columns/dictionary.js +102 -0
  42. package/dist/src/columns/dictionary.js.map +1 -0
  43. package/dist/src/columns/growable.d.ts +198 -0
  44. package/dist/src/columns/growable.d.ts.map +1 -0
  45. package/dist/src/columns/growable.js +296 -0
  46. package/dist/src/columns/growable.js.map +1 -0
  47. package/dist/src/columns/infer.d.ts +121 -0
  48. package/dist/src/columns/infer.d.ts.map +1 -0
  49. package/dist/src/columns/infer.js +268 -0
  50. package/dist/src/columns/infer.js.map +1 -0
  51. package/dist/src/columns/remap.d.ts +143 -0
  52. package/dist/src/columns/remap.d.ts.map +1 -0
  53. package/dist/src/columns/remap.js +588 -0
  54. package/dist/src/columns/remap.js.map +1 -0
  55. package/dist/src/columns/table.d.ts +184 -0
  56. package/dist/src/columns/table.d.ts.map +1 -0
  57. package/dist/src/columns/table.js +485 -0
  58. package/dist/src/columns/table.js.map +1 -0
  59. package/dist/src/constants.d.ts +34 -0
  60. package/dist/src/constants.d.ts.map +1 -0
  61. package/dist/src/constants.js +79 -0
  62. package/dist/src/constants.js.map +1 -0
  63. package/dist/src/errors.d.ts +76 -0
  64. package/dist/src/errors.d.ts.map +1 -0
  65. package/dist/src/errors.js +32 -0
  66. package/dist/src/errors.js.map +1 -0
  67. package/dist/src/ids/edge-id-index.d.ts +77 -0
  68. package/dist/src/ids/edge-id-index.d.ts.map +1 -0
  69. package/dist/src/ids/edge-id-index.js +155 -0
  70. package/dist/src/ids/edge-id-index.js.map +1 -0
  71. package/dist/src/ids/node-id-map.d.ts +280 -0
  72. package/dist/src/ids/node-id-map.d.ts.map +1 -0
  73. package/dist/src/ids/node-id-map.js +1139 -0
  74. package/dist/src/ids/node-id-map.js.map +1 -0
  75. package/dist/src/ids/string-store.d.ts +219 -0
  76. package/dist/src/ids/string-store.d.ts.map +1 -0
  77. package/dist/src/ids/string-store.js +526 -0
  78. package/dist/src/ids/string-store.js.map +1 -0
  79. package/dist/src/index.d.ts +30 -0
  80. package/dist/src/index.d.ts.map +1 -0
  81. package/dist/src/index.js +29 -0
  82. package/dist/src/index.js.map +1 -0
  83. package/dist/src/populate/from-csr.d.ts +61 -0
  84. package/dist/src/populate/from-csr.d.ts.map +1 -0
  85. package/dist/src/populate/from-csr.js +706 -0
  86. package/dist/src/populate/from-csr.js.map +1 -0
  87. package/dist/src/populate/from-edge-arrays.d.ts +51 -0
  88. package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
  89. package/dist/src/populate/from-edge-arrays.js +217 -0
  90. package/dist/src/populate/from-edge-arrays.js.map +1 -0
  91. package/dist/src/populate/from-records.d.ts +51 -0
  92. package/dist/src/populate/from-records.d.ts.map +1 -0
  93. package/dist/src/populate/from-records.js +447 -0
  94. package/dist/src/populate/from-records.js.map +1 -0
  95. package/dist/src/snapshot/derived.d.ts +211 -0
  96. package/dist/src/snapshot/derived.d.ts.map +1 -0
  97. package/dist/src/snapshot/derived.js +1254 -0
  98. package/dist/src/snapshot/derived.js.map +1 -0
  99. package/dist/src/snapshot/graph-meta.d.ts +23 -0
  100. package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
  101. package/dist/src/snapshot/graph-meta.js +159 -0
  102. package/dist/src/snapshot/graph-meta.js.map +1 -0
  103. package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
  104. package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
  105. package/dist/src/snapshot/graph-snapshot.js +1247 -0
  106. package/dist/src/snapshot/graph-snapshot.js.map +1 -0
  107. package/dist/src/snapshot/hash.d.ts +90 -0
  108. package/dist/src/snapshot/hash.d.ts.map +1 -0
  109. package/dist/src/snapshot/hash.js +228 -0
  110. package/dist/src/snapshot/hash.js.map +1 -0
  111. package/dist/src/snapshot/queries.d.ts +74 -0
  112. package/dist/src/snapshot/queries.d.ts.map +1 -0
  113. package/dist/src/snapshot/queries.js +131 -0
  114. package/dist/src/snapshot/queries.js.map +1 -0
  115. package/dist/src/snapshot/validate.d.ts +192 -0
  116. package/dist/src/snapshot/validate.d.ts.map +1 -0
  117. package/dist/src/snapshot/validate.js +990 -0
  118. package/dist/src/snapshot/validate.js.map +1 -0
  119. package/dist/src/snapshot/views.d.ts +274 -0
  120. package/dist/src/snapshot/views.d.ts.map +1 -0
  121. package/dist/src/snapshot/views.js +856 -0
  122. package/dist/src/snapshot/views.js.map +1 -0
  123. package/dist/src/types/builder.d.ts +456 -0
  124. package/dist/src/types/builder.d.ts.map +1 -0
  125. package/dist/src/types/builder.js +11 -0
  126. package/dist/src/types/builder.js.map +1 -0
  127. package/dist/src/types/columns.d.ts +615 -0
  128. package/dist/src/types/columns.d.ts.map +1 -0
  129. package/dist/src/types/columns.js +14 -0
  130. package/dist/src/types/columns.js.map +1 -0
  131. package/dist/src/types/index.d.ts +14 -0
  132. package/dist/src/types/index.d.ts.map +1 -0
  133. package/dist/src/types/index.js +11 -0
  134. package/dist/src/types/index.js.map +1 -0
  135. package/dist/src/types/internal.d.ts +172 -0
  136. package/dist/src/types/internal.d.ts.map +1 -0
  137. package/dist/src/types/internal.js +13 -0
  138. package/dist/src/types/internal.js.map +1 -0
  139. package/dist/src/types/snapshot.d.ts +801 -0
  140. package/dist/src/types/snapshot.d.ts.map +1 -0
  141. package/dist/src/types/snapshot.js +14 -0
  142. package/dist/src/types/snapshot.js.map +1 -0
  143. package/dist/src/types/wire.d.ts +196 -0
  144. package/dist/src/types/wire.d.ts.map +1 -0
  145. package/dist/src/types/wire.js +8 -0
  146. package/dist/src/types/wire.js.map +1 -0
  147. package/dist/src/util/mask.d.ts +55 -0
  148. package/dist/src/util/mask.d.ts.map +1 -0
  149. package/dist/src/util/mask.js +71 -0
  150. package/dist/src/util/mask.js.map +1 -0
  151. package/dist/src/util/options.d.ts +22 -0
  152. package/dist/src/util/options.d.ts.map +1 -0
  153. package/dist/src/util/options.js +49 -0
  154. package/dist/src/util/options.js.map +1 -0
  155. package/dist/src/util/shared-buffers.d.ts +42 -0
  156. package/dist/src/util/shared-buffers.d.ts.map +1 -0
  157. package/dist/src/util/shared-buffers.js +52 -0
  158. package/dist/src/util/shared-buffers.js.map +1 -0
  159. package/dist/src/util/typed-array.d.ts +205 -0
  160. package/dist/src/util/typed-array.d.ts.map +1 -0
  161. package/dist/src/util/typed-array.js +289 -0
  162. package/dist/src/util/typed-array.js.map +1 -0
  163. package/dist/src/wire/bytes.d.ts +69 -0
  164. package/dist/src/wire/bytes.d.ts.map +1 -0
  165. package/dist/src/wire/bytes.js +361 -0
  166. package/dist/src/wire/bytes.js.map +1 -0
  167. package/dist/src/wire/carried-views.d.ts +111 -0
  168. package/dist/src/wire/carried-views.d.ts.map +1 -0
  169. package/dist/src/wire/carried-views.js +286 -0
  170. package/dist/src/wire/carried-views.js.map +1 -0
  171. package/dist/src/wire/from-wire.d.ts +144 -0
  172. package/dist/src/wire/from-wire.d.ts.map +1 -0
  173. package/dist/src/wire/from-wire.js +1425 -0
  174. package/dist/src/wire/from-wire.js.map +1 -0
  175. package/dist/src/wire/to-wire.d.ts +171 -0
  176. package/dist/src/wire/to-wire.d.ts.map +1 -0
  177. package/dist/src/wire/to-wire.js +652 -0
  178. package/dist/src/wire/to-wire.js.map +1 -0
  179. package/dist/tsconfig.build.tsbuildinfo +1 -0
  180. package/package.json +72 -7
  181. package/src/builder/arena.ts +192 -0
  182. package/src/builder/compact.ts +1811 -0
  183. package/src/builder/counting-sort.ts +506 -0
  184. package/src/builder/freeze.ts +689 -0
  185. package/src/builder/graph-builder.ts +2264 -0
  186. package/src/builder/options.ts +29 -0
  187. package/src/columns/bitmap.ts +196 -0
  188. package/src/columns/column.ts +2574 -0
  189. package/src/columns/dictionary.ts +114 -0
  190. package/src/columns/growable.ts +350 -0
  191. package/src/columns/infer.ts +285 -0
  192. package/src/columns/remap.ts +663 -0
  193. package/src/columns/table.ts +568 -0
  194. package/src/constants.ts +89 -0
  195. package/src/errors.ts +115 -0
  196. package/src/ids/edge-id-index.ts +179 -0
  197. package/src/ids/node-id-map.ts +1340 -0
  198. package/src/ids/string-store.ts +589 -0
  199. package/src/index.ts +139 -0
  200. package/src/lib-resizable-array-buffer.d.ts +34 -0
  201. package/src/populate/from-csr.ts +802 -0
  202. package/src/populate/from-edge-arrays.ts +257 -0
  203. package/src/populate/from-records.ts +536 -0
  204. package/src/snapshot/derived.ts +1486 -0
  205. package/src/snapshot/graph-meta.ts +167 -0
  206. package/src/snapshot/graph-snapshot.ts +1503 -0
  207. package/src/snapshot/hash.ts +241 -0
  208. package/src/snapshot/queries.ts +136 -0
  209. package/src/snapshot/validate.ts +1108 -0
  210. package/src/snapshot/views.ts +943 -0
  211. package/src/types/builder.ts +480 -0
  212. package/src/types/columns.ts +727 -0
  213. package/src/types/index.ts +14 -0
  214. package/src/types/internal.ts +195 -0
  215. package/src/types/snapshot.ts +886 -0
  216. package/src/types/wire.ts +207 -0
  217. package/src/util/mask.ts +89 -0
  218. package/src/util/options.ts +57 -0
  219. package/src/util/shared-buffers.ts +56 -0
  220. package/src/util/typed-array.ts +371 -0
  221. package/src/wire/bytes.ts +440 -0
  222. package/src/wire/carried-views.ts +327 -0
  223. package/src/wire/from-wire.ts +1690 -0
  224. package/src/wire/to-wire.ts +755 -0
@@ -0,0 +1,1108 @@
1
+ /**
2
+ * `validate()` (design sections 3.2, 9.5 and 11.4): the invariants I1-I13 as one function each, the
3
+ * two levels "structure" (O(n + m + columns): lengths, ranges, counts, alignment, column length
4
+ * rules) and "full" (O(m log d): sortedness, orientation, pairing, NaN, flag recomputation, id
5
+ * bijection, unique columns), and the flag predicates of design section 3.8 shared with the derived
6
+ * graph core builder so that every flag has exactly one definition.
7
+ *
8
+ * Every check throws `E_INVALID_SNAPSHOT` on the first violation with `details.invariant` naming the
9
+ * number ("I4") and the location (`row`, `arc`, `edge`, `column`, `table`, `flag`, `reason`). The
10
+ * functions take the public `GraphSnapshot` contract, so test/invariants.test.ts runs the same
11
+ * helpers over hand-written fixtures. Checksum comparison needs the snapshot's private records and
12
+ * lives in graph-snapshot.ts.
13
+ */
14
+
15
+ import { bitmapCount, bitmapGet } from "../columns/bitmap.js";
16
+ import { verifyUniqueColumn } from "../columns/table.js";
17
+ import { ALIGNMENT, INVALID_INDEX, MAX_COUNT } from "../constants.js";
18
+ import { GraphFormatError } from "../errors.js";
19
+ import {
20
+ type ArenaLayout,
21
+ type AttributeTable,
22
+ type Column,
23
+ type CoreArrayName,
24
+ type F32,
25
+ type GraphSnapshot,
26
+ type SnapshotFlags,
27
+ type U32,
28
+ } from "../types/index.js";
29
+ import { isFourByteAligned, isOverPlainBuffer } from "../util/typed-array.js";
30
+
31
+ // ============================================================ errors
32
+
33
+ /**
34
+ * Build the E_INVALID_SNAPSHOT error of one violated invariant.
35
+ * @param invariant - the invariant number, e.g. "I4"
36
+ * @param message - a plain-ASCII description naming the location
37
+ * @param details - the location (row, arc, edge, column, ...); `invariant` is added
38
+ * @returns the error
39
+ */
40
+ export function invariantViolation(
41
+ invariant: string,
42
+ message: string,
43
+ details: Readonly<Record<string, unknown>> = {},
44
+ ): GraphFormatError {
45
+ return new GraphFormatError("E_INVALID_SNAPSHOT", `invariant ${invariant} violated: ${message}`, {
46
+ invariant,
47
+ ...details,
48
+ });
49
+ }
50
+
51
+ // ============================================================ flags (3.8)
52
+
53
+ /** The arrays the flag predicates read; a subset of the core so the builder can call it before the snapshot exists. */
54
+ interface FlagSource {
55
+ /** Whether the graph is directed. */
56
+ readonly directed: boolean;
57
+ /** The node count. */
58
+ readonly nodeCount: number;
59
+ /** The row offsets. */
60
+ readonly rowPtr: U32;
61
+ /** The sorted targets. */
62
+ readonly colIdx: U32;
63
+ /** The per-arc weights, or null. */
64
+ readonly weights: F32 | null;
65
+ /** The arc -> edge permutation, or null when it is known to be the identity. */
66
+ readonly arcToEdge: U32 | null;
67
+ /** The number of self-loop logical edges. */
68
+ readonly selfLoopCount: number;
69
+ }
70
+
71
+ /**
72
+ * Whether some row holds two arcs with equal colIdx (parallel edges); adjacent by invariant I4.
73
+ * @param rowPtr - the row offsets
74
+ * @param colIdx - the sorted targets
75
+ * @param nodeCount - the node count
76
+ * @returns true for a multigraph
77
+ */
78
+ function hasParallelArcs(rowPtr: U32, colIdx: U32, nodeCount: number): boolean {
79
+ for (let u = 0; u < nodeCount; u++) {
80
+ const end = rowPtr[u + 1];
81
+ for (let a = rowPtr[u] + 1; a < end; a++) {
82
+ if (colIdx[a] === colIdx[a - 1]) {
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+
90
+ /**
91
+ * The number of arcs a with colIdx[a] === row(a).
92
+ * @param rowPtr - the row offsets
93
+ * @param colIdx - the sorted targets
94
+ * @param nodeCount - the node count
95
+ * @returns the loop arc count
96
+ */
97
+ export function countLoopArcs(rowPtr: U32, colIdx: U32, nodeCount: number): number {
98
+ let loops = 0;
99
+ for (let u = 0; u < nodeCount; u++) {
100
+ const end = rowPtr[u + 1];
101
+ for (let a = rowPtr[u]; a < end; a++) {
102
+ if (colIdx[a] === u) {
103
+ loops++;
104
+ }
105
+ }
106
+ }
107
+ return loops;
108
+ }
109
+
110
+ /**
111
+ * Whether a permutation array is the identity.
112
+ * @param perm - the array
113
+ * @returns true when perm[i] === i for every i
114
+ */
115
+ export function isIdentity(perm: U32): boolean {
116
+ for (let i = 0; i < perm.length; i++) {
117
+ if (perm[i] !== i) {
118
+ return false;
119
+ }
120
+ }
121
+ return true;
122
+ }
123
+
124
+ /**
125
+ * Compute every flag of design section 3.8 from the arrays (invariant I9: never guessed).
126
+ * @param core - the arrays and counts the predicates read
127
+ * @returns the truthful flags
128
+ */
129
+ export function computeFlags(core: FlagSource): SnapshotFlags {
130
+ const { directed, nodeCount, rowPtr, colIdx, weights, arcToEdge } = core;
131
+ let allWeightsOne = true;
132
+ let nonNegativeWeights = true;
133
+ let finiteWeights = true;
134
+ if (weights !== null) {
135
+ for (let a = 0; a < weights.length; a++) {
136
+ const w = weights[a];
137
+ if (w !== 1) {
138
+ allWeightsOne = false;
139
+ }
140
+ if (!(w >= 0)) {
141
+ nonNegativeWeights = false;
142
+ }
143
+ if (!Number.isFinite(w)) {
144
+ finiteWeights = false;
145
+ }
146
+ }
147
+ }
148
+ return {
149
+ multigraph: hasParallelArcs(rowPtr, colIdx, nodeCount),
150
+ hasSelfLoops: core.selfLoopCount > 0,
151
+ arcToEdgeIsIdentity: directed && (arcToEdge === null || isIdentity(arcToEdge)),
152
+ weighted: weights !== null,
153
+ allWeightsOne,
154
+ nonNegativeWeights,
155
+ finiteWeights,
156
+ };
157
+ }
158
+
159
+ // ============================================================ I1 - I3: structure
160
+
161
+ /**
162
+ * I1: rowPtr has nodeCount + 1 entries, starts at 0, is non-decreasing and ends at arcCount ===
163
+ * colIdx.length.
164
+ * @param s - the snapshot
165
+ */
166
+ export function checkI1(s: GraphSnapshot): void {
167
+ const { rowPtr, colIdx, nodeCount, arcCount } = s;
168
+ if (rowPtr.length !== nodeCount + 1) {
169
+ throw invariantViolation("I1", `rowPtr has ${rowPtr.length} entries, expected ${nodeCount + 1}`, {
170
+ expected: nodeCount + 1,
171
+ found: rowPtr.length,
172
+ });
173
+ }
174
+ if (rowPtr[0] !== 0) {
175
+ throw invariantViolation("I1", `rowPtr[0] is ${rowPtr[0]}, expected 0`, { row: 0, found: rowPtr[0] });
176
+ }
177
+ for (let u = 0; u < nodeCount; u++) {
178
+ if (rowPtr[u + 1] < rowPtr[u]) {
179
+ throw invariantViolation("I1", `rowPtr decreases at row ${u}: ${rowPtr[u]} -> ${rowPtr[u + 1]}`, {
180
+ row: u,
181
+ });
182
+ }
183
+ }
184
+ if (rowPtr[nodeCount] !== arcCount || colIdx.length !== arcCount) {
185
+ throw invariantViolation(
186
+ "I1",
187
+ `rowPtr[${nodeCount}] = ${rowPtr[nodeCount]}, colIdx.length = ${colIdx.length}, arcCount = ${arcCount}`,
188
+ { row: nodeCount, found: rowPtr[nodeCount], expected: arcCount, colIdxLength: colIdx.length },
189
+ );
190
+ }
191
+ }
192
+
193
+ /**
194
+ * I2: every colIdx entry is below nodeCount (so INVALID_INDEX never appears).
195
+ * @param s - the snapshot
196
+ */
197
+ export function checkI2(s: GraphSnapshot): void {
198
+ const { colIdx, nodeCount } = s;
199
+ for (let a = 0; a < colIdx.length; a++) {
200
+ if (colIdx[a] >= nodeCount) {
201
+ throw invariantViolation("I2", `colIdx[${a}] = ${colIdx[a]} is not below nodeCount ${nodeCount}`, {
202
+ arc: a,
203
+ found: colIdx[a],
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * I3: the counts are non-negative integers at most MAX_COUNT, edgeCount <= arcCount and selfLoopCount
211
+ * <= edgeCount.
212
+ * @param s - the snapshot
213
+ */
214
+ export function checkI3(s: GraphSnapshot): void {
215
+ const counts: readonly [string, number][] = [
216
+ ["nodeCount", s.nodeCount],
217
+ ["edgeCount", s.edgeCount],
218
+ ["arcCount", s.arcCount],
219
+ ["selfLoopCount", s.selfLoopCount],
220
+ ];
221
+ for (const [name, value] of counts) {
222
+ if (!Number.isInteger(value) || value < 0 || value > MAX_COUNT) {
223
+ throw invariantViolation("I3", `${name} = ${value} is not an integer in [0, MAX_COUNT]`, {
224
+ count: name,
225
+ found: value,
226
+ });
227
+ }
228
+ }
229
+ if (s.edgeCount > s.arcCount) {
230
+ throw invariantViolation("I3", `edgeCount ${s.edgeCount} exceeds arcCount ${s.arcCount}`, {
231
+ edgeCount: s.edgeCount,
232
+ arcCount: s.arcCount,
233
+ });
234
+ }
235
+ if (s.selfLoopCount > s.edgeCount) {
236
+ throw invariantViolation("I3", `selfLoopCount ${s.selfLoopCount} exceeds edgeCount ${s.edgeCount}`, {
237
+ selfLoopCount: s.selfLoopCount,
238
+ edgeCount: s.edgeCount,
239
+ });
240
+ }
241
+ }
242
+
243
+ // ============================================================ I4 - I8: rows, permutations, pairing, weights
244
+
245
+ /**
246
+ * I4: within every row colIdx is non-decreasing and arcs with equal colIdx are ordered by ascending
247
+ * arcToEdge. Full level.
248
+ * @param s - the snapshot
249
+ */
250
+ export function checkI4(s: GraphSnapshot): void {
251
+ const { rowPtr, colIdx, nodeCount } = s;
252
+ const identity = s.flags.arcToEdgeIsIdentity;
253
+ const arcToEdge = identity ? null : s.arcToEdge;
254
+ for (let u = 0; u < nodeCount; u++) {
255
+ const end = rowPtr[u + 1];
256
+ for (let a = rowPtr[u] + 1; a < end; a++) {
257
+ if (colIdx[a] < colIdx[a - 1]) {
258
+ throw invariantViolation("I4", `row ${u} is not sorted at arc ${a}`, { row: u, arc: a });
259
+ }
260
+ if (colIdx[a] === colIdx[a - 1] && arcToEdge !== null && arcToEdge[a] <= arcToEdge[a - 1]) {
261
+ throw invariantViolation("I4", `parallel arcs ${a - 1} and ${a} in row ${u} are not in edge order`, {
262
+ row: u,
263
+ arc: a,
264
+ });
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * I5 (structure part): arcToEdge has arcCount entries below edgeCount and edgeToArc has edgeCount
272
+ * entries below arcCount; an identity flag requires arcCount === edgeCount and a directed snapshot.
273
+ * @param s - the snapshot
274
+ */
275
+ export function checkI5Ranges(s: GraphSnapshot): void {
276
+ const { arcCount, edgeCount } = s;
277
+ if (s.flags.arcToEdgeIsIdentity) {
278
+ if (!s.directed || arcCount !== edgeCount) {
279
+ throw invariantViolation("I5", "identity permutation claimed on a snapshot where it cannot hold", {
280
+ directed: s.directed,
281
+ arcCount,
282
+ edgeCount,
283
+ });
284
+ }
285
+ return;
286
+ }
287
+ const { arcToEdge, edgeToArc } = s;
288
+ if (arcToEdge.length !== arcCount) {
289
+ throw invariantViolation("I5", `arcToEdge has ${arcToEdge.length} entries, expected ${arcCount}`, {
290
+ expected: arcCount,
291
+ found: arcToEdge.length,
292
+ });
293
+ }
294
+ if (edgeToArc.length !== edgeCount) {
295
+ throw invariantViolation("I5", `edgeToArc has ${edgeToArc.length} entries, expected ${edgeCount}`, {
296
+ expected: edgeCount,
297
+ found: edgeToArc.length,
298
+ });
299
+ }
300
+ for (let a = 0; a < arcCount; a++) {
301
+ if (arcToEdge[a] >= edgeCount) {
302
+ throw invariantViolation("I5", `arcToEdge[${a}] = ${arcToEdge[a]} is not below edgeCount ${edgeCount}`, {
303
+ arc: a,
304
+ found: arcToEdge[a],
305
+ });
306
+ }
307
+ }
308
+ for (let e = 0; e < edgeCount; e++) {
309
+ if (edgeToArc[e] >= arcCount) {
310
+ throw invariantViolation("I5", `edgeToArc[${e}] = ${edgeToArc[e]} is not below arcCount ${arcCount}`, {
311
+ edge: e,
312
+ found: edgeToArc[e],
313
+ });
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * I5 (orientation part): arcToEdge[edgeToArc[e]] === e for every edge. Full level.
320
+ * @param s - the snapshot
321
+ */
322
+ export function checkI5Orientation(s: GraphSnapshot): void {
323
+ if (s.flags.arcToEdgeIsIdentity) {
324
+ return;
325
+ }
326
+ const { arcToEdge, edgeToArc, edgeCount } = s;
327
+ for (let e = 0; e < edgeCount; e++) {
328
+ if (arcToEdge[edgeToArc[e]] !== e) {
329
+ throw invariantViolation("I5", `arcToEdge[edgeToArc[${e}]] = ${arcToEdge[edgeToArc[e]]}, expected ${e}`, {
330
+ edge: e,
331
+ arc: edgeToArc[e],
332
+ });
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * I6: a directed snapshot has arcCount === edgeCount (structure) and arcToEdge is a permutation of
339
+ * 0..edgeCount-1 (full: every edge exactly once).
340
+ * @param s - the snapshot
341
+ * @param full - whether to run the permutation check
342
+ */
343
+ export function checkI6(s: GraphSnapshot, full: boolean): void {
344
+ if (!s.directed) {
345
+ return;
346
+ }
347
+ const { arcCount, edgeCount } = s;
348
+ if (arcCount !== edgeCount) {
349
+ throw invariantViolation("I6", `directed snapshot has arcCount ${arcCount} but edgeCount ${edgeCount}`, {
350
+ arcCount,
351
+ edgeCount,
352
+ });
353
+ }
354
+ if (!full || s.flags.arcToEdgeIsIdentity) {
355
+ return;
356
+ }
357
+ const { arcToEdge } = s;
358
+ const seen = new Uint8Array(edgeCount);
359
+ for (let a = 0; a < arcCount; a++) {
360
+ const e = arcToEdge[a];
361
+ if (e >= edgeCount || seen[e] === 1) {
362
+ throw invariantViolation("I6", `edge ${e} appears more than once in arcToEdge (arc ${a})`, {
363
+ arc: a,
364
+ edge: e,
365
+ });
366
+ }
367
+ seen[e] = 1;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * I7 (structure part): an undirected snapshot has arcCount === 2 * edgeCount - selfLoopCount.
373
+ * @param s - the snapshot
374
+ */
375
+ export function checkI7Counts(s: GraphSnapshot): void {
376
+ if (s.directed) {
377
+ return;
378
+ }
379
+ const expected = 2 * s.edgeCount - s.selfLoopCount;
380
+ if (s.arcCount !== expected) {
381
+ throw invariantViolation(
382
+ "I7",
383
+ `undirected snapshot has arcCount ${s.arcCount}, expected 2 * ${s.edgeCount} - ${s.selfLoopCount} = ${expected}`,
384
+ { arcCount: s.arcCount, expected },
385
+ );
386
+ }
387
+ }
388
+
389
+ /**
390
+ * I7 (pairing part): every arc u -> v with u !== v has exactly one mate v -> u with the same logical
391
+ * edge and weight, a self-loop edge has exactly one arc, and every edge appears once (loop) or twice
392
+ * (mates). The lockstep walk of design section 6.4. Full level.
393
+ * @param s - the snapshot
394
+ */
395
+ export function checkI7Pairing(s: GraphSnapshot): void {
396
+ if (s.directed) {
397
+ return;
398
+ }
399
+ const { nodeCount, arcCount, edgeCount, rowPtr, colIdx, weights, arcToEdge } = s;
400
+ const occurrences = new Uint8Array(edgeCount);
401
+ for (let a = 0; a < arcCount; a++) {
402
+ const e = arcToEdge[a];
403
+ if (occurrences[e] === 2) {
404
+ throw invariantViolation("I7", `edge ${e} appears more than twice in arcToEdge (arc ${a})`, {
405
+ arc: a,
406
+ edge: e,
407
+ });
408
+ }
409
+ occurrences[e]++;
410
+ }
411
+ const cursor = rowPtr.slice(0, nodeCount);
412
+ for (let u = 0; u < nodeCount; u++) {
413
+ const end = rowPtr[u + 1];
414
+ let a = cursor[u];
415
+ if (a < rowPtr[u] || a > end) {
416
+ throw invariantViolation("I7", `row ${u} was over-consumed by mates from earlier rows`, { row: u });
417
+ }
418
+ while (a < end) {
419
+ const v = colIdx[a];
420
+ let g = a + 1;
421
+ while (g < end && colIdx[g] === v) {
422
+ g++;
423
+ }
424
+ const k = g - a;
425
+ if (v === u) {
426
+ for (let i = a; i < g; i++) {
427
+ if (occurrences[arcToEdge[i]] !== 1) {
428
+ throw invariantViolation("I7", `self-loop edge ${arcToEdge[i]} has more than one arc`, {
429
+ row: u,
430
+ arc: i,
431
+ edge: arcToEdge[i],
432
+ });
433
+ }
434
+ }
435
+ } else {
436
+ if (v < u) {
437
+ throw invariantViolation("I7", `arc ${a} in row ${u} targets ${v} but row ${v} holds no mate`, {
438
+ row: u,
439
+ arc: a,
440
+ target: v,
441
+ });
442
+ }
443
+ const b = cursor[v];
444
+ const rowEnd = rowPtr[v + 1];
445
+ for (let i = 0; i < k; i++) {
446
+ const arc = a + i;
447
+ const mate = b + i;
448
+ if (mate >= rowEnd || colIdx[mate] !== u) {
449
+ throw invariantViolation(
450
+ "I7",
451
+ `arc ${arc} in row ${u} targeting ${v} has no mate in row ${v}`,
452
+ {
453
+ row: u,
454
+ arc,
455
+ target: v,
456
+ },
457
+ );
458
+ }
459
+ if (arcToEdge[mate] !== arcToEdge[arc]) {
460
+ throw invariantViolation(
461
+ "I7",
462
+ `arcs ${arc} and ${mate} should share a logical edge but hold ${arcToEdge[arc]} and ${arcToEdge[mate]}`,
463
+ { row: u, arc, mate },
464
+ );
465
+ }
466
+ if (occurrences[arcToEdge[arc]] !== 2) {
467
+ throw invariantViolation("I7", `edge ${arcToEdge[arc]} of arc ${arc} does not have two arcs`, {
468
+ row: u,
469
+ arc,
470
+ edge: arcToEdge[arc],
471
+ });
472
+ }
473
+ if (weights !== null && weights[arc] !== weights[mate]) {
474
+ throw invariantViolation(
475
+ "I7",
476
+ `arcs ${arc} and ${mate} carry different weights ${weights[arc]} and ${weights[mate]}`,
477
+ { row: u, arc, mate },
478
+ );
479
+ }
480
+ }
481
+ if (b + k < rowEnd && colIdx[b + k] === u) {
482
+ throw invariantViolation("I7", `row ${v} holds more arcs to ${u} than row ${u} holds to ${v}`, {
483
+ row: v,
484
+ arc: b + k,
485
+ target: u,
486
+ });
487
+ }
488
+ cursor[v] = b + k;
489
+ }
490
+ a = g;
491
+ }
492
+ cursor[u] = end;
493
+ }
494
+ }
495
+
496
+ /**
497
+ * I8 (length part): weights is null or has arcCount entries.
498
+ * @param s - the snapshot
499
+ */
500
+ export function checkI8Length(s: GraphSnapshot): void {
501
+ const { weights, arcCount } = s;
502
+ if (weights !== null && weights.length !== arcCount) {
503
+ throw invariantViolation("I8", `weights has ${weights.length} entries, expected ${arcCount}`, {
504
+ expected: arcCount,
505
+ found: weights.length,
506
+ });
507
+ }
508
+ }
509
+
510
+ /**
511
+ * I8 (value part): no weight is NaN. Full level.
512
+ * @param s - the snapshot
513
+ */
514
+ export function checkI8NaN(s: GraphSnapshot): void {
515
+ const { weights } = s;
516
+ if (weights === null) {
517
+ return;
518
+ }
519
+ for (let a = 0; a < weights.length; a++) {
520
+ if (Number.isNaN(weights[a])) {
521
+ throw invariantViolation("I8", `weights[${a}] is NaN`, { arc: a });
522
+ }
523
+ }
524
+ }
525
+
526
+ // ============================================================ I9 - I13: flags, alignment, ids, columns
527
+
528
+ /**
529
+ * I9: every flag equals its predicate over the arrays, and selfLoopCount equals the number of loop
530
+ * arcs. Full level.
531
+ * @param s - the snapshot
532
+ */
533
+ export function checkI9(s: GraphSnapshot): void {
534
+ const loopArcs = countLoopArcs(s.rowPtr, s.colIdx, s.nodeCount);
535
+ if (loopArcs !== s.selfLoopCount) {
536
+ throw invariantViolation("I9", `selfLoopCount is ${s.selfLoopCount} but ${loopArcs} loop arcs exist`, {
537
+ reason: "selfLoopCount",
538
+ expected: loopArcs,
539
+ found: s.selfLoopCount,
540
+ });
541
+ }
542
+ const recomputed = computeFlags({
543
+ directed: s.directed,
544
+ nodeCount: s.nodeCount,
545
+ rowPtr: s.rowPtr,
546
+ colIdx: s.colIdx,
547
+ weights: s.weights,
548
+ arcToEdge: s.flags.arcToEdgeIsIdentity ? null : s.arcToEdge,
549
+ selfLoopCount: s.selfLoopCount,
550
+ });
551
+ const names: readonly (keyof SnapshotFlags)[] = [
552
+ "multigraph",
553
+ "hasSelfLoops",
554
+ "arcToEdgeIsIdentity",
555
+ "weighted",
556
+ "allWeightsOne",
557
+ "nonNegativeWeights",
558
+ "finiteWeights",
559
+ ];
560
+ for (const name of names) {
561
+ if (s.flags[name] !== recomputed[name]) {
562
+ throw invariantViolation("I9", `flag ${name} is ${s.flags[name]} but the arrays say ${recomputed[name]}`, {
563
+ flag: name,
564
+ found: s.flags[name],
565
+ expected: recomputed[name],
566
+ });
567
+ }
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Check one core array against its arena segment (design section 10.3).
573
+ * @param arena - the arena
574
+ * @param name - the core array name
575
+ * @param array - the array, or null when absent
576
+ */
577
+ function checkSegment(arena: ArenaLayout, name: CoreArrayName, array: ArrayBufferView | null): void {
578
+ const segment = arena.segments[name];
579
+ if (segment === null) {
580
+ return;
581
+ }
582
+ if (array === null) {
583
+ throw invariantViolation("I10", `arena names a segment for absent core array ${name}`, { array: name });
584
+ }
585
+ const relative = segment.byteOffset - arena.byteOffset;
586
+ if (relative < 0 || relative % ALIGNMENT !== 0 || relative + segment.byteLength > arena.byteLength) {
587
+ throw invariantViolation(
588
+ "I10",
589
+ `arena segment ${name} at ${segment.byteOffset} is not 256-aligned inside the arena`,
590
+ {
591
+ array: name,
592
+ byteOffset: segment.byteOffset,
593
+ },
594
+ );
595
+ }
596
+ if (
597
+ array.buffer !== arena.buffer ||
598
+ array.byteOffset !== segment.byteOffset ||
599
+ array.byteLength !== segment.byteLength
600
+ ) {
601
+ throw invariantViolation("I10", `core array ${name} is not the view its arena segment describes`, {
602
+ array: name,
603
+ byteOffset: array.byteOffset,
604
+ byteLength: array.byteLength,
605
+ });
606
+ }
607
+ }
608
+
609
+ /**
610
+ * I10: every core array is 4-byte aligned over a plain ArrayBuffer and, when an arena exists, every
611
+ * array with a non-null segment is the 256-aligned view the segment describes and hotByteLength is the
612
+ * end of the weights (or colIdx, or rowPtr) segment.
613
+ * @param s - the snapshot
614
+ */
615
+ export function checkI10(s: GraphSnapshot): void {
616
+ const identity = s.flags.arcToEdgeIsIdentity;
617
+ const arrays: readonly [CoreArrayName, ArrayBufferView | null][] = [
618
+ ["rowPtr", s.rowPtr],
619
+ ["colIdx", s.colIdx],
620
+ ["weights", s.weights],
621
+ ["arcToEdge", identity ? null : s.arcToEdge],
622
+ ["edgeToArc", identity ? null : s.edgeToArc],
623
+ ];
624
+ for (const [name, array] of arrays) {
625
+ if (array === null) {
626
+ continue;
627
+ }
628
+ if (!isFourByteAligned(array)) {
629
+ throw invariantViolation("I10", `core array ${name} is not 4-byte aligned`, {
630
+ array: name,
631
+ byteOffset: array.byteOffset,
632
+ byteLength: array.byteLength,
633
+ });
634
+ }
635
+ if (!isOverPlainBuffer(array)) {
636
+ throw invariantViolation("I10", `core array ${name} is not a view over a plain ArrayBuffer`, {
637
+ array: name,
638
+ reason: "buffer",
639
+ });
640
+ }
641
+ }
642
+ const { arena } = s;
643
+ if (arena === null) {
644
+ return;
645
+ }
646
+ const { alignment }: { alignment: number } = arena;
647
+ if (alignment !== ALIGNMENT) {
648
+ throw invariantViolation("I10", `arena alignment is ${alignment}, expected ${ALIGNMENT}`, {
649
+ found: alignment,
650
+ });
651
+ }
652
+ for (const [name, array] of arrays) {
653
+ checkSegment(arena, name, array);
654
+ }
655
+ let hotEnd = 0;
656
+ for (const name of ["rowPtr", "colIdx", "weights"] as const) {
657
+ const segment = arena.segments[name];
658
+ if (segment !== null) {
659
+ hotEnd = segment.byteOffset + segment.byteLength - arena.byteOffset;
660
+ }
661
+ }
662
+ if (arena.hotByteLength !== hotEnd) {
663
+ throw invariantViolation("I10", `arena.hotByteLength is ${arena.hotByteLength}, expected ${hotEnd}`, {
664
+ reason: "hotByteLength",
665
+ found: arena.hotByteLength,
666
+ expected: hotEnd,
667
+ });
668
+ }
669
+ }
670
+
671
+ /**
672
+ * I11 (size part): the id map has nodeCount ids.
673
+ * @param s - the snapshot
674
+ */
675
+ export function checkI11Size(s: GraphSnapshot): void {
676
+ if (s.ids.size !== s.nodeCount) {
677
+ throw invariantViolation("I11", `ids.size is ${s.ids.size}, expected nodeCount ${s.nodeCount}`, {
678
+ expected: s.nodeCount,
679
+ found: s.ids.size,
680
+ });
681
+ }
682
+ }
683
+
684
+ /**
685
+ * I11 (bijection part): ids.indexOf(ids.idOf(i)) === i for every i and no id is NaN. Full level.
686
+ * @param s - the snapshot
687
+ */
688
+ export function checkI11Bijection(s: GraphSnapshot): void {
689
+ const { ids, nodeCount } = s;
690
+ for (let i = 0; i < nodeCount; i++) {
691
+ const id = ids.idOf(i);
692
+ if (typeof id === "number" && Number.isNaN(id)) {
693
+ throw invariantViolation("I11", `id of node ${i} is NaN`, { index: i });
694
+ }
695
+ const back = ids.indexOf(id);
696
+ if (back !== i) {
697
+ throw invariantViolation("I11", `ids.indexOf(ids.idOf(${i})) is ${back}`, { index: i, found: back });
698
+ }
699
+ }
700
+ }
701
+
702
+ /**
703
+ * The E_INVALID_SNAPSHOT error of one column length-rule violation (I12).
704
+ * @param table - the table name for the message
705
+ * @param column - the column
706
+ * @param message - what is wrong
707
+ * @param details - extra location details
708
+ * @returns the error
709
+ */
710
+ function columnViolation(
711
+ table: string,
712
+ column: Column,
713
+ message: string,
714
+ details: Readonly<Record<string, unknown>> = {},
715
+ ): GraphFormatError {
716
+ return invariantViolation("I12", `column "${column.meta.name}" of table ${table}: ${message}`, {
717
+ table,
718
+ column: column.meta.name,
719
+ ...details,
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Check that offsets are rows + 1 long, non-decreasing and end at `end`.
725
+ * @param table - the table name for the message
726
+ * @param column - the column
727
+ * @param offsets - the offsets
728
+ * @param rows - the row count
729
+ * @param end - the expected last offset
730
+ */
731
+ function checkOffsets(table: string, column: Column, offsets: U32, rows: number, end: number): void {
732
+ if (offsets.length !== rows + 1) {
733
+ throw columnViolation(table, column, `offsets has ${offsets.length} entries, expected ${rows + 1}`, {
734
+ expected: rows + 1,
735
+ found: offsets.length,
736
+ });
737
+ }
738
+ if (offsets[0] !== 0) {
739
+ throw columnViolation(table, column, `offsets[0] is ${offsets[0]}`, { row: 0 });
740
+ }
741
+ for (let r = 0; r < rows; r++) {
742
+ if (offsets[r + 1] < offsets[r]) {
743
+ throw columnViolation(table, column, `offsets decrease at row ${r}`, { row: r });
744
+ }
745
+ }
746
+ if (offsets[rows] !== end) {
747
+ throw columnViolation(table, column, `offsets[${rows}] is ${offsets[rows]}, expected ${end}`, {
748
+ row: rows,
749
+ found: offsets[rows],
750
+ expected: end,
751
+ });
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Check the values of a refersTo u32 array: every entry below `bound` or INVALID_INDEX; when
757
+ * `rowValidity` is given an INVALID_INDEX entry requires its row to be unset.
758
+ * @param table - the table name for the message
759
+ * @param column - the column
760
+ * @param values - the u32 values
761
+ * @param components - values per row
762
+ * @param bound - the referenced space's row count
763
+ * @param rowValidity - the validity bitmap of the rows (null = all set), or undefined to skip the unset rule
764
+ */
765
+ function checkReferences(
766
+ table: string,
767
+ column: Column,
768
+ values: U32,
769
+ components: number,
770
+ bound: number,
771
+ rowValidity: U32 | null | undefined,
772
+ ): void {
773
+ for (let i = 0; i < values.length; i++) {
774
+ const value = values[i];
775
+ if (value === INVALID_INDEX) {
776
+ const row = Math.floor(i / components);
777
+ if (rowValidity !== undefined && (rowValidity === null || bitmapGet(rowValidity, row))) {
778
+ throw columnViolation(table, column, `row ${row} is set but holds INVALID_INDEX`, { row });
779
+ }
780
+ continue;
781
+ }
782
+ if (value >= bound) {
783
+ throw columnViolation(table, column, `value ${value} at ${i} is not below ${bound}`, {
784
+ row: Math.floor(i / components),
785
+ found: value,
786
+ expected: bound,
787
+ });
788
+ }
789
+ }
790
+ }
791
+
792
+ /**
793
+ * The refersTo rules of invariant I12 for one column: a u32 column's values (or a list column's u32
794
+ * child items) are below `bound` or INVALID_INDEX, and a scalar INVALID_INDEX sits in an unset row.
795
+ * @param table - the table name for the message
796
+ * @param column - a column whose meta.refersTo is set
797
+ * @param bound - the referenced space's row count
798
+ */
799
+ function checkColumnReferences(table: string, column: Column, bound: number): void {
800
+ switch (column.dtype) {
801
+ case "u32":
802
+ checkReferences(table, column, column.data, column.meta.components, bound, column.validity);
803
+ return;
804
+ case "list": {
805
+ const { child } = column;
806
+ if (child.dtype !== "u32") {
807
+ throw columnViolation(table, column, `refersTo list requires a u32 child, found ${child.dtype}`);
808
+ }
809
+ checkReferences(table, column, child.data, 1, bound, undefined);
810
+ return;
811
+ }
812
+ default:
813
+ throw columnViolation(table, column, `refersTo requires dtype u32, found ${column.dtype}`);
814
+ }
815
+ }
816
+
817
+ /**
818
+ * The typed arrays a column exposes to a GPU consumer (data, validity, codes, list child data): each
819
+ * must be a view over a plain ArrayBuffer (I10, decision D-SAB).
820
+ * @param column - the column
821
+ * @returns the arrays with their names
822
+ */
823
+ function columnArrays(column: Column): readonly [string, ArrayBufferView | null][] {
824
+ switch (column.dtype) {
825
+ case "f32":
826
+ case "f64":
827
+ case "i32":
828
+ case "u32":
829
+ case "u8":
830
+ case "bool":
831
+ return [
832
+ ["data", column.data],
833
+ ["validity", column.validity],
834
+ ];
835
+ case "dict":
836
+ return [
837
+ ["codes", column.codes],
838
+ ["validity", column.validity],
839
+ ];
840
+ case "string":
841
+ return [
842
+ ["offsets", column.offsets],
843
+ ["utf8", column.utf8],
844
+ ["validity", column.validity],
845
+ ];
846
+ case "list":
847
+ return [
848
+ ["offsets", column.offsets],
849
+ ["validity", column.validity],
850
+ ...columnArrays(column.child).map(([name, array]): [string, ArrayBufferView | null] => [
851
+ `child.${name}`,
852
+ array,
853
+ ]),
854
+ ];
855
+ case "json":
856
+ return [["validity", column.validity]];
857
+ default: {
858
+ const unknown: never = column;
859
+ throw invariantViolation("I12", `unknown dtype ${(unknown as Column).dtype}`);
860
+ }
861
+ }
862
+ }
863
+
864
+ /**
865
+ * The length rules of design section 5.7 for one column, plus dictionary code ranges, refersTo
866
+ * ranges, the recorded nullCount (recomputed from the bitmap, design section 9.5) and the plain
867
+ * ArrayBuffer rule of I10 for every typed array the column exposes (I12).
868
+ * @param table - the table name for the message
869
+ * @param column - the column
870
+ * @param rows - the table's row count
871
+ * @param s - the snapshot (for the referenced row counts)
872
+ */
873
+ export function checkColumnRules(table: string, column: Column, rows: number, s: GraphSnapshot): void {
874
+ if (column.length !== rows) {
875
+ throw columnViolation(table, column, `has ${column.length} rows, expected ${rows}`, {
876
+ expected: rows,
877
+ found: column.length,
878
+ });
879
+ }
880
+ const { validity, meta } = column;
881
+ const validityWords = Math.ceil(rows / 32);
882
+ if (validity !== null && validity.length !== validityWords) {
883
+ throw columnViolation(table, column, `validity has ${validity.length} words, expected ${validityWords}`, {
884
+ expected: validityWords,
885
+ found: validity.length,
886
+ });
887
+ }
888
+ const nullCount = validity === null ? 0 : rows - bitmapCount(validity, rows);
889
+ if (column.nullCount !== nullCount) {
890
+ throw columnViolation(
891
+ table,
892
+ column,
893
+ `nullCount is ${column.nullCount}, the validity bitmap says ${nullCount}`,
894
+ {
895
+ expected: nullCount,
896
+ found: column.nullCount,
897
+ reason: "nullCount",
898
+ },
899
+ );
900
+ }
901
+ for (const [name, array] of columnArrays(column)) {
902
+ if (array !== null && !isOverPlainBuffer(array)) {
903
+ throw columnViolation(table, column, `${name} is not a view over a plain ArrayBuffer`, {
904
+ array: name,
905
+ reason: "buffer",
906
+ });
907
+ }
908
+ }
909
+ const refBound = meta.refersTo === "node" ? s.nodeCount : s.edgeCount;
910
+ const { dtype } = column;
911
+ switch (dtype) {
912
+ case "f32":
913
+ case "f64":
914
+ case "i32":
915
+ case "u32":
916
+ case "u8": {
917
+ const expected = rows * meta.components;
918
+ if (column.data.length !== expected) {
919
+ throw columnViolation(table, column, `data has ${column.data.length} values, expected ${expected}`, {
920
+ expected,
921
+ found: column.data.length,
922
+ });
923
+ }
924
+ if (meta.refersTo !== null) {
925
+ checkColumnReferences(table, column, refBound);
926
+ }
927
+ break;
928
+ }
929
+ case "bool":
930
+ if (column.data.length !== validityWords) {
931
+ throw columnViolation(
932
+ table,
933
+ column,
934
+ `bool data has ${column.data.length} words, expected ${validityWords}`,
935
+ {
936
+ expected: validityWords,
937
+ found: column.data.length,
938
+ },
939
+ );
940
+ }
941
+ break;
942
+ case "dict": {
943
+ if (column.codes.length !== rows) {
944
+ throw columnViolation(table, column, `codes has ${column.codes.length} entries, expected ${rows}`, {
945
+ expected: rows,
946
+ found: column.codes.length,
947
+ });
948
+ }
949
+ const size = column.dictionary.length;
950
+ for (let r = 0; r < rows; r++) {
951
+ if (column.codes[r] >= size && (validity === null || bitmapGet(validity, r))) {
952
+ throw columnViolation(table, column, `code ${column.codes[r]} at row ${r} is not below ${size}`, {
953
+ row: r,
954
+ found: column.codes[r],
955
+ });
956
+ }
957
+ }
958
+ break;
959
+ }
960
+ case "string":
961
+ checkOffsets(table, column, column.offsets, rows, column.utf8.length);
962
+ break;
963
+ case "list": {
964
+ const { child } = column;
965
+ checkOffsets(table, column, column.offsets, rows, child.length);
966
+ if (child.validity !== null) {
967
+ throw columnViolation(table, column, "list child column has a validity bitmap");
968
+ }
969
+ if (meta.refersTo !== null) {
970
+ checkColumnReferences(table, column, refBound);
971
+ }
972
+ break;
973
+ }
974
+ case "json":
975
+ if (column.values.length !== rows) {
976
+ throw columnViolation(table, column, `values has ${column.values.length} entries, expected ${rows}`, {
977
+ expected: rows,
978
+ found: column.values.length,
979
+ });
980
+ }
981
+ break;
982
+ default: {
983
+ const unknown: never = dtype;
984
+ throw columnViolation(table, column, `unknown dtype ${String(unknown)}`);
985
+ }
986
+ }
987
+ }
988
+
989
+ /**
990
+ * Check every column of a table against its row count.
991
+ * @param name - the table name for the message
992
+ * @param table - the table
993
+ * @param rows - the expected row count
994
+ * @param s - the snapshot
995
+ */
996
+ function checkTable(name: string, table: AttributeTable, rows: number, s: GraphSnapshot): void {
997
+ for (const column of table) {
998
+ checkColumnRules(name, column, rows, s);
999
+ }
1000
+ }
1001
+
1002
+ /**
1003
+ * I12: the node, edge and graph tables have nodeCount, edgeCount and 1 rows, every column (extension
1004
+ * tables included) obeys its length rules, dictionary codes are in range and refersTo values are in
1005
+ * range or INVALID_INDEX with the row unset.
1006
+ * @param s - the snapshot
1007
+ */
1008
+ export function checkI12(s: GraphSnapshot): void {
1009
+ const tables: readonly [string, AttributeTable, number][] = [
1010
+ ["nodes", s.nodes, s.nodeCount],
1011
+ ["edges", s.edges, s.edgeCount],
1012
+ ["graph", s.graph, 1],
1013
+ ];
1014
+ for (const [name, table, rows] of tables) {
1015
+ if (table.rowCount !== rows) {
1016
+ throw invariantViolation("I12", `table ${name} has ${table.rowCount} rows, expected ${rows}`, {
1017
+ table: name,
1018
+ expected: rows,
1019
+ found: table.rowCount,
1020
+ });
1021
+ }
1022
+ checkTable(name, table, rows, s);
1023
+ }
1024
+ for (const [name, table] of s.extensions) {
1025
+ checkTable(`extensions[${name}]`, table, table.rowCount, s);
1026
+ }
1027
+ }
1028
+
1029
+ /**
1030
+ * I13: edge attribute columns are indexed by logical edge: the edge table has edgeCount rows.
1031
+ * @param s - the snapshot
1032
+ */
1033
+ export function checkI13(s: GraphSnapshot): void {
1034
+ if (s.edges.rowCount !== s.edgeCount) {
1035
+ throw invariantViolation("I13", `edge table has ${s.edges.rowCount} rows, expected edgeCount ${s.edgeCount}`, {
1036
+ expected: s.edgeCount,
1037
+ found: s.edges.rowCount,
1038
+ });
1039
+ }
1040
+ }
1041
+
1042
+ // ============================================================ levels
1043
+
1044
+ /**
1045
+ * The "structure" level of design section 9.5: I1-I3, I5 lengths and ranges, I6 / I7 counts, I8
1046
+ * length, I10, I11 size, I12 lengths and ranges, I13. O(n + m + columns).
1047
+ * @param s - the snapshot
1048
+ */
1049
+ export function validateStructure(s: GraphSnapshot): void {
1050
+ checkI3(s);
1051
+ checkI1(s);
1052
+ checkI2(s);
1053
+ checkI5Ranges(s);
1054
+ checkI6(s, false);
1055
+ checkI7Counts(s);
1056
+ checkI8Length(s);
1057
+ checkI10(s);
1058
+ checkI11Size(s);
1059
+ checkI13(s);
1060
+ checkI12(s);
1061
+ }
1062
+
1063
+ /**
1064
+ * The "full" level of design section 9.5: structure plus I4, I5 orientation, I6 permutation, I8 NaN
1065
+ * (before the pairing walk, so a NaN weight is reported as I8 rather than as an unequal mate), I7
1066
+ * pairing, I9 flags, I11 bijection and unique columns. O(m log d).
1067
+ * @param s - the snapshot
1068
+ */
1069
+ export function validateFull(s: GraphSnapshot): void {
1070
+ validateStructure(s);
1071
+ checkI4(s);
1072
+ checkI5Orientation(s);
1073
+ checkI6(s, true);
1074
+ checkI8NaN(s);
1075
+ checkI7Pairing(s);
1076
+ checkI9(s);
1077
+ checkI11Bijection(s);
1078
+ const tables: readonly [string, AttributeTable][] = [
1079
+ ["nodes", s.nodes],
1080
+ ["edges", s.edges],
1081
+ ["graph", s.graph],
1082
+ ...[...s.extensions].map(([name, table]): [string, AttributeTable] => [`extensions[${name}]`, table]),
1083
+ ];
1084
+ for (const [name, table] of tables) {
1085
+ for (const column of table) {
1086
+ checkUniqueColumn(name, column);
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * The `unique` rule of a column as a validation failure (design sections 9.5 and 11.4): the
1093
+ * freeze-time codes E_DUPLICATE_ID / E_DUPLICATE_EDGE_ID of `verifyUniqueColumn` become
1094
+ * E_INVALID_SNAPSHOT with `details.invariant` "I12", `details.reason` "unique" and the original
1095
+ * code in `details.cause`, so a reader of untrusted input sees one error family.
1096
+ * @param table - the table name for the message
1097
+ * @param column - the column
1098
+ */
1099
+ export function checkUniqueColumn(table: string, column: Column): void {
1100
+ try {
1101
+ verifyUniqueColumn(column);
1102
+ } catch (err) {
1103
+ if (err instanceof GraphFormatError && (err.code === "E_DUPLICATE_ID" || err.code === "E_DUPLICATE_EDGE_ID")) {
1104
+ throw columnViolation(table, column, err.message, { ...err.details, reason: "unique", cause: err.code });
1105
+ }
1106
+ throw err;
1107
+ }
1108
+ }