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.
- package/README.md +81 -42
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +215 -30
- package/dist/index.d.ts +215 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +20 -1
- package/dist/run-cli.js +20 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +6 -2
- package/src/ast.ts +122 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +945 -238
- package/src/cli-options.ts +58 -0
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +59 -1
- package/src/evaluate.ts +489 -137
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +2 -2
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +103 -28
- package/src/liveness.ts +47 -91
- package/src/obligation-action.ts +384 -0
- package/src/obligation-anon.ts +575 -0
- package/src/obligation-deref.ts +637 -0
- package/src/obligation-expression.ts +1060 -0
- package/src/obligation-format.ts +406 -0
- package/src/obligation-generate.ts +1074 -0
- package/src/obligation-levels.ts +329 -0
- package/src/obligation-module.ts +1042 -0
- package/src/obligation-printer.ts +889 -0
- package/src/obligation-proof.ts +76 -0
- package/src/obligation-source.ts +1084 -0
- package/src/obligation-tuples.ts +184 -0
- package/src/obligations.ts +316 -0
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +958 -205
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +427 -61
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +65 -21
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +26 -5
- 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 {
|
|
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
|
-
|
|
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 =
|
|
93
|
-
|
|
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
|
-
};
|