tla-ts 0.0.1 → 0.0.3-dev.a50fc89

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 (71) hide show
  1. package/README.md +81 -42
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +215 -30
  4. package/dist/index.d.ts +215 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +20 -1
  11. package/dist/run-cli.js +20 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +6 -2
  15. package/src/ast.ts +122 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +945 -238
  20. package/src/cli-options.ts +58 -0
  21. package/src/codegen.ts +52 -18
  22. package/src/config.ts +39 -11
  23. package/src/environment.ts +485 -140
  24. package/src/errors.ts +59 -1
  25. package/src/evaluate.ts +489 -137
  26. package/src/exit-codes.ts +75 -0
  27. package/src/fingerprints.ts +2 -2
  28. package/src/index.ts +9 -4
  29. package/src/instances.ts +221 -66
  30. package/src/java-overrides.ts +241 -0
  31. package/src/json.ts +153 -72
  32. package/src/levels.ts +850 -0
  33. package/src/lexer.ts +103 -28
  34. package/src/liveness.ts +47 -91
  35. package/src/obligation-action.ts +384 -0
  36. package/src/obligation-anon.ts +575 -0
  37. package/src/obligation-deref.ts +637 -0
  38. package/src/obligation-expression.ts +1060 -0
  39. package/src/obligation-format.ts +406 -0
  40. package/src/obligation-generate.ts +1074 -0
  41. package/src/obligation-levels.ts +329 -0
  42. package/src/obligation-module.ts +1042 -0
  43. package/src/obligation-printer.ts +889 -0
  44. package/src/obligation-proof.ts +76 -0
  45. package/src/obligation-source.ts +1084 -0
  46. package/src/obligation-tuples.ts +184 -0
  47. package/src/obligations.ts +316 -0
  48. package/src/parallel-worker.ts +57 -28
  49. package/src/parallel.ts +116 -62
  50. package/src/parser.ts +958 -205
  51. package/src/pretty-print.ts +259 -0
  52. package/src/randomization.ts +24 -113
  53. package/src/resolve.ts +472 -41
  54. package/src/run-cli.ts +427 -61
  55. package/src/runtime.ts +63 -28
  56. package/src/spec-description.ts +509 -0
  57. package/src/standard-levels.ts +105 -0
  58. package/src/standard-modules.ts +223 -92
  59. package/src/subexpressions.ts +21 -35
  60. package/src/substitution-levels.ts +184 -0
  61. package/src/successors.ts +261 -46
  62. package/src/symmetry.ts +65 -21
  63. package/src/tableau.ts +43 -37
  64. package/src/tlc-ext.ts +120 -33
  65. package/src/tlc-module.ts +71 -47
  66. package/src/tlc-modules.ts +1022 -0
  67. package/src/tlc-order.ts +497 -0
  68. package/src/tlc-random.ts +411 -0
  69. package/src/tlc-summary.ts +109 -0
  70. package/src/transfer.ts +26 -5
  71. package/src/values.ts +1231 -176
