@fairfox/polly 0.89.1 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gallery/gallery.js +1 -1
- package/dist/src/index.js +25 -13
- package/dist/src/index.js.map +3 -3
- package/dist/src/shared/lib/resource.js +25 -13
- package/dist/src/shared/lib/resource.js.map +3 -3
- package/dist/src/shared/lib/state.js +25 -13
- package/dist/src/shared/lib/state.js.map +3 -3
- package/dist/src/shared/state/app-state.js +25 -13
- package/dist/src/shared/state/app-state.js.map +3 -3
- package/dist/tools/verify/src/cli.js +623 -92
- package/dist/tools/verify/src/cli.js.map +12 -10
- package/dist/tools/verify/src/config.d.ts +63 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/visualize/src/cli.js +85 -13
- package/dist/tools/visualize/src/cli.js.map +5 -4
- package/package.json +5 -4
|
@@ -245,6 +245,80 @@ var init_expression_validator = __esm(() => {
|
|
|
245
245
|
WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
// tools/verify/src/codegen/model-constants.ts
|
|
249
|
+
function sanitizeContextName(name) {
|
|
250
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
251
|
+
}
|
|
252
|
+
function hasDeclaredContexts(config) {
|
|
253
|
+
const declared = config?.contexts;
|
|
254
|
+
return Array.isArray(declared) && declared.length > 0;
|
|
255
|
+
}
|
|
256
|
+
function resolveContexts(config) {
|
|
257
|
+
if (!hasDeclaredContexts(config))
|
|
258
|
+
return [...DEFAULT_CONTEXTS];
|
|
259
|
+
return (config?.contexts).map(sanitizeContextName);
|
|
260
|
+
}
|
|
261
|
+
function targetSetCount(contexts) {
|
|
262
|
+
return 2 ** contexts.length - 1;
|
|
263
|
+
}
|
|
264
|
+
function hasProjectConstant(messages) {
|
|
265
|
+
return messages.maxWorkers !== undefined || messages.maxRenderers !== undefined || messages.maxContexts !== undefined || messages.maxClients !== undefined;
|
|
266
|
+
}
|
|
267
|
+
function generatedTabValues(messages) {
|
|
268
|
+
if (messages.tabSymmetry) {
|
|
269
|
+
const maxTabs = messages.maxTabs ?? 1;
|
|
270
|
+
return Array.from({ length: maxTabs + 1 }, (_, i) => `Tab${i}`);
|
|
271
|
+
}
|
|
272
|
+
if (messages.maxTabs !== undefined && messages.maxTabs !== null) {
|
|
273
|
+
return Array.from({ length: messages.maxTabs + 1 }, (_, i) => String(i));
|
|
274
|
+
}
|
|
275
|
+
return hasProjectConstant(messages) ? ["0"] : ["0", "1"];
|
|
276
|
+
}
|
|
277
|
+
function generatedTabCount(messages) {
|
|
278
|
+
return generatedTabValues(messages).length;
|
|
279
|
+
}
|
|
280
|
+
function sendBranchingFactor(handlerCount, tabCount, contexts) {
|
|
281
|
+
return contexts.length * targetSetCount(contexts) * tabCount * handlerCount;
|
|
282
|
+
}
|
|
283
|
+
var DEFAULT_CONTEXTS, CONTEXT_NAME_PATTERN;
|
|
284
|
+
var init_model_constants = __esm(() => {
|
|
285
|
+
DEFAULT_CONTEXTS = ["background", "content", "popup"];
|
|
286
|
+
CONTEXT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// tools/verify/src/runner/memory.ts
|
|
290
|
+
function parseMemoryBytes(spec) {
|
|
291
|
+
const match = /^(\d+)\s*([bkmg])?$/i.exec(spec.trim());
|
|
292
|
+
if (!match?.[1])
|
|
293
|
+
return null;
|
|
294
|
+
const value = Number.parseInt(match[1], 10);
|
|
295
|
+
if (value <= 0)
|
|
296
|
+
return null;
|
|
297
|
+
const unit = (match[2] ?? "b").toLowerCase();
|
|
298
|
+
const multiplier = UNIT_BYTES[unit];
|
|
299
|
+
if (multiplier === undefined)
|
|
300
|
+
return null;
|
|
301
|
+
return value * multiplier;
|
|
302
|
+
}
|
|
303
|
+
function jvmHeapArg(memoryBytes) {
|
|
304
|
+
const megabytes = Math.max(64, Math.floor(memoryBytes * JVM_HEAP_FRACTION / MEGABYTE));
|
|
305
|
+
return `${megabytes}m`;
|
|
306
|
+
}
|
|
307
|
+
function javaToolOptions(memoryBytes) {
|
|
308
|
+
return `-Xmx${jvmHeapArg(memoryBytes)} -XX:+UseParallelGC`;
|
|
309
|
+
}
|
|
310
|
+
var DEFAULT_TLC_MEMORY = "4g", MIN_TLC_MEMORY = "512m", JVM_HEAP_FRACTION = 0.75, MEGABYTE, UNIT_BYTES, MIN_TLC_MEMORY_BYTES;
|
|
311
|
+
var init_memory = __esm(() => {
|
|
312
|
+
MEGABYTE = 1024 ** 2;
|
|
313
|
+
UNIT_BYTES = {
|
|
314
|
+
b: 1,
|
|
315
|
+
k: 1024,
|
|
316
|
+
m: MEGABYTE,
|
|
317
|
+
g: 1024 ** 3
|
|
318
|
+
};
|
|
319
|
+
MIN_TLC_MEMORY_BYTES = parseMemoryBytes(MIN_TLC_MEMORY) ?? 512 * 1024 ** 2;
|
|
320
|
+
});
|
|
321
|
+
|
|
248
322
|
// tools/verify/src/analysis/model-coverage.ts
|
|
249
323
|
function norm(field) {
|
|
250
324
|
return field.replace(/_/g, ".");
|
|
@@ -751,6 +825,7 @@ async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis)
|
|
|
751
825
|
}
|
|
752
826
|
var TLAValidationError, TLAGenerator;
|
|
753
827
|
var init_tla = __esm(() => {
|
|
828
|
+
init_model_constants();
|
|
754
829
|
init_temporal();
|
|
755
830
|
TLAValidationError = class TLAValidationError extends Error {
|
|
756
831
|
report;
|
|
@@ -770,7 +845,6 @@ var init_tla = __esm(() => {
|
|
|
770
845
|
symmetrySets = [];
|
|
771
846
|
resolvedActionNames = new Map;
|
|
772
847
|
paramDomains = new Map;
|
|
773
|
-
tabSymmetryEnabled = false;
|
|
774
848
|
tabCount = 0;
|
|
775
849
|
moduleName = "UserApp";
|
|
776
850
|
currentConfig;
|
|
@@ -981,7 +1055,7 @@ var init_tla = __esm(() => {
|
|
|
981
1055
|
lines.push("CONSTANTS");
|
|
982
1056
|
}
|
|
983
1057
|
addBasicConstants(lines, config) {
|
|
984
|
-
lines.push(
|
|
1058
|
+
lines.push(` Contexts = {${resolveContexts(config).join(", ")}}`);
|
|
985
1059
|
lines.push(` MaxMessages = ${config.messages.maxInFlight || 3}`);
|
|
986
1060
|
lines.push(" NULL = NULL");
|
|
987
1061
|
if (config.messages.perMessageBounds) {
|
|
@@ -1002,27 +1076,18 @@ var init_tla = __esm(() => {
|
|
|
1002
1076
|
hasProjectConstant = true;
|
|
1003
1077
|
}
|
|
1004
1078
|
if ("maxContexts" in messages && messages.maxContexts !== undefined) {
|
|
1005
|
-
lines.push(` MaxContexts = ${messages.maxContexts}`);
|
|
1006
1079
|
hasProjectConstant = true;
|
|
1007
1080
|
}
|
|
1008
1081
|
if ("maxClients" in messages && messages.maxClients !== undefined && !hasProjectConstant) {
|
|
1009
1082
|
lines.push(` MaxClients = ${messages.maxClients}`);
|
|
1010
|
-
hasProjectConstant = true;
|
|
1011
1083
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
lines.push(` Tabs = {${tabValues}}`);
|
|
1018
|
-
} else if ("maxTabs" in messages && messages.maxTabs !== undefined && messages.maxTabs !== null) {
|
|
1019
|
-
const tabValues = Array.from({ length: messages.maxTabs + 1 }, (_, i) => i).join(", ");
|
|
1020
|
-
lines.push(` Tabs = {${tabValues}}`);
|
|
1021
|
-
} else if (hasProjectConstant) {
|
|
1022
|
-
lines.push(" Tabs = {0}");
|
|
1023
|
-
} else {
|
|
1024
|
-
lines.push(" Tabs = {0, 1}");
|
|
1084
|
+
const tabValues = generatedTabValues(messages);
|
|
1085
|
+
if (messages.tabSymmetry) {
|
|
1086
|
+
for (const tab of tabValues) {
|
|
1087
|
+
lines.push(` ${tab} = ${tab}`);
|
|
1088
|
+
}
|
|
1025
1089
|
}
|
|
1090
|
+
lines.push(` Tabs = {${tabValues.join(", ")}}`);
|
|
1026
1091
|
lines.push(" TimeoutLimit = 3");
|
|
1027
1092
|
}
|
|
1028
1093
|
addStateBoundsConstants(lines, state) {
|
|
@@ -1442,7 +1507,6 @@ var init_tla = __esm(() => {
|
|
|
1442
1507
|
}
|
|
1443
1508
|
const maxTabs = config.messages.maxTabs ?? 1;
|
|
1444
1509
|
this.tabCount = maxTabs + 1;
|
|
1445
|
-
this.tabSymmetryEnabled = true;
|
|
1446
1510
|
this.line("\\* Tab symmetry constants for state space reduction (Tier 1 optimization)");
|
|
1447
1511
|
this.line("CONSTANTS");
|
|
1448
1512
|
this.indent++;
|
|
@@ -3562,11 +3626,20 @@ class DockerRunner {
|
|
|
3562
3626
|
}
|
|
3563
3627
|
const containerName = mintContainerName("tlc");
|
|
3564
3628
|
sweepOrphanedContainersOnce();
|
|
3629
|
+
const memory = options?.memory ?? DEFAULT_TLC_MEMORY;
|
|
3630
|
+
const memoryBytes = parseMemoryBytes(memory);
|
|
3631
|
+
if (memoryBytes === null) {
|
|
3632
|
+
throw new Error(`Invalid verification.memory: "${memory}". Use a docker-style size such as "512m" or "8g".`);
|
|
3633
|
+
}
|
|
3565
3634
|
const args = [
|
|
3566
3635
|
"run",
|
|
3567
3636
|
"--rm",
|
|
3568
3637
|
"--name",
|
|
3569
3638
|
containerName,
|
|
3639
|
+
"--memory",
|
|
3640
|
+
memory,
|
|
3641
|
+
"-e",
|
|
3642
|
+
`JAVA_TOOL_OPTIONS=${javaToolOptions(memoryBytes)}`,
|
|
3570
3643
|
"-v",
|
|
3571
3644
|
`${specDir}:/work`,
|
|
3572
3645
|
this.IMAGE_NAME,
|
|
@@ -3585,9 +3658,9 @@ class DockerRunner {
|
|
|
3585
3658
|
timeout: options?.timeout,
|
|
3586
3659
|
containerName
|
|
3587
3660
|
});
|
|
3588
|
-
return this.parseTLCOutput(result);
|
|
3661
|
+
return this.parseTLCOutput(result, memory);
|
|
3589
3662
|
}
|
|
3590
|
-
parseTLCOutput(result) {
|
|
3663
|
+
parseTLCOutput(result, memory) {
|
|
3591
3664
|
const output = result.stdout + result.stderr;
|
|
3592
3665
|
const violationMatch = output.match(/Error: Invariant (.*?) is violated/);
|
|
3593
3666
|
if (violationMatch) {
|
|
@@ -3604,18 +3677,13 @@ class DockerRunner {
|
|
|
3604
3677
|
if (result.exitCode !== 0 || output.includes("Error:")) {
|
|
3605
3678
|
return {
|
|
3606
3679
|
success: false,
|
|
3607
|
-
error: this.extractError(output),
|
|
3680
|
+
error: this.extractError(output, result.exitCode, memory),
|
|
3608
3681
|
output
|
|
3609
3682
|
};
|
|
3610
3683
|
}
|
|
3611
|
-
const statesMatch = output.match(/(\d+) states generated/);
|
|
3612
|
-
const distinctMatch = output.match(/(\d+) distinct states/);
|
|
3613
3684
|
return {
|
|
3614
3685
|
success: true,
|
|
3615
|
-
|
|
3616
|
-
statesGenerated: statesMatch?.[1] ? Number.parseInt(statesMatch[1], 10) : 0,
|
|
3617
|
-
distinctStates: distinctMatch?.[1] ? Number.parseInt(distinctMatch[1], 10) : 0
|
|
3618
|
-
},
|
|
3686
|
+
...parseTLCStats(output) ?? {},
|
|
3619
3687
|
output
|
|
3620
3688
|
};
|
|
3621
3689
|
}
|
|
@@ -3637,7 +3705,14 @@ class DockerRunner {
|
|
|
3637
3705
|
}
|
|
3638
3706
|
return trace;
|
|
3639
3707
|
}
|
|
3640
|
-
extractError(output) {
|
|
3708
|
+
extractError(output, exitCode, memory) {
|
|
3709
|
+
const cap = memory ? ` (--memory ${memory})` : "";
|
|
3710
|
+
if (/java\.lang\.OutOfMemoryError/.test(output)) {
|
|
3711
|
+
return `TLC ran out of heap${cap}. The model is too large for this ceiling — raise verification.memory, lower messages.maxInFlight, or split the subsystem.`;
|
|
3712
|
+
}
|
|
3713
|
+
if (exitCode === 137) {
|
|
3714
|
+
return `TLC was killed for exceeding the container memory limit${cap}. Raise verification.memory, lower messages.maxInFlight, or split the subsystem.`;
|
|
3715
|
+
}
|
|
3641
3716
|
if (/StatePool\w*\.run/.test(output) && /states\/.*No such file or directory/.test(output)) {
|
|
3642
3717
|
return "TLC state-pool IO failure (states/.../<n>: No such file or directory). This is a Docker filesystem flake, not a spec error — re-run the verification. See issue #152.";
|
|
3643
3718
|
}
|
|
@@ -3734,8 +3809,36 @@ class DockerRunner {
|
|
|
3734
3809
|
});
|
|
3735
3810
|
}
|
|
3736
3811
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3812
|
+
function parseTLCStats(output) {
|
|
3813
|
+
const summary = TLC_SUMMARY.exec(output);
|
|
3814
|
+
if (!summary?.[1] || !summary[2] || !summary[3])
|
|
3815
|
+
return;
|
|
3816
|
+
const statesGenerated = Number.parseInt(summary[1], 10);
|
|
3817
|
+
const distinctStates = Number.parseInt(summary[2], 10);
|
|
3818
|
+
const statesLeftOnQueue = Number.parseInt(summary[3], 10);
|
|
3819
|
+
const initial = TLC_INITIAL_STATES.exec(output)?.[1];
|
|
3820
|
+
const initialStates = initial === undefined ? undefined : Number.parseInt(initial, 10);
|
|
3821
|
+
const depth = TLC_SEARCH_DEPTH.exec(output)?.[1];
|
|
3822
|
+
const searchDepth = depth === undefined ? undefined : Number.parseInt(depth, 10);
|
|
3823
|
+
return {
|
|
3824
|
+
stats: {
|
|
3825
|
+
statesGenerated,
|
|
3826
|
+
distinctStates,
|
|
3827
|
+
statesLeftOnQueue,
|
|
3828
|
+
...initialStates === undefined ? {} : { initialStates },
|
|
3829
|
+
...searchDepth === undefined ? {} : { searchDepth },
|
|
3830
|
+
exhaustive: statesLeftOnQueue === 0,
|
|
3831
|
+
noActionEnabled: initialStates !== undefined && distinctStates === initialStates
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src/runner", swept = false, TLC_SUMMARY, TLC_INITIAL_STATES, TLC_SEARCH_DEPTH;
|
|
3836
|
+
var init_docker = __esm(() => {
|
|
3837
|
+
init_memory();
|
|
3838
|
+
TLC_SUMMARY = /^(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue/m;
|
|
3839
|
+
TLC_INITIAL_STATES = /Finished computing initial states: (\d+) distinct states generated/;
|
|
3840
|
+
TLC_SEARCH_DEPTH = /The depth of the complete state graph search is (\d+)/;
|
|
3841
|
+
});
|
|
3739
3842
|
|
|
3740
3843
|
// tools/verify/src/cli.ts
|
|
3741
3844
|
init_expression_validator();
|
|
@@ -3778,6 +3881,9 @@ function computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs) {
|
|
|
3778
3881
|
return findings;
|
|
3779
3882
|
}
|
|
3780
3883
|
|
|
3884
|
+
// tools/verify/src/analysis/state-space-estimator.ts
|
|
3885
|
+
init_model_constants();
|
|
3886
|
+
|
|
3781
3887
|
// tools/verify/src/config/types.ts
|
|
3782
3888
|
function isAdapterConfig(config) {
|
|
3783
3889
|
return "adapter" in config;
|
|
@@ -3790,17 +3896,20 @@ function isLegacyConfig(config) {
|
|
|
3790
3896
|
function typedFieldCardinality(name, obj) {
|
|
3791
3897
|
if (!("type" in obj))
|
|
3792
3898
|
return null;
|
|
3793
|
-
|
|
3899
|
+
const values = obj["values"];
|
|
3900
|
+
const min = obj["min"];
|
|
3901
|
+
const max = obj["max"];
|
|
3902
|
+
switch (obj["type"]) {
|
|
3794
3903
|
case "boolean":
|
|
3795
3904
|
return { name, cardinality: 2, kind: "boolean" };
|
|
3796
3905
|
case "enum":
|
|
3797
|
-
if (Array.isArray(
|
|
3798
|
-
return { name, cardinality:
|
|
3906
|
+
if (Array.isArray(values)) {
|
|
3907
|
+
return { name, cardinality: values.length, kind: "enum" };
|
|
3799
3908
|
}
|
|
3800
3909
|
return { name, cardinality: "unbounded", kind: "enum" };
|
|
3801
3910
|
case "number":
|
|
3802
|
-
if (typeof
|
|
3803
|
-
return { name, cardinality:
|
|
3911
|
+
if (typeof min === "number" && typeof max === "number") {
|
|
3912
|
+
return { name, cardinality: max - min + 1, kind: "number" };
|
|
3804
3913
|
}
|
|
3805
3914
|
return { name, cardinality: "unbounded", kind: "number" };
|
|
3806
3915
|
case "array":
|
|
@@ -3812,20 +3921,23 @@ function typedFieldCardinality(name, obj) {
|
|
|
3812
3921
|
}
|
|
3813
3922
|
}
|
|
3814
3923
|
function legacyFieldCardinality(name, obj) {
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3924
|
+
const values = obj["values"];
|
|
3925
|
+
const abstract = obj["abstract"];
|
|
3926
|
+
const min = obj["min"];
|
|
3927
|
+
const max = obj["max"];
|
|
3928
|
+
if (Array.isArray(values)) {
|
|
3929
|
+
const extra = abstract === true ? 1 : 0;
|
|
3818
3930
|
return {
|
|
3819
3931
|
name,
|
|
3820
|
-
cardinality:
|
|
3821
|
-
kind:
|
|
3932
|
+
cardinality: values.length + extra,
|
|
3933
|
+
kind: abstract === true ? "enum (abstract)" : "enum (values)"
|
|
3822
3934
|
};
|
|
3823
3935
|
}
|
|
3824
3936
|
if ("maxLength" in obj && !("type" in obj)) {
|
|
3825
3937
|
return { name, cardinality: "unbounded", kind: "array" };
|
|
3826
3938
|
}
|
|
3827
|
-
if (
|
|
3828
|
-
return { name, cardinality:
|
|
3939
|
+
if (typeof min === "number" && typeof max === "number" && !("type" in obj)) {
|
|
3940
|
+
return { name, cardinality: max - min + 1, kind: "number" };
|
|
3829
3941
|
}
|
|
3830
3942
|
return null;
|
|
3831
3943
|
}
|
|
@@ -3839,15 +3951,6 @@ function fieldCardinality(name, value) {
|
|
|
3839
3951
|
const obj = value;
|
|
3840
3952
|
return typedFieldCardinality(name, obj) ?? legacyFieldCardinality(name, obj) ?? { name, cardinality: "unbounded", kind: "unknown" };
|
|
3841
3953
|
}
|
|
3842
|
-
function permutations(n, k) {
|
|
3843
|
-
if (k > n)
|
|
3844
|
-
return 1;
|
|
3845
|
-
let result = 1;
|
|
3846
|
-
for (let i = 0;i < k; i++) {
|
|
3847
|
-
result *= n - i;
|
|
3848
|
-
}
|
|
3849
|
-
return result;
|
|
3850
|
-
}
|
|
3851
3954
|
function getHandlerCount(config, analysis) {
|
|
3852
3955
|
if (isLegacyConfig(config)) {
|
|
3853
3956
|
const msgs = config.messages;
|
|
@@ -3869,11 +3972,14 @@ function getMaxInFlight(config) {
|
|
|
3869
3972
|
}
|
|
3870
3973
|
return 1;
|
|
3871
3974
|
}
|
|
3872
|
-
function
|
|
3975
|
+
function getTabSetInput(config) {
|
|
3873
3976
|
if (isLegacyConfig(config)) {
|
|
3874
|
-
return config.messages
|
|
3977
|
+
return config.messages;
|
|
3875
3978
|
}
|
|
3876
|
-
return
|
|
3979
|
+
return {};
|
|
3980
|
+
}
|
|
3981
|
+
function getContextSetInput(config) {
|
|
3982
|
+
return config;
|
|
3877
3983
|
}
|
|
3878
3984
|
function getFeasibility(states) {
|
|
3879
3985
|
if (states < 1e5)
|
|
@@ -3896,12 +4002,35 @@ function feasibilityLabel(f) {
|
|
|
3896
4002
|
return "infeasible (likely won't terminate)";
|
|
3897
4003
|
}
|
|
3898
4004
|
}
|
|
3899
|
-
|
|
4005
|
+
var OMITTED_VARIABLES = "ports, status, deliveredTo, time and payload";
|
|
4006
|
+
function dominantTerm(e) {
|
|
4007
|
+
const log10 = (n) => n > 0 ? Math.log10(n) : 0;
|
|
4008
|
+
const topology = e.contextCount * e.targetSets * e.tabCount;
|
|
4009
|
+
const terms = [
|
|
4010
|
+
{
|
|
4011
|
+
label: `state fields (${e.fieldProduct} combinations across ${e.contextCount} contexts)`,
|
|
4012
|
+
decades: e.contextCount * log10(e.fieldProduct)
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
label: `handler count (${e.handlerCount} handlers at maxInFlight ${e.maxInFlight})`,
|
|
4016
|
+
decades: e.maxInFlight * log10(e.handlerCount)
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
label: `send topology (${e.contextCount} sources x ${e.targetSets} target sets x ${e.tabCount} tabs)`,
|
|
4020
|
+
decades: e.maxInFlight * log10(topology)
|
|
4021
|
+
}
|
|
4022
|
+
];
|
|
4023
|
+
return terms.reduce((a, b) => b.decades > a.decades ? b : a);
|
|
4024
|
+
}
|
|
4025
|
+
function estimateForScope(config, scope) {
|
|
3900
4026
|
const state = config.state;
|
|
3901
4027
|
const fields = [];
|
|
3902
4028
|
const warnings = [];
|
|
3903
4029
|
const suggestions = [];
|
|
4030
|
+
const included = scope.stateFields ? new Set(scope.stateFields) : undefined;
|
|
3904
4031
|
for (const [name, value] of Object.entries(state)) {
|
|
4032
|
+
if (included && !included.has(name))
|
|
4033
|
+
continue;
|
|
3905
4034
|
fields.push(fieldCardinality(name, value));
|
|
3906
4035
|
}
|
|
3907
4036
|
const unboundedFields = fields.filter((f) => f.cardinality === "unbounded");
|
|
@@ -3911,33 +4040,56 @@ function estimateStateSpace(config, analysis) {
|
|
|
3911
4040
|
warnings.push(`${unboundedFields.length} field(s) have unbounded domains: ${unboundedFields.map((f) => f.name).join(", ")}`);
|
|
3912
4041
|
suggestions.push(`Fields ${unboundedFields.map((f) => f.name).join(", ")} have unbounded domains — their actual impact depends on handler logic`);
|
|
3913
4042
|
}
|
|
3914
|
-
const handlerCount =
|
|
3915
|
-
const
|
|
3916
|
-
const
|
|
3917
|
-
const
|
|
4043
|
+
const { handlerCount, maxInFlight } = scope;
|
|
4044
|
+
const tabSetInput = getTabSetInput(config);
|
|
4045
|
+
const tabCount = generatedTabCount(tabSetInput);
|
|
4046
|
+
const contextSetInput = getContextSetInput(config);
|
|
4047
|
+
const contexts = resolveContexts(contextSetInput);
|
|
4048
|
+
const contextsDeclared = hasDeclaredContexts(contextSetInput);
|
|
4049
|
+
const contextCount = contexts.length;
|
|
3918
4050
|
const totalStateSpace = fieldProduct ** contextCount;
|
|
3919
|
-
const
|
|
4051
|
+
const sendBranching = sendBranchingFactor(handlerCount, tabCount, contexts);
|
|
4052
|
+
const interleavingFactor = sendBranching ** maxInFlight;
|
|
3920
4053
|
const estimatedStates = totalStateSpace * interleavingFactor;
|
|
3921
4054
|
const feasibility = getFeasibility(estimatedStates);
|
|
4055
|
+
warnings.push(`Lower bound: the generated spec also carries ${OMITTED_VARIABLES}, none of which this figure models.`);
|
|
4056
|
+
if (tabSetInput.tabSymmetry) {
|
|
4057
|
+
warnings.push("tabSymmetry is enabled — TLC's symmetry reduction cuts the reachable set, so this is an upper bound on the tab dimension.");
|
|
4058
|
+
}
|
|
4059
|
+
const dominant = dominantTerm({
|
|
4060
|
+
fieldProduct,
|
|
4061
|
+
handlerCount,
|
|
4062
|
+
contextCount,
|
|
4063
|
+
targetSets: targetSetCount(contexts),
|
|
4064
|
+
tabCount,
|
|
4065
|
+
maxInFlight
|
|
4066
|
+
});
|
|
4067
|
+
suggestions.push(`Dominant term: ${dominant.label} — ~10^${dominant.decades.toFixed(1)} of ~10^${Math.log10(Math.max(estimatedStates, 1)).toFixed(1)}`);
|
|
4068
|
+
if (maxInFlight > 1) {
|
|
4069
|
+
suggestions.push(`maxInFlight ${maxInFlight} → ${maxInFlight - 1} divides the estimate by ${sendBranching.toLocaleString()}x (one fewer send to branch over)`);
|
|
4070
|
+
}
|
|
4071
|
+
if (!contextsDeclared) {
|
|
4072
|
+
suggestions.push(`Contexts is the default {${contexts.join(", ")}} — no \`contexts\` key is declared. ` + `Declaring the ones this project has divides state replication by fieldProduct^(${contextCount} - n) ` + "and send branching by the drop in |Contexts| * (2^|Contexts| - 1) (polly#185)");
|
|
4073
|
+
}
|
|
3922
4074
|
if (handlerCount > 15) {
|
|
3923
|
-
suggestions.push(
|
|
4075
|
+
suggestions.push(`${handlerCount} handlers enter the estimate as ${handlerCount}^${maxInFlight} — splitting into subsystems is the cheapest cut`);
|
|
3924
4076
|
}
|
|
3925
4077
|
for (const f of boundedFields) {
|
|
3926
4078
|
if (f.cardinality > 50) {
|
|
3927
|
-
suggestions.push(`
|
|
4079
|
+
suggestions.push(`Field "${f.name}" (${f.cardinality} values) is raised to the power of ${contextCount} contexts — reducing its bounds compounds`);
|
|
3928
4080
|
}
|
|
3929
4081
|
}
|
|
3930
|
-
if (maxInFlight > 2) {
|
|
3931
|
-
const reducedInterleaving = permutations(handlerCount, 2);
|
|
3932
|
-
const reduction = reducedInterleaving > 0 ? Math.round(interleavingFactor / reducedInterleaving) : 1;
|
|
3933
|
-
suggestions.push(`Reducing maxInFlight from ${maxInFlight} to 2 would reduce interleaving by ~${reduction}x`);
|
|
3934
|
-
}
|
|
3935
4082
|
return {
|
|
4083
|
+
...scope.name === undefined ? {} : { subsystem: scope.name },
|
|
3936
4084
|
fields,
|
|
3937
4085
|
fieldProduct,
|
|
3938
4086
|
handlerCount,
|
|
3939
4087
|
maxInFlight,
|
|
3940
4088
|
contextCount,
|
|
4089
|
+
contexts,
|
|
4090
|
+
contextsDeclared,
|
|
4091
|
+
tabCount,
|
|
4092
|
+
sendBranching,
|
|
3941
4093
|
totalStateSpace,
|
|
3942
4094
|
interleavingFactor,
|
|
3943
4095
|
estimatedStates,
|
|
@@ -3946,6 +4098,24 @@ function estimateStateSpace(config, analysis) {
|
|
|
3946
4098
|
suggestions
|
|
3947
4099
|
};
|
|
3948
4100
|
}
|
|
4101
|
+
function estimateStateSpace(config, analysis) {
|
|
4102
|
+
return estimateForScope(config, {
|
|
4103
|
+
handlerCount: getHandlerCount(config, analysis),
|
|
4104
|
+
maxInFlight: getMaxInFlight(config)
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4107
|
+
function estimateSubsystems(config) {
|
|
4108
|
+
const subsystems = config.subsystems;
|
|
4109
|
+
if (!subsystems)
|
|
4110
|
+
return [];
|
|
4111
|
+
const topLevelMaxInFlight = getMaxInFlight(config);
|
|
4112
|
+
return Object.entries(subsystems).map(([name, sub]) => estimateForScope(config, {
|
|
4113
|
+
name,
|
|
4114
|
+
stateFields: sub.state,
|
|
4115
|
+
handlerCount: sub.handlers.length,
|
|
4116
|
+
maxInFlight: sub.bounds?.maxInFlight ?? topLevelMaxInFlight
|
|
4117
|
+
}));
|
|
4118
|
+
}
|
|
3949
4119
|
|
|
3950
4120
|
// tools/verify/src/codegen/config.ts
|
|
3951
4121
|
class ConfigGenerator {
|
|
@@ -3959,6 +4129,7 @@ class ConfigGenerator {
|
|
|
3959
4129
|
this.addHeader();
|
|
3960
4130
|
this.addImports();
|
|
3961
4131
|
this.addExport();
|
|
4132
|
+
this.addContextsConfig(analysis);
|
|
3962
4133
|
this.addStateConfig(analysis.fields, analysis.resources);
|
|
3963
4134
|
this.addMessagesConfig();
|
|
3964
4135
|
this.addBehaviorConfig();
|
|
@@ -4194,6 +4365,53 @@ class ConfigGenerator {
|
|
|
4194
4365
|
}
|
|
4195
4366
|
return "{ min: /* CONFIGURE */ null, max: /* CONFIGURE */ null }";
|
|
4196
4367
|
}
|
|
4368
|
+
addContextsConfig(analysis) {
|
|
4369
|
+
const inferred = [];
|
|
4370
|
+
for (const handler of analysis.handlers ?? []) {
|
|
4371
|
+
const node = handler.node;
|
|
4372
|
+
if (node === undefined || node === "unknown")
|
|
4373
|
+
continue;
|
|
4374
|
+
if (!inferred.includes(node))
|
|
4375
|
+
inferred.push(node);
|
|
4376
|
+
}
|
|
4377
|
+
this.line("// ─────────────────────────────────────────────────────────");
|
|
4378
|
+
this.line("// Contexts — the processes/actors messages are routed between");
|
|
4379
|
+
this.line("// ─────────────────────────────────────────────────────────");
|
|
4380
|
+
this.line("// This is the cheapest term in the state space to cut. The model");
|
|
4381
|
+
this.line("// replicates state across the set and quantifies every send over");
|
|
4382
|
+
this.line("// |Contexts| x (2^|Contexts| - 1) source/target pairs.");
|
|
4383
|
+
this.line("//");
|
|
4384
|
+
this.line("// Measured on a two-handler model, one message in flight, one tab:");
|
|
4385
|
+
this.line("// • 3 contexts: 38,464 distinct states");
|
|
4386
|
+
this.line("// • 2 contexts: 4,896 distinct states");
|
|
4387
|
+
this.line("// • 1 context: 368 distinct states");
|
|
4388
|
+
this.line("//");
|
|
4389
|
+
const source = inferred.length > 0 ? inferred : this.projectTypeContexts();
|
|
4390
|
+
if (source.length > 0) {
|
|
4391
|
+
this.line(inferred.length > 0 ? "// ✓ Auto-configured from the file paths of the handlers" : `// ✓ Auto-configured from the project type (${this.projectType})`);
|
|
4392
|
+
this.line(`contexts: /* REVIEW */ [${source.map((c) => `'${c}'`).join(", ")}],`);
|
|
4393
|
+
} else {
|
|
4394
|
+
this.line("// No context could be inferred from the handler file paths.");
|
|
4395
|
+
this.line("// Omitting the key models the browser-extension default:");
|
|
4396
|
+
this.line("// ['background', 'content', 'popup']");
|
|
4397
|
+
this.line("// contexts: /* CONFIGURE */ ['server'],");
|
|
4398
|
+
}
|
|
4399
|
+
this.line("");
|
|
4400
|
+
}
|
|
4401
|
+
projectTypeContexts() {
|
|
4402
|
+
switch (this.projectType) {
|
|
4403
|
+
case "websocket-app":
|
|
4404
|
+
return ["server", "client"];
|
|
4405
|
+
case "pwa":
|
|
4406
|
+
return ["ServiceWorker", "Window"];
|
|
4407
|
+
case "electron":
|
|
4408
|
+
return ["main", "renderer"];
|
|
4409
|
+
case "chrome-extension":
|
|
4410
|
+
return ["background", "content", "popup"];
|
|
4411
|
+
default:
|
|
4412
|
+
return [];
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4197
4415
|
addMessagesConfig() {
|
|
4198
4416
|
this.line("messages: {");
|
|
4199
4417
|
this.indent++;
|
|
@@ -4263,13 +4481,6 @@ class ConfigGenerator {
|
|
|
4263
4481
|
this.line("maxTabs: 1,");
|
|
4264
4482
|
break;
|
|
4265
4483
|
case "generic":
|
|
4266
|
-
this.line("// Maximum contexts/actors to model.");
|
|
4267
|
-
this.line("//");
|
|
4268
|
-
this.line("// Recommended:");
|
|
4269
|
-
this.line("// • 2-3: Simple message passing");
|
|
4270
|
-
this.line("// • 4-5: Complex coordination");
|
|
4271
|
-
this.line("//");
|
|
4272
|
-
this.line("maxContexts: 3,");
|
|
4273
4484
|
break;
|
|
4274
4485
|
}
|
|
4275
4486
|
this.line("");
|
|
@@ -4401,7 +4612,12 @@ function generateConfig(analysis, projectType = "chrome-extension") {
|
|
|
4401
4612
|
return generator.generate(analysis, projectType);
|
|
4402
4613
|
}
|
|
4403
4614
|
|
|
4615
|
+
// tools/verify/src/cli.ts
|
|
4616
|
+
init_model_constants();
|
|
4617
|
+
|
|
4404
4618
|
// tools/verify/src/config/parser.ts
|
|
4619
|
+
init_model_constants();
|
|
4620
|
+
init_memory();
|
|
4405
4621
|
import * as fs from "node:fs";
|
|
4406
4622
|
import * as path from "node:path";
|
|
4407
4623
|
|
|
@@ -4483,6 +4699,45 @@ function validateCoupledFields(coupledFields, stateConfig) {
|
|
|
4483
4699
|
}
|
|
4484
4700
|
|
|
4485
4701
|
// tools/verify/src/config/parser.ts
|
|
4702
|
+
var RESERVED_CONTEXT_NAMES = new Set([
|
|
4703
|
+
"Contexts",
|
|
4704
|
+
"MaxMessages",
|
|
4705
|
+
"NULL",
|
|
4706
|
+
"Tabs",
|
|
4707
|
+
"TimeoutLimit",
|
|
4708
|
+
"MaxWorkers",
|
|
4709
|
+
"MaxRenderers",
|
|
4710
|
+
"MaxClients",
|
|
4711
|
+
"NoTarget",
|
|
4712
|
+
"MeshDocs",
|
|
4713
|
+
"ASSUME",
|
|
4714
|
+
"BOOLEAN",
|
|
4715
|
+
"CASE",
|
|
4716
|
+
"CHOOSE",
|
|
4717
|
+
"CONSTANT",
|
|
4718
|
+
"CONSTANTS",
|
|
4719
|
+
"DOMAIN",
|
|
4720
|
+
"ELSE",
|
|
4721
|
+
"ENABLED",
|
|
4722
|
+
"EXCEPT",
|
|
4723
|
+
"EXTENDS",
|
|
4724
|
+
"FALSE",
|
|
4725
|
+
"IF",
|
|
4726
|
+
"IN",
|
|
4727
|
+
"INSTANCE",
|
|
4728
|
+
"LET",
|
|
4729
|
+
"MODULE",
|
|
4730
|
+
"OTHER",
|
|
4731
|
+
"SUBSET",
|
|
4732
|
+
"THEN",
|
|
4733
|
+
"TRUE",
|
|
4734
|
+
"UNCHANGED",
|
|
4735
|
+
"UNION",
|
|
4736
|
+
"VARIABLE",
|
|
4737
|
+
"VARIABLES",
|
|
4738
|
+
"WITH"
|
|
4739
|
+
]);
|
|
4740
|
+
|
|
4486
4741
|
class ConfigValidator {
|
|
4487
4742
|
issues = [];
|
|
4488
4743
|
validate(configPath) {
|
|
@@ -4596,6 +4851,7 @@ class ConfigValidator {
|
|
|
4596
4851
|
this.findNullPlaceholders(config.messages, "messages");
|
|
4597
4852
|
this.validateBounds(config);
|
|
4598
4853
|
this.validateTier1Optimizations(config.messages);
|
|
4854
|
+
this.validateContexts(config);
|
|
4599
4855
|
if (config.verification) {
|
|
4600
4856
|
this.validateVerificationOptions(config.verification);
|
|
4601
4857
|
}
|
|
@@ -4875,6 +5131,140 @@ class ConfigValidator {
|
|
|
4875
5131
|
});
|
|
4876
5132
|
}
|
|
4877
5133
|
}
|
|
5134
|
+
this.validateMemory(verification.memory);
|
|
5135
|
+
}
|
|
5136
|
+
validateContexts(config) {
|
|
5137
|
+
this.warnOnDeadMaxContexts(config);
|
|
5138
|
+
const contexts = config.contexts;
|
|
5139
|
+
if (contexts === undefined)
|
|
5140
|
+
return;
|
|
5141
|
+
if (!Array.isArray(contexts)) {
|
|
5142
|
+
this.issues.push({
|
|
5143
|
+
type: "invalid_value",
|
|
5144
|
+
severity: "error",
|
|
5145
|
+
field: "contexts",
|
|
5146
|
+
message: "contexts must be an array of context names",
|
|
5147
|
+
suggestion: `Use an array, e.g. ["server"], or omit the key for the default {${DEFAULT_CONTEXTS.join(", ")}}`
|
|
5148
|
+
});
|
|
5149
|
+
return;
|
|
5150
|
+
}
|
|
5151
|
+
if (contexts.length === 0) {
|
|
5152
|
+
this.issues.push({
|
|
5153
|
+
type: "invalid_value",
|
|
5154
|
+
severity: "error",
|
|
5155
|
+
field: "contexts",
|
|
5156
|
+
message: "contexts is empty; the model needs at least one context",
|
|
5157
|
+
suggestion: `Name at least one context, e.g. ["server"], or omit the key for the default {${DEFAULT_CONTEXTS.join(", ")}}`
|
|
5158
|
+
});
|
|
5159
|
+
return;
|
|
5160
|
+
}
|
|
5161
|
+
const seen = new Map;
|
|
5162
|
+
for (const name of contexts) {
|
|
5163
|
+
this.validateContextName(name, seen);
|
|
5164
|
+
}
|
|
5165
|
+
this.warnOnSingleContextMesh(config, contexts.length);
|
|
5166
|
+
}
|
|
5167
|
+
validateContextName(name, seen) {
|
|
5168
|
+
if (typeof name !== "string") {
|
|
5169
|
+
this.issues.push({
|
|
5170
|
+
type: "invalid_value",
|
|
5171
|
+
severity: "error",
|
|
5172
|
+
field: "contexts",
|
|
5173
|
+
message: `Context name ${JSON.stringify(name)} is not a string`,
|
|
5174
|
+
suggestion: "Every entry must be a string naming one context"
|
|
5175
|
+
});
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
const emitted = sanitizeContextName(name);
|
|
5179
|
+
if (!CONTEXT_NAME_PATTERN.test(emitted)) {
|
|
5180
|
+
this.issues.push({
|
|
5181
|
+
type: "invalid_value",
|
|
5182
|
+
severity: "error",
|
|
5183
|
+
field: "contexts",
|
|
5184
|
+
message: `Context "${name}" is not a TLA+ model value (emits as "${emitted}")`,
|
|
5185
|
+
suggestion: "Start with a letter or underscore, then letters, digits or underscores"
|
|
5186
|
+
});
|
|
5187
|
+
return;
|
|
5188
|
+
}
|
|
5189
|
+
if (RESERVED_CONTEXT_NAMES.has(emitted)) {
|
|
5190
|
+
this.issues.push({
|
|
5191
|
+
type: "invalid_value",
|
|
5192
|
+
severity: "error",
|
|
5193
|
+
field: "contexts",
|
|
5194
|
+
message: `Context "${name}" collides with a TLA+ keyword or a constant the .cfg already declares`,
|
|
5195
|
+
suggestion: `Pick a name outside {${[...RESERVED_CONTEXT_NAMES].sort().join(", ")}}`
|
|
5196
|
+
});
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
const collidesWith = seen.get(emitted);
|
|
5200
|
+
if (collidesWith !== undefined) {
|
|
5201
|
+
this.issues.push({
|
|
5202
|
+
type: "invalid_value",
|
|
5203
|
+
severity: "error",
|
|
5204
|
+
field: "contexts",
|
|
5205
|
+
message: collidesWith === name ? `Context "${name}" is declared twice` : `Contexts "${collidesWith}" and "${name}" both emit as "${emitted}"`,
|
|
5206
|
+
suggestion: "Each context must emit a distinct TLA+ model value"
|
|
5207
|
+
});
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
seen.set(emitted, name);
|
|
5211
|
+
if (emitted !== name) {
|
|
5212
|
+
this.issues.push({
|
|
5213
|
+
type: "invalid_value",
|
|
5214
|
+
severity: "warning",
|
|
5215
|
+
field: "contexts",
|
|
5216
|
+
message: `Context "${name}" is emitted as the model value "${emitted}"`,
|
|
5217
|
+
suggestion: "Name it that way in the config so the spec and the config read alike"
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
warnOnDeadMaxContexts(config) {
|
|
5222
|
+
if (config.messages?.maxContexts === undefined)
|
|
5223
|
+
return;
|
|
5224
|
+
this.issues.push({
|
|
5225
|
+
type: "invalid_value",
|
|
5226
|
+
severity: "warning",
|
|
5227
|
+
field: "messages.maxContexts",
|
|
5228
|
+
message: "messages.maxContexts does not size the Contexts set and never did",
|
|
5229
|
+
suggestion: 'Declare the set instead, e.g. contexts: ["server", "client"]'
|
|
5230
|
+
});
|
|
5231
|
+
}
|
|
5232
|
+
warnOnSingleContextMesh(config, contextCount) {
|
|
5233
|
+
if (contextCount > 1)
|
|
5234
|
+
return;
|
|
5235
|
+
if (!config.mesh || Object.keys(config.mesh).length === 0)
|
|
5236
|
+
return;
|
|
5237
|
+
this.issues.push({
|
|
5238
|
+
type: "invalid_value",
|
|
5239
|
+
severity: "warning",
|
|
5240
|
+
field: "contexts",
|
|
5241
|
+
message: "one context with a `mesh` block: PropagateMeshOp needs two distinct contexts, so mesh propagation is never enabled",
|
|
5242
|
+
suggestion: "Declare at least two contexts, or drop the mesh block from this model"
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
validateMemory(memory) {
|
|
5246
|
+
if (memory === undefined)
|
|
5247
|
+
return;
|
|
5248
|
+
const bytes = parseMemoryBytes(memory);
|
|
5249
|
+
if (bytes === null) {
|
|
5250
|
+
this.issues.push({
|
|
5251
|
+
type: "invalid_value",
|
|
5252
|
+
severity: "error",
|
|
5253
|
+
field: "verification.memory",
|
|
5254
|
+
message: `"${memory}" is not a docker-style memory size`,
|
|
5255
|
+
suggestion: 'Use a number with an optional b/k/m/g suffix, e.g. "8g"'
|
|
5256
|
+
});
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
if (bytes < MIN_TLC_MEMORY_BYTES) {
|
|
5260
|
+
this.issues.push({
|
|
5261
|
+
type: "invalid_value",
|
|
5262
|
+
severity: "error",
|
|
5263
|
+
field: "verification.memory",
|
|
5264
|
+
message: `${memory} is below the ${MIN_TLC_MEMORY} minimum TLC can run in`,
|
|
5265
|
+
suggestion: `Use ${MIN_TLC_MEMORY} or more`
|
|
5266
|
+
});
|
|
5267
|
+
}
|
|
4878
5268
|
}
|
|
4879
5269
|
validateTier2Optimizations(tier2) {
|
|
4880
5270
|
if (tier2.temporalConstraints) {
|
|
@@ -8427,6 +8817,9 @@ async function analyzeCodebase(options) {
|
|
|
8427
8817
|
const extractor = new TypeExtractor(options.tsConfigPath);
|
|
8428
8818
|
return extractor.analyzeCodebase(options.stateFilePath);
|
|
8429
8819
|
}
|
|
8820
|
+
// tools/verify/src/cli.ts
|
|
8821
|
+
init_memory();
|
|
8822
|
+
|
|
8430
8823
|
// tools/verify/src/runner/mesh-seed.ts
|
|
8431
8824
|
function meshSeedCfg(baseCfg, opts) {
|
|
8432
8825
|
if (!opts.disableFix)
|
|
@@ -8612,11 +9005,30 @@ async function estimateCommand() {
|
|
|
8612
9005
|
if (exprValidation.warnings.length > 0) {
|
|
8613
9006
|
displayExpressionWarnings(exprValidation);
|
|
8614
9007
|
}
|
|
8615
|
-
const
|
|
8616
|
-
|
|
9008
|
+
const perSubsystem = estimateSubsystems(typedConfig);
|
|
9009
|
+
if (perSubsystem.length > 0) {
|
|
9010
|
+
for (const estimate of perSubsystem) {
|
|
9011
|
+
displayEstimate(estimate);
|
|
9012
|
+
}
|
|
9013
|
+
displaySubsystemEstimateSummary(perSubsystem);
|
|
9014
|
+
return;
|
|
9015
|
+
}
|
|
9016
|
+
displayEstimate(estimateStateSpace(typedConfig, typedAnalysis));
|
|
9017
|
+
}
|
|
9018
|
+
function displaySubsystemEstimateSummary(estimates) {
|
|
9019
|
+
console.log(color(`Per-subsystem summary:
|
|
9020
|
+
`, COLORS.blue));
|
|
9021
|
+
for (const e of estimates) {
|
|
9022
|
+
const name = (e.subsystem ?? "?").padEnd(20);
|
|
9023
|
+
const states = `~${e.estimatedStates.toLocaleString()}`.padEnd(20);
|
|
9024
|
+
const feasColor = e.feasibility === "trivial" || e.feasibility === "feasible" ? COLORS.green : e.feasibility === "slow" ? COLORS.yellow : COLORS.red;
|
|
9025
|
+
console.log(` ${name} ${states} ${color(e.feasibility, feasColor)}`);
|
|
9026
|
+
}
|
|
9027
|
+
console.log();
|
|
8617
9028
|
}
|
|
8618
9029
|
function displayEstimate(estimate) {
|
|
8619
|
-
|
|
9030
|
+
const heading = estimate.subsystem ? `State space estimate — subsystem ${estimate.subsystem}:` : "State space estimate:";
|
|
9031
|
+
console.log(color(`${heading}
|
|
8620
9032
|
`, COLORS.blue));
|
|
8621
9033
|
console.log(color(" Fields:", COLORS.blue));
|
|
8622
9034
|
for (const field of estimate.fields) {
|
|
@@ -8629,8 +9041,11 @@ function displayEstimate(estimate) {
|
|
|
8629
9041
|
console.log(` Field combinations: ${color(String(estimate.fieldProduct), COLORS.green)}`);
|
|
8630
9042
|
console.log(` Handlers: ${estimate.handlerCount}`);
|
|
8631
9043
|
console.log(` Max in-flight: ${estimate.maxInFlight}`);
|
|
8632
|
-
|
|
8633
|
-
console.log(`
|
|
9044
|
+
const contextSource = estimate.contextsDeclared ? "declared" : "the default — no `contexts` key";
|
|
9045
|
+
console.log(` Contexts: ${estimate.contextCount} — ${estimate.contexts.join(", ")} (${contextSource})`);
|
|
9046
|
+
console.log(` Tabs: ${estimate.tabCount}`);
|
|
9047
|
+
console.log(` State across contexts: ${estimate.fieldProduct}^${estimate.contextCount} = ${estimate.totalStateSpace.toLocaleString()}`);
|
|
9048
|
+
console.log(` Send branching: ${estimate.sendBranching.toLocaleString()} per message (^${estimate.maxInFlight} = ${estimate.interleavingFactor.toLocaleString()})`);
|
|
8634
9049
|
console.log();
|
|
8635
9050
|
console.log(` Estimated states: ${color(`~${estimate.estimatedStates.toLocaleString()}`, COLORS.green)}`);
|
|
8636
9051
|
const feasColor = estimate.feasibility === "trivial" || estimate.feasibility === "feasible" ? COLORS.green : estimate.feasibility === "slow" ? COLORS.yellow : COLORS.red;
|
|
@@ -8854,6 +9269,9 @@ function getTimeout(config) {
|
|
|
8854
9269
|
function getWorkers(config) {
|
|
8855
9270
|
return config.verification?.workers ?? 1;
|
|
8856
9271
|
}
|
|
9272
|
+
function getMemory(config) {
|
|
9273
|
+
return config.verification?.memory;
|
|
9274
|
+
}
|
|
8857
9275
|
function getMaxDepth(config) {
|
|
8858
9276
|
if ("tier2" in config && config.tier2?.boundedExploration?.maxDepth) {
|
|
8859
9277
|
return config.tier2.boundedExploration.maxDepth;
|
|
@@ -8888,6 +9306,55 @@ async function runCoupledFieldsLint(config, analysis) {
|
|
|
8888
9306
|
console.log(color(" the relationship with a `capabilities` invariant.", COLORS.yellow));
|
|
8889
9307
|
console.log();
|
|
8890
9308
|
}
|
|
9309
|
+
function describeContextSet(config) {
|
|
9310
|
+
const contexts = resolveContexts(config);
|
|
9311
|
+
const source = hasDeclaredContexts(config) ? "declared" : "default";
|
|
9312
|
+
return `${contexts.join(", ")} (${contexts.length}, ${source})`;
|
|
9313
|
+
}
|
|
9314
|
+
function inferredContexts(analysis) {
|
|
9315
|
+
const seen = [];
|
|
9316
|
+
for (const handler of analysis.handlers) {
|
|
9317
|
+
const node = handler.node;
|
|
9318
|
+
if (node === undefined || node === "unknown")
|
|
9319
|
+
continue;
|
|
9320
|
+
if (!seen.includes(node))
|
|
9321
|
+
seen.push(node);
|
|
9322
|
+
}
|
|
9323
|
+
return seen;
|
|
9324
|
+
}
|
|
9325
|
+
function displayContextDiagnostics(config, analysis) {
|
|
9326
|
+
const declared = hasDeclaredContexts(config);
|
|
9327
|
+
const contexts = new Set(resolveContexts(config));
|
|
9328
|
+
const inferred = inferredContexts(analysis);
|
|
9329
|
+
if (!declared) {
|
|
9330
|
+
console.log(color(`ℹ️ No \`contexts\` key — modelling the default {${DEFAULT_CONTEXTS.join(", ")}}`, COLORS.gray));
|
|
9331
|
+
if (inferred.length > 0) {
|
|
9332
|
+
console.log(color(` polly infers from the handler paths: ${inferred.join(", ")}`, COLORS.gray));
|
|
9333
|
+
console.log(color(` Declaring \`contexts: [${inferred.map((c) => `"${c}"`).join(", ")}]\` shrinks the model to the contexts you have.`, COLORS.gray));
|
|
9334
|
+
}
|
|
9335
|
+
console.log();
|
|
9336
|
+
return;
|
|
9337
|
+
}
|
|
9338
|
+
const outside = analysis.handlers.filter((h) => {
|
|
9339
|
+
const node = h.node;
|
|
9340
|
+
return node !== undefined && node !== "unknown" && !contexts.has(sanitizeContextName(node));
|
|
9341
|
+
});
|
|
9342
|
+
if (outside.length === 0)
|
|
9343
|
+
return;
|
|
9344
|
+
console.log(color(`⚠️ ${outside.length} handler(s) run in a context \`contexts\` does not declare:`, COLORS.yellow));
|
|
9345
|
+
for (const h of outside.slice(0, 10)) {
|
|
9346
|
+
const node = h.node;
|
|
9347
|
+
console.log(color(` • ${h.messageType} → ${node} (${h.location.file})`, COLORS.yellow));
|
|
9348
|
+
}
|
|
9349
|
+
if (outside.length > 10) {
|
|
9350
|
+
console.log(color(` ... and ${outside.length - 10} more`, COLORS.yellow));
|
|
9351
|
+
}
|
|
9352
|
+
console.log();
|
|
9353
|
+
console.log(color(` The model quantifies sends over {${[...contexts].join(", ")}} only, so these handlers`, COLORS.yellow));
|
|
9354
|
+
console.log(color(" are modelled from a source they are not in. Add the missing contexts, or correct the", COLORS.yellow));
|
|
9355
|
+
console.log(color(" file paths polly infers them from.", COLORS.yellow));
|
|
9356
|
+
console.log();
|
|
9357
|
+
}
|
|
8891
9358
|
async function runFullVerification(configPath) {
|
|
8892
9359
|
const config = await loadVerificationConfig(configPath);
|
|
8893
9360
|
const analysis = await runCodebaseAnalysis();
|
|
@@ -8902,6 +9369,7 @@ async function runFullVerification(configPath) {
|
|
|
8902
9369
|
const meshFindingCount = displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
|
|
8903
9370
|
await runCoupledFieldsLint(typedConfig, typedAnalysis);
|
|
8904
9371
|
await runModelCoverage(typedConfig, typedAnalysis, meshFindingCount);
|
|
9372
|
+
displayContextDiagnostics(typedConfig, typedAnalysis);
|
|
8905
9373
|
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
8906
9374
|
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
8907
9375
|
if (isWitnessMode()) {
|
|
@@ -8927,6 +9395,7 @@ async function runMonolithicVerification(config, analysis) {
|
|
|
8927
9395
|
const timeoutSeconds = getTimeout(typedConfig);
|
|
8928
9396
|
const workers = getWorkers(typedConfig);
|
|
8929
9397
|
const maxDepth = getMaxDepth(typedConfig);
|
|
9398
|
+
const memory = getMemory(typedConfig);
|
|
8930
9399
|
console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
|
|
8931
9400
|
if (timeoutSeconds === 0) {
|
|
8932
9401
|
console.log(color(" No timeout set - will run until completion", COLORS.gray));
|
|
@@ -8938,13 +9407,16 @@ async function runMonolithicVerification(config, analysis) {
|
|
|
8938
9407
|
if (maxDepth !== undefined) {
|
|
8939
9408
|
console.log(color(` Max depth: ${maxDepth}`, COLORS.gray));
|
|
8940
9409
|
}
|
|
9410
|
+
console.log(color(` Memory: ${memory ?? DEFAULT_TLC_MEMORY}`, COLORS.gray));
|
|
9411
|
+
console.log(color(` Contexts: ${describeContextSet(typedConfig)}`, COLORS.gray));
|
|
8941
9412
|
console.log();
|
|
8942
9413
|
const result = await docker.runTLC(specPath, {
|
|
8943
9414
|
workers,
|
|
8944
9415
|
timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
|
|
8945
|
-
maxDepth
|
|
9416
|
+
maxDepth,
|
|
9417
|
+
memory
|
|
8946
9418
|
});
|
|
8947
|
-
const meshSeed = await runMeshSeedGuard(docker, specDir, config);
|
|
9419
|
+
const meshSeed = await runMeshSeedGuard(docker, specDir, config, memory);
|
|
8948
9420
|
if (meshSeed && !meshSeed.success) {
|
|
8949
9421
|
console.log(color(`
|
|
8950
9422
|
❌ Mesh seed-race guard failed (polly#113 / polly#114)
|
|
@@ -9011,6 +9483,9 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
9011
9483
|
const timeoutSeconds = getTimeout(config);
|
|
9012
9484
|
const workers = getWorkers(config);
|
|
9013
9485
|
const maxDepth = getMaxDepth(config);
|
|
9486
|
+
const memory = getMemory(config);
|
|
9487
|
+
console.log(color(`⚙️ Contexts: ${describeContextSet(config)}`, COLORS.gray));
|
|
9488
|
+
console.log();
|
|
9014
9489
|
await Promise.resolve().then(() => init_tla());
|
|
9015
9490
|
const results = [];
|
|
9016
9491
|
const customTLAPaths = getCustomTLAPaths(config);
|
|
@@ -9036,7 +9511,8 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
9036
9511
|
const result = await docker.runTLC(specPath, {
|
|
9037
9512
|
workers,
|
|
9038
9513
|
timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
|
|
9039
|
-
maxDepth
|
|
9514
|
+
maxDepth,
|
|
9515
|
+
memory
|
|
9040
9516
|
});
|
|
9041
9517
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
9042
9518
|
results.push({
|
|
@@ -9044,9 +9520,8 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
9044
9520
|
success: result.success,
|
|
9045
9521
|
handlerCount: sub.handlers.length,
|
|
9046
9522
|
ensuresCount,
|
|
9047
|
-
stateCount: result.stats?.distinctStates ?? 0,
|
|
9048
9523
|
elapsed,
|
|
9049
|
-
stats: result.stats,
|
|
9524
|
+
...result.stats === undefined ? {} : { stats: result.stats },
|
|
9050
9525
|
error: result.error
|
|
9051
9526
|
});
|
|
9052
9527
|
if (result.success) {
|
|
@@ -9065,6 +9540,14 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
9065
9540
|
displayCompositionalReport(results, interference.valid);
|
|
9066
9541
|
}
|
|
9067
9542
|
var DEFAULT_WITNESS_TIMEOUT_MS = 180000;
|
|
9543
|
+
function witnessExplorationNote(stats) {
|
|
9544
|
+
if (!stats)
|
|
9545
|
+
return "TLC printed no summary line — exploration incomplete, verdict unsound";
|
|
9546
|
+
if (!stats.exhaustive) {
|
|
9547
|
+
return `${stats.distinctStates} distinct states explored, ${stats.statesLeftOnQueue} left on queue — not exhaustive`;
|
|
9548
|
+
}
|
|
9549
|
+
return `${stats.distinctStates} distinct states explored`;
|
|
9550
|
+
}
|
|
9068
9551
|
async function globAbsolute(cwd, pattern) {
|
|
9069
9552
|
const out = [];
|
|
9070
9553
|
for await (const f of new Bun.Glob(pattern).scan({ cwd, absolute: true, onlyFiles: true })) {
|
|
@@ -9097,6 +9580,7 @@ async function runWitnessVerification(config) {
|
|
|
9097
9580
|
const timeoutSeconds = getTimeout(config);
|
|
9098
9581
|
const timeout = timeoutSeconds > 0 ? timeoutSeconds * 1000 : DEFAULT_WITNESS_TIMEOUT_MS;
|
|
9099
9582
|
const workers = getWorkers(config);
|
|
9583
|
+
const memory = getMemory(config);
|
|
9100
9584
|
const results = [];
|
|
9101
9585
|
let idx = 0;
|
|
9102
9586
|
for (const w of witnesses) {
|
|
@@ -9183,7 +9667,7 @@ async function runWitnessVerification(config) {
|
|
|
9183
9667
|
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
9184
9668
|
let tlc;
|
|
9185
9669
|
try {
|
|
9186
|
-
tlc = await docker.runTLC(witnessTla, { workers, timeout });
|
|
9670
|
+
tlc = await docker.runTLC(witnessTla, { workers, timeout, memory });
|
|
9187
9671
|
} catch (err) {
|
|
9188
9672
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9189
9673
|
results.push({
|
|
@@ -9212,7 +9696,7 @@ async function runWitnessVerification(config) {
|
|
|
9212
9696
|
continue;
|
|
9213
9697
|
}
|
|
9214
9698
|
const verdict = witnessVerdict(polarity, reachable);
|
|
9215
|
-
const note = reachable ? undefined :
|
|
9699
|
+
const note = reachable ? undefined : witnessExplorationNote(tlc.stats);
|
|
9216
9700
|
results.push({
|
|
9217
9701
|
id,
|
|
9218
9702
|
status: verdict.status,
|
|
@@ -9278,6 +9762,53 @@ function displayEnsuresSummary(results) {
|
|
|
9278
9762
|
console.log(color(" A property only fires at states where its handler is enabled — raise", COLORS.gray));
|
|
9279
9763
|
console.log(color(" bounds.maxInFlight on a subsystem to reach multi-step-reachable handlers.", COLORS.gray));
|
|
9280
9764
|
}
|
|
9765
|
+
function stateColumn(stats) {
|
|
9766
|
+
if (!stats)
|
|
9767
|
+
return "? states";
|
|
9768
|
+
if (!stats.exhaustive)
|
|
9769
|
+
return `${stats.distinctStates}+ states`;
|
|
9770
|
+
return `${stats.distinctStates} states`;
|
|
9771
|
+
}
|
|
9772
|
+
function displayTLCStats(stats) {
|
|
9773
|
+
if (!stats) {
|
|
9774
|
+
console.log(color(" States explored: unknown (TLC printed no summary line)", COLORS.yellow));
|
|
9775
|
+
return;
|
|
9776
|
+
}
|
|
9777
|
+
console.log(color(` States explored: ${stats.statesGenerated}`, COLORS.gray));
|
|
9778
|
+
console.log(color(` Distinct states: ${stats.distinctStates}`, COLORS.gray));
|
|
9779
|
+
if (stats.searchDepth !== undefined) {
|
|
9780
|
+
console.log(color(` Search depth: ${stats.searchDepth}`, COLORS.gray));
|
|
9781
|
+
}
|
|
9782
|
+
if (!stats.exhaustive) {
|
|
9783
|
+
console.log(color(` ⚠ incomplete: ${stats.statesLeftOnQueue} states left on queue — the model was not exhausted`, COLORS.yellow));
|
|
9784
|
+
}
|
|
9785
|
+
if (stats.noActionEnabled) {
|
|
9786
|
+
console.log(color(` ⚠ no action was ever enabled: distinct states equal the ${stats.initialStates} initial states`, COLORS.yellow));
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9789
|
+
function displayIncompleteSubsystems(results) {
|
|
9790
|
+
const notes = [];
|
|
9791
|
+
for (const r of results) {
|
|
9792
|
+
if (!r.success)
|
|
9793
|
+
continue;
|
|
9794
|
+
if (!r.stats) {
|
|
9795
|
+
notes.push(`${r.name}: TLC printed no summary line — state count unknown`);
|
|
9796
|
+
continue;
|
|
9797
|
+
}
|
|
9798
|
+
if (!r.stats.exhaustive) {
|
|
9799
|
+
notes.push(`${r.name}: ${r.stats.statesLeftOnQueue} states left on queue — not exhausted`);
|
|
9800
|
+
}
|
|
9801
|
+
if (r.stats.noActionEnabled) {
|
|
9802
|
+
notes.push(`${r.name}: no action was ever enabled — the ${r.stats.initialStates} states explored are the initial states`);
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
if (notes.length === 0)
|
|
9806
|
+
return;
|
|
9807
|
+
console.log();
|
|
9808
|
+
for (const note of notes) {
|
|
9809
|
+
console.log(color(` ⚠ ${note}`, COLORS.yellow));
|
|
9810
|
+
}
|
|
9811
|
+
}
|
|
9281
9812
|
function displayCompositionalReport(results, nonInterferenceValid) {
|
|
9282
9813
|
console.log(color(`Subsystem verification results:
|
|
9283
9814
|
`, COLORS.blue));
|
|
@@ -9286,10 +9817,11 @@ function displayCompositionalReport(results, nonInterferenceValid) {
|
|
|
9286
9817
|
const name = r.name.padEnd(20);
|
|
9287
9818
|
const handlers = `${r.handlerCount} handler${r.handlerCount === 1 ? "" : "s"}`;
|
|
9288
9819
|
const ensures = `${r.ensuresCount} ensures`;
|
|
9289
|
-
const states =
|
|
9820
|
+
const states = stateColumn(r.stats);
|
|
9290
9821
|
const time = `${r.elapsed.toFixed(1)}s`;
|
|
9291
9822
|
console.log(` ${status} ${name} ${handlers.padEnd(14)} ${ensures.padEnd(12)} ${states.padEnd(14)} ${time}`);
|
|
9292
9823
|
}
|
|
9824
|
+
displayIncompleteSubsystems(results);
|
|
9293
9825
|
displayEnsuresSummary(results);
|
|
9294
9826
|
console.log();
|
|
9295
9827
|
const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
|
|
@@ -9387,7 +9919,7 @@ function findMeshSeedSpecDir() {
|
|
|
9387
9919
|
}
|
|
9388
9920
|
return null;
|
|
9389
9921
|
}
|
|
9390
|
-
async function runMeshSeedGuard(docker, specDir, config) {
|
|
9922
|
+
async function runMeshSeedGuard(docker, specDir, config, memory) {
|
|
9391
9923
|
if (getMeshDocIds(config).length === 0)
|
|
9392
9924
|
return;
|
|
9393
9925
|
const sourceDir = findMeshSeedSpecDir();
|
|
@@ -9400,7 +9932,7 @@ async function runMeshSeedGuard(docker, specDir, config) {
|
|
|
9400
9932
|
const baseCfg = fs4.readFileSync(path5.join(sourceDir, "MeshSeed.cfg"), "utf8");
|
|
9401
9933
|
fs4.writeFileSync(path5.join(specDir, "MeshSeed.cfg"), meshSeedCfg(baseCfg, { disableFix: fixDisabled }));
|
|
9402
9934
|
console.log(color(`⚙️ Running mesh seed-race guard (MeshSeed.tla, SeedDeterministic = ${fixDisabled ? "FALSE" : "TRUE"})...`, COLORS.blue));
|
|
9403
|
-
return docker.runTLC(path5.join(specDir, "MeshSeed.tla"), { workers: 1 });
|
|
9935
|
+
return docker.runTLC(path5.join(specDir, "MeshSeed.tla"), { workers: 1, memory });
|
|
9404
9936
|
}
|
|
9405
9937
|
async function setupDocker() {
|
|
9406
9938
|
await Promise.resolve().then(() => init_docker());
|
|
@@ -9460,8 +9992,7 @@ function displayVerificationResults(result, specDir) {
|
|
|
9460
9992
|
console.log(color(`✅ Verification passed!
|
|
9461
9993
|
`, COLORS.green));
|
|
9462
9994
|
console.log(color("Statistics:", COLORS.blue));
|
|
9463
|
-
|
|
9464
|
-
console.log(color(` Distinct states: ${result.stats?.distinctStates || 0}`, COLORS.gray));
|
|
9995
|
+
displayTLCStats(result.stats);
|
|
9465
9996
|
console.log();
|
|
9466
9997
|
return;
|
|
9467
9998
|
}
|
|
@@ -9574,4 +10105,4 @@ main().catch((error) => {
|
|
|
9574
10105
|
process.exit(1);
|
|
9575
10106
|
});
|
|
9576
10107
|
|
|
9577
|
-
//# debugId=
|
|
10108
|
+
//# debugId=3AB21B8DB351312E64756E2164756E21
|