@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.
@@ -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(" Contexts = {background, content, popup}");
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
- if (this.tabSymmetryEnabled) {
1013
- for (let i = 0;i < this.tabCount; i++) {
1014
- lines.push(` Tab${i} = Tab${i}`);
1015
- }
1016
- const tabValues = Array.from({ length: this.tabCount }, (_, i) => `Tab${i}`).join(", ");
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
- stats: {
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
- var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src/runner", swept = false;
3738
- var init_docker = () => {};
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
- switch (obj.type) {
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(obj.values)) {
3798
- return { name, cardinality: obj.values.length, kind: "enum" };
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 obj.min === "number" && typeof obj.max === "number") {
3803
- return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
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
- if ("values" in obj && Array.isArray(obj.values)) {
3816
- const base = obj.values.length;
3817
- const extra = obj.abstract === true ? 1 : 0;
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: base + extra,
3821
- kind: obj.abstract ? "enum (abstract)" : "enum (values)"
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 ("min" in obj && "max" in obj && typeof obj.min === "number" && typeof obj.max === "number" && !("type" in obj)) {
3828
- return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
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 getMaxTabs(config) {
3975
+ function getTabSetInput(config) {
3873
3976
  if (isLegacyConfig(config)) {
3874
- return config.messages.maxTabs ?? 1;
3977
+ return config.messages;
3875
3978
  }
3876
- return 1;
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
- function estimateStateSpace(config, analysis) {
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 = getHandlerCount(config, analysis);
3915
- const maxInFlight = getMaxInFlight(config);
3916
- const maxTabs = getMaxTabs(config);
3917
- const contextCount = maxTabs + 1;
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 interleavingFactor = permutations(handlerCount, maxInFlight);
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("Consider splitting into subsystems");
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(`Consider reducing bounds for field "${f.name}" (${f.cardinality} values)`);
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 estimate = estimateStateSpace(typedConfig, typedAnalysis);
8616
- displayEstimate(estimate);
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
- console.log(color(`State space estimate:
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
- console.log(` Contexts: ${estimate.contextCount} (${estimate.contextCount - 1} tab${estimate.contextCount - 1 === 1 ? "" : "s"} + background)`);
8633
- console.log(` Interleaving factor: ${estimate.interleavingFactor}`);
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 : `${tlc.stats?.distinctStates ?? 0} distinct states explored`;
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 = `${r.stateCount} 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
- console.log(color(` States explored: ${result.stats?.statesGenerated || 0}`, COLORS.gray));
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=92CAFBFCF98F237564756E2164756E21
10108
+ //# debugId=3AB21B8DB351312E64756E2164756E21