@@ -0,0 +1,411 @@
1
+ import { TlaEvaluationError } from "./errors";
2
+ import {
3
+ createRecord,
4
+ createSortedFunction,
5
+ createTuple,
6
+ getFunctionDomain,
7
+ getStringRank,
8
+ isEmptySet,
9
+ isFunctionValue,
10
+ isModelValue,
11
+ toFiniteFunction,
12
+ toFiniteSet,
13
+ type Value,
14
+ } from "./values";
15
+
16
+ export interface JavaRandom {
17
+ setSeed: (seed: bigint) => void;
18
+ nextInt: (bound: number) => number;
19
+ nextLong: () => bigint;
20
+ nextDouble: () => number;
21
+ }
22
+
23
+ const MULTIPLIER = 0x5deece66dn;
24
+ const SEED_MASK = (1n << 48n) - 1n;
25
+ const MAXIMUM_INT = 2147483647;
26
+
27
+ // java.util.Random, which TLC's RandomEnumerableValues extends.
28
+ export const createJavaRandom = (seed: bigint): JavaRandom => {
29
+ const state = { value: (BigInt.asUintN(64, seed) ^ MULTIPLIER) & SEED_MASK };
30
+ const next = (bits: number) => {
31
+ state.value = (state.value * MULTIPLIER + 0xbn) & SEED_MASK;
32
+ return Number(BigInt.asIntN(32, state.value >> BigInt(48 - bits)));
33
+ };
34
+ return {
35
+ setSeed: (value) => {
36
+ state.value = (BigInt.asUintN(64, value) ^ MULTIPLIER) & SEED_MASK;
37
+ },
38
+ nextInt: (bound) => {
39
+ const first = next(31);
40
+ const mask = bound - 1;
41
+ if ((bound & mask) === 0) return Number((BigInt(bound) * BigInt(first)) >> 31n);
42
+ for (let candidate = first; ; candidate = next(31)) {
43
+ const value = candidate % bound;
44
+ if (candidate - value + mask <= MAXIMUM_INT) return value;
45
+ }
46
+ },
47
+ nextLong: () => BigInt.asIntN(64, (BigInt(next(32)) << 32n) + BigInt(next(32))),
48
+ nextDouble: () => (next(26) * 2 ** 27 + next(27)) * 2 ** -53,
49
+ };
50
+ };
51
+
52
+ // FP64 with TLC's first irreducible polynomial, FP64.Polys[0], which `-fp 0` selects.
53
+ const IRREDUCIBLE_POLYNOMIAL = 0x911498ae0e66bad6n;
54
+
55
+ const BYTE_MOD_TABLE = (() => {
56
+ const powers: bigint[] = [];
57
+ for (let power = 1n << 63n; powers.length < 72;) {
58
+ powers.push(power);
59
+ power = (power >> 1n) ^ ((power & 1n) === 0n ? 0n : IRREDUCIBLE_POLYNOMIAL);
60
+ }
61
+ return Array.from({ length: 256 }, (_, byte) =>
62
+ powers
63
+ .slice(64, 72)
64
+ .reverse()
65
+ .reduce((value, power, bit) => ((byte >> bit) & 1 ? value ^ power : value), 0n),
66
+ );
67
+ })();
68
+
69
+ const BYTE_MOD_HIGH = Int32Array.from(BYTE_MOD_TABLE, (entry) =>
70
+ Number(BigInt.asIntN(32, entry >> 32n)),
71
+ );
72
+ const BYTE_MOD_LOW = Int32Array.from(BYTE_MOD_TABLE, (entry) => Number(BigInt.asIntN(32, entry)));
73
+
74
+ // The fingerprint being extended, as its high and low 32 bits, which avoids a BigInt operation
75
+ // for every byte.
76
+ const INITIAL_HIGH = Number(BigInt.asIntN(32, IRREDUCIBLE_POLYNOMIAL >> 32n));
77
+ const INITIAL_LOW = Number(BigInt.asIntN(32, IRREDUCIBLE_POLYNOMIAL));
78
+ let high = INITIAL_HIGH;
79
+ let low = INITIAL_LOW;
80
+
81
+ const extendByte = (byte: number) => {
82
+ const index = (byte ^ low) & 0xff;
83
+ low = ((low >>> 8) | (high << 24)) ^ BYTE_MOD_LOW[index];
84
+ high = (high >>> 8) ^ BYTE_MOD_HIGH[index];
85
+ };
86
+
87
+ const extendInt = (value: number) => {
88
+ for (let shift = 0; shift < 32; shift += 8) extendByte(value >>> shift);
89
+ };
90
+
91
+ const BOOL_VALUE = 0;
92
+ const INT_VALUE = 1;
93
+ const STRING_VALUE = 3;
94
+ const SET_ENUM_VALUE = 5;
95
+ const FCN_RCD_VALUE = 9;
96
+ const MODEL_VALUE = 21;
97
+
98
+ // Value#fingerPrint for each of TLC's value classes. A model value extends the fingerprint with
99
+ // its UniqueString token, and TLC numbers tokens from 1 in intern order.
100
+ const extendValue = (value: Value) => {
101
+ if (typeof value === "boolean") {
102
+ extendByte(BOOL_VALUE);
103
+ extendByte(value ? 116 : 102);
104
+ } else if (typeof value === "number") {
105
+ extendByte(INT_VALUE);
106
+ extendInt(value);
107
+ } else if (typeof value === "string") {
108
+ extendByte(STRING_VALUE);
109
+ extendInt(value.length);
110
+ for (let index = 0; index < value.length; index++) {
111
+ extendByte(value.charCodeAt(index));
112
+ }
113
+ } else if (isModelValue(value)) {
114
+ extendByte(MODEL_VALUE);
115
+ extendInt(getStringRank(value.name) + 1);
116
+ } else if (isFunctionValue(value)) {
117
+ const functionValue = toFiniteFunction(value);
118
+ extendByte(FCN_RCD_VALUE);
119
+ extendInt(functionValue.domain.length);
120
+ for (let index = 0; index < functionValue.domain.length; index++) {
121
+ extendValue(functionValue.domain[index]);
122
+ extendValue(functionValue.range[index]);
123
+ }
124
+ } else {
125
+ const elements = toFiniteSet(value).elements;
126
+ extendByte(SET_ENUM_VALUE);
127
+ extendInt(elements.length);
128
+ for (const element of elements) extendValue(element);
129
+ }
130
+ };
131
+
132
+ // TLCStateMut#fingerPrint of variable values in TLC's variable order. Evaluating a lazy value can
133
+ // fingerprint another, so this restores the fingerprint it interrupts.
134
+ export const getTlcFingerprint = (state: Value[]) => {
135
+ const outer = { high, low };
136
+ high = INITIAL_HIGH;
137
+ low = INITIAL_LOW;
138
+ for (const value of state) extendValue(value);
139
+ const fingerprint = (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
140
+ high = outer.high;
141
+ low = outer.low;
142
+ return fingerprint;
143
+ };
144
+
145
+ const getFingerprintLength = (value: Value): number => {
146
+ if (typeof value === "boolean") return 2;
147
+ if (typeof value === "number" || isModelValue(value)) return 5;
148
+ if (typeof value === "string") return 5 + value.length;
149
+ if (isFunctionValue(value)) {
150
+ const functionValue = toFiniteFunction(value);
151
+ return functionValue.domain.reduce<number>(
152
+ (length, key, index) =>
153
+ length + getFingerprintLength(key) + getFingerprintLength(functionValue.range[index]),
154
+ 5,
155
+ );
156
+ }
157
+ return toFiniteSet(value).elements.reduce<number>(
158
+ (length, element) => length + getFingerprintLength(element),
159
+ 5,
160
+ );
161
+ };
162
+
163
+ // The fingerprint after `length` zero bytes, as one table per byte of the fingerprint it shifts.
164
+ const createShiftTable = (length: number) => {
165
+ const table = new Int32Array(8 * 256 * 2);
166
+ for (let bit = 0; bit < 64; bit++) {
167
+ high = bit < 32 ? 0 : 1 << (bit - 32);
168
+ low = bit < 32 ? 1 << bit : 0;
169
+ for (let step = 0; step < length; step++) extendByte(0);
170
+ const position = bit >> 3;
171
+ const mask = 1 << (bit & 7);
172
+ for (let byte = mask; byte < 2 * mask; byte++) {
173
+ const entry = (position * 256 + byte) * 2;
174
+ const rest = (position * 256 + byte - mask) * 2;
175
+ table[entry] = table[rest] ^ high;
176
+ table[entry + 1] = table[rest + 1] ^ low;
177
+ }
178
+ }
179
+ return table;
180
+ };
181
+
182
+ // Extending a fingerprint by a byte is linear over GF(2), so extending it by a value's bytes
183
+ // XORs the value's own fingerprint from zero into the fingerprint shifted by their length.
184
+ // The fingerprinter caches each distinct variable value for one pass over the stored states.
185
+ export const createTlcFingerprinter = () => {
186
+ const shiftTables = new Map<number, Int32Array>();
187
+ const indices = new Map<Value, number>();
188
+ const contributions: number[] = [];
189
+ const getContribution = (value: Value) => {
190
+ const cached = indices.get(value);
191
+ if (cached !== undefined) return cached;
192
+ const length = getFingerprintLength(value);
193
+ high = 0;
194
+ low = 0;
195
+ extendValue(value);
196
+ indices.set(value, contributions.length);
197
+ contributions.push(length, high, low);
198
+ return contributions.length - 3;
199
+ };
200
+ const getShiftTable = (length: number) => {
201
+ const cached = shiftTables.get(length);
202
+ if (cached) return cached;
203
+ const table = createShiftTable(length);
204
+ shiftTables.set(length, table);
205
+ return table;
206
+ };
207
+ return (state: Value[]) => {
208
+ const outer = { high, low };
209
+ const fingerprint = { high: INITIAL_HIGH, low: INITIAL_LOW };
210
+ for (const value of state) {
211
+ const contribution = getContribution(value);
212
+ const table = getShiftTable(contributions[contribution]);
213
+ const extended = {
214
+ high: contributions[contribution + 1],
215
+ low: contributions[contribution + 2],
216
+ };
217
+ for (let position = 0; position < 8; position++) {
218
+ const word = position < 4 ? fingerprint.low : fingerprint.high;
219
+ const entry = (position * 256 + ((word >>> ((position & 3) * 8)) & 0xff)) * 2;
220
+ extended.high ^= table[entry];
221
+ extended.low ^= table[entry + 1];
222
+ }
223
+ fingerprint.high = extended.high;
224
+ fingerprint.low = extended.low;
225
+ }
226
+ high = outer.high;
227
+ low = outer.low;
228
+ return (BigInt(fingerprint.high >>> 0) << 32n) | BigInt(fingerprint.low >>> 0);
229
+ };
230
+ };
231
+
232
+ // RandomGenerator.primes holds the 1341 consecutive primes from 1277011 to 1295953.
233
+ const PRIMES: number[] = [];
234
+
235
+ const isPrime = (candidate: number) => {
236
+ for (let divisor = 2; divisor * divisor <= candidate; divisor++) {
237
+ if (candidate % divisor === 0) return false;
238
+ }
239
+ return true;
240
+ };
241
+
242
+ const getPrimes = () => {
243
+ for (let candidate = 1277011; PRIMES.length < 1341; candidate++) {
244
+ if (isPrime(candidate)) PRIMES.push(candidate);
245
+ }
246
+ return PRIMES;
247
+ };
248
+
249
+ const nextPrime = (random: JavaRandom) => {
250
+ const primes = getPrimes();
251
+ const choice = { index: primes.length };
252
+ while (choice.index === primes.length) {
253
+ choice.index = Math.floor(random.nextDouble() * primes.length);
254
+ }
255
+ return primes[choice.index];
256
+ };
257
+
258
+ const getPrimeFactors = (value: number) => {
259
+ const factors: number[] = [];
260
+ const remaining = { value };
261
+ for (let divisor = 2; divisor * divisor <= remaining.value; divisor++) {
262
+ while (remaining.value % divisor === 0) {
263
+ factors.push(divisor);
264
+ remaining.value /= divisor;
265
+ }
266
+ }
267
+ if (remaining.value > 1) factors.push(remaining.value);
268
+ return factors;
269
+ };
270
+
271
+ // EnumerableValue.computeOptimalMandA: the smallest modulus m >= max(size, 9) that isn't a
272
+ // multiple of 4 and has a repeated prime factor, and the multiplier 1 + the product of its
273
+ // distinct prime factors.
274
+ const getModulusAndMultiplier = (size: number) => {
275
+ for (let modulus = Math.max(size, 9); ; modulus++) {
276
+ const factors = getPrimeFactors(modulus);
277
+ const distinct = new Set(factors);
278
+ if (modulus % 4 !== 0 && distinct.size !== factors.length) {
279
+ return {
280
+ modulus: BigInt(modulus),
281
+ multiplier: BigInt([...distinct].reduce((product, prime) => product * prime, 1) + 1),
282
+ };
283
+ }
284
+ }
285
+ };
286
+
287
+ // EnumerableValue.SubsetEnumerator: count indices below size from a full-period linear
288
+ // congruential generator that starts at random.nextInt(size).
289
+ const getSubsetIndices = (count: number, size: number, random: JavaRandom) => {
290
+ if (size <= 0) return [];
291
+ const generator = getModulusAndMultiplier(size);
292
+ const state = { index: BigInt(random.nextInt(size)) };
293
+ const increment = BigInt(nextPrime(random));
294
+ return Array.from({ length: count }, () => {
295
+ do {
296
+ state.index = (generator.multiplier * state.index + increment) % generator.modulus;
297
+ } while (state.index >= size);
298
+ return state.index;
299
+ });
300
+ };
301
+
302
+ const LARGE_STRIDE = (1n << 63n) - 1n - 24n;
303
+
304
+ // SetOfFcnsOrRcdsValue.BigIntegerSubsetEnumerator, for products with more than 2^31 - 1
305
+ // elements.
306
+ const getLargeSubsetIndices = (count: number, size: bigint, random: JavaRandom) => {
307
+ const signed = random.nextLong();
308
+ const offset = BigInt.asIntN(64, signed < 0n ? -signed : signed);
309
+ return Array.from({ length: count }, (_, index) => {
310
+ const value = (LARGE_STRIDE * BigInt(index) + offset) % size;
311
+ return value < 0n ? value + size : value;
312
+ });
313
+ };
314
+
315
+ const getProductElements = (lists: Value[][], index: bigint) =>
316
+ lists.reduceRight<{ remaining: bigint; elements: Value[] }>(
317
+ (current, list) => ({
318
+ remaining: current.remaining / BigInt(list.length),
319
+ elements: [list[Number(current.remaining % BigInt(list.length))], ...current.elements],
320
+ }),
321
+ { remaining: index, elements: [] },
322
+ ).elements;
323
+
324
+ interface Product {
325
+ lists: Value[][];
326
+ create: (elements: Value[]) => Value;
327
+ }
328
+
329
+ const getProduct = (set: Value): Product | undefined => {
330
+ if (typeof set !== "object") return undefined;
331
+ if (set.kind === "FunctionSet") {
332
+ const domain = getFunctionDomain(set.domain);
333
+ const range = toFiniteSet(set.range).elements;
334
+ return {
335
+ lists: domain.map(() => range),
336
+ create: (elements) => createSortedFunction(domain, elements),
337
+ };
338
+ }
339
+ if (set.kind === "RecordSet") {
340
+ return {
341
+ lists: set.sets.map((field) => toFiniteSet(field).elements),
342
+ create: (elements) =>
343
+ createRecord(set.names.map((name, index): [string, Value] => [name, elements[index]])),
344
+ };
345
+ }
346
+ if (set.kind === "ProductSet") {
347
+ return {
348
+ lists: set.factors.map((factor) => toFiniteSet(factor).elements),
349
+ create: createTuple,
350
+ };
351
+ }
352
+ return undefined;
353
+ };
354
+
355
+ // Enumerable#getRandomSubset, which Randomization's RandomSubset calls.
356
+ export const getRandomSubset = (count: number, set: Value, random: JavaRandom): Value[] => {
357
+ if (isEmptySet(set)) return [];
358
+ if (typeof set === "object" && set.kind === "Interval") {
359
+ return getSubsetIndices(count, Math.max(0, set.high - set.low + 1), random).map(
360
+ (index) => set.low + Number(index),
361
+ );
362
+ }
363
+ const product = getProduct(set);
364
+ if (product) {
365
+ const size = product.lists.reduce((total, list) => total * BigInt(list.length), 1n);
366
+ const indices =
367
+ size > BigInt(MAXIMUM_INT)
368
+ ? getLargeSubsetIndices(count, size, random)
369
+ : getSubsetIndices(count, Number(size), random);
370
+ return indices.map((index) => product.create(getProductElements(product.lists, index)));
371
+ }
372
+ const elements = toFiniteSet(set).elements;
373
+ return getSubsetIndices(count, elements.length, random).map((index) => elements[Number(index)]);
374
+ };
375
+
376
+ const pickElement = (elements: Value[], random: JavaRandom) => {
377
+ const element = elements[Math.floor(random.nextDouble() * elements.length)];
378
+ if (element === undefined) throw new TlaEvaluationError("RandomElement of the empty set");
379
+ return element;
380
+ };
381
+
382
+ // TLC!RandomElement chooses each component of a set of functions, records, or tuples
383
+ // separately, and otherwise indexes the enumerated set with nextDouble.
384
+ export const getRandomElement = (set: Value, random: JavaRandom): Value => {
385
+ if (typeof set === "object" && set.kind === "FunctionSet") {
386
+ // TLC builds the chosen function without the interval its domain came from.
387
+ const domain = [...getFunctionDomain(set.domain)];
388
+ return createSortedFunction(
389
+ domain,
390
+ domain.map(() => getRandomElement(set.range, random)),
391
+ );
392
+ }
393
+ if (typeof set === "object" && set.kind === "RecordSet") {
394
+ return createRecord(
395
+ set.names.map((name, index): [string, Value] => [
396
+ name,
397
+ getRandomElement(set.sets[index], random),
398
+ ]),
399
+ );
400
+ }
401
+ if (typeof set === "object" && set.kind === "ProductSet") {
402
+ return createTuple(set.factors.map((factor) => getRandomElement(factor, random)));
403
+ }
404
+ if (typeof set === "object" && set.kind === "Interval") {
405
+ const size = Math.max(0, set.high - set.low + 1);
406
+ const offset = Math.floor(random.nextDouble() * size);
407
+ if (offset >= size) throw new TlaEvaluationError("RandomElement of the empty set");
408
+ return set.low + offset;
409
+ }
410
+ return pickElement(toFiniteSet(set).elements, random);
411
+ };
@@ -0,0 +1,109 @@
1
+ // TLC counts how many new states each expanded state yields in 32 buckets, the last of which
2
+ // holds every count from 31 up.
3
+ export const OUT_DEGREE_BUCKETS = 32;
4
+
5
+ // TLC's optimistic estimate of a fingerprint collision, from AbstractChecker.
6
+ export const getOptimisticProbability = (distinctStates: number, generatedStates: number) =>
7
+ distinctStates * ((generatedStates - distinctStates) / 2 ** 64);
8
+
9
+ // TLC computes the estimate from the actual fingerprints only when the optimistic one is this high.
10
+ const ACTUAL_PROBABILITY_THRESHOLD = 1e-10;
11
+
12
+ const withDecimalPoint = (text: string) => (text.includes(".") ? text : `${text}.0`);
13
+
14
+ // Java's Double.toString, which prints a number below 1E-3 or from 1E7 up in scientific notation.
15
+ const toJavaString = (value: number) => {
16
+ const magnitude = Math.abs(value);
17
+ if (magnitude >= 1e-3 && magnitude < 1e7) return withDecimalPoint(String(value));
18
+ const [mantissa, exponent] = value.toExponential().split("e");
19
+ return `${withDecimalPoint(mantissa)}E${exponent.replace("+", "")}`;
20
+ };
21
+
22
+ const isDigit = (character: string | undefined) =>
23
+ character !== undefined && character >= "0" && character <= "9";
24
+
25
+ // TLC's ProbabilityToString, which keeps two significant digits and drops a leading zero.
26
+ export const formatProbability = (value: number) => {
27
+ if (value === 0) return "0.0";
28
+ const text = toJavaString(value);
29
+ const cursor = { next: 0, digits: 0, result: "" };
30
+ while (text[cursor.next] === "0") cursor.next++;
31
+ while (isDigit(text[cursor.next])) {
32
+ cursor.result += text[cursor.next++];
33
+ cursor.digits++;
34
+ }
35
+ if (cursor.next === text.length) return cursor.result;
36
+ if (text[cursor.next] !== ".") return text;
37
+ cursor.next++;
38
+ if (cursor.digits >= 2) {
39
+ while (isDigit(text[cursor.next])) cursor.next++;
40
+ } else {
41
+ cursor.result += ".";
42
+ if (cursor.digits === 0) {
43
+ while (text[cursor.next] === "0") cursor.result += text[cursor.next++];
44
+ }
45
+ while (isDigit(text[cursor.next]) && cursor.digits < 2) {
46
+ cursor.result += text[cursor.next++];
47
+ cursor.digits++;
48
+ }
49
+ if (isDigit(text[cursor.next]) && Number(text[cursor.next]) >= 5) {
50
+ cursor.result = roundUp(cursor.result);
51
+ }
52
+ while (isDigit(text[cursor.next])) cursor.next++;
53
+ }
54
+ if (cursor.next >= text.length) return cursor.result;
55
+ if (text[cursor.next] === "E") return `${cursor.result}${text.slice(cursor.next)}`;
56
+ return text;
57
+ };
58
+
59
+ // Adds one to the last digit and carries it over nines and the decimal point.
60
+ const roundUp = (digits: string): string => {
61
+ const position = digits.length - 1;
62
+ if (position < 0) return "1";
63
+ const last = digits[position];
64
+ const front = digits.slice(0, position);
65
+ if (last === ".") return `${roundUp(front)}.`;
66
+ if (last === "9") return `${roundUp(front)}0`;
67
+ return `${front}${Number(last) + 1}`;
68
+ };
69
+
70
+ // 1 divided by the distance, rounded half up to two significant digits like Java's BigDecimal.
71
+ export const getActualProbability = (distance: bigint) => {
72
+ const exponent = { value: 0 };
73
+ while (10n ** BigInt(exponent.value + 1) < distance * 10n) exponent.value++;
74
+ const scale = 10n ** BigInt(exponent.value + 1);
75
+ const quotient = scale / distance;
76
+ const isRoundedUp = (scale % distance) * 2n >= distance;
77
+ return Number(`${quotient + (isRoundedUp ? 1n : 0n)}e-${exponent.value + 1}`);
78
+ };
79
+
80
+ // TLC splits its fingerprints into two sets by their top bit and reports the smallest distance
81
+ // between two fingerprints in the same set. Sorted as signed integers, each set is contiguous.
82
+ export const getFingerprintDistance = (
83
+ distinctStates: number,
84
+ generatedStates: number,
85
+ getFingerprints: () => BigInt64Array,
86
+ ) => {
87
+ const probability = getOptimisticProbability(distinctStates, generatedStates);
88
+ if (probability < ACTUAL_PROBABILITY_THRESHOLD) return undefined;
89
+ const fingerprints = getFingerprints().sort();
90
+ return fingerprints.reduce((distance, fingerprint, index) => {
91
+ const previous = fingerprints[index - 1];
92
+ const isSameSet = index > 0 && previous < 0n === fingerprint < 0n;
93
+ return isSameSet && fingerprint - previous < distance ? fingerprint - previous : distance;
94
+ }, 0x7fff_ffff_ffff_ffffn);
95
+ };
96
+
97
+ export const getOutDegreeSummary = (buckets: number[]) => {
98
+ const observations = buckets.reduce((sum, count) => sum + count, 0);
99
+ if (observations === 0) return undefined;
100
+ const degrees = buckets.flatMap((count, degree) => (count > 0 ? [degree] : []));
101
+ const total = buckets.reduce((sum, count, degree) => sum + count * degree, 0);
102
+ const position = Math.floor(observations * 0.95);
103
+ const cumulative = { count: 0 };
104
+ const percentile = degrees.find((degree) => {
105
+ cumulative.count += buckets[degree];
106
+ return cumulative.count > position;
107
+ });
108
+ return `The average outdegree of the complete state graph is ${Math.round(total / observations)} (minimum is ${degrees[0]}, the maximum ${degrees[degrees.length - 1]} and the 95th percentile is ${percentile}).`;
109
+ };
package/src/transfer.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { createModelValue, createSortedFunction, createSortedSet, type Value } from "./values";
1
+ import {
2
+ createModelValue,
3
+ createSortedFunction,
4
+ createSortedSet,
5
+ getDomainInterval,
6
+ getFunctionDomain,
7
+ type Value,
8
+ } from "./values";
2
9
 
3
10
  const FALSE = 0;
4
11
  const TRUE = 1;
@@ -7,6 +14,7 @@ const STRING = 3;
7
14
  const MODEL_VALUE = 4;
8
15
  const SET = 5;
9
16
  const FUNCTION = 6;
17
+ const INTERVAL_FUNCTION = 7;
10
18
 
11
19
  export interface EncodedStates {
12
20
  words: Int32Array;
@@ -14,7 +22,7 @@ export interface EncodedStates {
14
22
  count: number;
15
23
  }
16
24
 
17
- export interface StateEncoder {
25
+ interface StateEncoder {
18
26
  encode: (state: Value[]) => boolean;
19
27
  finish: () => EncodedStates;
20
28
  }
@@ -65,6 +73,13 @@ export const createStateEncoder = (): StateEncoder => {
65
73
  return value.elements.every(encodeValue);
66
74
  }
67
75
  if (value.kind === "Function") {
76
+ const interval = getDomainInterval(value.domain);
77
+ if (interval) {
78
+ push(INTERVAL_FUNCTION);
79
+ push(interval.low);
80
+ push(value.range.length);
81
+ return value.range.every(encodeValue);
82
+ }
68
83
  push(FUNCTION);
69
84
  push(value.domain.length);
70
85
  return value.domain.every(encodeValue) && value.range.every(encodeValue);
@@ -89,8 +104,9 @@ export const createStateEncoder = (): StateEncoder => {
89
104
  };
90
105
  };
91
106
 
92
- export const createStateDecoder = (width: number) => {
93
- return (encoded: EncodedStates): Value[][] => {
107
+ export const createStateDecoder =
108
+ (width: number) =>
109
+ (encoded: EncodedStates): Value[][] => {
94
110
  const words = encoded.words;
95
111
  const cursor = { index: 0 };
96
112
  const decodeElements = (length: number) => Array.from({ length }, () => decodeValue());
@@ -101,6 +117,12 @@ export const createStateDecoder = (width: number) => {
101
117
  if (tag === INTEGER) return words[cursor.index++];
102
118
  if (tag === STRING) return encoded.strings[words[cursor.index++]];
103
119
  if (tag === MODEL_VALUE) return createModelValue(encoded.strings[words[cursor.index++]]);
120
+ if (tag === INTERVAL_FUNCTION) {
121
+ const low = words[cursor.index++];
122
+ const length = words[cursor.index++];
123
+ const domain = getFunctionDomain({ kind: "Interval", low, high: low + length - 1 });
124
+ return createSortedFunction(domain, decodeElements(length));
125
+ }
104
126
  const length = words[cursor.index++];
105
127
  if (tag === SET) return createSortedSet(decodeElements(length));
106
128
  const domain = decodeElements(length);
@@ -110,4 +132,3 @@ export const createStateDecoder = (width: number) => {
110
132
  Array.from({ length: width }, () => decodeValue()),
111
133
  );
112
134
  };
113
- };