libpetri 5.0.0 → 6.0.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/{chunk-4HMFNYTH.js → chunk-5LE2M5PW.js} +2 -2
- package/dist/{chunk-SXK2Z45Z.js → chunk-H2KAMPGN.js} +1 -1
- package/dist/chunk-H2KAMPGN.js.map +1 -0
- package/dist/chunk-MQZ6IM63.js +8383 -0
- package/dist/chunk-MQZ6IM63.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/debug/index.js +1 -1
- package/dist/doclet/index.d.ts +1 -1
- package/dist/doclet/resources/petrinet-diagrams.js +4892 -4896
- package/dist/{event-store-hlH3-bzJ.d.ts → event-store-D6i4u41W.d.ts} +7 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +62 -10
- package/dist/index.js +359 -2156
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-CH7UvjOW.d.ts → petri-net-34SkD5RT.d.ts} +85 -3
- package/dist/render-dom/index.js +1 -1
- package/dist/verification/index.d.ts +843 -16
- package/dist/verification/index.js +755 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.js +1 -1
- package/dist/viewer/viewer.iife.js +4892 -4896
- package/package.json +5 -5
- package/dist/chunk-75KEJQGC.js +0 -4505
- package/dist/chunk-75KEJQGC.js.map +0 -1
- package/dist/chunk-SXK2Z45Z.js.map +0 -1
- /package/dist/{chunk-4HMFNYTH.js.map → chunk-5LE2M5PW.js.map} +0 -0
|
@@ -5,9 +5,12 @@ import {
|
|
|
5
5
|
MIN_Z3_VERSION,
|
|
6
6
|
MarkingState,
|
|
7
7
|
MarkingStateBuilder,
|
|
8
|
+
NOTE_ENUMERATED,
|
|
9
|
+
PetriNet,
|
|
8
10
|
SmtVerifier,
|
|
9
11
|
StateClass,
|
|
10
12
|
StateClassGraph,
|
|
13
|
+
Transition,
|
|
11
14
|
Z3ProcessError,
|
|
12
15
|
Z3Unavailable,
|
|
13
16
|
Z3_ENV,
|
|
@@ -16,14 +19,26 @@ import {
|
|
|
16
19
|
branchPlaceBound,
|
|
17
20
|
canonicalInvariantOrder,
|
|
18
21
|
checkCertificate,
|
|
22
|
+
checkLinearBoundExact,
|
|
23
|
+
compareCodePoints,
|
|
19
24
|
computePInvariants,
|
|
20
25
|
computePSemiflows,
|
|
26
|
+
countAcross,
|
|
27
|
+
countPhrase,
|
|
28
|
+
countViolation,
|
|
21
29
|
deadlockFree,
|
|
30
|
+
decideOverClasses,
|
|
22
31
|
decode,
|
|
32
|
+
decodeLinearBound,
|
|
23
33
|
decodeStateSet,
|
|
34
|
+
describeSinks,
|
|
24
35
|
encode,
|
|
36
|
+
encodeLinearBound,
|
|
37
|
+
encodeNet,
|
|
25
38
|
encodeStepRelationSmt2,
|
|
26
39
|
enumerateBranches,
|
|
40
|
+
failureReason,
|
|
41
|
+
findFiringBound,
|
|
27
42
|
findMaximalTrapIn,
|
|
28
43
|
findMinimalSiphons,
|
|
29
44
|
flatNetIndexOf,
|
|
@@ -31,31 +46,50 @@ import {
|
|
|
31
46
|
flatNetTransitionCount,
|
|
32
47
|
flatTransition,
|
|
33
48
|
flatten,
|
|
49
|
+
fork,
|
|
50
|
+
formatLinearBound,
|
|
51
|
+
formatLinearDemand,
|
|
52
|
+
formatRanking,
|
|
34
53
|
formatZ3Version,
|
|
35
54
|
ignore,
|
|
36
55
|
isCoveredByInvariants,
|
|
56
|
+
isPassthrough,
|
|
37
57
|
isProven,
|
|
58
|
+
isUntimed,
|
|
38
59
|
isViolated,
|
|
39
60
|
joinedOrDeadLettered,
|
|
40
61
|
mutualExclusion,
|
|
62
|
+
one,
|
|
63
|
+
outPlace,
|
|
41
64
|
pInvariant,
|
|
42
65
|
pInvariantToString,
|
|
43
66
|
parseZ3Version,
|
|
67
|
+
place,
|
|
44
68
|
placeBound,
|
|
45
69
|
placeholderCertificate,
|
|
46
70
|
propertyDescription,
|
|
71
|
+
quiescentCount,
|
|
47
72
|
replayCounterexample,
|
|
48
73
|
resolveZ3,
|
|
74
|
+
rethrowIfProgrammingError,
|
|
49
75
|
runZ3Spacer,
|
|
50
76
|
runZ3Text,
|
|
77
|
+
strandedPlaces,
|
|
78
|
+
strandingExcuses,
|
|
79
|
+
strandsToken,
|
|
51
80
|
strengthenWithSemiflows,
|
|
52
81
|
structuralCheck,
|
|
53
82
|
terminatesAtSink,
|
|
83
|
+
timeoutBudget,
|
|
84
|
+
tokensAcross,
|
|
85
|
+
transform,
|
|
54
86
|
unreachable,
|
|
55
87
|
vcScript,
|
|
88
|
+
verifyViaStateClassGraph,
|
|
89
|
+
violationDemand,
|
|
56
90
|
z3Available,
|
|
57
91
|
z3SolverAt
|
|
58
|
-
} from "../chunk-
|
|
92
|
+
} from "../chunk-MQZ6IM63.js";
|
|
59
93
|
import "../chunk-ATT7U5H5.js";
|
|
60
94
|
|
|
61
95
|
// src/verification/analysis/scc-analyzer.ts
|
|
@@ -405,6 +439,705 @@ var TimePetriNetAnalyzerBuilder = class {
|
|
|
405
439
|
);
|
|
406
440
|
}
|
|
407
441
|
};
|
|
442
|
+
|
|
443
|
+
// src/verification/open-net/contract.ts
|
|
444
|
+
var CONTRACT_KEY = /* @__PURE__ */ Symbol("OpenNetContract.internal");
|
|
445
|
+
var OpenNetContract = class {
|
|
446
|
+
/** Tokens the subnet holds before anything arrives: its own resources and any shared pool it borrows from. */
|
|
447
|
+
initialMarking;
|
|
448
|
+
arrivals;
|
|
449
|
+
clauses;
|
|
450
|
+
/** Places that may hold any number of tokens at quiescence. */
|
|
451
|
+
rest;
|
|
452
|
+
terminals;
|
|
453
|
+
/** Transitions the environment fires: neighbours that react to what the subnet sends. */
|
|
454
|
+
environment;
|
|
455
|
+
/** Whether every run must come to rest. */
|
|
456
|
+
requiresTermination;
|
|
457
|
+
/** @internal Use {@link OpenNetContract.builder}. */
|
|
458
|
+
constructor(key, initialMarking, arrivals, clauses, rest, terminals, environment, requiresTermination) {
|
|
459
|
+
if (key !== CONTRACT_KEY) throw new Error("Use OpenNetContract.builder() to create instances");
|
|
460
|
+
this.initialMarking = initialMarking;
|
|
461
|
+
this.arrivals = arrivals;
|
|
462
|
+
this.clauses = clauses;
|
|
463
|
+
this.rest = rest;
|
|
464
|
+
this.terminals = terminals;
|
|
465
|
+
this.environment = environment;
|
|
466
|
+
this.requiresTermination = requiresTermination;
|
|
467
|
+
}
|
|
468
|
+
static builder() {
|
|
469
|
+
return new OpenNetContractBuilder();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Every place the initial marking, an arrival group, a clause, the rest set or a terminal
|
|
473
|
+
* marker names, then every place an environment transition touches, in first-mention
|
|
474
|
+
* order: the places a port trace reports. A terminal's excused places are not included;
|
|
475
|
+
* `closeOpenNet` adds them to the closed net itself.
|
|
476
|
+
*/
|
|
477
|
+
places() {
|
|
478
|
+
const seen = /* @__PURE__ */ new Map();
|
|
479
|
+
const add = (p) => {
|
|
480
|
+
if (!seen.has(p.name)) seen.set(p.name, p);
|
|
481
|
+
};
|
|
482
|
+
for (const p of this.initialMarking.placesWithTokens()) add(p);
|
|
483
|
+
for (const g of this.arrivals) g.places.forEach(add);
|
|
484
|
+
for (const c of this.clauses) c.places.forEach(add);
|
|
485
|
+
this.rest.forEach(add);
|
|
486
|
+
for (const t of this.terminals) add(t.marker);
|
|
487
|
+
for (const t of this.environment) transitionPlaces(t).forEach(add);
|
|
488
|
+
return [...seen.values()];
|
|
489
|
+
}
|
|
490
|
+
/** The contract as the report prints it, one line per part. */
|
|
491
|
+
describe() {
|
|
492
|
+
const names = (ps) => ps.map((p) => p.name).join(", ");
|
|
493
|
+
const lines = [` Initial marking: ${this.initialMarking.toString()}`];
|
|
494
|
+
lines.push(this.arrivals.length === 0 ? " Arrivals: none" : ` Arrivals: ${this.arrivals.map((g) => `${countPhrase(g.min, g.max)} onto {${names(g.places)}}`).join("; ")}`);
|
|
495
|
+
lines.push(this.clauses.length === 0 ? " At quiescence: no count clauses" : ` At quiescence: ${this.clauses.map((c) => `${c.name} = ${countPhrase(c.min, c.max)} across {${names(c.places)}}`).join("; ")}`);
|
|
496
|
+
if (this.rest.length > 0) lines.push(` Rest: ${names(this.rest)}`);
|
|
497
|
+
for (const t of this.terminals) {
|
|
498
|
+
lines.push(t.excused.length === 0 ? ` Terminal: when ${t.marker.name}` : ` Terminal: when ${t.marker.name}: ${names(t.excused)}`);
|
|
499
|
+
}
|
|
500
|
+
if (this.environment.length > 0) {
|
|
501
|
+
lines.push(` Environment transitions: ${this.environment.map((t) => t.name).join(", ")}`);
|
|
502
|
+
}
|
|
503
|
+
lines.push(` Termination: ${this.requiresTermination ? "every run comes to rest" : "not required"}`);
|
|
504
|
+
return lines;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function transitionPlaces(t) {
|
|
508
|
+
return [
|
|
509
|
+
...t.inputSpecs.map((s) => s.place),
|
|
510
|
+
...t.reads.map((a) => a.place),
|
|
511
|
+
...t.inhibitors.map((a) => a.place),
|
|
512
|
+
...t.resets.map((a) => a.place),
|
|
513
|
+
...t.outputPlaces()
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
var OpenNetContractBuilder = class {
|
|
517
|
+
_initialMarking = MarkingState.empty();
|
|
518
|
+
_arrivals = [];
|
|
519
|
+
_clauses = [];
|
|
520
|
+
_rest = /* @__PURE__ */ new Map();
|
|
521
|
+
_terminals = [];
|
|
522
|
+
_environment = [];
|
|
523
|
+
_requiresTermination = true;
|
|
524
|
+
initialMarking(arg) {
|
|
525
|
+
if (arg instanceof MarkingState) {
|
|
526
|
+
this._initialMarking = arg;
|
|
527
|
+
} else {
|
|
528
|
+
const builder = MarkingState.builder();
|
|
529
|
+
arg(builder);
|
|
530
|
+
this._initialMarking = builder.build();
|
|
531
|
+
}
|
|
532
|
+
return this;
|
|
533
|
+
}
|
|
534
|
+
/** The environment delivers exactly `count` tokens, each onto one of `places`, at any point of the run. */
|
|
535
|
+
arrive(count, ...places) {
|
|
536
|
+
return this.arriveBetween(count, count, ...places);
|
|
537
|
+
}
|
|
538
|
+
/** The environment delivers at most `max` tokens, possibly none. `arriveAtMost(1, halt)` is "never or once". */
|
|
539
|
+
arriveAtMost(max, ...places) {
|
|
540
|
+
return this.arriveBetween(0, max, ...places);
|
|
541
|
+
}
|
|
542
|
+
/** The environment delivers between `min` and `max` tokens in total, each onto one of `places`, at any point of the run. */
|
|
543
|
+
arriveBetween(min, max, ...places) {
|
|
544
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max < min || max < 1) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`OpenNetContract: an arrival group needs whole bounds with 0 <= min <= max and max >= 1, got ${min}..${max}. A bound is both the runtime cap and the width of the claim, so it is finite.`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
this._arrivals.push({ places: distinct(places, "an arrival group"), min, max });
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
/** At every quiescent marking, exactly `count` tokens across `places`. */
|
|
553
|
+
expect(name, count, ...places) {
|
|
554
|
+
return this.expectBetween(name, count, count, ...places);
|
|
555
|
+
}
|
|
556
|
+
/** At every quiescent marking, between `min` and `max` tokens across `places`; `max` may be `Infinity`. */
|
|
557
|
+
expectBetween(name, min, max, ...places) {
|
|
558
|
+
if (name.length === 0) throw new Error("OpenNetContract: a clause needs a name");
|
|
559
|
+
if (this._clauses.some((c) => c.name === name)) {
|
|
560
|
+
throw new Error(`OpenNetContract: duplicate clause name '${name}'`);
|
|
561
|
+
}
|
|
562
|
+
if (!Number.isInteger(min) || min < 0 || !(max === Infinity || Number.isInteger(max)) || max < min) {
|
|
563
|
+
throw new Error(`OpenNetContract: clause '${name}' needs whole bounds with 0 <= min <= max, got ${min}..${max}`);
|
|
564
|
+
}
|
|
565
|
+
this._clauses.push({ name, places: distinct(places, `clause '${name}'`), min, max });
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
/** Places that may hold any number of tokens at quiescence. */
|
|
569
|
+
rest(...places) {
|
|
570
|
+
for (const p of places) if (!this._rest.has(p.name)) this._rest.set(p.name, p);
|
|
571
|
+
return this;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* A designed terminal: while `marker` holds a token, lower bounds are waived and tokens may
|
|
575
|
+
* rest on `excused`. Repeated calls for one marker accumulate.
|
|
576
|
+
*/
|
|
577
|
+
terminal(marker, ...excused) {
|
|
578
|
+
let entry = this._terminals.find((t) => t.marker.name === marker.name);
|
|
579
|
+
if (entry == null) {
|
|
580
|
+
entry = { marker, excused: /* @__PURE__ */ new Map() };
|
|
581
|
+
this._terminals.push(entry);
|
|
582
|
+
}
|
|
583
|
+
for (const p of excused) if (!entry.excused.has(p.name)) entry.excused.set(p.name, p);
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Transitions the environment fires: a neighbour that reacts to what the subnet sends, such
|
|
588
|
+
* as a tool answering a request. An arrival group cannot say that: its tokens do not wait
|
|
589
|
+
* for a request.
|
|
590
|
+
*
|
|
591
|
+
* They join the closed net unchanged and are marked as environment steps in the port trace.
|
|
592
|
+
* A place only they touch belongs to the environment and may hold tokens at quiescence; a
|
|
593
|
+
* place they share with the subnet is a port, judged like any other. Their actions never
|
|
594
|
+
* run, so one that declares outputs may keep `passthrough()`.
|
|
595
|
+
*/
|
|
596
|
+
environment(...transitions) {
|
|
597
|
+
for (const t of transitions) {
|
|
598
|
+
if (this._environment.some((e) => e.name === t.name)) {
|
|
599
|
+
throw new Error(`OpenNetContract: duplicate environment transition '${t.name}'`);
|
|
600
|
+
}
|
|
601
|
+
this._environment.push(t);
|
|
602
|
+
}
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
/** Whether every run must come to rest (default `true`). */
|
|
606
|
+
requireTermination(required) {
|
|
607
|
+
this._requiresTermination = required;
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
build() {
|
|
611
|
+
return new OpenNetContract(
|
|
612
|
+
CONTRACT_KEY,
|
|
613
|
+
this._initialMarking,
|
|
614
|
+
[...this._arrivals],
|
|
615
|
+
[...this._clauses],
|
|
616
|
+
[...this._rest.values()],
|
|
617
|
+
this._terminals.map((t) => ({ marker: t.marker, excused: [...t.excused.values()] })),
|
|
618
|
+
[...this._environment],
|
|
619
|
+
this._requiresTermination
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
function distinct(places, what) {
|
|
624
|
+
if (places.length === 0) throw new Error(`OpenNetContract: ${what} names no place`);
|
|
625
|
+
const byName = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const p of places) if (!byName.has(p.name)) byName.set(p.name, p);
|
|
627
|
+
return [...byName.values()];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/verification/open-net/closure.ts
|
|
631
|
+
var ENVIRONMENT_ACTION = transform(() => null);
|
|
632
|
+
function closeOpenNet(net, contract) {
|
|
633
|
+
const byName = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const p of net.places) if (!byName.has(p.name)) byName.set(p.name, p);
|
|
635
|
+
const taken = new Set(byName.keys());
|
|
636
|
+
for (const t of net.transitions) taken.add(t.name);
|
|
637
|
+
const fresh = (name) => {
|
|
638
|
+
if (taken.has(name)) {
|
|
639
|
+
throw new Error(`VER-022: closing the net would add '${name}', which the net already declares`);
|
|
640
|
+
}
|
|
641
|
+
taken.add(name);
|
|
642
|
+
return name;
|
|
643
|
+
};
|
|
644
|
+
const environment = /* @__PURE__ */ new Map();
|
|
645
|
+
const environmentPlaces = /* @__PURE__ */ new Map();
|
|
646
|
+
for (const t of contract.environment) {
|
|
647
|
+
environment.set(fresh(t.name), { kind: "transition" });
|
|
648
|
+
for (const p of transitionPlaces(t)) {
|
|
649
|
+
if (!byName.has(p.name) && !environmentPlaces.has(p.name)) environmentPlaces.set(p.name, p);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
for (const [name, p] of environmentPlaces) {
|
|
653
|
+
taken.add(name);
|
|
654
|
+
byName.set(name, p);
|
|
655
|
+
}
|
|
656
|
+
const undeclared = [];
|
|
657
|
+
const extra = [];
|
|
658
|
+
const named = [...contract.places(), ...contract.terminals.flatMap((t) => t.excused)];
|
|
659
|
+
for (const p of named) {
|
|
660
|
+
if (byName.has(p.name)) continue;
|
|
661
|
+
undeclared.push(p.name);
|
|
662
|
+
byName.set(p.name, p);
|
|
663
|
+
extra.push(p);
|
|
664
|
+
}
|
|
665
|
+
const canonical = (p) => byName.get(p.name) ?? p;
|
|
666
|
+
const envTransitions = [];
|
|
667
|
+
const marking = MarkingState.builder().copyFrom(contract.initialMarking);
|
|
668
|
+
const inject = (group, source, target, optional) => {
|
|
669
|
+
const name = fresh(`env:arrive${optional ? "?" : ""}[${group}]:${target.name}`);
|
|
670
|
+
envTransitions.push(
|
|
671
|
+
Transition.builder(name).inputs(one(source)).outputs(outPlace(target)).action(fork()).build()
|
|
672
|
+
);
|
|
673
|
+
environment.set(name, { kind: "arrival", group, place: target.name });
|
|
674
|
+
};
|
|
675
|
+
contract.arrivals.forEach((group, i) => {
|
|
676
|
+
if (group.min > 0) {
|
|
677
|
+
const source = place(fresh(`env:arrivals[${i}]`));
|
|
678
|
+
marking.tokens(source, group.min);
|
|
679
|
+
for (const p of group.places) inject(i, source, canonical(p), false);
|
|
680
|
+
}
|
|
681
|
+
if (group.max > group.min) {
|
|
682
|
+
const source = place(fresh(`env:optional[${i}]`));
|
|
683
|
+
marking.tokens(source, group.max - group.min);
|
|
684
|
+
for (const p of group.places) inject(i, source, canonical(p), true);
|
|
685
|
+
const name = fresh(`env:decline[${i}]`);
|
|
686
|
+
envTransitions.push(Transition.builder(name).inputs(one(source)).build());
|
|
687
|
+
environment.set(name, { kind: "decline", group: i });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
const placeholder = new Set(contract.environment.filter((t) => t.outputSpec !== null && isPassthrough(t.action)).map((t) => t.name));
|
|
691
|
+
const closed = PetriNet.builder(`${net.name}+environment`).places(...net.places, ...extra).transitions(...net.transitions, ...contract.environment, ...envTransitions).build().bindActionsWithResolver((name) => placeholder.has(name) ? ENVIRONMENT_ACTION : null);
|
|
692
|
+
return {
|
|
693
|
+
net: closed,
|
|
694
|
+
initialMarking: marking.build(),
|
|
695
|
+
environment,
|
|
696
|
+
environmentPlaces: [...environmentPlaces.values()],
|
|
697
|
+
undeclared
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/verification/open-net/predicate.ts
|
|
702
|
+
function restDeclarationOf(contract, closed) {
|
|
703
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const c of contract.clauses) for (const p of c.places) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
705
|
+
for (const p of contract.rest) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
706
|
+
for (const p of closed.environmentPlaces) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
707
|
+
return {
|
|
708
|
+
sinks: new Set(sinks.values()),
|
|
709
|
+
conditional: contract.terminals.map((t) => ({ marker: t.marker, places: new Set(t.excused) }))
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function waiverMarkers(contract) {
|
|
713
|
+
return contract.terminals.map((t) => t.marker);
|
|
714
|
+
}
|
|
715
|
+
function strandedNames(m, rest) {
|
|
716
|
+
return strandedPlaces(m, rest.sinks, rest.conditional).sort((a, b) => compareCodePoints(a.name, b.name));
|
|
717
|
+
}
|
|
718
|
+
function quiescenceFindings(m, contract, rest) {
|
|
719
|
+
const findings = [];
|
|
720
|
+
const markers = waiverMarkers(contract);
|
|
721
|
+
for (const clause of contract.clauses) {
|
|
722
|
+
const bound = countViolation(m, clause.places, clause.min, clause.max, markers);
|
|
723
|
+
if (bound !== null) findings.push({ kind: "clause", clause, count: tokensAcross(m, clause.places), bound });
|
|
724
|
+
}
|
|
725
|
+
for (const place2 of strandedNames(m, rest)) {
|
|
726
|
+
findings.push({ kind: "stranded", place: place2.name, count: m.tokens(place2) });
|
|
727
|
+
}
|
|
728
|
+
return findings;
|
|
729
|
+
}
|
|
730
|
+
function subjectOf(f) {
|
|
731
|
+
return f.kind === "clause" ? f.clause.name : f.place;
|
|
732
|
+
}
|
|
733
|
+
function describeFinding(f) {
|
|
734
|
+
if (f.kind === "stranded") {
|
|
735
|
+
return `${f.place} holds ${f.count} at quiescence, and nothing in the contract lets a token rest there`;
|
|
736
|
+
}
|
|
737
|
+
const { clause } = f;
|
|
738
|
+
const expected = countAcross(clause.min, clause.max, clause.places);
|
|
739
|
+
return f.bound === "upper" ? `${expected} at quiescence, found ${f.count} (an upper bound holds under a terminal too)` : `${expected} at quiescence, found ${f.count}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/verification/open-net/result.ts
|
|
743
|
+
function contractViolation(closed, tracedPlaces, violation) {
|
|
744
|
+
const { transitions, markings } = violation;
|
|
745
|
+
const portTrace = [];
|
|
746
|
+
if (markings.length === transitions.length + 1) {
|
|
747
|
+
for (let i = 0; i < transitions.length; i++) {
|
|
748
|
+
const before = markings[i];
|
|
749
|
+
const after = markings[i + 1];
|
|
750
|
+
const changes = [];
|
|
751
|
+
for (const p of tracedPlaces) {
|
|
752
|
+
const delta = after.tokens(p) - before.tokens(p);
|
|
753
|
+
if (delta !== 0) changes.push({ place: p.name, delta });
|
|
754
|
+
}
|
|
755
|
+
const env = closed.environment.get(transitions[i]);
|
|
756
|
+
if (changes.length > 0 || env !== void 0) {
|
|
757
|
+
portTrace.push({ step: i + 1, transition: transitions[i], environment: env?.kind ?? null, changes });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { ...violation, portTrace };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/verification/open-net/graph-route.ts
|
|
765
|
+
function decideOnGraph(closed, contract, maxClasses, tracedPlaces) {
|
|
766
|
+
const graph = StateClassGraph.build(
|
|
767
|
+
closed.net,
|
|
768
|
+
closed.initialMarking,
|
|
769
|
+
maxClasses,
|
|
770
|
+
void 0,
|
|
771
|
+
void 0,
|
|
772
|
+
{ untimed: true }
|
|
773
|
+
);
|
|
774
|
+
const classes = graph.stateClasses();
|
|
775
|
+
const rest = restDeclarationOf(contract, closed);
|
|
776
|
+
const tree = bfsTree(graph);
|
|
777
|
+
const first = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const sc of classes) {
|
|
779
|
+
if (sc.enabledTransitions.length > 0) continue;
|
|
780
|
+
for (const finding of quiescenceFindings(sc.marking, contract, rest)) {
|
|
781
|
+
const key = `${finding.kind}:${subjectOf(finding)}`;
|
|
782
|
+
if (!first.has(key)) first.set(key, { finding, target: sc });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const clauseOrder = new Map(contract.clauses.map((c, i) => [c.name, i]));
|
|
786
|
+
const rank = (f) => f.kind === "clause" ? clauseOrder.get(f.clause.name) : contract.clauses.length;
|
|
787
|
+
const ordered = [...first.values()].sort((a, b) => rank(a.finding) - rank(b.finding) || compareCodePoints(subjectOf(a.finding), subjectOf(b.finding)));
|
|
788
|
+
const violations = ordered.map(({ finding, target }) => {
|
|
789
|
+
const path = pathTo(tree, target);
|
|
790
|
+
return contractViolation(closed, tracedPlaces, {
|
|
791
|
+
kind: finding.kind,
|
|
792
|
+
subject: subjectOf(finding),
|
|
793
|
+
detail: describeFinding(finding),
|
|
794
|
+
transitions: path.transitions,
|
|
795
|
+
markings: path.classes.map((c) => c.marking),
|
|
796
|
+
cycleStart: null,
|
|
797
|
+
confirmed: true
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
if (contract.requiresTermination) {
|
|
801
|
+
const cycle = findCycle(graph, tree);
|
|
802
|
+
if (cycle !== null) violations.push(contractViolation(closed, tracedPlaces, cycle));
|
|
803
|
+
}
|
|
804
|
+
return { complete: graph.isComplete(), classCount: classes.length, violations };
|
|
805
|
+
}
|
|
806
|
+
function bfsTree(graph) {
|
|
807
|
+
const tree = /* @__PURE__ */ new Map();
|
|
808
|
+
const seen = /* @__PURE__ */ new Set([graph.initialClass]);
|
|
809
|
+
const queue = [graph.initialClass];
|
|
810
|
+
for (let head = 0; head < queue.length; head++) {
|
|
811
|
+
const current = queue[head];
|
|
812
|
+
for (const [transition, edges] of graph.outgoingBranchEdges(current)) {
|
|
813
|
+
for (const edge of edges) {
|
|
814
|
+
if (seen.has(edge.target)) continue;
|
|
815
|
+
seen.add(edge.target);
|
|
816
|
+
tree.set(edge.target, { parent: current, via: transition.name });
|
|
817
|
+
queue.push(edge.target);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return tree;
|
|
822
|
+
}
|
|
823
|
+
function pathTo(tree, target) {
|
|
824
|
+
const classes = [];
|
|
825
|
+
const transitions = [];
|
|
826
|
+
for (let cur = target; cur !== void 0; ) {
|
|
827
|
+
classes.push(cur);
|
|
828
|
+
const edge = tree.get(cur);
|
|
829
|
+
if (edge === void 0) break;
|
|
830
|
+
transitions.push(edge.via);
|
|
831
|
+
cur = edge.parent;
|
|
832
|
+
}
|
|
833
|
+
return { classes: classes.reverse(), transitions: transitions.reverse() };
|
|
834
|
+
}
|
|
835
|
+
function findCycle(graph, tree) {
|
|
836
|
+
const edgesOf = (sc) => {
|
|
837
|
+
const out = [];
|
|
838
|
+
for (const [t, edges] of graph.outgoingBranchEdges(sc)) for (const e of edges) out.push([t.name, e.target]);
|
|
839
|
+
return out;
|
|
840
|
+
};
|
|
841
|
+
const position = /* @__PURE__ */ new Map([[graph.initialClass, 0]]);
|
|
842
|
+
const stack = [{ node: graph.initialClass, edges: edgesOf(graph.initialClass), next: 0, via: null }];
|
|
843
|
+
while (stack.length > 0) {
|
|
844
|
+
const top = stack[stack.length - 1];
|
|
845
|
+
if (top.next >= top.edges.length) {
|
|
846
|
+
position.set(top.node, -1);
|
|
847
|
+
stack.pop();
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const [via, target] = top.edges[top.next++];
|
|
851
|
+
const at = position.get(target);
|
|
852
|
+
if (at === void 0) {
|
|
853
|
+
position.set(target, stack.length);
|
|
854
|
+
stack.push({ node: target, edges: edgesOf(target), next: 0, via });
|
|
855
|
+
} else if (at >= 0) {
|
|
856
|
+
const loop = stack.slice(at);
|
|
857
|
+
const stem = pathTo(tree, target);
|
|
858
|
+
const cycle = [...loop.slice(1).map((f) => f.via), via];
|
|
859
|
+
return {
|
|
860
|
+
kind: "termination",
|
|
861
|
+
subject: "termination",
|
|
862
|
+
detail: `a run can repeat ${cycle.join(" \u2192 ")} forever without coming to rest`,
|
|
863
|
+
transitions: [...stem.transitions, ...cycle],
|
|
864
|
+
markings: [...stem.classes.map((c) => c.marking), ...loop.slice(1).map((f) => f.node.marking), target.marking],
|
|
865
|
+
cycleStart: stem.transitions.length,
|
|
866
|
+
confirmed: true
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/verification/open-net/report.ts
|
|
874
|
+
function renderReport(input) {
|
|
875
|
+
const { net, closed, contract, graph, smtLines, verdict, violations } = input;
|
|
876
|
+
const lines = ["=== OPEN-NET CONTRACT VERIFICATION (VER-022) ===", ""];
|
|
877
|
+
lines.push(`Net: ${net.name}, closed by ${closed.environment.size} environment transitions over ${contract.arrivals.length} arrival groups`);
|
|
878
|
+
lines.push("Contract:", ...contract.describe());
|
|
879
|
+
if (closed.undeclared.length > 0) {
|
|
880
|
+
lines.push(`Not declared by the net: ${closed.undeclared.join(", ")} (no arc touches them; a clause there counts zero)`);
|
|
881
|
+
}
|
|
882
|
+
lines.push("");
|
|
883
|
+
if (graph === null) {
|
|
884
|
+
lines.push(`State-class graph: skipped (${input.graphSkipped ?? "class budget 0"})`);
|
|
885
|
+
} else {
|
|
886
|
+
lines.push("=== State-class graph (untimed, priority-blind) ===");
|
|
887
|
+
lines.push(graph.complete ? ` Classes: ${graph.classCount}, closed` : ` Classes: ${graph.classCount}, truncated at the class budget of ${input.maxClasses}`);
|
|
888
|
+
}
|
|
889
|
+
if (smtLines !== null) {
|
|
890
|
+
lines.push("", "=== SMT route ===", ...smtLines);
|
|
891
|
+
}
|
|
892
|
+
lines.push("", "=== RESULT ===");
|
|
893
|
+
switch (verdict.type) {
|
|
894
|
+
case "proven":
|
|
895
|
+
lines.push(`PROVEN: every quiescent marking meets the contract${contract.requiresTermination ? " and every run comes to rest" : ""} (${verdict.method})`);
|
|
896
|
+
break;
|
|
897
|
+
case "unknown":
|
|
898
|
+
lines.push(`UNKNOWN: ${verdict.reason}`);
|
|
899
|
+
break;
|
|
900
|
+
case "violated":
|
|
901
|
+
lines.push(`VIOLATED: ${violations.length} ${violations.length === 1 ? "part" : "parts"} of the contract broken`);
|
|
902
|
+
for (const v of violations) lines.push(...renderViolation(v));
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
return lines.join("\n");
|
|
906
|
+
}
|
|
907
|
+
function renderViolation(v) {
|
|
908
|
+
const lines = [` [${v.subject}] ${v.kind}: ${v.detail}`];
|
|
909
|
+
if (!v.confirmed) lines.push(" (the solver's counterexample did not replay as an ordered firing sequence)");
|
|
910
|
+
if (v.portTrace.length > 0) {
|
|
911
|
+
lines.push(" Port trace:");
|
|
912
|
+
for (const s of v.portTrace) {
|
|
913
|
+
if (v.cycleStart !== null && s.step === v.cycleStart + 1) lines.push(" -- the cycle starts here --");
|
|
914
|
+
const env = s.environment === null ? "" : s.environment === "transition" ? " [environment]" : ` [environment ${s.environment}]`;
|
|
915
|
+
const changes = s.changes.map((c) => `${c.place} ${c.delta > 0 ? "+" : ""}${c.delta}`).join(", ");
|
|
916
|
+
lines.push(` ${s.step}. ${s.transition}${env}${changes.length > 0 ? ` ${changes}` : ""}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (v.cycleStart !== null) {
|
|
920
|
+
const stem = v.transitions.slice(0, v.cycleStart);
|
|
921
|
+
const cycle = `then repeating ${v.transitions.slice(v.cycleStart).join(", ")}`;
|
|
922
|
+
lines.push(` Firing sequence: ${stem.length === 0 ? cycle : `${stem.join(", ")}, ${cycle}`}`);
|
|
923
|
+
} else if (v.transitions.length > 0) {
|
|
924
|
+
lines.push(` Firing sequence: ${v.transitions.join(", ")}`);
|
|
925
|
+
}
|
|
926
|
+
const last = v.markings.at(-1);
|
|
927
|
+
if (last !== void 0) lines.push(` ${v.kind === "termination" ? "Marking on the cycle" : "Quiescent marking"}: ${last.toString()}`);
|
|
928
|
+
return lines;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/verification/open-net/smt-route.ts
|
|
932
|
+
async function decideViaSmt(closed, contract, tracedPlaces, configure, terminationTimeoutMs) {
|
|
933
|
+
const violations = [];
|
|
934
|
+
const undecided = [];
|
|
935
|
+
const lines = [];
|
|
936
|
+
const certificates = [];
|
|
937
|
+
const net = untimed(closed.net);
|
|
938
|
+
for (const part of partsFor(closed, contract)) {
|
|
939
|
+
if (part.kind === "vacuous") {
|
|
940
|
+
lines.push(` [${part.subject}] ${part.detail}: proven (no query needed)`);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const q = part.query;
|
|
944
|
+
let verifier = SmtVerifier.forNet(net).initialMarking(closed.initialMarking).property(q.property).sinkPlaces(...q.sinks).enumerationMaxClasses(0);
|
|
945
|
+
for (const c of q.conditional) verifier = verifier.sinkPlacesWhen(c.marker, ...c.places);
|
|
946
|
+
const result = await configure(verifier).verify();
|
|
947
|
+
const verdict = result.verdict;
|
|
948
|
+
lines.push(` [${q.subject}] ${propertyDescription(q.property)}: ${verdict.type}` + (verdict.type === "unknown" ? ` (${verdict.reason})` : ""));
|
|
949
|
+
if (verdict.type === "unknown") undecided.push(`${q.subject}: ${verdict.reason}`);
|
|
950
|
+
else if (verdict.type === "violated") violations.push(contractViolation(closed, tracedPlaces, q.onViolated(result)));
|
|
951
|
+
else if (verdict.inductiveInvariant !== null) {
|
|
952
|
+
certificates.push({ subject: q.subject, invariant: verdict.inductiveInvariant });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (contract.requiresTermination) {
|
|
956
|
+
const termination = await terminationByRanking(closed, terminationTimeoutMs);
|
|
957
|
+
if (termination.proven) {
|
|
958
|
+
lines.push(` [termination] Firing bound (VER-019): ${termination.detail}`);
|
|
959
|
+
} else {
|
|
960
|
+
lines.push(` [termination] Firing bound (VER-019): undecided (${termination.reason})`);
|
|
961
|
+
undecided.push(`termination: ${termination.reason}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return { violations, undecided, lines, certificates };
|
|
965
|
+
}
|
|
966
|
+
function untimed(net) {
|
|
967
|
+
if (isUntimed(net)) return net;
|
|
968
|
+
const transitions = [...net.transitions].map((t) => {
|
|
969
|
+
if (t.timing.type === "immediate") return t;
|
|
970
|
+
const b = Transition.builder(t.name).inputs(...t.inputSpecs).priority(t.priority).action(t.action);
|
|
971
|
+
if (t.outputSpec !== null) b.outputs(t.outputSpec);
|
|
972
|
+
for (const arc of t.inhibitors) b.inhibitor(arc.place);
|
|
973
|
+
for (const arc of t.reads) b.read(arc.place);
|
|
974
|
+
for (const arc of t.resets) b.reset(arc.place);
|
|
975
|
+
if (t.matchSpec !== null) b.match(t.matchSpec);
|
|
976
|
+
return b.build();
|
|
977
|
+
});
|
|
978
|
+
return PetriNet.builder(net.name).places(...net.places).transitions(...transitions).build();
|
|
979
|
+
}
|
|
980
|
+
function partsFor(closed, contract) {
|
|
981
|
+
const rest = restDeclarationOf(contract, closed);
|
|
982
|
+
const markers = waiverMarkers(contract);
|
|
983
|
+
const quiescentMarking = (result) => result.counterexampleConfirmed === true ? result.counterexampleTrace.at(-1) : void 0;
|
|
984
|
+
const witness = (result) => ({
|
|
985
|
+
transitions: [...result.counterexampleTransitions],
|
|
986
|
+
markings: [...result.counterexampleTrace],
|
|
987
|
+
cycleStart: null,
|
|
988
|
+
confirmed: result.counterexampleConfirmed === true
|
|
989
|
+
});
|
|
990
|
+
const stranding = {
|
|
991
|
+
subject: "stranding",
|
|
992
|
+
property: deadlockFree(),
|
|
993
|
+
sinks: [...rest.sinks],
|
|
994
|
+
conditional: rest.conditional,
|
|
995
|
+
onViolated: (result) => {
|
|
996
|
+
const last = quiescentMarking(result);
|
|
997
|
+
const stranded = last === void 0 ? [] : strandedNames(last, rest).map((p) => p.name);
|
|
998
|
+
return {
|
|
999
|
+
kind: "stranded",
|
|
1000
|
+
subject: stranded.length === 0 ? "stranding" : stranded.join(", "),
|
|
1001
|
+
detail: stranded.length === 0 ? "the solver found a reachable quiescent marking that leaves a token where the contract lets none rest" : `${stranded.join(", ")} ${stranded.length === 1 ? "holds" : "hold"} a token at quiescence, and nothing in the contract lets one rest there`,
|
|
1002
|
+
...witness(result)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const parts = [{ kind: "query", query: stranding }];
|
|
1007
|
+
for (const clause of contract.clauses) {
|
|
1008
|
+
const across = countAcross(clause.min, clause.max, clause.places);
|
|
1009
|
+
if (clause.min === 0 && clause.max === Infinity) {
|
|
1010
|
+
parts.push({ kind: "vacuous", subject: clause.name, detail: `${across} at quiescence` });
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const query = {
|
|
1014
|
+
subject: clause.name,
|
|
1015
|
+
property: quiescentCount(clause.places, clause.min, clause.max, markers),
|
|
1016
|
+
sinks: [],
|
|
1017
|
+
conditional: [],
|
|
1018
|
+
onViolated: (result) => {
|
|
1019
|
+
const last = quiescentMarking(result);
|
|
1020
|
+
return {
|
|
1021
|
+
kind: "clause",
|
|
1022
|
+
subject: clause.name,
|
|
1023
|
+
detail: last === void 0 ? `${across} at quiescence: the solver found a quiescent marking outside it` : `${across} at quiescence, found ${tokensAcross(last, clause.places)}`,
|
|
1024
|
+
...witness(result)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
parts.push({ kind: "query", query });
|
|
1029
|
+
}
|
|
1030
|
+
return parts;
|
|
1031
|
+
}
|
|
1032
|
+
async function terminationByRanking(closed, timeoutMs) {
|
|
1033
|
+
const flat = flatten(closed.net);
|
|
1034
|
+
const initial = flat.places.map((p) => closed.initialMarking.tokens(p));
|
|
1035
|
+
let solver;
|
|
1036
|
+
try {
|
|
1037
|
+
solver = resolveZ3();
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
if (e instanceof Z3Unavailable) return { proven: false, reason: e.message };
|
|
1040
|
+
throw e;
|
|
1041
|
+
}
|
|
1042
|
+
const ask = async (script) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const reply = await runZ3Text(solver, script, "ranking", timeoutMs, []);
|
|
1045
|
+
const answer = firstLine(reply.stdout);
|
|
1046
|
+
if (answer === "sat" || answer === "unsat" || answer === "unknown") return reply.stdout;
|
|
1047
|
+
return new Error(failureReason(reply, timeoutBudget(timeoutMs)));
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
rethrowIfProgrammingError(e);
|
|
1050
|
+
return new Error(String(e?.message ?? e));
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
const ranking = await findFiringBound(flat, initial, ask);
|
|
1054
|
+
switch (ranking.kind) {
|
|
1055
|
+
case "bound":
|
|
1056
|
+
return {
|
|
1057
|
+
proven: true,
|
|
1058
|
+
detail: `every run has at most ${ranking.bound.bound} firings (${formatRanking(flat, ranking.bound)} drops on every firing)`
|
|
1059
|
+
};
|
|
1060
|
+
case "unbounded":
|
|
1061
|
+
return {
|
|
1062
|
+
proven: false,
|
|
1063
|
+
reason: ranking.repeatable === null ? "no firing bound: no weights drop on every firing" : `no firing bound: the marking equation lets ${ranking.repeatable.map((t) => flat.transitions[t].name).join(", ")} repeat`
|
|
1064
|
+
};
|
|
1065
|
+
case "rejected":
|
|
1066
|
+
return { proven: false, reason: "the firing-bound ranking failed the exact re-check" };
|
|
1067
|
+
case "unknown":
|
|
1068
|
+
return { proven: false, reason: "the firing-bound query answered unknown" };
|
|
1069
|
+
case "failed":
|
|
1070
|
+
return { proven: false, reason: ranking.reason };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function firstLine(stdout) {
|
|
1074
|
+
return stdout.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/verification/open-net/verify-open-net.ts
|
|
1078
|
+
var DEFAULT_MAX_CLASSES = 5e4;
|
|
1079
|
+
var METHOD_ENUMERATION = "open-net contract by state-space enumeration (VER-022)";
|
|
1080
|
+
var METHOD_SMT = "open-net contract by the SMT pipeline (VER-022)";
|
|
1081
|
+
var SKIPPED_BY_BUDGET = "class budget 0";
|
|
1082
|
+
var SKIPPED_BY_MATCH = "the closed net declares match (\u03BD-join) transitions, which the graph does not model";
|
|
1083
|
+
async function verifyOpenNet(net, contract, options = {}) {
|
|
1084
|
+
const start = performance.now();
|
|
1085
|
+
const closed = closeOpenNet(net, contract);
|
|
1086
|
+
const maxClasses = options.maxClasses ?? DEFAULT_MAX_CLASSES;
|
|
1087
|
+
const useSmt = options.smt ?? true;
|
|
1088
|
+
const tracedPlaces = contract.places();
|
|
1089
|
+
const graphSkipped = [...closed.net.transitions].some((t) => t.matchSpec !== null) ? SKIPPED_BY_MATCH : maxClasses > 0 ? null : SKIPPED_BY_BUDGET;
|
|
1090
|
+
const graph = graphSkipped === null ? decideOnGraph(closed, contract, maxClasses, tracedPlaces) : null;
|
|
1091
|
+
const result = (verdict, route, violations, smtLines) => ({
|
|
1092
|
+
verdict,
|
|
1093
|
+
violations,
|
|
1094
|
+
route,
|
|
1095
|
+
classCount: graph?.classCount ?? 0,
|
|
1096
|
+
graphComplete: graph?.complete ?? false,
|
|
1097
|
+
report: renderReport({ net, closed, contract, maxClasses, graph, graphSkipped, smtLines, verdict, violations }),
|
|
1098
|
+
closedNet: closed.net,
|
|
1099
|
+
closedMarking: closed.initialMarking,
|
|
1100
|
+
elapsedMs: performance.now() - start
|
|
1101
|
+
});
|
|
1102
|
+
if (graph !== null) {
|
|
1103
|
+
if (graph.violations.length > 0) {
|
|
1104
|
+
return result({ type: "violated" }, "enumeration", graph.violations, null);
|
|
1105
|
+
}
|
|
1106
|
+
if (graph.complete) {
|
|
1107
|
+
return result({ type: "proven", method: METHOD_ENUMERATION, inductiveInvariant: null }, "enumeration", [], null);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const why = graph !== null ? `the state-class graph did not close within ${maxClasses} classes` : graphSkipped === SKIPPED_BY_BUDGET ? "the state-class graph was skipped" : `the state-class graph was skipped: ${graphSkipped}`;
|
|
1111
|
+
if (!useSmt) {
|
|
1112
|
+
return result({ type: "unknown", reason: `${why}, and the SMT route is disabled` }, "enumeration", [], null);
|
|
1113
|
+
}
|
|
1114
|
+
const smt = await decideViaSmt(
|
|
1115
|
+
closed,
|
|
1116
|
+
contract,
|
|
1117
|
+
tracedPlaces,
|
|
1118
|
+
options.configureSmt ?? ((v) => v),
|
|
1119
|
+
options.terminationTimeoutMs ?? 6e4
|
|
1120
|
+
);
|
|
1121
|
+
if (smt.violations.length > 0) return result({ type: "violated" }, "smt", smt.violations, smt.lines);
|
|
1122
|
+
if (smt.undecided.length === 0) {
|
|
1123
|
+
return result(
|
|
1124
|
+
{ type: "proven", method: METHOD_SMT, inductiveInvariant: combineCertificates(smt.certificates) },
|
|
1125
|
+
"smt",
|
|
1126
|
+
[],
|
|
1127
|
+
smt.lines
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return result(
|
|
1131
|
+
{ type: "unknown", reason: `${why}; left undecided by the SMT route: ${smt.undecided.join("; ")}` },
|
|
1132
|
+
"smt",
|
|
1133
|
+
[],
|
|
1134
|
+
smt.lines
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function combineCertificates(certificates) {
|
|
1138
|
+
if (certificates.length === 0) return null;
|
|
1139
|
+
return certificates.map((c) => `[${c.subject}] ${c.invariant}`).join("\n");
|
|
1140
|
+
}
|
|
408
1141
|
export {
|
|
409
1142
|
DBM,
|
|
410
1143
|
DUMP_ENV,
|
|
@@ -412,6 +1145,9 @@ export {
|
|
|
412
1145
|
MIN_Z3_VERSION,
|
|
413
1146
|
MarkingState,
|
|
414
1147
|
MarkingStateBuilder,
|
|
1148
|
+
NOTE_ENUMERATED,
|
|
1149
|
+
OpenNetContract,
|
|
1150
|
+
OpenNetContractBuilder,
|
|
415
1151
|
SmtVerifier,
|
|
416
1152
|
StateClass,
|
|
417
1153
|
StateClassGraph,
|
|
@@ -428,13 +1164,20 @@ export {
|
|
|
428
1164
|
branchPlaceBound,
|
|
429
1165
|
canonicalInvariantOrder,
|
|
430
1166
|
checkCertificate,
|
|
1167
|
+
checkLinearBoundExact,
|
|
1168
|
+
closeOpenNet,
|
|
431
1169
|
computePInvariants,
|
|
432
1170
|
computePSemiflows,
|
|
433
1171
|
computeSCCs,
|
|
434
1172
|
deadlockFree,
|
|
1173
|
+
decideOverClasses,
|
|
435
1174
|
decode,
|
|
1175
|
+
decodeLinearBound,
|
|
436
1176
|
decodeStateSet,
|
|
1177
|
+
describeSinks,
|
|
437
1178
|
encode,
|
|
1179
|
+
encodeLinearBound,
|
|
1180
|
+
encodeNet,
|
|
438
1181
|
encodeStepRelationSmt2,
|
|
439
1182
|
findMaximalTrapIn,
|
|
440
1183
|
findMinimalSiphons,
|
|
@@ -444,10 +1187,13 @@ export {
|
|
|
444
1187
|
flatNetTransitionCount,
|
|
445
1188
|
flatTransition,
|
|
446
1189
|
flatten,
|
|
1190
|
+
formatLinearBound,
|
|
1191
|
+
formatLinearDemand,
|
|
447
1192
|
formatZ3Version,
|
|
448
1193
|
ignore,
|
|
449
1194
|
isCoveredByInvariants,
|
|
450
1195
|
isProven,
|
|
1196
|
+
isUntimed,
|
|
451
1197
|
isViolated,
|
|
452
1198
|
joinedOrDeadLettered,
|
|
453
1199
|
mutualExclusion,
|
|
@@ -457,15 +1203,23 @@ export {
|
|
|
457
1203
|
placeBound,
|
|
458
1204
|
placeholderCertificate,
|
|
459
1205
|
propertyDescription,
|
|
1206
|
+
quiescentCount,
|
|
460
1207
|
replayCounterexample,
|
|
461
1208
|
resolveZ3,
|
|
1209
|
+
rethrowIfProgrammingError,
|
|
462
1210
|
runZ3Spacer,
|
|
463
1211
|
runZ3Text,
|
|
1212
|
+
strandedPlaces,
|
|
1213
|
+
strandingExcuses,
|
|
1214
|
+
strandsToken,
|
|
464
1215
|
strengthenWithSemiflows,
|
|
465
1216
|
structuralCheck,
|
|
466
1217
|
terminatesAtSink,
|
|
467
1218
|
unreachable,
|
|
468
1219
|
vcScript,
|
|
1220
|
+
verifyOpenNet,
|
|
1221
|
+
verifyViaStateClassGraph,
|
|
1222
|
+
violationDemand,
|
|
469
1223
|
z3Available,
|
|
470
1224
|
z3SolverAt
|
|
471
1225
|
};
|